OpenClaw核心原理与架构工程实践全解析:从控制流到多Agent系统的完整指南

本文深入剖析Agent智能体系统的核心原理与工程实践,揭示决定Agent成败的关键并非模型能力,而是围绕Harness构建的测试、验证与约束基础设施。文章详细讲解控制流、上下文工程、工具设计、记忆管理、多Agent组织、评测体系、追踪机制和安全边界八大核心模块,并通过OpenClaw开源实现展示这些设计原则如何落地。


Agent Loop 的基本运转方式

Agent Loop 的核心实现逻辑抽象后其实不到 20 行代码:

typescript
const messages: MessageParam = [{ role: "user", content: userInput }];

while (true) {
  const response = await client.messages.create({
    model: "claude-opus-4-6",
    max_tokens: 8096,
    tools: toolDefinitions,
    messages,
  });

  if (response.stop_reason === "tool_use") {
    const toolResults = await Promise.all(
      response.content
        .filter((b) => b.type === "tool_use")
        .map(async (b) => ({
          type: "tool_result" as const,
          tool_use_id: b.id,
          content: await executeTool(b.name, b.input),
        }))
    );
    messages.push({ role: "assistant", content: response.content });
    messages.push({ role: "user", content: toolResults });
  } else {
    return response.content.find((b) => b.type === "text")?.text ?? "";
  }
}

对应的控制流如下,感知 -> 决策 -> 行动 -> 反馈四个阶段不断循环,直到模型返回纯文本为止。

agent三板斧:感知、决策、行动(然后循环到死)

你看上面这段代码,说白了就是一个死循环,就像你每天起床、吃饭、上学、睡觉一样,AI也有它的循环:

  1. 感知:先看看用户说了啥,或者现在是个啥情况。
  2. 决策:问一下它的大脑(大模型),我下一步该干啥?是继续唠嗑,还是用个工具?
  3. 行动:如果大脑说要“用工具”,比如查天气、写代码,那就赶紧去执行。
  4. 反馈:把用工具得到的结果告诉大脑,然后重复这个循环。
  5. 结束:直到大脑觉得,行了,事儿办完了,不用再干别的了,直接告诉用户结果,然后收工。

就这么简单!很多看起来很牛掰的Agent,底层都是这个循环。那些看起来很花哨的新功能,比如支持子Agent、能压缩记忆、能加载技能包,其实都是在这个循环外面加的“外挂”,循环本身稳如老狗。就像一个汽车发动机,你要装空调、装音响、装导航,都是外挂,发动机核心的“吸气-压缩-燃烧-排气”四冲程循环,一百年都不会变。

看过不少 Agent 实现和官方 SDK,结构都差不多,循环本身相当稳定,从最小实现一路扩展到支持子 Agent、上下文压缩和 Skills 加载,主循环基本没有变化,新增能力通常都是叠加在循环外部,而不是改动循环内部。

新能力基本只通过三种方式接入:扩展工具集和 handler、调整系统提示结构、把状态外化到文件或数据库,不应该让循环体本身变成一个巨大的状态机,模型负责推理,外部系统负责状态和边界,一旦这个分工确定下来,核心循环逻辑就很少需要频繁调整了。



Workflow 和 Agent 有什么区别


很多号称是Agent的产品,其实更像是一个提前写好的“工作流”(Workflow)。工作流就是一条路走到黑,代码里写死了第一步干啥,第二步干啥。

而Agent呢?它更像一个真人,每一步都是动态决定的,它自己也不知道下一步会干啥,全看当时的情况。没有谁高谁低,关键看你让AI去干啥活儿。你让AI帮你取个快递,工作流就够了;你让它自己规划路线去帮你买杯奶茶,还得看路上有没有堵车、奶茶店有没有关门,那你就得用Agent。


Anthropic 对这两类系统有一个直接区分:
执行路径由代码预先写死的是 Workflow,由 LLM 动态决定下一步的是 Agent。

核心区别在于控制权掌握在谁手里,现实中很多标着 Agent 的产品,深入看其实更接近 Workflow,不过两者本身并无高下之分,真正重要的是给任务找到更适合的解决方案。
好的,这是用大白话整理的对比:

控制权

  • Workflow:代码早就写死了,同样的输入必定走同样的流程,像流水线一样死板。
  • Agent:由大模型自己看着办,动态决定怎么走,但有时候需要验证一下它靠不靠谱。


执行方式

  • Workflow:工具按固定顺序执行,出错了就走预设好的分支,像地铁换乘图一样固定。
  • Agent:工具想用哪个用哪个,模型自己挑,出了问题还能自己试着修。

状态与记忆

  • Workflow:状态机明明白白,走到哪一步、怎么跳转,一清二楚。
  • Agent:状态藏在对话历史里,上下文就是记忆,越聊越积累。

维护成本

  • Workflow:想改流程?改代码、重新部署,麻烦得很。
  • Agent:改改系统提示词就行,不用重新上线。

可观测性

  • Workflow:看日志就知道卡在哪一步,延迟多少心里有数。
  • Agent:得看完整执行记录才能理解决策链,跑多少轮不确定。

人机协作

  • Workflow:人只能在预设好的节点插手。
  • Agent:人随时能介入,或者直接把控制权接过来。

