第一章 上下文工程的崛起:从提示词到上下文系统 / Chapter 1 The Rise of Context Engineering
在 2023 年初,「prompt engineering」还是一个让无数工程师兴奋的词。那时人们相信,只要把指令写得更精巧、把示例排得更巧妙,模型就能给出更准确的答案。一年多过去,这种信念正在被一个更冷峻的事实取代:当 LLM 应用从单轮生成走向长程任务,工程的核心矛盾已经从「写出好提示」转变为「管理上下文」。本章回答一个看似简单、实则牵动整个 AI 工程范式转移的问题——为什么写好提示词已经不够?以及,当我们承认这一点之后,应该用什么样的统一心智模型来重新组织我们与模型之间的全部信息交换。
1.1 LLM 应用的三个时代:从 completion 到 agent
要理解 context engineering 为何会从 prompt engineering 中分化出来,最有效的视角是回顾 LLM 应用的演化史。我把 2020 年至今的工程实践划分为三个时代,它们之间的边界并非泾渭分明,但每一次跃迁都改变了「上下文」在系统中的角色。
时代一:Completion(2020–2022)。 这一时期的代表性产品是 GPT-3 的 `davinci` 系列以及 EleutherAI 的 GPT-J。工程师面对的是一个「文本接龙」接口:传入一段 prompt,模型续写出若干 token。当时的最佳实践是把所有希望模型看到的内容——指令、示例、问题——都拼成一段静态字符串。`max_tokens` 参数几乎决定了整个系统的形态。在这种模式下,「写好提示」就等于「写好整个应用」。我们今天熟悉的 system / user / assistant 三段式尚未成为主流,因为早期 API 根本没有 chat 概念。
时代二:Chat(2022–2024)。 ChatGPT 的发布是一个分水岭。多轮对话接口让「对话历史」成为一等公民,OpenAI 也随之引入了 `messages` 数组、`system` 角色、function calling 等结构化能力。这一时期,prompt engineering 的版图急剧扩张:few-shot、chain-of-thought、ReAct、self-consistency 都被封装成提示模板。然而,工程师很快发现两个新痛点:第一,多轮对话会无限累积,超过窗口就会被截断;第二,关键信息(用户偏好、任务约束)被淹没在噪声里,模型开始「忘记」五分钟前说过的话。`ChatHistory` 成了一个必须显式管理的对象。
时代三:Agent(2024–至今)。 当模型开始自主规划、调用工具、维护 todo list、与环境交互时,上下文的内容组成发生了质变。一段对话中可能同时存在:system 指令、检索到的文档、过去的工具返回结果、子任务的中间状态、失败重试的日志、自我反思的笔记。这种异构信息以每秒数次的速度被写入同一个 context window,而窗口的大小——哪怕是 200K tokens——也不是无限的资源。context engineering 一词正是在这个背景下被 Andrej Karpathy 等人反复提及:它不是 prompt engineering 的升级版,而是一门关于如何在有限的注意力预算内,为模型组织出一份「恰到好处的输入」的工程学科。
```mermaid flowchart LR E1["Era 1: Completion<br/>2020 - 2022"] --> E2["Era 2: Chat<br/>2022 - 2024"] E2 --> E3["Era 3: Agent<br/>2024 +"] E1 --> N1["single-shot, static prompt"] E2 --> N2["multi-turn, messages array"] E3 --> N3["long-horizon, tool-augmented"] ```
这个演化轨迹揭示了一个清晰的趋势:上下文从「静态字符串」变成了「动态数据流」。在 Era 1,prompt 是一次性写好的脚本;在 Era 3,context 是由多种来源实时拼装、压缩、丢弃、检索的活体系统。prompt engineering 关注的是「这一段文字怎么写更好」,context engineering 关注的是「这一整个窗口的状态如何演化才健康」。两者并非替代关系,但后者正在变成前者的母集。
1.2 提示工程的天花板:当 prompt 不再够用
很多工程师在 Era 2 积累了丰富的提示词技巧,却在进入 Era 3 后遭遇系统性失败。这种失败不是某一个提示词写得不好,而是提示工程作为单一手段已经触到了天花板。下面我用三个具体场景来说明这个天花板在哪里。
场景 A:超长多轮客服对话。 一位用户在第 47 轮提到「我对花生过敏,这个需求适用于所有订单」。如果系统使用一个简单的 `messages[:50]` 截断策略,这条关键事实很可能在第 60 轮就被丢弃,模型会再次推荐花生酱口味的零食。修一个 prompt 解决不了这个问题——你必须建立一个「关键事实优先保留」的机制,这已经越过了 prompt 的范畴。
场景 B:检索增强生成(RAG)的幻觉治理。 一个企业知识库问答系统,检索返回了 12 个相关文档,每个 500 tokens,加起来 6000 tokens;system prompt 300 tokens;用户问题 200 tokens;预留输出 800 tokens。窗口只剩 1700 tokens 给对话历史。工程师反复调优 prompt 让模型「只用文档回答,不要编造」,但当对话进入第 10 轮、history 突破 1700 tokens 时,文档会被静默截断,幻觉随之出现。这里的根因是窗口预算与内容优先级没有显式管理,prompt engineering 无法触及。
场景 C:长程 agent 的任务规划。 一个编程 agent 试图重构一个有 200 个文件的代码库。它需要:理解项目结构(工具结果)、记录已完成的文件(memory)、维护 todo list(meta)、查看上一次失败的 diff(反思)。这些内容互相竞争注意力,每多执行一步,下一步可用的窗口就少一些。提示词再精巧,也无法阻止「上下文越长,关键信号越被淹没」这一物理现象。
这三个场景的共同点是:问题的本质不再是「说什么」,而是「让模型看到什么、不看到什么、按什么顺序看、看多久」。这就是 context engineering 与 prompt engineering 的边界。
为了更精确地刻画这条边界,我列出一个对比清单:
| 维度 | Prompt Engineering | Context Engineering | |---|---|---| | 关注对象 | 单次输入的文本 | 整个 context window 的状态 | | 时间尺度 | 一次调用 | 跨多轮、多步骤的状态演化 | | 核心动作 | 措辞、格式、示例 | 选取、压缩、检索、丢弃 | | 失败模式 | 模型误解指令 | 关键信息被淹没或丢失 | | 度量指标 | 单次回答准确率 | 长程任务成功率、token 利用率 | | 典型工具 | Prompt 模板库 | 检索器、压缩器、记忆系统 |
两者的关系不是非此即彼,而是包含:context engineering 把 prompt engineering 视为它的一个子模块——即「指令部分的设计」。但在 agent 时代,只优化子模块而忽略系统的整体组织,是一种典型的局部最优陷阱。
1.3 三类根本性约束
任何 context engineering 实践都必须直面三个不可消除的物理约束。它们不是工程缺陷,而是 LLM 当前架构的内禀属性。
1.3.1 上下文窗口的物理边界
所有商用 LLM 都存在一个最大 token 数(context window),从 GPT-3.5 的 4K 到 Gemini 1.5 Pro 的 1M 不等。然而,窗口大小不是免费的午餐。更大的窗口意味着:
1. 推理成本线性增长。自注意力机制的计算复杂度是 $O(n^2)$——窗口从 4K 扩到 128K,单次推理的浮点运算量增加约 1000 倍。厂商必须用 KV cache、稀疏注意力、滑动窗口等技巧才能压低成本,而即便如此,长上下文的单价仍然显著高于短上下文。 2. 延迟上升。更大的 KV cache 意味着更慢的 prefill 与更高的显存占用。在交互式应用中,从 200ms 增加到 2s 的延迟往往就已经不可接受。 3. 行为偏移。多项研究(包括 Liu et al. 2023 的「Lost in the Middle」)证明,模型对窗口中不同位置信息的利用效率并不均匀——开头和结尾最强,中间最弱。简单地把窗口撑大,并不能保证关键信息被同等对待。
因此,窗口是稀缺资源,必须像对待内存一样去管理:分配、回收、淘汰、压缩,每一项都需要工程上的权衡。
1.3.2 注意力稀释的成本曲线
继续深入上一点。Transformer 的注意力机制本质上是让序列中每一对 token 互相「看一眼」。当序列长度从 $n$ 增长到 $n+k$ 时,每个 token 需要多关注 $k$ 个新 token。从信号处理的角度看,这等同于给原有信号叠加了 $k/(n+k)$ 比例的新噪声。注意力的「信噪比」随序列长度单调下降。
更精确地,如果我们把 context 中每条信息的「重要性」记为 $w_i$,模型从中抽取的有效信号可以粗略建模为:
$$S(n) \propto \frac{\sum_{i=1}^{n} w_i \cdot f(i, n)}{\sqrt{n}}$$
其中 $f(i, n)$ 是位置权重函数(开头和结尾较高)。分母 $\sqrt{n}$ 来自注意力分数归一化的统计特性。这个公式告诉我们:窗口越长,每条信息能分到的注意力份额就越小。当 $n$ 翻倍时,单条信息的有效信号约下降到原来的 $1/\sqrt{2} \approx 0.71$ 倍。
这就是为什么「把全部历史塞进 prompt」不是好策略——它不仅烧钱,还会主动稀释信号。context engineering 的一个核心动作就是主动丢弃低价值信息,让注意力集中在高价值信号上。
1.3.3 信息熵在长序列中的衰减
第三条约束更具哲学意味:长上下文本身会变得「难以使用」。当我们把 50K tokens 的代码库塞给模型,它能「看完」,但它无法像人类那样记住一个符号在第 100 个文件中的定义。LLM 没有真正的记忆,它只能在一次前向传播中重新激活所有信息。当信息密度降低、互相引用增多时,模型的推理能力会显著退化。
一个有用的类比是:context window 之于 LLM,就像工作记忆之于人类。我们可以同时处理 7±2 个组块(Miller's Law),但当我们尝试同时处理 70 个组块时,错误率会急剧上升。context engineering 本质上是在为模型管理工作记忆。
这三条约束共同决定了 context engineering 的设计空间:
- 既然窗口是物理边界 → 必须**显式管理 token 预算**。
- 既然注意力会稀释 → 必须**主动压缩与丢弃**。
- 既然信息熵会衰减 → 必须**检索 + 摘要 + 结构化**。
后续章节将围绕这三个方向展开对应的技术:第三章讲压缩,第四到第六章讲检索,第七章到第九章讲记忆系统。但在此之前,我们需要一个统一的语言来描述 context 本身。这就是下一节的内容。
1.4 统一心智模型:上下文五元组
要把 context engineering 从一袋零散技巧升级为一门工程学科,第一步是建立一个可枚举、可度量、可组合的心智模型。我提议用五元组来描述任何一次模型调用所消费的完整上下文:
$$\text{Context} = \langle \text{Instruction}, \text{Knowledge}, \text{Memory}, \text{Tool}, \text{Meta} \rangle$$
下面对每一项给出严格的定义、典型来源与判断标准。
Instruction(指令):模型应当如何行动。包括系统提示(persona、角色、风格约束)、任务指令(这次要做什么)、输出格式要求(JSON、Markdown、函数调用)。Instruction 的特征是相对静态——一旦设定,不会在多次调用中频繁变化;以及强优先级——它定义了模型的「宪法」,其他上下文都要服从它。
Knowledge(知识):模型回答问题所需的参考材料。最典型的来源是 RAG 检索到的文档片段、few-shot 示例、工具的 schema 描述。Knowledge 的特征是按需加载——同一 agent 在不同任务下需要的知识完全不同;以及可验证——每一条知识都应该有出处。
Memory(记忆):跨时间持久化的信息。包括对话历史、用户偏好、过去的工具调用记录、自我反思。Memory 的特征是单调累积——它会随着交互增长;以及必须管理——无界增长会击穿窗口。
Tool(工具结果):模型调用外部工具后获得的输出。包括 API 返回值、数据库查询结果、代码执行输出、网页抓取内容。Tool 的特征是结构化——通常是 JSON 或表格;以及异构尺寸——一个 API 调用可能返回 10 tokens,也可能返回 10K tokens。
Meta(元数据):不直接喂给模型、但用于系统调度与可观测性的信息。包括 token 计数、压缩比例、检索得分、时间戳、trace ID、A/B 测试分组。Meta 的特征是系统专用——它对模型透明,只服务于工程的监控、调试与优化。
这五个维度不是互斥的——同一条信息可以根据上下文扮演不同角色。例如一段对话历史,既是 Memory 也是 Knowledge(当我们把它作为示例展示给模型时)。但五元组提供了一种思考的脚手架:当我们面对一个 context 管理难题时,可以问「这个问题出在五元组的哪一项?」。是 Instruction 写得不清楚?还是 Knowledge 检索错了?是 Memory 太长挤占了空间?还是 Tool 结果太大?还是 Meta 缺失导致无法调试?
```mermaid flowchart TD IN["Instruction"] --> PKT["Context Packet"] KN["Knowledge"] --> PKT ME["Memory"] --> PKT TR["Tool Result"] --> PKT MT["Meta"] --> PKT PKT --> LLM["LLM"] PKT --> LOG["Audit Log"] MT --> LOG ```
这个图揭示了一个常被忽视的事实:Meta 是一条平行的管道。它和前四项一起进入 ContextPacket,但不进入模型本身;它流向可观测性系统,成为工程师理解系统行为的唯一窗口。在长程 agent 中,Meta 往往比模型输出更重要——因为模型的输出只是一瞬,而 Meta 记录了整个演化过程。
为了让五元组不只是概念,我还给出一组可操作的判断清单,帮助你在工程中快速分类:
1. 这段文本改变了模型「如何回答」吗?→ Instruction。 2. 这段文本是模型回答所需的「事实或示例」吗?→ Knowledge。 3. 这段文本是「之前发生过的事」吗?→ Memory。 4. 这段文本是「外部系统刚刚返回的」吗?→ Tool。 5. 这段文本是「给系统自己看的」吗?→ Meta。
分类的准确性直接决定了后续压缩策略的有效性。在第五章我们会看到,不同维度的内容应该有不同的压缩顺序:Memory 最容易丢弃(因为它可以被总结),Knowledge 次之(因为它可以被重检索),Tool 结果最难丢弃(因为它代表了与外部世界交互的不可逆成本)。Instruction 和 Meta 则几乎永远保留——前者是宪法,后者是审计线。
1.5 一个能自我管理的最小上下文系统
理论讲了这么多,是时候给出可运行的代码。下面这段约 200 行的 Python 实现了一个 `ContextContainer` 基类,它封装了五元组、token 预算、压缩管线与快照机制。后续 14 章的所有示例都会基于它做扩展,因此我建议你直接 fork 到自己的项目里。
```python """ Chapter 1 · ContextContainer A minimal, self-managing context system that we will reuse throughout this book. Target: ~200 lines, fully commented, only uses the standard library. """
from __future__ import annotations
import copy import time import uuid from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import Any, Dict, List, Optional, Protocol
─────────────────────────────────────────────────────────────────────────────
1. Token estimation
─────────────────────────────────────────────────────────────────────────────
def estimate_tokens(text: str) -> int: """Rough estimator: ~1 token / 4 chars for ASCII, ~1 token / 1.5 chars for CJK. Replace with tiktoken in production for accuracy.""" if not text: return 0 cjk = sum(1 for c in text if "\u4e00" <= c <=