# 上下文工程
> 💡 **学习指南**：提示词工程解决的是“怎么把话说清楚”，上下文工程解决的是“让模型在合适的时刻看到合适的信息”。本章节会围绕一个问题展开：**在有限的上下文窗口里，如何既让模型懂你，又不把钱烧光？**

在开始之前，建议你先补两块“基础砖”：

- **Token 是什么**：可以先阅读 [大语言模型入门](./llm-principles.md) 的「分词 & Token」部分。
- **Prompt 是什么**：如果你还不熟悉 System / User / Assistant 的基本结构，可以先看 [提示词工程](./prompt-engineering/)。

---

## 0. 引言：为什么聊着聊着，它就忘事，还越来越贵？

<AgentContextFlow />

很多人在实际使用大模型时都会遇到类似的情况：

- 聊到一半，模型突然“忘记”之前说过的关键条件；
- 长对话里，前后回答自相矛盾，很难保持同一套设定；
- 对话轮次一多，账单像打车计价一样不断往上走。

直觉上，我们会以为是：**“这个模型记性不好”**。
但大多数时候，问题并不在于模型“不会记”，而在于我们**没有设计好它能看到的上下文**。

<IntroProblemReasonSolution />

面对这些挑战，单纯依靠“写好提示词”已经捉襟见肘。我们需要一套更系统的工程方法，来在有限的窗口和预算内，让模型始终获得最关键的信息。这正是**上下文工程**试图解决的问题。

---

## 1. 什么是“上下文工程”？（定义 + 场景）

先给一个简短的工作定义，再看几个典型场景。

> 上下文工程，是一门为 LLM 构建和管理“信息环境”的工程方法，决定模型“看到什么、忽略什么、什么时候看到”，从而在有限的上下文窗口内稳定完成任务。

你可以简单地把它理解成三件事：整理信息、控制窗口、管理成本。  
常见会用到它的场景包括：

- 对话型 Agent 和客服机器人
- 代码 / 文档助手
- 多轮工具调用和长流程编排

接下来，我们就从一个真实团队的“血泪教训”出发，看看他们是怎么一点点从“只会写 Prompt”进化到“会做上下文工程”的。

---

## 2. 从"血泪教训"说起：Manus 团队踩过的坑

本章案例来自 **Manus**（一款通用 AI Agent）。
与普通对话不同，Manus 需要自主规划并调用工具完成长任务（涉及几十甚至上百轮交互）。

这带来了核心矛盾：
- **如果不记**：关键信息丢失，任务中断。
- **全记**：成本和延迟爆炸，甚至超出窗口限制。

Manus 团队经历过多次架构重构，才明白一个道理：**上下文不能只靠“写”，而要靠“设计”。**

### 2.1 四次重构教会我们什么？

Manus 的联合创始人季逸超分享过他们的"踩坑史"：

| 阶段 | 遇到的问题 | 当时的想法 | 结果 |
| :--- | :--- | :--- | :--- |
| **第一次** | AI 聊着聊着就忘事 | "多写点提示词就好了" | 越写越长，越写越贵 |
| **第二次** | 重要信息总被挤掉 | "把重要的多复制几遍" | 文本更长，成本更高 |
| **第三次** | 账单高得吓人 | "能不能复用之前的计算？" | 找到降低重复计算成本的方式 |
| **第四次** | 长文档处理不了 | "能不能需要时再查？" | 建立“图书馆+按需检索”的方案 |

**核心领悟**：**不是记得越多越好，而是记得越巧越好**。

### 2.2 AI 的"记性"到底像什么？

**传统电脑内存** = **硬盘**：
- 容量大：可以长期保存大量数据；
- 价格低：存放一年成本较低；
- 读写速度相对较慢，查找信息需要一定时间。

**AI 的上下文** = **小黑板**：
- 读写快：模型可以在一次调用中直接看到全部上下文；
- 容量有限：写满后不得不擦除旧内容；
- 每写入一个 token 都会带来额外计算与费用。

**Manus 的经验**：**小黑板要用得省，用得巧，别用来存百科全书**。

---

## 3. 第一步：认识成本 - 你的每一分钱花在哪？

### 3.1 为什么要先看成本？

让我们看看一次典型的 AI 对话，你的钱是怎么花的：

```
💰 成本构成（一次对话）：
├─ 70% 重复看旧内容（"刚才聊了什么？"）
├─ 20% 处理新内容（"现在说什么？"）  
└─ 10% 生成回复（"怎么回答？"）
```