适用场景

  • Workflow:适合流程固定、输入范围明确的情况。
  • Agent:适合需要中间推理、灵活判断的复杂场景。

五种常见控制模式

大多数 AI 系统拆开看,其实都是这五种模式的组合。很多场景并不需要完整的 Agent 自主权,把其中几种模式搭起来就够了,关键还是看任务本身适合哪一种设计。

  1. 提示链 Prompt Chaining:任务拆成顺序步骤,每步 LLM 处理上一步的输出,中间可加代码检查点,适合生成后翻译、先写大纲再写正文这类线性流程。
  2. 路由 Routing:对输入分类,定向到对应的专用处理流程,简单问题走轻量模型,复杂问题走强模型,技术咨询和账单查询走不同逻辑。
  3. 并行 Parallelization:两种变体:分段法把任务拆成独立子任务并发跑,投票法把同一任务跑多次取共识,适合高风险决策或需要多视角的场景。
  4. 编排器-工作者 Orchestrator-Workers:中央 LLM 动态分解任务,委派给工作者 LLM,综合结果。nanobot 的 spawn 工具和 learn-claude-code 的子 Agent 模式都是这个原型。
  5. 评估器-优化器 Evaluator-Optimizer:生成器产出,评估器给反馈,循环直到达标,适合翻译、创意写作这类质量标准难以用代码精确定义的任务。

上面这些模式解决的是控制流怎么搭,下面再看另一个更工程的问题,系统为什么能跑稳。



为什么 Harness 比模型更关键

决定AI能不能把活儿干稳的,是它周围的那套“工程环境”,也就是Harness。这个词翻译过来就是“挽具”,就是给马套的那个东西。你想想,一匹再好的马,没有好的挽具,你拉得动它吗?它能帮你拉货吗?


Harness 是指围绕 Agent 构建的测试、验证与约束基础设施,这里的 Harness 至少包括四个部分:验收基线、执行边界、反馈信号和回退手段。

Harness这个“挽具”至少包含四样东西:

  • 验收基线:什么是“干完了”?标准是什么?
  • 执行边界:它能在哪儿跑?不能碰哪些东西?
  • 反馈信号:它干得怎么样?有没有人给它鼓掌或者骂它?
  • 回退手段:它要是把事儿搞砸了,怎么让它回到原地重来?

3 个工程师 5 个月写了百万行代码就是Harness的完美体现。你发现没有,他们的Harness就是把“人工”的管理和监督,变成了“机器”能执行的规则。比如,“代码要符合规范”,这玩意儿交给AI去理解和遵守,就像让一只哈士奇理解“不能拆家”一样不靠谱。所以,他们直接上代码检查工具,AI一写不合规的代码,机器就直接报错,根本不给它犯错误的机会。

再比如“验证Bug修好了没”,也不是等AI说“我修好了”就信了,而是直接让它自己去跑测试、查日志、看指标。AI能自己验证,自己就能形成闭环,不需要你当监工。

所以,Harness的精髓就在于,把那些模糊的、靠人盯的“规矩”,翻译成机器能执行的、冷酷无情的“法律”。你的AI能不能成事儿,不看你请了多贵的“老师”(模型),而看你给它配了多牛的“教练和裁判”(Harness)。

模型虽然重要,但决定系统能不能稳定运行的,往往是这些外围工程条件,这个判断在代码编写这类高可验证任务上最成立,但在开放式研究、多轮协商这类弱验证任务里,模型上限本身仍然更关键。

Harness 要做的就是让对错有机器可以执行的判断标准,而不是靠人盯。

OpenAI 的 Agent 优先开发实践

3 个工程师 5 个月写了百万行代码,将近 1500 个 PR,是传统开发速度的 10 倍。这个速度背后不是模型有多强,而是几个工程决策做对了:

  • Agent 看不到的内容等于不存在:知识必须存在于代码库本身,外部文档对运行中的 Agent 不可见,AGENTS.md 只保留约 100 行作为索引,细节拆到各 docs 目录按需引用。
  • 约束编码化而非文档化:写在文档里的规范很容易被忽略,编码进 Linter、类型系统或 CI 规则里的约束才具备可执行性,架构分层靠自定义 Linter 机械强制,不靠人工 Review。
  • Agent 端到端自主完成任务:从验证当前状态、复现 Bug、实现修复、驱动应用验证,到开 PR、处理 Review 反馈、自主合并,全链路不需要人介入,查日志、查指标、查追踪都由 Agent 主动完成。
  • 最小化合并阻力:测试偶发失败用重跑处理而不是阻塞进度,在高吞吐环境下等待人工审查的成本往往高于修复小错误的成本。写代码的纪律没有消失,只是从人工 Review 变成了机器执行的约束,一次写进去,到处生效。

APP 把日志、指标、追踪三路数据经由 Vector 分发到 Victoria 存储层,对应 LogQL、PromQL、TraceQL 三个查询接口,Codex 通过这三个接口查询、关联、推理,完成改动后重启应用、重跑工作负载,结果再打回给 Codex,UI Journey 也作为输入接入。整套可观测性栈按任务临时创建、任务完成即销毁,Agent 不需要等人告知错误,直接查询系统状态验证修改是否生效。



