上下文工程:比 Prompt 更重要的那一层

2023 年大家卷 Prompt 工程,2025 年之后行业共识逐渐转向一个更大的概念:上下文工程(Context Engineering)。两者的区别一句话能说清——Prompt 工程优化「你怎么对模型说话」,上下文工程管理「模型做决策的那一刻,窗口里到底有什么」。

单轮问答时代两者几乎是一回事。但当任务变成长程 Agent——跑几十分钟、调几百次工具、处理几万行代码——窗口里的内容 95% 不再是你写的 prompt,而是系统注入的工具定义、检索结果、历史消息、文件内容。这时候决定产出质量的,是这 95% 的管理水平。

上下文是稀缺资源,要当预算管

先建立一个核心心智:上下文窗口不是仓库,是工作台。仓库越大越好,工作台不是——堆满杂物的工作台干不了精细活。

这不只是比喻。实证上有两个效应:一是「迷失在中间」(lost in the middle),窗口中部的信息被利用的概率显著低于首尾;二是上下文腐烂(context rot),随着无关信息占比上升,模型遵循指令的能力整体退化,哪怕关键信息明明还在窗口里。窗口标称 100 万 token,不代表你应该塞 100 万——有效注意力远小于标称容量。

所以上下文工程的本质是预算管理:每一段进入窗口的内容,都要回答「它在这个决策时刻的边际价值,配得上它占的位置吗」。

上下文预算分配

四个操作:写入、选取、压缩、隔离

业内逐渐收敛出四个基本操作,几乎所有上下文工程技巧都是它们的组合。

写入(Write):把信息移出窗口,存到外部。 Agent 的中间结论、计划、待办,写到文件或状态存储里,而不是依赖对话历史。典型实现是让 Agent 维护一个 notes.md / 任务清单文件——窗口会被挤爆,文件不会。跨会话的记忆系统也属于这类。

选取(Select):按需拉回来。 决策时刻只注入相关的部分。RAG 是选取,工具的动态裁剪是选取(一百个工具只注入语义相关的十个),Agent 主动 grep/read 文件也是选取。选取的关键指标不是召回率,是信噪比——多拉进来的每一段无关内容都在划伤工作台。

压缩(Compact):有损地缩小占用。 对话历史超过阈值时做摘要压缩;工具返回的 5000 行 JSON 先裁剪成关键字段再入窗。压缩的风险是丢掉后来才知道重要的细节,所以原则是:可再生的信息(随时能重新读到的文件内容)激进压缩,不可再生的信息(一次性的执行结果、用户的口头约束)保守压缩。

隔离(Isolate):拆给子上下文。 把高消耗的子任务委托给 sub-agent,它在自己的窗口里烧几十万 token,最后只把三段结论带回主窗口。上一篇讲的多 Agent 协作,从上下文工程视角看就是隔离操作的系统化。

一个真实的调优案例

我们一个内部代码审查 Agent,初版效果很差:审到第 30 个文件时开始胡言乱语,把第 5 个文件的问题安到第 28 个文件头上。排查后发现窗口已被三类东西塞满:全量工具定义(18k token)、每个文件的完整内容(平均 6k×30)、以及越滚越长的审查历史。

按四操作改造:

写入  审查发现实时写入 findings.json,窗口里只保留
      「当前文件 + 最近 3 条发现的摘要」
选取  文件内容不再全量注入,先注入结构骨架(函数签名+行数),
      Agent 按需读具体函数体
压缩  每审完 10 个文件做一次历史压缩,保留结论丢弃过程
隔离  跨文件的一致性检查拆给独立 sub-agent,
      输入是 findings.json 而非对话历史

改造后单文件审查的窗口占用从持续增长变成稳定在 25k token 左右,30 文件之后的准确率和前 5 个文件无显著差异。值得注意的是:模型没换、prompt 几乎没动,纯上下文结构的改造带来了从不可用到可用的跨越。这就是我说「比 Prompt 更重要」的依据。

给上下文做监控

工程化的标志是可度量。我们现在给所有 Agent 加了三个上下文指标:

@dataclass
class ContextMetrics:
    window_usage: float      # 窗口占用率,>70% 触发压缩
    composition: dict        # 构成比例:指令/工具/历史/检索各占多少
    staleness: float         # 陈旧度:窗口中多少 token 与最近
                             # 5 次决策无关(采样评估)

构成比例(composition)最有诊断价值。健康的长程 Agent 大致是:系统指令 5~10%、工具定义 10~15%、工作记忆(计划+近期历史)30~40%、当前任务素材 30~40%、预留 15~20%。如果发现工具定义占了 35%,不用看效果就知道该做工具裁剪了。

几个容易踩的反模式

落地过程中见过的典型错误,列出来供对照自查。

反模式一:把系统提示词当垃圾抽屉。 每次出 bad case 就往 system prompt 里加一条规则,半年后系统提示词膨胀到八千 token,规则之间互相打架,模型无所适从。正确做法是定期重构提示词,就像重构代码一样——合并同类规则、删除已被模型默认行为覆盖的规则,把场景化的规则移到 Skill 里按需加载。

反模式二:检索结果不设预算上限。 RAG 召回 top-k 的 k 拍脑袋设成 20,每段 chunk 500 token,单次检索就吃掉一万 token,其中真正相关的往往只有前三段。检索预算应该从答案质量倒推:先用小 k 跑评测集,k 加大到指标不再提升就停——多数场景这个拐点出现在 3~5。

反模式三:用「重新开一个会话」代替压缩策略。 窗口满了就让用户开新会话,所有上下文清零重来。这是把成本转嫁给了用户——他们要手动复述背景。哪怕最朴素的自动压缩(保留系统指令 + 最近 N 轮 + 一段历史摘要)都比清零体验好得多。

反模式四:只在出问题时才看上下文。 上下文构成应该和延迟、错误率一样进日常监控面板。腐烂是渐进的,等用户投诉”AI 变笨了”再去看,往往已经烂了几周。

和 Skill、Harness 的关系

把这半年聊过的几个概念串起来:Harness 是 Agent 的执行框架(循环、工具、权限),Skill 是按需加载的能力包,上下文工程是贯穿其中的资源管理纪律。Skill 的「渐进式披露」——先给模型看一行摘要,需要时才加载全文——本身就是上下文工程里「选取」操作的优雅实现。这些东西不是并列的三个 buzzword,是同一套系统的三个切面:Harness 定义边界,Skill 提供能力,上下文工程决定单位注意力的产出效率。

结语

回头看这几年的路线,从优化一句话(Prompt),到优化一次调用的输入(RAG),再到管理一个长程系统里信息的全生命周期,方向是一致的:把炼丹一步步变成工程,可度量、可监控、可优化。

如果只能带走一条:下次 Agent 表现退化,先别急着改 prompt 或者换模型,把那一刻的窗口内容完整 dump 出来,自己从头读一遍。我每次这么做都会看到一个被工具返回值、过期历史和无关检索塞满的工作台,然后改法自然就出来了。这个笨办法教给过好几个同事,没有一次落空。