**惊人发现**：**70% 的钱花在让 AI 重新看你之前说过的话！**

### 3.2 什么是 KV Cache？（前缀复用）

在讨论价格之前，我们得先搞懂一个核心技术概念：**KV Cache（键值缓存）**。
别被这个技术名词吓到，它其实就是 AI 的“短期记忆速查表”。

- **没有 KV Cache 时**：AI 每次都要像第一次看到这篇文章一样，从第一个字开始重新阅读、理解、计算。
- **有了 KV Cache 时**：AI 会把看过的部分（Pre-fill）计算结果存下来。下次如果开头的内容没变，它就直接调取记忆，不用重新算了。

这就好比：
> 你去考场考试。
> **情况 A**：每次都要把整本教材从头读一遍，再开始答题。（慢、累、贵）
> **情况 B**：教材内容你已经背滚瓜烂熟了（Cache），坐下直接答题。（快、轻松、便宜）

在云厂商的计费表里，**“背过的书”（Cache Hit）**通常比**“新看的书”（Cache Miss）**便宜 90% 以上。

### 3.3 "背课文" vs "现查现用"的价格差

以 Claude 为例：
- **现查现用**（没缓存）：$3.00 / 百万字
- **背过再用**（有缓存）：$0.30 / 百万字  
- **相差 10 倍**！

**Manus 的实践**：通过让 AI "背课文"，他们把成本从 **$0.15 降到 $0.02**，**省了 87%**！

<ContextWindowVisualizer />

### 3.4 避坑指南：别让时间戳毁了你的“缓存”

很多开发者习惯把“当前时间”写在 System Prompt 的第一句，觉得这样很严谨。
**但这其实是上下文工程中最大的反模式之一。**

想象一下：你背了一整本历史书（System Prompt），结果书的第一行写的是“现在的秒数”。
如果这行字每秒都在变，那你上一秒背的所有内容，下一秒就全废了——你得从头再背一遍。

这就是**前缀复用（KV Cache）**的死穴：**只要开头变了，后面全都要重算。**

#### 错误示范：把动态信息放前面
```text
System: 现在是 2024-01-01 12:00:01。你是助手...
(一分钟后)
System: 现在是 2024-01-01 12:01:01。你是助手...
```
**后果**：虽然只变了几个字，但因为在开头，导致后续 99% 的固定内容无法复用缓存，每次请求都像第一次一样慢且贵。

#### 正确姿势：动静分离
```text
System: 你是助手... (这里放几千字的固定规则、知识库)
User: (在这里通过工具调用或用户消息传入当前时间)
```
**好处**：前面的几千字规则永远不变，AI 只需要“背”一次。后续请求直接调用记忆，速度极快。

👇 **动手点点看**：
点击下方的开关，开启**“背课文加速”**，然后多次点击“发送新请求”。
观察一下：当第一块内容变成“已背过”时，**开口速度（TTFT）**会发生什么变化？

<KVCacheDemo />

---

## 4. 第二步：滑动窗口 - 当"记性"变成"成本"

随着对话越来越长，最先遇到的问题就是：**窗口满了怎么办？**

### 4.1 为什么“先进先出”会出问题？

最简单的记忆管理是**滑动窗口（Sliding Window）**：**新的进来，旧的出去**。
这听起来很公平，但在实际任务中却是个灾难。

**场景重现**：
```text
对话记录：
[1] 用户：我是张三，负责支付系统  
[2] 用户：项目用 Go 语言开发
[3] 用户：数据库是 PostgreSQL
...
[20] 用户：帮我写个接口
```
**结果**：当聊到第 20 句时，第 1 句“我是张三”已经被挤出了窗口。AI 彻底忘了你是谁，也不知道你在负责什么系统。

**问题本质**：这种策略把**重要信息**（身份、技术栈）和**废话**（“好的”、“收到”）同等对待，一起被踢了出去。

### 4.2 "中间失忆症" - 为什么 AI 总看不到关键信息？

除了“忘得快”，AI 还有一个怪癖：**它也会“看漏”**。
研究发现：**AI 对开头和结尾最敏感，中间最容易被忽略**。这就是著名的 **Lost in the Middle（中间迷失）**现象。

**U 型记忆曲线**：
```text
位置：开头 → 中间 → 结尾
记忆： 高  →  低  →  高
```

👇 **动手点点看**：
1. 先试试**“滑动窗口”**：在下面的聊天框里多发几条消息，看看旧的对话是怎么被无情“挤出去”的。
2. 再看看**“中间迷失”**：观察一下，当关键信息藏在整段话的中间位置时，检索成功率是不是最低的？

