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 |
|
|
四、核心总装线拆解:buildAgentSystemPrompt到底是怎么工作的?
这个函数,就是整个文件的心脏。它接收所有运行时参数,按照固定的流水线顺序,拼装出完整可用的系统提示词。
先看它要处理的核心输入参数,每一个都决定了最终提示词的形态:
-
• 工作目录路径:决定AI的操作边界 -
• 可用工具列表与描述:告诉AI能调用什么能力 -
• 提示词模式:控制提示词的详细程度,适配不同代理场景 -
• 项目上下文文件:注入项目专属的规则、人格、文档 -
• 沙箱环境信息:明确AI的运行环境与权限边界 -
• 授权用户列表:划定AI的服务对象与权限 -
• 模型提供商自定义注入:适配不同大模型的专属要求 -
• 技能与心跳提示:补充AI的专属能力与实时状态
而它的内部执行流水线,一步都不能乱:
-
1. 预处理所有参数,规范化工具名,解析模型提供商的自定义要求 -
2. 最简模式短路:如果是none模式,直接返回最简身份声明,零多余开销 -
3. 按固定顺序拼装各个模块:工具规则、安全规则、CLI规则、技能、记忆、项目文档、沙箱信息 -
4. 插入缓存边界标记,拆分稳定内容与动态内容 -
5. 拼接动态上下文文件与额外提示内容 -
6. 添加运行时元信息行,明确当前环境的所有参数 -
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,拼到最后,拼的从来都不是谁的提示词写得更花哨。而是谁的工程化能力更强,谁的架构更稳定,谁的细节更到位。
夜雨聆风