Harness Engineering

第4章 上下文工程:比 Prompt Engineering 更重要的事

作者 杨艺韬 · 7,036 字

第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 可能需要:

  1. 检索现有的 API 文件,了解代码风格
  2. 检索路由配置文件,了解路由注册方式
  3. 检索数据库模型定义,了解数据结构

每个检索结果可能有几百到几千 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 (当轮)    │ 高优先级       │ 原样保留 │
│ 预留 (安全边际)        │ 必须保留       │ 防止超窗 │
├─────────────────────────────────────────────┤
│ 总计                   │ 不超过模型窗口 │ 动态调整 │
└─────────────────────────────────────────────┘

这个分配不是固定的,而是动态调整的。在会话早期,对话历史很短,可以给检索文档和工具结果更多空间。在会话后期,对话历史占据主导,检索文档和工具结果需要更积极地压缩。

实践原则:

  1. 固定开销最小化。 系统提示、工具定义、记忆这些在每轮都会出现的部分,要尽可能精简。省下的空间都是给动态内容的。

  2. 最新信息优先。 当空间不足时,优先保留最近几轮的完整信息,压缩或丢弃更早的历史。因为模型在多数情况下需要最近的上下文来做出连贯的决策。

  3. 保留关键锚点。 即使压缩历史,也要保留关键的决策点——比如用户的原始需求、重要的中间结论、关键的错误信息。这些锚点帮助模型维持对整体任务的理解。

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_000getAutoCompactThreshold() 返回 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.ts1705本目录最大——主压缩流程(编排 + state 管理 + 失败重试)
sessionMemoryCompact.ts630session memory 写入式压缩(默认 maxTokens: 40_000
microCompact.ts530微压缩——比对话紧缩小一档的局部摘要
prompt.ts374压缩用的 LLM prompt 模板
autoCompact.ts351自动触发逻辑——getAutoCompactThreshold 函数
apiMicrocompact.ts153API 层的微压缩入口
postCompactCleanup.ts77压缩后清理
grouping.ts63消息分组算法
其余(compactWarningHook / compactWarningState / timeBasedMCConfig)余下警告 UI + 时间窗口配置

../claude-code-main/src/services/SessionMemory/ + extractMemories/ 共 1795 行——对应 §4.3.4 “记忆”组件的实现。

合计 5755 行,专门处理压缩、session memory 和 memory extraction;对比 src/services/ 的 53495 行,这是服务层里相当可观的一块。

两条值得记住的源码常数

  1. AUTOCOMPACT_BUFFER_TOKENS = 13_000../claude-code-main/src/services/compact/autoCompact.ts:62)——自动压缩阈值是有效窗口减去缓冲区。这样做是为了给压缩请求本身预留输入和输出空间。
  2. 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 系统设计中最具杠杆效应的环节。它不像提示词工程那样有大量的”技巧集锦”可以速成,而是需要系统性的工程思维。

核心要点回顾:

  1. 区分 Prompt 和 Context。 提示词只是上下文的一小部分,上下文才是决定 Agent 行为的全部输入。

  2. 理解上下文的七大组成部分。 系统提示、工具定义、对话历史、记忆、检索文档、工具结果、用户消息——每一部分都有其特性和管理策略。

  3. 做好预算分配。 把上下文窗口当作有限的预算来管理,为每个组成部分设定合理的 token 额度。

  4. 实施压缩策略。 分层压缩、增量更新、结构化提取——确保上下文在整个会话生命周期中保持高信噪比。

  5. 避免反模式。 无限制的工具结果、缺失的压缩策略、忽略 token 成本、信息矛盾、信息过载——这些都是上下文工程的常见陷阱。

物理事实:Claude Code 上下文工程子系统在本地快照中包括 compact/ 11 文件 3960 行,SessionMemory/ + extractMemories/ 1795 行,合计 5755 行;AUTOCOMPACT_BUFFER_TOKENS = 13_000 和 session memory 默认 maxTokens = 40_000 说明它采用了全局压缩、session memory 压缩和微压缩并存的分层策略。