作者归档:admin

OpenClaw 的 Skills 的实现和 Claude Code 不一样

OpenClaw 的 Skills,本质上不是一个「可调用工具」,它更像一套经过约束的运行手册:启动时把技能目录扫描出来,压成一份 <available_skills> 清单塞进 system prompt,模型自己判断要不要选一个 skill,然后再通过 Read 工具去读这个 skill 的 SKILL.md。读完以后,没有任何独立执行器接管,还是在当前这条 session 的 tool-loop 里继续跑。

Claude Code 走的是另一条路。它把 skill 做成了 tool,工具里负责校验、加载、执行,甚至可以放进一个新上下文里跑完,再把结果回传主对话。

这两个实现方向,表面上都叫 skills,工程含义完全不一样。

如果你是做 Agent 平台、企业内 Copilot、代码助手、任务执行器,这个差异不只是架构图上的审美问题。它会直接影响:

  • prompt 预算怎么花
  • 权限边界放在哪里
  • skill 的治理成本有多高
  • 运行链路是可控还是失控
  • 你后面想不想做隔离执行、审计、回放、灰度发布

1. OpenClaw 的 Skills,到底是怎么被「召回」的

很多人一看到 skills,第一反应是:是不是和 memory 一样,先走 embedding 检索,再把相关技能召回进上下文。

不是。

OpenClaw 的 skills 召回机制非常直接,甚至可以说有点「朴素」:扫描目录,生成目录清单,注入提示词,让模型自己选。整条链路分三段:

  1. 发现
  2. 注入
  3. 读取

OpenClaw 反过来做了一件更工程化的事:先把 skill 列表显式给模型,再用 system prompt 约束它怎么选。

这个机制的起点在 src/agents/skills/workspace.tsloadSkillEntries()

1.1 技能发现:先扫目录,再谈调用

OpenClaw 会从多个 root 目录扫描 SKILL.md,然后合并成最终技能集。这里是有明确的覆盖优先级的:

extra < bundled < managed < agents-personal < agents-project < workspace

它的逻辑是:越靠近当前工作空间、越贴近用户项目的 skill,优先级越高。平台预置的 bundled skill 可以兜底,但项目级 skill 要能覆盖它。否则你做企业落地时会很难受,团队定制流程永远被平台内置逻辑压着打。

从目录结构看,OpenClaw 支持两种 skill 形态:

  • root 本身就是一个 skill,目录下直接有 SKILL.md
  • root 下的子目录分别是 skill,每个子目录里有自己的 SKILL.md

这样,团队在实际维护 skill 时,有两种典型组织方式:

  • 单一 skill 仓库,根目录就是技能内容
  • skills 集合仓库,每个子目录一个技能

都支持,落地阻力会小很多。

它对 SKILL.md 做了体积限制,默认超过 256KB 就直接跳过。

SKILL.md 本来就应该是高密度操作指南,不应该演变成一个什么都往里塞的大文档。你一旦允许 skill 文件无限变大,后面一定会有人把 SOP、FAQ、设计文档、事故复盘全扔进去,最后技能选择和加载成本一起爆炸。OpenClaw 在扫描阶段直接卡体积,其实是在替平台维护纪律。

1.2 frontmatter 解析

给 skill 增加最小限度的结构化元信息

扫描到 SKILL.md 后,OpenClaw 会读原文并解析 frontmatter。坏掉或者缺失的 frontmatter 会被忽略。

这个决策也很对。

OpenClaw 这里更像是「有就用,没有拉倒」。它承认 markdown 本体才是 skill 的核心,frontmatter 只是辅助控制面。这个姿态很适合 skills 这种增长很快、来源很多的资产类型。

但这里也埋了一个工程 trade-off:frontmatter 被弱约束,意味着后续治理和平台能力扩展会受限。你今天只做 discovery 和 basic gating,这么玩没问题;你明天如果要做 skill 分类、依赖分析、版本兼容、批量审计,元信息松散会让成本陡增。

OpenClaw 当前站在了「先把系统跑起来」的一边,没有走「先把规范做重」的路子。

这意味着它更适合中小规模 skill 生态,或者说更适合个人助手这类工具,而不是一个强治理的企业级 skill marketplace。

2. 不是所有 skill 都会进 <available_skills>

扫描出来只是候选集,不等于模型能看到。

OpenClaw 在注入 system prompt 之前会做一轮 gating。这个步骤比很多人想象得重要,因为它决定了模型到底暴露给了哪些能力。

过滤逻辑里有几类条件。

2.1 配置开关

最基础的 enable/disable

如果某个 skill 在配置里被标成 skills.entries.<skillKey>.enabled === false,它就会被剔除。

这是最基础的 kill switch。工程上没什么可争议的,必须有。不然你没法快速止血。某个 skill 写坏了、依赖环境挂了、被发现会引导模型做危险操作,没有一键下线能力,平台就不配上线。

2.2 bundled allowlist

对内置技能单独管控

skills.allowBundled 只约束 bundled 来源。

这个细节很有意思。它说明 OpenClaw 把 bundled skills 当成一类特殊资产处理:平台自带,但不默认无条件信任。

为什么这事重要?因为内置 skill 经常是平台演进中最容易「偷偷变多」的那部分。你今天打包 5 个,明天为了演示方便塞到 20 个,后天 prompt 里一大坨 descriptions,模型选 skill 的噪声越来越大。allowlist 的存在,本质上是在给平台预装能力上保险栓。

2.3 eligibility 判断

按运行环境判断 skill 是否可用

OpenClaw 会根据 metadata.requires 去判断 skill 能不能用,条件包括:

  • 二进制依赖是否存在
  • 环境变量是否满足
  • 配置是否具备
  • 操作系统是否匹配
  • remote 平台是否匹配

这个设计非常工程化。因为 skill 如果涉及真实操作,它一定和环境耦合。比如某个 skill 依赖特定 cli,或者要求某个 API token,或者只能在 Linux 跑。你如果不在注入前做 eligibility,而是让模型先看到 skill,再在执行时失败,用户体验会很差,模型行为也会变形:它会看到一个貌似可用的方案,实际一调就炸。

OpenClaw 选择「先过滤,再暴露」,这是我非常认同的策略。因为对模型来说,看得见就等于潜在可用。你让它看见无效 skill,本质上是在制造认知噪声。

2.4 disable-model-invocation

系统内部保留,模型不可见

如果某个 skill 标记了 disable-model-invocation,它不会进入 prompt 的 <available_skills>,但仍然存在于系统内部,可用于管理或校验。

因为技能资产不只有「给模型用」这一种角色。你可能有些 skill 需要:

  • 用于测试
  • 用于审核
  • 用于运维校验
  • 用于内部 pipeline
  • 用于未来发布但暂不开放

