news 2026/10/20 22:45:51

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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)就是干这个的。具体流程是:

  1. 模型接到一个复杂任务,先拆解出第一步需要什么信息;
  2. 决定调用某个工具,并生成参数;
  3. 程序执行工具,返回结果;
  4. 模型观察结果,判断是否还需要更多信息;
  5. 如果需要,继续调用下一个工具;如果信息齐了,就生成最终回答。

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-SNAPSHOT,JDK 17。这里注意,Spring AI的版本迭代特别快,API变化也大,如果你用的是正式发布版,命名可能有差异,但核心思路一致。

<dependencies> <!-- Spring AI 核心依赖 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>0.8.1-SNAPSHOT</version> </dependency> <!-- 如果用通义千问等其他模型,换对应starter即可 --> </dependencies>

Spring AI的兼容性设计得不错,底层的模型接入是高度抽象的。你只需要改依赖和配置,代码本身不用动太多,就能从OpenAI切换到其他大模型。这算是一个隐藏福利。

配置文件中,最关键的是模型API的Key和基础地址:

spring: ai: openai: api-key: ${OPENAI_API_KEY} base-url: ${OPENAI_BASE_URL}

这里要提醒一个坑:如果你使用的是代理地址或兼容API,base-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 List<CustomerRank> 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来管理。我简单实现过一个滑动窗口:

List<Message> 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能准确找到"手"和"脚",剩下的复杂推理,交给模型就好。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 22:45:25

OpenClaw+Ollama跨平台远程接入实战指南

1. OpenClaw不是“又一个AI工具”&#xff0c;而是自动化工作流的物理接口层你有没有过这种体验&#xff1a;在终端里敲完ollama run llama3&#xff0c;看着模型吐出一串文字&#xff0c;心里却在想——这结果能不能直接触发某个动作&#xff1f;比如检测到“紧急”就发邮件&a…

作者头像 李华
网站建设 2026/10/20 22:44:49

AI操作员:大模型驱动的浏览器智能代理实战解析

先说结论&#xff1a;这个「AI 操作员」本质上是一个运行在浏览器里的智能代理&#xff0c;它把自然语言指令翻译成连续、可验证的浏览器操作序列。我在实际开发与长期使用过程中&#xff0c;完整走通了需求拆解、动作执行、结果校验这一整套闭环&#xff0c;也踩了不少坑。下面…

作者头像 李华
网站建设 2026/10/20 22:43:50

步进与伺服电机加减速控制:梯形与S曲线算法源码详解

做运动控制的人都知道&#xff0c;电机启动和停止这件事&#xff0c;远没有“给个脉冲它就转”那么简单。尤其是步进电机和伺服电机在面对较大惯量负载或者高速运动时&#xff0c;如果速度跳变太猛&#xff0c;轻则丢步、过冲&#xff0c;重则直接撞限位甚至烧驱动器。我自己在…

作者头像 李华
网站建设 2026/10/20 22:42:30

MySQL查看库表大小:一条SQL精准定位磁盘占用大表

前阵子半夜被值班电话叫醒&#xff0c;某业务服务器磁盘使用率飙到了90%。登上机器第一件事&#xff0c;就是打开mysql命令行&#xff0c;用SQL命令查看mysql数据库中各业务库及对应库中表的大小&#xff0c;十几分钟后就锁定了一张疯狂膨胀的日志表&#xff0c;占掉了大半空间…

作者头像 李华
网站建设 2026/10/20 22:40:41

逆向工程工具集retoolkit使用指南:80+工具分类与实战分析路线

说起逆向工程&#xff0c;很多刚入行的朋友第一反应是“装一堆工具&#xff0c;没事点点看”。实战里真正麻烦的不是装工具&#xff0c;而是面对一个未知二进制时&#xff0c;不知道从哪个工具下手&#xff0c;也不知道每个工具最适合解决哪类问题。retoolkit这个工具集的价值在…

作者头像 李华