Harness Engineering
第4章 上下文工程:比 Prompt Engineering 更重要的事
第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)就是关于如何设计、组装、压缩和管理这个上下文包的工程实践。
换成工程语言说:Agent 的关键能力不是”写出一段更漂亮的 prompt”,而是持续策展上下文,让模型每一轮都看到足够相关、足够新、足够可信的信息。
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 上下文窗口有限且昂贵
即使现代模型的上下文窗口已经很大,这个窗口仍然是有限的、昂贵的资源。有三个关键约束:
硬约束:窗口大小。 超过上下文窗口的内容会被截断或导致请求失败。
软约束:注意力衰减。 研究表明,模型对上下文中间位置的信息关注度较低(“Lost in the Middle” 问题)。即使技术上放得下,信息放在 100K 上下文的中间地带,模型可能会”忽略”它。
经济约束:token 费用。 每个 token 都有成本。如果你在每轮交互中都传递完整代码库和完整日志,成本和延迟都会迅速失控。
这意味着上下文工程的核心目标是:在有限的窗口内,放入最相关的信息,以最高的信噪比支撑模型做出正确决策。
4.2.3 提示词是静态的,上下文是动态的
提示词在你写完之后基本固定不变。但上下文是随着每一轮交互动态变化的——新的工具结果加入,旧的信息可能需要压缩或移除。这种动态性是 Prompt Engineering 完全无法覆盖的维度。
用一个比喻来说:如果 Prompt Engineering 是写好一份菜单,那么 Context Engineering 就是经营整个厨房——管理食材进出、控制库存、确保每道菜上桌时用的都是最新鲜的原料。
4.3 上下文的七大组成部分
一个 Agent 系统的上下文通常由以下七个部分组成。理解每一部分的特性,是做好上下文工程的基础。
graph TD
subgraph Context["上下文窗口"]
direction TB
SP["① 系统提示\n稳定"] --- TD["② 工具定义\n半稳定"]
TD --- CI["③ 动态上下文注入\n每会话不同"]
CI --- CH["④ 对话历史\n持续增长"]
CH --- TR["⑤ 工具结果\n波动最大"]
TR --- RM["⑥ 记忆/检索\n按需注入"]
RM --- RS["⑦ 模型响应空间\n必须预留"]
end
style SP fill:#dbeafe,stroke:#3b82f6
style TD fill:#dbeafe,stroke:#3b82f6
style CI fill:#fef3c7,stroke:#f59e0b
style CH fill:#fce7f3,stroke:#ec4899
style TR fill:#fee2e2,stroke:#ef4444
style RM fill:#dcfce7,stroke:#22c55e
style RS fill:#f3e8ff,stroke:#a855f7
4.3.1 系统提示(System Prompt)
系统提示是 Harness 注入的”基础人格”,定义了 Agent 的角色、能力边界和行为规则。它通常是上下文中最稳定的部分——在整个会话过程中不会改变。
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.
"""
工程要点: 系统提示应该简洁、精确。每多一句废话,就占用了本可以放更有价值信息的空间。更重要的是,系统提示属于每轮都要携带的固定开销;固定开销越大,动态任务信息的预算就越少。
4.3.2 工具定义(Tool Definitions)
工具定义是 JSON Schema 格式的工具说明,告诉模型有哪些工具可用、每个工具接受什么参数。这部分的开销常常被低估。
以 Claude Code 为例,它定义了 Read、Edit、Write、Bash、Grep、Glob 等工具。每个工具的 schema 加上描述都会占用固定上下文。如果工具数量持续增加,工具定义本身就会成为一项显著的预算开销。
工程要点: 工具定义的质量直接影响模型使用工具的正确率。描述要精确但不冗长。如果可能,使用”延迟加载”策略——不要一次性注入所有工具定义,而是根据当前任务动态选择相关工具。
# 反模式:一次性注入所有工具
tools = load_all_tools()
# 正确做法:根据任务阶段选择工具子集
if task_phase == "analysis":
tools = [read_tool, grep_tool, glob_tool]
elif task_phase == "editing":
tools = [read_tool, edit_tool, write_tool, bash_tool]
4.3.3 对话历史(Conversation History)
对话历史是之前所有轮次的用户消息、模型回复、工具调用和工具结果的完整记录。这是上下文中增长最快的部分,也是最需要管理的部分。
一个典型的 Agent 会话中,对话历史的增长曲线大致如下:
Token 消耗
│
│ ╱ 未压缩
│ ╱
│ ╱
│ ╱
│ ╱
│ ╱
│ ────────────────────── 压缩后
│ ╱
│ ╱
│╱
└──────────────────────────────── 轮次
不做任何管理的情况下,长会话的对话历史会持续增长。这不仅占满上下文窗口,还会导致模型把注意力分散到大量不再相关的历史信息上。
4.3.4 记忆(Memory)
记忆是跨会话持久化的信息,比如 Claude Code 中的 CLAUDE.md 文件。它记录了项目规则、用户偏好、常用命令等。
# 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 预算。下面不是固定配额,而是一种预算模型:
┌─────────────────────────────────────────────┐
│ 组成部分 │ 预算策略 │ 管理方式 │
├─────────────────────────────────────────────┤
│ System Prompt │ 固定且尽量小 │ 版本化 │
│ Tool Definitions │ 半固定 │ 按需加载 │
│ Memory (CLAUDE.md) │ 小而稳定 │ 定期清理 │
│ Conversation History │ 可增长 │ 压缩摘要 │
│ Retrieved Documents │ 按任务注入 │ 检索排序 │
│ Tool Results (当轮) │ 波动最大 │ 截断摘要 │
│ User Message (当轮) │ 高优先级 │ 原样保留 │
│ 预留 (安全边际) │ 必须保留 │ 防止超窗 │
├─────────────────────────────────────────────┤
│ 总计 │ 不超过模型窗口 │ 动态调整 │
└─────────────────────────────────────────────┘
这个分配不是固定的,而是动态调整的。在会话早期,对话历史很短,可以给检索文档和工具结果更多空间。在会话后期,对话历史占据主导,检索文档和工具结果需要更积极地压缩。
实践原则:
-
固定开销最小化。 系统提示、工具定义、记忆这些在每轮都会出现的部分,要尽可能精简。省下的空间都是给动态内容的。
-
最新信息优先。 当空间不足时,优先保留最近几轮的完整信息,压缩或丢弃更早的历史。因为模型在多数情况下需要最近的上下文来做出连贯的决策。
-
保留关键锚点。 即使压缩历史,也要保留关键的决策点——比如用户的原始需求、重要的中间结论、关键的错误信息。这些锚点帮助模型维持对整体任务的理解。
4.4.1 上下文预算的决策表
预算分配不能只按 token 数做机械切分,还要看信息对当前决策的作用。一个实用的判断表如下:
| 信息类型 | 默认处理 | 升级保留的条件 | 降级/丢弃的条件 |
|---|---|---|---|
| 用户原始目标 | 原样保留 | 任务跨多轮、需求多次变更 | 几乎不丢弃,只做短摘要备份 |
| 当前计划 | 原样保留 | 计划仍在执行中 | 计划已完成或被用户明确替换 |
| 文件内容 | 保留相关片段 | 即将编辑、测试失败指向该文件 | 只是背景阅读且后续未引用 |
| 命令输出 | 保留错误和摘要 | 包含失败原因、路径、退出码 | 成功日志、下载进度、重复输出 |
| 搜索结果 | 保留命中列表和少量上下文 | 后续步骤依赖文件集合 | 已经读取了具体目标文件 |
| 历史推理 | 保留结论和分歧点 | 方案选择仍影响后续执行 | 中间探索已被新事实取代 |
| 项目规则 | 保留适用规则 | 与当前文件或工具直接相关 | 与任务无关、过期、可按需读取 |
这个表的核心思想是:上下文不是档案库,而是决策工作台。工作台上应该放当前要用的工具、材料和图纸;已经用完的材料要归档,可能稍后要用的材料要能快速取回,而不是一直堆在手边。
还要区分”删除”和”可恢复”。很多信息不应该永久丢失,只是不应该继续占据模型上下文。例如完整测试日志可以写入 transcript 或观测系统,模型上下文只保留失败摘要和路径;完整历史对话可以保留在会话存储里,当前请求只携带压缩摘要。这样既不牺牲可追溯性,也不让模型每轮背负全部历史。
4.5 上下文压缩:对话紧缩策略
当对话历史接近上下文窗口的限制时,Harness 需要执行”对话紧缩”(Conversation Compaction)。这是 Claude Code 中一个非常精巧的机制,值得深入了解。
4.5.1 压缩的触发时机
flowchart LR
A["每轮迭代后"] --> B{"达到压缩阈值?"}
B -->|否| C["继续正常对话"]
B -->|是| D["触发自动压缩"]
D --> E["用 LLM 摘要\n旧消息"]
E --> F["保留最近 N 轮\n+ 摘要"]
F --> G["释放 token 空间"]
G --> C
U["用户输入 /compact"] --> D
Claude Code 的本地快照中,自动压缩阈值不是写死的百分比,而是用有效上下文窗口减去缓冲区来计算:AUTOCOMPACT_BUFFER_TOKENS = 13_000,getAutoCompactThreshold() 返回 effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS,同时支持 CLAUDE_AUTOCOMPACT_PCT_OVERRIDE 作为测试覆盖(../claude-code-main/src/services/compact/autoCompact.ts:62-90)。用户也可以主动触发 /compact。
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 compacted
4.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 截断与摘要
最简单的策略是截断——限制工具结果的最大长度。但粗暴截断可能丢失关键信息。更好的做法是结合截断和摘要:
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 结构化提取
对于特定类型的工具输出,可以做结构化提取:
# 测试输出:只保留失败的测试
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
)
这样处理后,原本很长的测试输出可以被压缩成只包含失败用例、错误位置和关键堆栈的摘要;关键不是压缩比例,而是保留足够支撑下一步决策的信息。
4.6.3 增量式结果
对于需要多次调用的工具(比如多次运行测试),可以只展示与上次相比的增量变化:
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 summary
4.7 检索增强的上下文构建
在大型代码库中,Agent 不可能把所有代码都放入上下文。它需要一个检索系统来”按需拉取”相关信息。这就是 Retrieval-Augmented Context 的核心理念。
4.7.1 多级检索策略
高效的检索系统通常采用多级策略:
第一级:文件级检索。 根据用户意图和当前任务,确定需要查看哪些文件。这通常通过文件名匹配(Glob)和内容搜索(Grep)来实现。
# 用户说"修复登录页面的 bug"
# 第一级检索:找到相关文件
files = glob("**/login*.{ts,tsx,vue}")
files += grep("LoginPage|LoginForm|useAuth", type="ts")
第二级:片段级检索。 找到文件后,不一定要读取整个文件。如果一个文件有 2000 行,只需要读取相关的函数或类。
# 第二级检索:只读取相关片段
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 从”倾倒”到”策展”
初学者的做法是”倾倒”——把所有可能有用的信息都塞进上下文,让模型自己去找需要的。这就像把整个图书馆的书倒在桌子上,然后让人从中找到答案。
高手的做法是”策展”——像博物馆策展人一样,精心挑选每一件展品,确保每一件都有意义,整体形成一个连贯的叙事。
# 反模式:倾倒一切
context = {
"all_source_files": read_entire_repo(),
"all_git_history": git_log("--all"),
"all_docs": read_all_docs(),
"user_message": "修复这个 bug"
}
# 结果:上下文溢出,模型无法找到重点
# 正确做法:策展相关信息
context = {
"bug_report": issue_description,
"relevant_file": read_file(buggy_file),
"related_test": read_file(test_file),
"error_log": extract_error(log_output),
"user_message": "修复这个 bug"
}
# 结果:上下文更短,信号更集中,模型更容易定位问题
4.8.2 从”一次性”到”持续管理”
上下文不是设置一次就完事的,它需要在整个 Agent 会话生命周期中持续管理。就像一个运行中的服务器需要持续的运维一样,上下文也需要:
- 监控: 实时追踪 token 使用量
- 清理: 定期压缩和丢弃过时信息
- 补充: 根据任务进展动态拉取新信息
- 优化: 根据模型的行为反馈调整上下文构成
4.8.3 信噪比是核心指标
衡量上下文工程质量的核心指标是信噪比(Signal-to-Noise Ratio)。如果上下文中大部分内容都和当前决策无关,模型就要在噪音中寻找少量信号,效率和准确率都会下降。
优秀的上下文工程不追求”塞满窗口”,而追求”每一段信息都有明确用途”:它要么定义行为边界,要么提供当前任务事实,要么保留关键历史,要么支持下一步验证。说不清用途的信息,就应该被延迟加载、摘要或丢弃。
4.9 常见反模式与修正
在实际构建 Agent 系统时,有几个常见的上下文工程反模式需要警惕。
反模式一:无限制的工具结果
# 问题:把原始的 npm install 输出全部放入上下文
result = bash("npm install")
# 可能是几百行的依赖解析信息、下载进度等
# 修正:只保留关键信息
result = bash("npm install")
processed = extract_summary(result)
# "安装完成。新增 3 个包,更新 2 个包,共 847 个包。"
反模式二:没有压缩策略的长会话
# 问题: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 成本
# 问题:每轮都重新读取整个项目配置
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.9.1 本地快照:Claude Code 上下文工程子系统的代码量
§4.5 讨论”压缩触发时机 / 质量控制 / 分层策略”。这些抽象在本地 ../claude-code-main/src/services/ 里有具体实现:
../claude-code-main/src/services/compact/ 11 文件 3960 行:
| 文件 | 行 | 角色 |
|---|---|---|
compact.ts | 1705 | 本目录最大——主压缩流程(编排 + state 管理 + 失败重试) |
sessionMemoryCompact.ts | 630 | session memory 写入式压缩(默认 maxTokens: 40_000) |
microCompact.ts | 530 | 微压缩——比对话紧缩小一档的局部摘要 |
prompt.ts | 374 | 压缩用的 LLM prompt 模板 |
autoCompact.ts | 351 | 自动触发逻辑——getAutoCompactThreshold 函数 |
apiMicrocompact.ts | 153 | API 层的微压缩入口 |
postCompactCleanup.ts | 77 | 压缩后清理 |
grouping.ts | 63 | 消息分组算法 |
| 其余(compactWarningHook / compactWarningState / timeBasedMCConfig) | 余下 | 警告 UI + 时间窗口配置 |
../claude-code-main/src/services/SessionMemory/ + extractMemories/ 共 1795 行——对应 §4.3.4 “记忆”组件的实现。
合计 5755 行,专门处理压缩、session memory 和 memory extraction;对比 src/services/ 的 53495 行,这是服务层里相当可观的一块。
两条值得记住的源码常数:
AUTOCOMPACT_BUFFER_TOKENS = 13_000(../claude-code-main/src/services/compact/autoCompact.ts:62)——自动压缩阈值是有效窗口减去缓冲区。这样做是为了给压缩请求本身预留输入和输出空间。sessionMemoryCompact.ts默认maxTokens: 40_000(../claude-code-main/src/services/compact/sessionMemoryCompact.ts:45-60)——session memory 有自己的压缩上限。上下文管理不是只有一次全局 compact,而是有对话压缩和 memory 内部压缩的分层。
microCompact.ts 530 行 + apiMicrocompact.ts 153 行 = 683 行专给”微压缩”。这是一种介于 §4.6 “工具结果摘要” 和 §4.5 “全局压缩” 之间的策略:不等到全局 compact 才处理上下文压力,而是在工具结果或局部历史变重时先做局部整理。
autoCompact.ts:79-89 还提供 CLAUDE_AUTOCOMPACT_PCT_OVERRIDE,用于按百分比覆盖阈值。这是生产代码保留调试出口的范例:默认策略稳定,测试和诊断时可以显式覆盖。
4.10 本章小结
上下文工程是 Agent 系统设计中最具杠杆效应的环节。它不像提示词工程那样有大量的”技巧集锦”可以速成,而是需要系统性的工程思维。
核心要点回顾:
-
区分 Prompt 和 Context。 提示词只是上下文的一小部分,上下文才是决定 Agent 行为的全部输入。
-
理解上下文的七大组成部分。 系统提示、工具定义、对话历史、记忆、检索文档、工具结果、用户消息——每一部分都有其特性和管理策略。
-
做好预算分配。 把上下文窗口当作有限的预算来管理,为每个组成部分设定合理的 token 额度。
-
实施压缩策略。 分层压缩、增量更新、结构化提取——确保上下文在整个会话生命周期中保持高信噪比。
-
避免反模式。 无限制的工具结果、缺失的压缩策略、忽略 token 成本、信息矛盾、信息过载——这些都是上下文工程的常见陷阱。
物理事实:Claude Code 上下文工程子系统在本地快照中包括 compact/ 11 文件 3960 行,SessionMemory/ + extractMemories/ 1795 行,合计 5755 行;AUTOCOMPACT_BUFFER_TOKENS = 13_000 和 session memory 默认 maxTokens = 40_000 说明它采用了全局压缩、session memory 压缩和微压缩并存的分层策略。