Blog /

别再写死你的系统提示了——从 672 行代码中提炼出的 Prompt 组装模式

从 OpenClaw 的系统提示词构建器中,提炼出模块化、条件渲染的 Prompt 组装框架

你的系统提示词有多少行?如果超过 100 行,你大概率在用字符串拼接维护它。然后某天你加了一个功能,提示词崩了,你花了 2 小时排查——原来是少了一个换行符。

问题

在前两篇文章里,我从 OpenClaw 提炼出了 LLM 容错层上下文管理三级防线。这次我要看的是另一个核心文件——system-prompt.ts,672 行。

这个文件只做一件事:组装系统提示词

672 行,就为了生成一段 system prompt。

你可能觉得夸张。但当你的 AI 应用有 10+ 种工具、3 种运行模式、动态上下文文件、条件性的技能描述、运行时环境信息……你的系统提示词就不再是一个字符串常量了。它是一个需要被编排的复杂输出

大多数人是怎么做的:

const systemPrompt = `You are a helpful assistant.

${tools.length > 0 ? `## Tools\n${tools.map(t => `- ${t.name}`).join('\n')}` : ''}

${isAdvanced ? 'You have access to advanced features.' : ''}

${contextFiles.map(f => `## ${f.path}\n${f.content}`).join('\n\n')}

${runtime ? `Runtime: os=${runtime.os} model=${runtime.model}` : ''}

Be concise and helpful.`

看起来还行?等你加到第 15 个条件分支的时候再说。

这种写法的三个致命问题:

  1. 不可维护 — 嵌套的三元表达式、模板字符串、换行控制,改一处崩三处
  2. 不可测试 — 整个提示词是一个巨大的表达式,你没法单独测试”工具列表部分是否正确”
  3. 不可复用 — 每个项目从头写一遍,重复造轮子

OpenClaw 的做法:Section Builder 模式

OpenClaw 的 672 行 system-prompt.ts 不是一个巨大的模板字符串。它是20+ 个独立的 section(8 个独立 builder 函数 + 十余个内联条件块),每个 section 负责提示词的一个部分:

buildAgentSystemPrompt(params)

    ├─ identity section         → "You are Claude Code..."
    ├─ tool list section        → "## Tools\n- read\n- exec\n..."
    ├─ tool documentation       → 每个工具的详细用法
    ├─ context files section    → CLAUDE.md、.cursorrules 等
    ├─ skills section           → 可用的技能描述
    ├─ memory section           → 持久化记忆内容
    ├─ git status section       → 当前 git 状态
    ├─ runtime info section     → 操作系统、模型、shell 信息
    ├─ ...(还有十余个 sections)

    └─ 合并 → filter(Boolean) → join("\n")

每个 section builder 都是一个独立函数,接收上下文参数,返回 string[](行数组)或空数组。

// OpenClaw 中的一个 section builder(简化)
function buildToolListSection(tools: Tool[]): string[] {
  if (tools.length === 0) return []  // 没有工具?跳过这个 section

  const seen = new Set<string>()
  const lines = ['## Tools']

  for (const tool of tools) {
    const key = tool.name.toLowerCase()
    if (seen.has(key)) continue  // 去重
    seen.add(key)
    lines.push(`- ${tool.name}: ${tool.summary}`)
  }

  return lines
}

注意三个关键设计:

  1. 返回空数组 = 跳过 — 不需要外层 if/else 判断
  2. 返回行数组 — 最终由框架 join,不需要手动管理换行
  3. 独立函数 — 可以单独测试

这就是我要提炼的模式。

提炼:三个概念

从 OpenClaw 672 行中剥离掉 20+ 个具体 section 和 OpenClaw 特有逻辑后,剩下的通用框架只有三个概念:

1. Section — 提示词的积木块

每个 section 有三种方式提供内容:

// 静态内容 — 永不变化的部分
{ name: 'identity', content: 'You are a helpful assistant.' }

// 动态 builder — 根据上下文生成
{ name: 'tools', builder: (ctx) => ctx.tools.map(t => `- ${t.name}`) }

// 条件渲染 — 满足条件才包含
{
  name: 'advanced',
  content: 'You have access to advanced features.',
  when: (ctx) => ctx.isAdvanced,
}

静态内容用于固定不变的部分(身份、基本规则)。动态 builder 用于需要根据运行时数据生成的部分(工具列表、上下文文件)。条件渲染用于”有时需要有时不需要”的部分(高级功能、调试信息)。

2. Assembler — 编排器

把所有 sections 按顺序处理,合并成最终的 prompt:

sections.forEach(section => {
  if (section.when && !section.when(ctx)) → 跳过
  if (section.builder) → 执行 builder
  else → 使用静态 content
  收集结果
})
→ join(separator)
→ 最终 prompt string

3. Section Helpers — 可复用的格式化函数

OpenClaw 中有几个 section builder 的逻辑在所有 LLM 应用中通用:

  • 工具列表格式化 — 几乎所有 Agent 都需要告诉 LLM “你有哪些工具”
  • 上下文文件注入 — CLAUDE.md、.cursorrules 这类项目配置文件
  • 运行时信息 — OS、模型名称、Node 版本等环境信息

这些可以直接提取为通用 helper。

对比:改造前后

用一个真实场景来对比。假设你在做一个 AI 编码助手,系统提示词需要包含:身份、工具列表、项目文件、运行时信息、以及可选的高级功能描述。

改造前:模板字符串地狱

function buildSystemPrompt(
  tools: Tool[],
  files: File[],
  runtime: Runtime,
  isMinimal: boolean,
): string {
  let prompt = 'You are a coding assistant.\n'

  if (tools.length > 0) {
    prompt += '\n## Tools\n'
    const seen = new Set<string>()
    for (const tool of tools) {
      const key = tool.name.toLowerCase()
      if (!seen.has(key)) {
        seen.add(key)
        prompt += tool.summary
          ? `- ${tool.name}: ${tool.summary}\n`
          : `- ${tool.name}\n`
      }
    }
  }

  if (files.length > 0) {
    for (const file of files) {
      prompt += `\n## ${file.path}\n\n${file.content}\n`
    }
  }

  if (!isMinimal) {
    prompt += '\nYou have access to advanced features.\n'
  }

  const runtimeParts: string[] = []
  if (runtime.os) runtimeParts.push(`os=${runtime.os}`)
  if (runtime.model) runtimeParts.push(`model=${runtime.model}`)
  if (runtimeParts.length > 0) {
    prompt += `\nRuntime: ${runtimeParts.join(' ')}\n`
  }

  prompt += '\nBe concise. Follow best practices.'

  return prompt
}