上下文工程为什么决定稳定性

你有没有过这种体验?跟AI聊着聊着,它突然就变傻了,好像忘了最开始我们聊啥,甚至连自己刚才说的话都记不住。这不是它装傻,是它真的“脑容量”不够了。

AI的注意力机制(Transformer的Attention机制)有个硬伤,它的复杂度是O(n²)。啥意思?就是它能看到的信息越多,脑子就越乱,有用的信号就会被淹没在没用的噪音里。就像你在一间堆满东西的屋子里找钥匙,东西越多,你越找不到,最后反而可能被绊倒。这种情况有个很形象的名字叫Context Rot(上下文腐烂)。

很多你以为的“模型能力不行”,其实就是“上下文组织得一团糟”。就像你去考试,你脑子里的知识点本来是井井有条的,结果一紧张,所有乱七八糟的信息都涌上来,你当然答不好题。

那咋办?总不能给它灌“脑白金”吧。解决方案就是分层管理,给信息分门别类,就像整理你的书桌一样:

  • 常驻层:身份、规矩、底线。比如,“你是我的AI助手”,“不能说脏话”。这部分要短小精悍,是它的“身份证”。
  • 按需加载:各种技能和专业知识。比如“写Python代码”、“做PPT”。就像你的工具书,要用的时候再拿出来翻,不用的书别摊在桌上。
  • 运行时注入:当前时间、用户名、渠道ID。这些是“当前状态”,每轮对话都要更新,像实时刷新的天气预报。
  • 记忆层:跨会话的经验和偏好。比如“用户喜欢喝冰美式”。写在日记里,下次聊天时,需要了再翻看,不用天天带在身上。
  • 系统层:那些确定性的、逻辑性的东西。比如“如果文件路径不存在,就报错”。这种东西用代码去执行,比让AI自己理解靠谱一万倍。
最关键的一点是:别把那些用代码或规则就能搞定的逻辑,硬塞进AI的上下文里。就像你有个计算器,你就直接按数字算,别非让AI用语言跟你描述一遍“2+2等于4”的过程,纯属浪费脑力。

三种常见上下文压缩策略

别把确定性逻辑放进上下文,凡是可以通过 Hooks、代码规则或工具约束表达的内容,都应交给外部系统处理,而不是让模型反复读取。

1. 滑动窗口:丢弃旧消息,成本极低,会丢早期上下文,适合简短对话 
2. LLM 摘要:模型生成总结,成本中等,丢细节保留决策,适合长任务 
3. 工具结果替换:占位符替换原始输出,成本极低,适合工具调用密集型

滑动窗口实现最简单,但会丢掉早期决策背景。LLM 摘要的进阶做法是 branch summarization,摘要时明确保留架构决策、未完成任务和关键约束。工具结果替换里,micro_compact 每轮替换旧工具输出,auto_compact 在上下文超阈值时自动触发。

Prompt Caching 减少重复开销
LLM 推理时,Transformer attention 会为每个 token 计算 Key-Value 对,如果当前请求的输入前缀和之前某次请求完全一致,这部分 KV 就不需要重新计算,直接从缓存读取,这就是 Prompt Caching 的底层原理。命中的前提是精确前缀匹配,不是内容相似就能触发,任何一个 token 不同都会破坏匹配,所以缓存友好的设计核心是稳定性,系统提示、工具定义、长文档这类在多轮请求里基本不变的内容天然适合缓存,动态信息(当前时间、用户输入、工具调用结果)放在后面,不影响前缀的稳定性。

这和上下文分层设计直接相关。常驻层越稳定,前缀命中率越高,边际成本越低,所以“常驻层短而稳定”不只是为了节省 token,也在保护缓存命中。



为什么 Skills 要按需加载

Skills 延迟加载的好处也在这里,按需注入的内容不破坏系统提示前缀,而是追加在稳定前缀之后,工具定义同样参与缓存计算,接了很多 MCP 工具的 Agent 如果工具集频繁变动,缓存命中就会不断失效。有一个反直觉的地方:稳定的大系统提示,比频繁变动的小提示实际成本更低,因为写入成本只付一次,后续每次调用读取的折扣可以达到 90%。

Skills 是上下文工程里非常有效的一种模式,核心思路是:系统提示只保留索引,完整知识按需加载。


const systemPrompt = <code>
可用 Skills:
- deploy: 部署到生产环境的完整流程
- code-review: 代码审查检查清单
- git-workflow: 分支策略和 PR 规范
</code>;

async function executeLoadSkill(name: string): Promise<string> {
  return fs.readFile(<code>./skills/${name}.md</code>, "utf-8");
}


Skill 描述要足够短,避免常驻上下文持续涨 token,也要足够像路由条件而不是功能介绍,至少说明什么时候用、什么时候不要用、产出物是什么,最直接的写法是 Use when / Don't use when 再补几条反例,很多路由失败不是模型能力问题,而是边界写得不清楚。系统提示里也要把调用规则写明确:每次回复前先扫描 available_skills,有明确匹配时再读取对应 SKILL.md,多个匹配时优先选最具体的那个,没有匹配就不读取,一次只加载一个。

