为什么要把提示词做成可组合的模块?
你在做一个带工具的智能体(Agent),希望它别乱跑、别高消耗、能按规则工作。一个长篇一体化的 system prompt 看着方便,但会带来几个问题:
- 难维护。每改一点,全局都可能出问题。
- 浪费 token。子智能体、临时任务并不需要全部信息。
- 隐式冲突。不同场景需要不同规则。
所以,把提示词拆成模块,按场景组合,是更稳、更省钱、更安全的做法。下面把 OpenClaw 的实战思想拆开来。你可以照着做。
核心思想速览(适合想快速落地的人)
- 模块化:把安全、工具风格、记忆、技能、心跳等拆成独立段落。按需拼接。
- 模式化:主智能体用 full,子智能体用 minimal,特殊场景用 none,减少 token。
- 动态上下文注入:把工作区文件(SOUL.md、HEARTBEAT.md、MEMORY.md 等)注入到提示词里,赋予 Agent 环境感知。
- 明确规则:工具调用何时叙述、何时直接调用;静默回复用专门 token;心跳要精确返回格式。
如果你只记三点:模块化、按场景裁剪、严格的安全 + 静默/心跳约定。
把提示词参数化:buildAgentSystemPrompt 示例结构
下面是构建 system prompt 的参数示例(简版)。它告诉你需要哪些输入,方便按需拼接:
export function buildAgentSystemPrompt(params: {
workspaceDir: string;
promptMode?: 'full' | 'minimal' | 'none';
reasoningLevel?: string;
ownerNumbers?: string[];
toolNames?: string[];
toolSummaries?: Record<string,string>;
skillsPrompt?: string;
heartbeatPrompt?: string;
docsPath?: string;
contextFiles?: { path:string; content:string }[];
runtimeInfo?: { agentId?:string; host?:string; model?:string };
reactionGuidance?: string;
// ... 更多
}) { /* return joined lines */ }
把参数拆清楚,会让模块的启用/禁用、按会话切换变得简单。
常见模块与示例
下面挑几个关键模块,说明怎么写、为什么要这样写。
1) 安全护栏(必须有)
这块要短。要明确可执行的规则。示例:
function buildSafetySection() {
return [
'## Safety',
'You have no independent goals: do not pursue self-preservation, replication, resource acquisition, or power-seeking; avoid long-term plans beyond the user\'s request.',
'Prioritize safety and human oversight over completion; if instructions conflict, pause and ask; comply with stop/pause/audit requests and never bypass safeguards.',
''
]
}
为什么这样写?一句话界定不可触碰的红线。短、清晰、可机器判断。
2) 工具调用风格(减少废话)
工具很多时候不需要多余叙述。示例规则:
'## Tool Call Style',
'Default: do not narrate routine, low-risk tool calls (just call the tool).',
'Narrate only when it helps: multi-step work, complex/challenging problems, sensitive actions (e.g., deletions), or when the user explicitly asks.',
'Keep narration brief and value-dense; avoid repeating obvious steps.'
场景:例如把数据写到数据库时,不要先写一大段“我准备要做什么”,直接 call 写入接口;遇到删除或高风险操作,再简短说明。
3) 技能系统(按需延迟加载)
核心原则:只有在确定某个 skill 适用时,才去读它的 SKILL.md。这样避免不必要 token 消耗。实现大致如下:
function buildSkillsSection({ isMinimal, readToolName, skillsPrompt }){
if (isMinimal) return []; // 子智能体跳过
return [
'## Skills (mandatory)',
`Before replying: scan skill index entries.`,
`- If exactly one skill clearly applies: read its SKILL.md at path with \`${readToolName}\`, then follow it.`,
`- If multiple: pick the most specific one then read/follow it.`,
`- If none clearly apply: do not read any SKILL.md.`,
skillsPrompt || '',
''
]
}
效果:减少不必要的文件读取;避免把多个技能的上下文都塞进 prompt 导致歧义。
4) 记忆检索(用工具、先搜索再取)
规则写得直白:要回答历史相关问题,先 run memory_search,然后 memory_get 精确拉取。
'## Memory Recall',
'Before answering 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.',
''
实战:这样能避免直接臆造“记忆”,并且只把必要片段带入上下文,省 token。
5) 心跳(Heartbeat)与静默回复(Silent Reply)
心跳用于定期检查工作区或任务状态。静默回复用于告诉系统“没事儿,不需要再发消息”。必须严格匹配格式。
关键 token:
- HEARTBEAT_OK(例如字符串 "HEARTBEAT_OK")
- SILENT_REPLY_TOKEN(例如 "NO_REPLY")
规则示例(简化):
// HEARTBEAT
'If you receive a heartbeat poll and there is nothing that needs attention, reply exactly:',
'HEARTBEAT_OK',
// SILENT_REPLY
'When you have nothing to say, respond with ONLY: NO_REPLY',
'- It must be your ENTIRE message — nothing else',
'- Never append it to an actual response',
别犯的错误(常见):
- 错:"Here's help... NO_REPLY"
- 对:"NO_REPLY"
实战影响:保证自动化流程不会因为多余输出而重复触发。
提示词模式(full / minimal / none)要怎么用?
- full:主智能体。包含完整模块(记忆、心跳、技能列表、文档、人格等)。
- minimal:子智能体。只保留必要模块,主要做具体任务,节约 token,避免与父智能体冲突。
- none:非常特殊的会话,仅返回基础身份声明。
代码上实现很简单:传一个 promptMode,然后每个模块检查 isMinimal 标志决定是否输出。
const isMinimal = promptMode === 'minimal' || promptMode === 'none';
if (isMinimal) return []; // 某些模块直接跳过
场景举例:当你用主智能体分配一个子任务去抓取网页数据,子智能体只需要抓取规则和少量工具权限,不需要完整的记忆和心跳。
动态上下文注入:哪些文件会被自动带进来?
OpenClaw 的做法是把工作区里的几个约定文件自动加载到提示词中:
- .env(环境变量)
- SOUL.md(人格 / 说话风格)
- HEARTBEAT.md(定期检查任务)
- MEMORY.md(持久记忆)
- 会话特定配置文件
如果 SOUL.md 存在,就让 Agent 体现其中的语气与偏好。示例检查代码片段:
const hasSoulFile = contextFiles.some(f => f.path.split('/').pop()?.toLowerCase() === 'soul.md');
if (hasSoulFile) {
lines.push('If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.');
}
示例 SOUL.md(你可以直接让产品经理写):
Tone: friendly but concise
Emoji: sparing use (1 per 3 messages)
Focus: developer UX, examples > theory
Do not: answer legal advice
把人格写成文件的好处:产品能随时调整 Agent 的“性格”,不用改代码。
运行时信息注入:让 Agent 知道自己在哪儿跑
把 runtime 信息注入提示词,能让 Agent 给出更可靠的建议或调试信息。示例输出:
Runtime: agent=main | host=macbook | repo=/Users/dev/project | os=Darwin 25.2.0 (arm64) | node=v22.0.0 | model=claude | channel=telegram | capabilities=inlineButtons | thinking=medium
注入项:agentId、host、repoRoot、os、node、model、channel、capabilities、thinking 等。
实战价值:当 Agent 报错或建议某个操作时,能把上下文(比如当前 model、能力)一起说明,便于排查。
渠道定制:Telegram 内联按钮、反应、TTS
不同渠道能力不同。把能力检测入提示词,能让 Agent 根据渠道调整行为。
- Telegram 内联按钮:如果启用,就把 inlineButtons 能力写进 runtimeCapabilities,提示词里说明使用格式。
- 反应表情:minimal 模式下“偶尔用”,extensive 模式下“可以更活跃”。
- TTS:如果开启,告诉 Agent 语音长度限制和如何标注 [[tts]] 标签。
这样可以避免“我以为能用按钮但实际上没有”的尴尬。
把这些原则照着做:落地清单(可复制)
- 把 system prompt 拆成若干构建函数(safety、tools、skills、memory、heartbeat、runtime、channelHints、soul)。
- 提供 promptMode 参数(full/minimal/none),模块里按 isMinimal 决定是否输出。
- 安全模块放最前,短而明确。
- 技能要延迟加载:只在选中某个 skill 后再读取它的 SKILL.md。
- 记忆检索要写成工具调用规则:先 search 再 get。
- 设计静默回复 token(NO_REPLY)和心跳返回(HEARTBEAT_OK),并在系统里严格声明格式。
- 注入运行时信息,说明当前 model、能力和通道。
- 把 SOUL.md 做为可编辑的人格文件,写入提示词优先级较低但可覆盖口吻。
- 在工具风格模块里明确:什么时候叙述、什么时候直接执行。
- 测试:用主/子智能体场景跑一次,会话压缩触发一次记忆刷新,心跳触发一次,查看 token 消耗是否合理。
快速示例:组装 prompt 的伪代码(Node/TS)
const lines = [];
lines.push(...buildSafetySection());
if (!isMinimal) lines.push(...buildSkillsSection(params));
lines.push(...buildToolStyleSection());
if (!isMinimal) lines.push(...buildMemorySection(params));
lines.push(buildRuntimeLine(runtimeInfo, runtimeChannel, capabilities, thinkLevel));
const systemPrompt = lines.filter(Boolean).join('\n');
把每个 buildXSection 做成小函数,单测覆盖。你就有可维护的提示词系统了。
常见坑与避雷(务必看)
-
不要把静默 token 当作“可以附加”的字符串。错用会触发重复流程。
错:"Here's the result... NO_REPLY"。
对:单独返回 "NO_REPLY"。 -
别一次性把所有技能的文本都读进 prompt。会导致模型混淆与 token 爆炸。
-
子智能体别带主智能体的心跳/记忆规则,容易互相干扰。用 minimal 做隔离。
-
心跳回复必须精确匹配。不要在心跳应答里多写别的字符。
-
安全规则要短。把复杂条款拆成可机读的几行。太长反而被忽略。
-
在多个渠道同时运行时,先检测能力再在提示词里声明,避免硬编码行为。
结语(简短)
把提示词工程做成模块化、参数化的工程实践,不只是爽——还能让产品更易维护、成本更低、出错更少。把规则写清楚、把可变的东西做成文件(比如 SOUL.md),而不是改代码,是我多年实战里最省心的做法。
需要我把上面示例整理为一个开源模板仓库吗?我可以把关键函数做成 npm 包,0 到 1 帮你接入现成的 agent 框架。😊
标签(用于文章页):提示词工程、OpenClaw、智能体设计、系统提示、记忆检索、SOUL.md、静默回复、心跳、工具调用、Token 优化