第 3 章 上下文工程
本章要解决的问题
Agent 聊几轮就「失忆」、长文档塞不进提示词------上下文窗口明明够大,为什么效果还是崩?
章节大纲
- 3.1 上下文窗口:空间、成本与策略
- 3.2 对话历史管理:压缩、摘要、滑动窗口
- 3.3 长文本处理:RAG 前置与上下文编排
- 3.4 状态持久化与跨会话记忆
- 🛠 解决方案:Token 预算分配模板 + 上下文溢出排错指南
3.1 上下文窗口:空间、成本与策略
3.1.1 为什么"窗口够大"还是崩
很多人的困惑:"模型支持 128K 窗口,为什么我塞 20K 就效果崩了?"
答案有三个层面:
| 层面 | 原因 | 缓解 |
|---|---|---|
| 注意力稀释 | 窗口越大,模型对关键信息的"注意力密度"越低(第 2 章注意力机制) | 关键信息前置、冗余裁剪 |
| 指令被淹没 | 系统提示词淹没在长历史里,模型"忘记"了任务要求 | 系统提示词放在消息最前 |
| 上下文污染 | 无关内容(历史闲聊、工具噪音)干扰当前判断 | 按需裁剪、摘要化 |
窗口是"容量上限",不是"推荐用量"。 就像桌子能放 100 本书,但你只需要 5 本------把桌子堆满反而找不到要用的那本。上下文工程的核心:在窗口内,用最少的 token 装下最关键的信息。
3.1.2 Token 预算分配模板
生产级 Agent 应该在系统层面对窗口做预算分配:

图 1:Token 预算分配(32K 例)
erlang
窗口总预算(以 32K 为例)
├── 系统提示词(角色/任务/规则) ~2K (6%)
├── 工具定义(Function Schema) ~4K (12%)
├── 对话历史(用户+助手消息) ~8K (25%)
├── 当前输入(本次请求) ~8K (25%)
├── 工具执行结果(观察) ~4K (12%)
└── 输出预留(max_tokens) ~6K (20%,必须留!)
输出预留是最容易被忽视的一环 ------不留足输出空间,模型会"生成到一半被截断",这是很多"答案不完整"问题的真正原因(呼应第 24 章 max_tokens 设置)。
3.1.3 预算的硬性纪律
- 总量设上限 :
总输入 + 输出预留 ≤ 窗口,超了就触发裁剪策略(3.2 节)。 - 优先级排序:系统提示词 > 当前输入 > 工具定义 > 最近历史 > 旧历史。裁剪从低优先级开始。
- 量化意识 :1 个汉字 ≈ 1~2 token,1 个英文单词 ≈ 1.3 token,心里要有数。不同模型 tokenizer 差异很大,生产环境应以 API 返回的 token 统计为准,而非字符估算。
3.2 对话历史管理
3.2.1 三大策略:截断、滑动窗口、摘要
| 策略 | 做法 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| 硬截断 | 只保留最近 N 条消息 | 零成本 | 丢失早期关键信息 | 简单对话 |
| 滑动窗口 | 保留最近 N 条 + 固定系统提示词 | 实现简单、状态新鲜 | 早期信息全丢 | 大多数场景 |
| 摘要压缩 | 旧历史压缩成摘要 + 保留最近原文 | 保留长期信息 | 摘要损失细节 | 长会话 |

图 2:历史管理三策略
3.2.2 摘要压缩的实现(推荐组合)
生产推荐"摘要 + 滑动窗口"组合:超龄历史转摘要,近期保留原文。

