---
title: 上下文工程
url: https://doc.liz6.com/ai/01-models-and-context/01-context-engineering
locale: zh
area: ai
tags:
- ai
- 模型与上下文
date: 2026-06-30
modified: 2026-07-16
description: 模型是无状态的——它"知道"的一切都在每次请求的上下文窗口里。把窗口当预算经营:缓存稳定前缀、压缩旧历史、按需加载工具,而不是无节制地往里塞。窗口管理是 agent 性能的第一变量。
---

# 上下文工程

> 模型是无状态的——它"知道"的一切都在每次请求的上下文窗口里。把窗口当预算经营:缓存稳定前缀、压缩旧历史、按需加载工具,而不是无节制地往里塞。窗口管理是 agent 性能的第一变量。

## 概述

大语言模型(LLM)在一次请求里是**无状态**的:它不记得上一次对话,也没有隐藏的"记忆"。它对世界的全部认知,就是这一次请求里塞给它的那段文本——**上下文窗口(context window)**。所谓"上下文工程"(context engineering),就是在这个有限、昂贵、且对位置敏感的窗口里,决定放什么、放在哪、怎么复用,以换取更准、更快、更便宜的输出。

这件事之所以重要,是因为大家对 LLM 的直觉常常错位:以为"把更多资料喂进去"总会让答案更好。实际上窗口是有上限的(Claude Opus 4.8 是 1M token),注意力计算随长度呈 ~O(n²) 增长,而且模型对窗口中间的信息会"看不清"。把窗口当成一种**预算**来经营,而不是一个无底的垃圾桶,是用好任何现代模型的第一原则。

历史上这是从"prompt engineering"(怎么措辞)演进来的:单轮短 prompt 时代,措辞是全部;进入长上下文、工具调用、多轮 agent 时代后,**窗口里有什么、按什么顺序排、哪些能被缓存**才是主导因素。Anthropic 在 2024–2026 年陆续把 prompt caching、compaction、context editing、adaptive thinking 等做成 API 原语,本质都是上下文工程的工具。

## 模型一轮里"知道"的只有上下文窗口

每一次 `/v1/messages` 请求,模型看到的是这样一段被拼接起来的文本(渲染顺序固定为 `tools → system → messages`):

<svg viewBox="0 0 720 380" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="一次请求的上下文窗口:tools、system、messages 拼接后送入模型,产出 output">
  <defs><marker id="ctxwinah" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="380" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">一次请求的上下文窗口:tools → system → messages 拼接送入模型</text>
  <rect x="40" y="46" width="460" height="210" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="54" y="64" font-size="11.5" font-weight="700" fill="#3730a3">上下文窗口(渲染顺序固定)</text>
  <rect x="54" y="72" width="432" height="30" rx="5" fill="#e0e7ff" stroke="#c7d2fe"/>
  <text x="70" y="91" font-size="11" fill="#3730a3">tools 工具定义(name / description / JSON schema)</text>
  <rect x="54" y="108" width="432" height="30" rx="5" fill="#e0e7ff" stroke="#c7d2fe"/>
  <text x="70" y="127" font-size="11" fill="#3730a3">system 系统提示(角色、规则、稳定指令)</text>
  <rect x="54" y="144" width="432" height="104" rx="5" fill="#e0e7ff" stroke="#c7d2fe"/>
  <text x="70" y="160" font-size="11" font-weight="700" fill="#3730a3">messages</text>
  <rect x="130" y="166" width="348" height="22" rx="4" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="144" y="181" font-size="10.5" fill="#3730a3">历史(user/assistant 轮次,含过往 tool_use/tool_result)</text>
  <rect x="130" y="192" width="348" height="22" rx="4" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="144" y="207" font-size="10.5" fill="#3730a3">检索内容(RAG 命中的文档片段)</text>
  <rect x="130" y="218" width="348" height="22" rx="4" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="144" y="233" font-size="10.5" fill="#3730a3">本轮 user 输入</text>
  <line x1="270" y1="256" x2="270" y2="266" stroke="#475569" stroke-width="1.8" marker-end="url(#ctxwinah)"/>
  <rect x="200" y="270" width="140" height="40" rx="8" fill="#4f46e5"/>
  <text x="270" y="295" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">模型(无状态)</text>
  <line x1="340" y1="290" x2="396" y2="290" stroke="#475569" stroke-width="1.8" marker-end="url(#ctxwinah)"/>
  <rect x="400" y="270" width="270" height="40" rx="8" fill="#0d9488"/>
  <text x="535" y="295" text-anchor="middle" font-size="11.5" font-weight="700" fill="#ffffff">output(thinking / text / tool_use)</text>
  <rect x="40" y="322" width="640" height="44" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="54" y="340" font-size="12.5" fill="#3730a3">模型"记得"上一轮,只是因为调用方把整段历史又发了一遍——API 本身无状态。</text>
  <text x="54" y="358" font-size="12.5" fill="#3730a3">"记忆"由调用方维护,窗口里的内容和顺序,就是模型这一轮全部的"认知"。</text>
