资讯详情

Spring AI实现自主决策Agent:从Function Calling到ReAct实战

📅 2026/10/20 22:46:05 | 华诺云谱 👁 阅读
Spring AI实现自主决策Agent:从Function Calling到ReAct实战
Spring AI实现自主决策Agent这题我会。先说结论这套方案的落地难度没有想象中高但坑是真不少。我前前后后改了三版从最初单纯调大模型接口到最后跑通一个能自己决定下一步调用什么工具、什么时候返回结果的Agent中间踩过的坑足够写一篇长文了。这篇文章就围绕自主决策四个字展开讲清楚Spring AI在这中间到底扮演了什么角色以及一套完整的、能直接抄作业的实现路径。1. 先搞清楚自主决策Agent到底解决什么问题如果你只是让大模型做一问一答式的对话压根不需要Agent。举个最常见的场景用户问帮我查一下上个季度的订单总量顺便把前三名客户的名单列出来。如果只接一个Chat模型模型能做的只有两件事——要么凭训练数据瞎编一个数字要么告诉你我没有访问您的数据库的能力。这不是模型不够聪明是它的能力边界被锁死了。模型本身不联网、不访问内部系统、不会执行代码它的所有知识都停留在训练截止日期之前。而自主决策Agent的核心价值就是给模型装上手和脚让它能主动调用外部工具拿到真实的、最新的数据再基于这些数据完成推理和回答。Spring AI在这个架构里的角色是一个胶水层。它负责三件事把大模型的接口统一封装成Spring风格的工具把业务系统里的方法暴露成模型可以调用的工具把整个对话过程中的工具调用链路由框架接管开发者只需要关注业务逻辑本身。我用一个生活化的类比来解释如果把大模型比作一个聪明但手无缚鸡之力的实习生Agent框架就是给这个实习生配了一个秘书。秘书负责帮他打电话、查资料、跑腿实习生只负责动脑子做决策。Spring AI就是这个秘书的中枢神经系统——让实习生能顺畅地指挥秘书干活。这也是为什么选择Spring AI而不是直接裸调大模型API裸调API时你要自己维护对话历史、自己解析工具调用的返回结构、自己处理多轮工具调用的状态管理这些脏活累活Spring AI都替你做了而且做得还挺稳。2. 自主决策的技术底座Function Calling与ReAct模式要理解Spring AI是怎么实现自主决策的必须先搞清楚两个底层机制Function Calling和ReAct模式。这两个词是Agent能自主起来的灵魂。2.1 Function Calling给模型一把瑞士军刀Function Calling的思路很直接你在调用模型时除了发送用户消息还附带一份工具清单清单里写明每个工具的名字、功能描述、参数结构。模型在理解用户请求后会自己判断这个问题需要调用某个工具然后输出一个结构化的调用指令——注意模型此时并没有真正执行工具它只是提出请求。真正的执行动作发生在你的代码里。模型返回我想调用queryOrderCount(2024年Q3)你的程序收到这个指令后去数据库里跑一遍SQL把结果再塞回给模型。模型拿到真实数据后再进行下一步推理生成最终回答。这个过程的关键在于模型自己决定要不要调用工具、调用哪个工具、传什么参数。开发者不需要写一堆if-else分支去判断用户意图只需要定义好工具剩下的决策全部交给模型。2.2 ReAct模式让模型想一步做一步如果一次只能调用一个工具那还算不上自主决策。真正的Agent像是一个有耐心的侦探它需要的是思考-行动-观察-再思考的循环。ReAct模式Reasoning Acting就是干这个的。具体流程是模型接到一个复杂任务先拆解出第一步需要什么信息决定调用某个工具并生成参数程序执行工具返回结果模型观察结果判断是否还需要更多信息如果需要继续调用下一个工具如果信息齐了就生成最终回答。Spring AI框架对这套循环的调度是通过内部的ToolCallingManager来完成的。我们不用手动写while循环去反复调用模型Spring AI的ChatClient会帮我们自动完成多轮工具调用直到模型认为可以给出最终答案为止。这一步的实现细节很值得说Spring AI在每次工具调用完成后会把工具返回的结果追加进对话历史然后重新发给模型。这个过程是自动的但你需要理解它——因为后面排查问题时定位哪一轮调用出了问题全靠这些概念。3. 从零搭建一个可运行的Agent项目光讲理论没意思直接上实操。我以一个订单查询助手为例子假设它是一个电商系统的内部工具用户可以查询订单数量、客户排名、发货状态。目标是通过Spring AI让大模型自主调用这些查询接口完成用户复杂的问题。3.1 环境依赖与项目结构首先是最基础的Spring Boot工程。我用的版本组合是Spring Boot 3.2.5、Spring AI 0.8.1-SNAPSHOTJDK 17。这里注意Spring AI的版本迭代特别快API变化也大如果你用的是正式发布版命名可能有差异但核心思路一致。dependencies !-- Spring AI 核心依赖 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version0.8.1-SNAPSHOT/version /dependency !-- 如果用通义千问等其他模型换对应starter即可 -- /dependenciesSpring AI的兼容性设计得不错底层的模型接入是高度抽象的。你只需要改依赖和配置代码本身不用动太多就能从OpenAI切换到其他大模型。这算是一个隐藏福利。配置文件中最关键的是模型API的Key和基础地址spring: ai: openai: api-key: ${OPENAI_API_KEY} base-url: ${OPENAI_BASE_URL}这里要提醒一个坑如果你使用的是代理地址或兼容APIbase-url配置务必确认正确。我在调试阶段因为URL多了一个斜杠浪费了半小时排查。3.2 定义Agent的业务工具这个工具类是整个Agent的基础设施。根据我的实践经验建议把工具按照单一职责原则拆分每个方法只做一件独立的事情描述写清楚。你给工具起名和写描述的方式直接影响模型选工具的准确率。先来看一个完整的工具类定义Component Description(订单数据查询工具组) public class OrderTools { private final JdbcTemplate jdbcTemplate; public OrderTools(JdbcTemplate jdbcTemplate) { this.jdbcTemplate jdbcTemplate; } Tool(name queryOrderCount, description 查询指定时间范围内的订单总数支持按季度、月度过滤) public int queryOrderCount(String startDate, String endDate) { String sql SELECT COUNT(*) FROM orders WHERE create_time BETWEEN ? AND ?; return jdbcTemplate.queryForObject(sql, Integer.class, startDate, endDate); } Tool(name queryTopCustomers, description 查询指定时间范围内消费金额排名前N的客户列表返回客户ID和消费金额) public ListCustomerRank queryTopCustomers(String startDate, String endDate, int topN) { String sql SELECT customer_id, SUM(amount) as total FROM orders WHERE create_time BETWEEN ? AND ? GROUP BY customer_id ORDER BY total DESC LIMIT ?; return jdbcTemplate.query(sql, (rs, rowNum) - new CustomerRank(rs.getString(customer_id), rs.getBigDecimal(total)), startDate, endDate, topN); } Tool(name queryOrderStatus, description 根据订单ID查询订单当前状态返回状态码和状态描述) public OrderStatus queryOrderStatus(String orderId) { String sql SELECT status_code, status_desc FROM orders WHERE order_id ?; return jdbcTemplate.queryForObject(sql, (rs, rowNum) - new OrderStatus(rs.getString(status_code), rs.getString(status_desc)), orderId); } }这里用到的Tool注解是Spring AI提供的注解标注一个方法为可被模型调用的工具。标注后Spring AI启动时会自动扫描这个类把方法信息注册到工具注册表中。3.3 构建Agent核心逻辑工具定义好了接下来是Agent的核心入口。这一步的关键是使用ChatClient并通过.system()和.tools()方法完成装配。Service public class OrderAgent { private final ChatClient chatClient; public OrderAgent(ChatClient.Builder builder, OrderTools orderTools) { this.chatClient builder .defaultSystem( 你是一个订单查询助手。你可以查询订单数量、客户排名和订单状态。 请遵循以下规则 1. 如果用户的问题涉及具体数据必须先调用相应工具获取真实数据。 2. 如果用户的提问不够明确比如缺少日期范围请主动向用户询问清楚不要擅自假设。 3. 获取到数据后用简洁清晰的格式向用户展示结果。 4. 如果工具返回的数据为空如实告知用户不要编造。 ) .defaultTools(orderTools) .build(); } public String ask(String userMessage) { return chatClient.prompt() .user(userMessage) .call() .content(); } }这段代码的精华在于.defaultTools(orderTools)。一旦注册了工具ChatClient就进入了Agent模式——当用户问题需要数据时模型会自动决定调用工具不需要时它也会直接回答。0.8.1这个版本里工具调用是自动完成的不需要手动区分到底是直接回复还是调用工具。框架内部已经实现了工具调用的闭环。不过有一点要提醒如果你用的版本是1.0.0-M系列API有调整建议先用我给的版本跑通流程再升级到新版。3.4 工具的注册与发现机制这里值得单独说几句内部的注册机制理解了这个排查问题会轻松很多。Spring AI启动时会扫描带有Tool注解的方法然后通过ToolCallingManager构建工具定义列表随请求一起发给模型。每个工具定义都包含工具名称、描述、参数结构由方法签名自动推导。这里有个重要机制工具方法的参数描述如果直接使用Java类型Spring AI会自动转换成JSON Schema。但如果参数是复杂对象建议在方法上补充描述或者给参数加ToolParam注解ToolParam(description 查询起始日期格式为YYYY-MM-DD) String startDate加了ToolParam后模型对参数的理解会更准确传参的格式错误率会显著下降。我在实践中发现不加这个注解时模型偶尔会把日期传成2024年9月1日这种自然语言格式导致程序解析异常。加了描述后基本杜绝了这类问题。4. 自主决策的调用循环是怎么跑起来的到这一步我们已经有一个能跑出结果的Agent了。但既然标题叫自主决策就有必要深入看一下多轮工具调用的运转流程。光会用还不够你得知道Agent在连续调用多个工具时内部发生了什么。4.1 一次复杂的多工具决策过程拆解假设用户输入帮我看看2024年第三季度的订单总数然后查一下排名前三的客户。第一次请求发给模型附带的信息包括系统提示词、用户问题、工具列表queryOrderCount、queryTopCustomers。此时模型生成的回复并不是最终答案而是一个工具调用请求[TOOL_CALL] 工具名称: queryOrderCount 参数: {startDate: 2024-07-01, endDate: 2024-09-30}Spring AI收到这个工具调用请求通过ToolCallingManager找到对应的方法在本地执行SQL拿到结果后把这个结果以工具响应的形式追加到消息历史中然后再次发给模型。注意此时模型看到的是完整的对话链用户问题、自己的工具调用请求、系统返回的查询结果。它继续推理发现还需要调用queryTopCustomers才能满足排名前三客户的需求于是再次发起工具调用。等第二个工具也执行完模型才有足够的信息生成最终回答。这个模型发起调用-程序执行-结果回传-模型再推理的循环可能持续多轮。Spring AI内部用while循环控制直到模型认为可以生成最终答案为止。从开发者的视角看整个过程一次chatClient.prompt().call()就完成了但实际在后端发生了数次HTTP请求。4.2 多工具决策的结果展示与封装多轮调用完成后最终的回答可能长这样2024年第三季度订单总数为1,256单。 消费金额排名前三的客户分别是 1. 客户A001消费金额28,500元 2. 客户B023消费金额19,200元 3. 客户C107消费金额15,800元模型会把工具结果转化成自然语言这体验比直接把SQL结果丢给用户强太多了。另外需要留意的是如果用户一次提了一个特别复杂的请求比如对比一下上半年和下半年的发货时效模型可能会发起两次同工具的调用一次查上半年数据一次查下半年数据然后自己对比分析。这种能力就是用Function Calling实现自主决策最典型的场景。5. 实操中的性能与参数调优Agent跑通之后下一步要考虑的就是怎么让它既聪明又稳定。这里我分享几个实际调参过程中非常关键的经验。5.1 控制模型自作主张的力度大模型天然有过度自信的倾向。特别是当你给它装了工具之后它有时会想当然地调用工具哪怕用户根本没让查数据。比如用户随口问订单一般多久发货模型可能会自作主张去调用queryOrderStatus但实际上这个问题根本不需要具体订单ID。解决方案有两种我实践下来都有效第一在系统提示词中明确约束工具调用的触发条件。我现在的提示词里特意加了如果用户只是询问一般性知识不需要调用工具请直接回答。这话看起来很朴素但对模型的行为有很强的引导作用。第二调整模型的temperature参数。工具调用场景下我一般设置temperature在0.2以下。温度越低模型的输出越保守越不容易产生天马行空的工具调用。这个参数是通过ChatClient的defaultOptions配置的.defaultOptions(ChatOptions.builder() .withTemperature(0.2) .build())5.2 工具数量与决策质量的平衡这是一个容易被忽视的点。工具不是越多越好。当工具数量超过20个时模型的决策准确率会明显下降——它会经常搞混相似工具的功能选错工具。我的建议是把工具按业务域拆分到不同Agent而不是在一个Agent里塞进所有工具。比如订单Agent只负责订单查询类工具物流Agent只负责物流查询类工具。如果你确实需要跨域能力可以在上层做一个路由Agent让用户先选领域再交给具体的子Agent处理。另外给工具命名时不要用太长的名字也别用容易混淆的近义词。我见过有人把queryUserInfo和getUserProfile同时注册结果模型经常分不清该调哪个。遇到这种情况我一般会合并成一个工具或者在描述里写得更具体让模型能区分。5.3 上下文窗口与长对话场景多轮工具调用非常消耗令牌。一次复杂查询下来光工具调用的中间结果就可能占满几千个令牌。如果你做的是一个长期对话的Agent上下文窗口很容易被撑爆。Spring AI目前不会自动做上下文修剪所以需要你自己处理。我在实际项目中会在每次交互前做一次历史消息裁剪超过一定轮数后把最早的消息从消息历史中移除。有经验的同事可能会问直接截断会不会丢失信息答案是会有一定取舍但对大多数业务场景最近的对话内容才是最关键的早期上下文丢弃影响有限。用ChatClient的API你可以手动控制消息列表把历史消息交给一个MessageWindow来管理。我简单实现过一个滑动窗口ListMessage conversationHistory new ArrayList(); // 每次交互前检查长度 if (conversationHistory.size() 10) { conversationHistory conversationHistory.subList(conversationHistory.size() - 10, conversationHistory.size()); }这个方案虽然糙但在业务系统中足够实用。6. 常见问题与排查技巧实录这部分是我的血泪教训合集。下面这些坑我基本都踩过而且每一个都花了不少时间才定位。6.1 模型完全不调用工具这是最常遇到的问题工具注册了提示词也写了但模型对什么都不调直接凭记忆回答。排查思路确认工具是否被正确扫描注册。我在项目启动时发现过Tool注解方法所在的类没有被Spring扫描到导致工具列表为空。解决方法是给工具类加Component并确保它在启动类扫描路径之下。确认模型是否支持Function Calling。并不是所有模型都支持早期的某些接口是不支持的。如果模型不支持Spring AI会在调用时报错或直接忽略工具定义。确认系统提示词是否对工具有引导。我之前写过一套通用提示词在没有任何提及你可以调用工具获取数据的情况下模型的工具调用频率低得可怜。后来在提示词里明确引导情况立刻好转。6.2 工具被调用但参数错误模型明明调用了工具但传的参数不对。常见的有日期格式传成自然语言、数字参数传成字符串、必填参数漏掉。排查手段主要靠日志。Spring AI默认会输出工具调用相关的日志建议在开发环境把日志级别调低观察模型请求和响应的完整内容logging: level: org.springframework.ai: DEBUG从DEBUG日志里你能清楚看到模型传的原始参数JSON一眼就能定位是格式问题还是语义问题。修复手段主要是给工具描述和ToolParam加更明确的说明。6.3 多轮工具调用后回答质量下降当Agent连续调用3次以上工具时最终回答偶尔会出现逻辑混乱甚至把不同工具的结果搞混。我总结的根因有两个上下文过长模型在长上下文中的注意力分散导致它忘了前面某次工具返回的结果。解决办法是让工具返回结果不要太冗长比如SQL查询结果只返回必要的字段不要一股脑把所有列都塞给模型。工具返回的数据结构描述不清楚。我建议在工具返回的对象类中用清晰的字段名必要时返回给模型时做一次摘要转换只保留核心信息。6.4 并发与线程安全问题Spring AI的ChatClient看起来是无状态的但如果你在工具方法里维护了实例级状态就很容易踩并发坑。比如有些工具方法内部用了一个共享的Map做缓存在高并发下会出问题。我的原则是工具方法尽量做成纯函数只用入参计算不持有状态。如果非要有缓存用线程安全的结构或者干脆放到外部存储。还有一个并发层的坑Spring AI的默认HTTP客户端在连接池不足时会报连接超时。高并发场景下务必配置连接池的大小别用默认值。这个我是在压测时才发现的一度以为是大模型接口的问题排查了半天才发现是本地连接池撑不住了。7. 从单Agent到多Agent协作最后聊一点进阶思路。单个Agent解决的是单一领域的自主决策问题但如果你的系统横跨多个领域比如既要查订单又要查库存还要查物流单Agent的上下文会越来越臃肿决策质量也会下降。我建议的方向是主控Agent 若干子Agent的架构。主控Agent读用户的请求判断属于哪个业务域将请求转发给对应的子Agent。子Agent只知道自己领域的工具专注于本领域的自主决策效率更高。在Spring AI里实现这个架构时可以把每个子Agent的execute能力注册为主控Agent的工具。也就是说子Agent本身变成一个可被调用的工具。这样一来主控就能通过工具调用的方式把任务委托给子Agent。这种嵌套式Agent在调试时比较复杂因为可能涉及两个Agent的多轮工具调用日志会非常长。我的建议是先确保每个子Agent单独运行完全正常再组装成级联架构。另外要特别注意超时控制嵌套调用的响应时间可能会成倍增加给用户的反馈需要做异步化处理。8. 后续值得扩展的方向最后说几个我试验过或者正在尝试的方向。一个是让Agent具备记忆能力。目前的Agent是无状态的用户说过的偏好和历史操作不会保留。可以在工具层加一个memory工具让Agent自己决定是否把某些信息存起来、是否读取历史记录。另一个是给Agent加反馈闭环。比如用户对最终回答点有帮助/无帮助时可以把这个反馈异步记录用于后续优化工具描述和提示词。这虽然是大模型应用里最笨的优化方式但在实际业务中非常有效——毕竟模型的输出质量高度依赖你喂给它的工具描述和提示词。还有一个建议是别急着把Agent调到完美先用最小的闭环跑通业务价值。我见过太多团队花几周时间调一个Agent结果连业务方真实的使用场景都没有确认清楚。先上线让用户真用了再根据实际请求日志去调工具的描述和流程比闭门造车高效得多。在实际操作中我最深的体会是Spring AI在Java生态里已经把Agent落地的门槛降得足够低了真正的难点反而在工具本身的定义质量和系统提示词的设计。工具定义好了Agent的自主决策能力自然就强了工具描述含糊再好的模型也白搭。你只需要把每个工具的边界划定清楚让Agent能准确找到手和脚剩下的复杂推理交给模型就好。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