系统提示词组装
系统提示词不是一个字符串常量,而是若干具名有序片段在每次请求时组装出来的快照。
对应实现:src/Tether.Core.SystemPrompt/,契约在 src/Tether.Core.Contracts/。
提示词片段
// 静态文本
var banner = new PromptSection("workspace:banner", order: 10, text: "当前仓库是 tether。");
// 每次组装都重新解析
var toolHint = new PromptSection(
"workspace:tools",
order: 20,
resolveText: ctx => $"你可以使用 {ctx.Tools.Count} 个工具。");
using var registration = prompts.Register(banner, scope: null);
| 成员 | 含义 |
|---|---|
Name | 在同一个注册层内唯一。 |
Order | 升序渲染顺序。 |
Complete | 该片段是否独占整个提示词。 |
ResolveText(context) | 为一次组装解析文本;返回 null 会抛异常。 |
动态片段的解析器只在该片段可见时才被调用,因此可以放心地在里面做与作用域相关的计算。
组装的输入
PromptAssemblyContext 是一次组装的全部输入:
Scope—— 可见性作用域,null表示只看全局提供者;Tools—— 本次请求模型可见的工具 schema(脱离副本);Cancellation—— 仅作用于这一次组装。
工具 schema 之所以进入提示词组装的输入,是为了让提示词能引用当前真实可用的工具集,而不是写死一份可能过期的清单。
分层可见性
注册分两类层:全局层(scope 传 null)与按 AgentPipelineScope 建立的作用域层。组装时先解析出该作用域可见的片段集合,再按 Order 升序排列。片段在层内以名字为键,因此同名片段之间构成覆盖关系。
AgentPipelineScope 可以有父层,可见性沿父链向上包含更宽的层。
组装快照
PromptAssemblySnapshot snapshot = await prompts.AssembleAsync(
new PromptAssemblyContext(scope, toolSchemas, cancellation));
string prompt = snapshot.SystemPrompt;
PromptAssemblySnapshot 有三个成员:
Sections—— 按渲染顺序排列的已解析片段;Tools—— 模型可见的工具 schema;SystemPrompt—— 非空片段文本用空行(\n\n)连接的结果。
空文本片段会被 SystemPrompt 跳过,但仍然留在 Sections 里,便于诊断”这个片段确实参与了组装,只是这次没产出内容”。
组装 waterfall
解析出种子快照之后,组装会跑一次 system-prompt/assemble waterfall,插件可以在这里整体改写:
// Cordis 便捷写法:注册随作用域回收
context.RegisterPromptAssemblyListener(async (assembly, ctx, next) =>
{
var inner = await next(); // 必须调用续延才会委托给下游
return new PromptAssemblySnapshot(
inner.Sections.Append(new PromptSectionSnapshot("audit", "本次会话被审计。")),
inner.Tools);
});
监听器是环绕式的:不调用 next() 就短路了后续监听器与终局行为(终局返回种子快照)。监听器也可以用 scope 过滤,null 表示对每次组装都可见。
waterfall 返回 null 是错误
组装 waterfall 必须返回一个快照。返回 null 会抛
InvalidOperationException,而不是静默退化成空提示词。
complete 片段独占整个提示词
把片段的 complete 设为 true,表示它自己就是完整提示词。
var replacement = new PromptSection("eval:fixed", order: 0, text: fixedPrompt, complete: true);
规则有两条,都很硬:
- 同时活跃的 complete 片段最多一个,超过一个直接抛
InvalidOperationException,并在消息里列出全部冲突片段名; - 存在 complete 片段时,最终快照的片段集合只有它一个——即使组装 waterfall 增删改过片段,也会被它覆盖掉。
complete 会盖掉 waterfall 的片段修改
complete 片段是从 waterfall 之前的定义里解析的,并在 waterfall 结束后覆盖片段列表。 工具 schema 不受影响,仍然采用 waterfall 的结果。想做整体替换又保留其它插件的贡献时, 应当用组装监听器,而不是 complete 片段。
内置 persona 片段
提供注册表的插件会自动注入两个片段(对应上游 deployment:persona-prefix / deployment:persona-suffix 拆分):
| 属性 | 前缀 | 后缀 |
|---|---|---|
| 名字 | deployment:persona-prefix | deployment:persona-suffix |
| 顺序 | 0 | 10200 |
| 文本 | 来自 StaticSystemPromptOptions.PersonaPrefix | 来自 StaticSystemPromptOptions.PersonaSuffix |
默认前缀文本是 You are Tether, a concise coding assistant.,后缀默认为空,通过配置替换:
plugins:
- id: system-prompt
name: system-prompt
config:
personaPrefix: 你是 Tether,一个简洁的编码助手。
personaSuffix: 当前工作目录见会话上下文。
前缀标注了 [Required(AllowEmptyStrings = true)]:缺省会拒绝,显式空串允许(前缀可为空,但配置项本身必须在场),详见 YAML 组合与加载。
顺序 0 是 persona 前缀的固定位次,位于全部 first-party 指令之前(位次表还给 harness identity 预留了 -1000、给 runtime context 预留了 -99,见 ISystemPromptProvider.GetSectionOrder / GetContextOrder);后缀排在 10200,位于全部可复用指令之后——携带本地路径/端点等环境事实的内容后置,保证 KV-cache 前缀稳定(上游 dsh #3644 的动机)。自定义片段按 Order 落在两者之间,需要抢在前缀之前则用负数。
可见性变更通知
提示词可见性发生变化时会发出 system-prompt/change(常量 CoreEvents.SystemPromptChange),表示至少有一个作用域的提示词组成变了。