Skills 不能等 Agent 想起来再用,要每轮都先扫描描述,但扫描成本要足够低,实际加载数量也要受控,如果 Skill 会触发外部 API 写操作,系统提示里应显式补充速率限制要求,尽量批量写入、避免逐条循环、遇到 429 主动等待。

Skill 描述符有两个写法陷阱值得单独说。第一个是字数:


# 低效(约 45 tokens)
description: |
  This skill handles the complete deployment process to production.
  It covers environment checks, rollback procedures, and post-deploy
  verification. Use this before deploying any code to production.

# 高效(约 9 tokens)
description: Use when deploying to production or rolling back.


路由准确率差距不大,但每个启用的 Skill 描述符都常驻上下文,Skill 一多,长描述的累积成本很可观。第二个是精度:描述太短(help with backend)等于任何后端工作都能触发,路由会乱。真正有效的描述符是路由条件,不是功能介绍,“何时该用我”比“我能做什么”重要得多。

数量上同样要控制:常驻系统提示的只放高频 Skill,低频的不要塞进默认列表,需要时再手动引入,极低频的直接用文档替代就够了,不必做成 Skill。几个典型反模式:正文几百行工作手册全塞进 Skill 正文而不是拆成 supporting files;一个 Skill 试图覆盖 review、deploy、debug、incident 五件事;有副作用的 Skill 没有显式限制调用时机。这三个问题都会让 Skill 路由失准,而且很难排查。

Skills 和 MCP 在上下文成本上的特征并不相同,很多 MCP 会把完整结果直接返回给模型,更容易迅速吃掉上下文预算,CLI + 单句描述的 Skill 更接近模型熟悉的调用方式,在大多数可过滤、可拼接的数据读取任务里也更简洁,当然 MCP 也有明确适用场景,例如 Playwright 这类需要维护状态的任务。



上下文压缩最容易丢掉什么?

压缩阶段最常见的问题,不是摘要不够短,而是保留顺序设错了,LLM 通常会优先删除那些看起来还可以重新获取的信息,早期的 tool output 通常最先被移除,但与之相关的架构决策、约束理由和失败路径也很容易一并丢失。最好在 CLAUDE.md 或等价文档里明确写出压缩时的保留优先级:

markdown
# Compact Instructions 如何保留关键信息
保留优先级:
1. 架构决策,不得摘要
2. 已修改文件和关键变更
3. 验证状态,pass/fail
4. 未解决的 TODO 和回滚笔记
5. 工具输出,可删,只保留 pass/fail 结论

压缩时还有一条容易踩的坑:不要改动标识符,UUID、hash、IP、端口、URL、文件名这类值必须原样保留,一旦把 PR 编号或 commit hash 改错一位,后续工具调用就会直接失效。


文件系统为什么适合做上下文接口

Cursor 把这种方式叫 Dynamic Context Discovery,默认少给,只在需要时读取。文件系统天然适合做这个接口,工具调用经常返回大量 JSON,几次搜索就能堆出成千上万 token,不如直接写入文件,让 Agent 通过 grep、rg 或脚本按需读取,工具写文件,Agent 读文件,开发者也可以直接查看。

Cursor 在 MCP 工具上也验证过这个方向:他们把工具描述同步到文件夹,Agent 默认只看到工具名,需要时再查询具体定义,A/B 测试中,调用 MCP 工具的任务总 token 消耗减少了 46.9%。

同样的思路也适用于长任务压缩,压缩触发时,不直接丢弃历史,而是把聊天记录完整保留为文件,摘要里只引用文件路径,后续如果 Agent 发现摘要缺少细节,仍然可以回到历史文件里检索,这样压缩就变成了一种有损但可追溯的操作,而不是一次不可恢复的硬截断。



工具设计决定 Agent 能做什么

上下文决定模型能看到什么,工具决定模型能做什么。工具定义的质量比数量更关键,仅 5 个 MCP 服务器就可能带来约 55,000 tokens 的工具定义开销,相当于在 200K 上下文里还没开始对话就用掉了近三成,工具一旦过多,模型对单个工具的注意力也会被稀释。

工具问题多数不在数量不够,而在选不对、描述看不懂、返回一堆没用的、出了错 Agent 也不知道怎么改。

工具设计大致经历了三个阶段,早期做法是直接把现有 API 封装成工具扔给模型,后来发现模型选错工具,问题不在模型能力,而在工具本身的设计视角就错了,原来是给工程师设计的,不是给 Agent 设计的。

第一代,API 封装:每个 API Endpoint 对应一个工具,粒度过细,Agent 往往需要协调多个工具才能完成一个目标。 
第二代,ACI,即 Agent-Computer Interface:工具应对应 Agent 的目标,而不是底层 API 操作。不要分别暴露 create_file、write_content、set_permissions,而是直接给一个 create_script(path, content, executable),一次搞定。

第三代,Advanced Tool Use:在工具设计之上,进一步优化工具的发现、调用和描述方式,主要包括三个方向:

Tool Search,动态工具发现:别把全部工具定义一次性塞给模型。Agent 通过 search_tools 按需发现工具定义,上下文保留率可达到 95%,Opus 4 的准确率也从 49% 提升到 74%。

