1. 为什么一定要动地基:旧架构的账本
先交代一下背景。Orkas 是我从三年前就开始维护的一个 Agent 开发框架,定位是帮团队快速搭建带记忆、会调用工具、能编排多步骤任务的智能体。早期版本核心就是一套 ReAct 循环:模型推理出下一步动作,执行工具,观察结果,再推理,直到产出最终答案。这套逻辑在 demo 阶段非常好用,十行代码就能跑通一个 Agent,大家都很兴奋。但等我们真正把它塞进生产环境,接到第二十个需求时,问题开始像藤蔓一样缠上来。
旧架构最大的问题是“什么都在循环里”。循环里既要解析模型输出,又要做工具路由,还要管记忆读写,甚至情绪化地塞进一些业务校验。结果就是核心循环越来越胖,每次加功能都像往一个已经装满的行李箱里再塞东西——能塞进去,但拉链迟早会崩开。我们经常遇到的情况是:一个团队改了工具返回格式,另一个团队调了模型 prompt,两个 PR 合在一起,Agent 的行为就变得不可预测。代码评审变成考古,git blame 翻遍五层调用栈才能定位到改动来源。
真正让我下决心重构的是一次线上事故。某个客户跑了一个多小时的金融报告生成任务,Agent 在执行到第 27 步时,因为一个工具接口超时,整个循环直接终止,前面所有历史、缓存、中间结论全部丢失。用户拿到一条干巴巴的错误提示,只能从头再跑。复盘时我们发现,旧架构把“执行步骤”和“状态持久化”完全耦合在一次内存调用里,进程一退出什么都没了。这已经不是优化能解决的问题,而是地基层面的缺陷。那次事故之后,我花了整整两周梳理需求,最终决定:不修修补补,直接重写 Orkas 的底层。
这次重构的核心原则可以浓缩成一句话:让 Agent 框架更像一个操作系统,而不是一条写死的流水线。操作系统管进程、管内存、管调度,应用程序只需要关心自己的业务逻辑。Orkas 的目标也应该一样——运行时负责生命周期和资源,记忆系统负责状态存取,工具层负责能力接入,编排层负责策略,各层之间通过清晰接口通信,谁也不碰谁的内部实现。下面我会把整个重构过程中的设计思路、踩过的坑、以及最终落地的细节都摊开讲,希望给同样在做 Agent 框架或打算重构 Agent 基础组件的朋友一些参考。
2. 重构目标与顶层设计:下沉核心,上浮业务
2.1 四条设计原则
重构启动前,我先把团队拉到一起定了几条不允许妥协的原则。没有这些原则,重构很容易在过程中失去方向,最终变成一个加了更多补丁的新版旧系统。
第一条原则是“运行时与策略彻底分离”。运行时只负责任务编排的底层机制:步骤调度、异常捕获、上下文传递、生命周期管理。策略则属于上层业务:下一步调用哪个工具、什么时候该结束循环、如何规划多步任务。旧架构里这两件事全揉在 agent 主类里,重构后策略应该由外部注入,运行时通过标准接口去调用策略,让框架本身不绑定任何固定思维模式。这样才能同时支持简单的 ReAct、复杂的 Plan-and-Solve、甚至未来可能出现的全新范式。
第二条原则是“一切状态皆可持久化”。Agent 执行过程中的每一步中间状态,包括当前步骤编号、工具调用参数、返回结果摘要、工作记忆内容,都必须能序列化到一个快照里。这样就算进程崩溃,也可以从最近的快照恢复,而不是从头再来。基于这个原则,我们顺带设计了一套“断点续训”能力——不对,应该叫断点续跑能力,对长时间运行的任务特别重要。
第三条原则是“记忆要有层次,不能一个列表打天下”。旧系统里记忆就是一个 List[Message],把系统提示、历史对话、工具结果全塞在一起。随着对话变长或任务变复杂,这个列表很快就会被无关信息淹没,模型既记不住重点,也容易串场。重构后我们做了三层记忆:短期工作记忆、长期事实记忆、永久结构化记忆,各有各的读写协议和清理策略。
第四条原则是“工具接入必须标准化,MCP 是一等公民”。业界对工具调用的协议已经有不少共识,MCP 就是其中比较有代表性的一个。我们不打算发明新协议,而是把 MCP 作为工具层的首选接入方式,同时保留一层适配器,让内部已有的 HTTP 工具、Python 函数也能被包成统一接口。
2.2 分层架构总览
经过几轮讨论,最终 Orkas 的底层分成了五个主要模块,从下往上依次是:运行时内核、记忆系统、工具总线、编排策略层、接口层。我画了一张架构图记在设计文档里,这里用文字描述一下各层职责。
运行时内核是核心的心脏,它不关心你的 Agent 是干什么的,只负责按指令一步步执行。每个“步骤”是一个不可变的事件对象,内核维护一个执行队列,把当前步骤交给策略层决定怎么处理,再把结果写回状态存储。这一层还包含重试机制、超时控制、错误分类和生命周期钩子。
记忆系统是独立的存储模块,内部区分短期工作记忆、长期事实记忆和永久记忆。短期工作记忆类似进程内的缓存,容量有限,每次执行只保留最近的关键信息;长期事实记忆类似数据库,按实体和关系组织,定期从对话与工具结果中提炼;永久记忆则是用户主动写入的配置、偏好和不可遗忘的约束,优先级最高。
工具总线负责统一管理所有可被 Agent 调用的能力。每个工具在总线上注册为一个描述符,包含名称、参数 Schema、校验规则、执行函数。工具总线不直接调用工具,而是把调用请求和返回值封装成事件,这样一来可以方便地实现日志审计、权限控制、并发限制和 mock 测试。
编排策略层是唯一允许写业务逻辑的地方。你可以实现一个策略类,比如 ReActStrategy、PlanAndSolveStrategy,或者一个完全自定义的 GraphStrategy。策略类只做决策,不自己执行工具,而是向运行时提交一个“下一步动作请求”。这个设计让框架可以随时换策略,而不需要修改底层机制。
接口层提供面向不同使用场景的 API:同步调用、异步流式输出、Webhook 回调、以及一个 Python 装饰器风格的高层接口。我始终认为框架不能被 API 绑架,但好用的 API 能决定开发者愿不愿意用你。
2.3 关键取舍:不用 Workflow DSL,改用事件总线
在设计编排层的过程中,我们遇到了一个分叉路口:是采用当时很流行的 Workflow DSL(用 JSON/YAML 定义节点和连线),还是做成事件总线加策略注入。
一开始团队里有声音支持 DSL,因为可视化好、业务人员也能看懂。但深入评估后我发现 DSL 有个致命问题:它把图的结构当成静态配置,但 Agent 的运行本身就是动态的。模型下一步会说什么、工具返回什么结果,这些都无法提前写进一个静态图里。如果强行用 DSL 表达动态分支,只能写出大量条件节点,最终和写代码无异,还比代码难调试。
所以我们选择了事件总线模式。运行时内核只发布事件和响应事件,策略层监听这些事件,决定下一步发布什么新事件。整个过程像一个状态机在驱动自己,而不是中央控制器在指挥。这样做的好处是极其灵活,坏处是新手可能觉得“框架没有形状”。为缓解这一点,我们后来封装了一个高层 API,把最常见的事件序列藏起来,让普通用户仍然可以像写 ReAct 一样快速上手。但底层,绝对是事件驱动的。
3. 核心细节:从 ReAct 循环到运行时内核
3.1 运行时内核:Step、ExecContext 与 Agent Loop
下面直接进入代码层面的重构细节。旧版 Orkas 的核心循环大概长这样(简化版):
# 旧版示意:所有逻辑堆在循环里 class OldAgent: async def run(self, task: str): messages = [{"role": "user", "content": task}] while True: response = await self.llm.chat(messages) action = parse_action(response) if action.type == "finish": return action.output result = await call_tool(action.name, action.args) messages.append({"role": "tool", "content": result})这段代码的问题在于:没有回归上限、没有状态持久化、没有记忆管理、没有中间事件、无法单独测试任何环节。重构后的运行时内核变成了这个结构:
@dataclass class Step: step_id: str # 全局唯一 kind: str # "observe", "decide", "act", "finish" payload: dict parent_step_id: str | None created_at: float class ExecContext: def __init__(self, state_store: StateStore): self.state_store = state_store self.current_step: Step | None = None self.hooks: list[StepHook] = [] async def emit(self, step: Step): # 持久化当前步骤,然后交给事件总线 await self.state_store.append_step(step) await self.event_bus.publish(step) class OrkasRuntime: def __init__(self, strategy: Strategy, executor: Executor, memory: MemorySystem): self.strategy = strategy self.executor = executor self.memory = memory async def run(self, task: str, context: ExecContext): init_step = Step(step_id=new_id(), kind="observe", payload={"task": task}, parent_step_id=None) await context.emit(init_step) while not context.finished: step = await self.event_bus.next_step() decision = await self.strategy.decide(step, context) next_steps = await self.executor.execute(decision, context) for ns in next_steps: await context.emit(ns) return context.final_output这里的关键点在于:OrkasRuntime本身不包含任何业务决策逻辑,它只负责“把一个步骤放进去,从事件总线里取出下一步,再放进去”。Strategy.decide负责思考,Executor.execute负责执行,两者完全解耦。
我特意保留了ExecContext这个对象,它就像是线程中的 TLS,贯穿整个执行过程。所有状态快照、步骤历史、临时变量都挂在 context 上,而不是散落在各个模块里。这样做的好处是:如果要实现断点恢复,只需序列化ExecContext;如果要调试,只要打印context.steps就能看到完整执行轨迹。
3.2 记忆体系:短期、长期与永久记忆的实现方案
记忆是我们重构时投入最多精力的一部分。Agent 如果没有记忆,就像一个人每句话都要重新解释背景,根本没法完成复杂任务。旧版用一个 messages 数组打天下的方案已经被证明不可持续,我们在新版里实现了三层记忆系统。
短期工作记忆本质上是“当前任务相关的上下文窗口”。它存储最近 N 轮交互、当前步骤结果摘要、临时推理草稿。实现上我用了类似缓存淘汰的思路:每条记忆有一个recency分数和一个importance分数,当总长度超过阈值时,按加权分数淘汰最不重要的条目。这样既能防止上下文无限膨胀,又能让关键信息比如用户反复强调的需求保留下来。
class ShortTermMemory: def __init__(self, max_items: int = 50): self.items = [] self.max_items = max_items def add(self, content: str, importance: float): self.items.append({ "content": content, "importance": importance, "recency": time.time() }) self._evict() def _evict(self): if len(self.items) <= self.max_items: return # 综合分数 = 0.7 * 新近度 + 0.3 * 重要性 self.items.sort(key=lambda x: x["recency"] * 0.7 + x["importance"] * 0.3) self.items = self.items[-self.max_items:]长期事实记忆解决的是“跨任务的知识沉淀”。比如 Agent 之前发现用户喜欢简洁风格,或者某个 API 返回的字段含义,这些信息应该被提取出来并长期保存。我们实现了一个提炼器:每隔几轮交互,用一个轻量模型把短期记忆中符合“事实”的句子抽取出来,经过去重和置信度打分后写入长期存储。长期存储用的是向量数据库加结构化表混合方式:实体和关系放结构化表,描述性知识放向量库。查询时先解析当前问题里的实体,再向量召回相关背景,合成为增强上下文。
永久记忆则是最顶层的约束,比如用户明确设置的偏好、合规要求、安全规则。永久记忆的读写权限被严格管控,普通 Agent 不能自动写入,只有用户或管理员通过专门接口才能修改。执行时,永久记忆会以最高优先级注入提示词,保证 Agent 不越界。
这三层记忆互相配合的工作流程是这样的:任务开始,先加载永久记忆和与任务相关的长期记忆;执行过程中,短期记忆实时读写;任务结束后,异步运行提炼器,把新的长期记忆入库。整个过程对上层策略透明,策略只管调用memory.search()和memory.remember()。
3.3 工具层与 MCP:让 Agent 自己决定用哪个工具箱
工具层在旧版里只是一个简单的注册表:一个字典,key 是字符串工具名,value 是函数。新版将工具抽象成“工具箱 + 总线”,一个工具不仅是可调用函数,还自带描述、参数 Schema、权限要求、适用场景标签。
最关键的变化是我们把 MCP 作为标准接入协议。MCP 的好处在于它把“工具的定义”和“工具的执行”分开:模型看到的是标准化的工具描述,执行时通过 MCP 客户端与远端服务通信。这样即使工具背后的实现从 Python 函数换成了独立微服务,Agent 侧完全无感。
在 Orkas 里,我设计了一个ToolDescriptor:
@dataclass class ToolDescriptor: name: str description: str input_schema: dict tags: list[str] # 如 ["finance", "read_only", "low_latency"] permission: str # "public" / "restricted" / "admin" invoke: Callable[..., Any] mcp_endpoint: str | None = None工具总线在每次决策前会生成一张“候选工具清单”。这张清单不是把所有工具都丢给模型,而是根据当前步骤的上下文做预筛选。比如当前任务是查天气,就没必要把支付工具加进来;当前工具标签包含read_only,就不能调用写数据库的工具。预筛选大大减少了模型的决策噪音,也降低了幻觉概率。
关于并行调用,我们在工具层实现了简单的gather语义。当策略明确表示“这几个工具互不依赖”时,工具总线可以并发执行,并把结果合并成一个聚合步骤。这在旧版里需要手写并发逻辑,现在只要在策略决策里声明依赖关系,运行时自动处理。
3.4 Skill 与 Agent 的边界划分
这是一个让很多人混淆的概念,我在这轮重构里也把它彻底理清了。简单说,Skill 是“能力单元”,Agent 是“拥有目标和策略的主体”。一个 Agent 可以拥有多个 Skill,但 Skill 是不具备自主决策能力的,它只是被 Agent 调用的工具或流程模板。
在我们的架构里,Skill 被实现为一个“带预设步骤序列的超级工具”。普通的 Tool 是一次函数调用,而 Skill 可以包含多个 Tool 调用、内嵌的条件判断、甚至调用子 Agent。比如“生成周报”这个 Skill,内部可能涉及读取数据工具、汇总模型调用、生成图表工具,最后格式化输出。对 Agent 来说,它只需要决定“用不用周报 Skill”,而 Skill 内部的流程由 Skill 自己的小型状态机管理。
为了支持 Skill 和 Agent 的良性互动,我们给 Skill 接口加了一个feedback通道。Skill 执行到一半如果发现前置条件不满足,可以主动返回一个“需要更多信息”的信号,Agent 看到这个信号后会调整自己的策略,比如先去问用户补充数据,再重新执行 Skill。这个设计让 Agent 不再是生硬地按顺序调用技能,而是像人一样,根据执行情况动态调整方案。
4. 实操过程:一场持续三周的迁移手术
4.1 第一步:先给旧系统打上“观测补丁”
重构的第一天我没有写任何新代码,而是先在旧系统里加上了全链路日志和指标采集。这一招是从一次架构评审会学到的:不把旧系统的真实运行数据摸清楚就动手重写,等于蒙眼换发动机。
我们需要采集的数据包括:每次执行的总时长、模型推理耗时、工具调用耗时、循环次数、错误类型分布、上下文 token 消耗量。其中最核心的是“步骤级耗时拆解”。旧系统没有这样的细粒度日志,我不得不临时在循环的每个关键节点加了埋点。这些埋点丑,但至关重要。
因为有了这些数据,我们才能在重构后做一比一的性能对比。比如我们发现旧系统平均 70% 的耗时来自模型推理,工具调用只占 15%,这告诉我们新架构应该重点优化推理相关的缓存和并行,而不是纠结于工具函数的 IO。另一个发现是约有 12% 的执行是因为 JSON 解析错误导致循环重启,这个数据直接推动了我们在新系统中引入“自适应结构化输出”机制,而不是让模型裸输出 JSON。
4.2 第二步:用适配器模式双跑新旧内核
重构最怕的是“一把梭”:旧代码删掉,新代码重写,结果新架构半年跑不通,业务全部停摆。我们采取的过渡方案是双跑模式——新旧两套内核同时存在,通过配置开关切换,并且让它们跑同一组测试用例和同一批线上流量镜像。
具体做法是定义了三个接口:LegacyRunner、NewRunner、CompareRunner。CompareRunner接收同样的输入,同时调用新旧两个内核,然后对比输出和中间轨迹。对于输出不同的案例,我们一件件分析原因:有的是旧系统 bug,有的是新系统行为改进,有的是两边对工具结果的理解不同。这个过程非常耗时,但每一份差异报告都让新系统更可靠。
双跑期间我们特意挑了一些长尾场景,比如超长上下文对话、多语音输入(语音转文字后的长文本)、嵌套工具调用、模型突然返回格式错误等。这些场景在单元测试里很难构造,但在真实流量里随处可见。我记得有个案例是真事:旧系统在处理“计算上月同比”时会把“上月”理解成上一个自然月,而新系统结合长期记忆后能理解业务上下文中的“上月”。这种差异只有对比测试才能发现。
4.3 第三步:分批切换流量与回归清单
双跑稳定后,我们开始分批切换生产流量。切换顺序遵循“从低风险到高风险”的原则:第一批是只读查询类的轻量 Agent 任务,比如信息检索、文档摘要;第二批加入工具调用,但工具只限于只读 API;第三批开放写操作任务,比如发送邮件、更新数据库;最后一批才是涉及支付、合约的高风险任务。
每一批切换都配套一张回归清单。清单中除了功能点,还有几个固定项目:执行成功率、平均耗时、p95 延迟、上下文 token 消耗、工具调用失败率。我们设定了红线指标,任何一项劣化超过 10% 就立即回滚该批次。这里特别想说一下“回滚预案”的重要性。很多团队重构不做回滚演练,真出问题时手忙脚乱。我们在切换前已经演练了三次从新系统切回旧系统的操作,整个流程压缩到不到五分钟。结果真正用到这个预案的次数是零,但演练带来的安心感和操作肌肉记忆,让团队在发布当天心态非常稳。
回归清单里还包含了一项非常容易被忽视的指标:用户体感语义一致性。我们每一批随机抽出 20 个案例,让业务同事盲评新旧系统的输出,看是否出现“改坏了”的迹象。这个指标无法自动化,但能捕捉到自动化测试发现不了的行为偏移。曾经就发生过新系统把一个口语化的助理改成了机械式回复,模型分数没变,但用户明显觉得别扭。靠着人工盲评,我们在上线前及时调回来。
4.4 第四步:压测与性能回退预防
压测不是简单地把并发打到多高,而是找出新架构的瓶颈分布。我们使用 Locust 模拟了不同规模的任务:单轮对话型 Agent、多工具调用型 Agent、长时间训练型 Agent。压测结果有几个比较有意思的发现。
第一,新内核的事件总线在低并发下比旧循环多了约 3% 的延迟,因为毕竟多了一层事件拆包和分发。但到了高并发(500 并发以上),新内核的优势出来了:由于步骤持久化是异步写的,我们可以在内存里批量聚合快照,磁盘 IO 的摊销效应让整体吞吐比旧系统高了近 40%。第二,工具并行调用的收益非常明显。旧系统里如果策略决定调用三个独立工具,必须串行等待;新系统自动并行,在模型推理占大头的前提下,工具阶段耗时几乎可以忽略不计。第三,短期记忆的淘汰算法在高频写入场景下会出现一点竞争锁,解决办法是把内存队列改成分片结构,每个片段一个锁,冲突概率大幅降低。
性能回归防护方面,我们在 CI 里加入了一个基准测试套件,每次 PR 合并前都会跑一遍标准任务集,记录耗时和资源占用。一旦发现比基线劣化超过 5%,就会拦截 PR 并要求给出解释。这套机制在重构后帮我们抓到了不少因为日志库误用导致的性能回退,可以说非常值。
5. 常见问题与排查技巧实录
5.1 循环终止失控:Agent Execution Terminated Due to Error
这可能是 Agent 开发里最经典的一类报错了。旧版系统里,循环一旦遇到异常就可能直接终止整个进程,导致用户工作全部丢失。新版虽然做了断点快照,但我们还是收到过几次这个报错,排查后发现大多是策略层主动抛出终止异常,而不是运行时的问题。
我们的做法是把“错误”分成两类:可恢复错误和致命错误。可恢复错误,比如工具超时、模型输出格式不对,会被运行时捕获并生成一个“重试步骤”,策略可以选择再次尝试或换一种工具。致命错误,比如找不到依赖的配置、权限被拒绝,才会触发用户可见的AgentExecutionTerminated。为了让策略层能区分这两类错误,我定义了异常层级:
class RecoverableAgentError(Exception): """可重试的错误,例如临时性网络故障、工具超时""" retry_after: float = 1.0 class FatalAgentError(Exception): """无法继续执行的错误,必须终止""" user_message: str = ""并且为致命错误附带了完整的ExecContext快照。这样用户崩溃后可以通过oras.resume(snapshot_path)恢复,而不是从头再来。如果你在自己的框架里也遇到这类报错,建议先看日志里错误的父类,再决定是要增加重试策略还是真正修复底层状态存储。
5.2 记忆错乱:短期记忆串场
我遇到过几个典型的记忆错乱案例。最典型的是:Agent 在完成一个多步骤任务后,把上一步的工具输出错误地当成了当前步骤的输入。排查后发现是短期记忆在读取时没有区分“当前步骤上下文”和“历史步骤摘要”,导致模型把旧数据当成新数据。
解决方法是给短期记忆的每个条目强制附加一个scope字段,取值是current或history。策略层构造 prompt 时,先放current范围的条目,再放history摘要。同时我在记忆写入入口做了校验:凡是工具返回结果,都自动归类为current;凡是对话历史提炼的内容,归类为history。这个小小的字段改动,让记忆混乱的场景几乎归零。
还有一个容易踩坑的地方:长期记忆的写入很可能和用户隐私相关。我们在写入前会做一次实体脱敏,把手机号、身份证、地址等敏感字段替换成占位符。如果你做的是面向 C 端的 Agent,这一步必须要有,否则一旦长期记忆被坏人以提示注入的方式提取出来,就是严重事故。
5.3 工具调用顺序错乱:并行与依赖如何协调
引入并行工具调用后,出现了一个新问题:策略层有时会错误地声明“工具 A 和工具 B 无依赖”,但实际上 B 的输入要等 A 的输出。如果运行时盲目并行,就会拿到空值或脏数据。
我们的解法是在策略层的决策对象里增加dependencies字段。每个动作声明它依赖哪些上游步骤 ID。运行时看到一个动作的依赖未完成时,会把它优先调度到依赖完成后,而不是直接执行。换句话说,事件总线内置了一个轻量的 DAG 调度器。但这里的坑在于,如果策略写错了依赖,运行时无法自行判断。所以我在工具总线里加了一个运行时校验:根据工具的输入 Schema,检查上游步骤中是否有匹配的变量名。如果没有,即使策略声称不依赖,总线也会拒绝执行并提示错误。这个“双重保险”让工具调用顺序问题减少了 80%。
5.4 安全加固:防注入与权限边界
Agent 框架的安全问题在重构中必须前置考虑。旧版系统里,工具返回内容被直接拼进上下文,如果某个工具返回的是用户可控的文本,攻击者就能通过构造恶意字符串诱发提示注入,诱导 Agent 执行危险操作。
我们的加固分三层。第一层,所有外部返回内容在进入记忆前都经过内容清洗,剥离掉看起来像是指令的片段,但保留信息本身。这个清洗不是简单地删掉“ignore previous instructions”之类的话,而是用专门的小模型对文本做指令意图检测,命中则进行隔离。第二层,工具调用的权限边界统一由permission字段控制,Agent 不能动态提权。也就是说,即使模型被注入诱导去调用“删除用户”这个工具,只要该工具标记为restricted,运行时就会直接拒绝并记录审计日志。第三层,对于高风险操作默认启用“人工确认”钩子,Agent 必须返回一个等待用户确认的步骤,而不是直接执行。
这三层加在一起,不能说绝对安全,但至少能挡住绝大多数已知的注入手段。安全是 Agent 进入生产环境的第一道门槛,建议大家在架构设计阶段就留好这些钩子,不要等到出事了再补。
6. 一些没有写进文档的经验
6.1 重构不是重写,兼容性是一面镜子
这是我在整个项目里最深的体会。很多人一重构就想着推翻一切,“旧 API 反正没人用,直接换掉”。但实际上的教训是:兼容性不仅仅是对用户的承诺,更是对自己的约束。正是因为我们保留了大量旧版接口的兼容层,迁移才没有变成一场大爆炸。旧接口可以在一段时间内作为一个适配器,内部调用新内核;这不仅让老用户代码不用改,也让我们能渐进式地验证新内核的正确性。
兼容性还有一个好处:它会暴露出你对业务理解的盲区。每当你发现某个旧接口难以映射到新架构时,就应该停下来想,是不是新架构缺失了某种能力?而不是粗暴地回答“这是旧设计不合理,该改”。有时候确实是旧设计不合理,但更多时候旧接口代表着某类真实需求,新架构需要有自己的承接方式。
6.2 测试要跟着架构走
旧版 Orkas 的测试全都建立在“端到端调用 agent.run()”这一个维度上。重构后我们发现这种测试太粗,一旦某个中间环节出错,定位成本极高。于是我们把测试分成了四层:单元测试覆盖每个工具函数和记忆策略;组件测试覆盖运行时内核的事件分发逻辑;契约测试覆盖工具描述符与真实工具的 Schema 一致性;端到端测试数量最少,只保留真正跨层的关键路径。
这套分层测试最直接的效果是:每次提交后能在五分钟内知道新改动到底影响了哪一层。旧系统经常出现那种“跑十遍偶然挂一次”的怪问题,现在因为每层都有自己可控的输入,重复概率大大降低。我建议所有做 Agent 框架的人都应该认真设计分层测试,不要迷信全链路的 golden 测试。全链路测试更适合作为发布前的安全网,而不是日常开发的主要验证手段。
6.3 最后再分享一个小技巧:给每一步都留个名字
新版 Orkas 里我给每一步骤都加了step_id和parent_step_id,一开始只是为了实现断点恢复。但在实际使用中我发现,这组标识符对调试的帮助远超预期。每当用户报告一个奇怪行为,我只要把整个执行轨迹导出,按 parent 关系构建一棵树,肉眼就能看到 Agent 的决策路径,找到哪一步出现了逻辑偏离。如果你也在做自己的 Agent 框架,哪怕不重构,我也强烈建议从今天开始给每一次 Agent 决策打上结构化的 trace 标识。这几乎零成本,但未来省下的调试时间会非常可观。
Orkas 的底层重构到现在已经稳定运行了半年,这半年里我们又在这个地基上加了长任务调度、多 Agent 协作、以及更细粒度的权限策略。回头看这次重构,最值得的其实不是技术选型本身,而是我们想清楚了一件事:Agent 框架的本质不是“帮模型做决定”,而是“给模型提供做决定的稳定环境”。地基稳了,上面长什么草都好办。