深入理解 LLM Tool Calling:从 REST 到 Spring AI 完整实战

0 阅读2分钟

一、Tool Calling 是什么?

LLM 擅长对话,但它对你的数据一无所知——不知道你的库存、不知道你的 API、不知道你的系统如何运作。

Tool Calling 让 LLM 能够"调用"你的函数

"我无法直接回答这个问题,但我想调用这个函数,参数是这些。"

你执行函数,把结果返回给模型,模型将其整合到最终回答中。


二、REST API 实现 Tool Calling(5步完整流程)

场景:用户问"AirPods Pro 有货吗?"

Step 1:发送 Prompt + 工具定义

POST /v1/chat/completions

{
  "model": "gpt-4o",
  "messages": [
    { "role": "user", "content": "AirPods Pro 有货吗?" }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "findProductByName",
        "description": "根据名称或描述查找商品",
        "parameters": {
          "type": "object",
          "properties": {
            "name": {
              "type": "string",
              "description": "商品名称或关键词"
            }
          },
          "required": ["name"]
        }
      }
    }
  ],
  "tool_choice": "auto"
}

关键点

  • tools 数组定义可用工具
  • tool_choice 可设为 auto(自动选择)、none(不调用)、或强制指定

Step 2:模型响应工具调用请求

{
  "choices": [
    {
      "message": {
        "tool_calls": [
          {
            "id": "call_abc123",
            "type": "function",
            "function": {
              "name": "findProductByName",
              "arguments": "{\"name\":\"AirPods Pro\"}"
            }
          }
        ]
      }
    }
  ]
}

模型告诉你:请运行 findProductByName 函数,参数是 {"name": "AirPods Pro"}


Step 3:执行函数

// 你的业务逻辑
List<Product> result = productService.findByName("AirPods Pro");

// 序列化为 JSON
{
  "name": "AirPods Pro",
  "price": 249,
  "stock": 5
}

Step 4:将工具结果返回给模型

{
  "messages": [
    { "role": "user", "content": "AirPods Pro 有货吗?" },
    {
      "role": "assistant",
      "tool_calls": [
        {
          "id": "call_abc123",
          "type": "function",
          "function": {
            "name": "findProductByName",
            "arguments": "{\"name\":\"AirPods Pro\"}"
          }
        }
      ]
    },
    {
      "role": "tool",
      "tool_call_id": "call_abc123",
      "content": "{\"name\":\"AirPods Pro\",\"price\":249,\"stock\":5}"
    }
  ],
  "tools": [ /* 同样的工具定义 */ ],
  "tool_choice": "auto"
}

注意:必须保持完整的消息历史,包括用户的原始问题和 assistant 的工具调用请求。


Step 5:模型返回最终答案

{
  "choices": [
    {
      "message": {
        "content": "AirPods Pro 有货,售价 $249,库存 5 件。"
      }
    }
  ]
}

完整流程图

用户提问 → 模型判断需调用工具 → 返回工具调用请求
    ↓
执行函数 → 返回结果 → 模型整合 → 最终回答

三、手动实现的痛点

如果你用 REST API 手动实现 Tool Calling,需要处理:

痛点说明
JSON Schema 编写每个工具都要写参数 Schema
tool_call_id 追踪多工具调用时要正确关联 ID
参数解析绑定从 JSON 字符串解析参数
响应序列化将结果转回 JSON
多工具编排并行/顺序调用的复杂逻辑
会话状态管理维护完整的消息历史
错误处理工具执行失败的重试逻辑

这些工作繁琐且易出错,Spring AI 可以帮你搞定。


四、Spring AI 实现:零胶水代码

4.1 定义工具方法

@Component
public class ProductTools {

    private final ProductService productService;

    public ProductTools(ProductService productService) {
        this.productService = productService;
    }

    @Tool(description = "根据名称或描述查找商品")
    public String findProductByName(
        @ToolParam(description = "商品名称或关键词", required = true) String name
    ) {
        List<Product> products = productService.findByName(name);
        return toJson(products);
    }

    private String toJson(Object obj) {
        try {
            return new ObjectMapper().writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            return "{\"error\": \"" + e.getMessage() + "\"}";
        }
    }
}

关键注解

  • @Tool:标记方法为可调用工具
  • @ToolParam:描述参数,支持 requireddescription

4.2 调用 ChatClient

@RestController
@RequestMapping("/api")
public class ChatController {

    private final ChatModel chatModel;
    private final ProductTools productTools;

    public ChatController(ChatModel chatModel, ProductTools productTools) {
        this.chatModel = chatModel;
        this.productTools = productTools;
    }

    @PostMapping("/chat")
    public ChatResponse chat(@RequestBody ChatRequest request) {
        String answer = ChatClient.builder(chatModel)
            .defaultTools(productTools)
            .build()
            .prompt()
            .user(request.question())
            .call()
            .content();

        return new ChatResponse(request.question(), answer);
    }
}

4.3 Spring AI 自动处理的事项

功能说明
✅ 工具 Schema 生成自动从 @Tool 注解生成 JSON Schema
✅ 参数绑定自动解析 JSON 并绑定到方法参数
✅ tool_call_id 映射自动关联请求和响应
✅ 消息状态管理自动维护对话历史
✅ 并行工具编排自动处理多工具并行调用
✅ 顺序工具路由支持链式工具调用
✅ 可观测性集成 Micrometer 指标

