系统提示词组装

系统提示词不是一个字符串常量,而是若干具名有序片段在每次请求时组装出来的快照。 对应实现: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-prefixdeployment:persona-suffix
顺序010200
文本来自 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),表示至少有一个作用域的提示词组成变了。

下一步

在 GitHub 上编辑此页