DeepSeek Harness 中工具 schema 的系统提示词组装机制:PromptAssembly 与 system-prompt/assemble 瀑布事件
在 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 同样保留了独立的 system 与 tools 字段,这一点可以从请求构建处直接确认:agent.ts 中 buildRequest 将 header.system 与 header.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」在源码中的完整调用链是:
- 每步组装:agent.ts 的 preStep 中,
const assembly = await this.loopCtx.systemPrompt.assemble(assembleContextFor(this, signal))在每个 step 开头执行一次。assembleContextFor携带 agent 的 scope 与本轮AbortSignal,AssembleContext(定义)本身就是 merge-extensible 的,插件可声明合并自己的上下文字段。 - 动态上下文投影:同一函数内,
renderContextSections(assembly)解析contexts,再由this.runtimeContext.project(...)决定是否注入 user 角色快照消息(includeRuntimeContext关闭时为空)。 - 提示词渲染:step 方法 中
const system = renderPrompt(assembly),将sections严格插值{{variable}}、丢弃空段落、以空行连接。 - tools 映射到 wire 字段:
buildRequest接收assembly.tools与system,仅在非空时写入请求头(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_]*$ 且值为字符串或 undefined(validateAssembly)。这意味着任何瀑布监听器改写出「结构非法」的 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.sections 与 assembly.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 继续阅读,它们与本文引用的源码路径一一对应。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00