☰
Agent与Workflow:Spring AI Alibaba Graph实战
2026/10/14 8:54:01 网站建设 项目流程

如果只看Demo,很多企业级AI项目早该上线了。让大模型帮忙查个订单、生成一段回复、做一趟工具调用,Demo里都跑得顺顺当当。可一旦放进生产环境,问题就换了一副面孔:自主循环停不下来、关键步骤依赖模型心情、出了错不知道在哪一步补救,最后整个业务流程变成了一个不可复现的黑盒。我在实际项目中陆续接触Spring AI Alibaba Graph,把原来的单次AI调用改成可编排的Agent和Workflow之后,才慢慢找到了一套让AI环节嵌入企业流程的确定性做法。这篇文章打算结合落地经历,从ReAct Agent到Workflow,把企业AI复杂流程的编排思路、选型边界、调试心得一次讲清楚。主要适合正在犹豫“到底该让模型自由发挥,还是先把流程画成图”的团队参考。

1. 为什么企业AI流程总在“演示正常,生产就崩”的边缘试探

1.1 单次调用解决不了真实的业务链路

先还原一个我常常见到的场景:某团队打算做一个售后客服助手,最初方案是“把用户问题拼到Prompt里,调一次大模型,让它直接回复”。Demo阶段确实好看,输入“我订单一直没发货”,模型能生成一段得体的道歉和催促话术。但到了生产环境,这套方案立刻暴露出一堆问题:模型没有查真实订单状态的能力,说的“已催促仓库”是编的;遇到需要区分“退款”“换货”“仅咨询”的场景,单次调用的输出完全不受控;出了纠纷想追溯,发现根本没有中间过程记录。

这里要理解一个概念:企业里的AI应用,绝大多数不是单点任务,而是一条业务链路。售后工单从产生到完结,至少经过“接收—解析—查单—核对策略—生成回复—人工复核”几个环节,每个环节之间需要传递状态,需要分支判断,需要失败兜底。单次模型调用本质上是一个无状态函数:给一段输入,吐一段文本,它不关心前一步发生了什么,也不承诺下一步会怎样。

单次调用还有一个更隐蔽的问题:无法补偿。如果第一遍调用超时了、返回了非法JSON、或者输出了一个不存在的订单号,代码层只能重试,可重试之后的结果可能和第一次完全不一样。业务流程里一旦出现“不确定的输出”,后面所有依赖它的环节全都跟着抖。这不是模型不够聪明,而是架构没有给模型划好跑道。

用生活中的事情打比方:单次调用像问路,路人指了一个方向,你只能选择信或不信;流程编排像导航系统,先定位,再规划路线,遇到路口自动判断,走错了还能重新计算路径。企业级AI要的是后者——不是让模型一次性给出终极答案,而是让模型作为整条流水线上的一道工序,和其它代码工序一起,把一件事稳定做完。

1.2 自主Agent不是银弹,失控才是常态

因为单次调用不够用,很多人立刻跳到另一个极端:直接上Agent。ReAct模式这几年很流行,核心思想是让模型“思考一步,行动一步”:先根据用户请求推理需要哪些信息,然后调用工具,看到工具返回结果后再推理下一步。听起来很完美,工具随便调,信息随便查,模型像是变成了一个能自己干活的实习生。

但实际上,把Agent直接暴露在企业流程里,很快会遇到三个麻烦。

第一个是无限循环。模型拿到工具返回的“未找到记录”之后,不会停手,它会换一套参数再试一次,试完再换一种说法,直到把上下文窗口撑爆。没有迭代上限的Agent,在生产环境里就是一台吞金机器。

第二个是权限边界模糊。工具越灵活,危险越大。允许Agent调用“查询订单”和允许Agent调用“修改订单状态”,完全是两个风险等级。可很多团队在一开始并没有区分工具的执行权限,一个能读能写的API暴露给模型之后,模型在幻觉驱动下的一个错误参数,就可能把脏数据写进核心系统。