40 行,而且只有 5 个 section。想象一下 20+ 个 section 时的样子。

改造后:Section Builder 模式

import {
  createPromptAssembler,
  formatToolList,
  formatContextFiles,
  formatRuntimeInfo,
} from '@yuyuqueen/prompt-assembler'

type MyContext = {
  tools: ToolEntry[]
  files: ContextFile[]
  runtime: RuntimeInfo
  isMinimal: boolean
}

const prompt = createPromptAssembler<MyContext>({
  sections: [
    { name: 'identity', content: 'You are a coding assistant.' },
    {
      name: 'tools',
      builder: (ctx) => formatToolList(ctx.tools),
      when: (ctx) => ctx.tools.length > 0,
    },
    {
      name: 'context',
      builder: (ctx) => formatContextFiles(ctx.files),
      when: (ctx) => ctx.files.length > 0,
    },
    {
      name: 'advanced',
      content: 'You have access to advanced features.',
      when: (ctx) => !ctx.isMinimal,
    },
    {
      name: 'runtime',
      builder: (ctx) => formatRuntimeInfo(ctx.runtime),
    },
    { name: 'rules', content: 'Be concise. Follow best practices.' },
  ],
})

// 一行调用
const systemPrompt = prompt.build({
  tools: [...],
  files: [...],
  runtime: { os: 'Darwin', model: 'claude-opus-4' },
  isMinimal: false,
})

同样的功能,但每个 section 的边界清清楚楚。加一个 section?加一行。删一个?删一行。改条件?改 when。不用在 40 行的字符串拼接里翻找。

附:通用 Section Checklist

从 OpenClaw 的 20+ 个 section 中,有 11 个是任何 LLM Agent 都可以借鉴的通用模式(其余是 OpenClaw 的产品特有逻辑,如消息路由、心跳检测等)。搭建你自己的 Agent system prompt 时,可以参考这个清单:

Section作用适用场景
Identity角色定义(“你是一个…”)所有 Agent
Tooling工具列表 + 摘要,自动去重有工具调用的 Agent
Tool Call Style何时解释操作、何时静默执行有工具调用的 Agent
Safety安全护栏(不自主扩权、不绕过审查)所有 Agent
Memory Recall回答前先搜记忆库有持久记忆的 Agent
Workspace工作目录声明文件/编码类 Agent
User Identity用户身份和偏好个性化 Agent
Date & Time时区和当前时间时间敏感的 Agent
Context Files项目配置文件注入(CLAUDE.md 等)编码/项目类 Agent
RuntimeOS、模型、Node 版本等环境快照所有 Agent
Reasoning Formatthinking tag 格式控制使用推理模型时

不是每个 Agent 都需要全部 11 个——根据你的场景按需选取。但如果你在做一个 coding agent 或 AI 助手,大概率需要其中 7-8 个。

你的 LLM 应用今天就能用上

npm install @yuyuqueen/prompt-assembler

GitHub: github.com/yuyuqueen/llm-toolkit — Star

核心 API

import { createPromptAssembler } from '@yuyuqueen/prompt-assembler'

