DeepSeek Harness深度拆解-15:体验系统提示词的构建从编程的角度利用提供的一系列实例演示如何使用SystemPrompt这个基础服务,这篇文章将会完整介绍该服务针对相关注册方法的处理和最终对系统提示词的组装流程。
1. 不仅仅提供单纯系统提示词文本
SystemPrompt并不是单纯地为我们生成调用LLM提供的系统提示词(对应GenerateOptions的system字段),它最终构建的产物体现在其assemble方法构建的PromptAssembly对象上,只有sections部分的内容才会参与存储系统提示的渲染。contexts部分会转换成消息,并最终添加到GenerateOptions的messages列表中,而tools提供的工具Schema自然就对应GenerateOptions的同名字段。
typescript
export class SystemPrompt extends Service {
async assemble(context: AssembleContext = {}): Promise<PromptAssembly>
}
export interface AssembleContext {
scope?: ScopeKey
signal?: AbortSignal
}
export interface PromptAssembly {
sections: AssembledSection[]
contexts: AssembledContext[]
tools: ToolSchema[]
variables: Record<string, string | undefined>
}
定义在PromptAssembly四个字段说明如下:
- sections :系统提示词的正文片段,已求值但未插值,按
order排序,最终由renderPrompt插值、过滤、拼接成提示词字符串; - contexts :动态运行时上下文快照,已求值但未插值,按
order排序,最终渲染成用户消息插入模型历史; - tools :面向模型的工具Schema列表,已结构化、已按
toolOrder排序,不做文本渲染,直接作为LLM API的tools参数传入; - variables :变量名到值的扁平映射,已求值,仅作为
sections和contexts插值时的查表来源。
DSH中的大部分资源都需要与具有某个Scope绑定,SystemPrompt提供的PromptAssembly自然也是如此,这一点从作为assembly方法的签名就可以看出来。该方法的参数context对应的AssembleContext接口定义了ScopeKey类型的scope字段,意味着可以针对指定的Scope动态提供对应的PromptAssembly对象。如果将Scope映射为Agent,那么就意味着可以针对性地为具体的Agent注册和提供系统提示词。
2. SystemPrompt插件配置
SystemPrompt即使服务也是插件,即以插件的方式将自己作为服务注册到Context上,作为插件的配置对应于如下这个Config。
typescript
export interface Config {
includeHarnessIdentity?: boolean
includeRuntimeContext?: boolean
personaPrefix?: string
personaSuffix?: string
toolOrder?: string[]
}
Config接口定义的配置选项包括:
- includeHarnessIdentity :是否注入Harness身份声明(默认
true),即"You are an AI agent powered by DeepSeek Harness."; - includeRuntimeContext :是否包含
PromptContext(默认true),如当前工作目录、可用工具等动态信息,每轮请求重新求; - personaPrefix :作为persona的前缀,出现在第一个
PromptSection之前,用于定义部署特有的角色描述,支持严格变量插值; - personaSuffix :部署级人格后缀,出现在最后一个
PromptSection之后,用于追加全局补充约束; - toolOrder:显式指定工具呈现顺序。
3. 基于Scope的分层注册表
SystemPrompt采用标准的Builder模式来构建最终的PromptAssembly,它内部提供了一个基于Scope的分层注册表,我们针对section、context、tools和variable方法的调用都是在注册表中添加相应的条目,最终利用assembly方法根据指定的Scope提取匹配的注册条目进行合并,最终将PromptAssembly创建出来。在DSH中但凡涉及基于Scope的分层注册表概念,必然会使用ScopedLayers对象,并通过实现ScopeLayer接口提供基于某个Scope的单层注册表。如下这个PromptLayer类型就是SystemPrompt内部使用的单层注册表。
typescript
class PromptLayer implements ScopeLayer {
readonly sections: NamedEntries<PromptSection>
readonly contexts: NamedEntries<PromptContext>
readonly runtimeContextSuppressors = new AnonymousEntries<true>()
readonly toolProviders = new AnonymousEntries<ToolProvider>()
readonly variables: NamedEntries<VariableProvider>
isEmpty(): boolean {
return this.sections.isEmpty()
&& this.contexts.isEmpty()
&& this.runtimeContextSuppressors.isEmpty()
&& this.toolProviders.isEmpty()
&& this.variables.isEmpty()
}
}
export interface PromptSection {
readonly name: string
readonly order: number
readonly text: string | ((context: AssembleContext) => string)
readonly complete?: boolean
}
export interface PromptContext {
readonly name: string
readonly order: number
readonly text: string | ((context: AssembleContext) => string)
}
type ToolProvider = (context: AssembleContext) => ToolProviderResult
export interface ToolProviderResult {
readonly schemas: readonly ToolSchema[]
readonly knownNames?: readonly string[]
}
type VariableProvider = (context: AssembleContext) => string | undefined
如上面的代码所示,PromptLayer内部定义了五个字段来存储与某个Scope绑定或者全局系统提示词注册条目,其中包括三个类型为NamedEntries<T>的字段sections、contexts和variables用来存储注册的PromptSection、PromptContext和VariableProvider(用来提供模板渲染时替换占位符的变量值)。至于两个AnonymousEntries<T>类型的字段,toolProviders用来保存用于提供工具Schema的ToolProvider对象,runtimeContextSuppressors字段则用来存储当前Scope是否开启动态运行时上下文的开关。
4. 注册请求的处理
SystemPrompt定义了如下这个ScopedLayers类型的字段layers,它内部使用正是上面定义的这个PromptLayer。PromptLayer中定义的五个注册表分别通过五个对应的方法来维护。
typescript
export class SystemPrompt extends Service {
private readonly layers = new ScopedLayers(
scope => new PromptLayer(scope),
() => { this.ctx.emit('system-prompt/change') })
section(section: PromptSection): () => void
context(context: PromptContext): () => void
suppressRuntimeContext(): () => void
tools(provider: (context: AssembleContext) => ToolProviderResult): () => void
variable(name: string, provider: (context: AssembleContext) => string | undefined): () => void
}
SystemPrompt还定义了getSectionOrder和getContextOrder方法为DSH原生提供的PrompSection和PromptContext提供对应的order值。如下面的代码所示,@deepseek-ai/dsh-system-prompt定义了常量SECTION_ORDERS和CONTEXT_ORDERS为为这些PrompSection和PromptContext提供的名称和order值得映射关系,并以PromptSectionOrderName和PromptContextOrderName类型名称将它们的keyof typeof导出。
typescript
export class SystemPrompt extends Service {
getSectionOrder(name: PromptSectionOrderName): number {
return SECTION_ORDERS[name]
}
getContextOrder(name: PromptContextOrderName): number {
return CONTEXT_ORDERS[name]
}
}
const SECTION_ORDERS = {
HARNESS_IDENTITY: -1000,
DEPLOYMENT_PERSONA_PREFIX: 0,
PLAN_POLICY: 500,
TEAM_POLICY: 600,
PTC_ONLY: 800,
FILE_REFERENCE: 900,
TOOL_BASH: 1000,
TOOL_PWSH: 1010,
TOOL_READ: 1100,
TOOL_WRITE: 1200,
TOOL_EDIT: 1300,
TOOL_GLOB: 1400,
TOOL_GREP: 1500,
TOOL_JOBS: 1600,
TOOL_PTY: 1700,
TOOL_WEB_SEARCH: 2000,
TOOL_WEB_FETCH: 2100,
TOOL_LSP: 2200,
TOOL_SESSION_QUERY: 2300,
TOOL_GOAL: 2400,
TOOL_CORDIS: 2500,
TOOL_WORKFLOW: 2600,
TOOL_RALPH: 2700,
TOOL_SUBAGENT: 2800,
TOOL_REPORT: 2900,
TOOLS_SDK: 5000,
DELIVERABLE_FILE_REFERENCES: 9000,
STRUCTURED_OUTPUT: 9900,
HARNESS_SOURCE: 10000,
WEB_SURFACE: 10100,
DEPLOYMENT_PERSONA_SUFFIX: 10200,
} as const
const CONTEXT_ORDERS = {
SANDBOX_POLICY: 110,
APPROVAL_POLICY: 115,
SUBAGENT_DELEGATION: 120,
} as const
export type PromptSectionOrderName = keyof typeof SECTION_ORDERS
export type PromptContextOrderName = keyof typeof CONTEXT_ORDERS
5. PromptAssembly的组装
在assemble方法中,会提取AssembleContext提供的Scope,然后利用它从layers字段表示的ScopedLayers对象中提取注册的条目,并生成最终的PromptAssembly对象。
typescript
export class SystemPrompt extends Service {
async assemble(context: AssembleContext = {}): Promise<PromptAssembly>
}
具体会按照如下的规则生成PromptAssembly的四个成员:
- variables :将Scope作为参数调用
layers的chainLayers方法得到按照Scope路径排序的PromptLayer(不包含全局层),按照这个顺序提取变量进行合并(如果变量名称相同,与Scope越近的层具有更高遇优先级); - sections & contexts :将Scope作为参数调用
layers的merge方法合并每个PromptLayer提供的PromptSection和PromptContext; - tools : 将Scope作为参数调用
layers的chainLayers方法得到按照Scope路径排序的PromptLayer,并使用全局层与之合并,然后按照此顺序提取每个PromptLayer的ToolProvider。将AssembleContext作为参数调用每个ToolProvider函数得到一组ToolSchema和工具名称列表,然后执行一系列验证和排序生成最终的ToolSchema列表。
在返回最终生成的PromptAssembly对象之前,assemble方法会以waterfall形式触发具有如下签名的system-prompt/assemble事件对外发送通知。当然,此事件肯定是以Scope绑定的范围进行分发的,本着事件向上传播的原则,之后当前以及上级Scope才会监听到此通知。整个事件流返回的PromptAssembly将会作为assemble方法的返回值,意味着我们还可以通过注册此事件的形式对PromptAssembly实施再加工。
typescript
declare module '@deepseek-ai/cordis' {
interface Events {
'system-prompt/assemble'(
this: Scoped<SystemPrompt>,
assembly: PromptAssembly,
context: AssembleContext,
next: () => Promise<PromptAssembly>): Promise<PromptAssembly>
}
}
6. 提示词和上下文快照渲染
如下所示的是两个针对PromptAssembly进行文本渲染的renderPrompt和renderContextSnapshot方法的定义。它们分别从PromptAssembly提取sections和contexts,将它们的内容作为模板,调用interpolate函数使用variables提供的变量执行插值操作,生成最终的文本。
typescript
export function renderPrompt(assembly: PromptAssembly): string {
return assembly.sections
.map(section => interpolate(section, assembly.variables, 'section'))
.filter(text => text.length > 0)
.join('\n\n')
}
export function renderContextSnapshot(assembly: PromptAssembly): string {
return joinContextSections(renderContextSections(assembly))
}
export function renderContextSections(assembly: PromptAssembly): ContextSnapshotSection[] {
return assembly.contexts
.map(context => ({ name: context.name, text: interpolate(context, assembly.variables, 'context') }))
.filter(section => section.text.length > 0)
}
export function joinContextSections(sections: readonly ContextSnapshotSection[]): string {
const body = sections.map(section => section.text).join('\n\n')
if (body.length === 0) return ''
return `Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\n${body}`
}
export interface ContextSnapshotSection {
readonly name: string
readonly text: string
}
function interpolate(
input: AssembledSection | AssembledContext,
variables: Record<string, string | undefined>,
kind: 'section' | 'context',
): string
renderContextSection和joinContextSections函数作为renderContextSnapshot的辅助函数,同时可以直接导入使用。前者生成一个ContextSnapshotSection列表,后者将这个列表转换成字符串,并前置一段指示性文本。