🧑 博主简介CSDN博客专家「历代文学网」(PC端可以访问:https://lidaiwenxue.com/#/?__c=1000)总架构师,首席架构师,也是联合创始人!16年工作经验,精通Java编程高并发设计分布式系统架构设计Springboot和微服务,熟悉LinuxESXI虚拟化以及云原生Docker和K8s,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。
🤝商务合作:请搜索或扫码关注微信公众号 “ 心海云图

在这里插入图片描述


在这里插入图片描述

Spring AI-2.0 Agent 编程实战:从 Tool Calling 到 MCP 协议,附完整报文级日志

一次真实完整的 Tool Calling 交互,从 HTTP 请求报文到模型思维链,全部可视化。


前言

2025-2026 年,大模型应用的范式正在发生根本性转变。单纯靠 Prompt 工程已经无法满足复杂业务需求——模型需要访问实时数据(天气、股价、库存)、调用外部系统数据库APIERP),甚至自主编排多步任务

Spring AI 正是在这个背景下诞生的框架,它为 Java/Spring Boot 生态提供了与 LLM 交互的标准抽象,核心能力包括:

  • ChatClient — 统一对话客户端,类似 RestTemplate 之于 HTTP
  • Tool Calling — 让 LLM 可以调用本地 Java 方法获取实时数据
  • MCP 协议 — 标准化的工具管理协议(Anthropic 提出)
  • ReactAgent — 自动编排多步工具调用

本文用一个完整的智能旅行规划助手项目,带你从零理解这些概念的底层原理。最关键的是,我会展示通过 RestClient 请求拦截器捕获的真实 HTTP 请求/响应报文,让你看到 Tool Calling 的"第一性原理"。

本项目github地址(真实可直接运行的最小demo):

https://github.com/lilinhai/spring-ai-alibaba-2.0-agent


一、技术栈

组件 版本
Spring Boot 4.0.0
Spring Framework 7.0.1
Spring AI 2.0.0-M1
spring-ai-alibaba 2.0.0-M1.1
JDK 17+
大模型 智谱 GLM-4.7

⚠️ 注意:Spring AI 2.0.0-M1 与后续 GA 版本的 API 不兼容,例如 ToolExecutionEligibilityPredicate 在 M1 中存在,GA 版已移除。


二、全流程架构

用户 HTTP 请求
     │
     ▼
TravelController (REST API)
     │
     ▼
ChatClient (统一对话入口)
  ├── .defaultSystem("提示词")
  ├── .defaultToolCallbacks(...)
  ├── .defaultAdvisors(...)
  └── .build()
     │
     ▼
ZhiPuAiChatModel → ZhiPuAiApi → RestClient → HTTP → 智谱 API
     │                                                   │
     │  ← tool_calls  ← ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
     │
     ├── ToolCallingManager.executeToolCalls()
     │     ├── WeatherTool.getWeather("北京")     ← @Tool 反射调用
     │     ├── AttractionTool.getAttractions("上海")
     │     └── HotelTool.getHotels("北京", "舒适")
     │
     └── 递归 call() 带上工具结果,送回 LLM 生成最终回答

三、模型注册:接入智谱 GLM

依赖

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-zhipuai</artifactId>
</dependency>

配置

spring:
  ai:
    zhipuai:
      api-key: your_api_key_here
      base-url: https://open.bigmodel.cn/api/paas
      chat:
        options:
          model: glm-4.7-flash    # ← 注意:不是 spring.ai.zhipuai.chat.model
          temperature: 0.7

配置陷阱

ZhiPuAiChatPropertiesmodel 字段位于 @NestedConfigurationProperty 注解的 options 对象内,所以正确路径是 spring.ai.zhipuai.chat.options.model。如果配成 spring.ai.zhipuai.chat.model,模型会使用默认值 glm-4-air(已废弃),导致 HTTP 429。


四、ChatClient:与 LLM 对话的统一入口

基本用法

