凌晨两点,你负责的多 Agent 系统正在跑一个跨 6 个子 Agent 的复杂任务。任务已经完成了 70%,这时告警响了:其中一个子 Agent 的模型出了严重 bug,必须立即替换。你打开控制台,想先重启它——然后你愣住了:这个 Agent 一旦重启,它手里的所有中间状态全没了,正在执行的子任务会断裂,上下游 Agent 的交接会全部中断,整个工作流可能要从头再来。

这不是假想场景。多 Agent 系统一上线,你一定会遇到这个问题:Agent 跑起来了,但你还在持续迭代它。停机、替换、重启——这是最直觉的做法,但对有状态的 Agent 系统来说,代价高得吓人。Hot-Swap(热替换),也就是让系统在不停止运行、不中断在途任务的情况下,把一个内部组件换成新版本,在 Agent 场景下远比微服务复杂。
Kubernetes 早就解决了微服务的滚动更新问题,但 Agent 和微服务有一个本质区别:微服务重启后是无状态的,连接断了可以重连;Agent 重启后,它跟用户和上下游 Agent 的对话上下文、它尚未走完的决策路径,全都丢了。
这篇文章我想聊:如果你必须在运行中的多 Agent 系统里替换一个子 Agent,真正可行的设计是什么。我会从状态管理、路由切换、交接协议三个层面展开,最后给出一个能落地的实现思路。
为什么 Agent 的重启比微服务贵这么多?
一个微服务重启,最坏的情况是丢失几个在途请求,客户端重试就行。一个 Agent 重启,丢失的可能是整条推理链。
| 维度 | 微服务 | Agent |
|---|---|---|
| 状态 | 无状态或轻量会话 | 多轮上下文、中间推理结果 |
| 失败代价 | 请求失败,重试即可 | 整条工作流断裂 |
| 恢复方式 | 客户端重连 | 必须从检查点恢复 |
| 依赖关系 | 调用其他服务的 API | 依赖其他 Agent 的输出来拼接上下文 |
我记得有项目上线第一天就踩了这个坑:一个子 Agent 在长任务跑到 80% 时崩了,监控显示它把关键中间结果只存在内存里,没有任何持久化。我们只能眼睁睁看着整个工作流失败,然后从零重跑。那一刻我意识到:在 Agent 系统里,状态不落地,重启就是在赌运气。
这里说的"状态",不只是数据库里的业务数据,还包括:
- 对话上下文:Agent 到目前为止说过什么、收到过什么
- 工具调用轨迹:它调了哪些工具、拿到了什么结果
- 决策分支:它为什么选择了这条路径
- 未完成目标:它承诺要做但还没做完的事
这些运行时状态,就是热替换的前提:你必须有办法把它们序列化、迁移和恢复。做不到这一点,换 Agent 等于杀 Agent。
热替换的三种策略,各有什么代价
我梳理了一下业界常见的做法,大致有三种。名字是我自己起的,因为 Agent 热替换太新,还没有统一术语。
策略一:冷启交接。新 Agent 实例启动,完全看不到旧 Agent 的中间状态,只保留最终目标,重新跑一遍。优点是实现简单;致命缺陷是之前 70% 的工作白白浪费,而且如果任务依赖时间顺序(比如已经调用过外部 API 发了请求),重跑还可能造成副作用重复。
策略二:Checkpoint 恢复。旧 Agent 定期把状态快照写到持久化存储,替换时新 Agent 从最近快照恢复。优点是不浪费进度;代价是需要框架内置状态序列化能力,并且最后一段未快照的状态会丢失。
策略三:影子交换。新旧 Agent 同时运行,流量继续发给旧 Agent,同时复制一份给新 Agent。等新 Agent 跑完一段、结果与旧 Agent 语义一致,再切换流量。优点是不会让错误立即影响生产;代价是双倍算力,而且 Agent 是有随机性的系统,要求结果完全一致几乎不可能,只能做语义级对比。
落地:状态序列化与 Checkpoint 的放置位置
你可能会想:Agent 的状态不就是一堆 JSON 吗?存下来就行。没那么简单。Agent 状态里最麻烦的是不可序列化的对象——打开着的数据库连接、正在等待的异步任务、已经加载的本地代码。这些都没法快照。
所以设计上必须做一个关键让步:你只能快照到可控的暂停点。Agent 的状态流转必须被设计成可打断的——在每个原子步骤之间,状态是一致的、可以落盘的。
async function agentLoop(taskId, context) { while (!isDone(context)) { await saveCheckpoint(taskId, context); const action = await decide(context); const result = await executeAction(action); context.merge(result); } return context.finalOutput(); }
这段代码最重要的决定是 saveCheckpoint 放在动作之前,而不是之后。为什么?如果放在动作之后,动作执行到一半崩溃了,状态没存上,恢复后系统会重新执行这个动作——但动作可能已经对外部产生了副作用。放在动作之前,配合幂等的动作设计,崩溃后重放就是安全的。
一个真实的性能经验:检查点的开销没有你直觉那么大。不用每次序列化全部历史,只保存增量 delta,配合定期全量快照,单次 checkpoint 可以控制在毫秒级。
路由切换:让新的子 Agent 接住新流量
状态搞定了,下一个问题是流量怎么切。微服务领域有成熟方案,但 Agent 场景多了一个难点:父 Agent 和子 Agent 之间不是简单的请求-响应,而是长上下文的任务委托。
想象这个场景:Agent A 把子任务委托给子 Agent B,然后 A 在等 B 的结果。此时要替换 B,A 完全不知道 B 已经被换了。解决思路是引入一个代理层——Agent Gateway。所有子 Agent 的调用都不直连,而是经过网关:
Agent A → Agent Gateway → Sub-Agent B
网关维护一份镜像路由表,标明逻辑 Agent 名指向哪个版本的实例。替换时只需更新路由表:
/sub-agents/intent-classifier: active: v1.0 standby: v2.0 status: migrating
流量切换分两步走:新请求走新版本,在途任务走旧版本。新发起的子任务交给新 Agent;已经委托出去、执行到一半的任务,让旧 Agent 继续完成。这借鉴了 Kubernetes 滚动更新的思路,但区别在于,K8s 对在途请求的态度是"等请求结束就杀",而 Agent 场景必须做到任务级感知——不是等一个请求结束,而是等整条推理链结束。
交接协议:旧 Agent 与新 Agent 的三次握手
到这里,最关键的机制来了。替换发生时,新旧两个 Agent 之间需要一个明确的交接协议,分三个阶段:
- Preparing(准备):网关通知旧 Agent 即将被替换。旧 Agent 停止接收新任务,把所有在途任务的状态推送到存储。
- Handoff(交接):网关把流量切到新 Agent。新 Agent 从存储读取检查点,恢复上下文,并向网关确认恢复成功。
- Cleanup(清理):网关确认旧 Agent 没有在途任务、且新 Agent 已就绪,然后关闭旧实例。
里面有一个极易被忽略的细节:旧 Agent 必须活到新 Agent 完全就绪之后。如果先杀旧再启新,中间会有一段没有 Agent 在干活的时间窗口,上下游全部超时。正确顺序是"先建后杀"。
这是我交过学费的地方。我第一次实现时把 Cleanup 放在了 Handoff 前面。演练一切正常,直到有一次,新 Agent 恢复检查点时发现数据不完整,而旧 Agent 已经被杀了。整个任务卡死——检查点读不出来,旧实例又没了。那一刻我意识到:检查点机制自身也必须支持重试和修复,不能假设它一次就能成功。
什么时候不需要热替换
看到这里,你可能已经在心里盘算怎么给自家系统加这个功能了。先冷静,以下三种情况,不要实现热替换:
- 你的任务基本都是几十秒内完成。重启的代价完全可以接受,引入热替换的复杂度是亏的。
- 子 Agent 之间没有共享状态,输入输出接近纯函数。失败重跑的成本很低。
- 系统还在快速原型阶段,模型和提示词天天改。此时设计热替换等于在流沙上打地基。
我见过不少团队在产品验证阶段花一周做热替换,结果产品方向变了,整个机制作废。Hot-Swap 的正确适用时机,是你的系统已经稳定运行、用户依赖它持续产出、并且你无法接受半小时的升级窗口。这个阈值,比你直觉认为的要高得多。
再补一个局限性:热替换解决不了提示词升级带来的行为漂移。新版本 Agent 恢复旧状态后,可能用不同的方式处理同一个上下文。对精度要求高的任务,这是隐患。所以生产环境里常见的做法是:热替换用于修复 bug 和回滚,重要能力升级仍然走完整重跑流程。
回到开头那个问题:凌晨两点,任务跑到 70%,子 Agent 出 bug,怎么换?现在的答案应该是:不是重启,是热替换。核心就是三件事——状态落地、路由切换、交接协议。状态落地让进度不丢,路由切换让新请求找得到新版本,交接协议让新旧之间无缝握手。
但我也要说一句清醒的话:Agent 热替换是一个高成本工程,本质上是在给尚不稳健的运行时加麻醉剂。如果你还在快速迭代 Agent 本身,优先把状态持久化和工具幂等做扎实——那才是热替换的地基。地基稳了,热替换只是一片瓦。
原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/677.html