
“或跃在渊”这句话放到 Agent 开发这事儿上莫名贴切。九四爻辞讲龙进退有度、伺机而动这不就是 ReAct 模式下大模型该有的状态——先想清楚再动手观察结果继续想。我这是在“降SpringAI阿里”系列第九篇里借着 ReActAgent 这个项目把一套真正能落地的智能体搭建思路完整捋一遍。如果你正在用 Java 做 AI 应用想搞懂 Spring AI Alibaba 里的 Agent 到底怎么用、ReAct 模式怎么从概念变成代码这篇就是给你准备的。1. 内容整体设计与思路拆解1.1 从“ReAct”这个名字说起模型不是机器是带脑子的执行者很多朋友第一次看到“ReactAgent”会误以为是 React 前端框架相关的机器人实际上这里指的是 ReAct 模式下的 Agent。ReAct 是 Reasoning Acting 的缩写核心想法很简单让大模型在解决问题的过程中交替进行“推理”和“行动”。想象一个场景你请助理帮忙安排一场跨城出差助理不会不说一句话直接下单订票而是先分析——你要去哪个城市、什么时间出发、预算多少、有没有偏好航班——然后决定第一步先查航班信息拿到结果后再决定第二步订哪一班最后还要跟你确认。ReAct 模式下的 Agent 就是这样一个助理大模型是那个“大脑”负责分析、思考、决策工具调用是那个“手”负责查天气、查数据库、调接口、写文件。在结构化实现上这个循环是接收用户请求生成初始推理。模型判断是否需要调用工具如果不需要直接输出最终答案。如果需要从模型返回的函数调用请求中解析出工具名和参数。执行对应工具拿到结构化结果。将工具结果反馈给模型让模型结合结果继续推理。这个循环会一直持续到模型给出最终答复或者达到最大轮数限制为止。1.2 为什么Spring AI Alibaba适合做这件事Spring AI Alibaba 是阿里云基于 Spring AI 规范做的适配层最大的价值在于你只需要写很少的配置和代码就能把通义千问系列模型接入 Spring 应用并且用上 Spring AI 标准化的 ChatClient API。和裸调模型 API 相比它解决了三个核心痛点。首先是依赖管理。框架内部封装了模型服务接入、错误重试、流式响应、工具描述生成等底层逻辑不需要你自己处理 JSON Schema 的拼接和转换也不用关心 API 签名细节。其次是Spring 生态融合。Tool 注解可以直接把 Spring Bean 的方法暴露成工具函数服务类可以注入其他 Bean天然支持依赖注入、配置管理、日志链路这些成熟机制。这意味着你可以在 Agent 里直接调用已有的业务服务比如订单查询、库存判断、用户画像模块Agent 成为业务系统的“对话式网关”。最后是可扩展性。DashScope 模型里对 function calling 的支持比较成熟Spring AI Alibaba 把这种能力抽象成统一的 Tool Calling 机制后续想换其他模型或者加新工具改动成本都很小。这也是我在系列文章里逐步推进到这一篇的原因。前面的内容更多是讲模型接入和提示词工程到了 ReactAgent 这一掌相当于从“能对话”跨进了“能干活”的阶段。别小看这一步它决定了你的 AI 应用是聊天玩具还是真正能解决业务问题的助手。2. 核心细节解析与实操要点2.1 Agent 运行时的三大核心组件要搭建一个可用的 ReactAgent你需要先理清三个组件各自扮演的角色。它们组合在一起才构成完整的 Agent 运行链路。ChatClient这是 Spring AI Alibaba 中跟模型打交道的一等公民入口。所有提示词组装、工具注册、调用模式同步/流式都是围绕它来配置的。你可以把 ChatClient 理解为“请求分发器”每个 Agent 请求都会经过它带着系统提示词和用户消息去问模型。Tool 注解与工具注册器工具是 Agent 的“行动清单”。通过 Tool 注解标注方法后Spring AI Alibaba 会在应用启动时扫描这些 Bean自动生成大模型可识别的函数描述 JSON并在每次请求时把这些描述注入到系统消息中。模型通过阅读描述决定是否调用、调用哪个工具、传什么参数。ChatMemoryReAct 循环中模型需要感知“刚才已经查到了什么现在还需要什么”。ChatMemory 就是把每一轮推理结果、工具返回结果按顺序存起来在下一轮请求时一并发给模型。没有它模型每次都是“失忆”状态无法完成多轮推理任务。这三者的关系可以这样理解ChatClient 是通道工具是能力ChatMemory 是记忆。缺了任何一环ReActAgent 都跑不起来。2.2 ReAct 循环中的消息组装策略实现 ReAct Agent 时最容易出问题的地方往往不是模型本身而是消息上下文的组装方式。Spring AI Alibaba 在底层帮你处理了大部分消息转换但你仍然需要理解“当前这一轮模型看到的到底是什么”。ReAct 的一个标准循环在消息层面大致是这样展开的系统消息包含角色定义、任务规则、工具描述。用户消息初始问题。模型返回内容模型输出的推理文本或者一段 function call 请求。工具消息工具执行结果通常以 ToolResponseMessage 的形式追加到上下文中。模型再次返回基于工具结果继续推理或输出最终答案。关键点在于每一轮模型请求发送给模型的消息列表必须是累积的完整对话历程而不是只发最后一轮的内容。如果只把最新工具结果发给模型模型会丢失之前的推理链路很容易出现“明明刚查过北京天气又问北京天气”的诡异行为。我在实际项目中会维护一个 ArrayList 作为对话窗口每一轮交互之后把模型输出、工具调用记录、工具结果都 add 进去。这样做还有个额外好处方便审计。整个 Agent 的推理过程一目了然出了问题可以直接回溯。2.3 工具描述与参数设计的门道工具描述写得好不好直接决定模型“会不会用、用得对不对”。有句话说得很真实给模型的工具描述就像面试官面前的简历描述写得含糊面试官模型就会瞎猜。我总结了几条比较实用的经验工具名用动词短语比如query_weather比weather更清晰calculate_delivery_fee比fee更明确。描述里写清“什么时候该用”不要只写“查询天气”而要写“当用户询问某城市的天气情况时使用参数需要完整城市名称”。参数说明要带约束比如“城市名称必须是中文全称如北京不能用缩写BJ”。模型在解析参数时会优先遵循这类显式约束。返回值格式要尽量结构化无论是 JSON 还是纯文本要让模型容易从结果里提取关键信息。比如返回{city:北京,temp:25,condition:晴}模型能直接引用这些字段生成答案如果返回一坨无规律文本模型往往要猜。3. 实操过程与核心环节实现3.1 项目初始化与依赖配置这一步我会直接给出可复制的配置方式。假设你用的是 Spring Boot 3.2.x 或者更高版本JDK 17Maven 构建。在 pom.xml 里加入dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId version1.0.0/version /dependency然后在 application.yml 中配置模型接入参数spring: application: name: react-agent-demo ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.7这里有个细节值得提一下api-key 不要写在配置文件里用环境变量占位符避免密钥泄露。qwen-plus 在 function calling 方面表现稳定如果你预算有限qwen-turbo 也可以但复杂推理任务建议还是用 qwen-plus。3.2 定义一个真正干活的 Agent Service下面是一个完整的 Agent 服务类结合了工具注册和 ReAct 循环处理。为了简洁我抽掉了具体业务实现保留核心链路。Service public class ReactAgentService { private final ChatClient chatClient; public ReactAgentService(ChatClient.Builder builder) { this.chatClient builder .defaultSystem( 你是一个乐于助人的智能助手能够调用工具完成用户请求。 请严格按照以下步骤思考 Thought: 分析用户需求判断是否需要调用工具。 Action: 如果需要使用工具获取信息每次只调用一个工具。 Observation: 观察工具结果。 ...重复以上步骤... 当你确定已有足够信息回答用户时直接输出最终答案。 ) .build(); } public String execute(String userMessage) { // 维护完整上下文 ListMessage messages new ArrayList(); messages.add(new UserMessage(userMessage)); int maxIterations 5; for (int i 0; i maxIterations; i) { ChatResponse response chatClient.prompt() .messages(messages) .tools(new Object[] { new OrderTool(), new WeatherTool() }) .call(); String content response.getResult().getOutput().getText(); // 如果模型直接输出了答案 if (content ! null !content.isBlank()) { return content; } // 获取模型请求的工具调用 ListFunctionCallingToolResponse toolCalls response.getResult().getOutput().getToolCalls(); if (toolCalls.isEmpty()) { // 模型既没有输出文本也没有请求工具说明异常直接返回提示 return 抱歉我暂时无法处理这个请求。; } for (FunctionCallingToolResponse toolCall : toolCalls) { String toolName toolCall.name(); String arguments toolCall.arguments(); // 调用对应工具并构造 ToolResponseMessage String result invokeTool(toolName, arguments); messages.add(new ToolResponseMessage(toolName, result)); } } return 经过多轮尝试后仍未得到结果请调整问题描述后重试。; } private String invokeTool(String toolName, String arguments) { // 这里根据 toolName 分发到具体的工具 Bean 方法 return 工具调用结果; } }这段代码演示了两个核心点最大迭代限制避免模型陷进死循环和手动消息累积每一轮工具结果都追加到上下文。你别小看这两点缺了任何一个Agent 都可能变成“脱缰野马”。Spring AI Alibaba 新版本里Tool 方法可以直接注入 Spring Bean所以我更推荐把工具也做成品类清晰、职责单一的服务类然后在 execute 里通过 ApplicationContext 动态获取。这样每个工具的逻辑都独立测试起来也方便。3.3 工具定义实战从查询天气到调用业务接口用一个常见场景来演示工具定义。假设你的 Agent 需要给用户提供“目的地天气 穿衣建议”你就要注册一个天气工具。Component public class WeatherTool { Tool(name query_weather, description 查询指定城市的实时天气情况当用户询问某地天气、温度、是否适合出行时使用) public String queryWeather( ToolParam(description 城市中文全称例如杭州) String city) { // 调用第三方天气接口或者本地数据源 WeatherInfo info weatherApi.getByCity(city); return 城市 city 天气 info.getCondition() 温度 info.getTemperature() ℃; } }实际生产环境中工具不一定只是查天气。我在一个订单场景里Agent 需要调用订单系统的查询接口来判断用户订单状态这时候工具的返回值应该直接封装成 JSON 字符串返回方便模型提取。Tool(name query_order_status, description 查询用户的订单配送状态当用户询问订单进度、物流信息时使用) public String queryOrderStatus(ToolParam(description 订单编号) String orderId) { OrderDO order orderService.getByOrderId(orderId); return new ObjectMapper().writeValueAsString(Map.of( orderId, order.getOrderId(), status, order.getStatus(), expectedArrival, order.getExpectedArrival() )); }有一点要切记工具方法内部不要做太复杂的业务逻辑最好只做“数据获取”这一件事。模型需要的是清晰、可信、能被再次解读的结果如果你在工具里塞了十几层判断最终返回一句模棱两可的提示模型反而不知道该信什么。3.4 配置多轮对话记忆与流式输出ReAct Agent 如果只做单轮问答很多价值发挥不出来。生产环境中用户往往会追问“杭州天气怎么样那上海呢”这种场景下Agent 需要记住上一轮讨论的城市。Spring AI Alibaba 提供了 ChatMemory 接口可以直接基于内存或者 Redis 来存储历史会话。最简单的接入方式Service public class ChatAgentService { private final ChatClient chatClient; private final ChatMemory chatMemory; public ChatAgentService(ChatClient.Builder builder, ChatMemory chatMemory) { this.chatClient builder.build(); this.chatMemory chatMemory; } public String chat(String sessionId, String userMessage) { // 获取或创建会话 ChatMemory memory this.chatMemory; memory.add(sessionId, List.of(new UserMessage(userMessage))); String response chatClient.prompt() .chatMemory(memory, sessionId) .tools(...) .call() .getResult().getOutput().getText(); memory.add(sessionId, List.of(new AssistantMessage(response))); return response; } }流式输出也非常推荐特别是生成较长回复时用户不用盯着转圈等全部生成完。Spring AI Alibaba 里用.stream()返回 FluxFluxString flux chatClient.prompt() .user(userMessage) .stream() .content();把流式输出跟 ReAct 循环结合时有个技巧流式输出一般用于最终答案阶段。工具调用过程建议还是用同步模式一步一步拿结果等确认 Agent 已经准备输出最终答复时再切到流式给用户展示。这样既保证了循环逻辑的可控性又兼顾了交互体验。4. 常见问题与排查技巧实录4.1 死循环模型反复调用同一个工具这是 ReAct Agent 最经典的坑。模型有时候会“钻牛角尖”拿着工具结果继续调同一个工具或者工具返回错误信息后模型不理解还尝试用相同参数再调一次。我的排查思路很简单给工具调用加上日志和计数器。每次工具调用打印出轮次、工具名、参数。如果发现同一工具在超3轮里重复出现基本就是下面三种情况之一一是工具返回结果太模糊模型不知道该信什么。解决方法是把返回值写得更明确比如“查询失败原因是城市不存在请尝试其他城市”。二是系统提示词里没有说明“一次最多调用一个工具结果足够时直接回答”。把这句话加进去模型通常就不会再多轮折腾。三是温度参数设置太高模型在随机性中打转。把 temperature 降到 0.5 以下能明显减少不必要的发散推理。更稳妥的办法是硬性限制。就是我在前面代码里写的 maxIterations固定轮数上限超了就终止。这个限制是 Agent 工程里必须有的“安全带”。4.2 工具参数解析错误模型传了奇怪的值模型生成的工具参数是基于 JSON 的偶尔会出现类型不匹配、字段缺失、传入空字符串的情况。比如你要求城市名传中文它传了个“beijing”或者直接传{}空对象。处理思路有两种。第一种是在工具方法里做容错比如if (city null || city.isBlank()) { return 城市参数为空请提供城市中文名称; }第二种是优化描述。把参数约束写得更直白比如“必须提供具体的城市名禁止使用拼音或英文”。别觉得这是在小题大做实测下来描述里多一句约束参数错误率能明显下降一个量级。还有一种兜底办法自己写一个 JSON Schema在框架默认描述的基础上附加正则校验。Spring AI Alibaba 的 ToolParam 注解目前还支持有限但你可以通过工具方法的入参校验来实现同样的效果。4.3 上下文爆炸多轮工具调用后消息超长模型上下文窗口有限即使是最长的模型也有上限。ReAct 循环每多一轮就会多出模型推理文本、工具结果等一堆 token。用户问题再稍微长一点很容易在第五六轮把上下文挤爆。我在生产项目里的做法是设定上下文窗口上限比如最多保留最近10条消息更早的可以压缩成摘要。工具结果做截断只保留关键字段比如状态、数值、简短描述不要返回几百行的原始数据。如果确实需要长文档信息考虑走 RAG 而不是让 Agent 自己去读全文。这里可以简单算一笔账假设每轮工具调用消耗约800 token5轮就是4000 token加上系统提示词、工具描述、用户原问题一次完整 Agent 执行可能消耗 6000 以上的 token。用 qwen-plus 勉强够用但如果你是逐字把整份合同丢进上下文让模型“自己看”那个量级会迅速爆掉。精简单上下文既是性能优化也是成本优化。4.4 工具执行报错导致 Agent 链路中断有时候工具业务代码本身会抛异常比如下游接口挂了、数据库超时如果不处理异常会直接把 ReAct 循环打断用户就看到一个 500 错误。我的建议是在工具方法内部就把异常“消化”成模型能理解的文本。比如 catch 住异常返回“查询订单系统异常请稍后重试”这类信息。这样模型会把这句当成工具执行结果结合场景给用户一个合理的答复甚至自动换个工具试试。但要注意千万不要把原始异常堆栈直接返回给模型。堆栈里有大量无用信息模型很难从其中提炼有用信号而且可能泄露内部实现细节。5. 从 demo 到生产的进阶建议5.1 给工具调用加观测与审计生产环境里的 Agent任何一个决策失误都可能带来真实影响。要不要给用户退款、要不要锁定库存这种动作如果 Agent 自己判断错了代价不小。我建议在 Agent 执行链路里埋好观测点。至少要做到记录每一轮模型的 Thought、Action、Observation。记录工具调用的入参和出参并对敏感字段脱敏。对 Agent 执行耗时、token 消耗做 metrics 上报。对于高影响的工具涉及资金、权限、隐私的操作增加人工确认环节Agent 生成动作后先挂起等人点确认再执行。这一步不只是为了保护自己也是建立 Agent 信任感的基础。用户不敢用黑盒子做决策但一套行为透明、可回溯的 Agent即使犯了错也能让问题定位和修正变得清晰。5.2 多工具协作与动态工具选择ReAct Agent 的魅力在于工具可以越来越多。你可以注册一个查天气的、一个查订单的、一个发消息的、一个算运费的……但工具太多以后模型反而会出现选择困难。解决办法是给工具打标签在描述里写清适用场景和优先级。比如同一件事有两个工具都能做就要明确“优先使用 A”。另外也可以在系统提示词中增加“工具决策规则”段落比如“当用户查询订单状态时直接使用 query_order_status不要尝试从其他工具中推断”。如果你想让 Agent 能动态决定启用哪些工具可以结合模型返回的文本做二次路由先让模型分析用户意图再由你的代码根据意图把对应工具集合塞给 ChatClient。这也是一种很实用的“动态工具选择”模式。5.3 从单 Agent 到多 Agent 协调当你把 ReactAgent 的逻辑跑通之后下一步自然是想拆多个 Agent 协作一个当主管负责拆解任务两个下属 Agent 分别处理不同领域的问题。Spring AI Alibaba 对多 Agent 编排也提供了支持但我的建议是先别急着上编排框架。先把单个 Agent 的工具能力打磨扎实把错误处理、上下文管理、观测机制都做顺了。多 Agent 的本质还是每个 Agent 内部跑同样的 ReAct 循环只是消息路由和结果汇聚的复杂度上来了。基础不牢Agent 多了只会让问题更多。我在实际项目里的习惯是单 Agent 能解决的绝不用多 Agent必须有多个领域专家系统时才考虑拆分。因为每多一个 Agent就多一层上下文消耗、多一套失败链路、多一堆需要排查的状态。“或跃在渊”这四个字我越来越觉得不只是讲龙的状态也是在讲 Agent 的开发节奏。模型在渊中蓄力思考清楚了才跃出水面调用工具工具结果回来了它又回到渊中继续权衡。这套循环本身就有某种“易经的智慧”在里面。我们做工程的人与其追逐花哨的框架和名词不如先把这“一思一行”的循环做扎实。最后说个私藏的调优习惯把 maxIterations 从 5 改成 8从 8 改成 3分别跑同一批测试问题记录成功率、平均耗时、token 消耗你会发现一个很明显的拐点。超出拐点之后增加轮数只会徒增开销。每个业务场景的拐点都不一样但跑过一轮数据你心里就有数了Agent 应该有多少耐心既不是模型说了算也不是产品经理拍脑袋说了算是数据说了算。