ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

SpringAI 自主规划智能体实现及解析:TaoToken 统一 Key 接入 ReAct Agent 实战

SpringAI 自主规划智能体实现及解析:TaoToken 统一 Key 接入 ReAct Agent 实战 1. 从一次“翻车”的 Spring Boot 任务说起为什么普通对话应用不够用我试过在 Spring Boot 里用 SpringAI 做一个“帮我整理本周技术资讯并生成摘要”的小工具。第一版很朴素用户提问模型回答最多再挂一个搜索工具。结果一跑就露馅——模型搜到一堆链接后直接回了一句“以上是相关资讯”既没打开页面也没做摘要更没生成文件。它只完成了“一次提问 → 一次回答”剩下的步骤全得我手动追问。这就是普通对话应用和自主规划智能体的分水岭。普通对话应用是反应式的你问一句它答一句工具调用最多一次任务分解依赖你逐步引导。而自主规划智能体Autonomous Planning Agent能根据目标自主拆解任务、制定计划、选择工具并一步步执行直到任务完成。它靠的是 ReAct 模式——Reasoning推理 Acting行动的循环先思考下一步做什么再调用工具执行拿到结果后观察然后进入下一轮思考。在 Spring Boot SpringAI 的技术栈里实现这套循环核心难点不在模型能力而在两件事一是怎么把 SpringAI 默认的“框架托管工具调用”关掉夺回循环控制权二是怎么让模型调用一个统一的、稳定的模型端点。前者靠ToolCallingChatOptions.internalToolExecutionEnabled(false)后者我用 TaoToken 的统一 Key 通道来解决——一个 Key 就能接入 OpenAI 兼容端点省去在application.yml里反复换 base-url 和 api-key 的麻烦。这篇文章会交付可复制的application.yml、Agent 配置片段以及启动后验证“规划-执行-观察”循环是否跑通的具体动作。适合已经会用 Spring Boot 3、想从“问答应用”跨到“自主智能体”的 Java 后端同学。读完你能得到一个能自主完成“搜索 → 抓取 → 下载 → 生成 PDF → 结束”这类复合任务的 ReAct Agent。2. TaoToken 前置准备统一 Key 接入 OpenAI 兼容端点在写 Agent 代码之前先把模型通道打通。SpringAI 的OpenAiChatModel需要一个 base-url、一个 api-key 和一个 model 名称。传统做法是每个供应商配一套切换模型就得改配置重启。TaoToken 的思路是提供一个统一的 OpenAI 兼容入口你只维护一份 Key模型名按需切换。先拿到 Key。访问 TaoToken 控制台创建 API Key地址是https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentconsole。创建后复制那串sk-开头的字符串后面配置里要用。如果你还没注册官网入口在https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contenthome注册流程不复杂这里不展开。Key 拿到后API 端点固定为https://taotoken.net/api注意这个地址不带任何查询参数。它兼容 OpenAI 的/v1/chat/completions协议所以 SpringAI 的 OpenAI starter 可以直接对接不需要写自定义的ChatModel实现。这里要澄清一个常见误解TaoToken 不是“中转”或“代理”它是一个统一的模型调用通道帮你把多个 OpenAI 兼容模型的接入收敛成一份配置。你在application.yml里只需要写一个 base-url 和一个 api-key换模型时只改model字段。这对 ReAct Agent 特别重要——因为 Agent 循环里每一步都可能调用模型如果模型端点不稳定或配置分散排查问题会非常痛苦。关于模型选择ReAct Agent 对模型的“工具调用”能力有要求。不是所有模型都擅长结构化地返回tool_calls。建议选支持 function calling 的模型比如 Claude 系列或 GPT 系列。具体哪个模型 ID 可用可以在模型对话页面实测https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel-chat。在页面里发一句“调用工具查天气”之类的测试看它是否返回结构化的工具调用意图。如果你打算长期跑编码类或 Agent 类任务可以了解下 Coding Plan地址是https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-plan。它面向的是持续性的编码和 Agent 场景和本文这种一次性接入是互补的。接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc里面有完整的端点说明和参数列表。API Keys 管理页在https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi-keysKey 泄露或轮换时来这里重新生成。3. 可复制配置application.yml 与 Agent 装配片段这一节是全文的核心直接给可复制的配置。先看application.yml这是 SpringAI 对接 TaoToken 的关键spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY:sk-你的Key} chat: options: model: claude-3-5-sonnet-20241022 temperature: 0.7 max-tokens: 4096三个字段对应三件套base-url是 TaoToken 的 API 端点api-key是你的统一 Keymodel是模型 ID。建议把 Key 放到环境变量TAOTOKEN_API_KEY里不要硬编码进仓库。temperature对 Agent 场景建议调低一点0.3 到 0.7 之间太高会让模型的工具选择变得随机。接下来是 Agent 的装配。先定义状态枚举这是循环控制的基础package com.example.agent; public enum AgentState { IDLE, // 空闲可接受新任务 RUNNING, // 运行中 FINISHED, // 完成 ERROR // 异常 }然后是BaseAgent它定义执行循环的骨架。核心是run()方法里的 while 循环循环条件是“步数没超 且 状态不是 FINISHED”package com.example.agent; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.UserMessage; import java.util.ArrayList; import java.util.List; Data Slf4j public abstract class BaseAgent { private String name; private String systemPrompt; private String nextStepPrompt; private AgentState state AgentState.IDLE; private int currentStep 0; private int maxSteps 10; private ChatClient chatClient; private ListMessage messageList new ArrayList(); public String run(String userPrompt) { if (this.state ! AgentState.IDLE) { throw new RuntimeException(Cannot run agent from state: this.state); } this.state AgentState.RUNNING; this.messageList.add(new UserMessage(userPrompt)); ListString results new ArrayList(); try { while (currentStep maxSteps state ! AgentState.FINISHED) { log.info(Executing step {}/{}, currentStep, maxSteps); String stepResult step(); results.add(Step currentStep : stepResult); currentStep; } if (currentStep maxSteps) { state AgentState.FINISHED; results.add(Terminated: Reached max steps ( maxSteps )); } return String.join(\n, results); } catch (Exception e) { state AgentState.ERROR; log.error(error executing agent, e); return 执行错误 e.getMessage(); } finally { this.clean(); } } public abstract String step(); protected void clean() { // 子类可重写做资源释放 } }ReActAgent把step()拆成think()和act()落实 ReAct 模式package com.example.agent; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; EqualsAndHashCode(callSuper true) Data Slf4j public abstract class ReActAgent extends BaseAgent { public abstract boolean think(); public abstract String act(); Override public String step() { try { boolean shouldAct think(); if (shouldAct) { return act(); } return think finish, not do anything; } catch (Exception e) { log.error(单个操作执行失败, e); return 单个操作执行失败 e.getMessage(); } } }ToolCallAgent是最关键的一层它用 SpringAI 的工具调用能力实现think()和act()。注意构造器里那行internalToolExecutionEnabled(false)这是夺回循环控制权的开关package com.example.agent; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.ToolResponseMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.tool.ToolCallingChatOptions; import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.ai.model.tool.ToolExecutionResult; import org.springframework.ai.tool.ToolCallback; import org.springframework.ai.tool.ToolCallbackProvider; import java.util.List; import java.util.stream.Collectors; EqualsAndHashCode(callSuper true) Data Slf4j public class ToolCallAgent extends ReActAgent { private final ToolCallback[] availableTools; private final ToolCallbackProvider toolCallbackProvider; private ChatResponse toolCallChatResponse; private final ToolCallingManager toolCallingManager; private final ChatOptions chatOptions; public ToolCallAgent(ToolCallback[] availableTools, ToolCallbackProvider toolCallbackProvider) { super(); this.availableTools availableTools; this.toolCallbackProvider toolCallbackProvider; this.toolCallingManager ToolCallingManager.builder().build(); this.chatOptions ToolCallingChatOptions.builder() .toolCallbacks(this.availableTools) .internalToolExecutionEnabled(false) // 关键禁用框架托管 .build(); } Override public boolean think() { if (StrUtil.isNotBlank(getNextStepPrompt())) { getMessageList().add(new UserMessage(getNextStepPrompt())); } Prompt prompt new Prompt(getMessageList(), this.chatOptions); try { ChatResponse chatResponse getChatClient().prompt(prompt) .system(getSystemPrompt()) .tools(availableTools) .tools(toolCallbackProvider) .options(chatOptions) .call() .chatResponse(); this.toolCallChatResponse chatResponse; AssistantMessage assistantMessage chatResponse.getResult().getOutput(); ListAssistantMessage.ToolCall toolCalls assistantMessage.getToolCalls(); log.info(getName() 的思考 assistantMessage.getText()); log.info(getName() 选择了 toolCalls.size() 个工具来使用); if (CollUtil.isEmpty(toolCalls)) { getMessageList().add(assistantMessage); return false; } return true; } catch (Exception e) { log.error(getName() 在思考过程中遇到了问题 e.getMessage()); getMessageList().add(new AssistantMessage(处理时遇到了问题 e.getMessage())); return false; } } Override public String act() { if (!toolCallChatResponse.hasToolCalls()) { return 不需要调用工具; } Prompt prompt new Prompt(getMessageList(), chatOptions); ToolExecutionResult toolExecutionResult toolCallingManager.executeToolCalls(prompt, toolCallChatResponse); setMessageList(toolExecutionResult.conversationHistory()); ToolResponseMessage toolResponseMessage (ToolResponseMessage) CollUtil.getLast(toolExecutionResult.conversationHistory()); boolean terminateToolCalled toolResponseMessage.getResponses().stream() .anyMatch(response - response.name().equals(doTerminate)); if (terminateToolCalled) { setState(AgentState.FINISHED); } String results toolResponseMessage.getResponses().stream() .map(response - 工具 response.name() 返回的结果 response.responseData()) .collect(Collectors.joining(\n)); log.info(results); return results; } }最后是LoveManus装配工具、Prompt 和模型package com.example.agent; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.tool.ToolCallback; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; Component public class LoveManus extends ToolCallAgent { public LoveManus(ToolCallback[] toolCallbacks, ToolCallbackProvider toolCallbackProvider, Qualifier(openAiChatModel) ChatModel deepChatModel) { super(toolCallbacks, toolCallbackProvider); this.setName(LoveManus); this.setSystemPrompt( You are LoveManus, an all-capable AI assistant, aimed at solving any task presented by the user. You have various tools at your disposal that you can call upon to efficiently complete complex requests. ); this.setNextStepPrompt( Based on user needs, proactively select the most appropriate tool or combination of tools. For complex tasks, you can break down the problem and use different tools step by step to solve it. After using each tool, clearly explain the execution results and suggest the next steps. If you want to stop the interaction at any point, use the terminate tool/function call. ); this.setMaxSteps(50); ChatClient chatClient ChatClient.builder(deepChatModel) .defaultAdvisors(new MyLoggerAdvisor()) .build(); this.setChatClient(chatClient); } }三件套在这里齐了base-url和api-key在application.ymlmodel也在application.ymlAgent 通过Qualifier(openAiChatModel)注入。换模型只改 yml 里的model字段代码不动。4. 验证请求确认规划-执行-观察循环跑通配置写完怎么确认循环真的跑通了写一个测试给一个需要多步才能完成的复合任务SpringBootTest class LoveManusTest { Resource private LoveManus loveManus; Test void run() { String userPrompt 帮我找 3 个适合周末徒步的北京近郊路线 整理成一份带路线简介的清单并以 PDF 格式输出 ; String answer loveManus.run(userPrompt); Assertions.assertNotNull(answer); System.out.println(answer); } }启动后观察控制台日志。一个跑通的循环应该长这样Executing step 0/50 LoveManus的思考需要先搜索北京近郊徒步路线 LoveManus选择了1个工具来使用 工具 WebSearchTool 返回的结果找到 5 条候选路线... Executing step 1/50 LoveManus的思考需要进一步了解每条路线的难度和交通 LoveManus选择了1个工具来使用 工具 WebScrapingTool 返回的结果路线A 难度中等地铁可达... Executing step 2/50 LoveManus的思考信息够了整理成 PDF LoveManus选择了1个工具来使用 工具 PdfGenerationTool 返回的结果PDF 已生成到 /output/hiking.pdf Executing step 3/50 LoveManus的思考任务已完成 LoveManus选择了1个工具来使用 工具 doTerminate 返回的结果任务结束看到doTerminate被调用、状态置为FINISHED、while 循环退出就说明规划-执行-观察循环跑通了。整个过程没有用户介入模型自主完成了“搜索 → 抓取 → 生成 PDF → 结束”的链路。如果日志里只出现一次Executing step 0/50就停了说明think()返回了 false模型没选择任何工具。这时候检查两件事一是nextStepPrompt有没有正确注入二是模型是否支持工具调用。可以到模型对话页面单独测一下模型的 function calling 能力。验证模型本身是否正常可以在模型对话页发一条带工具描述的请求看返回里有没有结构化的tool_calls。如果模型对话页正常但 Agent 里不正常问题多半在 SpringAI 的配置或工具注册上。5. 本篇常见错排查401、local proxy failed 与 reading choices跑 Agent 的过程中报错基本集中在这几类。逐个对照排查。401 Unauthorized。这是最常见的。原因通常是api-key没配对或者环境变量TAOTOKEN_API_KEY没生效。检查application.yml里的api-key是否写成了${TAOTOKEN_API_KEY:sk-你的Key}冒号后面是默认值。如果你在 IDE 里跑确认环境变量真的注入了。还有一种情况是 Key 被复制时带了空格或换行重新从 API Keys 页面复制一次。401 的排查顺序先确认 Key 字符串本身再确认环境变量最后确认 base-url 有没有写错。local proxy failed / Connection refused。这个报错说明 SpringAI 尝试连接base-url时失败了。检查base-url是不是写成了https://taotoken.net/api注意不要多加/v1或结尾斜杠。SpringAI 的 OpenAI starter 会自动拼接/v1/chat/completions你多写一层路径就会 404 或连接失败。另外确认你的网络能正常访问这个域名公司内网如果有出站限制需要放行。Error reading choices / 返回体解析失败。这个报错通常出现在模型返回了非标准格式或者你用的模型不支持 OpenAI 兼容协议。SpringAI 期望的响应体里有choices数组如果模型端点返回的是别的结构就会解析失败。解决办法是确认model字段填的是 TaoToken 支持的模型 ID并且该模型走的是 OpenAI 兼容协议。可以先用 curl 直接打一次接口看返回体结构curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-3-5-sonnet-20241022, messages: [{role: user, content: hi}] }如果 curl 返回正常但 SpringAI 报错问题在配置映射上。如果 curl 也报错问题在 Key 或模型 ID。OAuth / 认证方式不匹配。有些模型端点要求 OAuth 而非 Bearer Token。TaoToken 走的是标准 Bearer 认证所以api-key直接填sk-开头的 Key 即可。如果你看到 OAuth 相关的报错检查是不是误配了别的认证方式或者api-key字段被写成了别的名字。工具调用返回空 / think 一直返回 false。这不是报错但循环跑不起来。原因通常是模型不支持 function calling或者工具 Schema 没正确注册。检查think()里.tools(availableTools)和.tools(toolCallbackProvider)是否都传了以及availableTools数组是否非空。如果工具是空的模型自然没得选。消息上下文重复膨胀。如果你发现 Token 消耗异常快检查act()里是不是用了addAll而不是setMessageList。executeToolCalls返回的conversationHistory()已经包含助手消息和工具响应直接整体替换即可追加会导致每轮翻倍。排查时建议打开MyLoggerAdvisor把每次 LLM 调用的请求和响应都打出来。这样能清楚看到模型返回了什么、工具执行了什么。日志级别调到 DEBUGSpringAI 的OpenAiChatModel会输出请求体对照着看很快能定位。6. 把循环控制权握在自己手里写到这里一个能跑的 ReAct Agent 就成型了。回头看最关键的一步不是写循环而是那行internalToolExecutionEnabled(false)。SpringAI 默认的自动工具调用很省事但它把“请求工具 → 执行工具 → 回填结果 → 再请求”整条链路封成了黑盒。自主规划需要在这条链路的每一步插入观察、判断和终止逻辑所以必须把控制权拿回来。TaoToken 在这里的角色是让模型通道变简单。一份 Key、一个 base-url换模型只改model字段。Agent 循环里每一步都调模型通道稳定、配置集中排查问题时才不会被“到底是模型问题还是配置问题”绕晕。最后给一个实用建议maxSteps别设太大50 已经够跑复杂任务了。再大只会让失控时烧更多 Token。配合doTerminate工具让模型自己决定收手比硬性截断优雅得多。跑通之后你可以试着加一个AskHuman工具让模型在关键决策点停下来问你——这是从“全自动”走向“可控自主”的下一步。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表