OpenClaw 没把「存在」和「可被模型调用」绑死,这样 skill registry 才像个平台能力,而不是一个 prompt 拼装器。

3. <available_skills> 才是 OpenClaw 的真正召回入口

很多人说 skills 被召回,其实这句话在 OpenClaw 语境里容易让人误解。

真正被放进上下文里的第一层,不是 SKILL.md 内容,而是 <available_skills> 这个目录清单。它由 buildWorkspaceSkillsPrompt() 生成,注入到 system prompt。

也就是说,模型最先看到的是技能目录,不是技能正文。

这个做法也算是渐进式披露的一种实现。先给模型足够决定是否要读 skill 的最小信息,避免一开始就把所有技能全文灌进去。

3.1 system prompt 里的强规则,决定了技能选择流程

OpenClaw 在 system prompt 里有一段明确的 Skills mandatory 规则,大意是:

  • 先扫描 <available_skills> 里的 <description>
  • 只有明确匹配一个 skill 时,才去读对应的 SKILL.md
  • 最多只读一个 skill

这三条里,最重要的是后两条。

3.1.1 只有明确匹配一个才读

这是在压制模型的泛化冲动。模型很喜欢「我觉得这个也相关,那个也相关」,然后多读几个,最后把自己卷进上下文泥潭。OpenClaw 用提示词硬性要求「明确匹配一个」才允许进入下一步,这本质上是在给模型加稀疏化约束。

效果未必 100% 可控,但方向是对的。

3.1.2 最多只读一个 skill

如果你做过基于 prompt 的 tool orchestration,就会知道一个常见死法:模型在一堆说明里来回跳,读完 A 觉得 B 也有用,再读 B,顺手 C 也看看,最后 token 花了不少,任务还没干。

OpenClaw 这里直接规定 upfront 只能读一个 skill。我个人判断,这不是因为理论上最优,而是因为工程上最稳。

它牺牲了一部分组合式技能能力,换来了几个非常实际的好处:

  • token 消耗更可控
  • 模型决策路径更短
  • skill 选择失败更容易定位
  • 调试和回放更容易做

代价也很明确:如果任务天然需要多个 skill 协同,OpenClaw 当前机制会显得笨。模型要么自己在单个 skill 指导下绕着做,要么根本选不准。

这也是它和 Claude Code 一个很大的分歧。Claude Code 把 skill 视作 tool,更容易天然支持复杂编排;OpenClaw 把 skill 视作可读说明,天然倾向于单次聚焦。

3.2 技能过多时的降级策略:compact 和 truncated

<available_skills> 不是无上限注入的。技能太多时,OpenClaw 会降级成 compact 格式,甚至截断,并提示 skills truncated

这个点看起来像 prompt 小技巧,其实是很重要的预算治理。

因为技能系统一旦跑起来,数量几乎只会越来越多。最初十几个 skill,description 全量塞进去还行;到几十上百个 skill,再这么搞,system prompt 很快会变成垃圾堆。OpenClaw 至少意识到了这个问题,所以做了两级退化:

  • compact:保留更少信息
  • truncated:截断并明确提示

这说明它把 prompt 当成有限资源,而不是无限容器。

但实话实说,这个策略只是在「延缓爆炸」,不是根治。skill 数量继续增长以后,靠 compact/truncate 顶不住。因为问题不只是 token 多了,而是模型在目录里做决策的辨识难度会越来越高。description 再压缩,skill 之间的区分度也会变差。

所以我对这块的判断是:OpenClaw 的 catalog 注入机制适合中等规模 skill 集,不适合无限扩张。
如果你团队未来真要做上百个 skill 的企业级平台,迟早要引入更分层的 catalog、分类路由、或者显式 selector,而不是靠一坨 <available_skills> 让模型裸选。

4. 真正的 SKILL.md 是怎么进入上下文的

OpenClaw 的第二层加载是按需读取。模型先看到目录清单,选中 skill 以后,才通过 Read 工具去读对应路径的 SKILL.md

4.1 技能正文不是 system prompt 的一部分

system prompt 里只有规则和技能目录。真正的 SKILL.md 内容,是在模型发起一次 read tool call 后,作为 toolResult 追加进当前消息流。

也就是说,skill 正文进入上下文的方式,和你用工具读一个普通文件没有本质区别。差别只是 system prompt 先规定了什么时候允许这样读。

这个设计的好处非常明确:

  • 大幅降低初始上下文体积
  • 让技能正文成为按需成本,而不是固定成本
  • skill 更新后无需改 system prompt 模板,只影响运行时读到的内容
  • 便于把 skill 当文件资产管理,而不是 prompt 模板片段

坏处也很明确:

  • skill 的执行效果更依赖模型有没有正确触发 read
  • skill 的权威性被弱化成一条 toolResult,而不是顶层指令
  • 如果 toolResult 很长,后续上下文里它和普通读文件结果没什么层级差异

从实现角度看,OpenClaw 这里是很克制的。它没有发明一个 Skill Runtime,没有做一套专门的 skill 调度协议,就是借已有的 read 工具完成正文加载。

这让系统保持简单,但也让 skill 更像「读到的一份说明书」,而不是「被激活的执行单元」。

5. OpenClaw 的安全边界

既然 skill 是文件,模型要去读 SKILL.md,安全问题马上就来了:路径能不能逃逸?symbolic link 能不能把 skill 指到 root 外?模型能不能顺着 location 读到不该读的内容?

OpenClaw 这里做了两层保护。

5.1 第一层:扫描阶段的 containment 检查

在技能发现阶段,它会对 realpath 做 containment 检查,防止 symlink 把 skill 指到 root 外面。

这是典型的「尽早失败」策略。我一直觉得文件系统相关的安全问题,能在发现阶段拦的,不要拖到执行阶段。因为一旦把异常路径放进 skill registry,后面每个环节都得假设它可能有问题,整个系统会变得很难推理。

扫描时就把越界项挡掉,registry 内部的数据至少是干净的。

5.2 第二层:Read 工具的 workspace root guard

模型真正发起读取时,Read 工具还会做 sandbox root 校验,禁止 .. 或绝对路径逃逸 workspace root。

也就是说,即使 discovery 阶段没出问题,真正读取文件时还有一层运行时防线。

文件边界这事,单点防护永远不够。扫描阶段挡的是「注册进来的 skill 本身」,读取阶段挡的是「模型实际提出的路径参数」。二者保护的对象不一样。

工程上要是只做第一层,你会被运行时路径拼接坑;只做第二层,你会把很多脏数据带进 registry,影响可观测性和调试。

OpenClaw 在这块虽然不算复杂,但做法是标准的。

6. Skills 的渐进式披露

OpenClaw 的 skills 机制,如果只看「枚举目录 -> 注入 prompt -> read 文件」,会有人觉得太朴素。真正值得注意的是,它和 memory 一样,贯彻了一套很明确的渐进式披露思路。

