第 6 章-上下文工程上下文工程(Context Engineering)

3 阅读11分钟

第 6 章 上下文工程(Context Engineering)

"上下文窗口是大模型的'工作记忆',管理好它,你的应用才能处理复杂的真实场景。"

上一章我们掌握了 Prompt Engineering。但 Prompt 只是"当前输入",大模型还能看到"历史对话"。如何管理这些历史?如何处理超长文档?这就是上下文工程要解决的问题。

本章内容:

  • 上下文的四个区域及其作用
  • 长对话的上下文管理策略
  • 信息密度优化
  • 多轮对话状态管理
  • KV Cache 与上下文复用

6.1 上下文窗口的本质:模型"看见"什么,就只知道什么

6.1.1 上下文 = 模型的"视野"

大模型没有真正的"记忆"。每次 API 调用,你发送的 messages 数组就是模型能看到的全部内容。超出上下文窗口的内容,模型完全不知道。

# 模型只能看到这里的 messages
response = client.chat.completions.create(
    model="gpt-4",
    messages=[  # ← 这就是上下文
        {"role": "system", "content": "..."},
        {"role": "user", "content": "..."},
        {"role": "assistant", "content": "..."},
        # ... 更多历史消息
    ]
)

6.1.2 上下文的四个区域

一个典型的对话上下文包含四个区域:

区域内容特点
System系统指令、角色设定固定不变,影响全局
Memory历史对话、用户画像动态变化,需要管理
Context当前任务的上下文信息任务相关,临时注入
Input用户的当前输入每次不同
上下文窗口(如 128K Token)
┌─────────────────────────────────────────────────────────────┐
│  System(固定)                                              │
│  "你是一位专业的客服助手..."                                  │
├─────────────────────────────────────────────────────────────┤
│  Memory(历史对话,需要管理)                                  │
│  User: "你好"                                               │
│  Assistant: "您好,有什么可以帮您?"                          │
│  User: "我想退货"                                            │
│  ...                                                        │
├─────────────────────────────────────────────────────────────┤
│  Context(任务上下文,临时注入)                               │
│  "当前订单信息:订单号 12345,商品:iPhone 15,状态:已发货"    │
├─────────────────────────────────────────────────────────────┤
│  Input(用户当前输入)                                        │
│  "怎么操作退货?"                                            │
└─────────────────────────────────────────────────────────────┘

对后端开发者的类比

上下文窗口就像HTTP 请求的 Header + Body,每次请求都要带上。不同的是,大模型的"请求"可以包含历史记录,但总量有限制。


6.2 上下文的四个区域:System / 记忆 / 工具结果 / 当前输入

6.2.1 System 区域:全局设定

System Prompt 放在最前面,定义全局行为。它应该:

  • 简洁但完整
  • 不随对话变化
  • 包含角色、约束、格式要求
system_prompt = """你是电商平台智能客服助手小E。

【身份】
- 你是专业的客服代表,语气友好耐心
- 你只回答与订单、商品、售后相关的问题

【约束】
- 不要编造政策,不确定时引导用户联系人工客服
- 涉及退款、赔偿等敏感操作,必须核实用户身份
- 单次回复不超过 200 字

【工具】
你可以使用以下工具:
- query_order(order_id): 查询订单信息
- apply_return(order_id, reason): 申请退货
"""

6.2.2 Memory 区域:历史对话管理

历史对话会累积,很快会超出上下文窗口。需要策略性地管理:

策略 1:滑动窗口(只保留最近 N 轮)

def manage_context_sliding(messages, max_rounds=10):
    """只保留最近 N 轮对话"""
    # System prompt 始终保留
    system_msg = [m for m in messages if m["role"] == "system"]
    # 其他消息只保留最后 max_rounds * 2 条(每轮包含 user + assistant)
    other_msgs = [m for m in messages if m["role"] != "system"]
    kept_msgs = other_msgs[-max_rounds * 2:]
    return system_msg + kept_msgs

策略 2:摘要压缩(早期对话压缩成摘要)

