第 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)
本章小结
上下文工程是构建复杂大模型应用的关键技能:
-
上下文四区域:System(固定)、Memory(历史)、Context(动态注入)、Input(当前输入),各有管理策略
-
长对话管理:滑动窗口(保留最近 N 轮)、摘要压缩(早期对话 → 摘要)、选择性保留(按相关性筛选)
-
信息密度优化:去除冗余、结构化表示、使用符号缩写,让有限 Token 承载更多信息
-
状态管理:用 Redis/数据库存储会话历史,每次请求时读取、管理、注入
-
KV Cache 优化:保持前缀一致,让服务端复用计算结果,降低成本和延迟
思考题
-
你正在开发一个法律咨询助手,用户可能连续问多个相关问题。设计一个上下文管理策略,既要保留足够的法律背景信息,又不能超出上下文窗口。
-
假设你的客服系统每天处理 10 万轮对话,每轮平均 20 条消息。设计一个存储方案,平衡查询性能和存储成本。
-
如何设计一个"对话主题检测"机制,当用户切换话题时,自动清理不相关的历史上下文?
下一章预告:现在你能直接调用 API、写好 Prompt、管理上下文了。但生产环境通常使用框架来简化开发。第 7 章,主流开发框架——LangChain、Spring AI、LlamaIndex,让你的开发效率翻倍。