
我最早做 AI Agent 应用的时候思路很简单一个 Agent 对应一个任务喂进去 Prompt拿到结果完事。后来东西越做越多我发现真正折磨人的根本不是单个 Agent 的能力上限而是当任务拆成多个环节、需要好几个 Agent 前后配合时编排就成了大问题——A 的输出要给 B 当输入B 跑挂了怎么办任务跑到一半进程重启了状态去哪找系统发布升级的时候正在执行的协作任务会不会直接作废OpenRig 就是在这个背景下被我折腾出来的一个多智能体编排内核核心就一句话把离散的 AI Agent 编织成一个可持久化、可恢复、可观测的协作系统。这篇文章我会把它的设计逻辑、核心模块、以及代码层面怎么落地完整拆开来讲适合已经用 Agent 在跑真实业务、但任务一复杂就开始失控的开发者参考。OpenRig 这个名字里的 Rig 取的是装配架的意思——单个 Agent 是散件Rig 负责把它们装到一条产线上让它们按流程协作并且整个流程的状态是落盘的、不会因为任何一次崩溃就归零。它不是某个大厂的开源框架而是我在实际项目中反复重构出来的一个精简实现下面所有内容都以这套设计为准。1. 当 Agent 开始单打独斗OpenRig 想解决的真实问题先描述一个很典型的场景。假设我要做一份技术调研报告拆成三步Agent-A 负责搜集资料Agent-B 负责根据资料写初稿Agent-C 负责校对和补充。如果三个 Agent 各自独立运行那么我只能这样操作先跑 A把结果复制出来粘贴给 B再跑 B再把输出复制给 C。这个过程有几个致命问题一是上下文传递靠人肉搬运中间只要有一次格式对不上后面的 Agent 就理解错。二是整个流程没有状态概念A 跑完了、B 还没跑这时候进程崩了或者我把服务关了A 的结果就丢在临时内存里重新启动后一切都得从头再来。三是没法做审计哪个 Agent 在什么时间点产出了什么结果、基于什么输入完全没有记录。OpenRig 想解决的就是把这套人肉协调 状态缺失的工作方式升级为系统协调 状态持久化的协作机制。它需要一个简单的数据模型来描述任务也需要一个运行引擎来驱动若干 Agent 按序或按条件执行还需要一个持久化层把每一条事件、每一个中间结果都记下来这样即便某一步执行失败、进程被杀、机器重启整个协作任务也可以从最近的快照恢复而不是推倒重来。我在设计 OpenRig 之前画过一张对比表用来明确目标场景和边界特性普通单 Agent 应用需要 OpenRig 的多 Agent 协作任务结构单一步骤多步骤、有依赖、有分支上下文来源用户输入 模型记忆上游 Agent 输出 共享状态失败恢复重试一次即可需要恢复到某个历史状态并续跑可观测性日志够用需要事件级审计和状态快照并发需求低串行足够多任务并发需要隔离和调度如果你的应用还停留在第一列那确实不需要引入编排框架直接写个循环调今天就能用。一旦到了第二列你会发现每个环节都在劝退——所以我决定自己做一个轻量级的内核而不是直接搬 LangGraph 或者 Temporal 那种全家桶。原因后面会讲。2. 编排模型从链式 call到显式状态机OpenRig 的核心编排模型不是画 DAG也不是写一堆 if-else 的链式调用而是一个显式的事件驱动状态机。为什么这么选这要从三种常见编排方式本身的特性说起。2.1 三种编排方式对比第一种是链式调用就是agent_a()返回后直接调用agent_b()。写起来最简单但有三个天然缺陷代码里写死流程想插入一个中间环节必须改代码没有任务级别的状态存储中间产物只能留在内存难以处理条件分支比如如果 Agent-A 的结论不完整就追加一个 Agent-D 补充这种逻辑链式调用很难优雅表达。第二种是声明式 DAG/Workflow 编排。LangGraph、Prefect 这类工具属于这个范畴它们用图结构描述谁依赖谁由框架负责调度。好处是表达能力确实强复杂业务能画出一张漂亮的流程图坏处是状态流转是框架内部维护的很多实现里状态是隐形地存在内存里或者依赖外部存储但对崩溃后从哪一步恢复这件事处理起来仍然偏重。而且框架一旦抽象层级高排查问题就会很痛苦——你往往不知道框架自己在后台维护了哪些隐含状态一旦出 bug根本无从下手。第三种就是 OpenRig 采用的方案显式事件驱动状态机。我把整个协作任务定义为一个状态机每个 Agent 的执行是一个状态迁移动作Agent 的输入、输出、异常都变为事件追加到持久化的事件流中。状态机自己管自己不依赖框架在后台偷偷维护任何东西。这样做的直接收益是任何一个时刻只要拿到事件流和历史状态就能精确回答任务现在到哪一步了、经历了什么。用表格对比更直观编排方式灵活性状态可观测性崩溃恢复排查复杂度链式调用低差差低声明式 DAG高中中中显式状态机高高好中2.2 OpenRig 状态机的具体定义一个协作任务在 OpenRig 中由以下元素构成状态State任务生命周期中的某个阶段比如PENDING、RUNNING、WAITING_INPUT、COMPLETED、FAILED、CANCELLED。事件Event触发状态迁移的因比如AGENT_STARTED、AGENT_OUTPUT_READY、AGENT_FAILED、TIMEOUT、RETRY_LIMIT_EXCEEDED。迁移表Transition Table描述当前状态 事件 → 下一个状态 要执行的动作这是状态机的核心OpenRig 会用一张显式的字典表来定义。举个具体例子。一个简单的调研 → 写作 → 校对任务迁移表长这样(INIT, AGENT_A_DONE) - 触发 Agent-B (WAITING_B, AGENT_B_DONE) - 触发 Agent-C (WAITING_C, AGENT_C_DONE) - 标记 COMPLETED (WAITING_C, AGENT_C_NEED_FIX) - 重新触发 Agent-B带批注 (ANY, AGENT_FAILED) - 记录失败原因进入 FAILED 或按策略重试这里的状态不是存在内存里的变量而是持久化到存储中的实体。每次状态迁移都会先落盘事件、再更新状态这样即便事件已落盘但状态未更新就崩溃了重放事件就能把状态补回来。我在这套模型上吃到的最大的教训是不要把 Agent 的执行逻辑和状态迁移逻辑混在一个函数里。状态机只关心发生什么事件、迁到什么状态、接下来该派谁而 Agent 内部到底是用 LangChain 的链、还是纯 Prompt 调用、还是外部 API那是 Worker 层的事情两者解耦之后改 Agent 实现完全不影响编排。3. 持久化层的选择为什么最终选了事件溯源 快照而不是只存终态持久化是整个 OpenRig 的地基。最开始我在这个问题上偷过懒——想着只要把每个任务的最终结果存进 Redis 就行中间过程不重要。结果跑了不到一个星期就发现这想法在简单任务上没问题但多 Agent 协作根本不是一个最终结果那么简单。3.1 只存终态为什么不行一个协作任务的过程本身就携带大量信息Agent-A 的输出可能是 Agent-B 决策的依据Agent-C 输出的校对比值得留档某一步失败是输入问题还是模型抽风也需要通过当时上下文来判断。如果只存终态一旦任务到不了终态那前面所有已完成环节的结果就全部丢失。而且多 Agent 协作中有大量部分完成的状态——A 和 B 已完成、C 正在跑、此时系统重启如果只存终态重启后我只能看到任务进行中但根本不知道 A 和 B 的结果是什么C 又该基于什么继续跑。3.2 事件溯源模式所以我改用了事件溯源Event Sourcing模式通俗讲就是只追加日志不覆盖历史。每个 Agent 的输出、每次状态迁移、每个错误记录都作为一个不可变事件追加到事件流中。当前任务状态则是通过对事件流做归约Reduction计算出来的——技术上我缓存了一个已归约到第 N 个事件的偏移量这样不必每次从头全量重放。事件溯源带来的额外好处有三个。第一是审计完整谁在什么时候做了什么一目了然。第二是回滚友好如果某次 Agent-B 的输出质量太差影响了后续流程我可以把事件流剪回到 Agent-B 执行之前的偏移量然后重新派发任务。第三是天然支持调试复现——保留事件流就相当于保留了完整的输入现场前台 Agent 反映上次任务结果不对时我拉出事件流看一遍就知道是哪一环出的问题。3.3 为什么用 Redis 而不是直接上数据库不少朋友看到事件溯源第一反应是上 Postgres 或者 MongoDB但我最终选型是 Redis并且是 Redis Stream Hash 持久化配置三件套。原因主要有三个一是事件流的读写模式天然适合 Stream。Redis Stream 的XADD是 O(1) 追加消费者组Consumer Group机制天然支持多个 Worker 并发消费不同任务的事件不需要自己写游标管理。二是任务的上下文数据给 Agent 的输入、输出的结构化字段用 Redis Hash 存随机读效率极高。任务跑起来之后Agent Worker 要频繁读取当前任务的完整上下文Hash 的HGETALL操作在毫秒级。换成数据库表每次都得多一条 SQL 的序列化和网络开销量大了容易成为瓶颈。三是热点数据的特性。协作任务在运行期间对状态和事件流的访问非常频繁但任务一旦结束历史数据基本就冷了Redis 天然的基于内存的读写性能非常适合这种热时频繁、冷后少读的模式。冷数据我后来又做了策略导出到对象存储做长期归档这是后话。3.4 Redis 持久化机制在这个场景下的具体取舍这里要连带回应一个常被问到的问题Redis 本身就是内存数据库你凭什么说它持久化答案在于 Redis 自带的 RDB 和 AOF 机制。我在 OpenRig 里用的是RDB AOF 同时开启的配置。RDB 做定期快照负责快速恢复基准数据AOF 做追加日志负责把两次快照之间的写入补全。具体配置如下appendonly yes appendfsync everysec save 900 1 save 300 10 save 60 10000appendfsync everysec是我权衡后的选择——每秒刷一次磁盘极端情况下最多丢 1 秒数据但对 Agent 协作任务来说丢 1 秒事件几乎无感换来的吞吐提升却非常明显。如果你要追求极致的零丢失可以改成always但那样 Redis 的写性能会掉到接近普通磁盘数据库的水平对于高频事件流的场景不划算。需要强调的是Redis 的持久化解决的是Redis 进程重启后数据不丢这一层问题它本身不解决任务重复执行的问题——后者要靠 OpenRig 的幂等控制来做这是第 6 节会展开的边界话题。你如果在网上搜redis 持久化机制详解看到的大多是 RDB/AOF 的原理和区别但很少有人告诉你在多 Agent 编排这种事件多、状态散、并发写频繁的负载下到底该怎么配、为什么这么配。我在实际项目里踩通的这套参数组合是直接可复用的。4. 代码结构拆解Dispatcher、Worker、Recovery 三件套如果把 OpenRig 比作一家公司那么 Dispatcher 是前台调度Worker 是执行岗位Recovery 是事故处理小组。下面把三个模块的核心职责和关键实现逐一拆开。4.1 Dispatcher事件的入口与分发中枢Dispatcher 是所有事件的唯一入口。它的职责有三接收上游任务启动请求把任务注册进状态机监听来自 Agent Worker 的完成事件、失败事件驱动状态迁移根据迁移结果派发下一批任务到消息通道。我给 Dispatcher 设计的是一个 Python 类核心方法只有三个用 FastAPI 暴露 HTTP 接口后外部业务要启动一个协作任务时只需要 POST 一个 JSON 即可。class Dispatcher: def __init__(self, redis_client, config): self.redis redis_client self.stream config.event_stream self.group config.consumer_group async def start_task(self, task_id, agent_specs, initial_context): 启动一个协作任务写入初始事件并创建状态机实例。 event { type: TASK_INIT, task_id: task_id, agent_specs: agent_specs, context: initial_context, ts: time.time() } await self.redis.xadd(self.stream, event) state_key ftask:{task_id}:state await self.redis.hset(state_key, mapping{ status: INIT, current_agent: agent_specs[0][name], attempts: 0 }) return task_id这里有一个容易被忽略的设计start_task做两件事先写事件再写状态。如果先写状态、再写事件时崩溃重放会出问题反过来先写事件、再写状态重放事件时能自己把状态推算出来即使状态没写入也是安全的。这个顺序在很多分布式系统教材里是常识但在 Agent 场景下尤其重要——因为一次崩溃后的恢复本质上就是重放事件、重建现场。4.2 WorkerAgent 的执行壳Worker 是真正调用 LLM 的地方。每个 Worker 负责执行一个具体的 Agent 实例。它的核心逻辑在execute方法里我加了三个重要机制超时控制、重试计数、幂等标记。class Worker: def __init__(self, agent_func, name, timeout60): self.agent_func agent_func self.name name self.timeout timeout async def execute(self, task_id, context, attempt): run_id f{task_id}:{self.name}:{attempt} try: result await asyncio.wait_for( self.agent_func(context, run_id), timeoutself.timeout ) return {status: OK, output: result} except asyncio.TimeoutError: return {status: TIMEOUT, output: None} except Exception as e: return {status: FAILED, message: str(e)}这个设计里最关键的是run_id。每次执行都携带唯一 IDLLM API 调用时我会把它放到请求的元数据里。下游系统比如知识库写入、外部 Webhook在收到结果时先查这个 ID 是否已经处理过如果处理过就直接丢弃——这是整个系统幂等性的基础。LLM 输出天然不具备确定性同一个 Prompt 跑两次结果不一样所以我没法靠校验输出内容是否相同来做幂等只能靠每个执行动作有唯一 ID来实现至少且尽可能只执行一次。Agent 函数本身是由开发者自己定义的OpenRig 不做任何限制。它可以是 LangChain 封装好的 chain也可以就是一段直接调 OpenAI SDK 的函数还可以是本地跑的开源模型推理。这种编排框架不绑架 Agent 实现的设计是我从一开始就刻意坚持的。4.3 Recovery崩溃后的恢复机制Recovery 模块是 OpenRig 区别于普通脚本编排的关键。它做的事总结起来就是三个步骤扫描、重放、补偿。扫描阶段Recovery 会遍历 Redis Stream 中所有TASK_INIT事件找出那些启动了但还没进入COMPLETED状态的任务。重放阶段把该任务的事件流从第一条重新归约一遍重建出完整的上下文和状态机迁移过程。补偿阶段则是对那些已经派发给 Worker、但 Worker 的结果事件没回来的情况做处理——也就是把这条任务重新塞回待执行队列。补偿逻辑的画像是这样的async def recover_suspended_tasks(self): # 找出状态为进行中、且距最后活跃时间超过阈值的任务 for task_id in self.scan_active_tasks(): last_event await self.get_last_event(task_id) if time.time() - last_event[ts] self.idle_timeout: # 该任务疑似因进程崩溃而停滞 self.requeue_task(task_id)这段话背后隐含一个判断什么情况下该认为任务死了最简单的做法是看最后一条事件的时间戳超过某个阈值比如 30 秒就认为 Worker 可能已经崩溃。这里要非常小心不能因为 Worker 处理 LLM 响应比较慢就误判——LLM 一次调用可能要几十秒阈值必须根据最慢 Agent 的耗时设定否则会频繁触发无效重派。4.4 为什么不用现成的 Agent 编排框架写到这里其实已经回答了开头提到的一个问题为什么我不直接选一个主流框架而是自己造这套轮子。主流的 Agent 编排框架当然能用但我在实际使用过程中总碰到几个痛点第一是框架的状态管理不够透明。很多框架把状态存在内部数据库中开发者要查某个任务现在卡在哪一步往往得去翻框架的实现代码或者通过不完整的 Dashboard 去猜排查效率很低。OpenRig 的事件流是原生的、全程可见的任何一个时刻我用 Redis CLI 就能查全。第二是抽象层级太高不好嵌入现有系统。很多框架要求你把整个业务流程用它的 DSL 或者特定数据结构写一遍和已有代码的融合成本很高。OpenRig 是库而不是平台我提供的只是 Dispatcher、Worker、Recovery 这几个可拼装的模块业务代码是可以完整保留的外壳。第三是定制成本。多 Agent 协作这种场景非常新今天的业务明天可能就变了封装的框架往往无法覆盖我们特殊的需求比如Agent 需要互相 review 后决定是否返工这种非标准流程自己控制状态机反而更好改。5. 一个完整示例让 3 个 Agent 协作完成一份技术调研报告理论讲了很多还是看一段能直接运行的实践更直观。这里我用 OpenRig 编排一个技术调研报告生成任务需要三个 Agent 协作Collector 负责搜集资料Writer 负责生成初稿Reviewer 负责校对并把修改意见回传给 Writer。5.1 定义 Agent 动作每个 Agent 就是一个异步函数接收结构化上下文返回结果。Collector 的简化实现如下async def collect_materials(context, run_id): topic context[topic] # 实际项目中这里会调用搜索引擎 API 或知识库检索接口 docs await search_knowledge_base(topic) return {materials: docs[:10], count: len(docs)}Writer 接受 collector 的产物并调 LLM 写稿async def write_draft(context, run_id): materials context[materials] prompt build_prompt(基于以下材料撰写技术调研初稿, materials) draft await llm_call(prompt, run_id) return {draft: draft, status: DRAFT_READY}Reviewer 检查初稿质量给通过或需要修改两种结论async def review_draft(context, run_id): draft context[draft] review_result await llm_call( 请审阅以下技术调研初稿指出事实错误和结构问题。若无需修改则输出 APPROVED。, draft ) if APPROVED in review_result: return {status: APPROVED} return {status: NEEDS_FIX, comments: review_result}5.2 注册状态机和执行流程接下来把这几个 Agent 挂到 OpenRig 上核心就是注册 Agent 名称到函数的映射以及状态迁移规则from openrig import Dispatcher, Worker, Recovery agent_registry { collector: Worker(collect_materials), writer: Worker(write_draft), reviewer: Worker(review_draft), } dispatcher Dispatcher(redis_clientredis, configconfig) # 注册迁移规则 dispatcher.register_transition( trigger_eventTASK_INIT, next_agentcollector ) dispatcher.register_transition( trigger_eventCOLLECTOR_DONE, next_agentwriter ) dispatcher.register_transition( trigger_eventWRITER_DONE, next_agentreviewer ) dispatcher.register_transition( trigger_eventREVIEWER_DONE, next_agentNone, # 流程结束 ) dispatcher.register_transition( trigger_eventREVIEWER_NEEDS_FIX, next_agentwriter, # 回到 Writer 返工 )这里REVIEWER_NEEDS_FIX是一个很典型的多 Agent 协作特征事件——它不是简单的一条直线流程而是存在了一个环Writer 写完初稿Reviewer 审阅后打回Writer 根据批注修改Reviewer 再审。如果做成静态 DAG这种带反馈回路的流程画图会非常麻烦而显式状态机只需要多加一条迁移规则就能实现。5.3 启动任务与故障注入启动一个任务task_id await dispatcher.start_task( task_idreport_202412_001, agent_specs[collector, writer, reviewer], initial_context{topic: 多智能体编排的主流架构} )假设执行过程中Writer 所在的进程在生成完初稿后突然崩溃。正常情况下事件流里已经有COLLECTOR_DONE和WRITER_DONE但WRITER_DONE对应的上下文结果还没有被持久化到任务状态中Recovery 启动后执行重放发现WRITER_DONE事件确实存在于是向前推进把 Reviewer 重新调度起来。这样整个任务只损失了一次多余的 Writer 重试实际上连这次都不需要因为结果是安全的其余全部无缝衔接。这就是持久化协作落地的直观效果——任务不会因为一个 Worker 挂了就全部作废重启后就像什么都没发生一样继续推进。我在演示这套流程时最喜欢做的一件事就是在运行中途直接kill -9掉 Worker 进程然后重新拉起 Recovery 服务让在场的人看任务自己续命的画面。这个演示比任何 PPT 都有说服力。6. 并发与恢复的边界踩过的坑和我的取舍最后必须聊一聊实际运行中踩过的坑因为设计文档永远写不出这些教训。6.1 LLM 非确定性与幂等的真正边界多 Agent 编排系统里最让我头疼的是 LLM 输出的非确定性。同一个 Prompt、同样的输入两次调用结果可能差别很大。这在幂等设计上直接制造了一个难题如果 Agent-B 对同一份上游结果执行了两遍得到略微不同的输出下游怎么办我的解法是执行结果以第一次为准。具体做法是Worker 在执行过程中如果发现run_id已经被处理过查 Redis 里有没有这个执行记录的键就直接返回之前的结果不再调用 LLM。这样即使 Recovery 因为误判重复派发了任务第二次执行也不会产生新的 API 消耗和新的不相关内容。这里有个细节值得单独说你的 LLM 调用函数必须支持透传 run_id 并去重。我在对接 OpenAI SDK 时是在请求的user字段里带上 run_id在结果返回后立刻用 SETNX 写入 Redis 标记已执行。如果 SETNX 返回 0说明之前已经执行过直接把旧结果捞出来返回一步到位。6.2 Redis Stream 消费者组的挂起消息陷阱Redis Stream 的消费者组模式本来是给我省事的但它在 Worker 崩溃时留下了一个坑已派发给消费者的消息会进入 Pending Entries ListPEL如果消费者长时间不响应消息会一直卡在 PEL 里不会被其他消费者接手。默认配置下这个状态能把我整个任务流堵死。解决办法是显式设置空闲消息认领机制。我写了一个定期执行的调度器遍历各消费者组的 PEL找出IDLE时间超过阈值的消息用XAUTOCLAIM把这些消息重新归给其他存活消费者。核心代码如下async def claim_stale_messages(self, group, consumer, min_idle_ms30000): # Redis 6.2 及以上支持 XAUTOCLAIM entries await self.redis.xautoclaim( self.stream, group, consumer, min_idle_timemin_idle_ms, start0-0, count100 ) return entries这个机制和 Recovery 的重新入队配合起来才能实现真正意义上的自愈。如果你用的是旧版 Redis 没有 XAUTOCLAIM就只能先 XPENDING 查 PEL再 XCLAIM 手动认领写法繁一点但效果一样。6.3 快照频率的取舍事件溯源最担心的问题之一是重放太久。如果任务跑了两小时产生了 10 万条事件崩溃后要从第一条开始重放恢复时间会变得很长。所以 OpenRig 引入了快照策略每处理完一个 Agent 的完整生命周期即一个 Agent 从开始到输出被确认就把当前任务状态的全部上下文做一次快照存储到 Redis Hash 中同时记录快照覆盖到事件流中的哪个偏移量。恢复时先加载最近快照再重放快照之后的事件重放成本大幅降低。快照也不是越频繁越好。写快照本身是 I/O 操作如果每个 Agent 一小步就写一次高频任务下 Redis 写入压力会明显增大。我的经验值是单 Agent 执行时间在 5 秒以上的任务每完成一个 Agent 拍一次快照很合适如果 Agent 执行特别快、单环节小于 1 秒就改成每完成 5 个 Agent 拍一次避免快照开销吞掉执行收益。这个阈值没有标准答案建议搭建时用压测定自己的最优值。6.4 扛并发时真正的瓶颈在 LLM API 限流最后关于ai agent 怎么扛并发这个问题很多人一上来就研究消费者组、线程池、异步 IO但实际上多 Agent 系统并发能力的瓶颈通常不在编排框架本身而在 LLM API 的限流。我的一个任务动辄要调用 5 到 8 次 LLM API如果同时来 100 个任务瞬间就是 800 次并发调用任何 API 供应商都会按 QPS 限流返回 429 错误。OpenRig 的调度层我做了一个简单的信号量限流器从 Redis 里获取一个 token 才能调用 LLM API用完归还全局控制并发 QPS 在供应商限额的 80% 左右。这个配额管理代码不算长但在实际压测中它比任何高级调度策略都更能决定系统能不能稳定跑起来。class RateLimiter: def __init__(self, redis_client, max_qps50): self.redis redis_client self.key openrig:llm_rate_limit self.max_qps max_qps async def acquire(self): # 用 Redis 的滑动窗口算法做限流 now time.time() window_start now - 1 count await self.redis.zcount(self.key, window_start, now) if count self.max_qps: await asyncio.sleep(0.05) return await self.acquire() await self.redis.zadd(self.key, {str(now): now}) await self.redis.zremrangebyscore(self.key, 0, window_start) return True编写这段代码时请特别注意递归和 sleep 的边界条件实际生产环境我建议用 Redis 官方推荐的 INCR EXPIRE 滑动窗口模板会更简洁这里展示的是最直白的语义版本方便理解原理。6.5 最后一点体会OpenRig 跑到现在我最大的感触是多智能体编排的重点从来不在智能而在工程纪律。Agent 本身是聪明但不可靠的它们会产生非确定性输出、会超时、会抽风但只要你把每次执行变成一条不可变事件、把每个状态迁移定义清楚、把每次恢复逻辑都做成可测试的路径这些不可靠就变得可以管理。而我实际运行中最受益的反而是最初设计的事件全部落盘这个略显繁琐的习惯——它让每一个诡异问题都能被回放、被复现、被解决也让团队协作时的沟通成本降了一个量级。如果你也在做多 Agent 系统我的建议是不要急着堆功能先把状态能不能恢复、事件能不能回放这两个问题想清楚再动手。Surface 上的花活永远是 Agent 的 Prompt 技巧但真正撑起一个系统的永远是底层的状态管理和恢复机制这些看似不性感的工程细节。