<SlidingWindowDemo />
<LostInMiddleDemo />

**解决方案**：把关键信息放在**开头**（系统提示）或**结尾**（用户问题）。

---

## 5. 第三步：选择性保留 - 如何"钉"住关键信息？

既然“先进先出”不靠谱，那我们该怎么办？
Manus 的答案是：**建立“信息等级制度”**。

### 5.1 为什么要给信息分等级？

不再平等对待每条信息，而是根据重要程度决定它们的去留：

| 等级 | 信息类型 | 待遇 | 成本影响 |
| :--- | :--- | :--- | :--- |
| **VIP** | 系统设定、用户身份 | **永远保留** | +15% 成本 |
| **重要** | 当前任务目标 | **任务期内保留** | +10% 成本 |
| **一般** | 普通对话历史 | **最近 5 轮保留** | 基准成本 |
| **可弃** | 可检索的知识 | **用时再查** | -60% 成本 |

**核心思想**：**用 25% 的成本增加，换取 90% 的关键信息保留**。

### 5.2 "钉钉子"策略

你可以把上下文窗口想象成一面黑板：
- **VIP 信息**：用钉子死死**钉在**黑板最上面（System Prompt）。
- **重要信息**：用磁铁**吸在**黑板中间（Context Injection）。
- **普通对话**：写在黑板下半部分，满了就擦掉旧的（Sliding Window）。

👇 **动手点点看**：
试着在下面的演示里，把某条重要的对话“钉”住。
观察一下：当你继续聊天时，被钉住的信息是不是一直都在，而没钉住的就被挤走了？

<SelectiveContextDemo />

---

## 6. 第四步：RAG - 当"记性"需要"图书馆"

有时候，我们要处理的信息太多了（比如几百页的技术文档），黑板根本写不下。这时候就需要外挂大脑——**RAG（检索增强生成）**。

### 6.1 为什么“小黑板”不够用？

Manus 面对百万字级的技术文档时，对比了两种做法：

1.  **全量写入**：所有内容一次性塞进上下文。
    *   **后果**：黑板瞬间被占满，处理极慢，而且根据“中间迷失”理论，AI 根本记不住中间的内容。
    *   **成本**：约 $50/次，等待 15 秒。
2.  **按需检索（RAG）**：先去图书馆（数据库）查，只把相关的几段话抄到黑板上。
    *   **后果**：黑板很清爽，AI 聚焦于关键信息。
    *   **成本**：约 $0.5/次，等待 2 秒。

**省了 99% 的钱，87% 的时间！**

### 6.2 "查资料"的最佳实践

Manus 的经验总结：
*   **每本书撕成多大片？** 500-1000 字效果最好。
*   **一次查几本书？** 3-5 本，多了反而干扰。
*   **多相关的书才查？** 相似度 > 0.7，避免“硬凑”不相关的内容。

👇 **动手点点看**：
在搜索框里输入问题（比如“如何重置密码”），看看系统是如何从一大堆文档里只找出最相关的那几条的。

<RAGSimulationDemo />

---

## 7. 第五步：压缩 - 如何让"小黑板"写得更密？

如果信息都很重要，实在删不掉，又不想查资料怎么办？
那就只能**把字写小点**——这就是**上下文压缩**。

### 7.1 什么时候需要"缩写"？
*   检索回来的资料太厚（>2000 字）。
*   对话历史太啰嗦（占了 >80% 黑板空间）。
*   需要快速回答，不想让 AI 读长篇大论。

### 7.2 "缩写"的三种境界

| 压缩方式 | 压缩率 | 保留什么 | 适用场景 | 省钱效果 |
| :--- | :--- | :--- | :--- | :--- |
| **总结式** | 70% | 主要意思 | 快速了解 | 省 30% |
| **要点式** | 50% | 关键要点 | 结构化输出 | 省 50% |
| **表格式** | 30% | 核心数据 | 程序处理 | 省 70% |

👇 **动手点点看**：
选择不同的压缩策略，看看长篇大论是如何变短、变精炼的。

<ContextCompressionDemo />

---

## 8. 系统整合：打造 AI 的“记忆宫殿”

前面我们像搭积木一样，学习了各种独立的策略：
*   **KV Cache**：帮我们省钱（第 3 章）
*   **滑动窗口**：帮我们腾位置（第 4 章）
*   **分级保留**：帮我们留重点（第 5 章）
*   **RAG**：帮我们开外挂（第 6 章）

