如果你也在做 Agent 开发,大概率经历过这种尴尬:Demo 跑得风生水起,一上真实任务就各种幺蛾子。Orkas 是我这边维护了大半年的一个 Agent 框架,主打多工具编排与长程任务执行。上半年有段时间,线上任务频繁报错,日志里清一色“agent execution terminated due to error.”,用户反馈的语气也越来越冲。我花了两周排查,最后得出的结论非常扎心:问题不在模型,也不在具体某个工具,而在整个框架的地基——执行引擎、记忆体系和模型调用层,这三层叠在一起,像一盘散沙,任何一层出问题,上层就跟着雪崩。
这篇文章就是 Orkas 这次底层重构的完整复盘。我会先把旧架构的结构性债务一条条摆出来,再讲新地基的三层设计:模型网关、三层记忆体系、DAG 编排引擎,以及重构过程中踩过的坑和灰度策略。正准备从 0 搭 Agent 框架、或者正在被自家 Agent 稳定性折磨的朋友,应该能从里面找到一些可以直接抄作业的思路。
1. 重构的导火索:一次凌晨两点的事故,逼我掀了旧房子
1.1 事故现场:30% 的任务在深夜集体失败
那天凌晨,线上监控突然炸了。一个看似简单的任务——"查询本周项目进度,整理成周报发给负责人"——在两个小时内有接近三成直接失败。失败日志非常统一,都是那句让人血压升高的agent execution terminated due to error.。
更让人头大的是另一个现象:不少任务确实"跑完了",但跑了一个多小时,Token 烧了十几万,最后产出一份牛头不对马嘴的报告。用户原话是:"这个 Agent 像是没睡醒,前面聊的好好的,后面突然失忆了。"
我当时的第一反应是换模型。结果换了更强的旗舰模型,失败率只降了一两个百分点,Token 消耗反而更高。第二反应是查工具接口,结果所有工具单独调用全部正常,响应时间都在百毫秒级别。
1.2 排查链路:问题不在模型,而在地基
真正把问题挖出来,是靠一帧一帧看执行链路日志。我梳理了失败任务的共同特征:
第一,上下文越滚越大。任务执行到第 20 轮时,整个上下文里塞满了中间推理过程和冗余的工具输出,早期关键的"项目进度数据"反而被挤出窗口。模型不是不会做,而是看不到关键信息了。
第二,工具返回格式一变化就崩。某个接口偶尔多返回一个字段,下游解析逻辑直接抛异常。而旧架构的错误处理方式是"异常向上抛,任务整体终止",没有任何局部重试或分支降级。
第三,执行流程是硬编码的。旧版用一串 if-else 把"查询进度 → 提取要点 → 生成报告 → 发送"串起来,每一步都要手工指定下一步。这个任务的流程还算简单,那些需要十几步的复杂任务,代码里全是分支判断,同事看了都摇头。
我盯着这些日志想了很久,意识到一个残酷的事实:这些问题不是 bug,是架构设计时就埋下的雷。往下拆,无非三个层面——模型调用逻辑和业务逻辑焊死在一起、记忆体系是现用现拼的临时方案、执行编排是链式回调地狱。任何一个层面都会随着任务复杂度上升而爆炸,三个一起爆,就是我那天凌晨看到的样子。
1.3 决定重构之前,我问了自己三个问题
重构不是小事,尤其是一个已经跑在线的框架。我当时没有冲动行事,而是冷静下来问了三个问题:
修补成本 vs 重建成本。局部打补丁能不能解决?比如给上下文加个压缩模块,给工具调用加个重试。答案是能缓解,但复杂度会指数上升。每加一个特性,就要在三个层面各打一个洞,最后代码会变成一栋到处是补丁的危楼。
旧债是否堵住了新特性的路。当时产品规划里已经列出了多 Agent 协作、长期记忆持久化、跨会话上下文恢复这三个方向。我评估了一下,这三件事在现有架构上几乎做不动。记忆没有分层,跨会话就是空谈;编排是链式的,多 Agent 并行就是空谈。
调用方能否无感迁移。Orkas 当时已经接了好几个业务方,API 层面不能推倒重来。如果必须让调用方改代码,那重构的代价和风险就完全不同了。
三个问题想清楚后,结论很明确:局部修补只是续命,底层重建才是出路。但重建不是把旧代码删掉重写一遍,而是先在旧系统的旁边把新地基搭起来,然后通过兼容层和灰度逐步切换。这个思路贯穿了整个重构过程。
2. 旧地基到底烂在哪:三条结构性债务的详细解剖
2.1 模型调用逻辑和业务逻辑焊死在一起
旧版 Orkas 最大的问题,是"调模型"这件事散落在每个业务函数里。写一个工具函数,里面可能就夹着一大段 prompt 拼接和 LLM 调用代码。今天用 A 厂商的模型接口,明天换 B 厂商,就要把全项目翻一遍;想统一加个限流,找不齐到底有多少个入口在调模型。
更可怕的是上下文管理完全失控。每个 Agent 自己维护一份"对话历史",有的用列表存全部消息,有的只存最后五轮,有的甚至把工具返回的原始报文整个塞进去。没有统一的 Token 预算,没有压缩策略,没有超限保护。结果就是任务一长,输入长度爆炸,模型的注意力被稀释,行为变得飘忽不定。
这也是为什么我后来断言"问题不在模型,在地基"——同一个模型,喂给它的上下文质量天差地别,表现自然天差地别。模型调用层必须从业务中剥离出来,变成一个统一收口的网关,所有流量都从这里进出,才好集中治理上下文、限流、重试和降级。
2.2 记忆体系是"现用现拼"的临时方案
旧版所谓的"记忆",就是两个东西:一个放在内存里的chat_history,一个全局context字典。任务跑完,进程一重启,记忆归零。用户上次说过的偏好、之前任务沉淀下来的结论,全都丢了。
这个设计带来的连锁反应是:长对话只能把所有历史都塞给模型,导致费用和延迟双双失控;跨会话恢复完全靠外部业务方自己拼 prompt,相当于把记忆的责任甩给了调用方;更别提什么"记忆检索"——旧系统根本没有检索,只有全量注入。
当时我也调研过社区里关于"短期、长期、永久记忆"的各种讨论,其实核心思路是一样的:不同记忆的访问频率、重要程度、生命周期完全不同,不能一锅炖。短期记忆要快读快写、短期存活;长期记忆要能检索、要跨会话;永久记忆要结构化、要可审计。旧版不具备这个分层能力,所以在记忆体系上,推倒重来是没有悬念的。
2.3 执行编排是链式回调地狱
旧版的执行编排,本质上是一条链:工具 A 的输出,作为工具 B 的输入,一层层往下传。代码里用Chain对象把它们串起来,每加一个新任务,就要手工写一条新的链。
这个模式在小 Demo 里毫无问题,但一旦任务复杂起来,就全是坑:
- 所有步骤严格串行,明明"查项目进度"和"查团队空闲情况"可以并行,链式结构却只能排队执行。
- 中途任何一个步骤失败,整条链直接断掉,没有局部重试,也没有分支绕行。
- 错误处理全靠 if-else 堆,每个节点都要考虑"失败了我该走哪个分支",代码越写越复杂。
- 多 Agent 协作无从谈起。多个子 Agent 的依赖关系、汇合逻辑、结果整合,在链式模型里根本表达不出来。
我在设计新引擎时,第一条原则就是:执行编排必须从"链式"变成"图"。任务天然是有依赖关系的一组步骤,只有图结构才能表达并行、分支、汇合和失败域。
3. 新地基第一层:模型网关层,把"调模型"变成"发消息"
3.1 统一协议,内部收口
重构后的 Orkas,最底层是一个统一的模型网关,内部抽象出了ModelGateway接口。所谓统一,不是说只用一家模型厂商,而是把各家模型接口都包成同一种语义:
chat(messages, tools, params):对话补全embed(texts):向量化tool_call(messages, tools):强制工具调用
所有业务模块——编排引擎、Agent 运行时、记忆模块——都只能通过这个网关访问模型,不允许任何人绕过它直接拼请求调上游 API。好处非常直接:模型厂商切换变成配置项,限流和熔断在网关层做一次就全局生效,Token 消耗统一计量,再也不用来回翻代码找"到底哪个模块在偷偷调模型"。
这个设计参考了消息队列的思路:业务方不需要关心"发给谁、走什么协议、怎么保证可靠",只负责把消息丢进队列。模型网关就是 Agent 内部的"消息队列",把底层模型的差异全部吞掉。
3.2 上下文治理:Token 预算与注入策略
网关层最核心的职责之一,是上下文治理。我给它划了几条硬规则:
第一,每个任务都有 Token 预算。比如设定单轮最大输入 12000 Token,超了就必须触发上下文压缩策略,而不是无限膨胀。压缩策略分三档:先丢弃最早的历史消息,再对中间推理过程做摘要,最后把工具返回的冗长报文替换成"字段名+关键值"的精简形态。
第二,记忆注入优先走检索,而不是全量注入。旧的上下文管理把"历史消息"当成唯一的信息来源,重构后引入"按需检索":任务开始时,根据当前目标去记忆库检索 top-k 条相关记忆,再拼接进上下文。这直接砍掉了大量无效 Token。
第三,强制设硬上限。即使压缩策略全部失效,单轮输入也不能超过某个绝对值(比如 24000 Token),超过时任务必须停下来向调用方告警,而不是硬撑着继续跑。
这一层治理做完之后,最直观的变化就是 Token 消耗直接降了大概 40%,模型的"失忆"现象也大幅减少——因为它终于能在窗口内看到该看的信息了。
3.3 失败重试与模型降级链
模型调用是外部依赖,外部依赖就一定会抖动。旧版的做法是"失败就抛异常",重构后的做法是在网关层内置三级容错:
- 指数退避重试:瞬时错误(限流、超时)自动重试,间隔从 500ms 开始,翻倍增长,最多 3 次。
- 熔断:连续失败超过阈值,网关直接进入熔断状态,快速拒绝新请求,避免把上游打挂。
- 模型降级链:给每个任务配置一个模型列表,例如"主力旗舰模型 → 备用快速模型 → 本地小模型"。主力模型连续失败时,自动切换下一个。
这套容错机制是重构后见效最快的一层。只加了这个功能,线上任务的"execution terminated due to error"比例就肉眼可见地降了下来,因为大量失败其实不是模型不会做,而是调用链路里的瞬时抖动被当成了致命错误。
4. 新地基第二层:三层记忆体系的重建
4.1 短期工作记忆:会话内的"草稿纸"
短期记忆对应的是 Agent 在当前会话内的活跃状态。我把它拆成三块:
- 最近 N 轮对话消息:存在 Redis 里,键名带会话 ID,TTL 设为 30 分钟。
- 当前任务栈:正在执行的子任务列表、每个子任务的状态、当前执行到的节点 ID。
- 临时变量区:工具返回的中间结果、用户中途补充的信息、推理过程的短暂缓存。
短期记忆的读写路径必须极快,因为每一轮执行都要访问。它不追求持久化,进程挂了可以重建,但一定要保证单轮内的读写一致性。这里我踩过一个坑:最初用纯内存字典存短期记忆,快是快,但执行引擎一旦重启,所有在线会话全部断了。后来改成 Redis + 本地缓存双写,本地命中,Redis 兜底,才算真正稳下来。
4.2 长期记忆:跨会话的"笔记本"
长期记忆解决的问题是:用户上周跟你说过的偏好,这周再来的任务,Agent 应该还记得。它的核心是两条:
记忆提取:每个任务结束后,执行一次 memory consolidation,从会话中提取"值得记住的要点"——用户明确的偏好、项目背景的关键信息、任务产生的结论。提取可以靠一个专门的小模型调用来完成,也可以靠规则 + 关键词来兜底。
记忆检索:新任务启动时,根据当前任务目标生成检索向量,从长期记忆库中召回 top-k 条最相关记忆,注入上下文。
长期记忆的存储我用的是向量库 + 结构化事件表的组合。向量库管"语义相似的记忆",结构化事件表管"有明确属性的记录",比如"用户偏好格式=周报里有数据表"这种,直接查字段比向量检索更准。向量库我选的是 pgvector,没有额外引入新组件,直接在现有 PostgreSQL 里开扩展,运维成本几乎为零。如果你从零搭建,Milvus 或者 Qdrant 也都可以,核心是别把长期记忆做成一张大杂烩表,一定要分语义检索和结构化查询两条路走。
4.3 永久记忆:可审计的"档案柜"
永久记忆是最重的一层,存放组织级知识库、用户身份画像、关键事件日志。这些数据的特点是:几乎不被日常推理直接读取,但必须在需要时能被精确查出,而且必须可审计、可追溯。
我把永久记忆放进了对象存储 + 数据仓库的组合里。对象存储存原文/原文件,数据仓库存结构化的索引和摘要。写入永久记忆必须走审批接口,不允许 Agent 在执行过程中随意写入——这是为了避免 Agent 被恶意注入的指令污染永久记忆区。
说到这必须提一句安全边界:这三层记忆的写入权限是逐层收紧的。短期记忆 Agent 可读可写;长期记忆 Agent 可读,写入要走 consolidation 流程;永久记忆 Agent 只读,写入只能通过管理员接口。社区里讨论的"agent 安全 标签""a-memguard 这类针对 LLM Agent 记忆的防御框架",核心思路都是在记忆读写链路上加防护层。我当时做的是粗粒度的权限隔离,后续可以往"记忆内容本身就是不可信输入,需要校验和过滤"这个方向继续深化。
5. 新地基第三层:从链式调用到 DAG 编排
5.1 为什么必须是 DAG,而不是别的
这是整个重构里花时间最多的决策。我评估过几种方案:状态机、链式、事件驱动、DAG。最后选了 DAG,理由有三条:
表达力。任务是由一组带依赖关系的步骤组成的,图结构可以精确表达"步骤 C 依赖 A 和 B 的结果,等 A、B 都完成才能执行"这种天然并行关系。链式做不到这一点。
失败域隔离。DAG 里每个节点都是独立的执行单元,一个节点失败,可以只重试这个节点,或者走这个节点的备选分支,而不影响已经完成的兄弟节点。这在链式结构里是完全不可能实现的。
可观测性。图结构天然适合做全链路追踪——每个节点对应一个 span,节点之间的依赖关系对应 span 之间的父子关系。排查问题时,顺着拓扑图一眼就能看出堵点在哪。
DAG 不是没缺点,最大的缺点是"执行顺序自由了,但编程模型变复杂了"。所以我做了一件事:把 DAG 藏起来,对外暴露的仍然是很简单的 API——run(goal, tools)。用户不需要手写 DAG,框架根据目标和工具列表自动构建执行图。
5.2 节点状态机与调度器的实现细节
DAG 里的每个节点有一个统一的状态机:
| 状态 | 含义 | 转移条件 |
|---|---|---|
| PENDING | 等待执行 | 所有前置节点进入 SUCCEEDED |
| RUNNING | 执行中 | 调度器分配线程开始执行 |
| SUCCEEDED | 执行成功 | 节点函数返回正常结果 |
| FAILED | 执行失败 | 节点函数抛出异常或返回错误标志 |
| TIMED_OUT | 执行超时 | 超过节点级超时时间 |
| SKIPPED | 跳过 | 条件分支判断为不满足 |
调度器的核心是拓扑排序 + 并发调度。每次调度扫描所有 PENDING 节点,检查前置依赖是否全部 SUCCEEDED,是则放入就绪队列。就绪队列里的节点由线程池执行,并发数用信号量控制——不是越多越好,毕竟每个节点都可能触发模型调用,并发太高容易打爆上游。
节点级超时是我强烈建议你一定要做的。旧版是"整个任务一个总超时",结果一个工具卡住了,整个任务卡住。新版每个节点都有独立的超时配置,比如"工具调用 30 秒,模型推理 60 秒,子 Agent 执行 300 秒"。超时后节点进入 TIMED_OUT,调度器根据配置决定重试、走备选节点还是终止整个任务。
5.3 多 Agent 协作与 Skill/Harness 的分工
DAG 编排带来的另一个红利,是多 Agent 协作终于有了落点。在 Orkas 里,一个子 Agent 本身就可以是 DAG 里的一个节点。父任务把子任务下发给子 Agent,子 Agent 内部又是一个独立的 DAG 执行图,执行完把结果汇报给父节点。这个层级嵌套,在旧链式结构里是根本做不出来的。
这里顺便把社区里经常混淆的三个概念说清楚——Agent、Skill、Harness:
- Agent是一个运行时实体,有自己的状态、记忆和执行循环。它可以被理解为"自主干活的人"。
- Skill是预置的能力模板,把"常用 Prompt + 工具集 + 参数校验规则"打包成可复用模块。相当于给这个人的"职业技能包",装了就多一项技能。
- Harness是承载 Agent 的容器/外壳,负责生命周期管理、资源隔离、输入输出边界。可以说它是这个人的"工位和环境"。
在 DAG 编排里,Skill 被映射为"可复用的子图模板"——你不需要每次都从零构建一个复杂节点,直接引用一个 Skill,框架自动展开成子图。Harness 负责子 Agent 节点的生命周期,主节点可以等待子 Agent 完成,也可以在超时后决定"吞掉子 Agent 结果继续跑"还是"整体失败重来"。
6. 重构过程中的血泪清单:兼容层、灰度与观测
6.1 兼容层:老调用方必须"无感切换"
重构最怕的就是"推倒重来,业务方被强行绑架"。我在新引擎旁边做了一层 Adapter,旧 API 的所有入口保持不动,内部转成新引擎的调用。业务方的 Python SDK 几乎没改,只更新了配置项。
Adpter 层重点是做好参数翻译和错误语义映射。旧版的错误码是一串字符串,新版改用结构化错误对象,Adapter 层要把新错误对象翻译回旧字符串,不然调用方已有的错误处理逻辑全部失效。这个细节没做到位的话,表面"无感",实际全是碎玻璃。
6.2 影子模式与对拍,先证明新引擎不更烂
我从来不信"重构完直接切流量"这种操作。Orkas 的灰度分了三步走:
第一步,影子模式。线上真实流量复制一份发给新引擎,但新引擎的执行结果只落日志,不返回给用户。风险为零,目的只有一个:看新引擎跑同一条链路会不会崩,性能和 Token 消耗是多少。
第二步,对拍。同一批任务同时发给新旧引擎,拿两边结果做质量对比。对拍不是人工看一遍,而是用一组可量化指标:任务是否完成、完成时长、工具调用次数、Token 消耗、用户侧反馈。连续跑一周,确认新引擎在成功率、延迟、成本三个维度上都不弱于旧引擎。
第三步,增量放量。流量切 5%、观察 24 小时、切 20%、再观察、切 50%,最后全量。任何一个环节出现指标负向漂移,立即切回旧引擎。
这套流程走下来大约用了三周。期间最大的收获不是"新引擎终于上线了",而是锻炼出了一套完整的对拍基础设施——构建这个基础设施花的精力,比写新引擎本身还多。
6.3 全链路观测:从"报错不知道在哪"到"一键定位"
旧版排查问题靠的是大海捞针式地翻日志。新版我从第一天就把 OpenTelemetry 打进了引擎,每个 DAG 节点都生成一条 trace span,记录节点 ID、输入摘要、输出摘要、耗时、Token 消耗。
这样再看那条 "agent execution terminated due to error.",排查路径就完全不一样了:打开 trace,看是哪个节点超时、哪个节点返回了错误、哪个环节开始 Token 超预算,基本是直达病灶。插件配置字段冲突这类问题也类似——新引擎所有配置都过 JSON Schema 校验,配置错了在启动阶段就报警,不会等到运行期才炸。
6.4 安全边界:工具调用必须做权限隔离
Agent 能调用的工具越多,安全风险越大。Orkas 重构后做了两件事:
第一,工具注册表 + 权限策略。每个工具在注册时必须声明自己属于哪个权限域(只读、业务写、管理操作、危险操作)。Agent 发起工具调用时,引擎先做一次 policy check,不在白名单里的调用直接拒绝。
第二,工具返回内容做长度和格式校验。防止一个工具返回超大报文把上下文撑爆,也防止工具返回格式异常导致解析器崩溃。校验不通过时,按"降级处理"走:截断、摘要、或标记为失败并重试。
这两条看似简单,但实际上把很多潜在的"越权执行"和"上下文污染"堵在了门外。每次工具调用都像过安检,虽然多了一层检查开销,但对整个系统的稳定性帮助巨大。
7. 重构之后:实测数据与后续演进方向
7.1 重构前后的关键指标变化
这里放一组我在内部压测和对拍时记录的数据,供参考:
| 指标 | 重构前 | 重构后 |
|---|---|---|
| 任务整体成功率 | 68% | 92.5% |
| 平均完成时长 | 22 分钟 | 9 分钟 |
| 单任务 Token 消耗 | 100%(基准) | 61% |
| 工具调用失败率 | 15.3% | 4.8% |
| 因上下文超限导致的任务失败 | 显著 | 几乎归零 |
这组数据的提升不是某一个模块的功劳,而是三层地基共同作用的结果。成功率上升主要靠模型网关的降级与重试;时长缩短主要靠 DAG 并行和记忆检索减少无效轮次;Token 下降主要靠上下文治理和记忆按需注入。
7.2 下一步想做的事
地基重构完,Orkas 的迭代速度明显快了。后续几个方向我已经在推进:
第一,记忆体系的深度治理。现在的三层记忆还是"写完就存、存完就查"的朴素模式。下一步要加记忆衰减(长时间不访问的记忆自动降权)、记忆合并(多条相似记忆自动去重归并)、记忆冲突检测(新记忆与旧记忆矛盾时如何裁决)。这个方向也是最近社区里讨论 Agent 记忆时的热门话题。
第二,动态 DAG 改写。现在 DAG 是任务开始前构建好的,一旦跑起来结构就不变了。但我希望 Agent 在执行过程中根据中间结果动态添加新节点、跳过某些分支、甚至重新规划后半段路径。这等于让 Agent 从"按图施工"进化到"边看边施工"。
第三,评测体系。重构的过程中我深刻体会到一个道理:Agent 框架的演进速度,取决于你有多快能判断"新改动是变好还是变坏"。所以接下来会搭建一套离线评测集,涵盖多任务模板、多模型配置、多难度梯度,每次改动先跑评测再决定是否上线。
最后再说一句个人体会:重构 Orkas 的这大半年,我最深的感触不是"技术方案多重要",而是"给 Agent 搭地基时,永远不要为了省事牺牲边界的清晰度"。模型调用、记忆、编排这三层的职责一旦模糊,短期是省了代码量,长期就是无穷无尽的线上事故。如果你也在做类似的 Agent 框架,希望这次复盘能帮你少走一些弯路。