主题
Context Engineering:比 Prompt Engineering 更大的工程——上下文放什么、怎么放、何时忘
提出问题
Prompt Engineering 教你把指令写清楚:角色、任务、约束、示例。很多人照着做,发现效果还是不稳定。
原因不是 prompt 写得差,是上下文里东西太多。系统指令 + 工具定义 + 检索结果 + 历史对话 + 记忆摘要 + 用户画像,全塞进一个 128K 窗口里——LLM 根本读不完,或者读完了但信号被噪声淹没了(Lost in the Middle)。更要命的是,输入越长,响应越慢、成本越高、幻觉风险越大。
Context Engineering 解决的就是这个矛盾:有限窗口里,装什么、怎么排列、什么时候压缩、什么时候遗忘。它不是 prompt 工程的替代,而是 prompt 工程的上层框架——先决定了上下文长什么样,再写 prompt 才有意义。
上下文的六个来源
一个典型的 LLM 应用,上下文通常来自六个渠道:
| 来源 | 典型内容 | 优先级 | 预算建议 |
|---|---|---|---|
| 系统指令 | 角色、行为约束、输出格式 | 最高 | 占用 5-10% 窗口,写死不动 |
| 工具定义 | Function Calling 的 schema 描述 | 高 | 每工具 200-500 token,选最相关的 |
| 检索结果 | RAG 从知识库召回的相关段落 | 中高 | 30-50% 窗口,按相关性截断 |
| 历史对话 | 当前会话的前几轮对话 | 中 | 滑动窗口,保留最近 3-5 轮 |
| 记忆摘要 | 跨会话的长期记忆(压缩后的) | 中低 | 10-15% 窗口,轮次压缩过 |
| 用户画像 | 用户偏好、身份、权限等静态信息 | 低 | 5% 以下,只在必要时注入 |
这六类信息不是每次都全量塞进去的。Context Engineering 的核心工作就是按优先级和预算,装配出一个刚好够用的上下文。
上下文装配流水线
不要把上下文当作静态的 prompt 模板,想想你写代码时的模块化思维——上下文也有一条流水线,每个环节都可以独立优化。
1. 检索 → 2. 重排 → 3. 压缩 → 4. 拼装
用户输入
│
▼
┌─────────────────────────────┐
│ 1. Query 预处理 + 检索 │ ← 检索什么、检索多少
│ - 改写 query(HyDE/多查询)│
│ - 向量检索 top-k 候选 │
└───────────┬─────────────────┘
▼
┌─────────────────────────────┐
│ 2. 重排(Rerank) │ ← 去掉不相关的噪声
│ - cross-encoder 打分 │
│ - 保留 top-n,n 按 token │
│ 预算动态计算 │
└───────────┬─────────────────┘
▼
┌─────────────────────────────┐
│ 3. 压缩 │ ← 候选段落太长,压进去
│ - 提取关键句 │
│ - LLMLingua 类压缩 │
│ - 摘要提取(费用高,谨慎) │
└───────────┬─────────────────┘
▼
┌─────────────────────────────┐
│ 4. 上下文拼装 │ ← 按优先级排列
│ - 系统指令固定(最前/最后) │
│ - 检索结果按重要性排序 │
│ - 历史对话滑动窗口 │
│ - 记忆摘要放系统指令之后 │
└───────────┬─────────────────┘
▼
LLM流水线关键决策
检索阶段:取多少候选。一个常见反模式是固定 top-k(比如永远取 5 条)。更好的做法是按 token 预算动态取:设定检索结果总 token 上限(比如窗口的 40%),把预算分配给相关性最高的段落,相关性低的直接丢掉。
压缩阶段:不是所有段落都值得压缩。如果段落本身小于 200 token,直接塞进去比压缩省事。长段落(>500 token)才需要压缩。压缩手段按成本排序:截断前 N 句(免费)→ 关键句提取(免费)→ LLMLingua 轻量压缩(低延迟)→ LLM 摘要(延迟高,只在必要时用)。
即时信息 vs 长期记忆的分层
上下文里最难处理的是历史对话和记忆。很多人把整轮对话历史全塞进去,窗口撑爆还在跑。
关键原则:新对话用滑动窗口,旧对话做摘要压缩。
- 滑动窗口:当前会话保留最近 3-5 轮(用户 + 模型),超过的丢弃或压缩成一句摘要
- 摘要压缩:每隔 N 轮,把之前的对话用 LLM 总结成 50-100 token 的摘要,替换掉原始对话
- 长期记忆:跨会话的记忆,用向量化存储 + 按需检索。不是每次请求都加载所有记忆,而是根据当前 query 检索相关的记忆片段
失效策略:什么时候该忘
上下文管理不止是"往里放",更是"往外丢"。
- 滑动窗口过期:超过轮次上限的整轮对话直接丢弃,不保留任何痕迹
- 摘要老化:超过 24 小时或跨了 3 个会话的摘要,标记为低优先级,只在上下文有多余空间时才载入
- 向量化遗忘:长期记忆按时间和访问频率降权,超过 30 天且未被访问的记忆片段,从检索索引中移除
常见反模式
1. 上下文膨胀
症状:prompt 越来越长,每次加新功能就往里塞一段,半年后窗口 128K 都用满。
根因:没有预算是最大的预算问题。给上下文定一个 token 预算上限,超出预算的必须砍掉别的。
2. 指令淹没在中间
Lost-in-the-Middle 实验已经证明:LLM 对上下文中间段的注意力最弱。如果你把最重要的系统指令放在中间(比如 prompt 模板里先写示例、再写历史对话、最后写指令),LLM 可能根本没读到。
对策:最重要的内容放两端。系统指令和当前最相关的检索结果放开头,或者末尾。工具描述、历史对话、次要信息放中间。
3. 工具描述过长
一个 Function Calling 的工具描述,动辄 500-1000 token。如果定义了 10 个工具,光工具描述就吃掉 5k-10k token。而且 LLM 并不需要所有工具的函数签名完整呈现——它可以只看工具名和一句话描述,调用时再决定用哪个。
对策:工具定义做分层展示。第一层只给工具名和一句描述(50 token 以内),LLM 决定要调用哪个工具了,再在下一轮把完整 schema 注入。大部分工具调用根本不会走到完整 schema 那一步。
4. 检索结果不排序
检索出来的段落按相关性得分从高到低排,这没问题。但很多人忘了把最相关的段落放到上下文开头或结尾。如果检索结果按原始文档顺序放,或者随机排,LLM 会漏掉关键信息。
Context Engineering 与相关概念的关系
┌──────────────┐
│ Prompt Eng. │
│(怎么写指令) │
└──────┬───────┘
│ 子集
┌──────▼───────┐
│ Context Eng. │
┌────────────┤(上下文装配) ├────────────┐
│ └──────┬───────┘ │
│ 组合使用 │ 组合使用 │ 组合使用
┌──────▼───────┐ ┌───────▼────────┐ ┌───────▼───────┐
│ RAG │ │ Agent Memory │ │ Long Context │
│(检索装配) │ │ (记忆管理) │ │ (压缩/窗口)│
└──────────────┘ └────────────────┘ └───────────────┘- Prompt Engineering 是 Context Engineering 的子集:prompt 是上下文里的系统指令部分
- RAG 是 Context Engineering 的检索装配环节
- Agent Memory 是 Context Engineering 的记忆管理环节
- Long Context 压缩 是 Context Engineering 的压缩工具(LLMLingua 等)
代码骨架:上下文装配器
下面是一个 60 行左右的上下文装配器,展示了按优先级和预算拼装上下文的核心逻辑:
python
from dataclasses import dataclass, field
from typing import List, Optional
@dataclass
class ContextItem:
content: str
priority: int # 0=最高, 越大优先级越低
category: str # "system" | "retrieval" | "history" | "memory" | "profile"
@dataclass
class AssemblyResult:
context: str
used_tokens: int
dropped: List[str] # 被丢弃的条目说明
def assemble_context(
items: List[ContextItem],
token_budget: int,
token_count_fn,
) -> AssemblyResult:
"""
按优先级和 token 预算装配上下文。
token_count_fn: 估算 token 数的函数(如 tiktoken 或简单除以 4)
"""
# 按优先级排序(0=最高)
sorted_items = sorted(items, key=lambda x: x.priority)
# 系统指令(最高优先级)必须保留
system_items = [it for it in sorted_items if it.category == "system"]
non_system = [it for it in sorted_items if it.category != "system"]
budget = token_budget
assembled = []
dropped = []
# 先装系统指令
for it in system_items:
tokens = token_count_fn(it.content)
if tokens <= budget:
assembled.append(it.content)
budget -= tokens
else:
dropped.append(f"system指令超出预算: {it.content[:50]}...")
# 系统指令超预算则截断
truncated = it.content[:int(budget * 4)]
assembled.append(truncated)
budget = 0
# 剩下的按优先级装
for it in non_system:
tokens = token_count_fn(it.content)
if tokens <= budget:
assembled.append(it.content)
budget -= tokens
else:
dropped.append(f"{it.category}(优先级{it.priority}): {it.content[:50]}...")
# 拼装:最重要的放两端
if len(assembled) <= 3:
final = "\n\n".join(assembled)
else:
# 两端放最重要的,中间放次要的
first = assembled[0]
last = assembled[-1]
middle = assembled[1:-1]
final = first + "\n\n" + "\n\n".join(middle) + "\n\n" + last
total = token_budget - budget
return AssemblyResult(context=final, used_tokens=total, dropped=dropped)
# 使用示例
items = [
ContextItem("你是一个专业的客服助手,回答必须简洁、基于事实,不知道就说不知道", priority=0, category="system"),
ContextItem("产品A的退货政策:收货后30天内可退...", priority=1, category="retrieval"),
ContextItem("用户:我想退货\n助手:请问订单号是多少?\n用户:12345", priority=2, category="history"),
ContextItem("用户偏好:喜欢快速回答,不耐烦长回复", priority=3, category="profile"),
]
# 假设 token_count_fn 只是个占位
result = assemble_context(items, token_budget=4096, token_count_fn=lambda x: len(x) // 4)
print(f"已用 {result.used_tokens} tokens")
print(f"丢弃 {len(result.dropped)} 项: {result.dropped}")这个骨架的核心思想是先定预算,再按优先级组装,而不是把所有东西都扔进去让 LLM 自己筛选。生产环境可以在此基础上加上缓存、动态调整优先级权重、以及异步压缩管线。
总结
Context Engineering 回答的是"上下文里放什么、怎么放、何时忘"三个问题。核心实践:
- 认识上下文的六个来源,给每个来源设定预算和优先级
- 建立装配流水线:检索→重排→压缩→拼装,每个环节独立可调
- 分层管理记忆:新对话用滑动窗口,旧对话做摘要压缩,跨会话记忆做向量检索
- 主动遗忘——没有遗忘策略的上下文管理,迟早被膨胀拖垮
- 警惕四个反模式:膨胀、指令淹没、工具描述过长、检索结果不排序
Context Engineering 是 RAG 和 Agent 系统的底层基础设施。先管好上下文,再谈 prompt 怎么写。
参考
- Liu et al., "Lost in the Middle: How Language Models Use Long Contexts", 2023
- 27-Long-Context-Compression(LLMLingua / StreamingLLM / KV Cache 压缩)
- 35-Agent-Systems-ReAct-Tools-Memory(记忆体系)
- 02-RAG-Pipeline(检索与重排)
- 34-RAG-Full-Pipeline(RAG 全链路)