</svg>

关键结论:模型"记得"上一轮,只是因为**调用方把整段历史又发了一遍**。API 是无状态的;"记忆"是调用方维护的。这也解释了为什么长对话会越来越贵——每一轮都要重发全部历史。

## token 与注意力:为什么越长越贵越慢

文本先被**分词器(tokenizer)**切成 token(Claude 的 token ≠ OpenAI 的 token,别用 `tiktoken` 估 Claude,会少算 15–20%;要准就调 `count_tokens`)。计费、限流、窗口上限,单位都是 token。

长度带来两重成本:

- **算力 ~O(n²)**:自注意力(self-attention)让每个 token 关注所有其它 token,序列翻倍、计算约四倍。这是"越长越慢"的根。
- **KV cache 显存**:为避免每生成一个 token 都重算历史,推理引擎把历史每层的 Key/Value 缓存下来,显存随上下文长度线性增长。本地部署尤其吃紧——参见 [本地 LLM 部署](/homelab/local-llm.md):64k 上下文的 f16 KV cache 要 ~5.37 GB,塞不下 24GB 卡,必须量化到 Q8_0 压到 ~2.85 GB。云端 API 看不到这块显存,但它就是"长上下文更贵"的物理原因。

> KV cache(推理引擎为加速而缓存的张量)和下文的 **prompt caching**(API 为省钱而缓存的前缀)是两个不同层次的"缓存",别混。前者是单次生成内部的加速,后者是跨请求复用前缀。

## 上下文预算:谁在抢这个窗口

把窗口当预算分配。同一个窗口被这些东西瓜分,挤掉谁都会影响效果:

| 占用方 | 典型体量 | 经营手段 |
|--------|----------|----------|
| 工具定义 tools | 几十~几千 token/工具 | 工具多了用 tool search 按需加载;别一次性全塞 |
| 系统提示 system | 固定 | 冻结它(见 caching);别在里头插时间戳 |
| 历史 messages | 随对话线性增长 | compaction / context editing |
| 检索内容 RAG | 可控 | 检索优于硬塞:只放命中片段,别塞整库 |
| 输出 output | max_tokens 预留 | 流式 + 合理 max_tokens |
| thinking | 由 effort 决定 | 用 effort 调,而非硬塞 |

一个常见误区是把整个知识库塞进 system 提示。正确做法是 **RAG**:先检索,只把相关片段放进窗口。检索的精度比上下文的长度更决定答案质量——这条线在 [RAG 与检索增强](/ai/03-applications-and-production/01-rag-and-retrieval-augmented-generation.md) 里展开。

## prompt caching:把稳定前缀缓存下来

多轮对话和 agent 循环里,每次请求的前缀(工具定义 + 系统提示 + 大段历史)高度重复。**prompt caching** 把这段稳定前缀缓存在服务端,后续命中只按 ~0.1x 价格计费(写入 5 分钟 TTL 是 1.25x、1 小时 TTL 是 2x)。

它的全部行为都从一条不变量推出:

> **Prompt caching 是前缀匹配(prefix match)。前缀里任何一个字节变了,从该位置往后的缓存全部失效。**

渲染顺序是 `tools → system → messages`,所以缓存的边界要卡在"稳定/易变"的交界:

