<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>潘锦的空间 &#187; OpenClaw</title>
	<atom:link href="https://www.phppan.com/tag/openclaw/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.phppan.com</link>
	<description>SaaS SaaS架构 团队管理 技术管理 技术架构 PHP 内核 扩展 项目管理</description>
	<lastBuildDate>Sat, 25 Apr 2026 00:56:17 +0000</lastBuildDate>
	<language>zh-CN</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=3.9.40</generator>
	<item>
		<title>OpenClaw 的 Skills 的实现和 Claude Code 不一样</title>
		<link>https://www.phppan.com/2026/03/openclaw-skilss-claude-code/</link>
		<comments>https://www.phppan.com/2026/03/openclaw-skilss-claude-code/#comments</comments>
		<pubDate>Sat, 21 Mar 2026 01:01:17 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[Claude Code]]></category>
		<category><![CDATA[OpenClaw]]></category>
		<category><![CDATA[skills]]></category>

		<guid isPermaLink="false">https://www.phppan.com/?p=2479</guid>
		<description><![CDATA[OpenClaw 的 Skills，本质上不是一个「可调用工具」，它更像一套经过约束的运行手册：启动时把技能目 [&#8230;]]]></description>
				<content:encoded><![CDATA[<section id="nice" data-tool="mdnice编辑器" data-website="https://www.mdnice.com">
<p data-tool="mdnice编辑器">OpenClaw 的 Skills，本质上不是一个「可调用工具」，它更像一套经过约束的运行手册：启动时把技能目录扫描出来，压成一份 <code>&lt;available_skills&gt;</code> 清单塞进 system prompt，模型自己判断要不要选一个 skill，然后再通过 Read 工具去读这个 skill 的 <code>SKILL.md</code>。读完以后，没有任何独立执行器接管，还是在当前这条 session 的 tool-loop 里继续跑。</p>
<p data-tool="mdnice编辑器">Claude Code 走的是另一条路。它把 skill 做成了 tool，工具里负责校验、加载、执行，甚至可以放进一个新上下文里跑完，再把结果回传主对话。</p>
<p data-tool="mdnice编辑器">这两个实现方向，表面上都叫 skills，工程含义完全不一样。</p>
<p data-tool="mdnice编辑器">如果你是做 Agent 平台、企业内 Copilot、代码助手、任务执行器，这个差异不只是架构图上的审美问题。它会直接影响：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>prompt 预算怎么花</section>
</li>
<li>
<section>权限边界放在哪里</section>
</li>
<li>
<section>skill 的治理成本有多高</section>
</li>
<li>
<section>运行链路是可控还是失控</section>
</li>
<li>
<section>你后面想不想做隔离执行、审计、回放、灰度发布</section>
</li>
</ul>
<h1 data-tool="mdnice编辑器"><span class="content">1. OpenClaw 的 Skills，到底是怎么被「召回」的</span></h1>
<p data-tool="mdnice编辑器">很多人一看到 skills，第一反应是：是不是和 memory 一样，先走 embedding 检索，再把相关技能召回进上下文。</p>
<p data-tool="mdnice编辑器">不是。</p>
<p data-tool="mdnice编辑器">OpenClaw 的 skills 召回机制非常直接，甚至可以说有点「朴素」：<strong>扫描目录，生成目录清单，注入提示词，让模型自己选</strong>。整条链路分三段：</p>
<ol data-tool="mdnice编辑器">
<li>
<section>发现</section>
</li>
<li>
<section>注入</section>
</li>
<li>
<section>读取</section>
</li>
</ol>
<p data-tool="mdnice编辑器">OpenClaw 反过来做了一件更工程化的事：先把 skill 列表显式给模型，再用 system prompt 约束它怎么选。</p>
<p data-tool="mdnice编辑器">这个机制的起点在 <code>src/agents/skills/workspace.ts</code> 的 <code>loadSkillEntries()</code>。</p>
<h2 data-tool="mdnice编辑器"><span class="content">1.1 技能发现：先扫目录，再谈调用</span></h2>
<p data-tool="mdnice编辑器">OpenClaw 会从多个 root 目录扫描 <code>SKILL.md</code>，然后合并成最终技能集。这里是有明确的覆盖优先级的：</p>
<p data-tool="mdnice编辑器"><code>extra &lt; bundled &lt; managed &lt; agents-personal &lt; agents-project &lt; workspace</code></p>
<p data-tool="mdnice编辑器">它的逻辑是：越靠近当前工作空间、越贴近用户项目的 skill，优先级越高。平台预置的 bundled skill 可以兜底，但项目级 skill 要能覆盖它。否则你做企业落地时会很难受，团队定制流程永远被平台内置逻辑压着打。</p>
<p data-tool="mdnice编辑器">从目录结构看，OpenClaw 支持两种 skill 形态：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>root 本身就是一个 skill，目录下直接有 <code>SKILL.md</code></section>
</li>
<li>
<section>root 下的子目录分别是 skill，每个子目录里有自己的 <code>SKILL.md</code></section>
</li>
</ul>
<p data-tool="mdnice编辑器">这样，团队在实际维护 skill 时，有两种典型组织方式：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>单一 skill 仓库，根目录就是技能内容</section>
</li>
<li>
<section>skills 集合仓库，每个子目录一个技能</section>
</li>
</ul>
<p data-tool="mdnice编辑器">都支持，落地阻力会小很多。</p>
<p data-tool="mdnice编辑器">它对 <code>SKILL.md</code> 做了体积限制，默认超过 256KB 就直接跳过。</p>
<p data-tool="mdnice编辑器"><code>SKILL.md</code> 本来就应该是高密度操作指南，不应该演变成一个什么都往里塞的大文档。你一旦允许 skill 文件无限变大，后面一定会有人把 SOP、FAQ、设计文档、事故复盘全扔进去，最后技能选择和加载成本一起爆炸。OpenClaw 在扫描阶段直接卡体积，其实是在替平台维护纪律。</p>
<h2 data-tool="mdnice编辑器"><span class="content">1.2 frontmatter 解析</span></h2>
<p data-tool="mdnice编辑器">给 skill 增加最小限度的结构化元信息</p>
<p data-tool="mdnice编辑器">扫描到 <code>SKILL.md</code> 后，OpenClaw 会读原文并解析 frontmatter。坏掉或者缺失的 frontmatter 会被忽略。</p>
<p data-tool="mdnice编辑器">这个决策也很对。</p>
<p data-tool="mdnice编辑器">OpenClaw 这里更像是「有就用，没有拉倒」。它承认 markdown 本体才是 skill 的核心，frontmatter 只是辅助控制面。这个姿态很适合 skills 这种增长很快、来源很多的资产类型。</p>
<p data-tool="mdnice编辑器">但这里也埋了一个工程 trade-off：frontmatter 被弱约束，意味着后续治理和平台能力扩展会受限。你今天只做 discovery 和 basic gating，这么玩没问题；你明天如果要做 skill 分类、依赖分析、版本兼容、批量审计，元信息松散会让成本陡增。</p>
<p data-tool="mdnice编辑器">OpenClaw 当前站在了「先把系统跑起来」的一边，没有走「先把规范做重」的路子。</p>
<p data-tool="mdnice编辑器">这意味着它更适合中小规模 skill 生态，或者说更适合个人助手这类工具，而不是一个强治理的企业级 skill marketplace。</p>
<h1 data-tool="mdnice编辑器"><span class="content">2. 不是所有 skill 都会进 <code>&lt;available_skills&gt;</code></span></h1>
<p data-tool="mdnice编辑器">扫描出来只是候选集，不等于模型能看到。</p>
<p data-tool="mdnice编辑器">OpenClaw 在注入 system prompt 之前会做一轮 gating。这个步骤比很多人想象得重要，因为它决定了模型到底暴露给了哪些能力。</p>
<p data-tool="mdnice编辑器">过滤逻辑里有几类条件。</p>
<h2 data-tool="mdnice编辑器"><span class="content">2.1 配置开关</span></h2>
<p data-tool="mdnice编辑器">最基础的 enable/disable</p>
<p data-tool="mdnice编辑器">如果某个 skill 在配置里被标成 <code>skills.entries.&lt;skillKey&gt;.enabled === false</code>，它就会被剔除。</p>
<p data-tool="mdnice编辑器">这是最基础的 kill switch。工程上没什么可争议的，必须有。不然你没法快速止血。某个 skill 写坏了、依赖环境挂了、被发现会引导模型做危险操作，没有一键下线能力，平台就不配上线。</p>
<h2 data-tool="mdnice编辑器"><span class="content">2.2 bundled allowlist</span></h2>
<p data-tool="mdnice编辑器">对内置技能单独管控</p>
<p data-tool="mdnice编辑器"><code>skills.allowBundled</code> 只约束 bundled 来源。</p>
<p data-tool="mdnice编辑器">这个细节很有意思。它说明 OpenClaw 把 bundled skills 当成一类特殊资产处理：平台自带，但不默认无条件信任。</p>
<p data-tool="mdnice编辑器">为什么这事重要？因为内置 skill 经常是平台演进中最容易「偷偷变多」的那部分。你今天打包 5 个，明天为了演示方便塞到 20 个，后天 prompt 里一大坨 descriptions，模型选 skill 的噪声越来越大。allowlist 的存在，本质上是在给平台预装能力上保险栓。</p>
<h2 data-tool="mdnice编辑器"><span class="content">2.3 eligibility 判断</span></h2>
<p data-tool="mdnice编辑器">按运行环境判断 skill 是否可用</p>
<p data-tool="mdnice编辑器">OpenClaw 会根据 <code>metadata.requires</code> 去判断 skill 能不能用，条件包括：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>二进制依赖是否存在</section>
</li>
<li>
<section>环境变量是否满足</section>
</li>
<li>
<section>配置是否具备</section>
</li>
<li>
<section>操作系统是否匹配</section>
</li>
<li>
<section>remote 平台是否匹配</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这个设计非常工程化。因为 skill 如果涉及真实操作，它一定和环境耦合。比如某个 skill 依赖特定 cli，或者要求某个 API token，或者只能在 Linux 跑。你如果不在注入前做 eligibility，而是让模型先看到 skill，再在执行时失败，用户体验会很差，模型行为也会变形：它会看到一个貌似可用的方案，实际一调就炸。</p>
<p data-tool="mdnice编辑器">OpenClaw 选择「<strong>先过滤，再暴露</strong>」，这是我非常认同的策略。因为对模型来说，看得见就等于潜在可用。你让它看见无效 skill，本质上是在制造认知噪声。</p>
<h2 data-tool="mdnice编辑器"><span class="content">2.4 <code>disable-model-invocation</code></span></h2>
<p data-tool="mdnice编辑器">系统内部保留，模型不可见</p>
<p data-tool="mdnice编辑器">如果某个 skill 标记了 <code>disable-model-invocation</code>，它不会进入 prompt 的 <code>&lt;available_skills&gt;</code>，但仍然存在于系统内部，可用于管理或校验。</p>
<p data-tool="mdnice编辑器">因为技能资产不只有「给模型用」这一种角色。你可能有些 skill 需要：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>用于测试</section>
</li>
<li>
<section>用于审核</section>
</li>
<li>
<section>用于运维校验</section>
</li>
<li>
<section>用于内部 pipeline</section>
</li>
<li>
<section>用于未来发布但暂不开放</section>
</li>
</ul>
<p data-tool="mdnice编辑器">OpenClaw 没把「存在」和「可被模型调用」绑死，这样 skill registry 才像个平台能力，而不是一个 prompt 拼装器。</p>
<h1 data-tool="mdnice编辑器"><span class="content">3. <code>&lt;available_skills&gt;</code> 才是 OpenClaw 的真正召回入口</span></h1>
<p data-tool="mdnice编辑器">很多人说 skills 被召回，其实这句话在 OpenClaw 语境里容易让人误解。</p>
<p data-tool="mdnice编辑器">真正被放进上下文里的第一层，不是 <code>SKILL.md</code> 内容，而是 <code>&lt;available_skills&gt;</code> 这个目录清单。它由 <code>buildWorkspaceSkillsPrompt()</code> 生成，注入到 system prompt。</p>
<p data-tool="mdnice编辑器">也就是说，模型最先看到的是技能目录，不是技能正文。</p>
<p data-tool="mdnice编辑器">这个做法也算是渐进式披露的一种实现。先给模型足够决定是否要读 skill 的最小信息，避免一开始就把所有技能全文灌进去。</p>
<h2 data-tool="mdnice编辑器"><span class="content">3.1 system prompt 里的强规则，决定了技能选择流程</span></h2>
<p data-tool="mdnice编辑器">OpenClaw 在 system prompt 里有一段明确的 Skills mandatory 规则，大意是：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>先扫描 <code>&lt;available_skills&gt;</code> 里的 <code>&lt;description&gt;</code></section>
</li>
<li>
<section>只有明确匹配一个 skill 时，才去读对应的 <code>SKILL.md</code></section>
</li>
<li>
<section>最多只读一个 skill</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这三条里，最重要的是后两条。</p>
<h3 data-tool="mdnice编辑器"><span class="content">3.1.1 只有明确匹配一个才读</span></h3>
<p data-tool="mdnice编辑器">这是在压制模型的泛化冲动。模型很喜欢「我觉得这个也相关，那个也相关」，然后多读几个，最后把自己卷进上下文泥潭。OpenClaw 用提示词硬性要求「明确匹配一个」才允许进入下一步，这本质上是在给模型加稀疏化约束。</p>
<p data-tool="mdnice编辑器">效果未必 100% 可控，但方向是对的。</p>
<h3 data-tool="mdnice编辑器"><span class="content">3.1.2 最多只读一个 skill</span></h3>
<p data-tool="mdnice编辑器">如果你做过基于 prompt 的 tool orchestration，就会知道一个常见死法：模型在一堆说明里来回跳，读完 A 觉得 B 也有用，再读 B，顺手 C 也看看，最后 token 花了不少，任务还没干。</p>
<p data-tool="mdnice编辑器">OpenClaw 这里直接规定 upfront 只能读一个 skill。我个人判断，这不是因为理论上最优，而是因为工程上最稳。</p>
<p data-tool="mdnice编辑器">它牺牲了一部分组合式技能能力，换来了几个非常实际的好处：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>token 消耗更可控</section>
</li>
<li>
<section>模型决策路径更短</section>
</li>
<li>
<section>skill 选择失败更容易定位</section>
</li>
<li>
<section>调试和回放更容易做</section>
</li>
</ul>
<p data-tool="mdnice编辑器">代价也很明确：如果任务天然需要多个 skill 协同，OpenClaw 当前机制会显得笨。模型要么自己在单个 skill 指导下绕着做，要么根本选不准。</p>
<p data-tool="mdnice编辑器">这也是它和 Claude Code 一个很大的分歧。Claude Code 把 skill 视作 tool，更容易天然支持复杂编排；OpenClaw 把 skill 视作可读说明，天然倾向于单次聚焦。</p>
<h2 data-tool="mdnice编辑器"><span class="content">3.2 技能过多时的降级策略：compact 和 truncated</span></h2>
<p data-tool="mdnice编辑器"><code>&lt;available_skills&gt;</code> 不是无上限注入的。技能太多时，OpenClaw 会降级成 compact 格式，甚至截断，并提示 <code>skills truncated</code>。</p>
<p data-tool="mdnice编辑器">这个点看起来像 prompt 小技巧，其实是很重要的预算治理。</p>
<p data-tool="mdnice编辑器">因为技能系统一旦跑起来，数量几乎只会越来越多。最初十几个 skill，description 全量塞进去还行；到几十上百个 skill，再这么搞，system prompt 很快会变成垃圾堆。OpenClaw 至少意识到了这个问题，所以做了两级退化：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>compact：保留更少信息</section>
</li>
<li>
<section>truncated：截断并明确提示</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这说明它把 prompt 当成有限资源，而不是无限容器。</p>
<p data-tool="mdnice编辑器">但实话实说，这个策略只是在「延缓爆炸」，不是根治。skill 数量继续增长以后，靠 compact/truncate 顶不住。因为问题不只是 token 多了，而是模型在目录里做决策的辨识难度会越来越高。description 再压缩，skill 之间的区分度也会变差。</p>
<p data-tool="mdnice编辑器">所以我对这块的判断是：<strong>OpenClaw 的 catalog 注入机制适合中等规模 skill 集，不适合无限扩张。</strong><br />
如果你团队未来真要做上百个 skill 的企业级平台，迟早要引入更分层的 catalog、分类路由、或者显式 selector，而不是靠一坨 <code>&lt;available_skills&gt;</code> 让模型裸选。</p>
<h1 data-tool="mdnice编辑器"><span class="content">4. 真正的 <code>SKILL.md</code> 是怎么进入上下文的</span></h1>
<p data-tool="mdnice编辑器">OpenClaw 的第二层加载是按需读取。模型先看到目录清单，选中 skill 以后，才通过 Read 工具去读对应路径的 <code>SKILL.md</code>。</p>
<h2 data-tool="mdnice编辑器"><span class="content">4.1 技能正文不是 system prompt 的一部分</span></h2>
<p data-tool="mdnice编辑器">system prompt 里只有规则和技能目录。真正的 <code>SKILL.md</code> 内容，是在模型发起一次 read tool call 后，作为 toolResult 追加进当前消息流。</p>
<p data-tool="mdnice编辑器">也就是说，skill 正文进入上下文的方式，和你用工具读一个普通文件没有本质区别。差别只是 system prompt 先规定了什么时候允许这样读。</p>
<p data-tool="mdnice编辑器">这个设计的好处非常明确：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>大幅降低初始上下文体积</section>
</li>
<li>
<section>让技能正文成为按需成本，而不是固定成本</section>
</li>
<li>
<section>skill 更新后无需改 system prompt 模板，只影响运行时读到的内容</section>
</li>
<li>
<section>便于把 skill 当文件资产管理，而不是 prompt 模板片段</section>
</li>
</ul>
<p data-tool="mdnice编辑器">坏处也很明确：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>skill 的执行效果更依赖模型有没有正确触发 read</section>
</li>
<li>
<section>skill 的权威性被弱化成一条 toolResult，而不是顶层指令</section>
</li>
<li>
<section>如果 toolResult 很长，后续上下文里它和普通读文件结果没什么层级差异</section>
</li>
</ul>
<p data-tool="mdnice编辑器">从实现角度看，OpenClaw 这里是很克制的。它没有发明一个 Skill Runtime，没有做一套专门的 skill 调度协议，就是借已有的 read 工具完成正文加载。</p>
<p data-tool="mdnice编辑器">这让系统保持简单，但也让 skill 更像「读到的一份说明书」，而不是「被激活的执行单元」。</p>
<h1 data-tool="mdnice编辑器"><span class="content">5. OpenClaw 的安全边界</span></h1>
<p data-tool="mdnice编辑器">既然 skill 是文件，模型要去读 <code>SKILL.md</code>，安全问题马上就来了：路径能不能逃逸？symbolic link 能不能把 skill 指到 root 外？模型能不能顺着 location 读到不该读的内容？</p>
<p data-tool="mdnice编辑器">OpenClaw 这里做了两层保护。</p>
<h2 data-tool="mdnice编辑器"><span class="content">5.1 第一层：扫描阶段的 containment 检查</span></h2>
<p data-tool="mdnice编辑器">在技能发现阶段，它会对 realpath 做 containment 检查，防止 symlink 把 skill 指到 root 外面。</p>
<p data-tool="mdnice编辑器">这是典型的「尽早失败」策略。我一直觉得文件系统相关的安全问题，能在发现阶段拦的，不要拖到执行阶段。因为一旦把异常路径放进 skill registry，后面每个环节都得假设它可能有问题，整个系统会变得很难推理。</p>
<p data-tool="mdnice编辑器">扫描时就把越界项挡掉，registry 内部的数据至少是干净的。</p>
<h2 data-tool="mdnice编辑器"><span class="content">5.2 第二层：Read 工具的 workspace root guard</span></h2>
<p data-tool="mdnice编辑器">模型真正发起读取时，Read 工具还会做 sandbox root 校验，禁止 <code>..</code> 或绝对路径逃逸 workspace root。</p>
<p data-tool="mdnice编辑器">也就是说，即使 discovery 阶段没出问题，真正读取文件时还有一层运行时防线。</p>
<p data-tool="mdnice编辑器">文件边界这事，单点防护永远不够。扫描阶段挡的是「注册进来的 skill 本身」，读取阶段挡的是「模型实际提出的路径参数」。二者保护的对象不一样。</p>
<p data-tool="mdnice编辑器">工程上要是只做第一层，你会被运行时路径拼接坑；只做第二层，你会把很多脏数据带进 registry，影响可观测性和调试。</p>
<p data-tool="mdnice编辑器">OpenClaw 在这块虽然不算复杂，但做法是标准的。</p>
<h1 data-tool="mdnice编辑器"><span class="content">6. Skills 的渐进式披露</span></h1>
<p data-tool="mdnice编辑器">OpenClaw 的 skills 机制，如果只看「枚举目录 -&gt; 注入 prompt -&gt; read 文件」，会有人觉得太朴素。真正值得注意的是，它和 memory 一样，贯彻了一套很明确的渐进式披露思路。</p>
<p data-tool="mdnice编辑器">这是上下文预算治理的逻辑。</p>
<p data-tool="mdnice编辑器">原则就两条：</p>
<ol data-tool="mdnice编辑器">
<li>
<section>先给最小可用信息</section>
</li>
<li>
<section>确认需要后再扩大读取范围</section>
</li>
</ol>
<p data-tool="mdnice编辑器">在 OpenClaw 里，memory 和 skills 都是这么干的，只是路径不同。</p>
<h2 data-tool="mdnice编辑器"><span class="content">6.1 Memory 是先搜 snippet，再精读指定行段</span></h2>
<p data-tool="mdnice编辑器"><code>memory_search</code> 先返回短片段，带 path 和 line range，不是全文注入。底层还有 <code>SNIPPET_MAX_CHARS = 700</code> 的硬上限。如果后端预算紧，还会继续裁结果。</p>
<p data-tool="mdnice编辑器">之后再通过 <code>memory_get</code> 按 <code>path + from/lines</code> 去拉具体行段。</p>
<p data-tool="mdnice编辑器">这个流程是非常标准的 progressive disclosure：先定位，再精读。</p>
<h2 data-tool="mdnice编辑器"><span class="content">6.2 Skills 是先给目录，再只读一个 <code>SKILL.md</code></span></h2>
<p data-tool="mdnice编辑器">skills 这边没有向量召回，而是目录清单。模型先看 <code>&lt;available_skills&gt;</code>，只在明确匹配一个时才读正文，并且 upfront 最多一个。</p>
<p data-tool="mdnice编辑器">两条链路表面不同，本质一样：都在防止大块文本无脑灌进上下文。</p>
<p data-tool="mdnice编辑器">我为什么说这点值钱？因为很多团队做 Agent 系统时，最大的问题不是模型不会做事，而是上下文管理太粗糙。什么都想喂进去，最后 token 烧得飞快，模型还因为噪声太高做不准。</p>
<p data-tool="mdnice编辑器">OpenClaw 至少在架构层面承认了一件事实：<strong>上下文是预算，不是仓库。</strong></p>
<p data-tool="mdnice编辑器">这件事说起来简单，真正落实到工具语义、提示词规则、底层裁剪，很多系统做不到。</p>
<h1 data-tool="mdnice编辑器"><span class="content">7. OpenClaw 没有「独立 Skill 执行器」</span></h1>
<p data-tool="mdnice编辑器">在 OpenClaw 里，skill 读完之后，并没有一个单独的执行环境接管它。没有所谓「Skill Runtime」去解释 <code>SKILL.md</code>，也没有「新上下文执行 skill」这回事。它还是在同一个 <code>activeSession.prompt(...)</code> 的 tool-loop 里继续跑。</p>
<p data-tool="mdnice编辑器">链路大概是这样：</p>
<ol data-tool="mdnice编辑器">
<li>
<section>system prompt 里给出 <code>&lt;available_skills&gt;</code> 和选择规则</section>
</li>
<li>
<section>模型决定某个 skill 匹配当前请求</section>
</li>
<li>
<section>模型调用 read 工具读取对应 <code>SKILL.md</code></section>
</li>
<li>
<section>toolResult 里带回 <code>SKILL.md</code> 文本</section>
</li>
<li>
<section>这个 toolResult 被追加到当前 session 的 messages</section>
</li>
<li>
<section>模型再次被调用，看到刚才读到的 skill 内容</section>
</li>
<li>
<section>模型按 skill 指导，继续发起后续 toolCall，比如 <code>exec</code>、<code>write</code>、<code>edit</code></section>
</li>
<li>
<section>直到输出最终回答</section>
</li>
</ol>
<p data-tool="mdnice编辑器">关键点：<strong>skill 内容只是同一消息流里多了一条 toolResult</strong>。</p>
<p data-tool="mdnice编辑器">这意味着什么？</p>
<p data-tool="mdnice编辑器">意味着 OpenClaw 的 skill 执行，本质上是「模型读了一份流程说明，然后继续在原对话里行动」。它没有新的边界，没有新的记忆隔离，没有新的权限域。真正的隔离，来自工具列表裁剪和文件读写沙箱，不来自上下文切换。</p>
<p data-tool="mdnice编辑器">这和 Claude Code 很不一样。</p>
<h1 data-tool="mdnice编辑器"><span class="content">8. Claude Code 的 skills，更像「可调用子程序」</span></h1>
<p data-tool="mdnice编辑器">Claude Code 的 skill 流程有几个特征：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>skill 是 tools 列表里的一个明确工具</section>
</li>
<li>
<section>模型会调用 Skill tool，而不是自己去 read 某个 markdown</section>
</li>
<li>
<section>skill prompt 的加载是 tool 内部动作</section>
</li>
<li>
<section>权限校验也主要发生在 tool 内</section>
</li>
<li>
<section>skill 可以在新上下文执行，执行完再把结果带回主对话</section>
</li>
</ul>
<p data-tool="mdnice编辑器">从工程抽象上看，Claude Code 的 skill 更像一个「子程序入口」。它有名字、有调用接口、有内部装载逻辑，甚至有上下文隔离能力。</p>
<p data-tool="mdnice编辑器">OpenClaw 没有走这条路。它把 skill 设计成「模型可读的文件」，由模型自己决定是否展开，并在原上下文里继续操作。</p>
<p data-tool="mdnice编辑器">这两个方向，没有谁天然高级，但适用面不同</p>
<p data-tool="mdnice编辑器">如果你要的是：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>更强的执行隔离</section>
</li>
<li>
<section>更容易做权限封装</section>
</li>
<li>
<section>更容易做结果回传和子任务边界</section>
</li>
<li>
<section>更适合复杂、多步骤、可复用的任务单元</section>
</li>
</ul>
<p data-tool="mdnice编辑器">Claude Code 那种 tool 化 skill 更合适。</p>
<p data-tool="mdnice编辑器">如果你要的是：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>实现简单</section>
</li>
<li>
<section>skill 作者门槛低</section>
</li>
<li>
<section>把 skill 当 markdown 资产管理</section>
</li>
<li>
<section>快速把流程知识接进现有 ReAct tool-loop</section>
</li>
</ul>
<p data-tool="mdnice编辑器">OpenClaw 这种文件化 skill 更实用。</p>
<p data-tool="mdnice编辑器">工程上不看理念，看代价结构。</p>
<h1 data-tool="mdnice编辑器"><span class="content">9. OpenClaw 为什么会做成这样</span></h1>
<p data-tool="mdnice编辑器">我猜它的设计动机是：<strong>尽量复用现有 agent runtime，而不是为 skills 单独发明一层执行框架。</strong></p>
<p data-tool="mdnice编辑器">你看它的做法就知道了：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>discovery 复用文件系统扫描</section>
</li>
<li>
<section>selection 复用 system prompt</section>
</li>
<li>
<section>loading 复用 read 工具</section>
</li>
<li>
<section>execution 复用原有 tool-loop</section>
</li>
<li>
<section>safety 复用 sandbox path guard 和 tool policy</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这是一套很节制的架构思路。好处是：</p>
<p data-tool="mdnice编辑器"><strong>实现成本低</strong> ：不用造新协议，不用加新消息类型，不用多一层 runtime。对一个正在演进中的 agent 系统来说，这非常现实。 <strong>Skill 作者认知负担小</strong>：本质上写一个 <code>SKILL.md</code> 就行。对于组织内部推广，这是巨大优势。因为真正阻碍 skill 规模化的，从来不是模型能力，而是作者生态能不能起来。 <strong>运行链路可观测性还不错</strong>：所有事情都发生在同一 session 里。read 了什么、接着调了什么工具、toolResult 长什么样，回放起来相对直观。</p>
<p data-tool="mdnice编辑器">但它的代价也有。</p>
<p data-tool="mdnice编辑器"><strong>代价 1：skill 缺少强执行边界</strong></p>
<p data-tool="mdnice编辑器">skill 本身不是 tool，没有独立权限面。你没法像封装一个函数那样，给 skill 绑定专属 schema、专属校验、专属 side effect 边界。最终还是模型在拿到 skill 文本后，自由地调用后续工具。</p>
<p data-tool="mdnice编辑器">这就意味着 skill 的约束力主要来自提示词，不来自执行器。</p>
<p data-tool="mdnice编辑器"><strong>代价 2：组合式编排能力弱</strong></p>
<p data-tool="mdnice编辑器">system prompt 里要求 upfront 最多读一个 skill，这对预算控制很好，对复杂任务不友好。多 skill 协同在这个体系里不是一等公民。</p>
<p data-tool="mdnice编辑器"><strong>代价 3：skill 的结果不可天然封装</strong></p>
<p data-tool="mdnice编辑器">Claude Code 那种「在新上下文执行 skill，再返回结果」，天然有个输入输出边界。OpenClaw 这里没有。skill 一旦展开，后续动作直接混进主会话消息流。你很难把它当作一个独立执行单元来治理。</p>
<p data-tool="mdnice编辑器"><strong>代价 4：对模型的选择质量要求高</strong></p>
<p data-tool="mdnice编辑器">因为没有独立 selector，也没有 tool-level dispatcher，skill 能不能选对，主要靠 <code>&lt;available_skills&gt;</code> 的 descriptions 和 system prompt 规则。这对 skill 描述质量要求很高。写得不清楚，模型就选歪。数量一多，问题更明显。</p>
<h1 data-tool="mdnice编辑器"><span class="content">10. 权限控制</span></h1>
<p data-tool="mdnice编辑器">OpenClaw 的思路是「先裁剪能力，再让模型行动」</p>
<p data-tool="mdnice编辑器">这一点我很喜欢，因为它比「调用时再临时拦截」更稳。</p>
<p data-tool="mdnice编辑器">OpenClaw 的权限控制，不是把所有工具都亮给模型，然后在调用某个危险工具时说不行。它更像是：</p>
<ol data-tool="mdnice编辑器">
<li>
<section>先根据 policy pipeline 把不允许的工具从列表里移掉</section>
</li>
<li>
<section>再把剩下的工具暴露给模型</section>
</li>
<li>
<section>模型根本看不到被禁的能力</section>
</li>
</ol>
<p data-tool="mdnice编辑器">skills 这边也一样：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>不合格的 skill 不进入 <code>&lt;available_skills&gt;</code></section>
</li>
<li>
<section><code>disable-model-invocation</code> 的 skill 对模型不可见</section>
</li>
<li>
<section>Read 路径有 workspace root guard</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这种「先裁剪，再推理」的方式有个很大的好处：模型认知空间更干净。它不会围着一堆不可用能力打转，也不会生成大量被拒调用的无效动作。</p>
<p data-tool="mdnice编辑器">如果你做企业场景，这比事后 deny 好得多。因为用户只会看到 agent 做合理尝试，而不是不停撞墙。</p>
<p data-tool="mdnice编辑器">当然，它的代价是灵活性差一点。你没法在非常细粒度的时刻做动态放行，除非重新构建 tool list 或 skill list。但我认为这笔账是划算的。Agent 系统的第一优先级不是灵活，是可控。</p>
<h1 data-tool="mdnice编辑器"><span class="content">11. 从平台设计角度看呢</span></h1>
<p data-tool="mdnice编辑器">觉得比较好的点：</p>
<p data-tool="mdnice编辑器"><strong>第一，简单。</strong><br />
这个简单不是简陋，是尽量不新增系统概念。skill 就是文件，加载靠 read，执行靠原有 tool-loop。对演进中的 agent 框架来说，这是非常健康的选择。</p>
<p data-tool="mdnice编辑器"><strong>第二，预算意识强。</strong><br />
<code>&lt;available_skills&gt;</code> 目录注入、最多只读一个 skill、compact/truncated 降级，这些都说明它把 context 当稀缺资源处理。</p>
<p data-tool="mdnice编辑器"><strong>第三，权限思路对。</strong><br />
先过滤 skill 和 tool，再让模型行动。可见性先于可调用性，这很工程化。</p>
<p data-tool="mdnice编辑器"><strong>第四，适合知识流程化。</strong><br />
很多团队真正需要的，不是一个会自己发明流程的 agent，而是把组织内已有 SOP 结构化地喂给模型。OpenClaw 这套很适合干这个。</p>
<p data-tool="mdnice编辑器">一些局限：</p>
<p data-tool="mdnice编辑器"><strong>第一，规模扩展性一般。</strong><br />
catalog 注入机制天然不适合 skill 数量无限增长。它适合几十级别，不适合大规模 marketplace。</p>
<p data-tool="mdnice编辑器"><strong>第二，组合能力弱。</strong><br />
「最多只读一个 skill」对稳态有帮助，对复杂任务编排是限制。</p>
<p data-tool="mdnice编辑器"><strong>第三，skill 缺少运行时身份。</strong><br />
它不是 tool，没有明确的输入输出边界，也没有独立权限与审计单元。后续做深治理会比较难。</p>
<p data-tool="mdnice编辑器"><strong>第四，过度依赖模型选择。</strong><br />
一旦 descriptions 写得不好，或者目录过大，skill 选择质量会变成系统上限。</p>
<h1 data-tool="mdnice编辑器"><span class="content">12. 适用场景</span></h1>
<p data-tool="mdnice编辑器">在系统架构选型时，如果有以下的条件，可以优先选择 OpenClaw 的设计逻辑：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>团队已经有一套成熟的 tool-loop，想低成本引入 skills</section>
</li>
<li>
<section>主要目标是把流程知识、操作手册、领域步骤注入 agent</section>
</li>
<li>
<section>skill 作者很多，技术水平参差不齐，需要低门槛 markdown 入口</section>
</li>
<li>
<section>更看重实现速度和治理可落地，而不是复杂编排能力</section>
</li>
<li>
<section>skill 数量在可控范围内，不会迅速膨胀到几百个</section>
</li>
</ul>
<p data-tool="mdnice编辑器">有如下的情况时，就不太适用使用这种方案：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>skill 本质上是可执行业务子任务，需要独立生命周期</section>
</li>
<li>
<section>需要强隔离执行、单独审计、结果回传和失败恢复</section>
</li>
<li>
<section>任务天然需要多个 skill 协同编排</section>
</li>
<li>
<section>技能库规模会非常大，必须做复杂路由</section>
</li>
<li>
<section>权限模型要求细到「某个 skill 可以做 A 但不能做 B」</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这些场景下，我更倾向 Claude Code 那种 tool 化 skill，或者更进一步，直接走 subagent / workflow node / task runtime。</p>
<h1 data-tool="mdnice编辑器"><span class="content">13. 小结</span></h1>
<p data-tool="mdnice编辑器">如果只用一句话概括两者区别：</p>
<p data-tool="mdnice编辑器"><strong>Claude Code 的 skill 更像可调用子程序，OpenClaw 的 skill 更像按需展开的运行手册。</strong></p>
<p data-tool="mdnice编辑器">前者强调执行单元，后者强调上下文注入。<br />
前者天然适合隔离和编排，后者天然适合轻量接入和流程沉淀。<br />
前者的复杂度在 runtime，后者的复杂度在 prompt 和内容治理。</p>
<p data-tool="mdnice编辑器">OpenClaw 用极其精简的 Prompt 规则和现成的 Read 工具，低成本实现了 Agent Skills 标准。它够用，但也把长文本管理的压力，原封不动地推给了底层大模型的 Context Window。</p>
<p data-tool="mdnice编辑器">我个人对 OpenClaw 这套实现是认可的，前提是别把它想象成一个万能技能平台。它解决的是「如何在不重写 agent runtime 的前提下，把结构化流程知识接进模型执行链路」这个问题。这个问题它解得挺干净。</p>
<p data-tool="mdnice编辑器">但如果你要的是 Claude Code 那种「新上下文执行 skill、像子程序一样调用、跑完把结果带回来」，你该找的是子会话、subagent、任务编排层，而不是继续往 <code>SKILL.md</code> 上堆规则。</p>
<p data-tool="mdnice编辑器">以上。</p>
</section>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2026/03/openclaw-skilss-claude-code/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>聊下 OpenClaw 的记忆系统</title>
		<link>https://www.phppan.com/2026/03/openclaw-memory/</link>
		<comments>https://www.phppan.com/2026/03/openclaw-memory/#comments</comments>
		<pubDate>Sun, 15 Mar 2026 11:37:01 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[Agent]]></category>
		<category><![CDATA[Memory System]]></category>
		<category><![CDATA[OpenClaw]]></category>

		<guid isPermaLink="false">https://www.phppan.com/?p=2477</guid>
		<description><![CDATA[OpenClaw 是最近 AI 圈最火的一个开源项目，没有之一。 从去年的 Agent 年，到今年的 AI 个 [&#8230;]]]></description>
				<content:encoded><![CDATA[<section id="nice" data-tool="mdnice编辑器" data-website="https://www.mdnice.com">
<p data-tool="mdnice编辑器">OpenClaw 是最近 AI 圈最火的一个开源项目，没有之一。</p>
<p data-tool="mdnice编辑器">从去年的 Agent 年，到今年的 AI 个人助理，OpenClaw 和去年 Manus 一样的，爆到不行，而且还是开源的版本。</p>
<p data-tool="mdnice编辑器">由于最近自己也在做 Agent，于是也看了 OpenClaw 的代码来了解其记忆系统的实现。有一些觉得可以借鉴学习的地方。</p>
<p data-tool="mdnice编辑器">OpenClaw 的记忆系统其实比较简单：它把「记忆」拆成了<strong>文件</strong>、<strong>索引</strong>、<strong>召回注入</strong>。</p>
<h1 data-tool="mdnice编辑器"><span class="content">1. 「Agent 记忆系统」的定义</span></h1>
<p data-tool="mdnice编辑器">在 Agent 工程里，记忆是一套能力组合：</p>
<ol data-tool="mdnice编辑器">
<li>
<section><strong>持久化</strong>：跨会话保存事实、偏好、决策、未完成事项。</section>
</li>
<li>
<section><strong>可检索</strong>：能在需要时把相关片段拉出来，且可控预算。</section>
</li>
<li>
<section><strong>可注入</strong>：把召回结果以确定的结构进入模型上下文，不靠「它自己想起来」。</section>
</li>
<li>
<section><strong>可审计</strong>：出了错能定位「写入发生在什么时候」「召回命中了什么」「注入了哪些行」。</section>
</li>
<li>
<section><strong>可治理</strong>：能处理泄露风险、过期信息、重复信息、冲突信息。</section>
</li>
</ol>
<p data-tool="mdnice编辑器">OpenClaw 的实现路径非常「工程」：<strong>Markdown 作为事实源</strong>，<strong>SQLite 作为检索索引</strong>，<strong>toolResult 作为注入通道</strong>。</p>
<h1 data-tool="mdnice编辑器"><span class="content">2. OpenClaw 的三层记忆</span></h1>
<p data-tool="mdnice编辑器">OpenClaw 的记忆从存储逻辑上来看可以分为三层：</p>
<h2 data-tool="mdnice编辑器"><span class="content">2.1 会话记忆</span></h2>
<ul data-tool="mdnice编辑器">
<li>
<section><strong>介质</strong>：内存为主，但 OpenClaw 会把 session 打印成类似日志的文件，放到 <code>sessions</code> 目录。</section>
</li>
<li>
<section><strong>内容</strong>：用户消息、OpenClaw 的思考过程、工具调用、skill 调用、最终回复。</section>
</li>
<li>
<section><strong>边界</strong>：会话结束后「可用性」就不可靠了。你能在文件里回放，但模型下一次对话并不会天然带着它。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">会话记忆更像「trace」。我们不能指望它解决跨会话连续性，只用它做排障、复盘、抽取素材（写入短期/长期）。</p>
<h2 data-tool="mdnice编辑器"><span class="content">2.2 短期记忆</span></h2>
<ul data-tool="mdnice编辑器">
<li>
<section><strong>介质</strong>：磁盘，<code>memory/YYYY-MM-DD.md</code> 为主（参考内容给了例子 <code>2026-03-10.md</code>）。</section>
</li>
<li>
<section><strong>内容</strong>：当天重要事件、过程笔记、TODO。关键点是「重要性」由人设与调教决定。</section>
</li>
<li>
<section><strong>边界</strong>：短期记忆是<strong>追加式日志</strong>，质量会漂移。写得越多，噪声越大；但写得太少，又召回不到。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">短期记忆适合承接「会话压缩之前的落盘」和「跨几天的上下文连续性」。它不是最终事实源，别把它当永久协议文档。</p>
<h2 data-tool="mdnice编辑器"><span class="content">2.3 长期记忆</span></h2>
<ul data-tool="mdnice编辑器">
<li>
<section><strong>介质</strong>：工作区根目录 <code>MEMORY.md</code>（参考内容明确）。</section>
</li>
<li>
<section><strong>内容</strong>：核心认知与关系、偏好风格、长期目标、进行中任务、关键事件/教训/决策。</section>
</li>
<li>
<section><strong>边界</strong>：参考内容强调「只在主会话加载」，群聊等任务不加载，避免泄露。</section>
</li>
</ul>
<p data-tool="mdnice编辑器"><code>MEMORY.md</code> ==「可执行的组织记忆」。它的价值不在于「写得多」，在于「冲突少、可被召回、能约束后续行为」。这层要治理，要像维护配置一样维护。</p>
<h1 data-tool="mdnice编辑器"><span class="content">3. OpenClaw 的文件布局</span></h1>
<h2 data-tool="mdnice编辑器"><span class="content">3.1 「会话快照」文件</span></h2>
<p data-tool="mdnice编辑器">快照文件主要是解决 <code>/new</code>、<code>/reset</code> 指令的断片</p>
<p data-tool="mdnice编辑器">session-memory 的 Hook 会在你执行 <code>/new</code> 或 <code>/reset</code> 前，把上一会话最近 N 条对话（默认 15 条）抽出来，写成一个 Markdown 文件放到 <code>workspace/memory/</code> 下。</p>
<ul data-tool="mdnice编辑器">
<li>
<section>命名：<code>YYYY-MM-DD-&lt;slug&gt;.md</code>，slug 通常由 LLM 根据主题生成；LLM 不可用就回退成 <code>HHMM</code>。</section>
</li>
<li>
<section>关键点：这种文件也会被检索索引到。原因是 <code>listMemoryFiles()</code> 会递归扫描 <code>workspace/memory/</code> 下所有 <code>.md</code>，并不要求必须是 <code>YYYY-MM-DD.md</code>（ <code>src/memory/internal.ts:115-145</code> ）。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这个机制对「人类工作流」很友好。很多团队的真实使用是：今天临时开了个话题，明天又忘了开在另一个会话里。会话快照能把碎片变成可检索素材，后面再沉淀进 <code>MEMORY.md</code>。</p>
<h2 data-tool="mdnice编辑器"><span class="content">3.2 <code>memory/main.sqlite</code>：索引库</span></h2>
<ul data-tool="mdnice编辑器">
<li>
<section><code>memory/main.sqlite</code> 基本可以确定是「记忆搜索（memory_search）」用的 SQLite 索引库。</section>
</li>
<li>
<section>索引对象：<code>MEMORY.md</code>、<code>memory/**/*.md</code>，以及你配置的 <code>extraPaths</code>，可选 session transcripts。</section>
</li>
<li>
<section>检索方式：FTS/BM25 关键词检索 +（可选）向量相似度检索（sqlite-vec）。</section>
</li>
<li>
<section>它存的典型结构：<code>files</code>、<code>chunks</code>、<code>embedding_cache</code>，以及可选的 FTS 表、向量虚表。</section>
</li>
</ul>
<p data-tool="mdnice编辑器"><strong>事实源是文本文件</strong>，<strong>索引是可重建的派生物</strong>。索引坏了你删掉重建就行；事实源坏了才是真的坏。</p>
<h1 data-tool="mdnice编辑器"><span class="content">4. 怎么建索引：</span></h1>
<p data-tool="mdnice编辑器">建索引我们关心的三件事：增量、去重、成本</p>
<p data-tool="mdnice编辑器">OpenClaw 的索引构建不是「每次全量重算」，也不是「精细 diff」：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><strong>更新粒度</strong>：按文件做增量。文件没变直接跳过。</section>
</li>
<li>
<section><strong>分块粒度</strong>：文件变了就重建该文件 chunks。</section>
</li>
<li>
<section><strong>向量成本</strong>：chunk embedding 通过缓存复用，避免重复调用 embedding provider。</section>
</li>
</ul>
<h2 data-tool="mdnice编辑器"><span class="content">4.1 扫描哪些文件会进索引</span></h2>
<p data-tool="mdnice编辑器">OpenClaw 会递归扫描 <code>workspace/memory/</code> 下所有 <code>.md</code>，并包含 <code>MEMORY.md</code>/<code>memory.md</code>。对应定位是 <code>src/memory/internal.ts:115-145</code>。</p>
<h2 data-tool="mdnice编辑器"><span class="content">4.2 文件级 hash：没变就跳过</span></h2>
<p data-tool="mdnice编辑器">文件 hash 的策略：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>Markdown：<code>hash = sha256(content)</code>（<code>internal.ts:L245-L263</code>）</section>
</li>
<li>
<section>多模态：buffer 也会 hash，最后把 <code>{path, contentText, mimeType, dataHash}</code> 做 JSON 再 sha256（<code>internal.ts:L204-L243</code>）</section>
</li>
<li>
<section>增量判定：对比 <code>files</code> 表里的 hash，一致就跳过（参考内容列了 memory 文件与 session 文件两条路径）。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">hash 是增量的核心。它的意义不止省时间，还省钱：embedding provider 往往是计费点。这种主要是对于使用第三方 embedding 的。</p>
<h2 data-tool="mdnice编辑器"><span class="content">4.3 分块策略：<code>tokens*4</code> 的字符近似</span></h2>
<p data-tool="mdnice编辑器"><code>chunkMarkdown()</code> 的策略：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><code>maxChars = tokens * 4</code>，<code>overlapChars = overlap * 4</code></section>
</li>
<li>
<section>以「行」为主，超长行会被切段</section>
</li>
<li>
<section>每块生成 <code>hash=sha256(text)</code>，并有 <code>embeddingInput</code></section>
</li>
</ul>
<p data-tool="mdnice编辑器"><code>tokens*4</code> 这种近似在工程里挺常见，优点是简单、稳定、跨模型大差不差。缺点也明显：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>语言差异会影响 token/char 比例；中英文混排时 chunk 尺寸会漂。</section>
</li>
<li>
<section>以行切块对 Markdown 友好，但对「一行很长的 JSON 或日志」不友好。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">可以盯两个指标来调 chunking：</p>
<ol data-tool="mdnice编辑器">
<li>
<section>平均召回 snippet 的「可读性」和「自洽性」；</section>
</li>
<li>
<section>SQLite 体积与索引更新耗时。chunk 太小召回碎，太大注入贵。</section>
</li>
</ol>
<h2 data-tool="mdnice编辑器"><span class="content">4.4 chunk 的唯一标识与 upsert</span></h2>
<p data-tool="mdnice编辑器">去重靠 id，chunk 写入策略如下：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>chunk <code>id</code>：<code>sha256("${source}:${path}:${startLine}:${endLine}:${chunk.hash}:${provider.model}")</code></section>
</li>
<li>
<section>同一 id：<code>ON CONFLICT(id) DO UPDATE</code> 覆盖更新</section>
</li>
<li>
<section>文件要重建时会先清旧再写新（参考内容总结了「清旧再写新」语义）</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这里有个很实际的 trade-off：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>它不做「chunk diff」，所以文件变了就重建该文件 chunks，逻辑简单，坏处是 IO 多。</section>
</li>
<li>
<section>但 embedding 通过缓存复用，把最贵的部分压下去了。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">另外，<code>id</code> 里带了 <code>provider.model</code>，这会带来一个工程后果：<strong>embedding 模型换了，chunk id 会变</strong>，索引层面等价于全量重建。 <code>provider/model/providerKey</code> 变化会触发 full reindex。</p>
<h2 data-tool="mdnice编辑器"><span class="content">4.5 embedding_cache</span></h2>
<p data-tool="mdnice编辑器">embedding 缓存的主键设计：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>主键：<code>(provider, model, provider_key, hash)</code></section>
</li>
<li>
<section><code>provider_key</code> 会把 endpoint/headers 等纳入指纹，避免跨配置污染缓存（而且会剔除授权头的细节在参考内容里提到）。</section>
</li>
<li>
<section>批量加载命中就跳过 embed；miss 才请求，成功回写缓存（对应 <code>manager-embedding-ops.ts</code> 的行段）。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这是「上线能用」的关键。否则就会遇到一个很尴尬的情况：<br />
索引更新频繁触发 embedding 重算 → 延迟抖动 → API 费暴涨 → 还可能被 provider 限流。 system prompt 的 skills 段落甚至提醒「假设有 rate limits，避免 tight loop」，这就有点被打过之后写进规范的味道。</p>
<h1 data-tool="mdnice编辑器"><span class="content">5. 更新怎么触发</span></h1>
<p data-tool="mdnice编辑器">watch、interval、onSearch、session-delta</p>
<p data-tool="mdnice编辑器">索引更新如果做得「过勤」，会把 CPU 和 IO 吃满；做得「过懒」，召回就是过期的。OpenClaw 在触发上给了多条路径：</p>
<ol data-tool="mdnice编辑器">
<li>
<section><strong>watch</strong>：chokidar 监听 <code>MEMORY.md</code>、<code>memory.md</code>、<code>memory/**/*.md</code>（以及 extraPaths、多模态扩展），变更标记 dirty，debounce 后 <code>sync(reason="watch")</code>。</section>
</li>
<li>
<section><strong>interval</strong>：<code>sync.intervalMinutes&gt;0</code> 就 <code>setInterval</code> 定时跑。</section>
</li>
<li>
<section><strong>onSessionStart</strong>：search 前 <code>warmSession(sessionKey)</code>，若开了 <code>sync.onSessionStart</code>，每个 sessionKey 首次触发后台 sync。</section>
</li>
<li>
<section><strong>onSearch</strong>：search 时如果 dirty 且开了 <code>sync.onSearch</code>，后台触发 sync。</section>
</li>
<li>
<section><strong>session-delta</strong>：监听 transcript 更新，累计新增 bytes/lines，达到阈值后把相关 session 文件标脏，再 sync。</section>
</li>
</ol>
<p data-tool="mdnice编辑器">还有两点：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><strong>单飞锁</strong>：同一时刻只跑一个 sync，复用同一个 Promise（参考内容定位 <code>manager.ts:452-467</code>）。</section>
</li>
<li>
<section><strong>全量重建的原子 swap</strong>：写到 <code>.tmp-UUID</code>，完成后 swap（含 <code>wal/-shm</code>），避免半成品索引（参考内容定位 <code>manager-sync-ops.ts:1050-1158</code>）。</section>
</li>
</ul>
<h1 data-tool="mdnice编辑器"><span class="content">6. 召回是怎么发生的</span></h1>
<p data-tool="mdnice编辑器">主要看 <code>buildMemorySection()</code> 的代码，因为它把策略写死了：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">提示词：
<span class="hljs-keyword">function</span> buildSkillsSection(params: { skillsPrompt?: string; readToolName: string }) {
  const trimmed = params.skillsPrompt?.trim();
  <span class="hljs-keyword">if</span> (!trimmed) {
    <span class="hljs-built_in">return</span> [];
  }
  <span class="hljs-built_in">return</span> [
    <span class="hljs-string">"## Skills (mandatory)"</span>,
    <span class="hljs-string">"Before replying: scan &lt;available_skills&gt; &lt;description&gt; entries."</span>,
    `- If exactly one skill clearly applies: <span class="hljs-built_in">read</span> its SKILL.md at &lt;location&gt; with \`<span class="hljs-variable">${params.readToolName}</span>\`, <span class="hljs-keyword">then</span> follow it.`,
    <span class="hljs-string">"- If multiple could apply: choose the most specific one, then read/follow it."</span>,
    <span class="hljs-string">"- If none clearly apply: do not read any SKILL.md."</span>,
    <span class="hljs-string">"Constraints: never read more than one skill up front; only read after selecting."</span>,
    <span class="hljs-string">"- 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."</span>,
    trimmed,
    <span class="hljs-string">""</span>,
  ];
}

<span class="hljs-keyword">function</span> buildMemorySection(params: {
  isMinimal: boolean;
  availableTools: Set&lt;string&gt;;
  citationsMode?: MemoryCitationsMode;
}) {
  <span class="hljs-keyword">if</span> (params.isMinimal) {
    <span class="hljs-built_in">return</span> [];
  }
  <span class="hljs-keyword">if</span> (!params.availableTools.has(<span class="hljs-string">"memory_search"</span>) &amp;&amp; !params.availableTools.has(<span class="hljs-string">"memory_get"</span>)) {
    <span class="hljs-built_in">return</span> [];
  }
  const lines = [
    <span class="hljs-string">"## Memory Recall"</span>,
    <span class="hljs-string">"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."</span>,
  ];
  <span class="hljs-keyword">if</span> (params.citationsMode === <span class="hljs-string">"off"</span>) {
    lines.push(
      <span class="hljs-string">"Citations are disabled: do not mention file paths or line numbers in replies unless the user explicitly asks."</span>,
    );
  } <span class="hljs-keyword">else</span> {
    lines.push(
      <span class="hljs-string">"Citations: include Source: &lt;path#line&gt; when it helps the user verify memory snippets."</span>,
    );
  }
  lines.push(<span class="hljs-string">""</span>);
  <span class="hljs-built_in">return</span> lines;
}

工具描述：
 label: <span class="hljs-string">"Memory Search"</span>,
    name: <span class="hljs-string">"memory_search"</span>,
    description:
      <span class="hljs-string">"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."</span>,
    parameters: MemorySearchSchema,
</code></pre>
<p data-tool="mdnice编辑器">有三点：</p>
<ol data-tool="mdnice编辑器">
<li>
<section><strong>触发条件写得具体</strong>：prior work / decisions / dates / people / preferences / todos。模型不需要猜「算不算记忆相关」。</section>
</li>
<li>
<section><strong>两阶段召回</strong>：先 <code>memory_search</code> 找片段，再 <code>memory_get</code> 精读少量行，控制注入体积。</section>
</li>
<li>
<section><strong>引用策略可控</strong>：<code>citationsMode</code> 可以关掉，避免模型动不动把路径行号甩出来（对产品形态很重要）。</section>
</li>
</ol>
<p data-tool="mdnice编辑器">两阶段召回比「一次性把相关文件 wholefile 塞进去」靠谱太多。</p>
<h1 data-tool="mdnice编辑器"><span class="content">7. 召回结果怎么进上下文</span></h1>
<p data-tool="mdnice编辑器">toolResult 消息是关键通道</p>
<p data-tool="mdnice编辑器">很多人以为「记忆」是把内容写进 system prompt。OpenClaw 不是这么干的。</p>
<p data-tool="mdnice编辑器">召回结果会以工具执行结果的形式进入会话消息列表，后续模型调用自然「看得到」。</p>
<ul data-tool="mdnice编辑器">
<li>
<section>工具返回会被包装成 JSON 文本块（参考内容定位 <code>jsonResult()</code> 在 <code>src/agents/tools/common.ts:230-239</code>）。</section>
</li>
<li>
<section>tool 返回会被标准化成 <code>content[] + details</code>（参考内容定位 <code>src/agents/pi-tool-definition-adapter.ts</code> 的 normalize）。</section>
</li>
<li>
<section>这些 toolResult 会被追加到 session messages，下一次模型调用会携带。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这条通道对排障非常友好：我们可以在 transcript 里看到「这次回答之前它到底召回了什么」。而且 toolResult 天然可控预算、可控格式，比让模型把记忆揉进自由文本稳得多。</p>
<h1 data-tool="mdnice编辑器"><span class="content">8. 写入时机</span></h1>
<h2 data-tool="mdnice编辑器"><span class="content">8.1 会话快照写入</span></h2>
<p data-tool="mdnice编辑器">人为触发的「切会话」</p>
<p data-tool="mdnice编辑器">当执行 <code>/new</code>、<code>/reset</code> 时，上一段会话尾部会被抽取成 <code>YYYY-MM-DD-&lt;slug&gt;.md</code>。这是「防断片」写入，价值是保住最近上下文。</p>
<p data-tool="mdnice编辑器">坑：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>抽取的 N 条对话里可能包含敏感信息。它会落在 <code>memory/</code>，并进入索引。</section>
</li>
<li>
<section>如果你把 workspace 目录同步到团队共享盘或提交到 repo，泄露面会扩大。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">我们的做法：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>明确区分「个人 workspace」和「团队 workspace」。个人的 <code>memory/</code> 默认不进 repo。</section>
</li>
<li>
<section>开启 citations 时，产品侧要想清楚是否允许暴露路径与行号。</section>
</li>
</ul>
<h2 data-tool="mdnice编辑器"><span class="content">8.2 短期记忆写入</span></h2>
<p data-tool="mdnice编辑器">「需要我们调教，告诉她哪些重要」。</p>
<p data-tool="mdnice编辑器">短期记忆要走「稀疏高密度」路线：条目少，但每条都能在未来的某个问题上直接复用。写入策略要围绕「将来会搜什么」来定，不要围绕「当下发生了什么」来记流水账。</p>
<h2 data-tool="mdnice编辑器"><span class="content">8.3 长期记忆更新</span></h2>
<p data-tool="mdnice编辑器">心跳 / AGENTS.md / cron</p>
<p data-tool="mdnice编辑器">三种更新机制：心跳、核心流程、cron。</p>
<p data-tool="mdnice编辑器">读最近几天短期记忆 → 选值得长期记住的 → 提炼写入 <code>MEMORY.md</code>。</p>
<p data-tool="mdnice编辑器">观点：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><strong>cron 最稳</strong>，可观测、可控、可回滚。</section>
</li>
<li>
<section>心跳更新很容易在负载高时抖动，或者在你最不想更新的时候更新。</section>
</li>
<li>
<section>把它塞进核心流程（AGENTS.md）要谨慎，一旦每次任务都触发提炼，会把延迟拉上去。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">一般做法：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>工作日每天固定一次 consolidation（cron）。</section>
</li>
<li>
<section>遇到重大决策或事故复盘，当天手动提炼写入 <code>MEMORY.md</code>，不等自动化。</section>
</li>
</ul>
<h1 data-tool="mdnice编辑器"><span class="content">9. 怎么「调教」</span></h1>
<p data-tool="mdnice编辑器">把记忆当成协议，不当成日记</p>
<p data-tool="mdnice编辑器">「告诉她哪些重要」。把「重要」拆成几类，每类有明确写入规则，避免模型自由发挥。</p>
<p data-tool="mdnice编辑器">一般的规则：</p>
<ol data-tool="mdnice编辑器">
<li>
<section><strong>稳定偏好</strong>：例如输出格式偏好、技术栈偏好、代码风格偏好。</section>
</li>
<li>
<section><strong>组织事实</strong>：团队结构、系统边界、核心服务依赖、环境约束。</section>
</li>
<li>
<section><strong>关键决策</strong>：ADR 级别的决策，包含时间点与理由。</section>
</li>
<li>
<section><strong>长期目标与在途事项</strong>：能跨周追踪的，不写「今天要做的」。</section>
</li>
<li>
<section><strong>事故教训</strong>：明确到「哪个坑踩过」「如何避免」。</section>
</li>
</ol>
<p data-tool="mdnice编辑器">短期记忆（<code>memory/YYYY-MM-DD.md</code>）会允许更多过程性信息，但要满足一个条件：<strong>未来能被搜索问题命中</strong>。比如你写「今天讨论了 A」，基本没用；你写「决定 A 的原因是 B，后续若出现 C 用 D 回滚」，会有用一些。</p>
<p data-tool="mdnice编辑器">如果希望 <code>memory_search</code> 在关键时刻召回到正确内容，就得用「未来的查询语句」来写记忆。</p>
<p data-tool="mdnice编辑器">以上。</p>
</section>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2026/03/openclaw-memory/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
