Agent底层重构实战:从执行模型到记忆分层,打造可靠AI Agent地基

发布时间:2026/9/28 15:52:00
Agent底层重构实战:从执行模型到记忆分层,打造可靠AI Agent地基 Agent 项目做得越多我就越有一种感觉大模型只是引擎真正决定 Agent 上限的是它脚下的地基。像 Orkas 这种自研的 Agent 运行时表面上是调度模型、调工具、管记忆的一层壳可一旦业务跑起来shell 下面全是坑——上下文爆掉、工具调用链断掉、状态散落各处、多 Agent 一发消息就互相踩踏。我这次干脆把 Orkas 的底层推倒重写从执行模型、记忆分层到工具抽象全部换血不是为了炫技而是旧架构已经撑不住真实场景。这篇文章就是这次底层重构的完整复盘包括设计取舍、核心模块的实操写法以及踩过的坑。如果你也在自研 Agent 框架、做 agent 架构选型或者正打算从 0 到 1 搭一个能扛事的 AI Agent这里面的思路和代码可以直接抄作业。1. 为什么非动地基不可旧版 Orkas 的四宗罪1.1 能用和靠谱之间的那一大段落差先说背景。Orkas 最早是一个实验性 Agent 框架设计得很天真核心思路就是把用户的请求拼进 prompt调大模型把结果再拼进下一轮。跑 demo 确实没问题做 RAG 问答、写个周报、调一两个 API 都挺顺。但当我把它接到一个内部数据看板助手时问题开始集中爆发一次任务要串十几个工具中间还有分支和人工确认旧框架根本表达不了这种流程只能在代码里套 case改一处崩三处。当时团队里流传一句话它不叫 Agent叫 Chatbot with Tools。这话虽然损但我认同。真正的 Agent 需要自主规划、调用工具、根据结果修正下一步还要能在中途失败时自愈。旧版 Orkas 本质上是一条直线prompt 进、文本出最多在中间塞一个 ReAct 风格的循环而且这个循环没有任何中断和恢复机制。用户等了三分钟却只看到一句带 error 的终止信息这种体验没法上线。我在调研了一圈现有方案之后发现很多 agent 框架的底层也没好到哪里去。场景一复杂大家纷纷开始自己造执行引擎市面上的开源项目也大多在编排层浅尝辄止真正把内核做硬的不多。这就更坚定了我要重写 Orkas 的想法——不是修修补补而是把地基换掉。1.2 从线上信号里读出的四个核心病根决定重构之前我花了两个星期给旧 Orkas 做体检把线上日志和性能数据翻了个底朝天最后总结出四个病根。第一个是上下文膨胀。旧框架把每轮对话、每个工具返回值全部塞进同一个上下文20 轮交互之后 token 数轻松破万。模型开始遗忘早期信息用户说我刚才让你查的那个数据呢Agent 一脸茫然。我后来统计了一下长会话里有超过一半的 token 是历史信息真正的当前意图只占很小比例这纯粹是在拿钱买痛苦。第二个是工具调用链断裂。旧架构里每一步都假设模型一定会正确返回工具调用但真实情况是API 超时、返回格式不规范、鉴权过期、下游服务 500任何一环出问题整个任务就死在那一环。日志里最常见的错误就是 agent execution terminated due to error没有重试、没有降级、没有部分结果的保留。第三个是状态管理混乱。旧 Orkas 把状态存在内存全局变量里单实例没问题一旦要支持多 Agent 协作A 的临时文件 B 能看见B 的中间结果 A 也容易改活脱脱一个公共澡堂。后来我加了锁发现锁比业务逻辑还多代码已经没法维护了。第四个是没有可观测性。出问题只能靠 print 大法根本不知道 Agent 在某一步想了什么、调了什么工具、为什么跳到了错误分支。排查一个线上事故动辄半天。这四个病根其实是互相关联的上下文没治理模型就乱模型乱工具调用就容易失败失败处理不到位状态就开始错乱状态错乱又让排查更困难。所以我才说这不是加两个补丁的事必须动地基。2. 重构的核心设计思路拆解2.1 四层分离内核、记忆、工具、编排各管各的账新 Orkas 的第一个设计决策是把过去All-in-One的模块拆成四层内核层、记忆层、工具层、编排层。这个边界划分看似朴素但它是整个重构的定海神针。内核层只负责一件事跑通模型推理 工具调用结果回填的最小循环。它不理解任何业务也不知道工具具体做什么只知道要调一个模型服务、拿到一个动作、执行动作、把结果交给下一步。这样内核可以保持稳定业务变化不会冲击到底层。记忆层独立出来后终于不再跟 prompt 编写混在一起。过去写 Agent记忆中短期、记忆长期、记忆什么都得自己拼 prompt现在记忆层统一提供读和写两个接口内部自己决定哪些信息放上下文窗口、哪些放向量库、哪些进长期知识库。对外部来说Agent 只是记得更多、忘得更少。工具层做了一个 Agent 与外部世界的隔离带。所有工具调用都走统一的协议工具可以是一个 HTTP API、一段本地脚本、一个数据库查询甚至另一个 Agent。这样工具之间不需要知道彼此的实现方式只用关心输入输出格式。编排层是唯一允许有业务逻辑的地方。它决定一个任务分几步、每一步用哪个 Agent、失败后走哪条降级路径、什么时候需要人工介入。这层以前是代码写死的流程现在改成了可配置的状态机业务同学都能看懂流程长什么样。这四层之间的关系我用一句大白话总结给团队听内核是发动机记忆是油箱和导航工具是外挂设备编排是方向盘。发动机不关心你要开到哪方向盘也不关心油路怎么走各司其职才不至于一踩油门整个车都抖。2.2 执行模型选型为什么是事件驱动的状态机旧 Orkas 用的是链式调用一次任务就是一串顺序执行的函数。这种模型最大的死穴在于它假设你不会中途停下来。但真实 Agent 任务常常要等用户确认、等异步任务完成、等外部 Webhook 回调。链式调用一等就会占住整个线程要么超时要么把所有流程塞进回调地狱。新 Orkas 换成事件驱动的有限状态机Finite State Machine with Event Queue。核心思想是每个任务是一个状态机实例它的状态包括 idle、planning、tool_calling、waiting_input、completed、failed 等等。状态之间通过事件迁移比如收到工具结果事件后从 tool_calling 迁移到 planning决定下一步是继续还是结束。这样做有三个直接好处。第一任务可以异步化状态机挂在队列里事件到了再唤醒线程资源不会被白占。第二任务可以暂停和恢复用户说等我一下状态机停在 waiting_input三天后用户回来说继续事件一推它从原来状态接着跑。这在旧架构里是不可想象的事。第三每个状态迁移都可以打日志和指标天然可观测。有人会问为什么不用图编排Graph我也考虑过 LangGraph 那种方案但对我们这种需要精细控制中断和恢复的场景状态机更直观而且状态数量可控。图编排适合流程特别复杂、分支特别多的场景代价是调试时你得盯着一条边一条边看。Orkas 目前的业务没有复杂到那个程度状态机加一个轻量级规则引擎反而更容易维护。2.3 记忆体系分层短期、中长期、永久记忆到底怎么落地Agent 记忆是被聊得最多也最虚的一个概念。我在重构里把记忆拆成了三层对应不同速度和成本。短期记忆就是上下文窗口里的内容只有当前执行任务相关的信息才放进来与任务无关的历史一律不进 prompt。比如用户连续问了三个问题旧框架会把三个问题全部带上新 Orkas 只保留当前这轮的意图和必要背景其他内容被丢到中层记忆。中期记忆放在向量数据库里用于想起来但不必时刻带着的信息。比如用户半个月前说过的偏好在一次新任务中需要用上我会从向量库里做相似度检索只取 Top K 片段注入短期记忆。这里要特别注意检索策略不能只按文本相似度还要加上时间衰减和来源权重。否则会出现久远的相似文本比近期的重要信息更靠前这种离谱情况。长期记忆是结构化知识库存的是经过提炼的事实用户身份的属性、项目的元信息、历史决策的原因等。这类信息不按片段存而是按实体存比如用户李明 / 偏好 / 邮件里直接说结论。这样记忆不会随着对话轮次膨胀也不会因为检索误差丢掉关键事实。实现上我给每一条记忆记录都加了三个字段importance重要性、recency时间衰减因子、access_count被读取的频率。写入时按重要性决定是否进长期库读取时按一个加权分数排序而不是单纯相似度。这套机制上线后长会话里的 token 消耗降了将近 40%而用户对它记得我的体感反而更强了。3. 核心模块的实现细节与实操记录3.1 Agent 内核重写让agent execution terminated due to error成为过去式内核层重写时我最想消灭的就是那个冷冷的一句 agent execution terminated due to error。旧框架里这句话出现时用户只能重试新框架里这句话应该变成第 2 步工具调用失败已尝试降级方案任务继续。要实现这个效果内核必须自带三类机制错误分类、重试与降级、部分结果保全。错误分类是所有机制的前提。我把 Agent 执行中的错误分成四类模型不可用、工具不可用、输入不合法、业务逻辑不满足。前两类是系统问题可以重试第三类是上游数据问题不能盲目重试得给模型反馈原始报错信息让它调整第四类是流程问题要跳到人工确认。错误处理的伪代码我写成了下面这个结构# agent_core/executor.py class Executor: def run(self, task_event): state self.state_machine.get_state(task_event.task_id) while not state.is_terminal(): try: action self.model_client.plan(state.context) if action.type tool_call: self.emit(tool_calling, action) result self.tool_registry.invoke(action) state.add_observation(result) if not result.ok: self.apply_retry_policy(action, result, state) elif action.type respond: return self.finish(action.content) except ModelTimeoutError: state.context self.compress_context(state.context) # 压缩后重试 self.emit(model_retry, action) except ToolAuthExpiredError: self.refresh_credentials() continue return self.state_machine.get_failure_report(state)这套逻辑的要点是任何异常都会被状态机记下来并且有一条明确的恢复路径而不是直接杀掉整个任务。比如模型超时我会先做一次上下文压缩把冗余的历史信息摘要化然后带压缩后的上下文重试一次工具鉴权过期就去刷新凭证刷新完在原地重放这个工具调用。实际跑了一段时间我的观测数据是旧框架里大约 18% 的任务会以 error 终止新架构在加了重试和降级之后这个比例降到了 4% 左右而且剩下的 4% 大多是业务规则上本来就不该继续的情况。这 4% 会带着完整的状态和原因报告返回给用户而不是一句冷冰冰的 error。3.2 工具抽象层Skill 和 Tool 的边界别再傻傻分不清在 agent 开发社区里skill 和 agent 的区别、[skill 和 tool 的区别]是高频搜索词。我在重构之前也纠结了很久最后总结出一条非常实用的判断标准Tool 是原子能力Skill 是带策略的编排单元。一个 Tool 只做一件事比如查询订单状态发送邮件获取天气。它没有状态输入输出都是 JSON。Skill 则是一段可复用的过程它可以串联多个 Tool并且内置决策逻辑比如客户投诉处理这个 Skill会先查订单、再查售后记录、然后按规则判断是退款还是补偿最后才调用邮件工具。这个过程如果全部逻辑都放在 Agent prompt 里模型成功率很难保证但放进 Skill 里模型只需要决定调用哪个 Skill具体的步骤由代码执行。定义接口的时候我给 Tool 和 Skill 都留了统一的入参格式interface Tool { name: string description: string inputSchema: JSONSchema invoke(input: Recordstring, unknown): PromiseToolResult } interface Skill { name: string description: string canHandle(input: Recordstring, unknown): boolean // 快速命中判断 execute(input: Recordstring, unknown, agent: AgentRuntime): PromiseSkillResult }注意 Skill 的 execute 里我传入了 agent 运行时是因为 Skill 在执行过程中可能还需要临时请求模型做一次判断比如这个退款金额是否在合理范围内。这种模型参与决策、代码控制流程的模式比纯代码死流程灵活又比纯 prompt 驱动可靠。在重构过程中我做出过一个错误决策一开始为了让Skill 很强大把所有 Skill 的实现都写得很重里面有各种条件分支。结果发现模型根本不知道什么时候该用哪个 Skill命中率极低。后来我把大 Skill 拆碎让小 Skill 尽量聚焦在一个场景再用一个轻量路由层去匹配意图命中率立刻上来了。记住一句话Skill 不是越大越好而是要边界清楚、描述能让人一眼听明白。3.3 多 Agent 协作改造从共享内存到消息总线多 Agent 协作是 Orkas 这次重构里最刺激的一部分。旧架构里多个 Agent 共享同一个上下文内存可以说是灾难Agent A 查到的数据被 Agent B 当成了自己查的Agent C 改了某个全局变量Agent A 的后续判断直接跑偏。这种伪协作上线即事故。新架构改成了消息总线 Worker 池模式。每个 Agent 是一个独立的状态机它们之间不共享任何内存只通过消息通信。消息有标准信封sender、receiver、message_type、payload、trace_id。其中 trace_id 尤其重要它让所有跨 Agent 的消息都能串起来出一张完整的链路图。我实现了一个非常简单的 Manager-Worker 协作示例# orchestration/collab.py class ManagerAgent: def on_task(self, task): workers self.allocate_workers(task) for subtask in task.decompose(): envelope Message( senderself.id, receiverworkers[subtask.skill], message_typesubtask, payloadsubtask.dict(), trace_idtask.trace_id ) self.bus.send(envelope) def on_result(self, envelope): self.results[envelope.payload[subtask_id]] envelope.payload if all_done(self.results): merged self.merge_results(self.results) self.respond(merged)这套模式看着简单实践中最容易被忽视的是消息时效和幂等性。Agent A 给 Agent B 发了一条查库存的消息B 处理了三次每次都广播一次结果A 这边怎么知道该以哪次为准我最后的方案是给每条消息带一个 idempotency_key接收方在去重表里按 key 去重。数据一致性上我的原则是宁可重复处理也不丢消息处理前先写日志处理后再标记完成这样至少能保证可重放。多 Agent 协作里另一个血泪教训是不是所有任务都适合拆给多个 Agent。有些任务拆开之后光是传递中间结果就能把上下文占满反而比单 Agent 更慢。我给 Orkas 写了一个简单的收益评估函数如果子任务之间的依赖密度高于阈值就不拆只有子任务确实可以并行且结果可以合并且不互相强依赖时才走多 Agent 路线。这个判断逻辑让我少走了很多弯路。4. 重构过程中踩过的坑与排查方法4.1 上下文膨胀导致的记忆混乱问题新框架上线第一次压力测试就出现了诡异现象Agent 明明有向量记忆库却总在重复提问同样的问题。一开始我怀疑是检索有问题查了半天日志才定位到——问题出在短期记忆写入时没有做去重。每轮对话的历史都被原样塞进上下文哪怕这些信息已经提取进了长期记忆短期窗口里还是有一堆冗余副本。模型被冗余信息干扰反而抓不住重点。排查思路是给每一轮 prompt 打一个 hash 标签然后用脚本统计相同 hash 出现的频率。结果显示20 轮内的 prompt 里有 12 轮存在超过 30% 的重叠内容模型每周转一次都会重新看一眼同样的背景介绍。解决方案是引入记忆摘要压缩每当短期记忆快要超过窗口上限就调用一次轻量模型把旧内容压成一个 500 字以内的摘要且必须包含用户的核心诉求和已确认的事实两类信息。压缩后的摘要替换掉被压缩的原始片段。这个操作看似损失了细节但实际体验是 Agent 的上下文连续性提高了很多因为它再也不会被几十条历史对话淹没。4.2 工具调用链断裂的调试实录重构后一个很典型的事故是Agent 调用查询订单工具成功后紧接着调生成物流面单工具失败了但整个任务没有终止而是在一个奇怪的分支里反复重试把用户给整懵了。我追查发现重试策略出了问题——新版内核把所有工具调用失败都视为可自动重试的系统错误但这个失败其实是业务逻辑错误订单已取消不能生成面单按理说应该跳过这个工具走订单状态异常分支。定位手段是我给每次工具调用都加了一个状态迁移记录打印出来之后发现任务卡在一个死循环里生成面单失败 - 重试 - 又失败 - 再重试。问题的根子在于异常分类不细导致业务错误被当成系统错误处理。修复方式是在工具返回结果里增加一个 error_category 字段强制所有工具开发者必须标注错误属于 retryable 还是 terminal。同时在编排层配置了一条规则terminal 类错误触发结果解释分支让模型基于错误原因向用户解释而不是无脑重试。这个小改动上线后工具链断裂导致的任务失败率又降了一截。4.3 性能与成本的平衡缓存、并行和批处理怎么搭配Agent 框架重构完功能是有了但一看账单成本比旧版高了不少因为重试机制、摘要压缩、向量检索都是额外的模型调用和计算开销。我花了一周做性能调优核心就三件事缓存、并行、批处理。缓存包括两层。第一层是工具结果缓存对相同参数的查询类工具结果缓存 5 分钟比如天气、库存这类不会秒变的接口。第二层是模型结果缓存如果模型在完全相同的上下文和工具列表下返回过相同的规划结果直接复用历史结果。实测下来有约 15% 的模型调用可以通过第二层缓存省掉。并行化针对的是多工具场景。以前一个任务要调 3 个工具是串行调的现在我把互相之间没有数据依赖的工具调用打包成一个 ParallelGroup一次性并发发出。日志里的 latency 从原来的 3 个工具 6 秒降到了 2.5 秒左右。批处理则是把摘要压缩这类旁路任务集中调度不是每个会话一满就立刻压缩而是攒到空闲时间或者批量任务时统一做。这样能避免压缩操作抢主干请求的资源。需要注意批处理会带来一点时效延迟所以只适合晚几分钟处理也没关系的记忆整理任务。优化之后整体成本回落到旧版的 85%而且因为失败率低了实际有效完成任务的平均成本低于旧版。这组数据也让我更坚定了一个观点不能为了省成本牺牲可靠性可靠之后成本反而能降下来。5. 重构完成后的验证与经验沉淀5.1 回归测试与稳定性验证里的关键指标重构完成后最重要的不是它能跑而是它是不是真的比旧版强。我建立了一套回归场景集包含 30 个典型的 Agent 任务单工具查询、多工具串联、中途用户打断恢复、工具失败后降级、多 Agent 协作任务。每个场景都要记录四个指标。第一个是任务成功率新 Orkas 从旧版的 76% 提到了 91%这个数字是最直观的。第二个是平均完成时长多工具场景从 12 秒降到了 5.8 秒主要收益来自并行调用和状态机减少无效重试。第三个是上下文平均 token 数重构后比旧版下降了约 40%直接影响成本和模型稳定性。第四个是失败任务的可诊断率旧版是 20%新版做到了 95% 以上基本上每条失败都能给出完整的状态迁移链和错误原因。我自己最看重的其实是可诊断率。因为 Agent 系统不可能做到 100% 成功只要失败原因能快速定位业务团队就有能力逐步完善流程如果失败原因永远查不清那成功率再高也让人心里没底。5.2 我体会最深的三件事第一件事是地基重写这种活最忌讳的就是一边重构一边加新功能。前期我们差点把记忆分层和多 Agent 消息总线同时做出来后来硬生生压住先把执行模型跑稳再一层层往上加。第二件事是任何 Agent 框架最后比拼的都是异常处理的质量而不是模型调得多花哨。模型在正常路径上几乎不会出错真正的差距全在出错后怎么办。第三件事是重构的价值要拿数据说话不是我感觉快了我感觉稳了而是成功率、成本、耗时这些数字缺一不可。Orkas 这次底层重构从设计到落地用了将近三个月。期间无数个夜里对着状态迁移图发呆也在生产环境里见过凌晨三点的报警。但如果再让我选一次我还是会推倒重写——因为 Agent 这个方向未来要承载的东西远比现在多。地基不稳楼越高越危险。希望这份复盘里的思路和代码能给正在做类似重构的你一点参考。