<svg viewBox="0 0 720 240" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="缓存边界:cache_control 断点前是稳定前缀命中缓存,断点后是易变后缀全价计费">
  <defs>
    <marker id="pcbrk1" markerWidth="10" markerHeight="8" refX="4" refY="0" orient="auto"><path d="M0,6 L4,0 L8,6 Z" fill="#0d9488"/></marker>
    <marker id="pcbrk2" markerWidth="10" markerHeight="8" refX="4" refY="0" orient="auto"><path d="M0,6 L4,0 L8,6 Z" fill="#64748b"/></marker>
  </defs>
  <rect width="720" height="240" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">prompt caching 的边界:cache_control 断点前后判若两价</text>
  <text x="483" y="54" text-anchor="middle" font-size="11" font-weight="700" fill="#dc2626">cache_control 断点</text>
  <line x1="483" y1="58" x2="483" y2="118" stroke="#ef4444" stroke-width="1.5" stroke-dasharray="5 4"/>
  <rect x="80" y="70" width="403" height="44" fill="#4f46e5"/>
  <text x="281" y="93" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">稳定前缀</text>
  <text x="281" y="108" text-anchor="middle" font-size="10.5" fill="#e0e7ff">工具定义(确定顺序)+ 冻结的系统提示</text>
  <rect x="483" y="70" width="173" height="44" fill="#94a3b8"/>
  <text x="569" y="93" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">易变后缀</text>
  <text x="569" y="108" text-anchor="middle" font-size="10.5" fill="#f1f5f9">本轮变化的问题</text>
  <line x1="281" y1="148" x2="281" y2="120" stroke="#0d9488" stroke-width="1.6" marker-end="url(#pcbrk1)"/>
  <text x="281" y="164" text-anchor="middle" font-size="12" font-weight="700" fill="#0f766e">命中缓存,~0.1x 计费</text>
  <line x1="569" y1="148" x2="569" y2="120" stroke="#64748b" stroke-width="1.6" marker-end="url(#pcbrk2)"/>
  <text x="569" y="164" text-anchor="middle" font-size="12" font-weight="700" fill="#475569">全价计费</text>
  <rect x="40" y="186" width="640" height="42" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="54" y="204" font-size="12.5" fill="#115e59">断点前的字节一个不能变,才能命中缓存;断点后是本轮新增的易变内容,</text>
  <text x="54" y="221" font-size="12.5" fill="#115e59">不参与缓存匹配,按全价计费——所以稳定的放前面,易变的放后面。</text>
</svg>

实操要点:

- **稳定的放前面,易变的放后面。** 系统提示里**别插** `now()`、UUID、用户 ID、随机 ID——它们在前缀里,会让后面全部失效。动态信息放到 messages 靠后的位置。
- **断点**:在稳定前缀的最后一个 block 上打 `cache_control: {type: "ephemeral"}`(或用顶层自动缓存)。每请求最多 4 个断点。
- **最小可缓存前缀**与模型相关:Opus 4.8 是 4096 token,Sonnet 4.6 / Fable 5 是 2048 token——前缀太短会**静默不缓存**,不报错。
- **验证命中**:看响应 `usage.cache_read_input_tokens`。若多次同前缀请求它一直是 0,说明有"静默失效器"(系统提示里的时间戳、未排序的 JSON、变动的工具集),逐字节 diff 两次请求的前缀去找。

| 改了什么 | 工具缓存 | 系统缓存 | 消息缓存 |
|----------|:--:|:--:|:--:|
| 工具定义(增删/重排)、换模型 | ❌ | ❌ | ❌ |
| 系统提示内容 | ✅保留 | ❌ | ❌ |
| 消息内容、`tool_choice`、开关 thinking | ✅ | ✅ | ❌ |

(✅=该层缓存仍有效)。结论:**别中途改工具集或换模型**——那会从头作废整段缓存。

## 当对话超过窗口:compaction vs context editing

长 agent 跑久了,历史会逼近甚至超过窗口。两种 API 原语,方向相反,常配合使用:

| | compaction(压缩) | context editing(裁剪) |
|---|---|---|
| 做什么 | 把早期历史**摘要**成一个 compaction block | 把旧的 tool_result / thinking **清除** |
| 信息 | 概括保留 | 直接删掉 |
| 何时用 | 逼近窗口上限、需要继续对话 | 旧工具输出已无关,想瘦身 |
| 关键坑 | 必须把 `response.content`(含 compaction block)**整块回传**,只回传 text 会丢状态 | 只删 tool_use/thinking,不动对话结构 |
| Beta header | `compact-2026-01-12` | `context-management-2025-06-27` |

记住二者的差别:compaction 是"概括",context editing 是"删除"。跨会话的持久记忆是第三件事(把要点写进文件/记忆库),不在单次窗口内解决——展开见 [记忆与状态](/ai/02-Agent/03-memory-and-state.md)。

## thinking / effort / task_budget:让模型自己经营预算

现代 Claude(Opus 4.6+)用 **adaptive thinking**(`thinking: {type: "adaptive"}`)——模型自行决定何时、想多深,不再手填 `budget_tokens`(在 Opus 4.7/4.8/Fable 5 上手填会 400)。想深浅的总闸是 **effort**:

```
output_config: { effort: "low" | "medium" | "high" | "xhigh" | "max" }
```

`effort` 越低,工具调用更少更聚合、前言更短;`high`/`xhigh` 适合 coding 与 agent。它直接决定一轮里 thinking + 工具 + 输出花多少 token——这是上下文预算最直接的旋钮。