这是上下文预算治理的逻辑。

原则就两条:

  1. 先给最小可用信息
  2. 确认需要后再扩大读取范围

在 OpenClaw 里,memory 和 skills 都是这么干的,只是路径不同。

6.1 Memory 是先搜 snippet,再精读指定行段

memory_search 先返回短片段,带 path 和 line range,不是全文注入。底层还有 SNIPPET_MAX_CHARS = 700 的硬上限。如果后端预算紧,还会继续裁结果。

之后再通过 memory_getpath + from/lines 去拉具体行段。

这个流程是非常标准的 progressive disclosure:先定位,再精读。

6.2 Skills 是先给目录,再只读一个 SKILL.md

skills 这边没有向量召回,而是目录清单。模型先看 <available_skills>,只在明确匹配一个时才读正文,并且 upfront 最多一个。

两条链路表面不同,本质一样:都在防止大块文本无脑灌进上下文。

我为什么说这点值钱?因为很多团队做 Agent 系统时,最大的问题不是模型不会做事,而是上下文管理太粗糙。什么都想喂进去,最后 token 烧得飞快,模型还因为噪声太高做不准。

OpenClaw 至少在架构层面承认了一件事实:上下文是预算,不是仓库。

这件事说起来简单,真正落实到工具语义、提示词规则、底层裁剪,很多系统做不到。

7. OpenClaw 没有「独立 Skill 执行器」

在 OpenClaw 里,skill 读完之后,并没有一个单独的执行环境接管它。没有所谓「Skill Runtime」去解释 SKILL.md,也没有「新上下文执行 skill」这回事。它还是在同一个 activeSession.prompt(...) 的 tool-loop 里继续跑。

链路大概是这样:

  1. system prompt 里给出 <available_skills> 和选择规则
  2. 模型决定某个 skill 匹配当前请求
  3. 模型调用 read 工具读取对应 SKILL.md
  4. toolResult 里带回 SKILL.md 文本
  5. 这个 toolResult 被追加到当前 session 的 messages
  6. 模型再次被调用,看到刚才读到的 skill 内容
  7. 模型按 skill 指导,继续发起后续 toolCall,比如 execwriteedit
  8. 直到输出最终回答

关键点:skill 内容只是同一消息流里多了一条 toolResult

这意味着什么?

意味着 OpenClaw 的 skill 执行,本质上是「模型读了一份流程说明,然后继续在原对话里行动」。它没有新的边界,没有新的记忆隔离,没有新的权限域。真正的隔离,来自工具列表裁剪和文件读写沙箱,不来自上下文切换。

这和 Claude Code 很不一样。

8. Claude Code 的 skills,更像「可调用子程序」

Claude Code 的 skill 流程有几个特征:

  • skill 是 tools 列表里的一个明确工具
  • 模型会调用 Skill tool,而不是自己去 read 某个 markdown
  • skill prompt 的加载是 tool 内部动作
  • 权限校验也主要发生在 tool 内
  • skill 可以在新上下文执行,执行完再把结果带回主对话

从工程抽象上看,Claude Code 的 skill 更像一个「子程序入口」。它有名字、有调用接口、有内部装载逻辑,甚至有上下文隔离能力。

OpenClaw 没有走这条路。它把 skill 设计成「模型可读的文件」,由模型自己决定是否展开,并在原上下文里继续操作。

这两个方向,没有谁天然高级,但适用面不同

如果你要的是:

  • 更强的执行隔离
  • 更容易做权限封装
  • 更容易做结果回传和子任务边界
  • 更适合复杂、多步骤、可复用的任务单元

Claude Code 那种 tool 化 skill 更合适。

如果你要的是:

  • 实现简单
  • skill 作者门槛低
  • 把 skill 当 markdown 资产管理
  • 快速把流程知识接进现有 ReAct tool-loop

OpenClaw 这种文件化 skill 更实用。

工程上不看理念,看代价结构。

9. OpenClaw 为什么会做成这样

我猜它的设计动机是:尽量复用现有 agent runtime,而不是为 skills 单独发明一层执行框架。

你看它的做法就知道了:

  • discovery 复用文件系统扫描
  • selection 复用 system prompt
  • loading 复用 read 工具
  • execution 复用原有 tool-loop
  • safety 复用 sandbox path guard 和 tool policy

这是一套很节制的架构思路。好处是:

实现成本低 :不用造新协议,不用加新消息类型,不用多一层 runtime。对一个正在演进中的 agent 系统来说,这非常现实。 Skill 作者认知负担小:本质上写一个 SKILL.md 就行。对于组织内部推广,这是巨大优势。因为真正阻碍 skill 规模化的,从来不是模型能力,而是作者生态能不能起来。 运行链路可观测性还不错:所有事情都发生在同一 session 里。read 了什么、接着调了什么工具、toolResult 长什么样,回放起来相对直观。

但它的代价也有。

代价 1:skill 缺少强执行边界

skill 本身不是 tool,没有独立权限面。你没法像封装一个函数那样,给 skill 绑定专属 schema、专属校验、专属 side effect 边界。最终还是模型在拿到 skill 文本后,自由地调用后续工具。

这就意味着 skill 的约束力主要来自提示词,不来自执行器。

代价 2:组合式编排能力弱

system prompt 里要求 upfront 最多读一个 skill,这对预算控制很好,对复杂任务不友好。多 skill 协同在这个体系里不是一等公民。

代价 3:skill 的结果不可天然封装

Claude Code 那种「在新上下文执行 skill,再返回结果」,天然有个输入输出边界。OpenClaw 这里没有。skill 一旦展开,后续动作直接混进主会话消息流。你很难把它当作一个独立执行单元来治理。

代价 4:对模型的选择质量要求高

因为没有独立 selector,也没有 tool-level dispatcher,skill 能不能选对,主要靠 <available_skills> 的 descriptions 和 system prompt 规则。这对 skill 描述质量要求很高。写得不清楚,模型就选歪。数量一多,问题更明显。

10. 权限控制

OpenClaw 的思路是「先裁剪能力,再让模型行动」

这一点我很喜欢,因为它比「调用时再临时拦截」更稳。

OpenClaw 的权限控制,不是把所有工具都亮给模型,然后在调用某个危险工具时说不行。它更像是:

  1. 先根据 policy pipeline 把不允许的工具从列表里移掉
  2. 再把剩下的工具暴露给模型
  3. 模型根本看不到被禁的能力

skills 这边也一样:

  • 不合格的 skill 不进入 <available_skills>
  • disable-model-invocation 的 skill 对模型不可见
  • Read 路径有 workspace root guard

这种「先裁剪,再推理」的方式有个很大的好处:模型认知空间更干净。它不会围着一堆不可用能力打转,也不会生成大量被拒调用的无效动作。