Programmatic Tool Calling,代码编排:别让中间数据一轮轮穿过模型,而是让模型用代码编排多个工具调用,中间结果在执行环境中流转,不进入 LLM 上下文,token 消耗可从约 150,000 降到约 2,000。

Tool Use Examples,示例驱动:每个工具附带 1-5 个真实调用示例。JSON Schema 只能描述参数类型,但无法表达调用方式,加入示例后,工具调用准确率可从 72% 提升到 90%。

ACI 工具设计有哪些原则?

类比 HCI 对人的影响,工具设计对 Agent 的影响一样直接,不能只看“工具能不能调用”,还要看“调用错了之后能不能自己修回来”。

三个原则放在一起看更清楚,差的做法参数模糊、错误不可修正、定义实现分离:

typescript
// 差:参数模糊,出错只返回字符串,Agent 不知道怎么修正
const tool = {
  name: "update_yuque_post",
  input_schema: {
    properties: {
      post_id: { type: "string" },
      content: { type: "string" },
    },
  },
};
// 出错时
return "Error: update failed";

好的做法用 betaZodTool 把定义和实现绑在一起,参数描述直接约束格式,错误结构化给出修正建议:

typescript
const updateTool = betaZodTool({
  name: "update_yuque_post",
  description: "更新语雀文章内容,不适合创建新文章",
  inputSchema: z.object({
    post_id: z.string().describe("语雀文章 ID,纯数字字符串,如 '12345678'"),
    title: z.string().optional().describe("文章标题,不改时可省略"),
    content_markdown: z.string().describe("Markdown 格式正文"),
  }),
  run: async (input) => {  // input 类型自动推导,问题尽量在编译期暴露
    const post = await getPost(input.post_id);
    if (!post) throw new ToolError("文章 ID 不存在", {
      error_code: "POST_NOT_FOUND",
      suggestion: "请先调用 list_yuque_posts 获取有效的 post_id",
    });
    return await updatePost(input.post_id, input.title, input.content_markdown);
  },
});

调试 Agent 时应先检查工具定义,大多数工具选择错误的原因出在描述不准确,不在模型能力,工具数量也要克制,能用 Shell 处理的、只需静态知识的、更适合 Skill 的,都不需要新增工具。

Zod schema 可以同时生成 JSON Schema 和 TypeScript 类型,把参数验证和文档约束合并在一处,工具调用循环也由 SDK 自动处理。

为什么工具消息也要隔离?

框架运行过程中会产生一些内部事件:压缩发生了、通知推送了、某个工具调用被跳过了,这些事件需要记在会话历史里,但不应该直接进 LLM,否则模型会看到一堆它不理解的字段,白白消耗 token。

解决方式是在框架层分两种消息类型:给应用层用的 AgentMessage 可以携带任意自定义字段,真正发给 LLM 的 Message 只保留 user、assistant、tool_result 三种标准类型,调用前过滤一遍,会话历史保留完整框架状态,LLM 只收它需要的部分。



记忆系统如何设计

AI有个致命的弱点:它没有时间连续性。每次会话结束,它就把刚才的事全忘了,就像你喝了“忘情水”。所以,要让AI能处理跨天的、复杂的长任务,必须给它建立外部记忆。
这个记忆不是随便找个数据库一存就完了,得根据用途分层:

  • 上下文窗口(工作记忆):当前任务需要记住的最小信息,比如“我们刚才聊到第几步了”。
  • Skills(程序性记忆):“怎么做”的记忆,比如“怎么发一封邮件”,按需加载,不常驻。
  • JSONL会话历史(情景记忆):“发生了什么”的记忆,完整记录每次聊天,供以后查询。
  • MEMORY.md(语义记忆):AI主动记下来的重要事实,比如“用户家里有只猫叫‘饭团’”,这个会每次启动时注入,成为它的长期记忆。
这就像一个普通人和一个记忆大师的区别。普通人(只有工作记忆)聊完就忘,而记忆大师(有结构化记忆)会把事情分类,重要的写进日记(MEMORY.md),不重要的就扔进回忆录(会话历史),用的时候再去查。

有了记忆,还得有方法。处理长任务,最怕的不是单步报错,而是做着做着就“断片”了。要么是在一个会话里想一口气干完,结果上下文用完了;要么是只干到一半,下一轮又不知道从哪儿开始了。

解决办法是把长任务拆成“制定计划”和“执行计划”两步:

  1. Initializer Agent:先让一个“计划员”Agent跑一次,把任务拆成一个个小步骤,生成一个“功能清单”(feature-list.json),就像项目经理把一个大项目拆成每天要干的小活。
  2. Coding Agent:然后让“程序员”Agent一遍遍地跑,每次只做功能清单里的一个小任务。它通过看进度文件和Git日志来恢复现场,做完一个,标记完成,提交代码,然后退出。下次再启动,它一看进度文件,就知道该做下一个任务了。这样就算中途程序崩溃了,也只需要从断点重启,不用从头再来。

这就是把进度放在文件里,而不是放在AI的脑子里。脑子会忘,文件不会。

MEMORY.md 和 Skills 如何协作