async def compress_early_messages(messages, client):
    """将早期对话压缩成摘要"""
    if len(messages) <= 10:
        return messages
    
    # 前 5 轮对话压缩成摘要
    early_msgs = messages[:10]
    recent_msgs = messages[10:]
    
    summary_prompt = f"""请总结以下对话的关键信息,用于后续对话参考:
{format_messages(early_msgs)}

请提取:用户的主要问题、已提供的信息、已完成的操作。"""
    
    summary = await client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": summary_prompt}]
    )
    
    # 用摘要替换原始对话
    compressed = [
        {"role": "system", "content": f"历史对话摘要:{summary}"}
    ] + recent_msgs
    
    return compressed

6.2.3 Context 区域:动态注入的业务信息

根据用户当前意图,动态查询并注入相关信息:

async def build_context(user_message, user_id):
    """根据用户消息构建上下文"""
    context_parts = []
    
    # 识别用户意图,查询相关信息
    if "订单" in user_message:
        recent_orders = await get_user_recent_orders(user_id, limit=3)
        context_parts.append(f"用户最近订单:{format_orders(recent_orders)}")
    
    if "会员" in user_message or "积分" in user_message:
        user_info = await get_user_membership(user_id)
        context_parts.append(f"用户会员信息:{user_info}")
    
    return "\n\n".join(context_parts)

# 使用
context = await build_context(user_message, user_id)
messages = [
    {"role": "system", "content": system_prompt},
    # ... 历史对话
    {"role": "user", "content": f"{context}\n\n用户问题:{user_message}"}
]

6.2.4 Input 区域:当前用户输入

用户的原始输入,通常是最后一轮对话的 user 消息。


📌 插图 6-1:上下文四区域示意图

上下文窗口 = System + Memory + Context + Input

┌────────────────────────────────────────────────────────────┐
│ System(固定,始终保留)                                     │
│ • 角色设定                                                 │
│ • 全局约束                                                 │
│ • 输出格式                                                 │
├────────────────────────────────────────────────────────────┤
│ Memory(动态管理)                                          │
│ • 滑动窗口:只保留最近 N 轮                                 │
│ • 摘要压缩:早期对话 → 摘要                                 │
│ • 选择性保留:只保留相关历史                                │
├────────────────────────────────────────────────────────────┤
│ Context(动态注入)                                         │
│ • 根据当前意图查询业务数据                                  │
│ • 订单信息、用户画像、知识库片段                            │
│ • 工具执行结果                                             │
├────────────────────────────────────────────────────────────┤
│ Input(当前输入)                                           │
│ • 用户的原始问题                                           │
└────────────────────────────────────────────────────────────┘

6.3 长对话上下文管理策略:截断、摘要、压缩

6.3.1 截断策略(Truncation)

最简单直接的方法:超出限制时,从最早的消息开始删除。

def truncate_messages(messages, max_tokens, encoder):
    """截断消息到最大 Token 数"""
    total_tokens = sum(len(encoder.encode(m["content"])) for m in messages)
    
    while total_tokens > max_tokens and len(messages) > 1:
        # 保留 system,从第二条开始删
        removed = messages.pop(1) if messages[0]["role"] == "system" else messages.pop(0)
        total_tokens -= len(encoder.encode(removed["content"]))
    
    return messages

问题:直接删除会丢失信息,用户可能问"你刚才说的那个方案呢?"

6.3.2 摘要策略(Summarization)

将早期对话压缩成摘要,保留关键信息。

async def summarize_and_truncate(messages, client, max_tokens=8000):
    """摘要 + 截断的组合策略"""
    encoder = tiktoken.encoding_for_model("gpt-4")
    current_tokens = sum(len(encoder.encode(m["content"])) for m in messages)
    
    if current_tokens <= max_tokens:
        return messages
    
    # 保留 system 和最近 6 轮对话
    system_msg = [m for m in messages if m["role"] == "system"]
    recent_msgs = messages[-6:]
    old_msgs = messages[len(system_msg):-6]
    
    # 压缩早期对话
    if old_msgs:
        summary = await generate_summary(old_msgs, client)
        summary_msg = {"role": "system", "content": f"历史对话摘要:{summary}"}
        return system_msg + [summary_msg] + recent_msgs
    
    return system_msg + recent_msgs

