首页
/ DeepSeek Harness 中工具 schema 的系统提示词组装机制:PromptAssembly 与 system-prompt/assemble 瀑布事件

DeepSeek Harness 中工具 schema 的系统提示词组装机制:PromptAssembly 与 system-prompt/assemble 瀑布事件

2026-09-03 16:19:42作者:袁立春Spencer

在 DeepSeek Harness(下文简称 dsh)中,「模型被告知它能做什么」被当作一个统一的架构关注点:系统提示词文本与工具 schema 同属一份 PromptAssembly,由 @deepseek-ai/dsh-system-prompt 包集中组装,agent loop 每一步消费一次,再经 system-prompt/assemble 瀑布事件统一拦截。本文基于该架构决策笔记 工具 schema 是系统提示词组装的一部分(英文对照版见 英文版),结合 SystemPrompt 服务源码agent loop 消费侧实现 与配套测试,完整讲解这一机制的动机、数据结构、消费链路、配置项与可扩展性设计。读完本文,你将能够回答:为什么工具过滤(如 ToolSearch / 渐进式披露)只是一次 assembly 重写,以及如何在单个监听器中同时替换提示词文本与可见工具。

问题背景:wire format 与架构关注点的错位

在协议格式(wire format)层面,工具 schema 通过模型请求中专用的 tools 字段传输,而不是嵌入提示词文本——这与 OpenAI/Anthropic 等接口的习惯一致。DeepSeek Harness 的 GenerateOptions 同样保留了独立的 systemtools 字段,这一点可以从请求构建处直接确认:agent.ts 中 buildRequestheader.systemheader.tools 分别填入冻结后的请求对象。

然而从架构角度看,「模型被告知它能做什么」是一个统一关注点:提示词段落(sections)与工具列表(tools)由相同的插件贡献组装,并在同一时刻被消费。如果按 wire format 的表象把它们拆成两个独立的 seam(接缝),任何想影响「模型被告知什么」的拦截——工具过滤、plan 模式的工具收窄、提示词改写——都会被迫挂到两个接口上。决策笔记对此的表述是:

提示词段落与工具列表由相同的插件贡献组装,并在同一时刻被消费。(决策笔记

决策:PromptAssembly { sections, tools }

PromptAssembly 是组装产物的核心类型,定义在 index.ts

/**
 * Merge-extensible assembled model input. Sections and contexts remain
 * uninterpolated until rendered; tools are already in canonical order.
 */
export interface PromptAssembly {
  sections: AssembledSection[]
  contexts: AssembledContext[]
  tools: ToolSchema[]
  variables: Record<string, string | undefined>
}

四个成员各有分工,且都可在瀑布事件中观察与改写:

成员 含义 消费位置
sections 有序的提示词文本段落(已解析、未插值 {{variable}} renderPrompt 渲染为 system
contexts 动态运行时上下文(按 order 排序),渲染后成为 user 角色的快照消息 preStep 中投影进消息历史
tools 工具 schema,已完成规范化排序 映射到 wire 的 tools 字段
variables 已解析的 {{name}} 变量表 渲染时严格插值

「工具注册表自动贡献一个提供方」这一句在源码中的落点是 SystemPrompt 服务暴露的 tools() 注册 API(index.ts):

tools(provider: (context: AssembleContext) => ToolProviderResult): () => void {
  return this.layers.effect(
    this.ctx,
    layer => layer.toolProviders.append(provider),
    { label: 'systemPrompt.tools()' },
  )
}

包 README 进一步说明:ToolRuntime 会自动注册自己作为工具提供方,因此大多数工具无需手工接线。提供方每次组装时求值,返回该次组装中模型可见的 ToolSchema 集合(schemas),以及可选的限制前全量名集(knownNames),后者供 toolOrder 配置校验使用(ToolProviderResult 定义)。

PromptAssembly 接口的可扩展性也值得注意:决策笔记的「后果」部分指出,assembly 接口通过 TypeScript 声明合并(declaration merging)实现扩展——插件可以为 PromptAssembly 直接声明合并新字段,而不需要无类型的 extras 包,为未来新增槽位预留了空间。

消费链路:agent loop 每步一次 assembly

「agent loop 每个步骤消费一份 assembly」在源码中的完整调用链是:

  1. 每步组装agent.ts 的 preStep 中,const assembly = await this.loopCtx.systemPrompt.assemble(assembleContextFor(this, signal)) 在每个 step 开头执行一次。assembleContextFor 携带 agent 的 scope 与本轮 AbortSignalAssembleContext定义)本身就是 merge-extensible 的,插件可声明合并自己的上下文字段。
  2. 动态上下文投影:同一函数内,renderContextSections(assembly) 解析 contexts,再由 this.runtimeContext.project(...) 决定是否注入 user 角色快照消息(includeRuntimeContext 关闭时为空)。
  3. 提示词渲染step 方法const system = renderPrompt(assembly),将 sections 严格插值 {{variable}}、丢弃空段落、以空行连接。
  4. tools 映射到 wire 字段buildRequest 接收 assembly.toolssystem,仅在非空时写入请求头(agent.ts...tools.length > 0 ? { tools } : {}),最终进入 GenerateOptions

于是决策笔记的映射关系得到逐字印证:适配器将 sections 映射到 system 槽位,将 tools 映射到协议格式的 tools 字段。

assemble() 的实现本身是一条两阶段管线(实现):

  • 合并与解析:按 scope 链合并全局层与当前 scope 层(scope 内同名段落/变量遮蔽全局),求值各段落的函数型 text,收集工具提供方并 structuredClone 参数以与提供方解耦;
  • 规范化排序orderTools() 在未配置 toolOrder 时按名称码元序排序,配置了则按显式顺序插入,未列工具按字典序落在 <unlisted-tools> 标记处;
  • 瀑布事件await this.ctx.waterfall(scopeTarget(this, scope), 'system-prompt/assemble', assembly, context, ...) 让作用域过滤的监听链按序改写 assembly,返回值是权威的;
  • complete 段恢复:若存在 complete: true 的有效段落,waterfall 之后恢复该段落为唯一提示词段(监听器无法增改该 scope 的系统提示词),而 contexts/tools/variables 保留。

事件签名声明在 模块增强声明 中,@mode waterfall,注释明确了三个契约:scope 过滤的派发(scoped 监听器只收到本 scope 的 assembly)、返回值为权威值、调用方 signal 只控制本次组装请求。

不变量伴侣:对权威结果的校验

除了业务监听器,仓库还为该瀑布安装了一个不变量伴侣 invariant.ts:它以 prepend: true 挂接 system-prompt/assemble,在 next() 之后校验最终 assembly——段落名非空且不重复、文本为字符串、工具名非空、变量名匹配 ^[a-z][a-z0-9_]*$ 且值为字符串或 undefinedvalidateAssembly)。这意味着任何瀑布监听器改写出「结构非法」的 assembly 都会被兜底拦截,而不是静默流入模型请求。

单一拦截点:工具过滤就是一次 assembly 重写

决策笔记的核心论断是:system-prompt/assemble waterfall 是模型预先获知的所有信息的唯一拦截点,工具过滤(ToolSearch / 渐进式披露)是一次 assembly 重写,与提示词编辑无异。

这一点在测试中有直接的行为佐证。tool-order.spec.ts 中,一个瀑布监听器对 assembly.tools 直接 push 新工具,组装结果即包含该工具——监听器对列表的修改就是最终的可见工具集:

ctx.on('system-prompt/assemble', function (assembly, _context, next) {
  ...
  assembly.tools.push(tool('aardvark'))
  return next()
})
const assembly = await ctx.systemPrompt.assemble()
expect(names(assembly)).toEqual(['alpha', 'zulu', 'aardvark'])

README 对这条契约的措辞同样直白:「waterfall 监听器若修改列表,则对输出的确定性负全责」(README Assembly and rendering 小节)。反过来,plan 模式这类需要「同时收窄提示词与工具」的能力,只需一个监听器同时改写 assembly.sectionsassembly.tools 即可——plan-mode 的测试 正是通过 ctx.on('system-prompt/assemble', ...) 对组装结果做断言的。

曾考虑的替代方案及其缺陷