图 3:增量摘要压缩
python
import json
from openai import OpenAI
client = OpenAI(base_url="https://api.deepseek.com", api_key="<你的Key>")
class ConversationManager:
def __init__(self, system_prompt, max_recent=8, max_tokens=12000):
self.system_prompt = system_prompt
self.max_recent = max_recent # 保留的最近消息数
self.max_tokens = max_tokens
self.summary = "" # 长期摘要
self.history = [] # 近期消息
def add(self, user_msg, assistant_msg):
self.history.append({"role": "user", "content": user_msg})
self.history.append({"role": "assistant", "content": assistant_msg})
self._maybe_compress()
def _estimate_tokens(self, messages):
"""粗略估算 token 数(1 个汉字≈1~2 token,1 个英文单词≈1.3 token)。
生产环境应以模型 tokenizer 或 API 返回的 token 统计为准。"""
total = sum(len(m["content"]) for m in messages if isinstance(m.get("content"), str))
return int(total * 1.5) # 中文偏多的粗略系数
def _maybe_compress(self):
# 双阈值触发:条数超限 OR token 估算超预算 70%
if len(self.history) <= self.max_recent * 2 and \
self._estimate_tokens(self.history) < self.max_tokens * 0.7:
return
oldest = self.history[: self.max_recent]
self.summary = self._summarize(oldest, self.summary) # 增量摘要
self.history = self.history[self.max_recent:]
def _summarize(self, messages, old_summary):
resp = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content":
f"把以下对话合并进已有摘要,输出更新后的摘要。"
f"已有摘要:{old_summary}\n新对话:{json.dumps(messages, ensure_ascii=False)}"}],
temperature=0.2, max_tokens=1000,
)
return resp.choices[0].message.content
def build_messages(self):
msgs = [{"role": "system", "content": self.system_prompt}]
if self.summary:
msgs.append({"role": "system",
"content": f"【历史摘要】{self.summary}"})
msgs.extend(self.history)
return msgs
工程要点:
- 摘要增量更新:旧摘要 + 新消息 → 新摘要,避免每次全量重摘要(省 token)。
- 摘要放 system:以"历史摘要"身份注入,不污染近期对话流。
- 只压缩最旧:近期原文保留,保证当前话题的完整上下文。
3.2.3 何时触发压缩:两个阈值
- 条数阈值:消息超过 N 条(如 16 条)触发。
- Token 阈值:估算总 token 超过窗口预算的 70% 触发。
- 双阈值取先到者:任一触发即压缩,防"对话太长直接爆窗"。
3.3 长文本处理:RAG 前置与上下文编排
3.3.1 两条路线:全量塞入 vs 检索式
面对长文档(一本书、万行代码),两条路线:
| 路线 | 做法 | 适用 |
|---|---|---|
| 全量塞入(大窗口) | 直接放窗口(Qwen 1M 等大窗口模型) | 单次分析、文档不长于窗口 |
| 检索式(RAG,第 8 章) | 分块 + 向量检索,只取相关块 | 长文档、多文档、频繁问答 |
经验法则:文档 ≤ 窗口的 50% 且只问一次 → 全量塞;否则 → RAG。 全量塞简单但贵(所有 token 都计费),RAG 省 token 但引入检索质量变量(第 8 章详述)。
3.3.2 上下文编排:把"喂什么"当架构设计
上下文工程的高级形态是编排(Context Engineering)------不是被动管理历史,而是主动设计"模型每一步看到什么":
| 编排动作 | 说明 | 呼应章节 |
|---|---|---|
| 按需投喂 | 只给当前子任务需要的信息(多 Agent 场景) | 第 16 章 |
| 关键信息前置 | 任务指令放最前,避免被淹没 | 本章 |
| 工具结果精简 | 工具返回只回填必要字段 | 第 14 章 |
| 信息分级 | 系统级(不变)/会话级(变)/临时级(一次) | 第 9 章 |
核心思想:上下文不是"记录",是"注意力预算"------模型的注意力有限(3.1.1),编排就是决定"把预算花在哪"。
3.4 状态持久化与跨会话记忆
3.4.1 会话内 vs 跨会话:记忆的分层
| 层 | 存活期 | 存储 | 例子 | 呼应 |
|---|---|---|---|---|
| 会话上下文 | 单次会话 | 内存/请求参数 | 本次对话历史 | 本章 |
| 工作记忆 | 任务期间 | 服务内存/Redis | 中间计算结果 | 第 6 章 |
| 长期记忆 | 跨会话 | 数据库/向量库 | 用户偏好、历史结论 | 第 9 章 |

图 4:记忆三层架构
只靠"上下文窗口"做不了跨会话记忆------会话结束窗口就清空。要记住用户,需要把关键信息写入持久化存储(第 9 章完整展开),本章先建立分层认知。
3.4.2 持久化最小实现
python
import redis, json
r = redis.Redis(host="localhost", port=6379)
def save_user_pref(user_id, pref):
"""把用户偏好写入长期记忆"""
key = f"user:{user_id}:pref"
r.set(key, json.dumps(pref, ensure_ascii=False))
def load_user_pref(user_id):
"""新会话开始时,把长期记忆注入上下文"""
raw = r.get(f"user:{user_id}:pref")
if not raw:
return ""
try:
pref = json.loads(raw)
except json.JSONDecodeError:
return "" # 记忆数据损坏时降级为空
return f"【用户已知偏好】{json.dumps(pref, ensure_ascii=False)}"
3.4.3 记忆的时效问题
持久化记忆有"过期"风险:用户偏好变了、历史结论失效了。对策:
- 时间戳:记忆带写入时间,超期降权。
- 主动验证:关键时刻问一句"还是按上次的偏好来吗?"(呼应第 18 章主动澄清)。
- 记忆版本化:可回滚(呼应第 24 章 G4 配置版本化)。
🛠 解决方案:Token 预算分配模板 + 上下文溢出排错指南
常见问题
- "窗口没满,但模型答非所问":注意力稀释 + 指令淹没(3.1.1)。对策:关键指令前置、裁剪冗余、系统提示词精简。
- "聊几轮就失忆":会话内失忆→历史被截断但未摘要;跨会话失忆→没有持久化(3.4)。对策:摘要压缩 + 长期记忆。
- "答案总是不完整":输出预留不足,生成被 max_tokens 截断(3.1.2)。对策:输出预留 ≥20%,检查 max_tokens。
- "工具结果太长,把上下文撑爆":全量回填工具响应(3.3.2)。对策:工具结果精简回填(第 14 章)。
- "长文档塞不进":全量塞超限。对策:转 RAG 检索式(3.3.1,第 8 章)。
解决方案速查表
| 现象 | 根因 | 解决方案 |
|---|---|---|
| 答非所问 | 注意力稀释 | 指令前置 + 裁剪 |
| 聊几轮失忆 | 无压缩/无持久化 | 摘要 + 长期记忆 |
| 答案不完整 | 输出预留不足 | 预留 20% + max_tokens |
| 上下文爆炸 | 工具结果全量回填 | 精简回填 |
| 长文档塞不进 | 全量路线超限 | 转 RAG |
实战提示
- 预算先行:任何 Agent 先做 token 预算分配(3.1.2 模板),再写逻辑。
- 输出预留尽量留:不留输出空间,容易导致模型"答题到一半交卷"。
- 摘要用增量:旧摘要+新消息→新摘要,别全量重摘要(省一半 token)。
- 上下文是注意力预算:设计"模型看到什么",而不是被动记录一切。
- 记忆分层落地:会话内→压缩,跨会话→持久化(第 9 章),别混为一谈。