乐于分享
好东西不私藏

OpenClaw的提示词哲学

OpenClaw的提示词哲学

动态系统提示词生成全链路工程化拆解

文件定位src/agents/system-prompt.ts


做AI智能体开发的人,几乎都踩过同一个致命的坑。

写死的静态系统提示词,换个场景就崩,加个工具就乱,缓存优化全白做,连最基本的身份权限都管不住。

而解决这一切问题的终极答案,全藏在OpenClaw架构里,这个叫system-prompt.ts的文件中。

它不是一段简单的提示词模板。它是AI智能体系统提示词的全自动总装工厂


一、先搞懂:这个文件在AI Agent架构里,到底是什么角色?

每一次AI对话启动的瞬间,都有一个核心问题必须解决。你要清清楚楚告诉大模型:你是谁、你能用什么工具、你有什么权限、你要遵守什么规则、你的用户是谁。

这些信息,就是决定AI行为边界的系统提示词

而system-prompt.ts,就是动态拼装这套系统提示词的核心中枢。

给你打个最直白的比方:如果AI模型是刚入职的新员工,系统提示词就是专属他的完整入职手册。而system-prompt.ts,就是生成这本手册的智能工厂。不同部门、不同权限、不同岗位的员工,拿到的手册内容完全不同,绝不会出现一本手册全公司通用的乱象。


二、别再写死提示词了!静态模板根本扛不住这些场景

很多人做Agent,上来就把系统提示词写死在代码里。结果就是,稍微改点需求,整个提示词就要推翻重写,跨场景直接水土不服。

静态提示词,天生就解决不了这6个核心痛点:

  • • 不同渠道适配难:Telegram用户和Web端用户的能力边界完全不同
  • • 工具集动态适配难:有的会话能调用工具,有的不行,静态模板根本没法灵活切换
  • • 权限管控难:沙箱环境和宿主环境、普通执行和提升执行,权限天差地别
  • • 上下文动态注入难:每个工作区的项目文档、人格设定都不一样,静态模板没法适配
  • • 模式切换难:主代理、子代理、最简模式,需要的提示词量级天差地别
  • • 缓存优化难:Anthropic等大模型的KV缓存,要求稳定的提示词前缀,静态模板根本做不到精准拆分

三、先看全景!这个核心文件的4大核心能力,全在这了

整个文件的核心能力,浓缩在4个导出项里,每一个都精准解决一个核心问题:

导出名称
核心类型
核心职责
buildAgentSystemPrompt
核心函数
总装线主入口:完整拼装最终的系统提示词
buildRuntimeLine
工具函数
生成运行时元信息,把所有环境参数一次性说清
buildAgentUserPromptPrefix
工具函数
项目未初始化时,引导模型先读引导文档,不瞎回复
OwnerIdDisplay
类型定义
规范所有者ID的显示方式,兼顾身份验证与隐私保护

四、核心总装线拆解:buildAgentSystemPrompt到底是怎么工作的?

这个函数,就是整个文件的心脏。它接收所有运行时参数,按照固定的流水线顺序,拼装出完整可用的系统提示词。

先看它要处理的核心输入参数,每一个都决定了最终提示词的形态:

  • • 工作目录路径:决定AI的操作边界
  • • 可用工具列表与描述:告诉AI能调用什么能力
  • • 提示词模式:控制提示词的详细程度,适配不同代理场景
  • • 项目上下文文件:注入项目专属的规则、人格、文档
  • • 沙箱环境信息:明确AI的运行环境与权限边界
  • • 授权用户列表:划定AI的服务对象与权限
  • • 模型提供商自定义注入:适配不同大模型的专属要求
  • • 技能与心跳提示:补充AI的专属能力与实时状态

而它的内部执行流水线,一步都不能乱:

  1. 1. 预处理所有参数,规范化工具名,解析模型提供商的自定义要求
  2. 2. 最简模式短路:如果是none模式,直接返回最简身份声明,零多余开销
  3. 3. 按固定顺序拼装各个模块:工具规则、安全规则、CLI规则、技能、记忆、项目文档、沙箱信息
  4. 4. 插入缓存边界标记,拆分稳定内容与动态内容
  5. 5. 拼接动态上下文文件与额外提示内容
  6. 6. 添加运行时元信息行,明确当前环境的所有参数
  7. 7. 过滤空行,合并生成最终的系统提示词字符串

整个流程,就像汽车生产的总装线。每一个模块都有固定的安装顺序,固定的安装标准,最终出来的成品,完全适配当前的运行场景,零冗余,零错配。

4.1 buildRuntimeLine:给AI的专属“环境身份证”

这个函数的职责很简单,生成一行完整的运行时元信息。就像给AI发了一张实时更新的环境身份证,清清楚楚告诉它当前在哪运行,有什么能力。

最终生成的格式,一目了然:

Runtime: agent=my-agent | host=my-pc | os=linux (x64) | node=22.0 | model=gpt-5.4 | channel=telegram | capabilities=inlinebuttons,reactions | thinking=off

4.2 buildAgentUserPromptPrefix:项目初始化的专属引导员

