本页目录

上下文工程

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

一次请求的上下文窗口:tools → system → messages 拼接送入模型 上下文窗口(渲染顺序固定) tools 工具定义(name / description / JSON schema) system 系统提示(角色、规则、稳定指令) messages 历史(user/assistant 轮次,含过往 tool_use/tool_result) 检索内容(RAG 命中的文档片段) 本轮 user 输入 模型(无状态) output(thinking / text / tool_use) 模型"记得"上一轮,只是因为调用方把整段历史又发了一遍——API 本身无状态。 "记忆"由调用方维护,窗口里的内容和顺序,就是模型这一轮全部的"认知"。

关键结论:模型"记得"上一轮,只是因为调用方把整段历史又发了一遍⁠。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 部署: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可控检索优于硬塞:只放命中片段,别塞整库
输出 outputmax_tokens 预留流式 + 合理 max_tokens
thinking由 effort 决定用 effort 调,而非硬塞

一个常见误区是把整个知识库塞进 system 提示。正确做法是 RAG:先检索,只把相关片段放进窗口。检索的精度比上下文的长度更决定答案质量——这条线在 RAG 与检索增强 里展开。

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

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

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

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

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

prompt caching 的边界:cache_control 断点前后判若两价 cache_control 断点 稳定前缀 工具定义(确定顺序)+ 冻结的系统提示 易变后缀 本轮变化的问题 命中缓存,~0.1x 计费 全价计费 断点前的字节一个不能变,才能命中缓存;断点后是本轮新增的易变内容, 不参与缓存匹配,按全价计费——所以稳定的放前面,易变的放后面。

实操要点:

  • 稳定的放前面,易变的放后面。 系统提示里别插 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 headercompact-2026-01-12context-management-2025-06-27

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

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 与检索增强)。

最佳实践

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

权衡与失败模式

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

前沿:上下文即"世界"

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

参考

  • Anthropic 官方文档⁠: Prompt Caching、Compaction、Context Editing、Adaptive Thinking & Effort(platform.claude.com,实现前以官方为准)
  • 本地实战⁠: 本地 LLM 部署(KV cache 量化、显存博弈)
  • 研究⁠: "Lost in the Middle: How Language Models Use Long Contexts" (Liu et al., 2023)
  • 下游⁠: Agent 循环与工具使用MCP 与 Skills

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