(1)ChatGPT 四层记忆:拿它当一个产品实现来看,它没有使用向量数据库,也没有引入 RAG 检索增强生成,整体结构比很多人的预期更简洁:

1. Session Metadata:设备、地点、使用模式,不持久化 
2. User Memory:约 33 条关键偏好事实,持久化,每次注入 
3. Conversation Summary:约 15 个最近对话的轻量摘要,持久化 
4. Current Session:当前对话滑动窗口,不持久化

(2)OpenClaw 混合检索:

1. memory/YYYY-MM-DD.md,追加写日志,保留原始细节 
2. MEMORY.md,精选事实,Agent 主动维护 
3. memory_search,70% 向量相似度 + 30% 关键词权重的混合检索

这个设计的好处是可读、可改、可检索,Markdown 文件可以直接查看和修订,搜索时按相关性拉取需要的内容,而不是把全部记忆一次性塞进上下文,对大多数 Agent 来说,记忆库规模并不需要一开始就引入向量存储,结构化 Markdown 加关键词搜索已经具备足够好的可调试性、可维护性和成本表现,只有当规模超过几千条、并且确实需要语义相似度检索时,再考虑引入向量检索会更合适。

记忆整合如何触发并回退

有了记忆分层之后,下一步要处理的就不是“要不要存”,而是“什么时候整合,以及整合失败怎么办”。

注意:不是“把旧消息删掉”,而是把它们从活跃上下文中安全移出。
在持续增长的对话消息流,用 tokenUsage / maxTokens >= 0.5 作为触发阈值,达到阈值后,成功路径会先对待整合消息做 llmSummarize(toConsolidate),再把摘要追加到 MEMORY.md,最后只更新 lastConsolidatedIndex,失败路径则把原始消息写入 archive/,保留完整历史,避免整合失败时丢失上下文。

最关键的不是摘要写得多漂亮,而是流程本身必须可回退,系统只移动指针,不删除原始消息,即使整合失败,也还能回到原始存档继续工作。

如何逐步放开 Agent 自主度

这里说的自主度,不是少几次人工确认,而是让 Agent 能在更长时间跨度内稳定推进任务,前提也不是直接放权,而是先补齐三类基础设施:跨 session 续跑、单个 session 内的进度约束,以及慢速 I/O 的后台接入。

长任务如何跨 session 继续?
长任务最常见的失败,不是单步报错,而是 session 结束时任务还没做完,即使启用 compaction,也挡不住两类问题:一是在单个 session 里试图做完整个应用,结果上下文先耗尽,二是只做完一部分,下一轮又无法准确恢复现场,过早判断完成。

更稳定的做法,是把长任务拆成 Initializer Agent 和 Coding Agent 两个角色协作,这种模式最适合代码生成、应用搭建、重构迁移这类单个 session 做不完、但又能拆成一批可验证子任务的工作。

Initializer Agent 只在第一轮运行一次,负责生成 feature-list.json、初始 git commit 和 claude-progress.txt,先把任务变成可持久化的外部状态,后面的多个 session 由 Coding Agent 循环执行,每次从 claude-progress.txt 和 git log 恢复现场,定位当前任务,实现一个功能,跑测试,更新 passes 字段,提交代码后退出,这样即使中途崩溃,也能直接从文件系统里的状态继续,而不是从头再来。

进度要放在文件里,不要放在上下文里,功能清单用 JSON,不用 Markdown,结构化格式更适合模型稳定修改,当 feature-list.json 里所有功能都变成 passes: true,任务才算完成。

为什么任务状态要显式写出来?
跨 session 解决的是“下次从哪里继续”,单个 session 内还要解决“当前做到哪一步”,长任务一旦拉长,没有外部进度锚点,Agent 很容易偏航,或者在还有任务未完成时过早结束。

任务状态要显式记录为外部控制对象,而不是留在模型的工作记忆里:

json
{
  "tasks": [
    {"id": "1", "desc": "读取现有配置", "status": "completed"},
    {"id": "2", "desc": "修改数据库 schema", "status": "in_progress"},
    {"id": "3", "desc": "更新 API 接口", "status": "pending"}
  ]
}

约束很简单,同一时间只能有一个 in_progress,每完成一步都先更新状态,再继续下一步,必要时再加轻量校正,例如连续多轮未更新任务状态时,自动注入 提示当前进度。

后台 I/O 如何接入?
自主度提高以后,真正容易拖慢主循环的,通常不是模型推理,而是文件操作、网络请求和长耗时命令这类外部 I/O,这些操作一旦阻塞主循环,执行节奏就会明显变差。

务实的做法,是把慢速 subprocess 放到后台线程,通过通知队列在下一轮 LLM 调用前注入结果,主循环不需要感知太多并发细节,只要在每轮开始前检查是否有新结果,再决定继续执行、等待还是调整计划,这通常比把整个 loop 改造成复杂的 async runtime 更稳,也更容易维护。



多 Agent 如何组织

现在流行搞多Agent,觉得Agent越多越牛掰。但文章里点醒了我们,一上来就搞多Agent,就像还没学会走就想跑,大概率会摔得很惨。