很多新手都会遇到一个问题:项目还没初始化完成,AI就开始瞎回复。这个函数,就是解决这个问题的。

当工作区还没完成Bootstrap流程时,它会在用户消息前加上引导提示,强制AI先读引导文档,再做回复,从根源上避免无效输出。


五、封神级代码设计!每一行都踩中了提示词工程的核心痛点

这个文件里的代码,没有一行是多余的。每一个核心设计,都精准解决了提示词工程里的高频致命问题,我们逐行扒透。

5.1 上下文文件固定排序:解决缓存失效的头号杀手

核心痛点很多人做动态提示词,每次生成的内容顺序都不一样。而大模型的KV缓存,核心要求就是相同输入必须产生相同输出顺序乱了,缓存直接全废,token成本直接翻倍,响应速度直线下降。

神仙设计开发者直接给所有核心上下文文件,定死了排序权重:

const CONTEXT_FILE_ORDER = new Map<string, number>([  ["agents.md", 10],  ["soul.md", 20],  ["identity.md", 30],  ["user.md", 40],  ["tools.md", 50],  ["bootstrap.md", 60],  ["memory.md", 70],]);
  • • 核心规则文件,权重最高,排最前面
  • • 人格设定文件,紧随其后,保证AI的核心身份稳定
  • • 增量记忆信息,权重最低,排最后
  • • 不在权重表里的文件,统一排最后,再按文件名、路径做二次排序

最终价值只要文件内容不变,每次生成的提示词顺序完全一致。缓存命中率直接拉满,token成本大幅降低,响应速度直接上一个台阶。

5.2 缓存边界设计:提示词性能优化的终极解法

核心痛点提示词里,有的内容万年不变,有的内容每次会话都要更新。如果全放在一起,只要有一个字变了,整个提示词的缓存就全废了。这也是90%的开发者,都做不好提示词缓存的核心原因。

神仙设计用一个缓存边界标记,直接把提示词切成两半:

// 缓存边界分隔符,整个缓存策略的核心lines.push(SYSTEM_PROMPT_CACHE_BOUNDARY);// 边界之前:稳定内容,万年不变的项目核心文件,缓存永久复用lines.push(...buildProjectContextSection({  files: stableContextFiles,  heading: "# Project Context",  dynamic: false,}));// 边界之后:动态内容,频繁更新的实时信息,不影响前面的缓存lines.push(...buildProjectContextSection({  files: dynamicContextFiles,  heading: stableContextFiles.length > 0 ? "# Dynamic Project Context" : "# Project Context",  dynamic: true,}));

而且,只有heartbeat.md这一个实时更新的文件,被标记为动态内容。

给你打个最直白的比方:这就像一本书的再版印刷。如果只有附录的实时数据变了,印刷厂根本不需要重新制版整本书,只需要替换附录就行。大模型的KV缓存,也是完全一样的逻辑。

最终价值稳定内容的缓存100%复用,只有动态内容需要重新计算。token消耗直接砍半,大模型响应速度直接提升数倍,长会话场景下的优势更是碾压级的。

5.3 所有者身份哈希:隐私保护的极致细节

核心痛点多用户场景下,用户的手机号、ID等敏感信息,会直接写进系统提示词里。一旦提示词被日志、调试输出捕获,用户隐私直接泄露,后果不堪设想。

神仙设计开发者做了一个极简又极致的哈希处理函数:

