You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Spring AI+Ollama Llama3.2的MCP客户端调用添加员工工具时出现JSON解析错误的排查请求

使用Spring AI+Ollama Llama3.2的MCP客户端调用添加员工工具时出现JSON解析错误的排查请求

大家好,我最近在基于Spring AI和Ollama Llama3.2(部署在Docker容器中,暴露端口http://localhost:11434)探索MCP(Model Context Protocol),采用STDIO传输方式,大部分工具调用操作都能正常执行,但唯独调用添加员工的工具时遇到了问题,推测是JSON解析相关的错误,想请社区的朋友们帮忙排查下。

环境与整体流程说明

  • MCP服务端维护了一个内存中的员工列表,提供了增删改查的工具方法,通过@Tool注解暴露给模型
  • MCP客户端基于spring-boot-web-starter实现,通过POST接口接收用户输入,再传递给模型触发工具调用
  • 我用curl命令发送请求体来测试客户端接口

关键代码片段

服务端EmployeeService

import com.demo.mcp.server.data.Employee;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;

@Service
public class EmployeeService {
    private static final Logger logger = LoggerFactory.getLogger(EmployeeService.class);
    private List<Employee> employees = new ArrayList<>();

    @Tool(name="t_list_all_employees", description = "This tool will use the list of employees that is managed in the in memory list within the application")
    public List<Employee> getEmployees() {
        logger.info("getEmployees() invoked");
        return employees;
    }

    @Tool(name="t_add_one_employee", description = "Add an employee to the employee list, before invoking this tool create the employee string with json template in following format" +
            "{ \"name\": \"SET-WITH-USER-INPUT\", \"email\": \"SET-WITH-USER-INPUT\", \"phone\": \"SET-WITH-USER-INPUT\"}" +
            " with this this method can add the employee to in memory list. For example, if the context include " +
            "add an employee to the list where the employee name is joe I, email is joe@email.com and phone is 11231" +
            "The employee name should be provided")
    public Employee addEmployee(Employee employee) {
        logger.info("Adding employee: {}", employee);
        if(employee != null && employee.name() != null && !employee.name().isEmpty()) {
            logger.info("Employee name is valid: {}", employee.name());
        } else {
            logger.warn("Invalid employee name provided: {}", employee);
            throw new IllegalArgumentException("Employee name cannot be null or empty");
        }
        employees.add(employee);
        return employee;
    }

    @Tool(name="t_fetch_employee_with_name", description = "This tool is used to get one employee based on user provided employee name from the in memory list in this app")
    public Employee getEmployee(String name) {
        logger.info("Getting employee by name: {}", name);
        if (name == null || name.isEmpty()) {
            logger.warn("Invalid employee name provided: {}", name);
            throw new IllegalArgumentException("Employee name cannot be null or empty");
        }
        return employees.stream()
                .filter(employee -> employee.name().equalsIgnoreCase(name))
                .findFirst()
                .orElse(null);
    }

    @Tool(name="t_delete_employee", description = "This tool will delete the employee matching the user provided name from the in memory list in this app")
    public void deleteEmployee(String name) {
        if (name == null || name.isEmpty()) {
            logger.warn("Invalid employee name provided for deletion: {}", name);
            throw new IllegalArgumentException("Employee name cannot be null or empty");
        }
        logger.info("Deleting employee by name: {}", name);
        employees.removeIf(employee -> employee.name().equalsIgnoreCase(name));
    }

    @PostConstruct
    public void init() {
        employees.addAll(List.of(
                new Employee("Joy one","joy_one@email.com","1234567890"),
                new Employee("Joy two","joe_two@email.com","0987654321"),
                new Employee("Joy three","joy_three@email.com","1122334455"),
                new Employee("Joy four","joy_four@email.com","5566778899"),
                new Employee("Joy five","joy_five@email.com","9988776655")
        ));
    }
}

Employee 实体类

public record Employee(String name, String email, String phone){}

客户端InputController(部分代码)

import io.modelcontextprotocol.client.McpSyncClient;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RequestMapping("/input")
@RestController
public class InputController {
    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(InputController.class);
    
    // 省略ChatClient、McpSyncClient等注入及初始化代码
    // ...

    @PostMapping
    public String handleInput(@RequestBody String userInput) {
        // 省略构建Prompt、调用模型及处理工具回调的逻辑
        // ...
    }
}

问题现象

当我通过curl发送类似"add an employee named Alice with email alice@test.com and phone 123456"的请求时,模型确实会触发t_add_one_employee工具调用,但服务端会抛出JSON解析相关的异常(或者参数绑定失败),其他工具比如查询所有员工、按名称查询都能正常工作。

我已经排查的点

  1. 确认了Employee record的字段名称和JSON模板中的字段完全一致
  2. 检查了服务端日志,能看到工具被调用,但参数解析失败
  3. 尝试手动构造符合模板的JSON请求,直接调用服务端接口是正常的

想请教大家的问题

  • 有没有可能是@Tool注解中的描述不够明确,导致模型生成的JSON格式不符合要求?比如有没有优化描述的建议?
  • Spring AI在处理MCP工具调用的参数解析时,有没有需要注意的配置点?
  • 有没有类似场景的调试经验,比如如何查看模型实际生成的工具调用参数?

感谢大家的帮助!

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 10:23:10