Skip to content

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 回答的是"上下文里放什么、怎么放、何时忘"三个问题。核心实践:

  1. 认识上下文的六个来源,给每个来源设定预算和优先级
  2. 建立装配流水线:检索→重排→压缩→拼装,每个环节独立可调
  3. 分层管理记忆:新对话用滑动窗口,旧对话做摘要压缩,跨会话记忆做向量检索
  4. 主动遗忘——没有遗忘策略的上下文管理,迟早被膨胀拖垮
  5. 警惕四个反模式:膨胀、指令淹没、工具描述过长、检索结果不排序

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 全链路)

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1