多Agent的工程核心,不是“并行”,而是隔离和协作。就像你开公司,不能直接把所有人都扔进一个开放办公室,得给他们分部门,划工位,定流程。

  • 指挥者模式:你和AI一对一,像老板和秘书,沟通紧密。但缺点是秘书一走(会话结束),啥都没留下。
  • 统筹者模式:你是CEO,设定目标后,让各部门经理(主Agent)去协调员工(子Agent)干活,最后你把关成果。这样,你只在开头和结尾出现,中间的工作成果都变成了可存档的文档或代码(PR)。

常见的组织方式是主 Agent 作为 Orchestrator 统筹全局,下挂多个子 Agent 独立并行工作。它们之间通过 JSONL inbox 协议通信,用 Worktree 隔离文件修改,用任务图管理依赖关系。
常见的组织方式是“主Agent+子Agent”:主Agent就像CEO,负责统筹全局,分派任务。子Agent就像各部门的小团队,独立工作。
它们之间怎么交流?不能靠“喊”,得靠“发文”。它们通过一个共享的“收件箱”(JSONL inbox)来传递任务。
比如,主Agent写一个任务放进子AgentA的收件箱,子AgentA做完后,把摘要放进主Agent的收件箱。
这样,子AgentA内部怎么搜索、怎么调试、怎么试错,都在自己的小圈子里,不会污染主Agent的大脑。

这就像一个大公司的跨部门协作:A部门不会把开会讨论时的所有八卦、内部斗争都告诉CEO,只汇报一个“项目已完成”的摘要就够了。

多Agent一旦协作,就得有协议。不能靠自然语言去对齐,什么“你做完这个给我说一下”,AI记不住。得用结构化的消息:

  • 有ID,知道是谁发的、发给谁。
  • 有状态,是“等待处理”还是“已批准”。
  • Append-only(只追加),不能删改,这样崩溃了也能恢复。

这就像在职场里,大家不是靠口头承诺,而是靠邮件或者工单系统来协同,有记录,可追溯。

最后,多Agent还有个坑叫“确认偏误”。Agent A犯了个错,Agent B可能会跟着强化它,Agent C再叠加一层,最后大家都觉得那个错误是对的。解决方法是引入“交叉验证”,比如让另一个独立的Agent,或者单元测试、编译器、人工来当裁判,打断这个错误链条。所以,多Agent的演进顺序很重要:先有协议和隔离,再谈协作和并行。

子 Agent 适合做什么?

子任务里的搜索、试错和调试过程,不该污染主 Agent 的上下文。主 Agent 真正需要的只是结论,探索细节留在子 Agent 自己的消息历史里。

typescript
// 子 Agent 有独立的 messages,跑完只回传摘要
const result = await runAgentLoop(task, { messages: });
return summarize(result); // 主 Agent 上下文里只有这一行

为什么协作方式要写成协议

多 Agent 协作一旦靠自然语言来对齐,很快就会出问题。模型记不稳谁承诺了什么,也记不稳谁在等谁的结果,任务开始互相依赖之后,就得先把协议写清楚:

typescript
// 消息结构:结构化,有状态,append-only,崩溃可恢复
{
  request_id, from_agent, to_agent,
  content,
  status: 'pending' | 'approved' | 'rejected',
  timestamp
}
// 写入:.team/inbox/{agentId}.jsonl,append-only,崩溃可恢复
// 读取:按行解析,按 status 过滤

这里至少要先有三样东西,协议、任务图、隔离边界,主 Agent 通过 JSONL 消息队列分派任务给子 Agent,子 Agent 执行后只回摘要,搜索和调试细节留在自己的独立上下文里,.tasks/ 记录任务图和依赖关系,.worktrees/ 隔离每个子 Agent 的文件修改,顺序也别反过来,协议先定,隔离先做,再谈协作和并行。

多 Agent 下幻觉会互相放大

多个 Agent 频繁互动时,错误也会被一层层放大。Agent A 先带偏,Agent B 跟着强化,Agent C 再继续叠加,最后所有 Agent 都收敛到同一个高置信度的错误结论。交叉验证的价值就在这里,它能打断这条链,让某个 Agent 独立判断,而不是顺着前面的结论继续走。这里也有顺序,先有可持久化任务图,再引入有身份的队友,再引入结构化通信协议,最后再加交叉验证或外部反馈,比如独立的第二个 Agent、单元测试、编译器或人工审查。

子 Agent 有两个基本限制:
第一是深度限制,防止无限递归生成孙 Agent,设一个最大深度就够了
第二是最小系统提示,只给 Tooling、Workspace、Runtime 三节,不带 Skills 和 Memory 指令,避免权限外泄,也避免破坏隔离边界。



可观测性
如果AI出了错,你都不知道它哪一步错了,那你怎么修它?所以,一定要先搭好可观测性,也就是给AI装上“行车记录仪”和“黑匣子”。

这个记录仪得录下所有细节:完整的Prompt、多轮对话的完整内容、每次调用了什么工具、传了什么参数、返回了什么结果、模型内部的思考过程、用了多少token、花了多少时间……最好还能有语义检索能力,你可以直接问系统:“帮我找出那些AI把‘增加’和‘删除’工具搞混的Trace(轨迹)。”

