Phase 1 / The Core Loop / s03

System Prompt

动态 prompt 构造——让 agent 知道自己是谁、在哪里、能做什么

动机:为什么 System Prompt 需要动态构造

你可能以为 system prompt 就是一段写死的文本:“You are a helpful assistant.”

但真实的 agent 完全不是这样。OpenClaw 的 src/agents/system-prompt.ts 有 689 行,运行时根据 PromptMode(full / minimal / none)动态组装不同模块。

为什么要动态?因为 agent 需要在 prompt 里知道:

OpenClaw 的 src/agents/workspace.ts 定义了一整组 bootstrap 文件常量:AGENTS.mdSOUL.mdTOOLS.mdIDENTITY.mdUSER.mdMEMORY.md——这些文件在每次 agent 启动时被加载并注入 system prompt。

组装流程如下:

System Prompt 组装流程ContextBuilder.build()
Identity身份定义Runtime时间/OS/路径AGENTS.md项目指令Memory长期记忆Skills技能目录parts.join("\n\n---\n\n")System Prompt发送给 LLM 的 system 参数if existsif existsif any

Identity 和 Runtime 每次必定存在,AGENTS.md、Memory、Skills 是条件加载——文件存在才注入。各部分用分隔符拼接成最终的 system prompt。

Ground Truth:真实的 System Prompt 构造

System Prompt 构造——跨代码库对比

动态 system prompt 在不同代码库中的实现方式

pythonagent/context.py
1class ContextBuilder:
2 """Build system prompt from workspace state."""
3
4 BOOTSTRAP_FILES = [
5 "AGENTS.md", "SOUL.md", "USER.md",
6 "TOOLS.md", "IDENTITY.md",
7 ]
8
9 def build_system_prompt(self) -> str:
10 parts = [self._get_identity()]
11
12 bootstrap = self._load_bootstrap_files()
13 if bootstrap:
14 parts.append(bootstrap)
15
16 memory = self.memory.get_memory_context()
17 if memory:
18 parts.append(f"# Memory\n\n{memory}")
19
20 skills = self.skills.get_always_skills()
21 if skills:
22 parts.append(skills_content)
23
24 return "\n\n---\n\n".join(parts)
pythonagents/default.py
1class DefaultAgent:
2 def _render_template(self, template: str) -> str:
3 return Template(
4 template,
5 undefined=StrictUndefined,
6 ).render(**self.get_template_vars())
7
8 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 )
typescriptsrc/agents/system-prompt.ts + workspace.ts
1// system-prompt.ts: PromptMode 控制内容级别
2type PromptMode = "full" | "minimal" | "none";
3// full = 主 agent, minimal = sub-agent, none = 最简
4
5// 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";
12
13// system-prompt.ts: 按模块组装
14function buildSkillsSection(params) { ... }
15function buildMemorySection(params) { ... }
16function buildUserIdentitySection(ownerLine) { ... }
17
18// bootstrap-files.ts: 加载并过滤
19async function resolveBootstrapFilesForRun(params) {
20 const rawFiles = await loadWorkspaceBootstrapFiles(workspaceDir);
21 const filtered = filterBootstrapFilesForSession(rawFiles, sessionKey);
22 return sanitizeBootstrapFiles(filtered);
23}

关键观察:

构建:ContextBuilder

从 s02 到 s03 的变化:

s02 → s03: 添加 Dynamic System Promptpython
- # 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 的三个组成部分:

  1. Identity——静态,定义 agent 的身份和基本行为
  2. Runtime Context——动态,注入当前时间、OS、工作区路径
  3. 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?"

可以尝试的提示:

  1. 创建 AGENTS.md 写入规则,然后问 agent 相关问题
  2. 不创建 AGENTS.md,对比 agent 的行为差异
  3. 问 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 不需要知道用户的身份信息和所有技能列表,塞太多内容反而会干扰它完成具体子任务。

其他差距

第一性原理思考

System prompt 是所有 agent 行为的”源代码”——它定义了 agent 是谁、会做什么、不会做什么。这就是为什么 prompt engineering 比代码工程更关键:代码 bug 会报错,你能看到 stack trace 去修复;prompt bug 只会让 agent “表现不好”,很难调试。当 agent 做了一个错误决策,你很难判断是模型能力不足还是 prompt 没写清楚。OpenClaw 把 prompt 拆成模块化函数,本质上是在用软件工程的方法(模块化、可测试、可复用)来管理这段”最关键的源代码”。