上下文工程-Context-Engineering比Prompt工程更重要
*Context Engineering 全景图 — Context Window 的六大组成部分及其管理策略*
上下文工程:Context Engineering 比 Prompt 工程更重要
1.1 为什么 Prompt 工程不够用:LLM 的决策依据是什么
Context Engineering 全景图 — Context Window 的六大组成部分及其管理策略
在理解上下文工程之前,需要先想清楚 LLM 是基于什么来生成输出的。
LLM 唯一能依赖的,是它在推理时刻能"看到"的所有信息——也就是上下文窗口(Context Window)里的全部内容。模型没有什么神奇的"内部理解"能力,它的每一个输出 token,都是基于当前 context 里的信息通过概率计算得出的。
这个认识非常重要:如果 context 里的信息不够好——关键信息缺失、重要信息被埋在中间被忽略、工具描述模糊导致模型选错工具——不管 System Prompt 写得多漂亮,输出质量都会差。
Prompt Engineering 的局限在哪里
Prompt Engineering 把注意力集中在 System Prompt 的措辞和结构上:如何表达角色、如何设定规则、如何引导思维链。这在单次对话的场景下效果显著。
但一个运行中的 Agent,它的 context 里有六种东西:System Prompt 只是其中一种,而且通常是 token 占比最少的那种。工具定义、对话历史、RAG 检索结果、工具执行结果——这些内容加起来可能占 context 的 80%。把全部精力放在 20% 的内容上,优化空间天然有限。
上下文工程的本质
上下文工程(Context Engineering)是一个更宏观的概念:系统性地设计和管理"什么信息、以什么格式、出现在 context 的什么位置",使得模型在推理时刻能得到最高质量的决策依据。
它包含 Prompt Engineering,但远不止于此。一个掌握上下文工程的工程师,关注的不是某句话怎么措辞,而是整个 context 的信息架构:哪些内容是噪音需要过滤、哪些内容需要放在最显眼的位置、如何在有限窗口里最大化信息价值。
本章介绍 Context Engineering 的概念、核心挑战,以及在 Agent 工程化过程中的四个关键实践。
1.2 Prompt Engineering 是 Context Engineering 的子集
很多人把 Prompt Engineering 当成 Agent 开发的核心技能,觉得把 System Prompt 写得足够好就够了。这个认知是不完整的。
Prompt 只是 context window(上下文窗口,LLM 每次推理时能"看到"的最大文本量,以 token 为单位;token 是模型处理文本的基本单元,大约每个汉字或英文单词算 1-2 个 token)里的一部分内容。一个 Agent 的 context 通常包含:
- System Prompt(角色定义和规则)
- 对话历史
- 工具定义
- RAG 检索到的文档
- 工具执行结果
- 用户当前输入
Prompt Engineering 主要优化第一项。但一个运行中的 Agent,context 里绝大多数 token 都是后几项。花了大力气优化 System Prompt,但如果对话历史太长挤掉了关键信息,或者工具描述写得模糊让模型调错工具,效果依然会很差。
Context Engineering 的定义:设计和管理"什么信息在什么时候以什么格式出现在 context window 里"的工程实践。
两个 Agent 使用同一个模型,使用效果差异悬殊的根本原因,往往不在 Prompt 的措辞有多精妙,而在于对整个 context 的设计是否清晰、合理。
1.3 Context 的完整构成
这六个部分,每一个都在争夺有限的 context window 空间,都在影响模型的输出质量。
1.4 核心挑战:Lost in the Middle
2023 年 Stanford 的研究《Lost in the Middle》发现了一个重要现象:当相关信息被放在 context 的中间位置时,模型对它的关注度显著下降。
这张图示意性地展示了模型注意力和信息位置的关系:
注意力
^
| * *
| * *
| * *
| * *
| * * * * * * * * *
+-----------------------------------> 位置
开头 结尾
开头和结尾的信息被模型"记住"得更好,中间的内容容易被忽略。
这个发现直接影响了 Context Engineering 的很多实践决策。
1.5 实践一:System Prompt 的设计
System Prompt 是整个 context 的"基调设定",它出现在最前面,权重最高。一个常见的错误是把所有规则塞进去,结果又臭又长,模型根本没法全部遵守。
# 差的写法:所有内容堆在一起,没有优先级
bad_system_prompt = """
你是一个代码审查助手。你需要检查代码质量、安全问题、性能问题、风格问题、
文档完整性、测试覆盖率、依赖管理、版本兼容性、错误处理、日志记录、
数据库查询优化、缓存使用、API 设计、并发安全、内存管理...(后面还有200行)
"""
# 好的写法:层次清晰,优先级明确
good_system_prompt = """
你是 Acme 公司的代码审查专家。
## 核心职责
审查 Python 代码,重点关注三个维度:安全性、可维护性、性能。
## 审查优先级
1. 高危(必须修改):SQL 注入、敏感数据明文、未验证输入
2. 中危(建议修改):函数超过 50 行、缺少错误处理、重复代码
3. 低危(可选优化):变量命名、注释质量
## 输出格式
按优先级分组,每条问题包含:位置、问题描述、修改建议。
## 工具使用原则
- 需要查看文件时,用 read_file
- 需要搜索代码模式时,用 search_code
- 不确定语义时,先读代码再判断,不要猜测
"""
关键原则:核心规则放前面,优先级要明确,工具使用原则要具体。
1.6 实践二:工具描述的质量决定调用准确率
工具描述是决定模型能否正确调用工具的关键因素。模糊的工具描述会直接导致工具调用错误率上升。
# 差的工具描述:模糊,没有边界
bad_tool = {
"name": "search",
"description": "搜索信息",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"}
}
}
}
模型看到这个描述,完全不知道"搜索信息"是什么意思,不知道什么时候该用,不知道怎么构造 query。
# 好的工具描述:具体、有边界、有示例
good_tool = {
"name": "search_codebase",
"description": """在项目代码库中搜索代码模式或文本。
适用场景:
- 查找某个函数或类的所有调用点
- 搜索特定的代码模式(如未处理的异常)
- 查找某个变量或常量的定义
不适用场景:
- 不要用于读取文件内容(用 read_file)
- 不要用于搜索互联网(用 web_search)
示例:
- 查找所有 SQL 拼接:query="cursor.execute" + "%" 或 query="f'SELECT"
- 查找函数定义:query="def process_payment"
""",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索关键词或正则表达式,支持精确匹配和模糊搜索"
},
"file_pattern": {
"type": "string",
"description": "限定搜索范围,如 '*.py' 或 'src/payment/**'"
}
},
"required": ["query"]
}
}
工具描述的核心原则:
- 说清楚"什么时候用"和"什么时候不用"
- 给出具体的使用示例
- 参数描述要包含格式要求和边界条件
1.7 实践三:对话历史的管理策略
对话历史是 context 里增长最快的部分。一次复杂任务可能有几十轮工具调用,每轮都往 context 里加内容。不加管理,很快就会撑爆 context window。
有三种常见策略:
from openai import OpenAI
from typing import List, Dict
client = OpenAI()
# 策略一:截断(简单粗暴,丢失信息)
def truncate_history(messages: List[Dict], max_messages: int = 20) -> List[Dict]:
"""只保留最近 N 轮对话"""
system_messages = [m for m in messages if m["role"] == "system"]
non_system = [m for m in messages if m["role"] != "system"]
if len(non_system) > max_messages:
non_system = non_system[-max_messages:]
return system_messages + non_system
# 策略二:滑动窗口(保留首尾,压缩中间)
def sliding_window_history(
messages: List[Dict],
keep_first: int = 3,
keep_last: int = 10,
max_total: int = 20
) -> List[Dict]:
"""保留开头的关键上下文和最近的对话"""
system_messages = [m for m in messages if m["role"] == "system"]
non_system = [m for m in messages if m["role"] != "system"]
if len(non_system) <= max_total:
return system_messages + non_system
# 保留开头(任务背景)和结尾(最近上下文)
preserved = non_system[:keep_first] + non_system[-keep_last:]
return system_messages + preserved
# 策略三:摘要压缩(保留信息,减少 token)
def summarize_history(messages: List[Dict], keep_recent: int = 5) -> List[Dict]:
"""将较早的对话历史压缩成摘要"""
system_messages = [m for m in messages if m["role"] == "system"]
non_system = [m for m in messages if m["role"] != "system"]
if len(non_system) <= keep_recent + 5:
return system_messages + non_system
# 需要压缩的部分
to_summarize = non_system[:-keep_recent]
recent = non_system[-keep_recent:]
# 用 LLM 生成摘要
summary_response = client.chat.completions.create(
model="gpt-4o-mini", # 用小模型降低成本
messages=[
{
"role": "system",
"content": "将以下对话历史压缩成简洁摘要,保留关键决策和重要发现。"
},
{
"role": "user",
"content": str(to_summarize)
}
]
)
summary = summary_response.choices[0].message.content
# 用摘要替换原始历史
summary_message = {
"role": "system",
"content": f"[历史对话摘要]:{summary}"
}
return system_messages + [summary_message] + recent
三种策略的选择依据:
- 截断:适合对连续性要求不高的简单任务
- 滑动窗口:适合需要保留任务起点上下文的场景
- 摘要压缩:适合长时间运行的复杂 Agent,保留信息量最大,但有额外的模型调用成本
1.8 实践四:RAG 结果的质量控制
RAG 检索到的文档会大量占用 context。但检索到的内容不一定都有用,质量差的 RAG 结果反而会干扰模型。
def prepare_rag_context(query: str, documents: list, max_chars: int = 3000) -> str:
"""准备 RAG 上下文,控制质量和长度"""
if not documents:
return ""
# 过滤低相关性文档(假设 documents 包含相关性分数)
relevant_docs = [
doc for doc in documents
if doc.get("relevance_score", 0) >= 0.6
]
if not relevant_docs:
return ""
# 按相关性排序,最相关的放最前面(利用 Lost in the Middle 效应)
relevant_docs.sort(key=lambda x: x.get("relevance_score", 0), reverse=True)
context_parts = []
total_chars = 0
for doc in relevant_docs:
content = doc["content"]
source = doc.get("source", "未知来源")
# 控制总长度,重要的放前面,不重要的截断
if total_chars + len(content) > max_chars:
remaining = max_chars - total_chars
if remaining > 200: # 至少 200 字才加入
content = content[:remaining] + "..."
else:
break
context_parts.append(f"[{source}]\n{content}")
total_chars += len(content)
return "\n\n---\n\n".join(context_parts)
1.9 把所有实践整合起来
def build_agent_context(
user_input: str,
conversation_history: list,
retrieved_docs: list,
tool_results: list
) -> list:
"""构建完整的 Agent context"""
# 1. System Prompt 放最前面(高权重位置)
system_prompt = good_system_prompt
# 2. 管理对话历史(控制长度)
managed_history = sliding_window_history(
conversation_history,
keep_first=2,
keep_last=8
)
# 3. 准备 RAG 上下文
rag_context = prepare_rag_context(user_input, retrieved_docs)
# 4. 构建 messages
messages = [{"role": "system", "content": system_prompt}]
# 5. 如果有 RAG 结果,在历史之前注入(靠近开头,提高关注度)
if rag_context:
messages.append({
"role": "system",
"content": f"相关参考资料:\n{rag_context}"
})
# 6. 加入对话历史
messages.extend(managed_history)
# 7. 用户当前输入放最后(高权重位置)
messages.append({"role": "user", "content": user_input})
return messages
注意信息的位置安排:System Prompt 在最前,RAG 结果紧随其后,用户输入在最后。重要信息不放中间。
1.10 Context Engineering 是 Agent 工程化的核心能力
高质量 Agent 的构建,通常需要在以下几件事上花费大量精力:
- 把工具描述打磨到能准确引导模型选择正确工具
- 设计对话历史的滑动窗口策略,确保关键任务背景不被挤掉
- 控制 RAG 结果的质量和位置,最相关的文档放最前面
- System Prompt 保持简洁,核心规则清晰,不堆砌
这些都不是 Prompt Engineering。这是对整个 context 的系统性设计。
一个 Agent 的输出质量,取决于模型在生成回答那一刻,它的 context window 里有什么,位置怎么排,格式是否清晰。Context Engineering 就是工程化地解决这个问题。
Prompt 工程是起点,Context Engineering 是终点。当 Agent 从 demo 走向生产,会发现大部分真实问题都出在 context 的设计上,而不是 Prompt 的措辞上。