五、进阶:多工具调用

当用户问:"AirPods Pro 和 Galaxy Buds 哪个便宜?"

模型会并行发起多个工具调用

{
  "tool_calls": [
    {
      "id": "call_001",
      "function": { "name": "findProductByName", "arguments": "{\"name\":\"AirPods Pro\"}" }
    },
    {
      "id": "call_002",
      "function": { "name": "findProductByName", "arguments": "{\"name\":\"Galaxy Buds\"}" }
    }
  ]
}

Spring AI 会自动并行执行这两个工具,然后将结果一起返回给模型。


六、进阶:顺序推理(SQL 生成场景)

更复杂的场景:用户问"库存少于 10 的商品有哪些?"

模型可能需要顺序调用多个工具

1. listTables() → 获取数据库表列表
2. getTableSchema("products") → 获取表结构
3. executeSQL("SELECT * FROM products WHERE stock < 10") → 执行查询

这需要系统提示引导

@SystemMessage("""
    你是一个数据库助手。
    当用户询问数据时,按以下步骤操作:
    1. 先调用 listTables 了解数据库结构
    2. 再调用 getTableSchema 了解表结构
    3. 最后调用 executeSQL 执行查询
    """)

七、坑点与最佳实践

7.1 工具描述要精准

// ❌ 不好的描述
@Tool(description = "查找商品")
public String findProduct(String name) { ... }

// ✅ 好的描述
@Tool(description = "根据商品名称或描述模糊查找,返回商品列表(包含价格、库存)")
public String findProductByName(
    @ToolParam(description = "商品名称、型号或关键词,支持模糊匹配") String name
) { ... }

原因:模型根据描述决定是否调用工具,描述越精准,调用越准确。


7.2 返回值必须是 String

// ❌ 编译通过,但运行时会出问题
@Tool
public List<Product> findProduct(String name) { ... }

// ✅ 正确做法:返回 JSON 字符串
@Tool
public String findProduct(String name) {
    List<Product> products = productService.findByName(name);
    return objectMapper.writeValueAsString(products);
}

7.3 处理工具执行错误

@Tool(description = "查询商品库存")
public String checkStock(@ToolParam(description = "商品ID") String productId) {
    try {
        Product product = productService.findById(productId);
        if (product == null) {
            return "{\"error\": \"商品不存在\"}";
        }
        return objectMapper.writeValueAsString(product);
    } catch (Exception e) {
        return "{\"error\": \"" + e.getMessage() + "\"}";
    }
}

模型能够理解错误信息并给出友好提示。


7.4 控制工具调用次数

ChatResponse response = ChatClient.builder(chatModel)
    .defaultTools(productTools)
    .build()
    .prompt()
    .user(question)
    .call()
    .chatResponse();

// 检查调用次数
List<AssistantMessage.ToolCall> toolCalls = 
    response.getResult().getOutput().getToolCalls();
System.out.println("工具调用次数: " + toolCalls.size());

八、MCP 扩展:工具即服务

如果你的工具需要被其他 Agent 或前端调用,可以用 MCP(Model Context Protocol):

8.1 添加依赖

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
</dependency>

8.2 配置 MCP Server

spring:
  ai:
    mcp:
      server:
        type: sse  # 或 stdio

效果:你的 @Tool 方法自动成为 MCP 兼容端点,无需额外代码!

同一个 @Tool 方法 → LLM 可调用
                 → MCP 客户端可调用
                 → 其他 Agent 可调用

九、完整代码示例

项目结构

src/main/java/com/xalgocapital/toolaicall/
├── ToolAiCallApplication.java
├── config/
│   └── AiConfig.java
├── controller/
│   └── ChatController.java
├── service/
│   └── ProductService.java
├── tools/
│   └── ProductTools.java
├── model/
│   ├── Product.java
│   ├── ChatRequest.java
│   └── ChatResponse.java
└── repository/
    └── ProductRepository.java

pom.xml 关键依赖

<dependencies>
    <!-- Spring Boot -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring AI -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    </dependency>

    <!-- 可选:MCP Server -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
    </dependency>
</dependencies>

application.yml

spring:
  application:
    name: tool-ai-call
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      base-url: https://api.openai.com
      chat:
        options:
          model: gpt-4o
          temperature: 0.7

# MCP Server(可选)
    mcp:
      server:
        type: sse

十、验证步骤

10.1 启动应用

./mvnw spring-boot:run

10.2 测试工具调用

curl -X POST http://localhost:8080/api/chat \
  -H "Content-Type: application/json" \
  -d '{"question": "AirPods Pro 有货吗?"}'

预期输出

{
  "question": "AirPods Pro 有货吗?",
  "answer": "AirPods Pro 有货,售价 $249,库存 5 件。"
}

10.3 检查日志

[INFO] Tool calling: findProductByName({"name": "AirPods Pro"})
[INFO] Tool result: {"name":"AirPods Pro","price":249,"stock":5}

十一、总结

方式代码量灵活性维护成本
REST API 手动实现最高
Spring AI @Tool
Spring AI + MCP最高

推荐

  • 简单场景:直接用 @Tool 注解
  • 需要跨 Agent 共享:启用 MCP

参考资料


本文基于 Spring AI 2.0.0-M3 编写,适用于 Spring Boot 4.0.x