上下文与投影

ContextMessage 与线上消息的分离、投影器的配对修复与 strict 模式、工具结果渲染、动态工具与注入格式。

作者 David更新于 第 8 篇(共 33 篇)

两套消息

Kimi Code 把「引擎持有的历史」与「发给 provider 的消息」分开,这一点 v2 没变:

概念类型位置
内部历史ContextMessagesrc/agent/contextMemory/types.ts
线上消息Messagesrc/llm-adapter/contract/message.ts

ContextMessage 就是 Message 加上一组模型看不到的字段(types.ts:122):

export type ContextMessage = Message & {
  readonly id?: string;
  readonly providerMessageId?: string;
  readonly origin?: PromptOrigin | undefined;
  readonly isError?: boolean;
  toolCallDisplays?: Record<string, ToolInputDisplay>;
  readonly note?: string;
};

最关键的是 origin:PromptOrigin 有 12 种来源,包括用户输入、skill 激活、插件命令、注入(injection)、shell 命令、压缩摘要、系统触发、后台任务、cron、hook 结果、重试。压缩保留哪些 user 消息、界面怎么显示一条消息,都看它。

历史归 Agent scope 的 IAgentContextMemoryService 所有。循环产生的内容以 step.begin、content.part、tool.call、tool.result、step.end 这些 LoopRecordedEvent 写入,由 contextMemory/loopEventFold.ts 折叠成消息。

投影器

投影在 src/agent/contextProjector/:纯函数在 projection.ts(428 行),contextProjectorService.ts 包一层策略与遥测。v1 的两个布尔开关(dropOrphanToolResults、synthesizeMissing)没有了,换成两个入口:

export function project(history: readonly ContextMessage[], onAnomaly?: OnAnomaly): Message[] {
  const layout = sliceLayout(history);
  return flattenBlocks(pairBlocks(history, layout, onAnomaly), layout, onAnomaly);
}

export function projectStrict(
  history: readonly ContextMessage[],
  onAnomaly?: OnAnomaly,
): Message[] {
  const projected = project(history, onAnomaly);
  return dropLeadingNonUserMessages(
    mergeConsecutiveAssistantMessages(dedupeDuplicateToolCalls(projected, onAnomaly), onAnomaly),
    onAnomaly,
  );
}

ProjectionPolicy 只有两个维度:structure?: 'strict' 与 media?: 'degraded' | { strip }。平时走 project();provider 报可恢复的结构错误时,请求服务才切到 projectStrict() 重发;媒体策略在 413 与图片格式错误时切换(见回合收敛)。

project() 修什么

pairBlocks() 把每个 assistant 的 tool call 与后面的 tool 结果配成「交换」,flattenBlocks() 再按交换展开。每处修复记为一个 ProjectionAnomaly:

  • 孤儿结果:找不到对应 tool call 的 tool 结果直接丢弃(orphan_tool_result_dropped)——它在严格 provider 上是非法报文,对模型也没用。
  • 错位结果:tool 结果与调用之间夹了别的消息,展开时挪回调用后面(tool_result_reordered)。
  • 缺失结果:一律补一条合成结果(tool_result_synthesized):
const TOOL_INTERRUPTED_TEXT =
  'Tool result is not available in the current context. Do not assume the tool completed successfully.';
  • 空内容:纯空白的文本块、只剩空洞内容的消息被丢掉;相邻的普通用户输入合并成一条。

v1 区分「末尾缺结果」与「非末尾缺结果」:末尾的可能还在执行,不能提前关掉。v2 的回合请求总在一个 step 的工具全部收齐之后才组装,后台工具也会先回一条 async running 的确认结果,所以投影无条件补齐也不会误关正在跑的调用;trailing 标记只决定要不要上报——末尾补齐不计入告警。

另一个保护对应 v1「估算切片 token 时不能误删」的顾虑:切片里若一条 assistant 消息都没有(sliceLayout 里的 sizing),tool 消息不参与配对,也就不会被当成孤儿删掉。

有修复时会记一条 repaired the request to keep it wire-valid 日志并上报 context_projection_repaired 遥测;同样的修复组合连续出现时只报一次。

工具结果渲染

contextMemory/toolResultRender.ts 把工具结果渲染成模型看到的样子:

export function renderToolResultForModel(result: RenderableToolResult): ContentPart[] {
  const rendered = renderStatus(result);
  if (result.note === undefined || result.note.length === 0) return rendered;
  const only = rendered[0];
  if (rendered.length === 1 && only?.type === 'text') {
    return [textPart(only.text + '\n' + result.note)];
  }
  return [...rendered, textPart(result.note)];
}
  • 出错的结果前面加 <system>ERROR: Tool execution failed.</system>,空输出换成 <system>Tool output is empty.</system>。
  • note 是「只给模型看」的侧信道,就在这里接到输出末尾(参见上一章的工具结果契约)。

动态工具

实验性的 tool-select 打开后,部分工具只以名字公告,模型用 select_tools 按需加载完整定义。相关上下文有两种:公告消息(origin.variant 为 loadable-tools,内容是 <tools_added> / <tools_removed> 名单)与携带 tools 声明的动态 schema 消息。

agent/toolSelect/dynamicTools.ts 的 stripDynamicToolContext() 在压缩前把两者剥掉:公告整条删除,schema 消息去掉 tools 字段。压缩之后,提醒服务按当前状态重新公告(见提示词与注入)。

注入的格式

注入上下文的系统信息都用结构化标记,与用户原话区分开:

  • 提醒类注入统一包成 <system-reminder> 块(features/reminder/systemReminder.ts)。
  • 后台任务的完成通知由 agent/task/notificationXml.ts 渲染成 <notification id="…" category="…" type="…"> 块。

prompt cache key

v1 的投影器会算哈希;v2 的投影器不再管缓存键。prompt cache key 直接用会话 ID——agent/profile/profileService.ts:471 的 cacheKey: this.sessionContext.sessionId,再由协议 trait 翻译成各家的字段(Kimi 是 prompt_cache_key)。请求服务里的 createHash 只给 system prompt 与工具表算指纹,用于日志去重和工具表快照。

为什么值得这么复杂

场景朴素实现的后果v2 的处理
压缩后历史被切片孤儿结果 → provider 400丢弃孤儿
用户中途取消悬空的 tool call补合成结果
并发工具先后完成结果错位按交换重排
fork 继承半截交换请求被拒继承时补「仍在执行、结果未知」
provider 仍嫌结构不对反复 400strict 投影重发

修复逻辑只有一份纯函数,可以单独测试;主回合请求与压缩请求都经过它。

下一篇:上下文压缩。

本页目录