动机:为什么 System Prompt 需要动态构造
你可能以为 system prompt 就是一段写死的文本:“You are a helpful assistant.”
但真实的 agent 完全不是这样。OpenClaw 的 src/agents/system-prompt.ts 有 689 行,运行时根据 PromptMode(full / minimal / none)动态组装不同模块。
为什么要动态?因为 agent 需要在 prompt 里知道:
- 现在几点(影响”刚刚""昨天”这些概念)
- 工作区在哪(文件路径要正确)
- 有没有 AGENTS.md / CLAUDE.md(项目级指令)
- 当前操作系统(bash 命令的差异)
OpenClaw 的 src/agents/workspace.ts 定义了一整组 bootstrap 文件常量:AGENTS.md、SOUL.md、TOOLS.md、IDENTITY.md、USER.md、MEMORY.md——这些文件在每次 agent 启动时被加载并注入 system prompt。
组装流程如下:
Identity 和 Runtime 每次必定存在,AGENTS.md、Memory、Skills 是条件加载——文件存在才注入。各部分用分隔符拼接成最终的 system prompt。
Ground Truth:真实的 System Prompt 构造
System Prompt 构造——跨代码库对比
动态 system prompt 在不同代码库中的实现方式
1class ContextBuilder:2 """Build system prompt from workspace state."""34 BOOTSTRAP_FILES = [5 "AGENTS.md", "SOUL.md", "USER.md",6 "TOOLS.md", "IDENTITY.md",7 ]89 def build_system_prompt(self) -> str:10 parts = [self._get_identity()]1112 bootstrap = self._load_bootstrap_files()13 if bootstrap:14 parts.append(bootstrap)1516 memory = self.memory.get_memory_context()17 if memory:18 parts.append(f"# Memory\n\n{memory}")1920 skills = self.skills.get_always_skills()21 if skills:22 parts.append(skills_content)2324 return "\n\n---\n\n".join(parts)
1class DefaultAgent:2 def _render_template(self, template: str) -> str:3 return Template(4 template,5 undefined=StrictUndefined,6 ).render(**self.get_template_vars())78 def get_template_vars(self, **kwargs) -> dict:9 return recursive_merge(10 self.config.model_dump(),11 self.env.get_template_vars(),12 self.model.get_template_vars(),13 {"n_model_calls": self.n_calls,14 "model_cost": self.cost},15 self.extra_template_vars,16 kwargs,17 )
1// system-prompt.ts: PromptMode 控制内容级别2type PromptMode = "full" | "minimal" | "none";3// full = 主 agent, minimal = sub-agent, none = 最简45// workspace.ts: Bootstrap 文件常量6const DEFAULT_AGENTS_FILENAME = "AGENTS.md";7const DEFAULT_SOUL_FILENAME = "SOUL.md";8const DEFAULT_TOOLS_FILENAME = "TOOLS.md";9const DEFAULT_IDENTITY_FILENAME = "IDENTITY.md";10const DEFAULT_USER_FILENAME = "USER.md";11const DEFAULT_MEMORY_FILENAME = "MEMORY.md";1213// system-prompt.ts: 按模块组装14function buildSkillsSection(params) { ... }15function buildMemorySection(params) { ... }16function buildUserIdentitySection(ownerLine) { ... }1718// bootstrap-files.ts: 加载并过滤19async function resolveBootstrapFilesForRun(params) {20 const rawFiles = await loadWorkspaceBootstrapFiles(workspaceDir);21 const filtered = filterBootstrapFilesForSession(rawFiles, sessionKey);22 return sanitizeBootstrapFiles(filtered);23}
关键观察:
- nanobot 从工作区加载 AGENTS.md 等文件——项目级指令注入
- mini-swe-agent 用 Jinja2 模板——运行时变量注入
- OpenClaw 用 PromptMode 区分主 agent(full)和 sub-agent(minimal),按模块组装 system prompt
- 所有实现都是拼接多个部分,不是一段写死的文本
构建:ContextBuilder
从 s02 到 s03 的变化:
- # s02: 硬编码的 system prompt- MODEL = "claude-sonnet-4-20250514"- SYSTEM = "You are a helpful assistant."+ # s03: 动态构造 system prompt+ class ContextBuilder:+ IDENTITY = "You are a helpful assistant..."- def agent_loop(query, registry):+ def build_system_prompt(self) -> str:+ parts = [self.IDENTITY]+ parts.append(self._runtime_context())+ memory = self._load_workspace_memory()+ if memory:+ parts.append(memory)+ parts.append(self._tool_hint())+ return "\n\n---\n\n".join(parts)++ def agent_loop(query, registry, context):+ system = context.build_system_prompt()messages = [{"role": "user", "content": query}]while True:response = client.messages.create(- model=MODEL, system=SYSTEM,+ model=MODEL, system=system,messages=messages, tools=...,)
ContextBuilder 的三个组成部分:
- Identity——静态,定义 agent 的身份和基本行为
- Runtime Context——动态,注入当前时间、OS、工作区路径
- Workspace Memory——条件加载,读取 AGENTS.md / CLAUDE.md
class ContextBuilder:
def _runtime_context(self) -> str:
now = datetime.now().strftime("%Y-%m-%d %H:%M (%A)")
return f"""## Runtime Context
- Current time: {now}
- OS: {platform.system()} {platform.machine()}
- Workspace: {self.workspace.resolve()}"""
def _load_workspace_memory(self) -> str:
for filename in ["AGENTS.md", "CLAUDE.md"]:
path = self.workspace / filename
if path.exists():
return f"## Workspace Memory\n\n{path.read_text()}"
return ""
测试:验证动态 Prompt
创建一个 AGENTS.md 文件,验证 agent 能读取项目级指令:
echo "## Rules\n- Always use Python 3.12\n- Write type hints" > AGENTS.md
python agent.py "What Python version should I use?"
预期结果:agent 的 system prompt 包含 AGENTS.md 内容,回答会引用 “Python 3.12”。
变更内容
| 组件 | 之前 (s02) | 之后 (s03) |
|---|---|---|
| System Prompt | 硬编码字符串 | ContextBuilder 动态构造 |
| 运行时信息 | 无 | runtime 注入 (时间、OS、路径) |
| 项目指令 | 无 | AGENTS.md 自动加载 |
本课代码: agents/s03_system_prompt.py — 140 行 (新增 10 行)
试一试
cd public/code
echo '## Rules\n- Use Python 3.12' > AGENTS.md && python agents/s03_system_prompt.py "What Python version?"
可以尝试的提示:
- 创建
AGENTS.md写入规则,然后问 agent 相关问题 - 不创建
AGENTS.md,对比 agent 的行为差异 - 问 agent “现在几点?在哪个目录?“——验证 runtime 注入
距离生产
模块化组装: OpenClaw 的 689 行 system prompt
OpenClaw 的 system-prompt.ts 有 689 行,但它不是一个巨大的字符串——它是由多个构建函数组装的:buildSkillsSection()、buildMemorySection()、buildUserIdentitySection() 等。每个函数负责一个独立的 prompt 模块,最终拼接成完整的 system prompt。
PromptMode 分级:PromptMode 有三个值——full(主 agent,包含所有信息)、minimal(sub-agent,只保留关键指令)、none(最简模式)。这解决了一个实际问题:sub-agent 不需要知道用户的身份信息和所有技能列表,塞太多内容反而会干扰它完成具体子任务。
其他差距
- 无 prompt 缓存——每次都重新构建完整 prompt,生产系统缓存不变的部分以节省 token
- 无 token 预算控制——我们的 prompt 可能超出模型限制,生产系统会动态裁剪
第一性原理思考
System prompt 是所有 agent 行为的”源代码”——它定义了 agent 是谁、会做什么、不会做什么。这就是为什么 prompt engineering 比代码工程更关键:代码 bug 会报错,你能看到 stack trace 去修复;prompt bug 只会让 agent “表现不好”,很难调试。当 agent 做了一个错误决策,你很难判断是模型能力不足还是 prompt 没写清楚。OpenClaw 把 prompt 拆成模块化函数,本质上是在用软件工程的方法(模块化、可测试、可复用)来管理这段”最关键的源代码”。