乐于分享
好东西不私藏

OpenClaw 的 System Prompt:每次 Agent 醒来看到什么

OpenClaw 的 System Prompt:每次 Agent 醒来看到什么

Carapace Huang · 2026-07-25 · 约 3220 字 · 阅读 12 分钟

AI AgentSystem Prompt架构解析Context Engineering

OpenClaw 的 System Prompt:每次 Agent 醒来看到什么

改 SOUL.md 能改变 Agent 行为——你知道。但 SOUL.md 是怎么变成 Agent 的「大脑」的?本文从 1,380 行 System Prompt 组装器源码出发,拆解 20+ section 的拼接流水线、7 层 context file 的排序策略、cache boundary 的分界机制、以及 sub-agent 的「极简模式」。

1. 引子:SOUL.md 只是一块砖

每个 OpenClaw 用户都知道这个「魔法」:在 workspace 里放一个 SOUL.md,Agent 就有了人格。改一个 MEMORY.md,Agent 就记住了你的偏好。

但文件到底是怎么变成 Agent 每次调 API 时携带的那个 System Prompt 的?

答案藏在 agents/system-prompt.ts 里——一个 1,380 行的组装器。它的工作不是「写 prompt」,而是像流水线一样,把 20+ 个 section 拼接成一套完整的指令。我打赌你改过 SOUL.md——所以,一起来看看你的文件到底是怎么被「吃」进去的。

2. 全景:20+ Section 装配流水线

先看 buildAgentSystemPrompt() 的参数——这本身就是一份「Agent 大脑的物料清单」。一共 30+ 个输入参数,每个都对应着 System Prompt 中的一个组成部分:

整个组装器分成两个阶段:第一阶段,20+ 个独立的 section builder 各自处理自己的输入(文件、工具列表、时区、运行时信息等);第二阶段,按固定顺序把所有 section 拼接在一起。最终输出是一段发给 LLM 的完整文本。

▲ 图1:30+ 输入 → 20+ section builder → SHA256 缓存 → 最终 System Prompt(chips 按源码 line 1013-1260 展开顺序校准)

3. Context Files 的 7 层注入

SOUL.md 是怎么出现在 System Prompt 里的?不是简单的 fs.readFile。这里有一个 排序—过滤—注入的三步流水线。

3.1 固定优先级排序

源码里硬编码了一个优先级表:

// system-prompt.ts:54-62const CONTEXT_FILE_ORDER = new Map([  ["agents.md",   10],   // 行为规则  ["soul.md",     20],   // 人格语气  ["identity.md", 30],   // Agent 身份  ["user.md",     40],   // 用户信息  ["tools.md",    50],   // 工具配置  ["bootstrap.md", 60],  // 引导指令  ["memory.md",   70],   // 长期记忆]);

两个值得注意的细节:

① AGENTS.md 优先级最高(10),不是 SOUL.md。 行为规则 > 人格。AGENTS.md 定义行为规则,规则比人格更底层。

② MEMORY.md 优先级最低(70)。 因为它是长期记忆,放在最下方——Agent 先读完「我是谁、有什么工具、有什么规则」,再去看「用户之前说过什么」。

更深的原因:利用 transformer 的 attention prior。 LLM 在生成每个 token 时,对前文各位置的 attention 权重并不均匀——早期 token 获得显著更高的权重,位置越靠前对模型行为的塑造越强。OpenClaw 把「行为规则」放 priority 10(最前),是显式利用这个 prior:让 rules 在认知锚定阶段就被加权,避开「SOUL.md 的风格化指令覆盖规则」的风险。本质选型:组装器在做的是「按 LLM 已有的偏好预先排序输入」,7 层 priority 数字 10/20/…/70 是对 attention 分布的工程映射。

3.2 注入方式与信任边界

注入时不是简单拼接,而是带注释说明。每个文件都带着一句 unless higher-priority instructions override——这是一个安全阀:开发者指令 > System Prompt > Project Files

