1. 从单工具到多工具:Java Agent 的真实痛点
很多 Java 同行在 Spring AI 里跑通第一个@Tool之后,都会有一种“不过如此”的错觉——挂一个天气查询,模型调一下,返回结果,收工。但真实业务场景从来不是单点调用。你回想一下企业里最常见的差旅审批流程:查目的地天气、算差旅预算、核对报销标准、发提醒邮件、写进审批系统。这一串动作如果靠硬编码串起来,就是一段典型的“意大利面条”代码,任何一个环节的接口变了,整条链路都得重写。
Agent 智能体要解决的就是这个问题:把“下一步该干什么”的决策权交给大模型,Java 侧只负责把工具挂载好、把执行环境准备好。而 ReAct(Reasoning + Acting)就是让模型学会“先想再做、做完再看、看完再想”的核心范式。这篇文章面向的是已经会用 Spring AI 做基础 Tool Calling、但还没跑通多工具协同与动态决策的 Java 开发者。我会用 TaoToken 作为统一的模型调用通道,把 API Key 管理、配置骨架、工具注册、ReAct 循环验证、以及踩坑排查一次性讲清楚。你跟着做,能拿到一个可运行的多工具 Agent 骨架,而不是停留在概念层面。
2. TaoToken 前置:统一 Key 与通道准备
在动手写 Agent 之前,先把模型调用通道理顺。Spring AI 默认对接各家模型时,每换一个模型就要改一套 base-url 和 api-key,多工具 Agent 在调试阶段会频繁切换模型做对比,这种反复改配置的方式非常低效。TaoToken 的思路是提供一个统一的 API 通道,你只需要维护一份 Key,就能在多个模型之间切换,配置层不用动。
你需要先拿到一个可用的 API Key。访问控制台创建:
https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite创建完成后在 API Keys 页面复制出来:
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite这里有个细节要注意:TaoToken 的 API 端点是不带 UTM 参数的,统一用https://taotoken.net/api。很多人在配置时把带追踪参数的完整 URL 填进base-url,结果请求 404,排查半天以为是 Key 的问题。记住这个区分:文档和 CTA 链接带 UTM,实际请求端点不带。
注意:API Key 属于敏感凭证,不要硬编码进
application.yml提交到 Git。生产环境用环境变量注入,本地调试可以用.env文件配合 IDE 的 EnvFile 插件。
如果你对 Spring AI 的接入方式还不熟,可以先翻一下接入文档:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite3. 可复制配置:settings.json 与 config.toml 骨架
这一节给你两份可直接复制的配置骨架。一份是给 Claude Code / Anthropic 风格客户端用的settings.json,一份是给通用 TOML 配置场景用的config.toml。两者都指向 TaoToken 的统一通道,你按自己项目的技术栈选一份即可。
先看settings.json,适合在 Claude Code 或类似支持 Anthropic 协议的客户端里使用:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "sk-你的TaoToken密钥", "ANTHROPIC_MODEL": "claude-sonnet-4-20250514" }, "permissions": { "allow": [ "Read", "Write", "Bash" ] } }这份配置的关键在于ANTHROPIC_BASE_URL指向 TaoToken 的 API 端点,ANTHROPIC_AUTH_TOKEN填你在控制台创建的 Key。模型名按你实际要用的填,切换模型只改这一行,不用动其他配置。
再看config.toml,适合 Spring AI 项目里做外部化配置管理:
[spring.ai.openai] base-url = "https://taotoken.net/api" api-key = "sk-你的TaoToken密钥" chat.options.model = "gpt-4o-mini" chat.options.temperature = 0.7 [agent] max-iterations = 8 enable-tool-callback = true tool-timeout-seconds = 30这里我特意加了[agent]段,max-iterations是 ReAct 循环的熔断阈值,后面排错章节会重点讲为什么必须设这个值。tool-timeout-seconds控制单个工具执行的超时,避免某个外部 API 卡死拖垮整个 Agent 循环。
对应的 Spring Bootapplication.yml里这样引用:
spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.7把 Key 通过环境变量TAOTOKEN_API_KEY注入,配置文件里不出现明文。这套配置跑通之后,你的 Agent 就有了稳定的模型调用底座。
4. 工具注册与 ReAct 循环代码片段
配置就绪后,进入核心部分:工具注册和 ReAct 循环。我按“声明工具 → 组装 Agent → 观察循环”三步走,每步都给可复制的代码。
4.1 声明多工具集合
先建一个工具类,把差旅场景需要的三个工具声明出来。注意工具描述要写清楚“什么时候该调用我”,这是模型做决策的唯一依据:
public class TravelAgentTools { @Tool(description = "查询指定城市指定日期的天气。当任务涉及天气、出行建议时必须调用。") public String getWeather( @ToolParam(description = "城市名称,例如:杭州") String city, @ToolParam(description = "日期,例如:明天") String date) { System.out.println("==> 工具调用: getWeather | " + city + " " + date); return city + date + "天气:多云转晴,气温 25°C,适合出行。"; } @Tool(description = "精确数学计算器。任何涉及金额、数量、加减乘除的计算都必须调用此工具,禁止口算。") public String calculator( @ToolParam(description = "数学表达式,例如:1200 + 50 * 3") String expression) { System.out.println("==> 工具调用: calculator | " + expression); return "计算结果: 1350"; } @Tool(description = "发送邮件给指定收件人。当所有数据收集和计算完成后,用此工具发送最终报告。") public String sendEmail( @ToolParam(description = "收件人姓名") String toName, @ToolParam(description = "邮件正文,需包含所有已收集的数据") String content) { System.out.println("==> 工具调用: sendEmail | 收件人: " + toName); System.out.println("[邮件正文]\n" + content); return "邮件已发送至 " + toName; } }三个工具的描述里我都加了强约束词,比如“必须调用”“禁止口算”。这不是可有可无的修饰,实测下来,模型在复杂任务里很容易偷懒直接口算,导致结果不准。把规则写进工具描述,比写在 system prompt 里更贴近调用点,效果更稳。
4.2 组装 Agent 并挂载工具
接下来把工具挂到 ChatClient 上,并注入 ReAct 引导提示词:
@Test public void testMultiToolAgent() { String task = """ 帮我查一下杭州明天的天气,然后计算如果去杭州出差3天, 机票1200元,每天打车50元,总共需要报销多少钱? 最后根据天气和报销额,发一封差旅提醒邮件给张三。 """; String systemPrompt = """ 你是一个企业行政 Agent,拥有多个外部工具。 执行规则: 1. 复杂任务必须拆解为多步,逐步执行。 2. 涉及计算必须调用 calculator,禁止口算。 3. 涉及天气必须调用 getWeather。 4. 所有数据收集完毕后再调用 sendEmail 发送报告。 """; String response = chatClientBuilder.build() .prompt() .system(systemPrompt) .user(task) .tools(new TravelAgentTools()) .call() .content(); System.out.println("最终响应:\n" + response); }这段代码里没有一行流程编排逻辑——没有 if-else 判断先查天气还是先算账,没有手动拼接邮件内容。所有决策都交给模型,Java 侧只负责把工具挂上去。
4.3 ReAct 循环的底层状态机
Spring AI 在挂载工具后,底层会自动开启一个循环状态机。简化后的核心逻辑是这样的:
public ChatResponse internalCall(Prompt prompt, ChatResponse previous) { ChatResponse response = callLlmApiAndParseResult(prompt); if (toolExecutionRequired(response)) { ToolExecutionResult toolResult = toolCallingManager.executeToolCalls(prompt, response); if (toolResult.returnDirect()) { return buildDirectResponse(toolResult); } else { Prompt newPrompt = new Prompt(toolResult.conversationHistory(), prompt.getOptions()); return this.internalCall(newPrompt, response); } } return response; }这个递归结构就是 ReAct 的“发动机”:模型返回工具调用请求 → 本地执行工具 → 把结果追加进上下文 → 再次请求模型 → 模型判断是否还需要调工具 → 循环直到模型给出最终答案。你不需要手写这个循环,但必须理解它,因为后面排错全靠对这个循环的认知。
5. 验证请求与成功结果
代码写完后,跑一次完整验证。启动测试方法,观察控制台输出顺序:
==> 工具调用: getWeather | 杭州 明天 ==> 工具调用: calculator | 1200 + 50 * 3 ==> 工具调用: sendEmail | 收件人: 张三 [邮件正文] 张三,杭州明天多云转晴,25°C。 出差3天费用:机票1200元 + 打车150元 = 总计1350元。 请做好出行准备。最终模型输出的响应会包含完整的任务总结。这里有个值得注意的细节:整个过程中,Spring Boot 应用和模型之间发生了 4 次网络请求——查天气、算账、发邮件、最终汇报。模型自主规划了行动路线,Java 侧没有写任何编排代码。
如果你想单独验证模型对话通道是否正常,可以先用模型对话页面做一次简单测试:
https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite确认通道没问题后,再跑 Agent 测试,能快速区分是配置问题还是代码问题。
6. 本篇常见错排查清单
多工具 Agent 跑不起来,问题通常集中在几个地方。我按排查优先级列出来,你对照检查。
第一个坑:工具描述太模糊导致模型不调用。如果你发现模型直接口算而不调 calculator,八成是工具描述里没写清楚“必须调用”。解决办法是在@Tool(description=...)里加约束词,同时在 system prompt 里再强调一遍。两处都写,双保险。
第二个坑:ReAct 循环死锁。如果模型陷入幻觉,反复调用同一个工具,递归会一直进行下去,最终栈溢出或者烧掉大量 Token。这就是为什么config.toml里要设max-iterations。Spring AI 较新版本支持在 ChatOptions 里配置最大迭代次数,老版本需要自己在工具执行层加计数器拦截。没有熔断机制的 Agent 不要上生产。
第三个坑:工具抛异常导致整个请求失败。默认情况下,工具方法抛出的异常会被 Spring AI 捕获并包装成错误信息回传给模型,模型有机会自我修正。但如果你在工具里 catch 了异常却返回 null,模型收到空结果会困惑,可能反复重试。正确做法是让异常抛出,或者返回明确的错误描述字符串。
第四个坑:base-url 填错。再强调一次,TaoToken 的 API 端点是https://taotoken.net/api,不带任何查询参数。如果你从浏览器地址栏复制了带 UTM 的链接填进去,请求会失败。这个错误很隐蔽,因为报错信息通常只说连接失败,不会告诉你 URL 多了参数。
第五个坑:上下文膨胀。每次工具返回结果都会追加到对话历史里,循环次数多了,Prompt 长度指数增长。如果你的 Agent 要处理十几步的长任务,需要在工具返回结果时做截断或摘要,控制单次返回的内容长度。简单做法是工具只返回关键字段,不要把整个 JSON 响应体塞回去。
排查时建议打开 Spring AI 的 debug 日志:
logging: level: org.springframework.ai: DEBUG这样你能看到每次请求的完整 Prompt 和模型返回,定位问题会快很多。
7. 下一步:从 ReAct 到长期编码 Agent
跑通这个多工具 Agent 之后,你已经掌握了 ReAct 的核心运转逻辑。但如果你想把 Agent 用在长期的编码任务或者自动化工作流里,单次对话的 Agent 还不够——你需要一个能持续运行、记住上下文、跨会话保持状态的编码 Agent。这类场景更适合用 Coding Plan 来承载:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite它和单次 API 调用的区别在于,Coding Plan 面向的是持续性的开发任务,Agent 可以在多轮交互中保持对项目上下文的理解,适合做代码重构、批量修改、持续集成这类长线工作。如果你只是做单次任务验证,用 API Key 直接调就够了;但如果要构建一个真正能“干活”的编码助手,Coding Plan 是更合适的底座。
回到技术本身,ReAct 只是 Agent 的一种决策范式。当任务复杂度继续上升,你会遇到纯 ReAct 搞不定的场景——比如需要先制定完整计划再执行的 Plan-and-Execute 模式,或者多个 Agent 分工协作的 Multi-Agent 架构。这些进阶内容建立在今天这个多工具协同的基础之上。把今天的工具注册、循环验证、熔断控制这三件事吃透,后面往上叠架构才不会虚。