当你的Agent变得复杂,日志量巨大,光靠人工看是看不过来的。这时候就需要自动化。可以分两层来搞:

第一层,人工抽样:基于规则,专门挑那些出错的、对话特别长的、用户给差评的Trace,让专家人工分析,搞清楚失败模式,这相当于给下一层提供“参考答案”。
第二层,LLM自动评估:对更大范围的Trace做全量覆盖,用第一层的结果作为校准,让LLM自动打分。这样既保证了效率,又保证了准确性。

而且,采样不能是随机的,得有策略。比如,用户说“不满意”的对话,100%要进审查队列;token花了很多的,也要优先看;每天固定时段随机采一些,看看正常情况。这就像警方办案,不会所有案件都投入同等警力,重大案件(负反馈、高成本)要重点查,同时也要定期抽查普通案件,防止有漏网之鱼。

在工程实现上,要让Agent在执行时,像放烟花一样,在每个关键节点(比如工具开始执行、工具执行完、整个回合结束)都“发射”一个事件。然后,日志系统、UI更新、评测系统、人工审查队列都可以去“监听”这些事件,各取所需。这样,Agent的核心代码非常干净,不需要为了任何一个下游系统去改代码,实现了很好的解耦。



OpenClaw是怎么练成的

好了,前面讲的都是理论,这一节我们直接看一个活生生的例子——OpenClaw。这是一个开源的Agent实现,它把前面说的那些原则,都变成了实实在在的代码。

OpenClaw的结构就像一栋五层楼:

  1. 顶楼(Gateway):一个WebSocket服务,统一收发消息,是个“前台的传话筒”。
  2. 二楼(Channel适配器):有23种聊天渠道(Telegram、飞书等)的适配器,它们不直接和Agent说话,而是把消息写成纸条,扔进一个叫MessageBus的“信箱”里。想加个新渠道?再写个适配器就行,Agent代码纹丝不动。
  3. 三楼(Pi Agent):这是真正的“大脑”,负责Agent的主循环、会话状态、调度。它只从MessageBus里拿纸条,处理完再放回去。核心循环和渠道彻底解耦,互不干扰。
  4. 四楼(工具集):各种工具,比如操作Shell、读写文件、控制浏览器等,都按ACI原则设计,好用又安全。
  5. 一楼(上下文+记忆):这里管理Skills的延迟加载和MEMORY.md,还负责在Token超过阈值时,自动整合记忆,防止AI犯“老年痴呆”。

OpenClaw还有个特别的设计:它支持定时任务。系统可以按设定的时间,主动叫醒Agent,让它去干活。比如每天早上9点,让Agent去查一下昨天的生产日志,有高频错误就给出排查建议。这时的Agent,不需要等用户发消息,自己就是“上班族”。

为了防止长任务崩了,OpenClaw把任务进度写到了磁盘上。这样即使重启,也能从断点继续。这个恢复机制不是“可选项”,而是“必选项”,尤其是那些要跑半小时以上的任务。
安全方面,OpenClaw也是铁面无私。首先,它有个白名单,只让授权的用户和它说话。其次,每个工具,比如执行Shell命令,都会强制检查路径,绝对不允许越出你的工作目录,乱改系统文件。最后,每一步操作都会写入审计日志,谁、在什么时候、干了什么,清清楚楚,想赖都赖不掉。

除了权限,它还有两层保险:防Prompt Injection和模型服务故障切换。

  • 对于Prompt Injection,就是把外部来的、不可信的内容,在输入时就标上“不可信输入”的标签,和它自己的系统指令严格分开。并且,任何敏感操作(比如删文件、写数据库),都必须先经过用户确认,AI不能自己偷偷干。
  • 对于模型服务故障,它做了个简单的容灾。如果当前用的模型(比如Anthropic)返回503了,它就会自动切换到下一个模型(比如OpenAI),完全不用人盯着。这就叫“鸡蛋不放在一个篮子里”。

总结

这篇文章主要讲 Agent 架构里几块最影响工程效果的内容,包括控制流、上下文工程、工具设计、记忆、多 Agent 组织、评测、追踪和安全,最后再用 OpenClaw 的实现把这些设计原则串起来看一遍。

最后,总结一堆常见的坑和解决方案,就像一份“避坑指南”:

  1. 把系统提示当百科全书:越写越长,关键规则被淹没。→ 修正:约定留在提示里,知识移到Skills里。
  2. 工具太多,AI选花眼:频繁选错。→ 修正:合并重叠的工具,给工具起清晰的名字。
  3. 验证闭环缺失:AI说“做好了”就没下文了。→ 修正:每个任务都绑定验收标准。
  4. 多Agent没边界:状态混乱,谁的责任都搞不清。→ 修正:明确角色权限,用worktree隔离文件。
  5. 记忆不整合:20轮后就变傻。→ 修正:监控Token,超阈值自动触发整合。
  6. 没有评测:改没改坏全凭感觉。→ 修正:失败案例立刻变成测试用例。
  7. 过早引入多Agent:协调成本比并行收益还高。→ 修正:先看看单Agent的上限。
  8. 约束靠“期望”:规则写在文档里,AI选择性遵守。→ 修正:把规则变成工具验证、Linter、Hook,让机器强制执行。