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


Spring AI-2.0 Agent 编程实战:从 Tool Calling 到 MCP 协议,附完整报文级日志
一次真实完整的 Tool Calling 交互,从 HTTP 请求报文到模型思维链,全部可视化。
前言
2025-2026 年,大模型应用的范式正在发生根本性转变。单纯靠 Prompt 工程已经无法满足复杂业务需求——模型需要访问实时数据(天气、股价、库存)、调用外部系统(数据库、API、ERP),甚至自主编排多步任务。
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
配置陷阱
ZhiPuAiChatProperties 中 model 字段位于 @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 负责协调整个流程:
resolveToolDefinitions()— 解析工具定义MethodToolCallback.doCall()— 通过反射调用@Tool方法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 条消息:
system— 系统提示词user— 用户原始问题assistant— 模型第一轮回答(含tool_calls)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 交互 + 一次本地方法反射调用。
核心要点总结
- 两轮交互是标配:第一轮 LLM 返回
tool_calls,第二轮在收到工具结果后返回stop。多个工具可能需要更多轮次。 - 工具定义(tools)每次请求都需要附带:因为 LLM 可能随时决定再次调用工具。
reasoning_content:GLM 的思维链,展示模型选择工具和参数的推理过程,对调试极为有用。tool_call_id是关联纽带:assistant 的tool_calls[].id与 tool 消息的tool_call_id一一对应。- 自动拼接对话历史:
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
更多推荐

所有评论(0)