# 上下文与投影

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

- 作者：David（道雾轩）
- 专栏：Kimi Code 源码解读（https://daiw.org/manual/kimi-code.md）
- 最后更新：2026-09-19
- 原文：https://daiw.org/manual/kimi-code/context
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

## 两套消息

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

| 概念 | 类型 | 位置 |
| --- | --- | --- |
| 内部历史 | `ContextMessage` | `src/agent/contextMemory/types.ts` |
| 线上消息 | `Message` | `src/llm-adapter/contract/message.ts` |

`ContextMessage` 就是 `Message` 加上一组模型看不到的字段（`types.ts:122`）：

```ts
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`）没有了，换成两个入口：

```ts
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 与图片格式错误时切换（见[回合收敛](https://daiw.org/manual/kimi-code/turn-convergence)）。

## project() 修什么

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

- **孤儿结果**：找不到对应 tool call 的 tool 结果直接丢弃（`orphan_tool_result_dropped`）——它在严格 provider 上是非法报文，对模型也没用。
- **错位结果**：tool 结果与调用之间夹了别的消息，展开时挪回调用后面（`tool_result_reordered`）。
- **缺失结果**：一律补一条合成结果（`tool_result_synthesized`）：

```ts
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` 把工具结果渲染成模型看到的样子：

```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` 是「只给模型看」的侧信道，就在这里接到输出末尾（参见[上一章](https://daiw.org/manual/kimi-code/agent-loop)的工具结果契约）。

## 动态工具

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

`agent/toolSelect/dynamicTools.ts` 的 `stripDynamicToolContext()` 在压缩前把两者剥掉：公告整条删除，schema 消息去掉 `tools` 字段。压缩之后，提醒服务按当前状态重新公告（见[提示词与注入](https://daiw.org/manual/kimi-code/prompt-system)）。

## 注入的格式

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

- 提醒类注入统一包成 `<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 仍嫌结构不对 | 反复 400 | strict 投影重发 |

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

下一篇：[上下文压缩](https://daiw.org/manual/kimi-code/compaction)。
