system-prompt:提示词是怎么组装出来的

专栏:DeepSeek Harness · 第 8 / 14 篇
DeepSeek Harnesssystem-prompt源码

上一篇的 pre-step 里有一步 systemPrompt.assemble(...),本篇就拆它。system prompt 在多数项目里是一段写死的长字符串,在 dsh 里它是注册表 + 确定性流水线——因为工具 schema、身份文案、策略句子来自几十个互不相识的插件,必须有一套机制把它们合成一份稳定、可缓存、可复现的文本。

四种注册

ctx.systemPrompt 服务接受四种贡献:

注册内容去向
section()提示词片段(静态文本或求值函数)system 文本
context()动态运行时上下文(时间、工作区、策略句子)不进 system,作为带来源的 user 消息注入历史
tools(provider)工具 schema 提供方请求的 tools wire 字段
variable(name, provider){{变量}} 求值器渲染时插值

所有注册都支持全局层与 agent 作用域层(后者遮蔽同名全局项)。片段接口:

// packages/core/system-prompt/src/index.ts:52-74
export interface PromptSection {
  /** Unique name — a duplicate registration throws. */
  readonly name: string
  /** Sections are concatenated in ascending order. Equal orders use code-unit name order. */
  readonly order: number
  /** Static text or a provider evaluated at each assembly. The text may
   * reference `{{variable}}`s — interpolated later, by {@link renderPrompt}. */
  readonly text: string | ((context: AssembleContext) => string)
  /**
   * Treat this contribution as the complete system prompt. Assembly still
   * runs the cooperative waterfall …, then restores this exact section as
   * the sole prompt section. More than one effective complete section makes
   * assembly fail.
   */
  readonly complete?: boolean
}

集中分配的稀疏 order

顺序不是「谁先注册谁在前」(那会随插件加载顺序漂移),而是集中分配的具名稀疏 order——数字之间留出大量空隙,新插件可以插进任何缝隙而不动别人:

// packages/core/system-prompt/src/index.ts:121-152(节选)
const SECTION_ORDERS = {
  HARNESS_IDENTITY: -1000,   // "You are an AI agent powered by DeepSeek Harness."
  HARNESS_SOURCE: -900,
  DEPLOYMENT_PERSONA: 0,     // 部署 persona(可配置)
  PLAN_POLICY: 500,
  FILE_REFERENCE: 900,
  TOOL_BASH: 1000,
  TOOL_READ: 1100,
  TOOL_WRITE: 1200,
  TOOL_EDIT: 1300,
  TOOL_WEB_SEARCH: 2000,
  TOOL_LSP: 2200,
  TOOL_SUBAGENT: 2800,
  TOOLS_SDK: 5000,
  STRUCTURED_OUTPUT: 9900,
} as const

排序是确定性的:order 升序,同号按名称代码单元序——注册顺序不影响最终文本,这是前缀缓存可复用的前提之一。

组装流水线

assemble() 的完整步骤:

图表(system-prompt-assembly.md)

waterfall 环节给了插件最后一道改写机会(比如 plan-mode 插件注入协作策略段)。流水线主体:

// packages/core/system-prompt/src/index.ts:573-611(节选)
const sectionDefinitions = [...sectionByName.values()].sort(comparePromptSections)
const completeSections = sectionDefinitions.filter(section => section.complete === true)
if (completeSections.length > 1) {
  throw new Error(`multiple complete prompt sections are active: ...`)
}
let completeSection: AssembledSection | undefined
const sections = sectionDefinitions.map((section) => {
  const assembled = {
    name: section.name,
    text: typeof section.text === 'function' ? section.text(context) : section.text,
  }
  if (section.complete === true) completeSection = { ...assembled }
  return assembled
})
const assembly: PromptAssembly = {
  sections,
  contexts: runtimeContextSuppressed ? [] : [...contextByName.values()]
    .sort((a, b) => a.order - b.order)
    .map(entry => ({
      name: entry.name,
      text: typeof entry.text === 'function' ? entry.text(context) : entry.text,
    })),
  tools: orderTools(collected, this.toolOrder, knownNames),
  variables,
}
const transformed = await this.ctx.waterfall(
  scopeTarget(this, scope), 'system-prompt/assemble', assembly, context,
  () => Promise.resolve(assembly),
)

关键设计:动态内容不进 system

如果每次请求都把「当前时间」「工作区文件列表」拼进 system 文本,前缀缓存就废了。dsh 的做法:动态上下文是独立的 context() 注册,组装成快照后,只有当快照发生变化时才作为一条带来源的 user/message 追加到历史末尾(source 标明来自哪个插件)。system 文本因此保持逐字节稳定。

配合第 6 篇的 request/header(记录渲染后的 system 与工具 schema),每次请求都能精确重建;前缀一致即 KV cache 可复用。dsh 甚至为策略句子准备了固定文案(如审批策略的 ask/never 各一句英文),追加在保留历史之后——一切为了前缀。

工具 schema:同一组装的另一半

「模型知道自己能用什么工具」与提示词是同一份组装结果的两个出口:assembly.sections → system 文本assembly.tools → 请求的 tools 字段。每个 agent 看到的工具子集由工具提供方返回的 ToolProviderResult.schemas 决定,toolOrder 配置再统一排序(未列出的工具按字典序插到标记位)。toolOrder 形状错误在加载时报错、未知工具名在组装时报错——防止静默漂移。

一次 assemble 的时序

图表(system-prompt-assembly.md)

与其他模块的联系

  • agent-loop(第 7 篇)是唯一的调用方:pre-step 每步 assemble 一次;
  • tools(第 9 篇):ToolRuntime 构造时向 systemPrompt 注册工具 schema 提供方——工具与提示词是同一组装的两个出口;
  • session(第 6 篇):request/header 落日志记录渲染后的 system 与工具清单,前缀一致性可审计;
  • 缓存:片段确定性排序 + 动态上下文走消息尾部,是第 6 篇 KV cache 纪律的实现。

写你自己的插件时(第 14 篇),给模型「讲规则」的正确姿势就是注册一个 section;而每次都可能变的内容请注册 context——前者图稳定缓存,后者图新鲜。用错位置要么缓存失效,要么信息陈旧。

循环的两大输入(prompt、tools)已经就位。下一篇看第三个部件:工具调用的把关流水线。

← 返回文章列表