// 基础版
ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultSystem("你是一位专业的旅行规划助手。")
    .build();

// 带工具版
ChatClient chatClientWithTools = ChatClient.builder(chatModel)
    .defaultSystem("你是一位专业的旅行规划助手。请使用提供的工具。")
    .defaultToolCallbacks(toolCallbackProvider)
    .build();

// 同步调用
String result = chatClient.prompt()
    .user("北京现在天气怎么样")
    .call()
    .content();

// 流式调用
Flux<String> stream = chatClient.prompt()
    .user("写一个上海一日游行程")
    .stream()
    .content();

Builder 状态共享陷阱(重要)

// ❌ 错误:注入的 Builder 是同一个实例
@Autowired ChatClient.Builder builder;

this.chatClient = builder.defaultSystem("提示词A").build();
this.chatClientWithTools = builder.defaultSystem("提示词B")
    .defaultToolCallbacks(...).build();
// chatClient 的 defaultSystem 和 tools 会被第二次调用污染!

// ✅ 正确:分别创建独立 Builder
this.chatClient = ChatClient.builder(chatModel)
    .defaultSystem("提示词A").build();
this.chatClientWithTools = ChatClient.builder(chatModel)
    .defaultSystem("提示词B").defaultToolCallbacks(...).build();

五、Tool Calling 第一性原理

先看一张时序图

User            ChatModel           LLM API         @Tool 方法
 │                  │                  │                │
 │  1. 问天气        │                  │                │
 │─────────────────>│                  │                │
 │                  │  2. System+User  │                │
 │                  │     +Tools定义    │                │
 │                  │─────────────────>│                │
 │                  │                  │                │
 │                  │  3. 判断需要调用  │                │
 │                  │     getWeather   │                │
 │                  │<─────────────────│                │
 │                  │                  │                │
 │                  │  4. 执行工具      │                │
 │                  │──────────────────────────────────>│
 │                  │                  │                │
 │                  │  5. 返回天气结果   │                │
 │                  │<──────────────────────────────────│
 │                  │                  │                │
 │                  │  6. System+User  │                │
 │                  │     +Tool结果     │                │
 │                  │─────────────────>│                │
 │                  │                  │                │
 │                  │  7. 返回最终回答   │                │
 │                  │<─────────────────│                │
 │<────────────────│                  │                │

核心流程分为两轮 HTTP 交互

第 1 轮:发送 System Prompt + 用户问题 + 所有工具定义(JSON Schema)
第 2 轮:将工具执行结果拼接回对话历史,重新请求 LLM 生成最终回答

下面看真实报文。


第 1 轮请求——携带工具定义

POST https://open.bigmodel.cn/api/paas/v4/chat/completions
Content-Type: application/json

{
  "messages": [
    {"content": "你是一位专业的旅行规划助手。请使用提供的工具来帮助用户查询天气、景点、酒店等信息。", "role": "system"},
    {"content": "北京现在天气怎么样", "role": "user"}
  ],
  "model": "glm-4.7",
  "stream": false,
  "temperature": 0.7,
  "tools": [
    {
      "function": {
        "description": "获取指定城市的实时天气信息和未来天气预报",
        "name": "getWeather",
        "parameters": {
          "$schema": "https://json-schema.org/draft/2020-12/schema",
          "type": "object",
          "properties": {
            "city": {"type": "string", "description": "城市名称,例如:北京、上海、广州"}
          },
          "required": ["city"]
        }
      },
      "type": "function"
    },
    // ... getDistance, getAttractions, getHotels 等工具定义
  ]
}

tools 数组中的 JSON Schema 是从 @Tool + @ToolParam 注解的元数据自动生成的。


第 1 轮响应——模型决定调用工具