第三个是审计困难。Agent自己选工具、自己定调用顺序,出了问题只能看到模型说了什么,看不到它为什么决定这么做。想复现生产问题的时候,同样输入再跑一遍,结果又不一样。对研发团队来说,不可复现就是不可排查,不可排查就是不可上线。

所以我对“全自主Agent”一直持保留态度。模型适合做判断、做生成,但并不适合在没有边界的情况下做执行。真正的解法不是给模型一把万能钥匙,而是把一个复杂任务拆成一段段可控制的步骤——哪些由代码做,哪些由模型做,哪些允许模型自主探索,哪些必须走固定路径,这些都需要提前用流程约束出来。

1.3 Graph的定位:让AI流程回到“画图”的确定性

Spring AI Alibaba Graph提供的思路,恰恰是把AI应用当成一张图来编排。

图里的节点是业务动作,可以是普通代码,也可以是一次模型调用;边是流转关系,决定了数据从一个节点流向哪个节点;图上运行着状态对象,相当于各个节点之间传递的数据包。整张图由开发者定义,模型只是图里某些节点的内部实现。它要做什么、能调用什么、最多走几步,都由外面的“图”说了算。

这样做最大的好处是确定性强。流程的骨架是代码写死的,不会出现“模型今天心情好走了A分支,明天心情差走了B分支”的情况。需要让模型动脑子的地方,单独留一个Agent节点;需要固定执行的地方,就用普通节点焊死。整张图可以编译、可以测试、可以随时查看当前执行到哪一步。

接下来的内容,我会先带你把Graph环境搭起来,然后分别实践两种典型模式——ReAct Agent模式和Workflow模式,最后给出我在这两类模式之间做选型判断的经验,以及生产环境里真正需要注意的坑。

2. 搭好Graph运行环境:先让最小流程跑起来

2.1 依赖引入和版本组合

Spring AI Alibaba Graph是基于Spring生态的框架,我当前使用的版本组合大致是:Spring Boot 3.x + JDK 17 + Spring AI Alibaba自带的Graph模块。Maven坐标看起来是下面这样:

<dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-graph</artifactId> <version>${spring-ai-alibaba.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>

第一次引入的时候,我遇到过starter之间的版本冲突,表现是启动时出现NoSuchMethodError。后来排查发现是项目里另一个依赖把一个旧版本的通用库顶了进来。处理办法很简单:在Maven依赖树里找到冲突的传递依赖,把旧版本排除掉,或者在父POM里统一用dependencyManagement固定版本。记住,版本号不要图新,要看Spring Boot主版本是否匹配。

这里多说一句:Graph模块本身是集成在AI框架里的,它不会自动帮你接大模型。你仍然需要单独引入模型平台的SDK,或者使用框架自带的模型适配层。模型通道配置得对不对,直接决定后面所有流程能不能跑起来。

2.2 模型通道与连接池配置

模型通道配置看着简单,但里面有几个参数直接影响工作流的稳定性。我的application.yml大致是这样:

spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} base-url: https://xxx.example.com/v1 chat: options: model: qwen-plus temperature: 0.3 timeout: 60s

这里我想强调两个不是每个人都会注意的点。

第一,超时时间不要设太短。Graph里一个节点调用大模型,模型本身可能就要跑10到20秒,如果HTTP客户端的超时设成5秒,一个稍长的推理请求就会误报失败,然后触发重试逻辑,白白消耗请求额度。我一般把模型调用的超时放到60秒以上,但不会无限大——太长会让故障节点的恢复时间变慢。

第二,连接池大小要和并发量匹配。Graph支持并行节点,如果某个流程里设计了4个并行调用,模型客户端的连接池还只有2,那并行等于没有,请求全在排队等连接。这个坑我在压测的时候踩过一次,日志里看不到报错,只有全链路耗时飙升,后来把连接池调大到合理值之后,整体耗时立刻降了下来。

2.3 一个最小Graph:串联两个固定节点

环境没问题之后,先别急着上复杂流程。我习惯先跑一个很小的图,验证框架本身工作正常。下面是当时验证用的示例:

public class SimpleGraphDemo { record BizState(String input, String parsed, String reply) {} static String parseRequest(BizState state) { // 这里模拟:把原始文本解析成结构化数据 return "parsed:" + state.input(); } static String buildReply(BizState state) { // 这里模拟:根据解析结果生成回复 return "reply:" + state.parsed(); } public static void main(String[] args) { StateGraph<BizState> graph = new StateGraph<>(BizState.class) .addNode("parseRequest", Node.of(SimpleGraphDemo::parseRequest)) .addNode("buildReply", Node.of(SimpleGraphDemo::buildReply)) .addEdge("START", "parseRequest") .addEdge("parseRequest", "buildReply") .addEdge("buildReply", "END") .compile(); BizState state = new BizState("我订单一直没发货", "", ""); BizState result = graph.invoke(state); System.out.println(result.reply()); } }

这段代码里有几个关键概念要理解清楚。

StateGraph是整张图的入口,泛型是状态类型。节点函数接收当前状态,返回更新后的数据;边把节点连接起来;compile()是把图编译成可执行对象。执行时调用invoke(),传入初始状态,图就会按照边的定义依次执行,每执行到一个节点,状态就被更新一次。

START和END是图的固定边界。所有流程都必须从START开始,到END结束。哪怕是最简单的两个节点,也要显式声明这两个边界,这是Graph构建时的一个约束。

跑通这个最小样例之后,你实际上已经拥有了一套可以承载AI流程的骨架。后面不管是接ReAct Agent,还是编排复杂的带分支Workflow,都是在这个骨架上加节点、加边而已。

3. ReAct Agent实践:给模型一个工具箱,而不是一把万能钥匙

3.1 在Graph中挂载ReAct Agent

先说清楚一件事:在Spring AI Alibaba Graph里,ReAct Agent通常不是整张图,而是图里的一个节点。它做的事仍然是“推理→调工具→再推理”,但它被限制在了流程图划定的范围内。

我定义一个Agent节点时,一般会先把工具准备好。工具就是一组可以被模型识别并调用的函数,比如查订单、查历史会话、查售后策略。示例大致长这样:

public class IntentAnalyzeNode { private final ChatClient chatClient; public IntentAnalyzeNode(ChatClient chatClient) { this.chatClient = chatClient; } public BizState invoke(BizState state) { String userInput = state.userInput(); String systemPrompt = """ 你是售后工单意图识别器。 你可以使用以下工具:queryOrder、querySessionLog、queryAfterSalePolicy。 请根据工具返回结果,把用户意图归类为:COMPLAINT、REFUND、EXCHANGE、CONSULT。 只输出JSON,格式为:{"intent":"..."} """; String modelOutput = chatClient.prompt() .system(systemPrompt) .user(userInput) .tools(OrderQueryTool.class, SessionLogTool.class, PolicyTool.class) .call() .content(); return state.withIntent(parseJson(modelOutput)); } }

这个节点挂在Graph里的方式,和普通节点没有本质区别:

StateGraph<BizState> graph = new StateGraph<>(BizState.class) .addNode("intentAgent", new IntentAnalyzeNode(chatClient)) .addNode("queryOrder", new OrderQueryNode(orderService)) .addEdge("START", "intentAgent") .addEdge("intentAgent", "queryOrder") // ... 继续定义后续节点 .compile();

也就是说,Agent的“自主性”被限制在了单个节点内部:它可以选择调哪个工具、按什么顺序调,但它不能跳出这个节点,更不能决定整个流程的走向。这是关键。

3.2 实战:用Agent做“工单意图初判”节点

我曾在某售后工单自动化项目里用过这个模式。用户消息进来之后,第一步就是判断意图。“我订单一直不发货,客服也不回消息”这句话,表面上是抱怨,但到底是催发货、要退款、还是单纯咨询配送政策,直接决定后面的分支流程。

如果不用Agent,只靠关键词正则,根本处理不了这种模棱两可的表达。“不发货”三个字可能是真的超时未发,也可能只是物流信息没更新;只看文本没法下结论,必须结合真实订单数据判断。

所以我在意图识别节点里给Agent挂了三个工具:queryOrderByUserId、queryLogisticsStatus、queryRecentSession。它先看到用户ID,再决定是否查询订单。如果订单确实未发货且超时,它很大概率会判断为“催发货投诉”;如果订单状态正常,它可能判断为“咨询物流进度”。实测下来,意图识别的准确率比纯Prompt分类高不少,原因很简单:它多了外部信息可以参考。

这个节点有一个很值得保留的习惯:模型的输出不是自由文本,而是强制JSON。意图枚举值都是预定好的,这样下游节点不用去解析一段散文,直接按JSON字段取值即可。模型再怎么发挥,也只能填入那些枚举值。

3.3 我实测中反复出现的三个问题

Agent节点看着好用,但我在真实项目里抓到过三个问题,可以说每一个都值得写进上线检查清单。

第一个是死循环。有一次,用户拿一个查无此人的订单号来投诉,Agent查了一次返回空结果,它不死心,换了个参数又查一次,还是空,又换另一种说法……迭代上限设了15次,它硬是跑满15次才停。那次让我意识到,必须给Agent节点设置maxIterations,比如初始值设4到5次,并且工具返回空结果时,Prompt里要明确写上“查不到就停止尝试,直接走兜底分支”。没有上限的Agent,成本是不可预测的。

第二个是参数幻觉。模型在构造工具调用时,偶尔会凭空捏造参数。比如状态里明明有orderId,它不去取,反而编一个不存在的订单号去查。这通常发生在工具描述不够精确的情况下。解决办法有两个:一是把状态里的关键字段显式拼进Prompt,让模型“复用上下文里的已有值,不要新造值”;二是在工具的参数Schema上做强校验,类型不对、必填缺失直接报错,不让错误参数进入业务系统。

第三个是不可审计。Agent自己决定调用哪些工具,如果每个工具调用前后都不留痕迹,后期排查工单为什么被分错类,基本只能靠猜。我的做法是在Agent节点的入口和出口各打一条结构化日志,记录完整的调用参数、工具返回结果、模型原始输出、Token消耗。日志字段得固定,方便接日志平台。平时看着冗余,出了生产事故,这些日志才是唯一的排查线索。

4. Workflow编排实战:先把确定的步骤焊死,再把不确定性留给分支

4.1 售后工单自动处理的主干流程

如果说Agent负责的是“这一步怎么判断”,那Workflow负责的就是“整条路怎么走”。Workflow模式下,每个节点做什么非常明确,模型只出现在真正需要它的地方。

还是拿售后工单举例。主干流程我设计成六步:

  1. 接收并解析工单
  2. 查询订单快照
  3. 查询售后保障策略
  4. 检查之前是否已有人工介入
  5. 生成回复草稿
  6. 按规则决定自动回复还是转人工

用Graph表达出来,就是一系列固定节点,连成一条确定的边:

StateGraph<BizState> graph = new StateGraph<>(BizState.class) .addNode("parseTicket", new ParseTicketNode()) .addNode("queryOrder", new QueryOrderNode(orderService)) .addNode("queryPolicy", new QueryPolicyNode(policyService)) .addNode("checkHuman", new CheckHumanInterventionNode(sessionService)) .addNode("generateReply", new GenerateReplyNode(chatClient)) .addNode("routeResult", new RouteResultNode()) .addEdge("START", "parseTicket") .addEdge("parseTicket", "queryOrder") .addEdge("queryOrder", "queryPolicy") .addEdge("queryPolicy", "checkHuman") .addEdge("checkHuman", "generateReply") .addEdge("generateReply", "routeResult") .addEdge("routeResult", "END") .compile();

注意看,这条链路里只有generateReply一个节点用了大模型,其他节点都是纯业务代码。这背后的设计原则很简单:能用规则和代码写清楚的逻辑,绝不交给模型;只有“根据前面所有信息生成一段自然语言”这一步,才需要模型的能力。

这种设计带来的直接收益是:整条流程的90%可以被单元测试覆盖。parseTicket节点传入一份工单文本,断言输出字段是否正确;queryOrder节点传入一个订单ID,断言拿到的快照是否符合预期。只有generateReply的输出需要特殊处理,但我也可以把它的职责限制在“基于固定模板填充变量”,连模型输出都变得可预测了。

4.2 条件分支和并行节点:流程从“直线”变成“路由”

主干流程跑通之后,真正让Workflow有业务价值的,是条件分支。

比如意图识别之后,不同意图走不同子流程:退款申请要走“核对订单状态→校验退款条件→生成退款单”;换货申请要走“校验库存→生成换货单”;仅咨询的则不需要动任何单据,直接生成回复就行。在Graph里,这种分叉用条件边实现:

graph.addConditionalEdge("intentAgent", state -> { return switch (state.intent()) { case "REFUND" -> "refundFlow"; case "EXCHANGE" -> "exchangeFlow"; case "CONSULT" -> "consultFlow"; default -> "manualReview"; }; });

条件边本质上是一个函数:接收当前状态,返回下一个节点的名称。这个函数是纯代码,完全可控,不会出现模型乱跳分支的情况。条件分支把一张线性图变成了有岔路的有向图,而每个岔路又可以是完整的子图。

除了分支,Workflow里另一个常用能力是并行。比如查订单快照和查最近客服会话,两个动作互不依赖,放在线上顺序执行可能要1秒,并行执行可能只要300毫秒。Graph一般在节点编排上支持并行执行思路,实践方式是把两个节点挂在同一个前驱节点之后,让它们同时运行,用一个汇总节点把它们的输出合并。这样既能减少整体耗时,又不会破坏状态的一致性。

并行节点要小心一个副作用:状态更新冲突。如果两个并行节点同时向状态对象里写同一个字段,后写完的会覆盖先写完的,数据就丢了。我处理这种问题的方法是:每个并行节点只写自己的独立字段,汇总节点负责合并,绝不让两个节点写同一个字段。

4.3 Agent节点与Workflow的协同:图管纪律,模型管判断

把Agent放进Workflow,是我目前认为最实用的组合方式。整个Flow架子是确定性的,Agent只是其中一个“高级节点”。

我在前面展示的意图识别节点,实际上就是这样一个组合。它整体是一个进行意图分类的步骤,使用的是Agent能力,但它的输出格式、可选意图、迭代上限全部由开发者定义。更重要的,是它后面跟着的流程不受它影响:无论Agent输出“REFUND”还是“EXCHANGE”,后续都有对应的固定流程接着走。Agent可以决定的是“当前工单属于哪一类”,但决定不了“这一类工单怎么处理”——后者由Workflow保证。

这里有一个技巧:Agent节点的Prompt里,要明确告诉模型“你只是整条流水线的一环,后续环节会继续处理”。这能显著减少模型“替用户把问题一次解决”的多余冲动。模型越想当全知全能,越容易越界,把不属于它权限的事情也顺手做了;一旦明确它是流水线的一环,它反而会更老实地只做分内判断。

5. 选型判断:纯ReAct、纯Workflow还是混合编排

5.1 什么场景真正需要纯ReAct

不是所有业务都适合硬套流程。我自己判断的标准只有一个:这个环节是否需要根据外部信息动态决定下一步动作,并且这个下一步的可能性集合没有边界。

举个例子,做一个企业内部知识库问答机器人,用户问的问题五花八门,可能涉及HR制度、IT运维、报销流程。模型需要先判断用户问的是哪类问题,再决定查哪套知识库,查完还要决定是否追问。这类场景的“下一步”集合是开放的,用固定Workflow根本画不全,纯ReAct反而合适——给模型几个工具,让它自己探索。

但即使在这种场景里,我也不会让Agent完全自由。至少要做三件事:限定工具白名单、给每次调用设超时、保留完整调用日志。纯ReAct不等于无边界Agent,“纯”指的是骨架里没有固定流程,每步动作由模型动态决定;“无边界”是连权限和退出条件都不约束,两者完全是两回事。

5.2 Workflow何时胜出

当业务流程里存在明确的阶段划分、必须执行的顺序、必须校验的规则时,Workflow是明显更优的选择。理由很简单:这些业务规则是确定的,不需要模型临场发挥。

比如“先查订单,再查策略,最后生成回复”,这里面的顺序是业务强制的。不查订单就直接生成回复,即使模型能靠猜生成一篇很像样的文案,那也是错的。流程里还有状态校验节点,查回来的订单状态是“已发货但运输中”,而工单诉求是“退款”,此时必须走“拦截退款,转人工”的路线。这种分支规则,用代码写比用模型判断可靠得多。

Workflow的另一个隐性优势是开发成本可控。节点都是普通函数,测试起来毫无魔法可言。整个图可以编译成一份清晰的流程清单,评审的人不需要理解模型细节,只要看节点和边就能明白业务是怎么流转的。对于团队协作来说,这是很大的效率提升。

5.3 混合编排:我推荐的企业级默认方案

说到默认方案,我现在几乎都会选择混合编排,核心是“外稳内智”:外层用Workflow控制主干,内层用Agent处理需要动态判断的节点。

我习惯采用“哨兵节点”的写法。所谓哨兵节点,就是加在Agent前后用于兜底的普通节点。前置哨兵负责检查输入状态是否完整,比如用户ID是否存在、工单标题是否为空;后置哨兵负责校验Agent的输出,比如JSON格式是否正确、意图枚举值是否合法、置信度是否达标。任何检查不通过,就走人工兜底分支。这样Agent的破坏力被限制在了一个很小范围内。

哨兵节点看起来只是多写几行校验代码,但在生产环境里的价值极高。AI输出的不确定性永远存在,你没法保证模型每次100%按格式输出,但你可以保证“格式错了也不会影响主流程”。这就是确定性和不确定性之间的缓冲带。

我整理了一个选型对照表,方便团队在方案评审时快速对齐:

维度纯ReAct纯Workflow混合编排
流程确定性低,模型动态决定下一步高,所有路径代码固定中高,骨架固定,节点内可探索
业务审计能力弱,需要额外记录日志强,天然按节点追溯强,Agent调用也被记录
开发成本低,配好工具就能跑中高,需要设计状态与分支中高,但可控性最好
测试覆盖难,输出不固定容易,各节点可单测大部分节点可单测
上线风险高,容易失控和超支低,故障面清晰中低,哨兵节点兜底
适合场景开放式问答、探索性任务流程催定、规则明确大部分企业既有流程

这个表不是绝对的,但它反映了一个共性规律:流程里确定的步骤越多,用Workflow越划算;动态判断的需求越强,越需要Agent;而企业里的大多数流程,其实是两者的混合体。

6. 企业落地时的排查清单与心得

6.1 状态设计与上下文长度:Graph跑着跑着就超限的根源

Graph里的状态对象,是整条流程传递数据的主通道。它设计得好不好,直接决定流程的稳定上限。

我第一次做Workflow时,图省事,把整段用户会话历史都塞进状态对象里,后面每个节点都把这个超大字符串带着跑。结果没跑几步,模型节点就报上下文超限。排查下来发现,问题不在模型本身,而是状态设计没有做裁剪。会话语料是给模型做语义理解用的,但如果已经抽出了结构化意图,原始文本就没有必要继续在后面的节点里传递。

我的改进方案是:每个节点只往状态里写下游节点真正需要的字段。长文档落到外部存储,状态里只留文档ID和摘要;模型调用的上下文,在调用前临时拼装,调用完就释放。这相当于给每个节点设了“最小权限”,既省Token,又减少出错面。

还要注意状态对象最好保持不可变或者半不可变。节点传入一个状态,返回一个新状态,而不是在原有状态上改来改去。这样一旦某个节点执行失败,你可以拿到上一份完整快照,快速定位是哪一步改坏了数据。

6.2 超时、重试与幂等:工作流里的异常处理三板斧

Graph里的节点一旦开始在企业环境运行,就要接受一个事实:外部系统一定会超时,一定会偶尔返回5xx,一定会偶发网络抖动。所以异常处理不是可选项,而是必选项。

我的习惯分三层。第一层是节点级超时,每个可能阻塞的节点都要设置超时上限,防止一个慢接口拖死整条工作流;第二层是重试策略,对于查询类节点,允许有限次重试,但要有退避,比如第一次失败等500毫秒,第二次等1秒,第三次放弃;第三层是幂等设计,对于写操作,比如生成退款单、更新工单状态,绝对不能盲目重试,而是要求接口本身支持幂等键,同一个请求重复提交不会产生两条单据。

这里有个容易被忽略的细节:大模型调用节点重试时要小心。如果第一次调用已经成功,但客户端超时了,重试会再次消耗一次模型请求,而且两次结果可能不同。所以模型节点我一般不重试,或者只重试一次,然后直接转人工兜底。宁可让用户多等一会儿,也不能让同一个工单触发两次不明确的处理动作。

6.3 一次生产事故的排查过程:JSON字段名不一致引发的“灵异事件”

有一次,售后工单流程在预发环境跑了两周,突然开始有工单卡在“转人工”分支上不去。日志里没有任何异常堆栈,只有某个节点出现了空指针错误。当时团队怎么查都没头绪,因为同一个入口反复提交,大部分时候是正常的。

后来我们把每个节点的输入输出快照打了完整日志,才发现问题藏得很深。意图识别Agent节点正常输出了JSON,但是有一次它把字段名从customerId写成了customerID,而后面queryOrder节点解析时用的是customerId,取到的值是null,空指针就发生了。

为什么是偶尔发生?因为模型生成JSON时,字段名并不能保证100%遵循约定,大多数时候它跟着Prompt里的示例走,但偶尔会自作主张换个大小写。其实Agent节点按时返回了合法JSON,它只是返回了一个字段名对不上的JSON。

这次事故给我留下的教训很深刻:凡是AI返回的JSON,一律不能直接用于下游业务代码。我后来在所有AI输出节点后面,都加了一个SchemaValidatorNode,专门检查字段名、类型、必填项。检查不过就直接转人工,绝不让脏数据流向业务系统。这个节点本身不复杂,但它把模型的不确定性变成了流程设计的一部分。

6.4 两个调试技巧:状态快照与链路追踪

最后分享两个我在实际调试里觉得非常有用的技巧。

第一个是给Graph加一个全局拦截器,在每个节点执行前和执行后,把状态对象的关键字段打印出来。注意只打印脱敏后的关键字段,不要打全量,不然日志量会大得吓人。这样每次跑完流程,你能按照时间线完整复盘:哪个节点改了哪些字段、执行耗时多少、输出是否符合预期。

第二个是给每次图执行绑一个traceId。从入口接收到最终回复,所有日志都带上这个ID,包括模型调用日志、工具调用日志、节点执行日志。出了问题,拿traceId一搜,整条链路一目了然。没有链路追踪的AI流程,排查一次线上问题会把人逼疯——模型调用和业务日志混在一起,没有关联维度,基本等于大海捞针。

做企业级AI流程编排,说到底是在跟不确定性做对抗。模型本身是不确定的,但流程可以是确定的。谁能把模型的不确定性关进笼子里,让它在划定的范围内发挥作用,谁才能真正把AI落到业务里。

最后再分享一个我自己的习惯:无论选ReAct还是Workflow,先接一个最小流程跑通,再逐步加节点。第一版不要追求复杂,一个能稳定运行的简单流程,远比一个花哨但总出问题的复杂编排有价值。等流程稳了,再一步一步把Agent加进来,把并行加上去,把优化做起来。这个节奏虽然慢,但每一步都是可控的,不会走到一半被各种意外拉回原点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询