但 escape hatch 不是 .md 文件作者写上去的——它由 OpenClaw 源码硬编码。源码中只有 SOUL.md 和 MEMORY.md 会被拼一段文字 role label(含 escape hatch),而 AGENTS.md / IDENTITY.md / USER.md / TOOLS.md / BOOTSTRAP.md 这五个文件完全没有文字 label——直接拼 ## 文件路径 头 + 纯文件内容。这是显式的信任边界:SOUL.md / MEMORY.md 的作者不应能通过在文件里加一句「可被覆盖」来自我声明 escape。人格软、偏好最软,两层有 label 有 escape;其余五类无 label 无 escape 的语义由 OpenClaw 锁定。

3.3 Static vs Dynamic 文件分离

这是 System Prompt 设计中最巧妙的工程决策。看源码:

// system-prompt.ts:64const DYNAMIC_CONTEXT_FILE_BASENAMES = new Set(["heartbeat.md"]);

HEARTBEAT.md 被标记为唯一的动态文件。为什么分离?因为 System Prompt 有缓存机制——静态文件很少变,可以安全缓存;HEARTBEAT.md 每 30 分钟更新一次,如果它混入静态区,每次心跳都触发全量 System Prompt 重新计算。这是一个只有对 LLM API 收费模型有深刻理解才会做的设计。

HEARTBEAT.md 是状态文件,不是定时器。它由 agent runtime 自己周期写入(系列 2 的 heartbeat loop 触发),prompt 拼装永远是被动读,没有「定时拼 prompt」的 trigger。「每次心跳」指的是 agent 状态被更新这件事,下一次 prompt 拼装时自动看到新值。对比 §6 的开放 plugin 扩展点——DYNAMIC_CONTEXT_FILE_BASENAMES 默认只包含 heartbeat.md,且不接受第三方注册。plugin 系统是开放的(§6),文件属性集是封闭的——两套扩展边界正交存在。

▲ 图2:Context File 注入流水线——排序→分离→注释→注入四步

4. Cache Boundary:64 字符 SHA256 摘要的威力

System Prompt 不是每次请求都重新生成的。组装器内部维护了一个 stable prompt prefix cache

// system-prompt.ts:108-126function cacheStablePromptPrefix(key: string, build: () => string) {  const cached = stablePromptPrefixCache.get(key);  if (cached) {    stablePromptPrefixCache.delete(key);    stablePromptPrefixCache.set(key, cached);  // LRU refresh    return cached.value;  }  const value = build();  stablePromptPrefixCache.set(key, { value });  while (stablePromptPrefixCache.size > 64) { /* evict oldest */ }  return value;}

key 是什么? SHA256 哈希(64 个十六进制字符,行 128-132)。只要 SOUL.md、MEMORY.md、工具列表、时区全部不变 → 相同哈希 → LRU 直接返回,显著降低重复构建开销。

⚠️ 缓存失效陷阱hashStablePromptInput 的入参包含 ~38 个字段——toolLines(新工具注册)、providerSectionOverridesruntimeChannel(channel 切换)、runtimeCapabilities(如 inlinebuttons 开关)、bootstrapSystemPromptSectionssandboxInfodisplayWorkspaceDir——任一变化都让整段 stable prefix 缓存失效,从头重建。所以在「MCP 工具频繁热插拔」或「channel 切换」场景下,缓存命中率大幅下降。这是 token 经济学的真正成本。

▸ 完整 hash key 含 38 个字段,按「变与不变」分三档:

档位
代表字段
必须性
每次都变
toolLines / runtimeChannel / runtimeCapabilities / ownerLine / stableContextFiles / memorySection / sandboxInfo / displayWorkspaceDir
必进 key
——变化即 prompt 变化
几乎不变
promptMode / promptSurface / acpEnabled / modelAliasLines / subagentDelegationMode
进没坏处——stale cache 比低命中更糟
先 derive/default
silentReplyPromptMode / subagentDelegationMode(normalize) / inlineButtonsEnabled / threadBoundAcpSpawnEnabled
进 key 前必须在 buildAgentSystemPrompt 顶部完成派生/标准化