6.3.3 选择性保留策略(Selective Retention)

根据相关性,只保留与当前话题相关的历史消息。

async def select_relevant_messages(current_query, history, client, top_k=5):
    """选择最相关的历史消息"""
    # 计算当前查询与每条历史消息的相关性(可以用 Embedding 相似度)
    query_embedding = await get_embedding(current_query)
    
    scored_history = []
    for msg in history:
        msg_embedding = await get_embedding(msg["content"])
        similarity = cosine_similarity(query_embedding, msg_embedding)
        scored_history.append((msg, similarity))
    
    # 按相关性排序,取 top_k
    scored_history.sort(key=lambda x: x[1], reverse=True)
    selected = [msg for msg, _ in scored_history[:top_k]]
    
    # 按时间顺序排列
    selected.sort(key=lambda x: history.index(x))
    return selected

6.4 信息密度优化:如何让有限的 Token 发挥最大作用

6.4.1 信息密度的概念

同样的 Token 预算,承载的信息量可以差别很大。优化信息密度,就是用更少的 Token 传递同样的信息量

6.4.2 优化技巧

1. 去除冗余

# 不好的:包含大量无关信息
context = """
用户张三,男,1985年出生,北京户口,手机号 138****1234,
邮箱 zhangsan@example.com,注册时间 2020-03-15,
最近登录时间 2024-01-20,登录 IP 192.168.1.1...
订单号 12345,商品 iPhone 15 Pro Max 256GB 黑色,
价格 9999 元,下单时间 2024-01-15 14:30:22,
支付时间 2024-01-15 14:31:05,支付方式 支付宝...
"""

# 好的:只保留关键信息
context = """
用户:张三
订单:12345 | iPhone 15 Pro Max | ¥9999 | 2024-01-15
状态:已发货,预计 2024-01-18 送达
"""

2. 结构化表示

# 不好的:自然语言描述
order_info = "用户张三在 1 月 15 日买了一个 iPhone,花了 9999 元,已经发货了"

# 好的:结构化表示
order_info = """
订单信息:
- 订单号:12345
- 商品:iPhone 15 Pro Max
- 金额:¥9999
- 状态:已发货
- 预计送达:2024-01-18
"""

3. 使用符号和缩写

# 优化前
"订单编号是 12345,商品名称是 iPhone 15 Pro Max,商品价格是 9999 元"

# 优化后
"订单#12345 | iPhone15PM | ¥9999"

6.4.3 动态信息密度调整

根据任务复杂度,动态调整信息密度:

def build_context_by_complexity(query, data, complexity="normal"):
    """根据复杂度调整信息密度"""
    if complexity == "high":
        # 复杂任务:提供详细信息
        return format_detailed(data)
    elif complexity == "normal":
        # 常规任务:标准信息
        return format_standard(data)
    else:
        # 简单任务:精简信息
        return format_compact(data)

6.5 多轮对话的状态管理:会话历史的存储与注入

6.5.1 会话状态存储

多轮对话需要持久化存储,通常用 Redis 或数据库:

import redis
import json

class ConversationStore:
    def __init__(self):
        self.redis = redis.Redis()
    
    def save_messages(self, session_id, messages, ttl=3600):
        """保存会话消息"""
        key = f"chat:{session_id}"
        self.redis.setex(key, ttl, json.dumps(messages))
    
    def get_messages(self, session_id):
        """获取会话消息"""
        key = f"chat:{session_id}"
        data = self.redis.get(key)
        return json.loads(data) if data else []
    
    def append_message(self, session_id, message):
        """追加消息"""
        messages = self.get_messages(session_id)
        messages.append(message)
        self.save_messages(session_id, messages)

