CHAPTER 08 · STAGE 02 · 追踪完整对话

System Prompt、LLM Adapter 与模型请求

一次模型请求通常包含四部分:调用配置、系统提示词、工具 schema 和由 Session 派生的消息历史。

模型真正收到的不是用户一句话

一次模型请求通常包含四部分:调用配置、系统提示词、工具 schema 和由 Session 派生的消息历史。

GenerateOptions
  config:provider、model、maxTokens、reasoning effort 等
  system:组装后的系统提示词
  tools:当前 Agent 可见的 Tool Schema
  messages:从 Session 派生的历史

AgentLoop 负责把这些部分放到一起,但每部分由不同插件贡献和拥有。

System Prompt 不是一段硬编码字符串

ctx.systemPrompt 是一个注册表 Service。插件可以注册有顺序的 section、动态 context、工具 schema provider 和变量。

例如 Harness identity、部署 persona、工具使用说明和当前工作目录可能来自不同插件。组装时按 order 排序,解析变量,合并当前 scope 的贡献,再产生最终文本。

这种设计让不同 Agent 可以获得不同 persona、工具说明或上下文,而不需要复制 AgentLoop。

Prompt Section 与 Dynamic Context 的区别

Prompt Section 适合稳定系统指令,例如身份、行为规则和工具说明。它会进入系统提示词,通常在多 Step 请求中反复出现。

Dynamic Prompt Context 表示会变化的模型可见事实,例如当前运行环境快照。为了保持可恢复性,它会作为持久的用户角色 context snapshot 进入 Session,而不是仅在内存中改写系统字符串。

稳定前缀与动态内容分开,也有利于模型 Provider 的 KV Cache:稳定系统前缀不必因为每次动态事实变化而从更早位置失效。

Tool Schema 为什么由 System Prompt 组装器参与

模型请求中的 Tool Schema 必须与当前 Agent 实际可执行的工具保持一致。ctx.tools 提供注册工具,System Prompt 组装器收集当前 scope 可见且未被限制的 schema。

如果模型看到一个工具 schema,但执行注册表找不到它,就会产生协议不一致。反过来,执行层存在但 schema 被隐藏,则模型不能直接请求它。

因此工具过滤要同时影响展示、lookup 和 execution,而不是只删除提示词中的一行文字。

Prompt Variable

Section 文本可以引用变量,例如当前 provider、model 或 cwd。变量 Provider 在每次 assembly 时解析。

未定义变量被引用时应失败,而不是静默替换成空字符串;静默缺失会让模型收到表面正常却语义残缺的提示词。

complete Section

普通 section 会一起拼接。标记为 complete 的 section 表示它拥有整个系统提示词。组装仍会解析工具、上下文和 waterfall,但最终只保留这个完整 section。

同一有效 scope 出现多个 complete section 是配置冲突,系统应明确失败。

LLM Service 与具体 Adapter

ctx.llm 是模型能力注册表。具体 Provider Adapter,例如 DeepSeek 或 Replay,把自己的 provider id、模型发现、默认参数和流式调用实现注册进去。

AgentLoop 只依赖统一 LLM 请求与 StreamChunk 词汇,不直接调用某个厂商 SDK。这样路由变化不需要修改循环。

LLM 与 Shell 的一个区别是:LLM 注册表通常允许多个具名 Adapter 同时存在,调用配置通过 provider/model 选择精确路由。

Prepare Call:默认值属于 Adapter

模型 Provider 可能拥有 reasoning effort、maxTokens 或 retry policy 默认值。AgentLoop 在发起请求前让精确 Adapter materialize 默认值,形成 PreparedLlmCall。

重要的是保持注册一致性:准备能力、记录 request/header 和最终 dispatch 必须对应同一个 Adapter 注册,不能在热重载中前半段使用旧 Adapter、后半段切到新 Adapter。

默认值的来源也会写入 request header。下一次路由变化时,Adapter 提供的默认值可以由新路由重新计算,而调用方显式设置的值继续保留。

StreamChunk:模型输出的统一语言

不同 Provider SDK 的流式事件格式不同。Adapter 把它们转换成统一 StreamChunk,例如 text delta、reasoning delta、Tool Call block 或 finish 信息。

AgentLoop 持续消费 chunk,写入 assistant/chunk,使用 BlockAssembler 组装完整内容,并在成功结束时写入 assistant/message。

Provider 失败、取消和普通成功都需要统一结束语义,Loop 才能决定重试、关闭 Step 或记录最终结果。

为什么保留 raw chunk 又要组装 message

Raw chunk 给 UI 提供实时 token 流,也支持精确重放。组装后的 message 则是下一次模型请求真正需要的稳定历史节点。

如果只保存 message,无法重现流式界面与 chunk 来源;如果只保存 chunk,每次派生历史都要重新解释 Provider 流,而且难以表达完整成功锚点。

模型请求失败与重试

最终 Adapter 或 terminal LLM dispatch 的失败会进入 agent/request-error。Retry 插件可以根据精确 Provider 的 retry policy 等待后返回 retry action。

失败尝试的部分 chunk 不应误进入正常模型历史;重试会发生在新的编号 Step 中,并保留可复用的请求前缀。

并非所有插件异常都是 LLM 请求失败。Prompt 组装、工具处理中间件或其他扩展点抛错可能直接关闭当前 Turn,错误所属域决定恢复位置。

KV Cache 的直观理解

许多 Provider 能复用与上一请求相同的 token 前缀。系统提示词、工具 schema 和早期历史保持字节一致时,后续 Step 通常只追加内容,缓存更容易复用。

修改前缀中较早的 prompt section、调整工具 schema 顺序或压缩旧历史,会从变化位置开始失去缓存复用。仓库文档中的 Model Experience 会说明插件对 token 和缓存前缀的影响。

源码阅读顺序

  1. packages/core/system-prompt/README.zh.md
  2. packages/core/system-prompt/src/index.ts
  3. packages/llm/llm/src/types.ts
  4. packages/llm/llm/src/index.ts
  5. packages/llm/llm/src/assembler.ts

本篇练习

  1. 列出一次模型请求的 config、system、tools、messages 四部分及其拥有者。
  2. 解释为什么动态运行时 context 不应只存在于临时 prompt 字符串。
  3. 找到一个 LLM Adapter 包,指出它怎样注册 provider。
  4. 解释 Adapter 默认 maxTokens 与调用方显式 maxTokens 在路由切换时的区别。

验收标准:能从 System Prompt section 和 Tool 注册开始,追踪到最终 GenerateOptions 与 request/header。