function formatOwnerDisplayId(ownerId: string, ownerDisplaySecret?: string) {  const hasSecret = ownerDisplaySecret?.trim();  // 有密钥用HMAC-SHA256防篡改,无密钥用SHA256防彩虹表  const digest = hasSecret    ? createHmac("sha256", hasSecret).update(ownerId).digest("hex")    : createHash("sha256").update(ownerId).digest("hex");  // 只取前12位字符,足够唯一,又足够简短  return digest.slice(0, 12);}

最终价值既能在系统里完成身份验证,又绝对无法通过哈希值反推用户的原始ID。从根源上杜绝了敏感信息泄露的风险,细节拉满。

5.4 提示词消毒:防注入攻击的最后一道防线

核心痛点提示词注入,是AI Agent最常见的攻击方式。攻击者只要能控制工作目录名、文件名等外部输入,就能通过换行符、控制字符,在系统提示词里插入伪造的指令,直接接管AI的行为。

神仙设计所有外部输入,在嵌入提示词之前,必须先经过消毒处理:

const sanitizedWorkspaceDir = sanitizeForPromptLiteral(params.workspaceDir);

这个函数,会直接剥离所有Unicode控制字符和格式字符,确保外部输入绝对无法破坏提示词的结构。

这就像网页开发里的XSS防护。绝对不信任任何外部输入,所有内容必须先消毒,再嵌入页面。

最终价值从根源上堵住了提示词注入的漏洞,守住了AI Agent的安全底线。

5.5 模式短路设计:把性能优化做到极致

核心痛点子代理、轻量会话,根本不需要主代理那一套完整的提示词。如果全量加载,不仅浪费token,还会拖慢响应速度,甚至让子代理偏离核心任务。

神仙设计一个极简的模式短路逻辑,直接把冗余开销砍到零:

if (promptMode === "none") {  return "You are a personal assistant running inside OpenClaw.";}
  • • full模式:主代理专用,全量加载所有模块,完整的能力与规则
  • • minimal模式:子代理专用,省略用户交互相关模块,保留核心工具与工作区信息
  • • none模式:最简场景专用,只返回一行身份声明,零多余开销

最终价值不同场景用不同量级的提示词,不浪费一个token,不增加一毫秒的多余开销。

5.6 提供商覆盖机制:可扩展架构的教科书级实现

核心痛点不同AI提供商,对工具调用、交互风格的要求天差地别。如果每适配一个新模型,就要改核心代码,整个架构会变得无比臃肿,维护成本直接爆炸。

神仙设计用“默认内容+提供商覆盖”的机制,完美实现了开闭原则:

...buildOverridablePromptSection({  override: providerSectionOverrides.tool_call_style,  fallback: [    "## Tool Call Style",    "Default: do not narrate routine, low-risk tool calls...",  ],})
  • • 提供商没有自定义要求,就用默认的核心内容
  • • 提供商有专属要求,直接覆盖对应模块,核心代码完全不用动

最终价值对扩展开放,对修改封闭。适配新的AI模型,只需要写对应的覆盖内容,不用碰核心逻辑,维护成本降到最低。

5.7 工具名规范化与排序:再小的细节,也为缓存服务

核心痛点工具名大小写不统一、顺序不固定,同样会导致提示词内容变化,缓存失效。很多开发者根本注意不到这个细节,白白浪费了大量缓存机会。

神仙设计

  • • 用Map做大小写无关的去重,保留第一次出现的原始写法,避免大小写混乱
  • • 给核心工具定死固定的显示顺序,外部工具按字母序追加,确保每次生成的顺序完全一致

最终价值再小的细节,也为缓存确定性服务。把缓存命中率,从根源上拉到极致。


六、不是孤军奋战!这个核心文件,如何撑起整个Agent架构?

system-prompt.ts不是一个孤立的文件。它是整个OpenClaw架构的核心枢纽,和周边10+模块深度协同,共同撑起了整个AI Agent的稳定运转。

协作模块
核心协作方式
prompt-cache-stability.ts
提供规范化函数,确保提示词文本的确定性
sanitize-for-prompt.ts
提供防注入消毒能力,守住安全底线
system-prompt-cache-boundary.ts
提供缓存边界标记与拆分合并工具
system-prompt-contribution.ts
定义模型提供商的提示词自定义规则
memory-state.ts
提供记忆相关的提示词模块
bootstrap-prompt.ts
提供项目初始化的引导文本

七、90%新手都会踩的5个坑,一次性讲透

Q1:为什么要用字符串数组拼装提示词,而不是模板字符串?

:因为系统提示词的内容,是高度条件化的。用数组拼装,每个模块的构建函数都返回字符串数组,主函数直接展开即可。空数组就代表不添加这个模块,比模板字符串里的undefined判断,要简洁10倍,可维护性也天差地别。

Q2:minimal模式和none模式,到底有什么区别?

:三个模式的边界,划分得极其清晰:

  • • full模式:主代理专用,全量加载所有模块,完整的能力与规则
  • • minimal模式:子代理专用,省略用户交互相关模块,保留核心工具与工作区信息
  • • none模式:最简场景专用,只返回一行身份声明,零多余开销

Q3:为什么一定要拆分稳定上下文和动态上下文?

:这是整个提示词缓存优化的核心。agents.md、soul.md这些核心文件,在整个会话周期里几乎不会变,放在缓存边界之前,可以永久复用缓存。而heartbeat.md这类实时更新的文件,放在边界之后,就算每次都变,也不会影响前面稳定内容的缓存。一分为二,直接把缓存效率拉到极致。

Q4:sanitizeForPromptLiteral,到底在防什么攻击?

:防的是致命的提示词注入攻击。如果攻击者能控制工作目录名、文件名这些外部输入,就能通过换行符、控制字符,在系统提示词里插入伪造的指令,直接接管AI的行为。这个消毒函数,会剥离所有控制字符,确保外部输入绝对无法破坏提示词的结构,从根源上堵住漏洞。

Q5:提供商覆盖机制,最常用的场景是什么?

:适配不同大模型的工具调用与交互风格。不同AI提供商,对工具调用的格式、语气、规则的要求,天差地别。通过sectionOverrides,提供商插件可以直接定制工具调用风格、执行偏好、交互风格这三个核心模块,完全不用修改核心代码,适配成本降到最低。


最后想说

提示词工程,从来都不是“写几句好听的话”这么简单。它是AI Agent的灵魂,是决定AI行为边界、能力上限、安全底线的核心。

而system-prompt.ts给我们的最大启发,从来都不是一段代码。而是一套完整的工程化思维:用确定性的设计,解决不确定性的场景;用模块化的架构,适配千变万化的需求;用极致的细节优化,守住性能、安全、成本的三重底线。

做AI Agent,拼到最后,拼的从来都不是谁的提示词写得更花哨。而是谁的工程化能力更强,谁的架构更稳定,谁的细节更到位。