决策笔记记录了被否决的替代方案:循环从工具注册表和提示词服务分别查询。其问题在于把一个统一关注点拆到两个 seam 上——任何想影响「模型被告知什么」的拦截(工具过滤、plan 模式)都需要在两个接口上各挂一个监听器,且两个接口各自演进,难以保证「文本说 A 可用、工具里却给了 B」这类不一致不会出现。采用 PromptAssembly 后,一致性由单一产物类型与单一瀑布事件在结构上保证。

配置与工具排序:让 assembly 可配置、可预测

组装管线受 Config 控制,四个字段在 包 README 中给出完整说明,这里继承并对照源码展开:

- name: '@deepseek-ai/dsh-system-prompt'
  config:
    includeHarnessIdentity: true
    includeRuntimeContext: true
    persona: 'You are the deployment assistant.'
    toolOrder: ['<unlisted-tools>']
字段 默认值 含义
includeHarnessIdentity true 在 order −1000 处包含固定开场白 You are an AI agent powered by DeepSeek Harness.构造函数 注册 harness:identity 段)。仅当兼容性部署拥有完整系统提示词时置 false
includeRuntimeContext true 是否将有序动态运行时上下文纳入组装;false 等价于 suppressRuntimeContext()index.ts
persona '' 全局部署人格提示片段,渲染在 order 0;同名的 scope 段 deployment:persona 可遮蔽它(PERSONA_SECTION 常量导出正为此用)
toolOrder 模型可见工具的显式顺序,必须恰好包含一个 <unlisted-tools> rest 条目

toolOrder 的校验分两层,源码与测试一一对应:

  • 加载期形状校验validateToolOrder 拒绝重复项与缺少 rest 条目的列表;
  • 组装期存在性校验orderTools 在每次 assemble() 时以「限制前名集」knownNames 校验配置中列出的名称,未注册名称会让组装直接失败——tool-order.spec.ts 断言错误信息为 toolOrder lists unregistered tools "ghost", "wraith"; known tools: bash, todo_write,且 README 的「已知限制」一节明确:这类错误在首轮组装(而非启动期)才暴露。

排序的确定性也是显式设计的:无配置时按工具名码元序(与 locale 无关,跨机器一致,见 compareNames 注释);同名工具的收集顺序保持稳定(测试 tool-order.spec.ts)。工具 schema 排序发生在瀑布之前,因此 waterfall 监听器拿到的是已规范化顺序的列表,「注册顺序是插件加载的副产品」不会泄漏到模型可见顺序中。

段落侧的确定性由 FIRST_PARTY_SECTION_ORDER稀疏命名 order 分配)承担:相邻值差至少 10,便于机械检测意外的碰撞;外部插件可用任意有限 order,同 order 时按名称码元序裁决。

已知限制与设计取舍

决策笔记「后果」第三条坦承了一个概念上的意外感:把 schema 放在「提示词」服务里略显反直觉,这一点在笔记与 包 README 中均有说明(README 的 Design concept 一节给出的理由正是本文的主线:「模型被告知它能做什么是一个连贯的整体,尽管适配器把 schema 作为独立 wire 字段传输」)。此外,README 的 Known Limitations 一节列出三条与本文相关的限制,值得在扩展 assembly 时留意:

  • 部署侧提示词文本只能通过 config/组合贡献,没有面向终端用户的提示词编辑 API;
  • {{…}} 花括号没有转义语法,完整组一律按注册变量插值,未知或无值引用会在渲染时抛错(严格优于静默失败);
  • toolOrder 配置错误在提示词组装(首轮)而非启动期才暴露。

小结

回到决策笔记的三个关键词:一份产物PromptAssembly { sections, tools })、一个消费点(agent loop 每步一次 assemble() + buildRequest 的双字段映射)、一个拦截点system-prompt/assemble 瀑布)。工具过滤、plan 模式、提示词改写因此都是同构的——对 assembly 的一次重写。若需要继续深入,建议按 system-prompt 子系统文档agent loop 消费侧源码tool-order 测试包 README 继续阅读,它们与本文引用的源码路径一一对应。

登录后查看全文
热门项目推荐
相关项目推荐