现在，是时候把这些积木搭成一座完整的城堡了——我们称之为 Manus 的**“记忆宫殿”**。

### 8.1 像盖房子一样组装上下文

不要把上下文看作一堆乱糟糟的文字，而要把它看作一座分层的建筑。每一层都有它独特的功能和“居住规则”。

👇 **动手点点看**：
点击“开始建造”，看看我们是如何一层层把这座宫殿盖起来的。

<MemoryPalaceDemo />

### 8.2 为什么这样设计最强？

这座宫殿的设计哲学，其实就为了解决三个矛盾：

1.  **地基（System Prompt）—— 解决“贵”的问题**
    *   **矛盾**：系统设定（你是谁、规则是什么）最长，每次都要发。
    *   **解法**：把它放在最底层，利用 **KV Cache** 技术，只要不改动，AI 就能“背诵全文”。后续几百轮对话，这部分的计算成本几乎为 **0**。

2.  **支柱（Task Context）—— 解决“忘”的问题**
    *   **矛盾**：对话一长，AI 容易忘了最初的任务目标（比如“写一个贪吃蛇游戏”）。
    *   **解法**：利用**分级保留**策略，把任务目标“钉”在第二层。不管聊了多少轮，这层永远不删，确保 AI 不忘初心。

3.  **顶层（Chat & RAG）—— 解决“乱”的问题**
    *   **矛盾**：又有新对话，又有查到的资料，混在一起容易晕。
    *   **解法**：
        *   **客厅（对话）**：用**滑动窗口**管理，只留最近 5-10 句热乎的。
        *   **图书馆（RAG）**：资料用完即走，不占地方。

### 8.3 实战效果

Manus 团队把这套架构搬上线后，效果立竿见影：

*   **省钱了**：因为地基被“背”下来了，每轮对话的成本暴跌 **84%**。
*   **变快了**：AI 不用每次都从头读几千字，平均响应时间从 8 秒缩短到 **2 秒**。
*   **更准了**：关键信息被“钉”死，再也不会聊着聊着就忘了自己是干嘛的。

---

## 9. 实战模板：直接抄作业

为了让你更直观地理解这套机制是如何运作的，我们为你准备了**全链路模拟**。

请选择一个场景，点击“下一步”，看看从用户发问到 AI 回答的几秒钟内，**记忆宫殿**是如何动态调取、组装和清理上下文的。

<MemoryPalaceActionDemo />

### 📝 拿来即用的实战设计

如果你要设计一个类似 Manus 的系统，不要只盯着 Prompt 怎么写，更要关注**系统架构如何调度上下文**。

以下是两个经典场景的**系统设计蓝图**，包含了**提示词设计**和**代码逻辑（伪代码）**。

#### 场景 1：全栈工程师 Agent（长程记忆型）
> **核心挑战**：任务周期长，容易忘了最初的需求和项目背景。
> **解决策略**：System 层（身份）+ Task 层（钉死目标）+ Chat 层（滑动窗口）。

**1. 系统提示词 (Layer 1 & 2)**
```markdown
# Layer 1: 身份设定 (System Prompt) - 永远不变，利用 KV Cache
你是一名资深的全栈工程师，精通 Python 和 Vue3。
代码风格：
- 变量命名严格遵守 PEP8
- 关键逻辑必须包含注释
- 优先使用项目已有的工具函数

# Layer 2: 任务锁定 (Task Context) - 任务期间不许删
当前任务：重构支付模块 (payment_module)
核心约束：
1. 必须兼容旧版 API 接口 v1.0
2. 数据库迁移脚本必须是幂等的
3. 截止时间：本周五
```

**2. 上下文组装逻辑 (Pseudo-Code)**
```python
def build_engineer_context(user_input, chat_history, task_info):
    context = []
    
    # 1. 地基层：身份设定 (利用 KV Cache 缓存)
    # 这部分内容几百轮对话都不变，计算成本几乎为 0
    context.append(SYSTEM_PROMPT)
    
    # 2. 支柱层：任务锁定 (Pinned)
    # 无论对话多长，这部分永远插入在 System 之后
    context.append(f"当前任务：{task_info}")
    
    # 3. 检索层：代码片段 (RAG)
    # 根据用户的问题，去代码库里找相关的代码
    relevant_code = search_codebase(user_input)
    if relevant_code:
        context.append(f"参考代码：\n{relevant_code}")
    
    # 4. 交互层：对话历史 (Sliding Window)
    # 只取最近 10 轮，避免撑爆上下文
    recent_chat = chat_history[-10:] 
    context.extend(recent_chat)
    
    # 5. 最新输入
    context.append(user_input)
    
    return context
```