如果你做企业场景,这比事后 deny 好得多。因为用户只会看到 agent 做合理尝试,而不是不停撞墙。

当然,它的代价是灵活性差一点。你没法在非常细粒度的时刻做动态放行,除非重新构建 tool list 或 skill list。但我认为这笔账是划算的。Agent 系统的第一优先级不是灵活,是可控。

11. 从平台设计角度看呢

觉得比较好的点:

第一,简单。
这个简单不是简陋,是尽量不新增系统概念。skill 就是文件,加载靠 read,执行靠原有 tool-loop。对演进中的 agent 框架来说,这是非常健康的选择。

第二,预算意识强。
<available_skills> 目录注入、最多只读一个 skill、compact/truncated 降级,这些都说明它把 context 当稀缺资源处理。

第三,权限思路对。
先过滤 skill 和 tool,再让模型行动。可见性先于可调用性,这很工程化。

第四,适合知识流程化。
很多团队真正需要的,不是一个会自己发明流程的 agent,而是把组织内已有 SOP 结构化地喂给模型。OpenClaw 这套很适合干这个。

一些局限:

第一,规模扩展性一般。
catalog 注入机制天然不适合 skill 数量无限增长。它适合几十级别,不适合大规模 marketplace。

第二,组合能力弱。
「最多只读一个 skill」对稳态有帮助,对复杂任务编排是限制。

第三,skill 缺少运行时身份。
它不是 tool,没有明确的输入输出边界,也没有独立权限与审计单元。后续做深治理会比较难。

第四,过度依赖模型选择。
一旦 descriptions 写得不好,或者目录过大,skill 选择质量会变成系统上限。

12. 适用场景

在系统架构选型时,如果有以下的条件,可以优先选择 OpenClaw 的设计逻辑:

  • 团队已经有一套成熟的 tool-loop,想低成本引入 skills
  • 主要目标是把流程知识、操作手册、领域步骤注入 agent
  • skill 作者很多,技术水平参差不齐,需要低门槛 markdown 入口
  • 更看重实现速度和治理可落地,而不是复杂编排能力
  • skill 数量在可控范围内,不会迅速膨胀到几百个

有如下的情况时,就不太适用使用这种方案:

  • skill 本质上是可执行业务子任务,需要独立生命周期
  • 需要强隔离执行、单独审计、结果回传和失败恢复
  • 任务天然需要多个 skill 协同编排
  • 技能库规模会非常大,必须做复杂路由
  • 权限模型要求细到「某个 skill 可以做 A 但不能做 B」

这些场景下,我更倾向 Claude Code 那种 tool 化 skill,或者更进一步,直接走 subagent / workflow node / task runtime。

13. 小结

如果只用一句话概括两者区别:

Claude Code 的 skill 更像可调用子程序,OpenClaw 的 skill 更像按需展开的运行手册。

前者强调执行单元,后者强调上下文注入。
前者天然适合隔离和编排,后者天然适合轻量接入和流程沉淀。
前者的复杂度在 runtime,后者的复杂度在 prompt 和内容治理。

OpenClaw 用极其精简的 Prompt 规则和现成的 Read 工具,低成本实现了 Agent Skills 标准。它够用,但也把长文本管理的压力,原封不动地推给了底层大模型的 Context Window。

我个人对 OpenClaw 这套实现是认可的,前提是别把它想象成一个万能技能平台。它解决的是「如何在不重写 agent runtime 的前提下,把结构化流程知识接进模型执行链路」这个问题。这个问题它解得挺干净。

但如果你要的是 Claude Code 那种「新上下文执行 skill、像子程序一样调用、跑完把结果带回来」,你该找的是子会话、subagent、任务编排层,而不是继续往 SKILL.md 上堆规则。

以上。

聊下 OpenClaw 的记忆系统

OpenClaw 是最近 AI 圈最火的一个开源项目,没有之一。

从去年的 Agent 年,到今年的 AI 个人助理,OpenClaw 和去年 Manus 一样的,爆到不行,而且还是开源的版本。

由于最近自己也在做 Agent,于是也看了 OpenClaw 的代码来了解其记忆系统的实现。有一些觉得可以借鉴学习的地方。

OpenClaw 的记忆系统其实比较简单:它把「记忆」拆成了文件索引召回注入

1. 「Agent 记忆系统」的定义

在 Agent 工程里,记忆是一套能力组合:

  1. 持久化:跨会话保存事实、偏好、决策、未完成事项。
  2. 可检索:能在需要时把相关片段拉出来,且可控预算。
  3. 可注入:把召回结果以确定的结构进入模型上下文,不靠「它自己想起来」。
  4. 可审计:出了错能定位「写入发生在什么时候」「召回命中了什么」「注入了哪些行」。
  5. 可治理:能处理泄露风险、过期信息、重复信息、冲突信息。

OpenClaw 的实现路径非常「工程」:Markdown 作为事实源SQLite 作为检索索引toolResult 作为注入通道

2. OpenClaw 的三层记忆

OpenClaw 的记忆从存储逻辑上来看可以分为三层:

2.1 会话记忆

  • 介质:内存为主,但 OpenClaw 会把 session 打印成类似日志的文件,放到 sessions 目录。
  • 内容:用户消息、OpenClaw 的思考过程、工具调用、skill 调用、最终回复。
  • 边界:会话结束后「可用性」就不可靠了。你能在文件里回放,但模型下一次对话并不会天然带着它。

会话记忆更像「trace」。我们不能指望它解决跨会话连续性,只用它做排障、复盘、抽取素材(写入短期/长期)。

2.2 短期记忆

  • 介质:磁盘,memory/YYYY-MM-DD.md 为主(参考内容给了例子 2026-03-10.md)。
  • 内容:当天重要事件、过程笔记、TODO。关键点是「重要性」由人设与调教决定。
  • 边界:短期记忆是追加式日志,质量会漂移。写得越多,噪声越大;但写得太少,又召回不到。

短期记忆适合承接「会话压缩之前的落盘」和「跨几天的上下文连续性」。它不是最终事实源,别把它当永久协议文档。

2.3 长期记忆

  • 介质:工作区根目录 MEMORY.md(参考内容明确)。
  • 内容:核心认知与关系、偏好风格、长期目标、进行中任务、关键事件/教训/决策。
  • 边界:参考内容强调「只在主会话加载」,群聊等任务不加载,避免泄露。

MEMORY.md ==「可执行的组织记忆」。它的价值不在于「写得多」,在于「冲突少、可被召回、能约束后续行为」。这层要治理,要像维护配置一样维护。

3. OpenClaw 的文件布局

3.1 「会话快照」文件

快照文件主要是解决 /new/reset 指令的断片

