ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Orkas重构实战:从主循环泥潭到四层Agent运行时架构

Orkas重构实战:从主循环泥潭到四层Agent运行时架构 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 循环到状态机 事件总线ReActReasoning Acting是 Agent 最基本的执行范式模型观察当前状态思考下一步调用一个工具拿到结果后继续观察。旧版 Orkas 把这个范式写成了一个粗放的while循环而新版把它改成了状态机驱动的事件循环。我贴一个简化版的内核结构用 Python 伪代码表示class AgentExecutor: def __init__(self, tools, memory, policy, max_steps12): 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(fblocked: {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 这件事本身的理解方式。
返回列表