const prompt = createPromptAssembler({
  sections: [
    // 静态
    { name: 'identity', content: 'You are a helpful assistant.' },
    // 动态
    { name: 'tools', builder: (ctx) => [`Tools: ${ctx.toolCount}`] },
    // 条件
    { name: 'debug', content: 'Debug mode on.', when: (ctx) => ctx.debug },
  ],
  separator: '\n',  // section 间的分隔符
})

const result = prompt.build({ toolCount: 5, debug: true })

内置 Section Helpers

三个从 OpenClaw 提取的通用格式化函数:

import {
  formatToolList,
  formatContextFiles,
  formatRuntimeInfo,
} from '@yuyuqueen/prompt-assembler'

// 工具列表(自动去重,大小写不敏感)
formatToolList([
  { name: 'read', summary: 'Read file contents' },
  { name: 'Read', summary: 'Duplicate' },  // 被去重
  { name: 'exec', summary: 'Run commands' },
])
// → ["## Tools", "- read: Read file contents", "- exec: Run commands", ""]

// 上下文文件
formatContextFiles([
  { path: 'CLAUDE.md', content: '# Project\nRules here.' },
])
// → ["## CLAUDE.md", "", "# Project\nRules here.", ""]

// 运行时信息(自动过滤 undefined)
formatRuntimeInfo({
  os: 'Darwin',
  model: 'claude-opus-4',
  node: undefined,  // 被过滤
})
// → ["Runtime: os=Darwin model=claude-opus-4"]

调试与 Token 估算

// 逐 section 查看输出(调试用)
const sections = prompt.buildSections(ctx)
for (const [name, content] of sections) {
  console.log(`[${name}] ${content.length} chars`)
}
// → [identity] 28 chars
// → [tools] 156 chars
// → [context] 2340 chars

// 估算 token 数
const tokens = prompt.estimateTokens(ctx)
console.log(`System prompt ≈ ${tokens} tokens`)

buildSections 返回 Map<string, string>,让你精确知道每个 section 贡献了多少内容。当提示词过长时,你可以快速定位是哪个 section 太大了——而不是在几百行的字符串里 ctrl+F。

配合 resilient-llm 和 llm-context-kit

三个库组合,就是完整的 LLM 应用基础设施:

import { createPromptAssembler } from '@yuyuqueen/prompt-assembler'
import { createContextBudget } from '@yuyuqueen/llm-context-kit'
import { createResilientLLM } from '@yuyuqueen/resilient-llm'

// 1. 组装系统提示词
const prompt = createPromptAssembler({ sections: [...] })
const systemPrompt = prompt.build(ctx)

// 2. 检查 token 预算(系统提示词也要算进去)
const budget = createContextBudget({ contextWindowTokens: 200_000 })
const status = budget.check(messages)  // messages 包含系统消息

// 3. 容错调用
const resilient = createResilientLLM({ providers: [...] })
await resilient.call(async (rCtx) => {
  return {
    response: await anthropic.messages.create({
      model: rCtx.model,
      system: systemPrompt,
      messages,
    }),
  }
})
系统提示词组装 (prompt-assembler)


上下文管理 (llm-context-kit)
    │ 预算检查 → 工具截断 → 会话压缩

容错调用 (resilient-llm)
    │ Key 轮换 → Provider 降级 → 指数退避

LLM API

改造前后

场景改造前改造后
加一个 section在 40 行模板字符串中找位置插入加一行 section 定义
删一个 section小心翼翼删除代码和换行删一行或加 when: () => false
测试某个 section无法单独测试buildSections(ctx).get('tools')
查看 token 分布手动计算estimateTokens(ctx) + buildSections
条件渲染嵌套三元表达式when: (ctx) => ctx.condition
工具列表去重手写 Set 去重逻辑formatToolList(tools) 内置
多人协作冲突地狱(同一文件同一函数)每人改自己的 section

设计原则

和前两个库一样:

  • 零依赖 — 纯 TypeScript,无运行时依赖
  • 泛型上下文createPromptAssembler<YourContext> 提供完整类型安全
  • Provider-agnostic — 输出纯字符串,不绑定任何 LLM SDK
  • 可组合 — section helpers 可独立使用,也可组合到 assembler 中

结论

系统提示词不是一个字符串,它是一个需要被工程化管理的产品。

当你的提示词超过 50 行,你需要 section 化。当你的提示词有条件分支,你需要条件渲染。当你的提示词有动态数据,你需要 builder 模式。

OpenClaw 用 672 行代码管理它的系统提示词,因为一个好的 AI 产品的提示词就是这么复杂。你不需要写 672 行——但你需要一个框架来管理这个复杂度。

@yuyuqueen/prompt-assembler on npm@yuyuqueen/llm-context-kit on npm@yuyuqueen/resilient-llm on npmGitHub 源码


这篇文章是「从开源项目中提炼工具库」系列的第三篇(完结)。

关注我获取更新 → Twitter @YuYuQueen_ · GitHub

评论