session-memory 的 Hook 会在你执行 /new/reset 前,把上一会话最近 N 条对话(默认 15 条)抽出来,写成一个 Markdown 文件放到 workspace/memory/ 下。

  • 命名:YYYY-MM-DD-<slug>.md,slug 通常由 LLM 根据主题生成;LLM 不可用就回退成 HHMM
  • 关键点:这种文件也会被检索索引到。原因是 listMemoryFiles() 会递归扫描 workspace/memory/ 下所有 .md,并不要求必须是 YYYY-MM-DD.mdsrc/memory/internal.ts:115-145 )。

这个机制对「人类工作流」很友好。很多团队的真实使用是:今天临时开了个话题,明天又忘了开在另一个会话里。会话快照能把碎片变成可检索素材,后面再沉淀进 MEMORY.md

3.2 memory/main.sqlite:索引库

  • memory/main.sqlite 基本可以确定是「记忆搜索(memory_search)」用的 SQLite 索引库。
  • 索引对象:MEMORY.mdmemory/**/*.md,以及你配置的 extraPaths,可选 session transcripts。
  • 检索方式:FTS/BM25 关键词检索 +(可选)向量相似度检索(sqlite-vec)。
  • 它存的典型结构:fileschunksembedding_cache,以及可选的 FTS 表、向量虚表。

事实源是文本文件索引是可重建的派生物。索引坏了你删掉重建就行;事实源坏了才是真的坏。

4. 怎么建索引:

建索引我们关心的三件事:增量、去重、成本

OpenClaw 的索引构建不是「每次全量重算」,也不是「精细 diff」:

  • 更新粒度:按文件做增量。文件没变直接跳过。
  • 分块粒度:文件变了就重建该文件 chunks。
  • 向量成本:chunk embedding 通过缓存复用,避免重复调用 embedding provider。

4.1 扫描哪些文件会进索引

OpenClaw 会递归扫描 workspace/memory/ 下所有 .md,并包含 MEMORY.md/memory.md。对应定位是 src/memory/internal.ts:115-145

4.2 文件级 hash:没变就跳过