{
  "choices": [{
    "finish_reason": "tool_calls",
    "index": 0,
    "message": {
      "content": "我来帮您查询北京现在的天气情况。",
      "reasoning_content": "\n用户问的是北京现在的天气情况。我需要使用getWeather函数来获取北京的天气信息。函数需要一个city参数,用户明确提到了\"北京\",所以我可以直接调用这个函数。",
      "role": "assistant",
      "tool_calls": [{
        "function": {
          "arguments": "{\"city\": \"北京\"}",
          "name": "getWeather"
        },
        "id": "call_-7453302695756030892",
        "type": "function"
      }]
    }
  }],
  "usage": {
    "completion_tokens": 60,
    "prompt_tokens": 565,
    "total_tokens": 625,
    "completion_tokens_details": {
      "reasoning_tokens": 39
    }
  }
}
字段 含义
finish_reason: "tool_calls" 模型判定需要调用工具,而非直接回答
reasoning_content GLM-4.7 的思维链,展示了模型为什么选择这个工具和参数
tool_calls[].function.arguments 模型填好的参数 JSON 字符串
tool_calls[].id 工具调用 ID,后续 tool 结果需要引用它

reasoning_content 是 GLM 特有的思维链输出,对调试非常有用——你能看到模型是如何推理的


中间环节:Spring AI 自动执行本地方法

收到 tool_calls 后,Spring AI 框架内部:

Executing tool call: getWeather
Starting execution of tool: getWeather
【Tool 本地方法】查询天气: city=北京              ← 反射调用 WeatherTool
Successful execution of tool: getWeather
Converting tool result to JSON.                   ← 序列化为 TOOL 消息

DefaultToolCallingManager 负责协调整个流程:

  1. resolveToolDefinitions() — 解析工具定义
  2. MethodToolCallback.doCall() — 通过反射调用 @Tool 方法
  3. DefaultToolCallResultConverter — 将返回值序列化为 JSON

第 2 轮请求——工具结果回传 LLM

POST https://open.bigmodel.cn/api/paas/v4/chat/completions
Content-Type: application/json

{
  "messages": [
    {"content": "你是一位专业的旅行规划助手。请使用提供的工具来帮助用户查询天气、景点、酒店等信息。", "role": "system"},
    {"content": "北京现在天气怎么样", "role": "user"},
    {
      "content": "我来帮您查询北京现在的天气情况。",
      "role": "assistant",
      "tool_calls": [{
        "id": "call_-7453302695756030892",
        "type": "function",
        "function": {"name": "getWeather", "arguments": "{\"city\": \"北京\"}"}
      }]
    },
    {
      "content": "\"🌍 北京\\n🌡️ 当前温度:22°C\\n☁️ 天气状况:晴转多云\\n💨 风速:12 km/h\\n💧 湿度:65%\\n📅 未来三天:明天 24°C 晴,后天 20°C 小雨\\n\"",
      "role": "tool",
      "name": "getWeather",
      "tool_call_id": "call_-7453302695756030892"
    }
  ],
  "model": "glm-4.7",
  "tools": [
    // ... 仍然包含所有工具定义
  ]
}

