1. 为什么要动地基:旧架构的三个致命伤
1.1 主循环是“大泥球”:加个工具要动核心逻辑
Orkas 原本是一套我私下维护的 Agent 运行时,最早只有两个文件:一个拼提示词,一个跑while主循环。那段时间跑 demo 非常爽,LLM 输出一个{"tool": "search", "query": "..."},主循环正则一抓,匹配到对应函数就执行,然后把结果塞回上下文继续让模型思考。整个链路看起来完整,实际上一推就倒。
我第一次意识到地基出问题,是往里面加第五个工具的时候。当时要加一个文件写入能力,却发现while循环里散落着三处针对旧工具写的特判逻辑,还有两处为了兼容不同模型的输出格式而打的补丁。新工具怎么接?最稳妥的办法是复制一段分支,再改八个地方。那会儿我站在代码面前,感觉不是在写程序,是在考古。
这还只是“代码组织”层面的问题。更棘手的是运行时没有一个统一的抽象,工具调用失败、超时、返回格式不合法,全靠外面一层try...except兜着。异常路径处理得越多,主循环越不透明。后来我统计了一下,跑一个五步以内的任务,日志要翻三屏,原因在于每一步的输入输出都混杂着框架日志、LLM 的原始返回、以及我手动加的print。想定位一个 Json 解析失败,得肉眼比对肉眼。这种开发体验,放到今天看是完全没有办法做调优的。
1.2 记忆=硬塞上下文:token 爆炸、任务失忆
旧版 Orkas 根本没有“记忆管理”这一层。短期记忆是直接把对话历史拼进 prompt,长期记忆是往 system 里贴一段从数据库捞出来的摘要。听上去简单粗暴,实战中立刻暴露两个问题。
第一个是 token 膨胀。接到一个数据分析类任务时,模型会先读取文件列表、再读取文件预览、再调工具做统计,中间结果和工具返回全部留在上下文里。任务跑到第三步,上下文已经消耗了七成,后面真正要模型发挥判断力的环节,空间反而所剩无几。模型开始表现得很奇怪:明明只让 TA 做汇总,TA 却把前面所有内容复述了一遍。那不是“模型变笨”,是窗口满了之后发生了内容挤占。
第二个是任务失忆。旧版在对话超长之后,会把最早的历史直接截断。这个策略在聊天场景里还能凑合,但在 Agent 任务里是致命的——因为最早的那几轮往往包含了用户的原始目标和约束条件。我遇到过一个真实 case:让 Agent 写一篇行业报告,它执行到中间阶段突然不再按一开始指定的格式输出,反而用了一个更通用的模板。我去查日志才发现,最初的格式要求已经被截掉了。不是能力问题,是记忆策略害了它。
所以后来我把记忆体系彻底拆掉重做,不只在技术上分了三层,还在写入策略上做了严格的“成本控制”,这一点放到后面的核心细节里细说。
1.3 工具调用裸奔:没有权限、没有审计、没有恢复
旧版对工具执行几乎没有任何防护。模型只要输出了格式相近的 JSON,主循环就会调用对应函数,连基本的参数校验都缺。我后来给 Orkas 接上真实文件系统的时候,心里是发毛的:一旦模型被诱导输出了一个删除文件的操作,程序不会问我,会直接执行。
更麻烦的是,旧版没有审计日志。某一次我在调试多步任务时,发现中间的临时文件被改了,但根本查不到是哪一个工具干的。所有调用只有返回值,没有调用方、没有参数快照、没有执行前后状态。那段时间我只能靠“在工具函数里面加 print”来反推,效率极低。
也没有任何意义上的故障恢复。一旦某个步骤抛异常,整个 Agent 就从零开始,已经调完的工具结果全部作废。我试过让它跑一个二十步的爬虫任务,第三部就因临时网络问题崩了,之后整个任务重新来过。每次都从头跑,费用和时间都是双倍支出。
这三个痛点叠加起来,让我做了一个判断:Orkas 缺的不是某个新功能,而是一个能支撑住新功能的底层结构。如果继续在旧代码上打补丁,我面对的会是越来越高的维护成本、越来越无法解释的运行时行为,以及永远不敢上线的尴尬。于是,我决定重写地基。
2. 重构的总目标与方案选型
2.1 新地基长什么样:四层架构与兼容层
重构之前,我先把目标写成了三句话:执行可控、记忆可管、失败可恢复。这三句话分别对应底层架构中的三个核心模块,另外再加一个安全/治理层把它串起来。最终落地的结构是四层:
| 层级 | 职责 | 核心模块 |
|---|---|---|
| 内核层 | Agent 主循环、状态机、事件驱动 | Executor / Event Bus |
| 记忆层 | 短期工作记忆、事件记忆、向量记忆、永久档案 | Memory Service |
| 能力层 | 工具注册、MCP 适配、输出收敛 | Tool Registry / MCP Adapter |
| 治理层 | 权限策略、审计日志、检查点恢复 | Policy Engine / Audit Log / Checkpoint |
设计原则是“单向依赖”:内核层不感知具体工具,只通过接口调用能力层;记忆层对外提供标准读写接口,内部实现随意替换;治理层像切面一样嵌在每一次动作的前后,但不参与 Agent 的业务判断。
同时我还留了一个兼容层,把旧版写好的脚本包了一组 Adapter,让它们能走新的工具接口。这样重写期间旧用例还能回归,不至于非要等全部重构完成才能验证。兼容层的价值在迁移阶段帮了大忙,后面我会讲到。
2.2 为什么没有直接换 LangChain / AutoGPT
决定重写的时候,身边不少朋友的第一反应是:这种轮子为什么不直接用现成的框架?我的回答是,现成的框架我试过,但 Orkas 的定位决定了它不适合。
Orkas 的目标不是做一个“开箱即用的脚手架”,而是做一个可以被嵌入到实际业务系统里的运行时内核。市面上主流 Agent 框架抽象层级偏高,封装了太多的链式调用、回调、代理器。它们适合快速验证想法,但问题是:第一,升级频繁,API 说变就变,跟着框架走相当于把地基交给别人管理;第二,抽象过重,底层细节被掩盖,真正出问题的时候需要钻进去读框架源码;第三,它们自带的工具生态和我的记忆体系设计有冲突,尤其是我想做的细粒度权限控制和检查点恢复,大部分框架并不原生支持。
AutoGPT 和 BabyAGI 这类项目则是反向,它们偏 demo 化,自主性很强但工程化很弱,缺少稳定的状态管理、记忆持久化和审计能力。作为学习材料很好,作为生产内核不行。
所以我当时的结论是:与其花大量时间学习怎么把业务塞进框架,不如花几周时间把一套薄而稳的内核抓在自己手里。Orkas 重写后并不排斥外部生态,它通过 MCP 这种公开协议去接入现成工具,只是核心执行层必须自研。事实证明这个选择是值得的。
2.3 迁移策略:先冻结功能,再拆模块
我确定了一个原则:重写期间,旧版 Orkas 的对外功能全部冻结,不再加新特性。所有精力集中在抽取边界、替换实现、回归验证这三件事上。整个过程分成五个阶段,每个阶段都有明确的“完成标准”:
- 抽取工具层:把主循环里的函数调用统一改成 Tool Registry 注册制。完成标准:旧任务能用新工具层跑通。
- 重写内核:把 while 循环改成有限状态机加事件总线。完成标准:旧任务能在新内核上跑通,且行为与旧版一致。
- 引入记忆服务:替换掉硬塞上下文的旧逻辑。完成标准:多步任务在 token 预算内稳定执行完。
- 加固治理层:加策略引擎、审计日志、检查点恢复。完成标准:故意触发危险操作时会被拦截,进程杀掉后能恢复断点。
- 观测闭环:打通事件日志和全链路追踪。完成标准:一次任务失败可以在 Dashboard 上定位到具体步骤。
每个阶段结束,我都会用同一套测试任务集跑一遍回归。所以哪怕重构中途出了偏差,也能第一时间知道是哪一层出了问题。现在回头看,这套迁移策略比具体技术选择更值得分享。
3. 核心细节实现:执行循环、记忆体系与工具接入
3.1 ReAct 内核:从 while 循环到状态机 + 事件总线
ReAct(Reasoning + Acting)是 Agent 最基本的执行范式:模型观察当前状态,思考下一步,调用一个工具,拿到结果后继续观察。旧版 Orkas 把这个范式写成了一个粗放的while循环,而新版把它改成了状态机驱动的事件循环。
我贴一个简化版的内核结构,用 Python 伪代码表示:
class AgentExecutor: def __init__(self, tools, memory, policy, max_steps=12): self.state = AgentState.IDLE self.memory = memory self.policy = policy self.max_steps = max_steps def run(self, task): self.state = AgentState.RUNNING for step in range(self.max_steps): context = self.memory.compose_context(task) response = self.llm.generate(context) action = self.parse_action(response) # 见下方鲁棒解析 if action.type == "finish": self.state = AgentState.DONE return action.answer if not self.policy.allow(action): raise PermissionError(f"blocked: {action.tool}") result = self.tools.execute(action) self.memory.observe(action, result) self.events.emit("agent.step", {...}) self.state = AgentState.ERROR raise MaxStepExceeded(self.max_steps)把while改成状态机的价值不只是代码好看。事件总线让每个环节的输入输出都能被旁路监听,治理层、观测层不再和主循环挤在一段代码里。之后要做“超时重试”“人工确认”“断点保存”,都是往总线里挂监听器的事,而不是往循环里塞 if。
这里有一个我踩过的坑,就是模型输出不稳定。agent execution terminated due to error这类报错,多半源自parse_action这一步:LLM 返回了一段 markdown、代码块、或者 JSON 后面跟着多余的文字,都会让json.loads炸掉。我后来写了一个三阶段鲁棒解析器:
def parse_action(raw): # 1. 提取 JSON 代码块或第一个 { 到最后一个 } 之间的内容 # 2. 用 JSON5 / yaml 宽松解析 # 3. 失败时让模型用“只输出JSON”的指令重试一次 # 4. 重试仍失败,回退到纯文本回复(视为 finish)这个解析器上线之后,因为格式问题导致的失败率大幅下降。注意:不要把 JSON 解析也丢给模型去“自己处理”,解析必须是确定性的代码逻辑,模型负责给出内容,代码负责保证格式。
3.2 记忆四层:短期工作记忆、事件记忆、长期向量记忆、永久档案
这部分是本次重构中改动最大、回报也最明显的地方。我把记忆拆成了四个层次。
短期工作记忆是最接近原版的部分,它负责保存当前任务窗口内最近几步的观察结果和工具返回。实现上我用一个滑动窗口加 token 预算来控制,窗口大小不按“对话轮数”算,而是按 token 数算,超过预算就把最旧的交互归档到事件记忆层。为什么这么设计?因为模型关注的不是“说过几句话”,而是“上下文里还剩多少信息”。
事件记忆层保存的是任务执行的历史摘要。每个子步骤完成后,系统会用 LLM 生成一句紧凑的摘要,例如“用户要求输出 Markdown 格式,已读取 sales.csv 前 5 行,字段包含 date/amount/region”。这些摘要会持续保留,作为中期上下文供后续步骤参考。生成的摘要本身还带着原始内容的引用 ID,一旦发现摘要失真,可以回溯到原始记录。
长期向量记忆层负责跨任务的语义检索。旧版完全没这层,新版里我把每条沉淀下来的经验性知识按 chunk 切分后做 embedding,存入向量库。检索时最重要的不是“取 top-k”,而是先定一个相似度阈值,低于阈值的记录直接丢弃,然后再对候选结果做 rerank。我测试过,不加阈值的检索会把无关的历史记录混进上下文,导致模型产生幻觉似的“记忆杂音”;而加了阈值加 rerank 之后,上下文干净了很多。
永久档案层存的是用户身份、项目约束、偏好这类不可变事实,写入后直接序列化成结构化文档,不走向量检索,每次任务开始时按规则注入。这一层的价值在于:它是模型的“长期约束”,不需要模型去联想。比如“所有报告默认输出中文”这类偏好,如果放到向量记忆里,可能因为检索不到而被忽略;放在永久档案里,每一次都会被加载。
关于记忆写入的时机,我最初的实现是“边跑边写”,后来发现问题很大——中间步骤的检索结果在后半程往往已经过时,反而干扰判断。后来改成:任务过程中只写事件摘要,向量库的写入放到任务完成后统一做异步回放。这个方法显著降低了记忆污染。
3.3 工具接入层:注册协议、MCP 适配、输出收敛
新版的工具接入不是写一个函数再挂到循环里,而是要遵循一套注册协议。每条工具元数据包含三块:人可读的描述、输入参数 JSON Schema、权限标签。权限标签分三种:
read:无副作用,可以自动执行。write:有副作用,但可撤销或影响可控。critical:会影响外部系统或不可恢复,必须二次确认。
执行前,策略引擎会检查“模型请求的动作 + 权限标签 + 当前环境”,如果命中critical,工具不会立刻执行,而是进入确认队列。这一条直接解决了旧版裸奔的问题。
工具来源也统一成三类:本地 Python 函数、HTTP API、MCP Server。MCP 是现在 Agent 工具接入比较主流的协议,我通过一个适配层把 MCP Server 暴露的工具转成本地注册项。这样外部工具接入变成了配置化操作,不用为每个外部服务手写适配代码。
还有一个容易被忽视的细节:工具返回结果的收敛。很多工具会返回很长的数据,比如数据库查询返回几百行、文件读取返回几千个字符。如果直接把原始结果塞进上下文,不出三步窗口就满了。我现在的做法是:所有工具返回都走一个管道,先截断到设定上限,再调用摘要模型生成一段结构化摘要,原始结果持久化保存,需要完整内容时通过“读取详情”工具再取。这一步看起来消耗了一点成本,但换来了整个执行过程的稳定性。
3.4 多 Agent 协作:编排层、任务总线、互不回环
重写前,多 Agent 协作是我最没有把握的部分。旧版里所谓的多 Agent 其实就是开几个线程各自跑自己的任务,互相之间完全没有信息同步。新版里我加了一个编排层(Orchestrator),它的职责是把一个大任务拆成多个可并行或串行的小任务,派发给不同子 Agent,再把结果汇总回来。
关键设计是:子 Agent 之间不直接对话,只通过一个共享的任务总线交换消息。这么做的好处是避免循环唤醒。如果两个 Agent 可以互相发消息,很容易出现 A 问 B、B 问 A 的无意义循环。事件总线则让消息变成了有明确 topic 的异步事件,A 发布了一个结果,B 订阅到了才消费;消费完 A 不会再收到 B 的原始回复,因为回复也是发到总线而不是直连 A。
每个任务下发的消息都会带一个全局 trace ID。这样无论中间经过了几个 Agent,我都能用同一个 ID 把整条链路串起来。排错的时候非常管用:有一次 review Agent 迟迟没启动,我顺着 trace 发现是 task queue 里的消息被另一个 Agent 误消费了,加了个task_type过滤字段就解决了。
多 Agent 的收益不是“看起来更智能”,而是“能用多个专用模型并行处理不同环节”。比如研究报告类任务,我可以让 research Agent 用轻量模型跑,review Agent 用更强的大模型做质量检查。成本分配更合理,效果也比单一 Agent 包办所有环节要好。
4. 可观测性、安全与失败恢复
4.1 安全守卫:从“禁止”到“策略”
旧版对工具调用没有安全概念,新版里我实现了一套三层策略引擎。
第一层是静态校验。工具注册的时候就声明好参数类型、取值范围、允许的操作路径。比如一个删除文件的工具,会校验传入的路径是否在白名单目录内。第二层是策略判断。根据当前 Agent 的身份、任务的目标、工具的影响面,决定动作可以直接执行、需要确认、还是一律拒绝。第三层是执行后审计。每一步动作都会生成一条不可篡改的审计日志,记录调用了什么、参数是什么、结果是什么、由哪个模型发起的。
实际使用中,大多数误操作都能被静态校验拦住。有一次模型在一个数据分析任务里试图修改用户原始数据文件,策略引擎识别到目标是只读受保护目录,直接拦截并返回了一个安全提示。如果放在旧版,这个动作会静默执行,直到后面步骤崩了才发现数据被改坏了。
对于critical级别的操作,我做成二次确认模式:当模型请求删除文件、发送外部请求、或修改关键配置,系统会暂停执行并输出一条待确认事件,由人工在控制台上批准或拒绝。这个机制在自动执行模式下天然降低了风险,也是 Agent 能被放心部署的关键底线。
4.2 事件日志与全链路追踪:把“执行终止”变成可查问题
重写后,我要求所有关键环节都生成结构化事件:agent.started、agent.llm_call、agent.tool_call、agent.tool_result、agent.failed等。每个事件带固定字段:agent_id、trace_id、event_name、timestamp、cost、latency、token_usage、error_info。全部写入本地事件库,同时可按需导出到外部监控系统。
这套机制带来的最大变化是,“agent execution terminated due to error”从一句报错变成了一条可以追踪的链路。我可以回答这些问题:它在第几步挂的?当时给模型的上下文是什么?模型返回了什么?工具执行结果是什么?是超时、格式错误,还是策略拦截?所有这些信息在 Dashboard 上一眼可见。
重试策略我也做了一个分级设计:对网络抖动、临时超时这类可恢复错误,采用退避重试,最多三次;对格式错误、策略拦截这类确定性问题,不重试,直接标记失败并返回上下文里的错误信息给模型,让它改用别的路径。这个设计避免了“同一错误无限重试”造成的无效消耗。
4.3 检查点机制:让 Agent 可以被“存档/读档”
检查点是这次重构里我最后做的一块,但也是让 Agent 从玩具走向工具的关键功能。
实现方式不复杂:在状态机的每个步骤结束时,把当前 Agent 的完整状态序列化存盘。状态包括当前任务目标、已执行的步骤列表、记忆层中的短期摘要、事件记忆索引、下一步候选动作。序列化格式用 JSON 或 MessagePack,关键要求是可序列化——所以工具执行过程中的临时文件、数据库连接句柄这类东西,不能放进状态里。
崩溃恢复的流程是:进程重启后,检查最近一次检查点是否存在,如果存在,从该状态恢复执行,而不是重新走一遍。实测效果非常明显:之前爬虫任务在第十步崩溃需要重跑全部,现在只需要恢复到第九步结束的状态,补跑最后几步。
检查点存储我不建议做得太重。我最初设计过一套“事务式检查点 + 版本管理”,结果复杂度翻倍,收益却很有限。最终落地方案就是每个任务一个目录,检查点文件按时间戳递增,恢复时取最新的有效快照,旧快照定期清理。这个“减法”让整套机制两周内就上线了。
5. 实际操作中的经验与典型问题
5.1 六周重构实录:每个阶段的产出和判断标准
原计划四周,实际六周。我在这里把这六周做了什么、怎么判断“这一步做完了”完整列出来,给想动手重构 Agent 的朋友一个参考。
第一周做工具层抽取。目标是把散落在主循环中的工具调用统一收口到 Tool Registry。完成标准是:旧版所有用例通过新工具层跑通,且行为一致。这个阶段偏机械,但很重要,它建立起测试基线。
第二到三周重写内核。目标是把 while 循环改造成状态机 + 事件总线。中间遇到一次大返工:我最初把所有事件都通过异步消息队列传递,导致调试时事件顺序错乱、问题难以复现,后来改成“同步写事件、异步批处理导出”,把不必要的中间件砍掉,才稳定下来。
第四周引入记忆服务。这周踩了这个重构中最深的坑:向量记忆写入过早。我在任务执行中途就做向量化写入,导致后半程检索出大量“过期记忆”,模型反而被干扰。改成任务结束后统一回放后才好转。完成标准定为:多步任务在 token 预算内稳定完成。
第五周做安全与审计。完成标准是:故意触发危险操作会被拦截,关键动作有完整审计日志。第六周打通可观测性和检查点恢复。完成标准是:杀掉进程后能从检查点恢复,且 Dashboard 能定位失败步骤。
六周下来最大的体会:每个阶段的“完成标准”必须可验证,不能是“我觉得应该差不多了”。我用一套固定的金标准测试集做回归,任何改动都要过这套测试,没过就说明这个阶段还没完。
5.2 踩坑清单:向量幻觉、事件风暴、并发脏读
这里我把实践中遇到的高频问题整理成了一张表,方便以后排查。
| 问题 | 根因 | 解决方案 |
|---|---|---|
| 向量记忆混入无关内容 | 检索只取 top-k,未设相似度阈值 | 增加阈值过滤 + 重排序 |
| 上下文仍然爆掉 | 工具返回原始长文本直接入上下文 | 截断 + LLM 摘要 + 原始结果持久化 |
| 事件日志顺序错乱 | 事件通过异步队列传输 | 同步写事件,异步批量导出 |
| 多 Agent 并发写同一记忆文件 | 无锁、无版本控制 | 记忆服务加锁 + 写入版本号 |
| 工具调用无限循环 | 没有步骤数硬限制 | 内核统一 max_steps 控制 |
| LLM 输出 JSON 解析失败 | 模型返回 markdown/附加文字 | 三段式鲁棒解析 + 一次重试 |
向量幻觉这件事值得多讲两句。我一开始把阈值设得很低,结果检索出的内容看似相关,实则只是字面相似。那段记忆让 Agent 在回答中主动提到了一些历史任务里根本不存在的“结论”,导致我用测试集一验证,发现质量不升反降。后来我把 embedding 模型换成更好的版本,同时把阈值从 0.5 提高到 0.75 并加了一层关键词过滤,才算压住了记忆杂音。
并发脏读则是在一次并行 Agent 测试中暴露的。三个子 Agent 同时向同一个记忆文件写入事件摘要,结果文件里出现了互相覆盖的乱码。修复方式很粗暴但有效:记忆服务内部对每次写入做资源锁和版本号校验,冲突时后写覆盖前写,但旧版本会被保留在备份目录里。Audit 日志里能看到每一次覆盖记录,所以事后可追溯。
6. 重构后的真实收益与下一步规划
6.1 数据对比:成功率、成本、接入效率
重构完成后,我用同一套任务集对比了新旧版本的表现。需要提前说明的是,不同任务集得到的具体数字会有差异,但趋势是可以参考的。
| 指标 | 重构前 | 重构后 |
|---|---|---|
| 多步任务成功率 | 68% | 91% |
| 平均 token 消耗(每任务) | 基线 | 约降低 45% |
| 平均时延 | 基线 | 约降低 30% |
| 定位一次失败的时间 | 数小时 | 数分钟 |
| 新增工具接入时间 | 半天 | 30 分钟内 |
token 消耗明显下降的原因主要在记忆层:旧版把所有历史全塞进上下文,新版用摘要替代原始内容,同时工具返回经过收敛处理,减少了无效 token。多步任务成功率提升则来自执行循环的稳定性:鲁棒解析器、重试策略、检查点恢复各自贡献了一部分。
我特别想强调接入效率这项变化。旧版接一个新工具,平均要改主循环、写解析分支、处理错误和日志,至少半天;现在只要写一个注册项声明元数据和权限标签,大部分情况 30 分钟内就跑通了。这个效率提升让 Orkas 的生态扩展速度快了很多。
6.2 下一步:skill 仓库、评测集、记忆安全
重构完成只是起点,我自己下一步还有三件事要推进。
第一件事是把工具层进一步升级成 skill 仓库。工具是单一动作,skill 是一组可复用的动作序列。比如“数据分析”这个 skill,内部封装了读取文件、清洗字段、统计指标、生成图表四个步骤。子 Agent 可以在一次任务里调用一个 skill,而不是重新组合四个工具。这是把 Agent 从“会调用”推向“会做事”的关键一步。
第二件事是建立一套针对 Agent 的评测集。现在测试还停留在“用一组固定任务看跑不跑得通”的阶段,缺少对答案质量、工具使用合理性、记忆检索准确性等方面的量化评估。我准备补一版多维度的评测方案,把成功率和成本之外的质量维度也纳入回归标准。
第三件事是记忆安全。我现在对记忆层的关注更多在“能不能装得下”,接下来要关注“会不会被污染”。等向量记忆规模大了以后,外部输入里可能夹带恶意内容试图污染记忆,一旦被检索出来,会直接影响 Agent 行为。我需要一套记忆写入前的过滤机制,以及记忆内容越权读取的管控策略。
重构 Orkas 的过程中,我个人最大的变化是:以前我相信“只要模型够强,Agent 就能处理好一切”,现在我相信“模型只是 Agent 的发动机,底盘、刹车、仪表盘都掌握在工程师手里”。一次重构能改变的,不只是代码结构,还有对 Agent 这件事本身的理解方式。