关键技术选型: OpenClaw 选择「宁可重建也不 stale」——~38 字段几乎全进 key,是偏保守的策略,但避免了「以为 cache 命中实际已经陈旧」的隐蔽 bug。cache 命中率由字段进 key 的覆盖度决定:全进 = 命中率低、正确性高;不全进 = 命中率高、正确性低。

5. Sub-Agent 的「极简模式」

这不是一个可有可无的 feature——它决定了子 Agent 能不能在 token 预算内正常工作。主 Agent 的 System Prompt 可能高达 10K+ tokens,但 Sub-Agent 只需要干一件事。

OpenClaw 提供了三档模式:

  • full
     — 所有 section(主 Agent 默认)
  • minimal
     — 保留 Tooling + Workspace + Skills + Safety + OpenClaw Control + Runtime
  • none
     — 仅一行身份标识

▲ 图3:三档对比——minimal 列已按源码 isMinimal 行为校准(token 数为估算)

minimal 模式下,被砍掉的 section:

砍掉的 Section
原因
Authorized Senders
子 Agent 不接受直接用户消息
Messaging
子 Agent 不直接回复用户
Dynamic Context
HEARTBEAT.md 仅主 Agent 相关
Voice / TTS
子 Agent 不出声
Silent Reply
子 Agent 不需要发送反馈

⚠️ 不在砍掉列表的非 full section: Skills(line 243-257)、Date & Time(line 398-403)、Sandbox(line 1136-1170)——三项源码都没有 isMinimal 分支。Skills 看 skillsPrompt、Date & Time 始终输出、Sandbox 看 sandboxInfo?.enabled。minimal 模式只要对应开启标志在,依然输出。

保留的都是「干活必需的」:工具列表(Tooling)、workspace 文件(Project Context)、Skills(如有)、Safety 规则、OpenClaw Control、Model Identity、以及运行时信息。

更深一层:isMinimal 是双层防御。源码 line 900 的关键定义:const isMinimal = promptMode === "minimal" || promptMode === "none";——这个 boolean 名叫 isMinimal,但语义是「任何非 full 模式」。none 模式在 line 952-957 提前 short-circuit 返回 2 行字符串,根本不走 builder pipeline。但所有 builder 也已经被 isMinimal 守卫——即使 short-circuit 被改坏,none 也不会输出完整 prompt。技术选型理由:单一守卫不可靠——加新 section 时忘记加 if (isMinimal) return [] 是高频 bug。「每个 builder 自守卫」比「集中维护一个 drop 列表」健壮得多——10+ 个 if 重复是有意为之的冗余换安全

6. Memory Section:条件注入 + Plugin 扩展

不是所有 Agent 都需要记忆能力。buildMemorySection 的门控逻辑很简单:includeMemorySection 为 true 就注入,false 就跳过。组装器本身不检查 memory_search 或 memory_get 是否在可用工具列表中——那是 plugin 内部的事。

先厘清概念:OpenClaw 的 "plugin" 不等同于用户安装到 system 的扩展。它仍然跑在 OpenClaw 进程内、由 registerMemoryCapability(...) 等函数调用直接修改 module-level 单例——没有沙箱、没有签名验证。但它支持通过 npm 或 clawHub 运行时安装。Plugin 来源有 4 类:bundled(OpenClaw 自带)、global(全局安装)、workspace(workspace 专用)、config(按 agent 配置启用)。一句话:运行态有完整 install 通道,安全模型是进程级信任。

更妙的是 plugin 扩展机制——buildMemoryPromptSection 不是硬编码的,它从注册的 memory plugin 中查询 prompt builder。两个设计细节:

① .toSorted(...) 按 pluginId 排序——保证多插件注册时 byte-identical 输出,是 SHA256 prefix cache 能命中的前提。

② capability.promptBuilder 和 promptSupplements 双通道——主 prompt builder 只有注册 memory capability 的 plugin 能设置,但任何 plugin 都可以注册 supplement。第三方插件可以往 System Prompt 里注入自己的 memory 使用指令。编译后的 wiki、向量数据库、外挂知识库——都可以通过这个机制把使用说明「写进」Agent 的 System Prompt。

