尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring AI Alibaba + ReAct Agent:企业AI复杂业务落地的工程实践

Spring AI Alibaba + ReAct Agent:企业AI复杂业务落地的工程实践 前阵子在企业做AI项目售前支持业务方提了一个很典型的需求让AI直接帮销售查客户信息、统计商机、填周报最好还能在钉钉群里自动回复客户问题。乍一听这好像就是给大模型接几个API但真正动手做的时候才发现事情远没那么简单。客户信息在CRM里商机数据在数仓里周报模板在文档系统里大模型本身既不掌握这些数据也没有能力去调用这些系统。于是问题就变成了一个非常具体的工程问题怎么让大模型安全可控地使用外部工具又怎么让它在一次复杂任务里自主决定先查什么、再算什么、最后写什么。这正是Spring AI Alibaba ReAct Agent要解决的核心问题。这篇文章我会从Tool Calling讲起一路做到ReAct Agent的完整落地最后聊聊企业级AI复杂业务设计里那些课本上不常写、但实战中绕不开的坑。内容面向的是有Spring Boot基础、想在企业项目里引入AI Agent能力的后端开发也适合正在做技术选型的架构师参考。下面我按自己做项目时的思考顺序来讲不绕弯子。1. 为什么企业AI复杂业务绕不开Tool Calling与Agent1.1 企业业务到底复杂在哪企业级业务和C端聊天机器人有一个本质区别C端聊天只需要模型会说企业业务要求模型会做。这个做字背后是大量异构系统的联动。我在实际项目中见过太多这样的场景销售在钉钉里问华东区Q3的商机漏斗怎么样这句话背后至少涉及三件事。第一要去CRM系统里查商机表按区域、按时间过滤数据第二要去BI平台或者数仓里算转化率、同比环比第三要按照公司固定的汇报格式生成一段解读。任何一个环节缺失AI给的回答都是看起来有道理但没法直接用。这种场景有几个共同特点数据实时变化、系统相互隔离、操作有权限边界。大模型训练时根本没见过你公司的CRM表结构也没法实时知道今天的商机数据。所以想让AI真正干活第一步想都不用想必然是要让它具备调用外部系统的能力这就是Tool Calling存在的根本理由。1.2 LLM的边界就是Tool Calling的起点大模型本质上是一个概率推理引擎它的参数里存储的是训练语料的压缩知识而不是你公司系统的实时状态。你问它客户A的信用额度是多少如果这个数据不在训练语料里模型只能靠猜猜出来的答案在企业场景里就是事故。Tool Calling做的事情说穿了就是把模型不知道但系统知道的信息通过一个标准化的函数调用接口补进去。模型在推理过程中发现这题我不会但我手头有个工具可以查于是生成一个结构化的调用请求比如调用queryCustomerCredit参数是customerId10086。这里有一个很多人容易误解的点Tool Calling不是模型学会了调用工具而是模型在推理时选择了调用工具。这个选择能力来自模型训练时的指令遵循和格式对齐。这就是为什么同一个工具不同模型的调用准确率差很多背后是模型能力差异不是框架的问题。Spring AI Alibaba正是把这块标准化了。你不需要自己解析模型吐出的JSON也不用手动维护函数签名列表框架会把你用注解定义的Java方法自动转成模型能理解的工具描述并处理整个调用编排。1.3 从工具到决策Agent出现的必然性有了Tool CallingAI能查数据了但新的问题马上冒出来一个复杂业务往往需要连续调用多个工具而且后一步怎么做取决于前一步的结果。这就不是一次工具调用能解决的了而是一连串有逻辑的决策。举个例子用户问帮我找出现有客户里最有价值但最近三个月没成交的Top 10然后给每个客户生成一条个性化挽回话术。这个需求至少四步查客户列表、过滤交易记录、计算价值评分、基于评分生成话术。每一步的输入都依赖上一步的输出而且模型需要不断观察中间结果、调整下一步计划。这就是Agent的定义一个能自主感知环境、做出决策、执行行动、并循环迭代直到完成目标的系统。ReAct是其中一种非常经典的Agent模式全称是Reasoning Acting强调模型在每一步都先思考再行动把推理过程和工具调用显式交替进行。Spring AI Alibaba对这套模式支持得很完整但能用好它需要你先理解底层机制而不是直接拿框架就开跑。2. Tool Calling到ReAct Agent一次能力跃迁2.1 先搞清楚Tool Calling做到了什么Tool Calling的阶段模型的角色更像一个聪明的调度员。你给它一个问题、一组工具描述它决定调用哪个、传什么参数然后你把工具结果原样返回它基于结果组织最终回答。这个过程的本质是单轮的多步处理模型不会主动展开一个任务计划它只响应最近的那一次工具返回值。如果这次返回值不足以回答用户问题模型不会自己去想那我是不是该再查一个别的接口而是直接基于已有信息硬答——除非你的提示词里明确告诉它如果信息不足请继续推理并再次调用工具。在实际项目中很多团队把Tool Calling用成了增强版知识库查询。模型问一句查一句查完回答勉强能覆盖查余额查订单状态这类固定动作场景。但一旦任务维度变成分析A、对比B、并给出C建议单次调用的模式就力不从心了。2.2 ReAct把推理和行动变成显式循环ReAct的核心创新在于把大模型解决问题的过程从一步到位改成了思考-行动-观察的循环。每一轮里模型先产生一个Thought我为什么要做这一步再产生一个Action我要调用哪个工具、传什么参数然后框架执行工具并返回Observation观察结果模型基于观察结果进入下一轮思考。这个过程非常像人解决问题的思路。你接到整理三季度销售报告的任务不会直接开始打字而是先想我需要哪些数据然后去CRM导数据看看数据长什么样再决定下一步是清洗还是直接分析。ReAct Agent就是把这个思路流程化、代码化了。站在工程实现的角度ReAct Agent比Tool Calling多的不是一两个API而是一个控制循环。这个循环需要管理上下文、记录中间状态、限制最大迭代次数、处理工具异常、判断何时终止。Spring AI Alibaba在底层封装了大部分循环逻辑但你需要理解它的行为边界才能在复杂业务里用好它。2.3 一张表看清Tool Calling与ReAct Agent的差异对比维度Tool CallingReAct Agent决策层级单次工具选择多轮计划-执行-观察上下文组织多被当前问题限制保留完整推理轨迹任务类型查询、计算、填充分析、编排、多步骤目标终止条件工具返回即结束模型判断目标达成才结束失败处理直接应答或报错可重试、可更换策略实现复杂度低成本需要设计推理边界典型场景查天气、算运费周报生成、客户运营分析表格里的差异在代码层面并不大往往只是是否循环的区别但在产品能力上是质变。Agent意味着AI不再是被动应答的工具而是一个能对目标负责的虚拟员工。当然能力变强了出问题的可能性也变大了这也是为什么企业级Agent必须做边界控制和安全兜底后面我会详细说。3. Spring AI Alibaba环境搭建与工程初始化3.1 工程初始化与依赖配置Spring AI Alibaba是基于Spring AI官方规范做的阿里系实现最大的好处是模型接入和工具调用体系都跟你熟悉的Spring Boot生态无缝集成。整个过程如果你已经用过Spring Boot基本不会有什么不适感。建一个普通的Spring Boot工程建议把JDK版本放到17以上Spring Boot版本用3.2.x或更高。然后在pom.xml里引入starterdependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId version1.0.0-M3/version /dependency这里有个细节不同版本的starter对应不同的Spring AI版本如果你项目中已经有其他Spring Cloud组件注意版本兼容性。建议直接用阿里提供的BOM来做依赖管理避免一大堆版本冲突的破事。dependencyManagement dependencies dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-bom/artifactId version1.0.0-M3/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement我在实际项目里遇到过一个问题只引入starter没有引入BOM导致Spring AI核心包版本被其他依赖抬高工具调用的序列化方式出现异常。所以如果你不是只有一个AI模块的纯新工程强烈建议用BOM统一管理。3.2 模型接入与配置Spring AI Alibaba默认接的是阿里云百炼平台的DashScope模型。你需要先去百炼控制台开通服务拿到API Key然后配置到application.yml里spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-max temperature: 0.7 max-tokens: 4096模型选择上qwen-max适合复杂推理场景qwen-plus性价比更高。做了一个多月实验后我的体感是工具调用场景里复杂度和准确性优先选qwen-max单纯的文本生成可以降级到qwen-plus。temperature建议设在0.5到0.7之间太高工具参数格式容易飘太低回答会显得机械。如果你公司已经接入了其他模型服务Spring AI Alibaba也支持通过spring.ai.model.chat配置切换但为了演示顺畅下面我统一用DashScope来串流程。3.3 第一个工具函数怎么写Spring AI Alibaba里定义一个工具非常简单一个注解搞定。下面这段代码定义了一个查询客户信息的工具模型需要的时候会自己调用它import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component; Component public class CrmTools { Tool(name queryCustomerById, description 根据客户ID查询客户的详细信息包括客户名称、所属行业、客户状态、信用额度) public CustomerInfo queryCustomerById( ToolParam(description 客户ID例如C0001) String customerId) { // 这里写真实的CRM查询逻辑 return customerService.queryById(customerId); } }这段代码里有三个关键信息工具名、工具描述、参数描述。后面两个描述容易被忽略但它们是模型决定什么时候用这个工具、传什么参数的唯一依据。描述写得越清楚模型的调用准确率越高。你写查询客户信息模型就只知道可以查但不知道什么时候该查、查出来有什么字段可用。写清楚字段含义和适用场景调用的准确性会明显上一个台阶。4. 实战一基于Tool的Tool Calling完整落地4.1 定义一个真实业务工具组合纯查询工具只能演示模型会调函数要贴近企业业务得做一个有业务含义的组合。我这里用客户画像查询 商机漏斗统计这组工具来演示这两个是销售侧最常见的需求。Component public class SalesTools { private final CustomerRepository customerRepository; private final OpportunityRepository opportunityRepository; public SalesTools(CustomerRepository customerRepository, OpportunityRepository opportunityRepository) { this.customerRepository customerRepository; this.opportunityRepository opportunityRepository; } Tool(name getCustomerProfile, description 根据客户ID查询客户画像包括客户名称、行业、所属区域、客户等级、最近联系时间) public CustomerProfile getCustomerProfile( ToolParam(description 客户ID格式为C加四位数字) String customerId) { return customerRepository.findProfileById(customerId); } Tool(name getOpportunityFunnel, description 统计指定区域、指定时间范围内的商机漏斗返回各阶段的商机数量和金额) public OpportunityFunnel getOpportunityFunnel( ToolParam(description 区域名称如华东、华北) String region, ToolParam(description 开始日期格式yyyy-MM-dd) String startDate, ToolParam(description 结束日期格式yyyy-MM-dd) String endDate) { return opportunityRepository.summarizeFunnel(region, startDate, endDate); } }写这类工具时有一个经验返回字段尽量用模型容易理解的命名并在注释里把每个字段的业务含义写清楚。因为模型靠字段名和值的语义去推理如果你返回的是一个status1这种枚举值模型并不知道1代表赢单还是丢单。最好直接返回status: WIN这样的语义化值或者连一个statusDesc字段。4.2 创建ChatClient并绑定工具Spring AI Alibaba中一切交互都从ChatClient开始。它整合了模型、工具、记忆、增强逻辑是Agent开发的核心入口。Service public class SalesAssistantService { private final ChatClient chatClient; public SalesAssistantService(ChatClient.Builder builder, SalesTools salesTools, CrmTools crmTools) { this.chatClient builder .defaultSystem(你是企业销售分析助手。你可以查询客户画像和商机漏斗数据。回答要简洁、准确、专业。) .defaultTools(salesTools, crmTools) .build(); } public String answer(String userQuestion) { return chatClient.prompt() .user(userQuestion) .call() .content(); } }这一步要解释一下框架在背后做了什么。当你调用prompt().user(...).call()时Spring AI Alibaba会把已注册工具的描述名称、描述、参数结构组装进给模型的系统消息里模型根据问题内容决定是否在回复中夹带工具调用请求。框架解析到这个请求后调用对应的Java方法把方法返回值塞回消息历史再发起新一轮模型调用。整个过程对调用方是透明的你看到的只有最终的content()。所以从表面看你只是问了一个问题但背后其实已经完成了一次完整的工具调用。4.3 调用链路的实际验证启动应用用一个真实的查询来验证用户帮我查一下客户C0101的画像以及华东区2024年Q3的商机漏斗模型会怎么处理大概率是这样先判断这个问题需要两个工具的结果于是并行或先后产生两个ToolCalling消息分别带上了参数C0101和(华东, 2024-07-01, 2024-09-30)。框架执行完两个查询后把结果拼接给模型模型看到数据后组织出最终回答。你可以在日志里看到类似这样的输出Received tool call: getCustomerProfile([customerIdC0101]) Received tool call: getOpportunityFunnel([region华东, startDate2024-07-01, endDate2024-09-30]) Returning tool result to model...这里我想强调一个容易被忽略的点模型在Tool Calling阶段有可能幻觉参数。比如用户只说了华东区三季度模型可能会自己补一个2024-01-01之类的错误时间范围因为它没有真正理解三季度的含义。这是Tool Calling方案的一个普遍短板不是你代码的bug。后面到了Agent阶段我们会有办法缓解这个问题但完全消除不现实。5. 实战二ReAct Agent跑通企业业务闭环5.1 Agent的核心循环设计前面讲的Tool Calling框架自动处理了模型请求调用工具 - 执行工具 - 返回结果 - 模型生成回答这一个来回。但企业复杂任务往往需要多个来回而且每个来回的推理都依赖前面的执行结果。ReAct Agent的关键在于你能否掌控这个循环。Spring AI Alibaba虽然封装了工具调用的底层逻辑但循环的控制权最大迭代次数、上下文裁剪、终止判断仍然需要你自己设计。下面给出一个我在项目中使用的Agent编排核心结构。public class ReActAgent { private final ChatClient chatClient; private final int maxIterations 5; public ReActAgent(ChatClient.Builder builder, Object... tools) { this.chatClient builder .defaultSystem( 你是一个智能业务分析Agent。你可以使用多种工具完成复杂任务。 执行流程要求 1. 分析用户需求必要时拆解为多个子任务。 2. 每一步调用合适的工具获取数据仔细观察工具返回的结果。 3. 如果当前数据不足以完成任务继续调用其他工具补充信息。 4. 数据齐全后基于所有信息生成最终回答。 5. 不要编造数据工具没有返回的数据一律不写。 ) .defaultTools(tools) .build(); } public String run(String userInput) { return chatClient.prompt() .user(userInput) .options(ChatOptions.builder() .internalToolExecutionEnabled(true) .build()) .call() .content(); } }这段代码看起来和Tool Calling没什么两样真正的区别在系统提示词和maxIterations的约束。ReAct模式下模型被明确告知如果信息不足可以继续调用工具于是它在第一轮工具返回后发现数据不够完整会自主发起第二轮、第三轮调用直到它认为所有信息都齐了才生成最终答案。5.2 多工具协作的真实案例光说理论没意思来看一个真实的业务闭环场景。假设销售运营问了一个非常典型的问题帮我分析一下华东区客户C0101、C0102、C0103这三个客户的价值 对比他们的商机总额和赢单率然后给每个客户生成一条跟进话术。这个问题至少涉及四步操作第一步分别查询三个客户的客户画像搞清楚这三家分别是什么行业、什么规模、什么合作历史。第二步分别查询三家客户在指定时间内的商机漏斗数据拿到商机总额、各阶段金额。第三步对比三家客户的数据计算各自的赢单率或者商机健康度。第四步基于前三步的结果为每个客户生成个性化的话术话术要能体现客户的特点。如果是Tool Calling单次模式模型大概率会卡在参数不全或者只查了一家就急着生成结论。但在ReAct模式下模型的执行轨迹会是查询全部三个客户 - 汇总数据 - 分析 - 生成话术并且每一步之间它会主动参考上一步所有工具的结果。这里面最核心的机制是上下文累积。工具返回结果会按顺序追加到模型的对话历史里模型在每一轮推理时看到的不是孤立的最近一条而是整个执行轨迹。这就是Agent连贯性的底层来源。5.3 完整对话实测与中间状态分析我在线下做过一次完整实测输入上面的问题后把框架的SystemLogger打开可以看到模型的工具调用序列Tool call 1: getCustomerProfile(C0101) Tool call 2: getCustomerProfile(C0102) Tool call 3: getCustomerProfile(C0103) Tool call 4: getOpportunityFunnel(华东, 2024-01-01, 2024-12-31)注意模型没有把三个商机查询都做掉而是合并成了一个华东区整年漏斗。这意味着它认为漏斗数据可以按区域汇总一次性拿回来然后自己在推理阶段做客户维度的拆解。这是一个比较合理的策略因为销售漏斗表通常是明细表按区域查全量数据后在模型侧做关联比按客户查三次更高效。最终模型生成的回答里每个客户的话术也不会是空泛的您好我们最近有个活动。它会引用客户的实际行业、商机阶段和金额比如贵司目前处于方案阶段的项目有2个金额约80万建议重点关注XX产品线的新版本升级。这种回答在业务侧的接受度远高于通用话术。6. 企业级Agent设计稳定运行比炫技更重要6.1 工具粒度与描述信息的那些细节做Agent项目最影响成败的往往是工具的粒度。粒度太粗一个工具干太多事模型不容易理解什么时候该用它粒度太细工具列表几十上百个模型在工具选择上浪费大量token甚至选错。我自己的经验是把工具控制在一个工具只做一件完整的业务动作这个粒度。查询客户就是一个独立工具创建工单是另一个独立工具批量发消息也可以是一个工具。不要做一个万能工具接受一个action参数去分发内部逻辑模型的参数生成能力还不足以稳定支撑这种设计。描述信息上有个很容易踩的坑不要在工具描述里写太多不相关的修饰语。模型会把这些修饰语当作是否需要调用该工具的参考条件如果你写这个工具非常重要稳定可靠模型并不会因此更爱用反而会困惑。最好的描述是这句话明确说明工具做什么、输入参数的含义和格式、输出的价值。你的描述越接近API文档里给人看的说明模型调用越准。6.2 上下文长度、并发与成本控制Agent虽然强但它的token消耗比普通聊天高一个量级。一个5轮的ReAct循环每轮都要把完整的历史记录包括工具返回结果发给模型如果工具返回的是大段明细数据很快就会把上下文撑爆。我遇到过一次线上问题Agent调用了商机明细查询工具返回了2000多行数据直接让上下文长度冲到了100K以上单次请求成本急剧上升响应时间也明显变慢。解决办法其实不复杂——在工具内部做摘要只返回模型真正需要的统计值而不是原始明细。并发这块没有太多新东西但要注意模型服务的限流。DashScope的qwen-max有QPS限制如果你一个Agent在循环里连续发出多次模型请求很容易触发限流错误。解决方案就是在工具执行层加一个简单的信号量或令牌桶限制单个Agent进程的最大并发模型请求数。spring: ai: dashscope: chat: options: model: qwen-max另外建议在ChatClient层面开启观测拦截器记录每次模型调用的token数和延迟。这个数据不只是为了监控更是为了后续做成本核算和劣化预警。6.3 可观测性、兜底与人工确认企业级系统永远要回答一个问题Agent做错了怎么办。我在设计里加了三个层次的兜底。第一层是工具调用拦截。所有工具调用统一经过一个Interceptor记录调用时间、参数、结果摘要。一旦发现某个工具的调用频率异常或者连续返回空结果可以自动降级。第二层是任务终止。ReAct Agent必须设置最大迭代次数防止模型陷入反复调用工具但始终不产最终答案的死循环。我一般设为5复杂业务可以通过参数调整到8但绝不允许设成无限。第三层是人工确认。凡是工具执行了写操作创建订单、修改状态、发送消息都必须先返回一个待确认动作让用户在界面确认模型不能直接执行。安全优先这句原则放在Agent设计里一点不过时。我给一个价值排序的例子如果Agent认为需要向客户发送一条营销消息它应该输出 【待确认】计划向客户C0101发送如下短信尊敬的客户您的专属优惠即将到期…… 请确认后我将执行发送。只有用户点了确认才真正走外呼接口。7. 踩坑记录与个人经验7.1 三个让我印象深刻的坑第一个坑是关于工具描述里写了如果条件。我在一个项目里给查询工具写了当用户询问商机时使用结果模型把商机这个词当成触发条件只要用户在问题里提到商机不管实际上是想问客户回款还是合同它都去调这个工具。最后我把描述改成了根据商机ID查询商机详细信息适用于拿到了具体商机ID后需要查细节的场景问题才解决。第二个坑是没有设置上下文裁剪。一个Agent跑了十几轮对话之后工具返回结果和历史消息堆在一起模型开始忘记用户最初的需求回答变得答非所问。后来我在ChatClient上配置了窗口裁剪策略只保留最近10条消息和一个系统级的历史摘要情况明显改善。第三个坑是工具方法内部抛异常时框架默认会把异常信息原样返回给模型。我曾经有一个工具因为数据库连接超时抛了异常异常堆栈直接拼进了模型上下文模型被堆栈里的一大串类名搞糊涂了开始脑补出一个莫名其妙的回答。后来我在所有工具方法里做了异常捕获返回一个统一格式的错误对象比如{error: DATABASE_TIMEOUT, message: 查询超时请稍后重试}模型就能正确理解并给出稍后再试的友好回复。7.2 给后续做Agent项目的朋友几条建议第一不要让Agent直接操作生产库。我在项目里给Agent暴露的数据库账号是只读的所有工具都只读写操作统一走另一个需要人工确认的通道。这个底线无论如何不能破。第二小步快跑。不要一上来就做一个万能Agent想cover所有业务场景先挑一个高频、边界清晰的任务跑通闭环再横向扩展。我第一个Agent只做商机查询与简单分析上线稳定运行一周后才加的自动生成跟进任务。第三关注模型的迭代。大模型的能力迭代很快同一个工具定义在旧模型上可能调用准确率不到80%换一个新模型可能就有95%。所以工具定义不是写一次就完了每次升级模型都要做回归测试重点看工具选择准确率和参数生成正确率。第四Agent不是银弹。有些问题用传统工作流5行代码就解决了没必要非套Agent。我见过有人连判断一句话是不是问候都要让Agent调用工具纯属给自己加戏。判断复杂任务就交给Agent简单逻辑能走规则就走规则混搭架构才是企业级系统的常态。
返回列表