还有 **task budget**(beta `task-budgets-2026-03-13`):给整个 agent 循环一个 token 预算(最小 20000),模型能**看到倒计时**并自我收尾。它和 `max_tokens` 不同:`max_tokens` 是单次响应的硬上限、模型看不到;`task_budget` 是模型可感知的"花销建议"。

## "lost in the middle" 与 context rot:多 ≠ 好

模型对窗口的注意力**不均匀**:开头和结尾记得清,中间容易"看不见"(研究界称 "lost in the middle")。再加上塞进太多弱相关内容会稀释信号——更长的上下文反而**降低**准确率,这就是 "context rot"。

两条可落地的对策:

- **把最重要的指令放在模型注意力强的位置**(靠前的 system,或靠后的 user),并用清晰分隔符 / XML 标签框住关键块。
- **宁可检索,不要硬塞。** 与其把 50 页文档全贴进去赌模型自己找,不如先 RAG 命中再放 2 页(见 [RAG 与检索增强](/ai/03-applications-and-production/01-rag-and-retrieval-augmented-generation.md))。

## 最佳实践

- **把窗口当预算经营,不是垃圾桶。** 决定放什么、放哪、怎么复用,而非无脑塞满——这是用好任何现代模型的第一原则。
- **稳定前缀焊死,易变内容后置。** 冻结 system、固定工具顺序、动态信息(时间戳/ID/检索片段)一律放靠后——这几乎就是缓存命中的全部秘诀。
- **effort 起步 `high`,按 eval 扫档。** 别反射式拉 `xhigh`/`max`;关系非单调,高 effort 在 agent 上常更省。
- **检索优于硬塞。** RAG 命中 2 页胜过硬贴 50 页(见 [RAG 与检索增强](/ai/03-applications-and-production/01-rag-and-retrieval-augmented-generation.md))。
- **关键指令放强位置 + 分隔符框住。** 靠前 system 或靠后 user,用 XML 标签隔开,别埋中段。
- **长对话主动管理,分清三件事。** compaction 概括、context editing 删除、[记忆](/ai/02-Agent/03-memory-and-state.md) 跨会话持久——别用一个手段干三件事。

## 权衡与失败模式

- **往窗口里硬塞**:以为越多越准,实则触发 context rot、注意力被稀释,准确率反降 → 检索优于硬塞,只放命中片段。
- **缓存静默失效**:system 里插了 `now()`/UUID/未排序 JSON,`cache_read_input_tokens` 长期为 0 → 冻结前缀、动态信息后置,逐字节 diff 两次请求排查。
- **一轮内改工具集或换模型**:整段前缀缓存作废、成本翻几倍 → 工具集与模型在同一循环里保持不变。
- **混淆两种"缓存"**:KV cache 是单次生成内的显存加速,prompt caching 是跨请求的前缀复用 → 分清层次再优化。
- **反射式拉满 effort**:简单任务高 effort 空转烧 token → 起步 `high`,按 eval 扫档(见 [推理与 thinking](/ai/01-models-and-context/04-reasoning-and-thinking.md))。

## 前沿:上下文即"世界"

把上下文窗口推到极致,会通向一个有意思的方向:**world model**。阿里 Qwen 团队的 [Qwen-AgentWorld-35B-A3B](https://huggingface.co/Qwen/Qwen-AgentWorld-35B-A3B) 是个 256K 上下文的 MoE(35B 总参 / 3B 激活),它不是用来聊天,而是**在上下文里模拟 agent 所处的环境**(终端、Web、OS……)。这类模型把"上下文"从"塞资料的地方"变成"承载一个可交互世界的状态"——上下文工程的尽头,是把窗口当成一台状态机来经营。这条线在 [Agent 循环与工具使用](/ai/02-Agent/01-the-agent-loop-and-tool-use.md) 的前沿小节展开。

## 参考

- **Anthropic 官方文档**: Prompt Caching、Compaction、Context Editing、Adaptive Thinking & Effort(platform.claude.com,实现前以官方为准)
- **本地实战**: [本地 LLM 部署](/homelab/local-llm.md)(KV cache 量化、显存博弈)
- **研究**: "Lost in the Middle: How Language Models Use Long Contexts" (Liu et al., 2023)
- **下游**: [Agent 循环与工具使用](/ai/02-Agent/01-the-agent-loop-and-tool-use.md)、[MCP 与 Skills](/ai/02-Agent/02-mcp-and-skills.md)

*Keywords: context window, token, tokenizer, KV cache, attention O(n²), prompt caching, prefix match, cache_control, TTL, compaction, context editing, adaptive thinking, effort, task budget, lost in the middle, context rot, RAG, world model*