此时 messages 数组包含 4 条消息

  1. system — 系统提示词
  2. user — 用户原始问题
  3. assistant — 模型第一轮回答(含 tool_calls
  4. tool — 工具执行结果(通过 tool_call_id 与第 3 条关联)

注意:第 2 轮请求仍然携带 tools 数组,因为 LLM 可能需要再次调用其他工具。


第 2 轮响应——模型输出最终回答

{
  "choices": [{
    "finish_reason": "stop",
    "index": 0,
    "message": {
      "content": "北京现在的天气情况如下:\n\n🌤️ **当前天气**\n- 温度:22°C\n- 天气状况:晴转多云\n- 风速:12 km/h\n- 湿度:65%\n\n📅 **未来三天预报**\n- 明天:24°C,晴天\n- 后天:20°C,小雨\n\n今天北京的天气还不错,温度适宜,不过后天会有小雨,如果计划出行的话建议关注一下天气变化哦!",
      "role": "assistant"
    }
  }],
  "usage": {
    "completion_tokens": 188,
    "prompt_tokens": 659,
    "total_tokens": 847,
    "completion_tokens_details": {
      "reasoning_tokens": 88
    }
  }
}

模型基于工具返回的原始数据,做了格式化加工后输出给用户——温度、天气状况、未来三天预报,还有语气友好的提醒。在用户侧看来,这就是一次普通的对话,背后却经历了两轮 HTTP 交互 + 一次本地方法反射调用。


核心要点总结

  1. 两轮交互是标配:第一轮 LLM 返回 tool_calls,第二轮在收到工具结果后返回 stop。多个工具可能需要更多轮次。
  2. 工具定义(tools)每次请求都需要附带:因为 LLM 可能随时决定再次调用工具。
  3. reasoning_content:GLM 的思维链,展示模型选择工具和参数的推理过程,对调试极为有用。
  4. tool_call_id 是关联纽带:assistant 的 tool_calls[].id 与 tool 消息的 tool_call_id 一一对应。
  5. 自动拼接对话历史ZhiPuAiChatModel.call() 递归调用自身,每次将上一轮的响应和新结果拼入 messages

六、@Tool 注解:注册本地方法

定义 Tool 类

任何 Spring Bean 都可以加 @Tool 方法,不限于 @Service

@Component  // @Service 也行
public class WeatherTool {
    private static final Logger log = LoggerFactory.getLogger(WeatherTool.class);

    @Tool(name = "getWeather", description = "获取指定城市的天气预报")
    public String getWeather(
            @ToolParam(description = "城市名称", required = true) String city
    ) {
        log.info("【Tool】查询天气: city={}", city);
        return "🌍 " + city + "\n🌡️ 当前温度:22°C...";
    }
}

注册到 Spring 容器

@Bean
public ToolCallbackProvider travelTools(
        WeatherTool weatherTool,
        AttractionTool attractionTool,
        HotelTool hotelTool
) {
    return MethodToolCallbackProvider.builder()
            .toolObjects(weatherTool, attractionTool, hotelTool)
            .build();
}

MethodToolCallbackProvider 自动反射扫描传入对象的 @Tool 方法,为每个方法创建一个 MethodToolCallback 实例。

挂载到 ChatClient

ChatClient.builder(chatModel)
    .defaultToolCallbacks(toolCallbackProvider)
    .build();

七、MCP 协议:工具的标准化

MCP(Model Context Protocol)是 Anthropic 提出的开放协议,旨在为大模型应用提供标准化的工具/资源/提示管理接口。

MCP Server

两种注册方式:

方式一:自动配置

spring:
  ai:
    mcp:
      server:
        name: travel-mcp-server
        transport: SSE
        capabilities:
          tools: true

Spring AI 自动将所有 ToolCallback Bean 注册为 MCP 工具。

方式二:编程式定义

@Bean
public ToolCallback getWeatherMcpTool() {
    return FunctionToolCallback.builder("mcp_getWeather",
            (Function<Map<String, Object>, String>) input -> {
                String city = (String) input.get("city");
                return weatherTool.getWeather(city);
            })
        .description("获取指定城市的天气预报")
        .inputSchema(jsonSchema(
            prop("city", "城市名称", true)
        ))
        .inputType(Map.class)   // 必须设置
        .build();
}

MCP Client(手动连接外部 MCP 服务)

// 1. 创建 SSE 传输层
McpClientTransport transport = HttpClientSseClientTransport
    .builder(mcpServerUrl)
    .sseEndpoint("/mcp/v1/sse")
    .build();

// 2. 构建同步 MCP 客户端
McpSyncClient mcpClient = McpClient.sync(transport)
    .requestTimeout(Duration.ofSeconds(60))
    .build();

// 3. 握手 + 发现工具
mcpClient.initialize();
ListToolsResult toolsResult = mcpClient.listTools();

// 4. 转换为 ToolCallback
for (McpSchema.Tool mcpTool : toolsResult.tools()) {
    ToolCallback callback = FunctionToolCallback.builder(mcpTool.name(), input -> {
            CallToolResult result = mcpClient.callTool(
                new CallToolRequest(mcpTool.name(), input));
            return extractText(result);
        })
        .description(mcpTool.description())
        .inputType(Map.class)
        .build();
    toolCallbacks.add(callback);
}

八、ReactAgent:自动编排工具

ReactAgent 基于 ReAct(Reasoning + Acting)模式,自动完成"思考→调用工具→观察结果→再思考"的循环。

ReactAgent agent = ReactAgent.builder()
    .name("travel_planner")
    .model(chatModel)
    .description("智能旅行规划助手")
    .instruction("""
        你是一位专业的旅行规划师。请按照以下步骤规划行程:
        1. 使用 getWeather 查询天气
        2. 使用 getAttractions 查询景点
        3. 使用 getHotels 查询酒店
        4. 综合所有信息输出完整行程计划
        """)
    .toolCallbackProviders(toolCallbackProvider)
    .saver(new MemorySaver())    // 支持多轮记忆
    .build();

Agent 执行流程:

_START_ → 分析用户需求
  ↓
_AGENT_MODEL_ → 思考需要调用哪些工具
  ↓
_AGENT_TOOL_  → 调用 getWeather("北京") → 获取天气数据
  ↓
_AGENT_MODEL_ → 思考下一步 → 需要查景点
  ↓
_AGENT_TOOL_  → 调用 getAttractions("北京") → 获取景点列表
  ↓
_AGENT_MODEL_ → 思考下一步 → 需要查酒店
  ↓
_AGENT_TOOL_  → 调用 getHotels("北京", "舒适") → 获取酒店推荐
  ↓
_AGENT_MODEL_ → 综合所有信息 → 输出完整行程
  ↓
_END_

九、常见踩坑记录

1. ChatClient.Builder 状态泄漏

注入的 Builder 是原型 Bean,但 build() 返回的 Client 与 Builder 共享同一个可变 RequestSpec 实例。后续修改 Builder 会污染已构建的 Client。解决方法:每次使用 ChatClient.builder(chatModel) 工厂方法

2. 模型名称配置位置错误

# ❌ 错误
spring.ai.zhipuai.chat.model=glm-4.7-flash
# ✅ 正确 — 多了一层 options
spring.ai.zhipuai.chat.options.model=glm-4.7-flash

3. SSE 流式输出中文乱码

Windows 平台默认编码非 UTF-8。用 OncePerRequestFilter 强制设置:

response.setCharacterEncoding(StandardCharsets.UTF_8.name());

4. FunctionToolCallback.inputType 必须设置

// 否则启动报错 "inputType cannot be null"
.inputType(Map.class)

5. GLM-4.7 模型重复调用工具

非 flash 版本在收到工具结果后可能再次调用相同工具。这是模型行为,不影响最终结果。


十、总结

本文通过一个真实的项目,完整演示了 Spring AI Agent 编程的四大核心场景:

场景 核心机制
基础对话 ChatClient.prompt().call()
流式对话 ChatClient.prompt().stream()
多轮记忆 MessageChatMemoryAdvisor
Tool Calling @Tool + MethodToolCallbackProvider
MCP Server FunctionToolCallback Bean 自动注册
MCP Client HttpClientSseClientTransport 手动连接
ReactAgent ReAct 循环自动编排多工具

最核心的收获是:Tool Calling 的本质就是两轮 HTTP 交互——第一轮模型返回 tool_calls 决定要调用哪些函数、参数是什么;第二轮将函数执行结果拼接回去,让模型生成最终回答。理解了这层报文级别的原理,你就能诊断绝大多数 Tool Calling 的问题。

如果你想获取完整可运行的项目源码,或者自己动手复现 Wire Log,只需添加一个 RestClientCustomizer Bean —— 我们会在下一篇技术文章中介绍具体实现方式。


项目源码基于 Spring Boot 4.0.0 + Spring AI 2.0.0-M1 + 智谱 GLM-4.7

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