从ReAct到Workflow:基于云端API的事件驱动智能体实践

发布时间:2026/10/19 3:10:27
从ReAct到Workflow:基于云端API的事件驱动智能体实践
从 ReAct 到 Workflow基于云端 API 构建事件驱动的智能体在不少项目里折腾过 ReAct 模式之后我得先给一个可能让部分人意外的结论如果你正在构建一个面向真实业务的智能体大部分场景下应该优先选择 Workflow 编排而不是让模型在 ReAct 循环里自由发挥。这个认知不是我一开始就有的。最早做内部知识库问答机器人时我完全站在ReAct 是智能体标配那一边——让大模型自己决定调用哪个工具、观察结果、再决定下一步。听起来很美实际跑起来却让我吃足了苦头一次语义模糊的查询模型在检索 API 和数据库之间反复横跳连续调用了二十多次外部接口单次任务的高峰 API 费用直接涨了一个数量级更麻烦的是整个执行过程像黑盒出了问题根本没法稳定复现。后来我把核心逻辑改成了事件驱动的 Workflow先解析意图、再按事件链触发对应的处理步骤、每一步只做一件确定的事。改造完以后同样的任务 API 调用次数从二十多次降到了三四次费用、延迟和可排查性都肉眼可见地变好了。这篇文章不打算站队说 ReAct 一无是处也不准备吹 Workflow 万能。我想把这次迁移的完整思考过程、设计取舍、代码形态和工程细节摊开来讲给正在做同样选型的读者一个可参考的依据。1. 先说结论为什么我把核心逻辑从 ReAct 循环换成了 Workflow 编排1.1 ReAct 不是不好而是自由的代价太高先简单对齐概念。ReAct 是 Reasoning Acting 的组合核心思路是让模型在一个循环里交替进行推理和行动模型生成一段思考决定调用某个工具得到工具返回的观测结果再基于新的观测继续思考直到它认为自己完成了任务。这个过程确实很符合人们对智能体的想象——自主、灵活、能应对未知情况。但问题也恰恰出在自由二字上。ReAct 的每一步行动都由模型自主决定这在 demo 里非常惊艳一放到真实业务里各种代价就开始显现上下文膨胀失控。每一轮思考、每一次工具调用结果都会被追加进对话上下文多轮循环下来很快就能把上下文窗口撑满。上下文越长单次请求的 token 消耗越高模型响应也越慢而且注意力分散后答案质量还会下降。不可预期性太高。模型可能在两个工具之间来回试探可能调用一个带副作用的写接口两次可能在一件简单任务上绕远路。你没法提前预估一次任务到底会花多少钱、多长时间。可观测性差、调试困难。ReAct 每一步都依赖模型的随机输出同样的问题输入两次运行可能走完全不同的路径。出了问题想复现基本靠运气。我并不是说这些代价不可接受。探索型任务——比如研究助理、开放域问答——确实需要 ReAct 这种灵活性因为它面对的输入空间几乎是无限的没法预先穷举所有可能路径。但这类任务在真实业务里其实是少数更多时候智能体要面对的是路径相对固定的流程。1.2 一次真实的失控经历循环调用险些烧穿配额说一个让我彻底下决心改造的实际案例。我当时在做的是一个面向企业内部的调研辅助智能体用户提交一个问题后系统会先做知识库检索再查内部数据库补充结构化数据最后汇聚生成答案。最初版本用的是 ReAct模型手里握着五个工具知识库检索、数据库查询、文档解析、数据汇总、答案生成。某次内部测试一位同学输入了一个含糊的问题大概是帮我看看最近那个项目的进展。这个描述既没有指定项目名称也没有说明要看什么维度的进展。ReAct 循环里的模型开始了疯狂试探先检索知识库发现结果太泛再查数据库发现条件不明确于是又换了一个关键词重新检索又去查另一个表……整个循环跑了二十多轮中间还重复调用了同一个接口若干次最后给出的答案仍然是一堆模棱两可的汇总。那次任务单次消耗的 API 费用是正常情况的十几倍运行耗时也达到了不可接受的程度。更重要的是我在事后试图复盘时发现整个调用序列充斥着大量无效动作根本找不到一个清晰的逻辑主线。如果这是一个面向外部用户的付费功能这种不可控性会直接毁掉成本模型和服务质量。正是在那次之后我开始把注意力从如何让模型更好地自主决策转移到如何设计一套让模型只负责局部决策的流程框架也就是 Workflow。2. ReAct 模式的价值边界它能做什么又在什么场景下兜不住2.1 ReAct 的运作原理推理、行动、观测循环到底在循环什么在深入 Workflow 之前我想花点篇幅把 ReAct 的边界讲清楚因为很多人选错方案本质上是没有分清探索型任务和流程型任务。ReAct 的正常工作循环是这样的推理模型根据当前状态和已有信息推理下一步应该做什么。行动模型生成一个结构化指令调用某个工具或 API。观测工具返回结果作为新的信息追加到上下文。回到第一步直到模型认为任务已完成输出最终答案。这个循环的关键在于模型握有全局控制权。它能决定调用什么工具、调用的顺序、是否重试、何时结束。这在面对开放式问题时非常有价值——因为你确实没法预先把所有可能的工具组合写死。举个例子一个帮我比较 A 产品和 B 产品的市场表现的任务ReAct 可以先检索 A 产品的报告再检索 B 产品的报告然后查行业基准数据最后生成对比分析。这个执行路径不是预先设计好的而是模型根据每一步的观测结果动态规划出来的。这是 ReAct 真正的魅力所在。2.2 三个让 ReAct 失效的典型场景但 ReAct 的魅力与稳定往往不可兼得。以我自己的观察和踩坑经验下面三类场景特别容易让 ReAct 循环失控。第一类路径固定但流程冗长的任务。比如上传文件 → 解析内容 → 提取字段 → 写入数据库 → 通知用户。这个流程在业务上完全可预期每个步骤的输入输出都很明确。如果交给 ReAct模型可以自主选择跳过某一步、重复某一步甚至临时起意调用一个不该在这个流程里用的工具。成本是浪费的风险却是实打实的——尤其是在涉及写操作的步骤上。第二类调用带副作用的 API。ReAct 天然不保证幂等性。模型在循环里完全可能因为上一次调用超时、或者它对结果不满意就把同一个写接口调用两次。订单创建、消息发送、余额扣减这类操作如果被重复执行后果比浪费点 token 严重得多。我在生产环境里见过因为循环重试导致用户收到三条重复短信的案例那还只是短信如果是重复扣款呢第三类需要严格审计和复现的场景。ReAct 的执行过程是模型一步步临场发挥出来的同样的输入给两次运行可能走出完全不同的路径。金融、医疗、合规审查这类领域任何一次执行都需要能完整解释为什么调用这个接口、为什么在这个顺序。ReAct 的动态性使得这种解释和复现变得极其困难。我用一个简单的表格来归纳 ReAct 与 Workflow 的适用场景差异对比维度ReAct 模式Workflow 模式任务路径未知、开放、需要探索已知、固定、高度可预期模型控制权模型主导全部决策流程引擎主导模型只做局部任务上下文使用全部历史动态追加每步只传入必要的结构化信息稳定性依赖模型临场发挥依赖流程设计和代码质量调试难度困难路径不可复现容易每个环节可单独回放典型场景开放问答、研究分析、动态规划内容处理管道、业务审批、自动化流程看完这张表基本就清楚了一个原则任务不确定性越高ReAct 越有价值任务确定性越高Workflow 越有优势。而真实业务里大量任务其实都属于后者只是很多人一开始被智能体这个概念带着走默认选了 ReAct。3. 事件驱动 Workflow 的核心设计把模型决策变成流程决策3.1 从让模型想下一步到让事件触发下一步Workflow 编排的核心思想很简单把一次复杂任务拆解成多个确定的步骤步骤与步骤之间通过事件来衔接而不是通过模型的自主判断来衔接。我之前做的调研辅助智能体改造后的流程变成了一条清晰的事件链用户提交问题 → 触发question_received事件。该事件被意图理解处理器消费 → 提取问题类型、关键实体、检索条件 → 产出结构化检索参数 → 触发retrieval_params_ready事件。该事件被知识库检索处理器消费 → 执行检索 → 产出检索结果摘要 → 触发knowledge_retrieved事件。该事件被数据库查询处理器消费 → 根据条件查询结构化数据 → 与知识库结果合并 → 触发data_merged事件。该事件被答案生成处理器消费 → 汇总后生成最终答案 → 触发answer_ready事件。最终答案交由输出组件推送给用户。注意这里有一个关键变化每个处理器都是在响应一个事件而不是在替模型执行一个判断。模型不再拥有全局控制权它只是某个处理器内部的一个执行组件负责完成这一步的局部任务。全局流程由代码和数据决定而不是由模型随机决定。这个变化带来的直接好处是流程变得可预测了。无论用户输入多离谱只要事件按顺序被正确地触发和消费整个任务就会按照既定的路径推进。模型在其中的职责被限制在一个极小的范围内——理解一次具体指令、提取若干结构化字段——这远比让它在整个任务空间里探索要稳定得多。3.2 步骤编排与上下文隔离每个 Step 只看它该看的信息在设计 Workflow 时我最看重的一个原则是上下文隔离。ReAct 模式下模型能看到完整的对话历史和所有中间工具调用结果。看似信息丰富实际上大量信息对这个步骤来说都是噪音而且白白增加了 token 开销。Workflow 模式下每个处理器的输入应该被严格限定它只需要接收完成本步骤所需的最小上下文通常是一个结构化的 JSON 数据块。举个例子。在会议纪要生成这个流程中步骤二是提取待办事项。这一步的模型输入不需要完整的会议转录全文也不需要历史对话只需要前一步生成的会议纪要文本。我把输入控制在这个范围模型处理得更快、更准token 消耗也大大降低。实现上下文隔离有一个实用技巧每个处理器的输出必须是结构化的而不是自然语言。也就是说不要让模型输出一段散文式的分析让下一个步骤去理解而是要求它输出严格的 JSON字段清晰、类型明确。下游处理器直接解析这个 JSON 作为输入不需要模型再去读一遍上游的原始结果。我常用的结构化中间数据结构大概是这样的{ event_id: evt_00123, task_id: task_456, step: extract_todos, input_summary: { source: meeting_minutes, document_id: doc_789 }, extracted_items: [ { content: 确认新版本发布流程, assignee: A, due_date: 2025-06-30, priority: high } ] }这种设计让每个步骤的输入输出都像是一个个小型的可验证的接口。调试的时候只需检查每个事件的 payload 是否符合预期就能快速定位问题出在哪个环节而不需要像 ReAct 那样把整个思考过程回放一遍。3.3 云端 API 的接入方式同步调用、异步任务与 Webhook 回执Workflow 里必然要调用各种云端 API但 API 的调用方式会直接影响工作流的健壮性这部分如果不处理好后续会有很多隐性问题。我把它分成三种类型同步调用适合耗时短、可快速返回结果的 API比如文本向量化、关键词提取、短文本分类。这类调用直接requests.post()等待返回即可。但要注意设置合理的超时时间建议 510 秒超时后走重试或降级逻辑。异步任务 轮询适合耗时较长的操作比如视频转写、大规模文档解析。这类 API 通常提供一个提交任务接口返回一个task_id真正的结果需要通过查询接口获取。Workflow 中遇到这类 API不应该让流程傻等而应该把任务完成视为一个独立事件的触发条件。异步任务 Webhook 回执这是最优雅的方式。云端 API 处理完后主动推送结果到你的回调接口你的服务收到回调后再触发下一个事件。这样的好处是整个流程可以完全事件驱动没有无意义的轮询开销。我在设计事件驱动工作流时对云端 API 接入有一个比较成熟的选择方式API 类型调用耗时推荐接入方式失败处理文本分类/提取秒级同步调用超时重试最多 2 次文本转写/解析分钟级异步任务 Webhook记录 task_id超时后查询状态批量数据写入分钟级异步任务 轮询/Webhook幂等键控制失败补偿流式生成秒到分钟流式响应/同步断点续传部分结果缓存一个核心建议是凡是可能耗时超过 10 秒的云端 API一律采用异步模式接入。如果业务对实时性要求高宁可把整体流程拆得更细让长任务提前跑也不要让整个工作流阻塞在一个同步等待上。4. 手把手迁移一个会议纪要与待办追踪智能体的两种实现对比理论说了一大堆我用一个具体的例子来演示迁移过程。这是一个我近期做得比较完整的模拟项目会议纪要与待办追踪智能体。功能要求如下用户上传会议录音或转录文本。系统生成结构化会议纪要。从纪要中提取待办事项标注负责人和截止日期。将待办事项写入任务管理系统。向相关成员发送通知。这个流程非常典型步骤明确、涉及长耗时 API、涉及带副作用的写操作。它很适合用来展示 ReAct 和 Workflow 的差异。4.1 ReAct 版本的关键代码与问题暴露点如果按 ReAct 模式实现第一版代码的伪代码大概是这样的tools [ transcribe_audio_tool, # 让模型决定是否调用 parse_meeting_tool, # 让模型决定是否调用 extract_todos_tool, # 让模型决定是否调用 create_task_tool, # 让模型决定是否调用 send_notification_tool # 让模型决定是否调用 ] def run_agent(user_input): messages [{role: user, content: user_input}] for _ in range(MAX_STEPS): response llm.generate( messagesmessages, toolstools, tool_choiceauto ) if response.tool_calls: tool_result execute_tool(response.tool_calls) messages.append(response.message) messages.append({role: tool, content: tool_result}) else: return response.content看起来挺简洁但实际跑起来问题很多。最典型的三个问题转写音频是一个长耗时异步任务。模型调用转写接口后拿到的只是一个任务已提交的中间状态。模型不知道应该轮询等待还是先做别的很多时候它就随便输出一个转写中请稍候之类的回答整个流程就悬在那里了。创建待办是写操作。如果模型认为第一次调用没成功其实成功了它会再次调用create_task_tool导致任务管理系统里出现重复数据。调试极难。某个环节出错后你很难知道模型当初为什么做出了那个决定因为它的推理链是不可复现的。更隐形的问题在于上下文膨胀。五步流程走下来完整对话里掺杂了模型思考、工具调用参数、中间返回结果token 消耗量是理想情况的数倍。对于需要频繁执行的业务场景这个成本很快会变成不可接受。4.2 Workflow 版本的关键代码与设计考量Workflow 版本的设计则完全不同。我把整个流程拆成五个事件和五个处理器形成如下的执行链audio_uploaded → 触发 transcript_requested transcript_ready → 触发 minutes_required minutes_generated → 触发 todos_extraction_required todos_extracted → 触发 task_creation_required tasks_created → 触发 notification_required每个处理器只干一件事。比如生成会议纪要处理器只负责调用大模型将转写文本整理成纪要它不关心待办提取更不关心通知发送。实际代码结构大致如下event_handler(audio_uploaded) def handle_audio_uploaded(event): task_id submit_transcription(event.payload.audio_url) emit(transcript_requested, {task_id: task_id}) event_handler(transcript_ready) def handle_transcript_ready(event): transcript fetch_transcript(event.payload.task_id) minutes call_llm(f将以下转录文本整理成结构化会议纪要: {transcript}) emit(minutes_generated, {minutes: minutes}) event_handler(minutes_generated) def handle_minutes_generated(event): todos call_llm(f从会议纪要中提取待办事项输出JSON数组: {event.payload.minutes}) validated_todos validate_todos(todos) # 必须校验字段LLM输出不可直接信任 emit(todos_extracted, {todos: validated_todos})这种设计带来的最大变化是模型不再决定流程走向只负责一个局部任务。它不能选择跳过创建待办这一步骤也不会在转写任务还没完成时假装结果已经出来。全局流程由事件引擎控制模型的随机性被牢牢限制在局部。4.3 两种模式的调试成本对比这个项目我先后用两种模式各实现了一版调试体验的差异非常直观。ReAct 版本在测试环境跑挂了一次转写任务提交后模型没有等待直接尝试生成纪要和待办结果拿到的是空内容还顺手创建了几条待办事项——内容是等待转写结果。当时我在日志里看到这幕第一反应是这 AI 也太蠢了但冷静下来想想这其实是架构问题让模型自主决定流程顺序就必然要承担它做出错误决策的风险。Workflow 版本则完全不一样。转录任务提交后事件是transcript_requested下游处理器根本不会在transcript_ready之前被触发——这是代码逻辑保证的不需要模型判断。某次转录 API 一直没回调我只需检查任务系统里那个task_id的状态发现是云端 API 那边超时了重新推送一次转录完成事件就自动继续整个链路非常容易理解和恢复。对比结果我用表格记录一下对比项ReAct 版本Workflow 版本转写等待模型自主决定是否等待常出错事件完成才触发下一步逻辑保证重复写操作可能重复创建任务幂等事件消费天然防重单次任务 API 调用次数1225 次46 次出错定位需要分析模型推理链直接定位到具体事件和处理器平均耗时模拟数据约 90 秒约 40 秒5. 构建事件驱动智能体的工程细节状态、幂等、超时与重试把架构从 ReAct 换成 Workflow代码写起来只是第一步。真正让这个系统能扛住生产压力的是那些容易被忽略的工程细节。我重点讲四个状态持久化、幂等消费、超时重试、成本控制。5.1 事件总线与持久化状态让 Workflow 跑一半也能恢复事件驱动的核心是事件总线。如果你用的是云厂商的消息队列产品比如一些托管 MQ 服务可以直接用如果是自建系统用消息中间件或云数据库也能实现基本的事件表。但有一点你不能妥协事件必须持久化不能只存在于内存里。原因很简单。Workflow 里可能有一个步骤调用了耗时几分钟的云端 API如果在这期间服务重启了内存里的事件全部丢失整个流程就断在那里了。我建议至少做到两点事件落库。每次事件触发都写入事件表记录事件 ID、类型、payload、创建时间、消费状态。执行状态持久化。每次任务开始时在数据库中记录任务状态例如pending → running → succeed → failed。处理到哪个步骤、下一步是什么都要能查询出来。有了这两层持久化即使服务中途宕机重启后也可以扫描未完成的任务从断点继续推进。这是 ReAct 模式完全不具备的特性——ReAct 一旦中断上下文丢了就是丢了。5.2 云端 API 调用的幂等设计与失败恢复事件驱动系统里消费者可能因为超时或异常对同一个事件执行多次。如果处理器里有写操作比如创建待办、发送通知就必须保证幂等。我的做法是给每个事件一个全局唯一的event_id在处理器内部维护一个已处理事件 ID 集合。消费事件时先检查这个 ID 是否已经处理过如果处理过就直接跳过或者返回已处理的结果。这里有两种常见方案数据库唯一约束。在业务表里加一列event_id并建立唯一索引。重复插入时数据库会拒绝这天然防止了重复创建。Redis 去重。利用 Redis 的SETNX命令事件 ID 不存在时才执行处理逻辑。适合对性能要求比较高的场景。我倾向于数据库唯一约束作为第一道防线因为它是最终一致的、不需要担心缓存失效问题。Redis 去重可以作为辅助手段减少不必要的重复计算但不能作为唯一依赖。另外对于云端 API 的失败恢复要区分两种错误可重试错误网络抖动、5xx 错误、超时。这类错误要重试但要搭配指数退避策略避免一次性并发重试打爆上游服务。不可重试错误参数校验失败、认证失败。这类错误重试多少次都不会成功应该直接把事件标记为失败记录原因走人工或死信处理。我习惯的默认配置是最多重试 5 次重试间隔按 1s、2s、4s、8s、16s 递增。超过 5 次仍失败事件进入死信队列并触发告警通知。不要让一个坏事件阻塞整个队列这是基本素养。5.3 成本控制与配额管理从 ReAct 切换到 Workflow 后API 成本通常会有明显下降但别高兴太早——事件驱动也会带来新的成本陷阱。最典型的一个是事件风暴。某个环节的 API 出现短暂的响应变慢下游多个处理器同时触发重试每个重试都会重新调用云端 API短时间内就能把配额耗掉一大截。解决方案有两个方向一是限制重试的并发度二是给整个 Workflow 设置总超时和总调用次数上限。另一个思路是尽量用小模型做局部任务。Workflow 里的很多处理步骤其实不需要强大的推理能力。比如从纪要中提取待办事项这种任务其实是一个典型的信息抽取任务用中等规模的模型就能做得很好。ReAct 模式下因为需要模型掌握全局能力往往只能使用能力更强的模型成本自然也更高。Workflow 把任务拆小之后你可以灵活地为每一步选择最合适的模型规格。我在实际项目里会用如下策略控制成本意图理解和步骤路由用功能最全的模型。信息抽取和格式转换用体积更小的模型。规则明确的操作比如校验字段、格式化时间直接用代码完成不调用模型。记住一点能用代码解决的问题就不要让模型参与。模型只做那些真正需要理解和生成的环节这样成本自然可控。6. 迁移之后的真实收益和一些不得不说的坑最后聊聊这次迁移后我观察到的真实收益以及一些如果不注意就容易踩进去的坑。先说说收益不然我也不会花这么大力气做这个改造。第一是成本。同一个会议纪要任务ReAct 版本一次任务平均消耗约 25k tokenWorkflow 版本降到了 8k 左右降幅超过六成。这里一半是减少了模型无效思考带来的冗余 token一半是为不同步骤选用了不同规格的模型。第二是稳定性。ReAct 版本的成功很多时候是看起来成功了比如模型生成了纪要但漏掉了某个重要待办或者创建了重复任务。Workflow 版本里每个环节的输出都要经过结构化校验不符合预期的直接报错重跑。系统整体成功率从 ReAct 的大约 80% 提升到了 95% 以上。第三是可测试性。Workflow 的每个处理器都是独立的可以单测、可以 mock 云端 API 来模拟不同情况。ReAct 版本想 mock 就非常困难因为所有步骤都在一个循环里交互影响。再说说坑都是我真金白银踩出来的。坑一过度编排导致灵活性下降。Workflow 最大的风险是把一切流程都写死遇到真正需要探索的任务时系统反应不过来。我现在的原则是混合架构固定流程用 Workflow需要开放探索的环节在某个处理器内部保留 ReAct 能力。比如知识库检索这个步骤里如果检索结果不理想我可以让模型在换关键词重跑、查关联文档、直接生成答案这三个选项里做一个局部决策。这样既保持了整体流程稳定又保留了必要的灵活性。坑二事件消息体膨胀。很多人在设计事件 payload 时喜欢把上一环节的所有结果都塞进事件里导致事件消息越来越大几千甚至上万行。这样做不仅浪费存储也破坏上下文隔离的设计初衷。我的建议是事件消息只携带必要的引用 ID 和一小段结构化输入真正的大数据放对象存储或数据库里通过 ID 读取。坑三死信队列没人处理。事件驱动系统一定会遇到坏事件如果你只做了死信队列但没配告警坏事件就会一直躺在那里用户任务失败后连报错都找不到原因。务必给死信队列配好通知这样至少能第一时间知道出了问题。坑四忽略本地重放测试。Workflow 系统调试时特别依赖本地重放能力。我会要求在测试环境保留所有历史事件任何一次修改后都可以选择某个历史事件重新跑一遍流程对比前后结果差异。这个能力在 ReAct 时代根本没法实现但在 Workflow 模式下它就是一套简单的管理脚本而已值得花费一点开发时间。迁移完成之后我自己的感觉是ReAct 更像一个聪明的实习生给它一个模糊目标它可能给你惊喜也可能给你惊吓而 Workflow 更像一条成熟的流水线每一步做什么、产出什么标准件心里有数。真实业务要的不是惊喜而是确定性和可维护性。这不是说 ReAct 没有价值而是说聪明的实习生应该被用在真正需要创新的环节而不是被撒在整个流水线上乱跑。后面我再做智能体类的项目默认起点都是 Workflow只有遇到真正的探索型子任务时才会在局部打开 ReAct 的能力。这个边界建议你在搭建自己的智能体时也认真想一想。