番外一篇我们理清了"Spring AI 是接口层、Alibaba 是编排层"。这篇动手------用 ReactAgent 十分钟搭一个会自己思考、自己调工具的 Agent。你会发现, Spring AI 第 5 篇手搓的那套
@Tool调用,这儿几乎零改动就能直接复用。
一、先回忆第5篇怎么查天气的
翻回专栏第5篇《AI 学会调接口了》,我们用 @Tool + ChatClient 让 AI 查了天气。那一刻其实已经白嫖了"AI 自动调工具"的能力------模型自己决定要不要查、Spring AI 在底层( ToolCallingAdvisor )把工具执行了、结果回灌、再问一次模型,整条链路都是框架内部逻辑,一行循环都没写。
既然 ChatClient 已经能自动调工具、还能自动跑工具循环,那 ReactAgent 到底多了啥?
答案是:第5篇跑的那条工具循环,是个黑盒 ------我们只拿到最终答案,看不见模型中间"想了啥、调了哪个工具、看了什么返回" ,也没法给它套个迭代上限防止跑飞。而 ReactAgent 干的事,就是把这条黑盒循环变成白盒:同样自动调工具,但它把每一步的 Thought / Action / Observation 摊开给我们看,还能控迭代、控停止。
(别急,好多同学八成会问"ChatClient 不是也能调工具吗、那要 ReactAgent 干啥",后面专门掰扯它俩的区别。)
二、十行代码,Agent 自己管循环
直接上我工程 ReactAgentConfig 里的配置:
scss
@Bean
public ReactAgent reactAgent(ChatModel chatModel, WeatherTools weatherTools) {
return ReactAgent.builder()
.name("demo-react-agent")
.description("一个简单的 ReAct Agent 演示,支持天气查询和四则运算")
.model(chatModel) // ← 你 Spring AI 的 ChatModel,原样喂
.instruction("你是一个智能助手,能通过工具查询天气和进行数学计算......")
.methodTools(weatherTools) // ← 直接挂你的 @Tool 对象
.enableLogging(true) // ← 把思考链打出来,调试神器
.build();
}
数一下,核心就六行。.build() 出来的 ReactAgent,已经是一个"给目标、自己干"的智能体了。
逐行说人话:
- name / description:给 Agent 起名和写简介,多智能体协作时靠它互相识别。
- model(chatModel) :番外一篇说过,这就是 Spring AI 第 1 篇认识的
ChatModel,通义千问产出的标准实现,一字没变。 - instruction:系统提示词,定人设和规矩------和第 2 篇《Prompt 不是玄学》里写的 system prompt 是一回事。
- methodTools(weatherTools) :把你的工具对象丢进去,框架自动把
@Tool方法抽成工具 schema 挂上。 - enableLogging(true) :强烈建议开着,后面你能直接看到它"想一步、做一步"的全过程。
三、工具?就是第 5 篇那套 @Tool
重点来了------工具代码根本不用重写 。工程里的 WeatherTools 就是标准 Spring AI 写法:
less
@Component
public class WeatherTools {
@Tool(description = "查询指定城市的实时天气信息,包括天气状况、温度和湿度")
public String getCurrentWeather(
@ToolParam(description = "要查询天气的城市名称,例如:北京、上海") String city) {
// 演示用:返回模拟数据(真实场景把这里换成调真实天气 API 即可)
int temperature = new Random().nextInt(35) + 5;
return String.format("{"city":"%s","temperature":%d}", city, temperature);
}
@Tool(description = "对两个整数进行四则运算")
public String calculate(@ToolParam(description = "运算符") String operator,
@ToolParam(description = "第一个操作数") int a,
@ToolParam(description = "第二个操作数") int b) {
// add / subtract / multiply / divide ...
}
}
看见没?@Tool、@ToolParam、返回 JSON------全是第 5 篇熟的套路。
四、"等等,ChatClient 也能调工具啊?"------它俩到底差在哪
写到这里,心里冒出一个疑问------老实说,我写稿时也卡了一下:
专栏第 5 篇用 ChatClient + @Tool,模型不也是自己决定要不要调工具 吗?而且 Spring AI 默认还会帮我们把工具执行了、结果回灌、再问一次模型......那 ReactAgent 这不就多此一举?
先把话说透:这个直觉完全成立,前半句一点没错。
翻 Spring AI 官方文档的原话------当 ChatClient.tools(...) 时,框架会在 advisor 链里自动挂一个 ToolCallingAdvisor,它干的事是:
"The ToolCallingAdvisor implements the tool calling loop... looping until no more tool calls are needed."
翻译过来:ChatClient 不只是"模型决定调工具",它还会替我们跑一个工具调用循环 ------模型发 tool_calls → 框架执行 → 结果回灌 → 模型再决定还要不要调 → 直到模型不再发 tool_calls 为止。要不要继续循环,靠的是 ToolExecutionEligibilityPredicate(默认逻辑就是 chatResponse.hasToolCalls())。
所以,区别根本不是"谁决定调工具"------这俩都是模型决定的 。真正的差别在三个字:循环的性质。
| 维度 | ChatClient.tools() |
ReactAgent |
|---|---|---|
| 谁决定调哪个工具 | 模型 | 模型 |
| 谁在跑循环 | ToolCallingAdvisor 自动接管(黑盒) |
框架的 ReAct 状态机 |
| 循环的停止条件 | "模型还在发 tool_calls 就继续" | "模型给出最终答案 / 达到迭代上限就停" |
| 中间过程可不可见 | 默认不暴露 | enableLogging 全程可见 Thought / Action / Observation |
| 迭代上限 | 默认没有显式上限,靠模型自己收手 | 可配置停止条件与迭代上限,防跑飞 |
| 推理是否结构化 | 模型原生 tool-calling,无强制结构 | 显式"想 → 做 → 看"三段式 |
一句话戳破:
ChatClient 的工具循环 = 模型原生 tool-calling 的"黑盒循环";ReactAgent = 被框架框成 Thought→Action→Observation 的"白盒状态机循环"。 前者更轻,后者更可控、可观测。
那实战怎么选?给你个口诀:
- 问一句、调一两个工具、直接给答案 (比如"现在几点""北京天气如何")→
ChatClient.tools()就够了。别动不动就上 Agent,杀鸡不用牛刀。 - 要"先查 A 拿 ID、再拿 ID 查 B、最后综合判断"这种多步依赖推理 ,或者你想看清它每一步在想啥、怕它跑飞想加迭代上限 →
ReactAgent。
顺手点破一个最常见的误解:很多人以为"Agent = 会自动调工具",其实"自动调工具"是 ChatClient 就有的能力 。Agent 真正的增量,是可控、可观测、可编排的多步推理。
五、跑起来,看它"想一步做一步"
Controller 极简,把 query 丢给 Agent:
less
@GetMapping("/react")
public Map<String, String> chat(@RequestParam String query) {
AssistantMessage message = reactAgent.call(query);
return Map.of("query", query, "answer", message.getText());
}
curl 一发:
rust
curl 'http://localhost:9999/react?query=北京和上海今天哪个温度更高?'
因为开了 enableLogging,控制台会吐出这样的 ReAct 轨迹(数据为模拟):
css
Thought: 要比较两地温度,需要先分别查询北京和上海的天气。
Action: getCurrentWeather("北京") → Observation: {"city":"北京","temperature":28}
Action: getCurrentWeather("上海") → Observation: {"city":"上海","temperature":31}
Thought: 上海 31 度高于北京 28 度。
Final Answer: 上海今天比北京热,上海 31℃,北京 28℃。
这就是 ReAct 循环被框架跑起来的样子:只问了一句,它自己决定"先查北京、再查上海、然后比大小"。

六、它到底替你干了啥
把话说透,ReactAgent 内置了三样东西:
- ReAct 循环(白盒版) :思考 → 调工具 → 观察 → 再思考,直到能回答。
ChatClient其实也会自动跑工具循环,但过程看不见;ReactAgent 把它摊开成看得见的 Thought / Action / Observation,还能控迭代上限。 - 工具调度 :多个
@Tool自动注册,模型自己选、自己传参。对应你第 6 篇"智能调度中心"。
等于说,第5篇用 ChatClient 已经拿到了"自动调工具"的能力,ReactAgent 是在这之上把循环白盒化、可观测、可编排------从"只拿到最终答案"变成了"既拿到答案、也看得清每一步、还能控住它"。
七、下篇预告
别被"十行代码"骗了------ReactAgent 这么丝滑,是因为它底下踩着一套 Graph 运行时。所谓"想一步做一步",本质是 Graph 里的一个个节点在流转。