# 系统提示词、上下文与提醒

> 每次模型请求前的那段“前缀”怎样拼出：十几个段落按静态、动态分进三条 system 消息，AGENTS.md 与记忆索引走 meta user 块；二十多种 system reminder 何时产生、落在请求哪里、是否落库；中途 system 消息投影，以及上下文用量拆解给谁看。

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

模型每次请求看到的消息序列，开头都是运行时拼出来的一段“前缀”：三条 system 消息，外加一两条包在 `<system-reminder>` 里的 user 消息；对话中间还散落着各种带来源标签的提醒。这一篇讲前缀由哪些段落组成、按什么顺序排、什么时候重建，提醒在什么条件下产生、最后落在请求的哪个位置。回合循环在哪一步调用这些逻辑，见[回合循环与 TurnMachine](https://daiw.net/manual/zcode/turn-loop)；对话本体怎样被摘要替换，留给下一篇。

以下目录都在 `apps/zcode-cli/packages` 下：

| 位置 | 职责 |
| --- | --- |
| `core/src/context/` | `ContextBuilder` 与各段落构造器，纯函数，不碰文件系统 |
| `core/src/system-reminder/` | 提醒来源登记表、包装与转义、附件与中途消息的正文格式 |
| `core/src/runtime/methods/context*.ts` | 首次初始化、每回合重建前缀、上下文用量快照 |
| `core/src/runtime/helpers/runtime-reminders.ts` | Plan 模式、Todo、日期等运行时提醒的正文与节奏 |
| `core/src/runtime/helpers/provider-*.ts` | 把历史投影成 provider 请求：提醒排位、中途 system 消息、缓存标记 |
| `adapters/src/context/` | `NodeContextSourceAdapter`：探测环境与 git、查找 AGENTS.md |

## 前缀怎样拼出来

第一次模型请求前，运行时走一次 `ensureContextInitialized`（`apps/zcode-cli/packages/core/src/runtime/methods/context.ts:31`）：向 `ContextSourcePort` 要一份上下文源快照，顺带启动 MCP、发现技能、定位项目记忆目录并读入 `MEMORY.md`，然后用快照构造 `ContextBuilder`，把 `build()` 的产物装进 `MessageHistory` 的开头（`context.ts:273`）。

```mermaid
flowchart LR
  P["ContextSourcePort<br/>NodeContextSourceAdapter"] --> S["快照<br/>cwd、envInfo、git、日期、AGENTS.md"]
  K["技能目录"] --> B["ContextBuilder.build()"]
  M["记忆目录与 MEMORY.md"] --> B
  S --> B
  B --> SEC["段落列表<br/>按注入位置与缓存提示排序"]
  SEC --> SYS["三条 system 消息<br/>CLI 前缀、稳定正文、动态正文"]
  SEC --> META["两条附件<br/>skills_listing、context_prefix"]
  SYS --> H["MessageHistory 前缀"]
  META --> H
  H --> R["每次请求：投影、包装、打缓存标记"]
```

每个段落是一个 `ContextSection`，除正文外还带两个标记：注入位置 `injectionTarget`（`system` 或 `meta_user`）与缓存提示 `cacheHint`（`stable` 或 `dynamic`）（`apps/zcode-cli/packages/core/src/context/types.ts:52`）。`build()` 收齐段落后，先按“system 稳定、system 动态、meta user 稳定、meta user 动态”重排（`apps/zcode-cli/packages/core/src/context/builder.ts:310`），再拼成三条 system 消息，每条都带 `ephemeral` 缓存标记（`builder.ts:230`）：

```ts
  private assembleSystemMessages(sections: ContextSection[]): ModelInputMessage[] {
    const messages: ModelInputMessage[] = [];

    const cliPrefixContent = buildSectionContent(
      sections.filter(
        (section) => section.injectionTarget === "system" && section.source === "cli_prefix",
      ),
    );
    if (cliPrefixContent) {
      messages.push({
        role: "system",
        content: cliPrefixContent,
        cacheControl: EPHEMERAL_CACHE_CONTROL,
      });
    }
  // ...
    const dynamicSystemContent = buildSectionContent(
      sections.filter(
        (section) => section.injectionTarget === "system" && section.cacheHint === "dynamic",
      ),
    );
    if (dynamicSystemContent) {
      messages.push({
        role: "system",
        // ZCode by design：Main Agent 的 dynamic system block 自带左边界，所有 provider 保持一致。
        content: `\n\n${dynamicSystemContent}`,
        cacheControl: EPHEMERAL_CACHE_CONTROL,
      });
    }
```

省略的中间一条收的是除 CLI 前缀以外的全部稳定段（`builder.ts:246`）。meta user 段拼成两条附件：技能清单单独一条，来源 `skills_listing`；其余段落合成一条，来源 `context_prefix`，首尾各加一句固定的话（`builder.ts:327`），开头一句是：

> As you answer the user's questions, you can use the following context:

附件在历史里以 `kind: "attachment"` 存放（`apps/zcode-cli/packages/core/src/runtime/methods/context-history-entries.ts:7`），发请求时才包进 `<system-reminder>` 标签。历史开头哪些条目算“前缀”，由 `countContextPrefixMessages` 判定：连续的 system 消息，加上来源为 `context_prefix` 或 `skills_listing` 的附件（`apps/zcode-cli/packages/core/src/agent/message-history.ts:269`）。重建前缀、压缩、回退都靠它把“前缀”和“对话”切开。

git 快照那一段还有一层约束：代码注释说 `git status` 只按 2000 字符截断、不按条目数截断，是为了“与上游 CLI 保持一致”，避免 provider 可见的提示词形状漂移（`apps/zcode-cli/packages/adapters/src/context/git-snapshot.ts:168`）。至少这部分提示词的形状是有意对齐某个上游 CLI 的，代码没有点名是哪一个。

## 段落清单

按 `build()` 的构造顺序（重排后次序不变）：

| 次序 | 段落（source） | 注入与缓存 | 出现条件 | 内容要点 |
| --- | --- | --- | --- | --- |
| 1 | CLI Prefix（`cli_prefix`） | system，稳定，独占第一条 | 不是工作流子代理（`builder.ts:103`） | 一句身份 |
| 2 | Agent Identity（`identity`） | system，稳定 | 默认（`builder.ts:121`） | 身份句、安全声明、`# Harness` 块 |
| 3 | ZCode Desktop Context（`desktop_context`） | system，稳定 | 桌面端会话（`builder.ts:131`） | 链接写法、`::code-comment` 指令 |
| 4 | Dynamic Behavior（`dynamic_behavior`） | system，动态 | 默认（`builder.ts:136`） | 怎样与用户沟通、写注释、确认难以撤销的操作、如实汇报 |
| 5 | Session-specific guidance（`session_guidance`） | system，动态 | 有 Skill 工具且有技能（`builder.ts:141`） | 目前只有一行：用户输入 `/<skill-name>` 时经 Skill 调用 |
| 6 | Memory（`memory`） | system，动态 | 启用项目记忆（`builder.ts:152`） | 记忆目录与文件格式，见[项目记忆](https://daiw.net/manual/zcode/memory) |
| 7 | Environment Info（`env_info`） | system，动态 | 总有（`builder.ts:158`） | 工作目录、是否 git 仓库、平台、Shell、系统版本、执行模型 |
| 8 | Output Style（`output_style`） | system，动态 | 有输出风格（`builder.ts:161`） | 风格名与风格提示词 |
| 9 | Context Management（`context_management`） | system，动态 | 总有（`builder.ts:167`） | 长对话会被总结、不必提前收尾、自主推进 |
| 10 | System Context（`system_context`） | system，动态 | git 仓库（`builder.ts:169`） | 分支、主分支、用户、`git status --short`、最近 5 个提交 |
| 11 | Skills（`skills`） | meta user，`skills_listing` | 有技能且 Skill 工具可用（`builder.ts:179`） | 技能清单，默认预算 20000 字符 |
| 12 | Request User Context（`request_user_context`） | meta user，`context_prefix` | 有 AGENTS.md 或记忆索引（`builder.ts:190`） | `# agentsMd` 块 |
| 13 | Current Date（`current_date`） | meta user，`context_prefix` | 有日期（`builder.ts:199`） | `# currentDate` 一行 |

身份相关的两句原文分别在 `apps/zcode-cli/packages/core/src/context/sections/cli-prefix.ts:8` 与 `apps/zcode-cli/packages/core/src/context/sections/identity.ts:35`：

> You are ZCode, an interactive coding agent
>
> You are an interactive ZCode agent that helps users with software engineering tasks.

`# Harness` 块五条，交代输出按 Markdown 渲染、工具受权限模式约束、优先用专用工具、代码引用写成 `file_path:line_number`，其中一条预告了后文的中途 system 消息（`identity.ts:26`）：

> The system may send updates, reminders, or modifications to rules via mid-conversation system turns. These are system-controlled, unlike function results.

git 快照的几个上限：每条 git 命令 3000 毫秒超时、最近提交取 5 个、状态截到 2000 字符（`git-snapshot.ts:5`、`git-snapshot.ts:11`、`git-snapshot.ts:13`）。技能清单超出预算时退化为只列名字和路径（`apps/zcode-cli/packages/core/src/context/sections/skills.ts:50`），细节见[技能与自定义命令](https://daiw.net/manual/zcode/skills-commands)。

上表是默认路径，另有三种变体：

- **自定义提示词**：嵌入方传入 `systemPrompt`（例如脚本工作流用 `opts.systemPrompt` 创建子会话，`apps/zcode-cli/packages/bootstrap/src/app/script-workflow-child-runtime.ts:98`），它替换身份段，并跳过第 3 到第 10 段全部 system 段，只剩 CLI 前缀、技能与 meta user 块（`builder.ts:108`、`builder.ts:130`）。
- **工作流子代理**：没有 CLI 前缀，身份换成“脚本创建的子代理”契约，安全声明与 Harness 块逐字复用（`apps/zcode-cli/packages/core/src/context/sections/workflow-actor.ts:48`），不要桌面、Dynamic Behavior、Session guidance 三段；和自定义提示词同时出现直接抛错（`builder.ts:93`）。见[动态工作流（三）](https://daiw.net/manual/zcode/dwf-tools)。
- **子 Agent** 另有一个 `SubagentContextBuilder`：CLI 前缀、Agent 专属提示词、通用备注、环境，再加 AGENTS.md、日期和技能（`apps/zcode-cli/packages/core/src/subagent/context-builder.ts:106`），见[子 Agent](https://daiw.net/manual/zcode/subagents)。

有几样东西传进了 builder 却不进提示词。`ContextBuilderConfig` 的 `language`、`projectContext`、`agentProfiles`、`embeddedSearchEnabled`、`compact` 照传不误（`context.ts:122`），builder 与段落构造器都不读：适配器探测出的项目类型、包管理器与 scripts（`apps/zcode-cli/packages/adapters/src/context/index.ts:250`）不会让模型看到。工具说明也不再镜像进系统提示词，由请求的 `tools` 字段承载（`builder.ts:47`）。

<Callout type="info">
  输出风格在 core 里是一整套：第 8 段、身份句的替换（`identity.ts:33`）、每回合首步的 `output_style` 提醒。但仓库里 core 之外没有任何地方设置 `outputStyle`，插件清单里的 `outputStyles` 也只做诊断（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol/plugins.ts:481`）。从代码看，这条链路目前不会被触发。
</Callout>

## 指令文件：只认 AGENTS.md

`NodeContextSourceAdapter` 查找指令文件的默认文件名只有 `AGENTS.md`，单个文件最多读 100 KiB（`apps/zcode-cli/packages/adapters/src/context/index.ts:24`）。候选只有两个，用户级在前、工作区在后（`index.ts:99`）：

```ts
  const priorityFiles = options.priorityFiles ?? DEFAULT_PRIORITY_FILES;
  const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
  const projectRoot = options.projectRoot ?? (await findProjectRoot(options.workingDirectory));

  const defaultUserInstructionFile = await findDefaultUserInstructionFile(priorityFiles, env);
  const workspaceInstructionFile = await findInstructionFile(
    options.workingDirectory,
    projectRoot,
    priorityFiles,
  );
  const candidates = dedupeInstructionFileCandidates([
    defaultUserInstructionFile
      ? { ...defaultUserInstructionFile, scope: "user" as const }
      : undefined,
    workspaceInstructionFile
      ? { ...workspaceInstructionFile, scope: "workspace" as const }
      : undefined,
  ]);
```

- **用户级**：`~/.zcode/AGENTS.md`，主目录取环境变量 `HOME` 或 `USERPROFILE`，都没有才用 `os.homedir()`（`index.ts:229`、`index.ts:245`）。
- **工作区级**：从工作目录逐级向上，第一个含 `AGENTS.md` 的目录即止，最远到 git 根（向上第一个有 `.git` 的目录，`index.ts:320`）；不在 git 仓库里就一直找到文件系统根（`index.ts:219`）。所以只取离工作目录最近的那一份，子目录与仓库根的两份不会叠加。
- **读取与合并**：两个候选按绝对路径去重后依次读（`index.ts:142`），超过 100 KiB 截断并记下 `truncated`，读失败只记一条诊断（`index.ts:159`）。渲染时每份单独成节，标题是 `Contents of <路径> (user default instructions):` 或 `(workspace instructions):`，截断的文件末尾补一行 `[File truncated: AGENTS.md]`（`apps/zcode-cli/packages/core/src/context/sections/request-user-context.ts:116`）。项目记忆的 `MEMORY.md` 索引接在后面，整块以 `# agentsMd` 开头（`request-user-context.ts:63`）：

> Codebase and user instructions are shown below. Be sure to adhere to these instructions. IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written.

代码里没有读取 `CLAUDE.md` 的地方。桌面端的新手引导提供一次迁移，把 `~/.claude/CLAUDE.md` 复制成 `~/.zcode/AGENTS.md`，目标已存在时默认跳过（`packages/services/src/settings-sync/settingsSyncService.ts:2221`）。子 Agent 默认继承父会话已解析的指令快照，配置里写了 `injectAgentsMd: false` 才不带（`apps/zcode-cli/packages/core/src/runtime/methods/subagent.ts:96`）。

## 前缀什么时候重建

上下文源快照在一个运行时里只取一次（`context.ts:36`）：AGENTS.md、git 状态、会话日期都定格在开始那一刻，中途改了 AGENTS.md 或提交了代码，模型看到的仍是旧的。冷恢复会话时运行时丢掉旧历史、重新初始化上下文（`apps/zcode-cli/packages/core/src/runtime/methods/resume.ts:147`），AGENTS.md 与记忆索引会重新读取、日期取当天；环境与 git 快照却不重新探测，而是沿用第一条用户消息落库时一并保存的那份（`resume.ts:114`、`apps/zcode-cli/packages/core/src/runtime/methods/message-persistence.ts:77`）。

前缀本身却每个回合都重算。首轮之后，每个回合开始都按这一步实际拿到的模型重建一次（`apps/zcode-cli/packages/core/src/runtime/methods/turn.ts:215`），做法是用同一份快照重新 `build()`，保留对话部分、只换前缀（`apps/zcode-cli/packages/core/src/runtime/methods/context-refresh.ts:37`）：

```ts
  const effectiveContextResult = runtime.contextBuilder.build();
  const contextEntries = buildContextHistoryEntries(effectiveContextResult);
  const canonicalEntries = runtime.messageHistory.borrowReadOnlyRuntimeEntries();
  const canonicalConversationEntries = canonicalEntries.slice(
    countContextPrefixMessages(canonicalEntries),
  );

  runtime.latestContextBuildResult = effectiveContextResult;
  runtime.messageHistory.replaceMessages([...contextEntries, ...canonicalConversationEntries]);

  const turnEntries = options.turnRequestEntries;
  if (!turnEntries) return runtime.messageHistory.borrowReadOnlyRuntimeEntries();
  return [...contextEntries, ...turnEntries.slice(countContextPrefixMessages(turnEntries))];
```

快照不变，那每回合重建改的是什么？主要是模型：`# Environment` 最后一行写的是本步骤的执行模型（`apps/zcode-cli/packages/core/src/context/sections/env-info.ts:77`），Session guidance 依据的也是这个模型可见的工具表（`context.ts:141`），二者都在第三条动态 system 消息里，前两条不受影响。这就是“中途变更系统提示词”的第一种方式：直接替换历史开头的前缀。另外几处也会触发重建：空闲时改语言或输出风格（`apps/zcode-cli/packages/core/src/runtime/methods/config.ts:59`）、回合中途经引导换模型（`apps/zcode-cli/packages/core/src/runtime/methods/turn-guide-drain.ts:45`）、回退消息之后（`apps/zcode-cli/packages/core/src/runtime/methods/rewind-message.ts:706`）、首发前刷新 Shell（`apps/zcode-cli/packages/core/src/runtime/methods/session-shell-environment.ts:225`）。

日期是个例外。`# currentDate` 用的是快照里的日期，跨过午夜不会变；运行时改在每个回合开头比对本地日期，变了就追加一条 `date_change` 提醒，并要求模型不要特意向用户提起（`turn.ts:848`、`apps/zcode-cli/packages/core/src/runtime/helpers/runtime-reminders.ts:91`）。

## 提醒：带来源标签的附件

提醒在历史里是带 `source` 的附件条目（`message-history.ts:59`）。全部来源登记在 `apps/zcode-cli/packages/core/src/system-reminder/source.ts`，分三组：前缀两种（`source.ts:20`）；可以随会话落库、冷恢复时按原来源重建的 15 种（`source.ts:22`）；只活在进程内存历史里的 10 种（`source.ts:40`）。每个来源有一张描述符，记投递通道与生命周期（`source.ts:88`）。包装时，正文里出现的 `<system-reminder` 或 `</system-reminder` 会把起始尖括号转义成 `&lt;`，防止提醒正文伪造标签边界（`source.ts:233`）。

回合循环每一步请求前依次检查几种运行时提醒，写进本回合的请求状态（`apps/zcode-cli/packages/core/src/runtime/methods/turn-loop.ts:120`）：

```ts
    if (!outputTokenRecoveryActive && this.needsPlanModeExitReminder) {
      this.needsPlanModeExitReminder = false;
      commitTurnRequestEntries(this, state.turnRequestState, [
        systemReminderAttachmentEntry("plan_mode_exit", buildPlanModeExitReminderBody()),
      ]);
    }
    const runtimeModeReminderBody = outputTokenRecoveryActive
      ? null
      : buildRuntimeModeReminderBody(
          state.turnRequestState.entries,
          this.getMode(),
          this.getPlanEnabled(),
        );
    if (runtimeModeReminderBody) {
      commitTurnRequestEntries(this, state.turnRequestState, [
        systemReminderAttachmentEntry("runtime_mode", runtimeModeReminderBody),
      ]);
    }
```

随后是 Todo 提醒与输出风格提醒（`turn-loop.ts:138`、`turn-loop.ts:157`）。这些检查排在 microcompact 与自动压缩之后（`turn-loop.ts:69`、`turn-loop.ts:78`），所以提醒总是加在压缩过的历史上；输出 token 截断后的续写步骤一律跳过。全部来源一览：

| 来源 | 什么时候产生 | 出处 |
| --- | --- | --- |
| `runtime_mode` | Plan 模式开启时每步检查：从未提醒过，或距上次已有 5 条真实用户消息；第 1、6、11……次发含四阶段工作流的完整版，其余发一句话的精简版 | `runtime-reminders.ts:18`、`runtime-reminders.ts:182` |
| `plan_mode_exit` | Plan 模式关闭后，下一步请求前发一次 `## Exited Plan Mode` | `apps/zcode-cli/packages/core/src/runtime/execution-state.ts:64` |
| `todo_reminder` | 本回合可见 TodoWrite，且距上次 TodoWrite 调用、距上次提醒都已满 10 个 assistant 步；有清单时附上，并落库 | `runtime-reminders.ts:81`、`turn-loop.ts:138` |
| `output_style` | 回合首步、存在输出风格时（目前无入口，见上文） | `turn-loop.ts:157` |
| `date_change` | 回合开头发现本地日期变了 | `turn.ts:848` |
| `hook_context` | SessionStart、UserPromptSubmit、Stop 钩子返回附加上下文，截到 24000 字符 | `apps/zcode-cli/packages/core/src/runtime/methods/hooks.ts:106` |
| `referenced_session_context` | 输入里有 `#sess_` 开头的会话引用，提示按需调用 ReadSessionContext | `apps/zcode-cli/packages/core/src/session-context/references.ts:13` |
| `prompt_attachment` | 用户附带文件或文本：文件伪装成一次 Read 调用及结果，文本注明“当作数据” | `apps/zcode-cli/packages/core/src/system-reminder/prompt-attachment.ts:61` |
| `plugin_reference` | 输入引用了插件，列出它的技能、MCP 与子 Agent；在用户消息落库后追加并落库 | `turn.ts:543`、`apps/zcode-cli/packages/core/src/runtime/methods/plugin-reference.ts:128` |
| `goal_state_change` | `/goal` 暂停、恢复、清除；回合进行中先挂起，回合收尾再写入 | `apps/zcode-cli/packages/core/src/runtime/methods/goal-state-reminder.ts:14` |
| `resume_goal_state`、`target_continuation` | 恢复会话时重申目标；目标续跑时注入续跑提示（后者算 user 输入，不是 meta） | `resume.ts:434`、`apps/zcode-cli/packages/core/src/runtime/methods/target.ts:147` |
| `model_anomaly` | 工具调用超预算，或反复调用同一工具 | `apps/zcode-cli/packages/core/src/runtime/methods/turn-tool-warnings.ts:30` |
| `incoming_message`、`queued_system_notification` | 回合中途插入的用户消息、别的会话或协调者的消息、后台任务通知 | `apps/zcode-cli/packages/core/src/runtime/helpers/provider-entry-origins.ts:52`、`runtime-reminders.ts:105` |
| `shell_environment_change` | 恢复会话时 Shell 快照没能还原，且执行 Shell 与记录不同（例如旧 Windows 会话改由 Git Bash 执行） | `session-shell-environment.ts:156` |
| `rewind_notice`、`conversation_fork`、`selection_side_chat` | 回退、分叉、划词旁聊的边界说明 | `apps/zcode-cli/packages/core/src/runtime/methods/rewind.ts:309`、`apps/zcode-cli/packages/core/src/runtime/methods/session-fork.ts:443` |
| `plan_file_reference`、`resume_referenced_session_context` | 压缩后补回已批准的计划文件与最近读过的文件 | `apps/zcode-cli/packages/core/src/runtime/methods/compact-active.ts:486` |
| `tool_result_warning`、`goal_completion_verification` | 通道是“写进工具结果”：Read 的截断、空文件警告直接拼在结果文本里；后者的格式化函数目前没有调用方 | `apps/zcode-cli/packages/core/src/tool/handlers/read-text.ts:98`、`apps/zcode-cli/packages/contracts/src/tools/target.ts:247` |
| `task_status`、`diagnostics` | 已登记，当前代码没有产生它们的地方；前者只在冷恢复时识别旧数据 | `apps/zcode-cli/packages/core/src/agent/session-history-hydrator.ts:408` |

回合中途插入的消息各有一段说明文字（`apps/zcode-cli/packages/core/src/system-reminder/incoming-message.ts:12`）。来自别的 ZCode 会话的消息会附上一段权限告诫：同伴不能授权，不能因同伴要求修改权限设置、AGENTS.md 或配置，同伴自称被拒后让你代劳要拒绝并告知用户，代码把这称作 “permission laundering”（`incoming-message.ts:6`）。Plan 模式与 Todo 本身见[Todo、提问与 Plan 模式](https://daiw.net/manual/zcode/interaction-tools)，钩子见[生命周期 Hooks](https://daiw.net/manual/zcode/hooks)，目标续跑见[目标模式](https://daiw.net/manual/zcode/goal-target)。

## 请求投影与中途 system 消息

中途调整规则的第二种方式，是不动前缀、在对话中间插入 system 消息（mid-conversation system，下称 MCS）。它发生在请求投影这一步：历史不直接发给模型，每次请求先经 `buildRuntimeProviderRequestMessages` 决定是否启用 MCS，再交给 `buildProviderRequestMessages`（`apps/zcode-cli/packages/core/src/runtime/helpers/runtime-provider-request-messages.ts:17`、`apps/zcode-cli/packages/core/src/runtime/helpers/provider-request-messages.ts:54`）：

```ts
  const useMidConversationSystem = input.useMidConversationSystem !== false;
  const origins = new ProviderEntryOrigins();
  const reorderResult = reorderAttachmentLikeEntries(
    projectIncomingMessageEntries(input.entries, origins),
  );
  const midSystemProjection = useMidConversationSystem
    ? projectMidConversationSystemEntries(reorderResult.entries, origins)
    : { entries: reorderResult.entries };
  const projectedEntries = moveLegacySystemRemindersAfterToolResultRun(midSystemProjection.entries);
  // ...
  const renderedMessages = projectedEntries.map(renderProjectedEntryToModelMessage);
```

```mermaid
flowchart TD
  E["历史条目"] --> I["中途输入换上说明文字<br/>_steer 结尾的改成 incoming_message 附件"]
  I --> O["meta 提醒冒泡<br/>排到最近的 assistant、tool、system 之后"]
  O --> Q{"启用 MCS？"}
  Q -- 是 --> S["可走 MCS 的提醒合并成 system 消息<br/>落在 user 或 tool 之后"]
  S --> V{"位置合法？"}
  V -- 否 --> D["降级为 system-reminder user 文本"]
  V -- 是 --> L
  D --> L
  Q -- 否 --> L["夹在工具结果中间的提醒挪到结果之后"]
  L --> RD["渲染：附件包成 system-reminder<br/>最后一条非 system 消息打缓存标记"]
```

几步的要点：

- **冒泡**：从尾部往前扫，meta 类提醒被挪到最近一个 assistant、tool、system 边界或中途插入的输入之后，也就是排在尾部真实用户消息的前面（`provider-request-messages.ts:106`）。`history_continuity` 通道的来源与 `goal_state_change` 不冒泡，保持因果位置（`provider-request-messages.ts:44`、`provider-request-messages.ts:148`）。
- **MCS 落点**：可走 MCS 的提醒先攒着，遇到下一条 assistant 或 system 才落下，落点须紧跟 user 或 tool 消息，连续几条合成一条 system 消息；回合中途插入的输入是因果边界，普通提醒不能越过它（`apps/zcode-cli/packages/core/src/runtime/helpers/provider-mid-conversation-system.ts:36`、`provider-mid-conversation-system.ts:79`）。最后校验一遍：前面不是 user 或 tool，或者后面跟的既不是 assistant 也不是请求结尾，这样的 system 消息降级为 `<system-reminder>` user 文本（`provider-mid-conversation-system.ts:128`）。
- **结果**：从代码看，围绕一条新用户消息产生的提醒（日期、钩子、插件引用等，不论写在用户消息之前还是之后），开启 MCS 时落成“用户消息，接一条 system 消息”，与 `turn.ts:544` 注释所说的 “user → system” 一致；关闭时是排在用户消息之前的几条 `<system-reminder>` user 消息。
- **缓存**：渲染后先清掉所有非 system 消息上的缓存标记，只给最后一条非 system 消息打 `ephemeral`（`provider-request-messages.ts:292`）。加上前缀三条 system 消息，默认路径下一次请求共四个缓存标记；它们怎样变成各家协议的缓存参数见[模型适配层](https://daiw.net/manual/zcode/model-adapters)。相邻 user 消息的合并开关是关着的，注释说这是 Anthropic 序列化层的职责（`provider-request-messages.ts:39`）。

有 8 种来源永远不走 MCS：`context_prefix`、两类压缩后提醒、分叉与旁聊边界、目标续跑、两种工具结果内联提醒（`source.ts:76`）。注释解释了分叉边界的理由：走 MCS 会被挪到新问题之后，改变上下文顺序（`source.ts:79`）。技能清单 `skills_listing` 不在其列，从代码看，开启 MCS 时它会离开前缀，以一条 system 消息落在第一条用户消息之后（`provider-mid-conversation-system.ts:84`）。

MCS 是否启用取决于模型属性 `supportsMidConversationSystem`，或运行时配置 `midConversationSystem.mode` 为 `force`（`runtime-provider-request-messages.ts:17`）。内置 Provider 配置里这个属性默认关（`config/provider/zcode-builtin.json:885`），对 Anthropic Messages 形态的 `api.z.ai`、`open.bigmodel.cn`、ZCode Coding Plan 与闲时端点、DeepSeek 的 Anthropic 端点打开（`zcode-builtin.json:4090`、`zcode-builtin.json:4067`），另对部分 Claude 模型打开（`zcode-builtin.json:2486`）。桌面端的模型设置里对应“对话中系统消息”开关（`packages/ui/src/i18n/locales/zh-CN.ts:2867`）；命令行的 `--force-mcs` 强制开启，只能与 `--prompt`、`--target` 或 `tui` 一起用（`apps/zcode-cli/packages/cli/src/run.ts:43`）。规则怎样匹配到模型见[Provider 规则与模型目录](https://daiw.net/manual/zcode/provider-config)。

## 上下文用量拆解

每次模型请求前，`runModelTextRequest` 都算一份上下文用量快照（`apps/zcode-cli/packages/core/src/runtime/methods/model.ts:137`），分七类：System prompt、Meta user context、Skills、Tool prompt、System tool schemas、MCP tool schemas、Messages（`apps/zcode-cli/packages/core/src/runtime/methods/context-usage.ts:158`）。

- 前四类直接用 `build()` 时每个段落记下的字符数与估算 token（`context-usage.ts:123`）。估算把中文字符按两个字符计，再除以 3（`apps/zcode-cli/packages/core/src/context/utils.ts:12`，除数见 `packages/shared/src/usage-stats.ts:9`）。
- 工具 schema 把名称、描述、输入输出 schema、权限等字段序列化后估算，MCP 工具按 `mcp__` 前缀区分（`context-usage.ts:310`）。
- Messages 按角色汇总，排除 system 消息与以 `<system-reminder>` 开头的 user 消息（后台任务通知除外）（`context-usage.ts:147`）。快照自带两条警告：token 是本地估算，Messages 不重复计 system 与 meta 内容（`context-usage.ts:248`）。从代码看，对话中途的提醒无论渲染成 system 还是 `<system-reminder>` 文本，两边都没算进去；Tool prompt 一类也恒为 0，因为已经没有 `tools` 来源的段落。

这份快照有两个去处。一是 debug 日志 `context_usage_snapshot`：每类下按段落、工具、技能或消息角色列出贡献者（`apps/zcode-cli/packages/core/src/runtime/helpers/context-usage-breakdown.ts:39`），写日志时每类只留前 5 个（`apps/zcode-cli/packages/core/src/runtime/methods/context-usage-log-compact.ts:1`）；初始化时的 `context.built` 日志更是带着每段全文（`context.ts:282`），调试工具据此画时间线（`apps/zcode-cli/packages/debug/server/analyzer.ts:1385`），见[遥测、调试与提示词轨迹](https://daiw.net/manual/zcode/telemetry-debug)。二是界面，给的只是按类汇总的字符数（`context-usage.ts:255`），而且只挂在主回合的 `ModelComplete` 事件上（`apps/zcode-cli/packages/core/src/runtime/methods/turn-model-step.ts:610`），经协议投影进 `usage.contextWindow.breakdown`（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol-v4/product-projection.ts:4525`）。桌面与 Web 端输入栏的上下文用量浮层据此画分段进度条，只显示各类占比，注释说分项 token 数会和顶部总量的口径混淆（`packages/ui/src/chat-input-toolbar/contextUsage.tsx:961`）。TUI 的状态栏与侧边栏只显示已用 token 与占窗口的比例（`apps/zcode-cli/packages/tui/src/app-input-status.tsx:177`），不展示拆解。

下一篇：[上下文压缩](https://daiw.net/manual/zcode/compaction)——上下文快满时，microcompact、自动压缩与手动 `/compact` 各自怎样把对话本体换成摘要，又保留了什么。