6.5.2 完整的对话处理流程

async def handle_chat(session_id, user_message, client):
    """处理多轮对话"""
    store = ConversationStore()
    
    # 1. 获取历史对话
    history = store.get_messages(session_id)
    
    # 2. 管理上下文(截断/摘要)
    managed_history = await manage_context(history, client)
    
    # 3. 构建当前上下文
    context = await build_context(user_message, session_id)
    
    # 4. 组装消息
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT}
    ] + managed_history + [
        {"role": "user", "content": f"{context}\n\n{user_message}"}
    ]
    
    # 5. 调用模型
    response = await client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages
    )
    
    assistant_message = response.choices[0].message.content
    
    # 6. 保存对话
    store.append_message(session_id, {"role": "user", "content": user_message})
    store.append_message(session_id, {"role": "assistant", "content": assistant_message})
    
    return assistant_message

6.6 KV Cache 与上下文复用:降本提速的工程技巧

6.6.1 什么是 KV Cache

大模型推理时,会计算每个 Token 的 Key 和 Value 向量。这些计算结果可以被缓存,下次生成时复用,避免重复计算。

关键洞察:如果两次请求的前缀相同,后面请求可以复用前面请求的 KV Cache,大幅减少计算量。

6.6.2 利用 KV Cache 优化

场景 1:批量处理相同前缀的请求

# 不好的:每个请求单独发送
for question in questions:
    response = client.chat.completions.create(
        messages=[
            {"role": "system", "content": long_system_prompt},
            {"role": "user", "content": question}
        ]
    )

# 好的:使用相同的 system prompt,让服务端复用 KV Cache
# (大多数 API 提供商会自动优化)

场景 2:多轮对话优化

# 保持 system prompt 和早期对话不变
# 只追加新消息,让服务端复用前面的 KV Cache

messages = [
    {"role": "system", "content": system_prompt},  # 复用
    {"role": "user", "content": "你好"},           # 复用
    {"role": "assistant", "content": "您好"},      # 复用
    {"role": "user", "content": "新问题"},         # 新增
]

6.6.3 本地部署的 KV Cache 管理

如果使用 vLLM 等本地部署方案,可以显式管理 KV Cache:

from vllm import LLM, SamplingParams

llm = LLM(model="meta-llama/Llama-2-7b")

# 启用 prefix caching
sampling_params = SamplingParams(temperature=0.7)

# 相同前缀的请求会自动复用 KV Cache
outputs = llm.generate([
    "System: 你是一位助手\nUser: 问题1",
    "System: 你是一位助手\nUser: 问题2",  # 前缀相同,复用 Cache
], sampling_params)

本章小结

上下文工程是构建复杂大模型应用的关键技能:

  1. 上下文四区域:System(固定)、Memory(历史)、Context(动态注入)、Input(当前输入),各有管理策略

  2. 长对话管理:滑动窗口(保留最近 N 轮)、摘要压缩(早期对话 → 摘要)、选择性保留(按相关性筛选)

  3. 信息密度优化:去除冗余、结构化表示、使用符号缩写,让有限 Token 承载更多信息

  4. 状态管理:用 Redis/数据库存储会话历史,每次请求时读取、管理、注入

  5. KV Cache 优化:保持前缀一致,让服务端复用计算结果,降低成本和延迟


思考题

  1. 你正在开发一个法律咨询助手,用户可能连续问多个相关问题。设计一个上下文管理策略,既要保留足够的法律背景信息,又不能超出上下文窗口。

  2. 假设你的客服系统每天处理 10 万轮对话,每轮平均 20 条消息。设计一个存储方案,平衡查询性能和存储成本。

  3. 如何设计一个"对话主题检测"机制,当用户切换话题时,自动清理不相关的历史上下文?


下一章预告:现在你能直接调用 API、写好 Prompt、管理上下文了。但生产环境通常使用框架来简化开发。第 7 章,主流开发框架——LangChain、Spring AI、LlamaIndex,让你的开发效率翻倍。