文件 hash 的策略:

  • Markdown:hash = sha256(content)internal.ts:L245-L263
  • 多模态:buffer 也会 hash,最后把 {path, contentText, mimeType, dataHash} 做 JSON 再 sha256(internal.ts:L204-L243
  • 增量判定:对比 files 表里的 hash,一致就跳过(参考内容列了 memory 文件与 session 文件两条路径)。

hash 是增量的核心。它的意义不止省时间,还省钱:embedding provider 往往是计费点。这种主要是对于使用第三方 embedding 的。

4.3 分块策略:tokens*4 的字符近似

chunkMarkdown() 的策略:

  • maxChars = tokens * 4overlapChars = overlap * 4
  • 以「行」为主,超长行会被切段
  • 每块生成 hash=sha256(text),并有 embeddingInput

tokens*4 这种近似在工程里挺常见,优点是简单、稳定、跨模型大差不差。缺点也明显:

  • 语言差异会影响 token/char 比例;中英文混排时 chunk 尺寸会漂。
  • 以行切块对 Markdown 友好,但对「一行很长的 JSON 或日志」不友好。

可以盯两个指标来调 chunking:

  1. 平均召回 snippet 的「可读性」和「自洽性」;
  2. SQLite 体积与索引更新耗时。chunk 太小召回碎,太大注入贵。

4.4 chunk 的唯一标识与 upsert

去重靠 id,chunk 写入策略如下:

  • chunk idsha256("${source}:${path}:${startLine}:${endLine}:${chunk.hash}:${provider.model}")
  • 同一 id:ON CONFLICT(id) DO UPDATE 覆盖更新
  • 文件要重建时会先清旧再写新(参考内容总结了「清旧再写新」语义)

这里有个很实际的 trade-off:

  • 它不做「chunk diff」,所以文件变了就重建该文件 chunks,逻辑简单,坏处是 IO 多。
  • 但 embedding 通过缓存复用,把最贵的部分压下去了。

另外,id 里带了 provider.model,这会带来一个工程后果:embedding 模型换了,chunk id 会变,索引层面等价于全量重建。 provider/model/providerKey 变化会触发 full reindex。

4.5 embedding_cache

embedding 缓存的主键设计:

  • 主键:(provider, model, provider_key, hash)
  • provider_key 会把 endpoint/headers 等纳入指纹,避免跨配置污染缓存(而且会剔除授权头的细节在参考内容里提到)。
  • 批量加载命中就跳过 embed;miss 才请求,成功回写缓存(对应 manager-embedding-ops.ts 的行段)。

这是「上线能用」的关键。否则就会遇到一个很尴尬的情况:
索引更新频繁触发 embedding 重算 → 延迟抖动 → API 费暴涨 → 还可能被 provider 限流。 system prompt 的 skills 段落甚至提醒「假设有 rate limits,避免 tight loop」,这就有点被打过之后写进规范的味道。

5. 更新怎么触发

watch、interval、onSearch、session-delta

索引更新如果做得「过勤」,会把 CPU 和 IO 吃满;做得「过懒」,召回就是过期的。OpenClaw 在触发上给了多条路径:

  1. watch:chokidar 监听 MEMORY.mdmemory.mdmemory/**/*.md(以及 extraPaths、多模态扩展),变更标记 dirty,debounce 后 sync(reason="watch")
  2. intervalsync.intervalMinutes>0setInterval 定时跑。
  3. onSessionStart:search 前 warmSession(sessionKey),若开了 sync.onSessionStart,每个 sessionKey 首次触发后台 sync。
  4. onSearch:search 时如果 dirty 且开了 sync.onSearch,后台触发 sync。
  5. session-delta:监听 transcript 更新,累计新增 bytes/lines,达到阈值后把相关 session 文件标脏,再 sync。

还有两点:

  • 单飞锁:同一时刻只跑一个 sync,复用同一个 Promise(参考内容定位 manager.ts:452-467)。
  • 全量重建的原子 swap:写到 .tmp-UUID,完成后 swap(含 wal/-shm),避免半成品索引(参考内容定位 manager-sync-ops.ts:1050-1158)。

6. 召回是怎么发生的

主要看 buildMemorySection() 的代码,因为它把策略写死了:

提示词:
function buildSkillsSection(params: { skillsPrompt?: string; readToolName: string }) {
  const trimmed = params.skillsPrompt?.trim();
  if (!trimmed) {
    return [];
  }
  return [
    "## Skills (mandatory)",
    "Before replying: scan <available_skills> <description> entries.",
    `- If exactly one skill clearly applies: read its SKILL.md at <location> with \`${params.readToolName}\`, then follow it.`,
    "- If multiple could apply: choose the most specific one, then read/follow it.",
    "- If none clearly apply: do not read any SKILL.md.",
    "Constraints: never read more than one skill up front; only read after selecting.",
    "- When a skill drives external API writes, assume rate limits: prefer fewer larger writes, avoid tight one-item loops, serialize bursts when possible, and respect 429/Retry-After.",
    trimmed,
    "",
  ];
}

function buildMemorySection(params: {
  isMinimal: boolean;
  availableTools: Set<string>;
  citationsMode?: MemoryCitationsMode;
}) {
  if (params.isMinimal) {
    return [];
  }
  if (!params.availableTools.has("memory_search") && !params.availableTools.has("memory_get")) {
    return [];
  }
  const lines = [
    "## Memory Recall",
    "Before answering anything about prior work, decisions, dates, people, preferences, or todos: run memory_search on MEMORY.md + memory/*.md; then use memory_get to pull only the needed lines. If low confidence after search, say you checked.",
  ];
  if (params.citationsMode === "off") {
    lines.push(
      "Citations are disabled: do not mention file paths or line numbers in replies unless the user explicitly asks.",
    );
  } else {
    lines.push(
      "Citations: include Source: <path#line> when it helps the user verify memory snippets.",
    );
  }
  lines.push("");
  return lines;
}

工具描述:
 label: "Memory Search",
    name: "memory_search",
    description:
      "Mandatory recall step: semantically search MEMORY.md + memory/*.md (and optional session transcripts) before answering questions about prior work, decisions, dates, people, preferences, or todos; returns top snippets with path + lines. If response has disabled=true, memory retrieval is unavailable and should be surfaced to the user.",
    parameters: MemorySearchSchema,

有三点:

  1. 触发条件写得具体:prior work / decisions / dates / people / preferences / todos。模型不需要猜「算不算记忆相关」。
  2. 两阶段召回:先 memory_search 找片段,再 memory_get 精读少量行,控制注入体积。
  3. 引用策略可控citationsMode 可以关掉,避免模型动不动把路径行号甩出来(对产品形态很重要)。

两阶段召回比「一次性把相关文件 wholefile 塞进去」靠谱太多。

7. 召回结果怎么进上下文

toolResult 消息是关键通道

很多人以为「记忆」是把内容写进 system prompt。OpenClaw 不是这么干的。

召回结果会以工具执行结果的形式进入会话消息列表,后续模型调用自然「看得到」。

  • 工具返回会被包装成 JSON 文本块(参考内容定位 jsonResult()src/agents/tools/common.ts:230-239)。
  • tool 返回会被标准化成 content[] + details(参考内容定位 src/agents/pi-tool-definition-adapter.ts 的 normalize)。
  • 这些 toolResult 会被追加到 session messages,下一次模型调用会携带。

这条通道对排障非常友好:我们可以在 transcript 里看到「这次回答之前它到底召回了什么」。而且 toolResult 天然可控预算、可控格式,比让模型把记忆揉进自由文本稳得多。

8. 写入时机

8.1 会话快照写入

人为触发的「切会话」

当执行 /new/reset 时,上一段会话尾部会被抽取成 YYYY-MM-DD-<slug>.md。这是「防断片」写入,价值是保住最近上下文。

坑:

  • 抽取的 N 条对话里可能包含敏感信息。它会落在 memory/,并进入索引。
  • 如果你把 workspace 目录同步到团队共享盘或提交到 repo,泄露面会扩大。

我们的做法:

  • 明确区分「个人 workspace」和「团队 workspace」。个人的 memory/ 默认不进 repo。
  • 开启 citations 时,产品侧要想清楚是否允许暴露路径与行号。

8.2 短期记忆写入

「需要我们调教,告诉她哪些重要」。

短期记忆要走「稀疏高密度」路线:条目少,但每条都能在未来的某个问题上直接复用。写入策略要围绕「将来会搜什么」来定,不要围绕「当下发生了什么」来记流水账。

8.3 长期记忆更新

心跳 / AGENTS.md / cron

三种更新机制:心跳、核心流程、cron。

读最近几天短期记忆 → 选值得长期记住的 → 提炼写入 MEMORY.md

观点:

  • cron 最稳,可观测、可控、可回滚。
  • 心跳更新很容易在负载高时抖动,或者在你最不想更新的时候更新。
  • 把它塞进核心流程(AGENTS.md)要谨慎,一旦每次任务都触发提炼,会把延迟拉上去。

一般做法:

  • 工作日每天固定一次 consolidation(cron)。
  • 遇到重大决策或事故复盘,当天手动提炼写入 MEMORY.md,不等自动化。

9. 怎么「调教」

把记忆当成协议,不当成日记

「告诉她哪些重要」。把「重要」拆成几类,每类有明确写入规则,避免模型自由发挥。

一般的规则:

  1. 稳定偏好:例如输出格式偏好、技术栈偏好、代码风格偏好。
  2. 组织事实:团队结构、系统边界、核心服务依赖、环境约束。
  3. 关键决策:ADR 级别的决策,包含时间点与理由。
  4. 长期目标与在途事项:能跨周追踪的,不写「今天要做的」。
  5. 事故教训:明确到「哪个坑踩过」「如何避免」。

短期记忆(memory/YYYY-MM-DD.md)会允许更多过程性信息,但要满足一个条件:未来能被搜索问题命中。比如你写「今天讨论了 A」,基本没用;你写「决定 A 的原因是 B,后续若出现 C 用 D 回滚」,会有用一些。

如果希望 memory_search 在关键时刻召回到正确内容,就得用「未来的查询语句」来写记忆。

以上。

多 Agent 架构上下文传递的 4 种策略

在多 Agent 系统里,上下文怎么传,决定了系统的稳定性上限、成本下限、以及排障时的血压。

最开始,很多团队把它当成「把聊天记录拼一拼」的问题。上线后就会这样的问题:

  • 同样的输入,输出飘得离谱。这里的问题是上下文污染和信息密度不稳定。
  • Token 成本控不住。明明一个任务只需要 10 句关键信息,每次都喂 200 句。
  • 自动化评测很难做。回归集跑出来波动大到没法设阈值,最后只能靠人工验收。

从工程的角度简单定义一下「上下文传递」:在一条多步协作链路里,把下游完成任务所需的信息,以可控成本、可追踪方式送到它面前。这里面有两个关键词:可控可追踪

下面聊下我理解的 4 种主流策略拆解:原理、适用场景、落地细节、坑、性能和效果的取舍。

先定义一下上下文

很多争论的根源是大家说的「上下文」不是一个东西。可以在团队里先把上下文分层,后面所有策略都能对齐。这里我们把「上下文」拆成 4 类:

  1. 任务上下文:当前这一步要干什么,验收标准是什么。
  2. 状态上下文:链路运行到哪了,已经产生了哪些中间产物。
  3. 记忆上下文:用户偏好、历史约束、长期设定,和当前任务不完全同一层级。
  4. 证据上下文:引用了哪些原始材料(文档片段、对话原句、文件、数据库记录),用于追溯和评测。

策略 1:共享状态或黑板模式

这套是 LangGraph、CrewAI 这类框架最常见的默认选项。工程上它像一个「全局状态对象」,也像一个「写满便签的白板」。

机制

  • 所有 Agent 对同一个 State 读写。
  • Agent A 产出结果写入 State 的某个字段。
  • Agent B 读取字段,继续处理,再写回。

如果我们不想把大对象放内存,也可以用文件系统或对象存储做「外置状态」,State 里只放路径和元信息。像 Manus 文章中说的「将文件系统作为存储,直接共享文件系统的路径,渐进式披露」,就是这么个逻辑:把大内容放外面,把指针放 State 里

什么时候用

  • 复杂图流转:有回路、有分支、有重试、有人工介入的链路。
  • 任务跨度长:要做断点续跑、要保留每一步证据,方便回放和审计。
  • 需要可视化排障:一个状态树摆在那,定位问题快很多。

这类场景里,黑板是省心的。我们不用操心「A 怎么把消息发给 B」,大家都围着同一块板子写字。

落地时的坑

坑 1:状态对象会长成「垃圾堆」

共享状态天然会诱导人偷懒:什么都往里塞。结果一周后 State 变成一个混杂体:

  • 当前任务指令
  • 全量聊天记录
  • RAG 检索结果
  • 中间产物全文
  • 模型输出草稿

后果是下游 Agent 读到的信息密度越来越低,注意力越来越散。我们会直观感受到「同一个 Agent,越跑越不稳定」。

共享状态可以用,前提是要给 State 立规矩。规矩不是写在 Confluence 上那种,是写进代码和评测里那种。

常用的做法是给 State 分区,至少三块:

  • 「control」:流程控制字段(步数、路由、重试次数)
  • 「artifacts」:产物指针(文件路径、对象存储 key、哈希)
  • 「capsules」:给 LLM 的上下文胶囊(后面会讲)

State 里尽量少放大段文本,放「引用」和「摘要」。

坑 2:并发写

多 Agent 并行时很容易出现:

  • 两个 Agent 同时更新同一字段,后写覆盖前写。
  • 一个 Agent 基于旧 State 做决策,写回时把别人新写入的字段抹掉。

解决思路按分布式系统处理:

  • 字段级别乐观锁(版本号 / compare-and-swap)
  • append-only 日志字段,避免覆盖(把「更新」变成「追加事件」),Gemini Cli 就是这个逻辑
  • 把「写入」限定为少数字段,其他字段只读

坑 3:评测没法做「输入对齐」

共享状态经常带来一个隐性问题:每次运行 State 的非关键字段变化很大,导致没法保证下游 Agent 的输入一致。回归测试时同一条用例,今天多了两段日志,明天多了一个草稿,指标就会飘。

建议:评测时固定「胶囊输入」,State 可以变,但进入 LLM 的那段上下文要可快照、可对比、可复现。

成本 vs 效果的取舍

  • 效果:流程可扩展,复杂图最好用。
  • 成本:需要治理 State 的 schema、并发、版本、清理策略。
  • 性能:状态越大,序列化 / 反序列化越痛;如果每一步都把 State 发给 LLM,更是直接烧钱。

共享状态是一把大锤。能砸钉子,也能把玻璃砸碎。关键看有没有「状态卫生」这件事。

策略 2:消息传递与直接调用

这套有点像微服务架构:上游把消息打包发给下游,下游处理完再回一个结果。

机制

  • Agent A 产出一个「消息」发给 Agent B。
  • 消息可以走 HTTP、RPC、队列,也可以是框架内的函数调用。
  • 每条消息都应该有明确的结构和版本。

什么时候用

  • 流水线式任务:每一步都很明确,上游输出就是下游输入。
  • 要强可观测性:链路追踪、审计、回放都好做。
  • 团队边界清晰:不同组负责不同 Agent,接口契约能拉齐。

这类场景用消息传递灵活性更强一些,但是如果规模不大,直接单体应用来搞,函数间调用吧。

落地时的坑

坑 1:消息里塞进「全量上下文」

很多团队为了省事,会把上游拿到的所有东西都塞进消息里。看起来省了裁剪逻辑,实际上把问题推给了下游:下游 LLM 要在一堆噪声里找信号。

如果走消息传递,消息必须有「字段语义」。比如:

  • 「task」字段是当前要做的事
  • 「constraints」字段是硬性限制
  • 「evidence」字段是引用(原文片段或路径)
  • 「history」字段如果存在,必须明确是「最近 N 轮且强相关」

这里的关键词是「必须明确」。否则会出现消息看着结构化,内容依然是散装的。

坑 2:接口版本失控

多 Agent 系统迭代快,接口字段会频繁变动。如果经历过一次「某个 Agent 升级后,下游全挂」就会理解版本的重要性。

建议至少做到:

  • 每条消息带「schema_version」
  • 下游支持 1~2 个旧版本的兼容解析
  • 重要字段改动要有灰度期,别全量切

Agent 世界里「prompt 和策略」变化太快,不做版本控制就是赌博。

坑 3:把「LLM 输出」当成接口返回

LLM 输出天然存在幻觉。如果我们直接把自由文本当成 RPC 返回,然后让下游再去解析,事故率会非常高。

有一个简单的方法:固定栏位的轻量输出格式,别一上来就上复杂 schema,也别放任自由发挥。它在工程上有一个很大的价值:解析稳定,回归测试有抓手。

类似于这样:

PROMPT:
...

NEGATIVE:
...

PARAMS:
- aspect: 16:9
- notes: ...

成本 vs 效果的取舍

  • 效果:可追踪性强,调试体验好。
  • 成本:要做接口契约、版本管理、兼容逻辑。
  • 性能:网络开销和序列化开销可控;真正的成本往往来自传了多少无用字段。

如果业务链路更像「微服务编排」,消息传递会比共享状态更干净。

策略 3:上下文压缩与自然语言传递

核心思路:下游 Agent 不该负责考古

把「长历史」变成「短胶囊」,把「噪声」变成「任务卡」,再交给执行 Agent。

机制

上游做三件事:

  1. 从历史里抽取和当前任务强相关的信息
  2. 把冲突的约束做决策或提出澄清问题
  3. 输出一个高密度、可控的自然语言指令

「上下文胶囊(context capsule)」

把给下游 Agent 的输入,固定成一个胶囊,结构大概是:

  • 必须给:任务卡(Planner 压缩/改写后的自然语言描述)
  • 可选给(按需):

    • 最近 N 轮「与任务强相关」的对话原句(最多 3–8 句)
    • 一段「用户偏好/风格记忆」摘要(1–3 句)
  • 坚决不直接给:全量聊天记录(除非做的就是风格延续式创作,而且做了脱敏)

它解决的是「你能不能控制它理解什么」。

示例:

任务卡:生成一张用于电商 banner 的图。主体是一只穿宇航服的柯基站在月球上,远处能看到地球。风格写实摄影,冷色调,高对比,电影感侧逆光。横向 16:9。不要任何文字、logo、血腥或恐怖元素。用户偏好极简、冷色、不要文字。若信息缺失请提 1–3 个澄清问题,否则直接输出可用于生图的 prompt 与 negative prompt。

这段话有几个关键点:

  • 主体、场景、风格、画幅、禁忌、用户偏好都在
  • 有「缺失信息时的行为规则」
  • 不需要表单,依然可评测、可回归

工具描述要写成「契约」

工具描述很重要。

我更喜欢把工具说明写成「契约」,至少包含:

  • 工具支持的参数(prompt / negative / size / seed / style 等)
  • 哪些信息必须出现(画幅、用途、限制)
  • 输出必须遵守的格式(哪怕只有 PROMPT/NEGATIVE/PARAMS)

可能翻车的地方

以一个增强提示词的 Agent 为例

翻车 1:指代延续没被写进任务卡

用户说「按刚才那张风格」「把她换成红裙子」,Planner 如果没有把「刚才那张」总结成可引用的描述,下游增强 Agent 根本无从得知。

补救方式使用「证据上下文」:把关键原句作为 1~3 条引用附在胶囊里。

翻车 2:约束冲突没被处理

用户一会儿要极简纯色,一会儿又要复杂赛博城市场景。Agent 去解决冲突会很糟糕,因为它的职责是「增强表达」,不是「做产品决策」。

冲突要在 Planner 层解决:要么做裁决(按最新指令为准、按用户偏好为准),要么问澄清问题。别把锅甩给执行 Agent。

翻车 3:压缩带来的信息损失

上下文压缩是有损的。 压缩做得越狠,成本越低,翻车概率越高;压缩做得越松,成本越高,稳定性也未必更好,因为噪声会上来。

建议做一个「胶囊长度预算」,按任务类型分档:

  • 低风险任务(格式化、简单问答):胶囊可以短到 200~400 tokens
  • 中风险任务(生成、改写、推理):600~1200 tokens
  • 高风险任务(工具调用、多约束、多回合创作):1200~2000 tokens,再往上就该考虑别的策略了

这样,可以让成本和稳定性可控一些。

成本 vs 效果的取舍

  • 效果:稳定性提升非常明显,Token 成本能压下来,评测也更容易做。
  • 成本:要多一个 Planner/Summarizer 步骤,链路延迟会上升;压缩质量要靠回归集打磨。
  • 工程判断:这套是「多 Agent 真正开始像工程系统」的起点。

策略 4:路由分发与层级管理

如果说策略 3 解决的是「给下游喂什么」,策略 4 解决的是「谁有资格看到什么」。

我喜欢用「最小信息原则」去设计多 Agent:每个 Agent 只拿自己需要的那一部分上下文,别让它看到不该看的东西。

机制

  • 一个 Supervisor(主管)拿到全量上下文。
  • Supervisor 拆任务、选 Agent、裁剪上下文。
  • 子 Agent 只看到被裁剪后的输入,产出结果回传 Supervisor。
  • Supervisor 汇总,决定下一步。

把「路由」和「信息裁剪」集中起来做,能显著减少上下游互相污染。

这本质就是一个主从的逻辑。

什么时候用

  • 权限和合规敏感:有 PII、有商业机密、有分级数据。
  • 子 Agent 职责清晰:比如「检索」「评审」「生成」「合规检查」。
  • 系统要长期维护:人员流动、策略变动、模型替换都很频繁。

层级路由把复杂性收敛到 Supervisor 这一点上。依赖它,也更容易把它做好。

落地时的坑

坑 1:Supervisor 变成性能瓶颈

所有东西都过 Supervisor,它会成为热点:吞吐、延迟、可用性全压在它身上。

解决办法通常有三种:

  • Supervisor 只做「路由与裁剪」,不要在它身上做重推理
  • 对路由做缓存(同一类任务走同一条路径)
  • Supervisor 逻辑尽量确定性,LLM 参与度降低

我见过很多团队把「大脑」写成一个超级 prompt,然后让它既拆任务又生成内容又做审查。这样迟早会蹦。

坑 2:裁剪策略一开始过度依赖「拍脑袋」

裁剪不是凭感觉。裁剪是一套数据工程问题:哪些字段必须给,哪些字段给了会干扰。

用「失败用例驱动」去迭代裁剪:每次线上翻车,都回放当时给子 Agent 的胶囊,问一个很残酷的问题:

  • 该给的没给,是哪一类信息缺失?
  • 不该给的给了,是哪一类噪声触发了跑偏?

把这两类问题沉淀成裁剪规则,会越做越稳。

坑 3:子 Agent 之间产生「隐性耦合」

很多系统表面上是层级的,实际上子 Agent 会通过共享外部资源互相影响,比如:

  • 共用同一个向量库检索空间,检索结果被不同策略污染
  • 共用同一个临时文件目录,路径命名冲突
  • 共用同一个「用户偏好记忆」,写入时缺少版本控制

如果走 Supervisor 模式,「写入边界」会比较重要:哪些 Agent 允许写记忆,哪些只能读;写入要不要审批;写入是否带证据引用。

成本 vs 效果的取舍

  • 效果:稳定性和安全性非常强,复杂系统更容易控住。
  • 成本:Supervisor 设计难度高,容易成为瓶颈;需要更完善的可观测性和回放能力。
  • 工程判断:当开始被「数据泄露」「上下文污染」「责任边界不清」折磨时,Supervisor 往往是解药。

选择策略时,如何判断

可以用四个可执行的问题来选方案:

  1. 谁需要看到全量上下文?谁只需要胶囊?
    如果答案是「大多数都只需要胶囊」,策略 3 和 4 优先级会上来。

  2. 要不要并发?要不要异步?
    需要并发、异步,策略 5 的价值会非常直接。

  3. 失败主要来自哪里:信息缺失,还是噪声过多?
    信息缺失优先补证据引用;噪声过多优先做裁剪和胶囊预算。

  4. 是否真的在做回归评测?
    没有回归,就别指望系统会「越调越稳」。上下文传递策略的好坏,最终都要落在可复现输入上。

小结

不做结构化,并不等于不做「约束与契约」

我见过的高质量落地项目,往往走的是「看起来很自然,实际上约束很硬」的路线。用户体验上像聊天,工程实现上像协议。

如果准备做多 Agent 的上下文传递,至少把三件事落下来:

  • 「上下文胶囊」:任务卡 + 少量强相关原句 + 记忆摘要
  • 「工具契约」:写清楚工具能力边界和必填信息
  • 「受控输出格式」:固定栏位,解析稳定,评测可做

这三件事做完,再谈共享状态、Supervisor、消息传递,才有意义。

以上。