#### 场景 2：智能客服 Agent（精准问答型）
> **核心挑战**：成本敏感，且绝对不能胡说八道。
> **解决策略**：System 层（强约束）+ RAG 层（动态注入）。

**1. 系统提示词 (Layer 1)**
```markdown
# Layer 1: 身份设定 (System Prompt)
你是一名专业的电商客服专员。
回复原则：
1. 语气温柔、专业、简洁
2. **绝对禁止**编造事实，只根据[参考资料]回答
3. 如果资料里没有答案，请直接回答“非常抱歉，这个问题我需要转接人工客服”
```

**2. 上下文组装逻辑 (Pseudo-Code)**
```python
def build_support_context(user_input):
    context = []
    
    # 1. 地基层：身份设定
    context.append(SYSTEM_PROMPT)
    
    # 2. 图书馆层：动态检索 (RAG)
    # 只有客服场景，RAG 才是主角，放在中间位置
    docs = vector_db.search(user_input, top_k=3)
    
    context.append("【参考资料开始】")
    for doc in docs:
        context.append(doc.content)
    context.append("【参考资料结束】")
    
    # 3. 交互层：极短的历史
    # 客服通常不需要太久远的记忆，保留最近 3 轮即可
    context.extend(get_recent_chat(limit=3))
    
    context.append(user_input)
    
    return context
```

---

## 10. 名词对照表

| 英文术语 | 中文对照 | 解释 |
| :--- | :--- | :--- |
| **Context Window** | 上下文窗口 | 模型一次性能够处理的文本最大长度（包括输入和输出）。超出限制的内容会被截断或遗忘。 |
| **Token** | 词元 | LLM 处理文本的最小单位。通常 1 个 Token 约等于 0.75 个英文单词或 0.5 个汉字。计费和窗口限制都以此为单位。 |
| **KV Cache** | KV 缓存 | 一种推理加速技术，通过缓存已经计算过的注意力键值对，避免对重复前缀进行重复计算，显著降低延迟和成本。 |
| **RAG** | 检索增强生成 | 在回答问题前，先从外部知识库检索相关信息，作为上下文提供给模型，以减少幻觉并扩展知识边界。 |
| **Sliding Window** | 滑动窗口 | 最基础的上下文管理策略。保持窗口内 Token 数量恒定，当新内容进入时，自动移除最早的旧内容。 |
| **Lost in Middle** | 中间迷失 | 大模型的一种局限性。研究表明，模型对长上下文开头和结尾的信息记忆最深，而容易忽略中间部分的信息。 |
| **System Prompt** | 系统提示 | 位于对话最开始的指令，用于设定模型的身份、行为规范、回复风格和核心任务。 |
| **Few-shot** | 少样本学习 | 在提示词中提供几个“问题-答案”的示例，帮助模型快速理解任务模式和输出格式。 |
| **Chain of Thought** | 思维链 | 引导模型在给出最终答案前，先输出推理步骤。这种方法能显著提升模型解决复杂逻辑和数学问题的能力。 |
| **Hallucination** | 幻觉 | 模型自信地生成看似合理但实际上错误或不存在的信息的现象。 |
| **Embedding** | 向量化 | 将文本转换为高维数值向量的技术。语义相似的文本在向量空间中的距离更近，是语义搜索的基础。 |
| **Vector DB** | 向量数据库 | 专门用于存储和检索向量数据的数据库。支持通过相似度搜索快速找到与查询最匹配的文档片段。 |
| **Temperature** | 温度 | 控制模型输出随机性的超参数。数值越高（如 0.8）输出越多样、有创意；数值越低（如 0.2）输出越确定、严谨。 |
| **TTFT** | 首字延迟 | Time to First Token，即从用户发送请求到模型输出第一个 Token 所花费的时间，是衡量交互体验的关键指标。 |

---

## 总结：上下文工程的本质

Manus 的四次重构告诉我们：

**从实践来看**：不是记得越多越好，而是记得越有结构、越有选择性越好。

**从成本视角看**：
- 大部分浪费来自对固定前缀的重复计算，需要通过前缀稳定和缓存机制解决；
- 重要信息被误删，往往源于“一视同仁”的滑动窗口，需要通过信息分级与钉住策略解决；  
- 面对超长文档和知识库时，仅依赖增大上下文窗口并不现实，必须结合检索与压缩机制。

目标是：在给定的模型与上下文上限下，让每一个 token 的投入都具备明确的用途。