关键技术坑点:capability.promptBuilder 是单值覆盖语义。registerMemoryCapability 是直接 replace——后注册 plugin 会替换前一个的整个 capability 对象,没有协商机制。patchMemoryCapability 只在同 pluginId 内做 merge;跨 pluginId 是 replace。多个 plugin 想当「主 memory provider」会赢者通吃。这是显式的「主权威 + 多附加」分工:prompt 顶层位置是稀缺资源,由一个 plugin 占据;其他 plugin 走 supplement 数组附加。plugin 作者必须知道这个设计约束。

7. Tool Summaries:27 个一等公民工具

System Prompt 的 Tooling 部分不是简单的 - exec: run commands。每个核心工具都有针对性的描述:

const coreToolSummaries = {  exec: "...pty available for TTY-required CLIs",  cron: "...reminder 的 text 应该读起来像一个提醒...",  sessions_spawn: "...omit context for isolated children...",  session_status: "...use for model-use questions (📊)...",};

这些说明不是 API 文档的搬运——它们是 behavioral prompt:不是告诉 Agent 这个函数怎么调用,而是告诉 Agent 什么时候该用、用的时候注意什么。例如 cron 的 summary 不仅是「管理定时任务」,而是明确指出「写 reminder 时 systemEvent text 应该读起来像一个提醒」——这是一个只有实际踩过坑才会加的提示。这些 summary 最终按固定 toolOrder 数组拼接,保证每次生成的 Tooling section 结构一致。

为什么 27 个 tool summary 不做成 plugin 系统?这是显式的扩展性边界选择——按「产品内功 vs 外部能力」分工:tool summary 是产品能力,UX copy 的一致性和质量由 OpenClaw 维护基线,让第三方 plugin 改 cron summary 会污染产品质量;memory backend 是外部能力,每个 backend 语义不同(向量检索 vs 关键词 vs wiki graph),不该被 OpenClaw 规定。技术选型原则:「产品内功 hardcode + 外部能力 plugin」——这种混合策略在不损失灵活性的同时守住产品质量基线。

8. 完整链路

Cache Boundary 是整个设计的核心。 边界之上 = SHA256 哈希后可复用的 stable prefix;边界之下 = 每次请求重算的动态后缀。改 SOUL.md 只有边界以上才触发缓存重建;HEARTBEAT.md 更新在边界以下,不影响缓存命中率。

* 条件性省略(stable prefix 内,不一定是每场都出):OpenClaw Self-Update(hasGateway)、Model Aliases(hasModelAliases)、Reasoning Format(reasoningHint)、Provider Stable Prefix(overridable)。

9. 总结

三个关键设计决策值得记住:

① Context file 优先级排序。 AGENTS.md(10) > SOUL.md(20) > MEMORY.md(70)。行为规则 > 人格 > 记忆。不是随便排的——是对 transformer attention prior 的工程映射。

② Static/Dynamic 分离 + Cache Boundary。 静态文件缓存复用,动态文件每次注入。这是 token 经济学的工程实践。但工具注册、channel 切换等也会触发缓存重建——这是「宁可重建也不 stale」的保守策略。

③ Sub-agent 极简模式。 isMinimal 双层防御裁三档。10+ 个 if (isMinimal) return [] 守卫——冗余但语义正确,是「冗余换安全」的经典案例。

这篇文章讲的是「Agent 每次醒来看到什么」。下一篇,我们讲「Agent 在运行中怎么记住、怎么忘记」——Compaction 和 Memory 引擎。

← #2 执行引擎的 10 级自愈OpenClaw 架构拆解 · 第三篇#4 记忆系统 →


作者:Carapace Huang,Android/Linux BSP 技术架构师,多年嵌入式系统经验。源码版本:OpenClaw main 分支,commit 220d3ec2主要参考agents/system-prompt.ts (1,380 行) · plugins/memory-state.ts (343 行) · agents/pi-embedded-runner/system-prompt.ts (144 行)适合平台:公众号 / 知乎  |  阅读 12 分钟