Appearance
第4章 上下文工程:比 Prompt Engineering 更重要的事
4.1 一个被忽略的关键区分
过去两年,"Prompt Engineering" 成了 AI 领域最热门的词汇之一。无数文章教你如何写出更好的提示词——用角色扮演、思维链、少样本示例等技巧来引导模型输出。这些技巧确实有用,但当你从写提示词跨越到构建 Agent 系统时,你会发现一个残酷的事实:提示词只是上下文的一小部分,而上下文才是决定 Agent 行为的全部。
让我用一个具体的例子来说明。假设你在 Claude Code 中执行一个任务:"帮我重构这个 React 组件,把状态管理从 useState 迁移到 Zustand"。在模型真正开始思考之前,它看到的不只是你这句话。它看到的是一个精心组装的上下文包,包含:
┌──────────────────────────────────────────┐
│ System Prompt(系统提示) │
│ - 角色定义、行为约束、安全规则 │
│ - 工具使用说明 │
├──────────────────────────────────────────┤
│ Tool Definitions(工具定义) │
│ - Read、Edit、Bash、Grep 等工具的 schema │
│ - 每个工具的参数说明和使用约束 │
├──────────────────────────────────────────┤
│ Memory(记忆) │
│ - CLAUDE.md 中的项目规则 │
│ - 用户偏好和历史约定 │
├──────────────────────────────────────────┤
│ Conversation History(对话历史) │
│ - 之前的对话轮次 │
│ - 之前的工具调用和结果 │
├──────────────────────────────────────────┤
│ Retrieved Context(检索上下文) │
│ - 被读取的文件内容 │
│ - 搜索结果 │
├──────────────────────────────────────────┤
│ User Message(用户消息) │
│ - "帮我重构这个 React 组件..." │
└──────────────────────────────────────────┘这个上下文包可能消耗了数万个 token。你的提示词只占其中很小一部分。上下文工程(Context Engineering)就是关于如何设计、组装、压缩和管理这个上下文包的工程实践。
Andrej Karpathy 曾这样总结:
"The hottest new programming language is English." 但如果要更精确一点,应该说:"The hottest new programming skill is context curation."
4.2 为什么上下文工程比提示词工程更重要
4.2.1 Agent 是多轮交互系统
传统的 Prompt Engineering 面向的是单轮交互:你写一段提示词,模型返回一段输出,交互结束。但 Agent 系统是多轮交互的——它会循环执行"思考-行动-观察"的步骤,每一步都会产生新的上下文。
考虑 Claude Code 执行一个稍复杂的任务,比如"找到并修复所有 TypeScript 类型错误"。整个过程可能是这样的:
第1轮:模型决定先运行 tsc --noEmit 查看错误
→ Bash 工具返回 47 行错误信息
第2轮:模型分析错误,决定先读取第一个出错的文件
→ Read 工具返回 200 行代码
第3轮:模型理解了问题,决定修复
→ Edit 工具修改了文件
第4轮:模型决定再运行一次 tsc 验证
→ Bash 工具返回更新后的错误列表
... 可能持续 20-30 轮每一轮结束后,工具的返回结果都会被追加到上下文中。到第 20 轮时,上下文中已经积累了大量的文件内容、命令输出、编辑历史。如果不加管理,上下文会在几轮之内溢出窗口限制,或者因为无关信息太多而导致模型"迷失"。
4.2.2 上下文窗口有限且昂贵
即使现代模型的上下文窗口已经扩展到了 100K 甚至 1M token,这个窗口仍然是有限的、昂贵的资源。有两个关键约束:
硬约束:窗口大小。 超过上下文窗口的内容会被截断或导致请求失败。
软约束:注意力衰减。 研究表明,模型对上下文中间位置的信息关注度较低("Lost in the Middle" 问题)。即使技术上放得下,信息放在 100K 上下文的中间地带,模型可能会"忽略"它。
经济约束:token 费用。 每个 token 都有成本。如果你在每轮交互中都传递一个完整的 10 万行代码库,你的 API 账单会让你怀疑人生。
这意味着上下文工程的核心目标是:在有限的窗口内,放入最相关的信息,以最高的信噪比支撑模型做出正确决策。
4.2.3 提示词是静态的,上下文是动态的
提示词在你写完之后基本固定不变。但上下文是随着每一轮交互动态变化的——新的工具结果加入,旧的信息可能需要压缩或移除。这种动态性是 Prompt Engineering 完全无法覆盖的维度。
用一个比喻来说:如果 Prompt Engineering 是写好一份菜单,那么 Context Engineering 就是经营整个厨房——管理食材进出、控制库存、确保每道菜上桌时用的都是最新鲜的原料。
4.3 上下文的七大组成部分
一个 Agent 系统的上下文通常由以下七个部分组成。理解每一部分的特性,是做好上下文工程的基础。
4.3.1 系统提示(System Prompt)
系统提示是 Harness 注入的"基础人格",定义了 Agent 的角色、能力边界和行为规则。它通常是上下文中最稳定的部分——在整个会话过程中不会改变。
python
system_prompt = """
You are an expert software engineer.
You have access to the following tools: Read, Edit, Bash, Grep.
Always read a file before editing it.
Never run destructive commands without confirmation.
Respond in the same language as the user.
"""工程要点: 系统提示应该简洁、精确。每多一句废话,就占用了本可以放更有价值信息的空间。在 Claude Code 中,系统提示大约占用 3000-5000 token,这是一个经过精心优化的数字。
4.3.2 工具定义(Tool Definitions)
工具定义是 JSON Schema 格式的工具说明,告诉模型有哪些工具可用、每个工具接受什么参数。这部分的开销常常被低估。
以 Claude Code 为例,它定义了 Read、Edit、Write、Bash、Grep、Glob 等工具。每个工具的 schema 加上描述,大约占用 200-500 token。如果你有 10 个工具,这就是 2000-5000 token 的固定开销。
工程要点: 工具定义的质量直接影响模型使用工具的正确率。描述要精确但不冗长。如果可能,使用"延迟加载"策略——不要一次性注入所有工具定义,而是根据当前任务动态选择相关工具。
python
# 反模式:一次性注入所有 50 个工具
tools = load_all_tools() # 消耗 15000 token
# 正确做法:根据任务阶段选择工具子集
if task_phase == "analysis":
tools = [read_tool, grep_tool, glob_tool] # 1500 token
elif task_phase == "editing":
tools = [read_tool, edit_tool, write_tool, bash_tool] # 2000 token4.3.3 对话历史(Conversation History)
对话历史是之前所有轮次的用户消息、模型回复、工具调用和工具结果的完整记录。这是上下文中增长最快的部分,也是最需要管理的部分。
一个典型的 Agent 会话中,对话历史的增长曲线大致如下:
Token 消耗
│
│ ╱ 未压缩
│ ╱
│ ╱
│ ╱
│ ╱
│ ╱
│ ────────────────────── 压缩后
│ ╱
│ ╱
│╱
└──────────────────────────────── 轮次不做任何管理的情况下,一个 20 轮的 Agent 会话可能消耗 50K-100K token 的对话历史。这不仅占满了上下文窗口,还会导致模型把注意力分散到大量不再相关的历史信息上。
4.3.4 记忆(Memory)
记忆是跨会话持久化的信息,比如 Claude Code 中的 CLAUDE.md 文件。它记录了项目规则、用户偏好、常用命令等。
markdown
# CLAUDE.md
- 本项目使用 pnpm,不要使用 npm 或 yarn
- 测试框架是 vitest,不是 jest
- 提交信息使用中文
- 代码风格遵循 .eslintrc.js 中的配置工程要点: 记忆的价值在于避免重复。如果没有记忆,用户每次开始新会话都要重复说明项目规则。但记忆也要定期清理——过时的记忆比没有记忆更糟糕,因为它会误导模型。
4.3.5 检索文档(Retrieved Documents)
当 Agent 需要理解代码库或外部知识时,检索系统会动态拉取相关文档注入上下文。这就是 RAG(Retrieval-Augmented Generation)在 Agent 系统中的体现。
例如,当用户说"帮我按照项目的 API 规范新增一个接口"时,Agent 可能需要:
- 检索现有的 API 文件,了解代码风格
- 检索路由配置文件,了解路由注册方式
- 检索数据库模型定义,了解数据结构
每个检索结果可能有几百到几千 token。关键在于检索的精度——拉取不相关的文档不仅浪费空间,还会干扰模型判断。
4.3.6 工具返回结果(Tool Results)
工具返回结果是上下文中最"重"的部分。一次 cat 命令可能返回上千行代码,一次 git log 可能返回几百条提交记录,一次 npm test 可能输出上万字的测试日志。
这是上下文爆炸的主要来源。 如果不对工具结果进行处理,一个 Agent 会话在几轮之内就会把上下文填满。
4.3.7 用户消息(User Message)
用户当前轮次的输入。相比其他部分,这通常是最小的,但优先级最高——Agent 必须首先理解用户想要什么。
4.4 上下文预算分配
既然上下文窗口是有限的,我们就需要像管理财务预算一样管理 token 预算。以下是一个基于 200K 上下文窗口的典型预算分配方案:
┌─────────────────────────────────────────────┐
│ 组成部分 │ Token 预算 │ 占比 │
├─────────────────────────────────────────────┤
│ System Prompt │ 4,000 │ 2% │
│ Tool Definitions │ 3,000 │ 1.5% │
│ Memory (CLAUDE.md) │ 2,000 │ 1% │
│ Conversation History │ 120,000 │ 60% │
│ Retrieved Documents │ 30,000 │ 15% │
│ Tool Results (当轮) │ 30,000 │ 15% │
│ User Message (当轮) │ 1,000 │ 0.5% │
│ 预留 (安全边际) │ 10,000 │ 5% │
├─────────────────────────────────────────────┤
│ 总计 │ 200,000 │ 100% │
└─────────────────────────────────────────────┘这个分配不是固定的,而是动态调整的。在会话早期,对话历史很短,可以给检索文档和工具结果更多空间。在会话后期,对话历史占据主导,检索文档和工具结果需要更积极地压缩。
实践原则:
固定开销最小化。 系统提示、工具定义、记忆这些在每轮都会出现的部分,要尽可能精简。省下的空间都是给动态内容的。
最新信息优先。 当空间不足时,优先保留最近几轮的完整信息,压缩或丢弃更早的历史。因为模型在多数情况下需要最近的上下文来做出连贯的决策。
保留关键锚点。 即使压缩历史,也要保留关键的决策点——比如用户的原始需求、重要的中间结论、关键的错误信息。这些锚点帮助模型维持对整体任务的理解。
4.5 上下文压缩:对话紧缩策略
当对话历史接近上下文窗口的限制时,Harness 需要执行"对话紧缩"(Conversation Compaction)。这是 Claude Code 中一个非常精巧的机制,值得深入了解。
4.5.1 压缩的触发时机
Claude Code 采用的策略是:当上下文使用率超过一定阈值(通常是 80% 左右)时,自动触发压缩。也可以由用户主动触发(输入 /compact 命令)。
python
def should_compact(context_tokens, max_tokens):
usage_ratio = context_tokens / max_tokens
return usage_ratio > 0.80
def compact_conversation(messages, system_prompt):
# 使用模型自身来总结对话历史
summary = model.summarize(
messages=messages,
instruction="总结这段对话的关键信息,包括:用户的目标、已完成的步骤、遇到的问题、当前状态。保留所有文件路径、代码片段和具体的技术细节。"
)
# 用摘要替换原始对话历史
compacted = [
{"role": "system", "content": system_prompt},
{"role": "assistant", "content": f"[对话历史摘要]\n{summary}"},
# 保留最近 N 轮原始对话
*messages[-N:]
]
return compacted4.5.2 压缩的质量控制
压缩的核心挑战是:如何在减少 token 的同时不丢失关键信息。以下是几个关键实践:
保留具体细节,丢弃冗余过程。 比如,如果 Agent 花了 5 轮才找到一个 bug,压缩后只需要保留"在文件 X 的第 Y 行发现了 Z 类型的 bug",而不需要保留中间的搜索过程。
保留决策依据,丢弃探索过程。 如果 Agent 考虑了方案 A、B、C,最终选择了 B,压缩后应保留"选择方案 B 的原因是...",而不需要保留对 A 和 C 的详细分析。
保留文件路径和代码关键行。 这些是后续编辑操作的基础。丢失文件路径意味着 Agent 需要重新搜索,浪费工具调用。
4.5.3 分层压缩策略
更高级的压缩策略是分层的:
距离当前轮次 保留级别
─────────────────────────
1-3 轮前 完整保留(原始消息+工具结果)
4-10 轮前 保留消息,压缩工具结果
11-20 轮前 只保留关键决策和结论
20 轮以上 合并为高级摘要这种策略模仿了人类记忆的工作方式:最近的事件记得最清楚,越远的事件越模糊,但关键事件(如重大决策、转折点)会被长期保留。
4.6 工具结果的摘要化处理
工具结果是上下文膨胀的"元凶"。一个不加处理的 ls -la 可能返回几百行,一个 git diff 可能返回几千行。高效的 Harness 会在工具结果进入上下文之前进行智能处理。
4.6.1 截断与摘要
最简单的策略是截断——限制工具结果的最大长度。但粗暴截断可能丢失关键信息。更好的做法是结合截断和摘要:
python
def process_tool_result(tool_name, raw_result, max_tokens=3000):
token_count = count_tokens(raw_result)
if token_count <= max_tokens:
return raw_result # 不需要处理
# 根据工具类型采取不同策略
if tool_name == "Bash":
return truncate_with_head_tail(raw_result, max_tokens)
elif tool_name == "Read":
return raw_result # 代码文件通常需要完整保留
elif tool_name == "Grep":
return keep_top_matches(raw_result, max_tokens)
else:
return summarize_with_model(raw_result, max_tokens)
def truncate_with_head_tail(text, max_tokens):
"""保留开头和结尾,中间用省略号标记"""
lines = text.split('\n')
head = '\n'.join(lines[:30])
tail = '\n'.join(lines[-30:])
omitted = len(lines) - 60
return f"{head}\n\n... ({omitted} lines omitted) ...\n\n{tail}"4.6.2 结构化提取
对于特定类型的工具输出,可以做结构化提取:
python
# 测试输出:只保留失败的测试
def extract_test_failures(test_output):
failures = parse_failures(test_output)
summary = f"共 {total} 个测试,{passed} 个通过,{len(failures)} 个失败。\n"
for f in failures:
summary += f"\n❌ {f.name}: {f.error_message}"
summary += f"\n 位置: {f.file}:{f.line}"
return summary
# TypeScript 编译错误:只保留错误信息
def extract_tsc_errors(tsc_output):
errors = parse_tsc_errors(tsc_output)
return "\n".join(
f"{e.file}:{e.line} - {e.code}: {e.message}"
for e in errors
)这样处理后,一个原本 5000 token 的测试输出可能被压缩到 500 token,但关键信息完全保留。
4.6.3 增量式结果
对于需要多次调用的工具(比如多次运行测试),可以只展示与上次相比的增量变化:
python
def diff_test_results(previous, current):
newly_passed = current.passed - previous.passed
newly_failed = current.failed - previous.failed
summary = f"与上次相比: +{newly_passed} 通过, +{newly_failed} 失败\n"
summary += f"剩余失败: {current.failures}\n"
for f in newly_failed:
summary += f" 新增失败: {f.name} - {f.error}\n"
return summary4.7 检索增强的上下文构建
在大型代码库中,Agent 不可能把所有代码都放入上下文。它需要一个检索系统来"按需拉取"相关信息。这就是 Retrieval-Augmented Context 的核心理念。
4.7.1 多级检索策略
高效的检索系统通常采用多级策略:
第一级:文件级检索。 根据用户意图和当前任务,确定需要查看哪些文件。这通常通过文件名匹配(Glob)和内容搜索(Grep)来实现。
python
# 用户说"修复登录页面的 bug"
# 第一级检索:找到相关文件
files = glob("**/login*.{ts,tsx,vue}")
files += grep("LoginPage|LoginForm|useAuth", type="ts")第二级:片段级检索。 找到文件后,不一定要读取整个文件。如果一个文件有 2000 行,只需要读取相关的函数或类。
python
# 第二级检索:只读取相关片段
content = read_file("src/pages/Login.tsx",
start_line=45, # handleSubmit 函数开始
end_line=120) # handleSubmit 函数结束第三级:语义级检索。 对于更复杂的场景,使用嵌入向量进行语义搜索,找到与当前问题语义最相关的代码片段。
4.7.2 上下文中的信息排列
检索到的信息放在上下文的什么位置也很重要。根据 "Lost in the Middle" 研究,模型对上下文开头和结尾的信息关注度更高。因此:
- 最重要的信息放在上下文的开头或结尾
- 辅助性的背景信息放在中间
- 用户的当前消息始终放在最后(这是大多数 API 的默认行为)
4.8 上下文工程的思维模式
到这里,我们已经讨论了上下文工程的各种技术手段。但最重要的不是具体技术,而是一种思维模式的转变。
4.8.1 从"倾倒"到"策展"
初学者的做法是"倾倒"——把所有可能有用的信息都塞进上下文,让模型自己去找需要的。这就像把整个图书馆的书倒在桌子上,然后让人从中找到答案。
高手的做法是"策展"——像博物馆策展人一样,精心挑选每一件展品,确保每一件都有意义,整体形成一个连贯的叙事。
python
# 反模式:倾倒一切
context = {
"all_source_files": read_entire_repo(), # 500K token
"all_git_history": git_log("--all"), # 100K token
"all_docs": read_all_docs(), # 200K token
"user_message": "修复这个 bug" # 10 token
}
# 结果:上下文溢出,模型无法找到重点
# 正确做法:策展相关信息
context = {
"bug_report": issue_description, # 200 token
"relevant_file": read_file(buggy_file), # 800 token
"related_test": read_file(test_file), # 400 token
"error_log": extract_error(log_output), # 300 token
"user_message": "修复这个 bug" # 10 token
}
# 结果:1710 token,模型可以精准定位问题4.8.2 从"一次性"到"持续管理"
上下文不是设置一次就完事的,它需要在整个 Agent 会话生命周期中持续管理。就像一个运行中的服务器需要持续的运维一样,上下文也需要:
- 监控: 实时追踪 token 使用量
- 清理: 定期压缩和丢弃过时信息
- 补充: 根据任务进展动态拉取新信息
- 优化: 根据模型的行为反馈调整上下文构成
4.8.3 信噪比是核心指标
衡量上下文工程质量的核心指标是信噪比(Signal-to-Noise Ratio)。如果你的上下文有 100K token,但其中只有 10K token 是模型真正需要的,那你的信噪比只有 10%。这意味着模型要在 90% 的噪音中搜索 10% 的信号,效率和准确率都会大打折扣。
优秀的上下文工程应该追求 50% 以上的信噪比——上下文中一半以上的内容都是对当前决策直接有用的。
4.9 常见反模式与修正
在实际构建 Agent 系统时,有几个常见的上下文工程反模式需要警惕。
反模式一:无限制的工具结果
python
# 问题:把原始的 npm install 输出全部放入上下文
result = bash("npm install")
# 可能是几百行的依赖解析信息、下载进度等
# 修正:只保留关键信息
result = bash("npm install")
processed = extract_summary(result)
# "安装完成。新增 3 个包,更新 2 个包,共 847 个包。"反模式二:没有压缩策略的长会话
python
# 问题:20 轮对话后,上下文中还保留着第 1 轮读取的文件内容
messages = []
for turn in range(20):
messages.append(user_input())
response = model.call(messages=messages)
messages.append(response)
tool_results = execute_tools(response)
messages.extend(tool_results)
# messages 只增不减,最终爆掉
# 修正:在每轮结束后检查并压缩
messages = []
for turn in range(20):
if should_compact(messages):
messages = compact(messages)
messages.append(user_input())
response = model.call(messages=messages)
messages.append(response)
tool_results = execute_tools(response)
messages.extend(tool_results)反模式三:忽略 token 成本
python
# 问题:每轮都重新读取整个项目配置
def on_each_turn():
config = read_file("tsconfig.json") # 每轮 200 token
eslint = read_file(".eslintrc.js") # 每轮 300 token
package = read_file("package.json") # 每轮 500 token
# 20 轮下来,光配置文件就重复消耗了 20000 token
# 修正:缓存不变的信息,只在需要时重新读取
cached_config = None
def on_each_turn():
global cached_config
if cached_config is None or config_changed():
cached_config = read_configs()
# 后续轮次直接使用缓存反模式四:上下文中的信息矛盾
这是最隐蔽也最危险的反模式。当上下文中包含互相矛盾的信息时,模型的行为会变得不可预测。
# 场景:
# - CLAUDE.md 中写着"使用 ESLint 进行代码检查"
# - 第 5 轮的工具结果显示项目使用的是 Biome
# - 用户在第 8 轮说"用我们的 linter 检查一下"
# 模型可能会困惑:到底用 ESLint 还是 Biome?修正方案:当发现矛盾时,Harness 应该主动更新或标注。最近的信息应该覆盖旧的信息,并且明确标记哪些是最新的。
反模式五:把上下文当"垃圾桶"
有些系统在每轮开始时都会注入大量的"以防万一"信息——完整的 API 文档、所有可能的错误码表、项目的全部文件列表。这种做法的逻辑是"宁可多了也不能少了",但实际效果恰恰相反。
信息过载会导致模型的"选择困难"。当上下文中有太多信息时,模型会在不同信息之间犹豫不决,输出质量反而下降。记住:少即是多,精即是准。
4.10 本章小结
上下文工程是 Agent 系统设计中最具杠杆效应的环节。它不像提示词工程那样有大量的"技巧集锦"可以速成,而是需要系统性的工程思维。
核心要点回顾:
区分 Prompt 和 Context。 提示词只是上下文的一小部分,上下文才是决定 Agent 行为的全部输入。
理解上下文的七大组成部分。 系统提示、工具定义、对话历史、记忆、检索文档、工具结果、用户消息——每一部分都有其特性和管理策略。
做好预算分配。 把上下文窗口当作有限的预算来管理,为每个组成部分设定合理的 token 额度。
实施压缩策略。 分层压缩、增量更新、结构化提取——确保上下文在整个会话生命周期中保持高信噪比。
避免反模式。 无限制的工具结果、缺失的压缩策略、忽略 token 成本、信息矛盾、信息过载——这些都是上下文工程的常见陷阱。
在下一章中,我们将深入探讨工具设计——Agent 通过工具与外部世界交互,工具的设计质量直接决定了 Agent 的能力上限。而工具的返回结果,正是本章讨论的上下文管理的重要对象。