Skip to content

12 · 上下文工程 ​

速览卡 ​

项内容
一句话定位决定模型"看什么"的那层工程——同样的模型,上下文装配质量不同,输出质量能差一个档次
核心内容上下文装配、Token 预算与压缩、失败模式识别、记忆分层、工具与上下文的关系
现状判断已取代"提示词工程"成为主流说法;是 Agent 类应用最容易被低估、也最容易返工的一层
读完能回答我的 Agent 跑几轮就变笨,到底是哪里出了问题?该加东西还是该删东西?
相关板块02 模型能力增强技术 | 03 Agent | 05 工程化 | 实操 06 Agent 实战

一、先纠正一个直觉:窗口大 ≠ 该多塞 ​

最常见的误区是把上下文窗口当成"硬盘"——1M 的窗口,那我把所有资料都塞进去不就好了?

塞得下和用得好是两回事。 三条绕不开的物理约束:

约束表现后果
注意力稀释上下文里的无关信息越多,模型对关键信息的注意力越分散相关信息明明在,却"看不见"
位置偏差模型对开头与结尾的敏感度高,中间部分容易被忽略关键信息埋在中间=白放
成本非线性注意力开销随序列长度增长,长上下文的边际成本高于短上下文 [易变]第 10 万个 token 的代价远高于第 1 千个

💡 一个反复被验证的经验值:窗口填充率控制在 30%~40% 时模型表现最好, 塞满反而触发下面第四节要讲的"上下文腐化"。这条比"用多大窗口"重要得多。

所以上下文工程的第一性原则是:每一个进入窗口的 token 都要"挣"到它的位置。


二、上下文里到底装了什么 ​

先把窗口拆开,才知道能优化哪里。一次典型调用的输入:

组成谁来定常见占比优化的杠杆
系统指令你小写清楚、别写全(见第七节反模式)
工具定义你中数量与描述质量影响巨大
检索来的资料检索系统大RAG 的主战场,见 02 篇
历史对话累积越跑越大压缩 / 驱逐
工具返回结果外部世界最容易失控截断、只留必要字段
工作记忆 / 中间产物你中结构化笔记、外部化存储
输出预留—必须留忘了留就是"生成到一半截断"

⚠️ 新手最常翻车的是"工具返回结果":一次 cat 一个大文件、一次全表查询, 几万 token 就进去了,而且每轮都在。这是上下文膨胀的头号来源, 却因为"我只是调了个工具"而被忽略。


三、Token 预算:先划地盘,再谈优化 ​

不要等跑崩了再压缩。开工前就给每一段划上限,超了就截断或淘汰。

一个可直接抄的比例框架(按窗口总量算,比例比绝对值稳定):

段建议占比超了怎么办
系统指令2%~4%拆出去:规则按目录放、能力做成按需加载
工具定义2%~3%减少工具数;或按阶段渐进披露
检索资料10%~20%重排后截断;按源设子预算
历史对话(已压缩)20%~30%淘汰旧轮次,保留决策与结论
工作记忆20%~30%结构化摘要,细节外置
安全缓冲20%~30%留给出乎意料的工具输出与生成

填充率的分区响应(把预算做成运行时策略,而不是一次性规定):

填充率状态动作
0~50%正常正常跑
50%~70%注意压缩工具输出
70%~85%警告淘汰低价值历史
85%~100%危险存档 + 起子任务(别硬撑)

这个"分区响应"是本篇最值得照搬的一条——它把"什么时候压缩"从拍脑袋变成了可配置的规则。 多数上下文事故,都是因为没有在 70% 那一步做任何动作。

3.1 把预算做成代码,而不是文档 ​

上面那张比例表只有落地成运行时检查才有用。一个最小可抄的预算守卫:

python
BUDGET = {                       # 各段占窗口总量比例
    "system": 0.04, "tools": 0.03, "retrieval": 0.20,
    "history": 0.30, "memory": 0.30, "buffer": 0.13,
}

def partition_response(fill_rate: float) -> str:
    """填充率到了就触发动作,而不是等崩。"""
    if fill_rate < 0.50:  return "normal"
    if fill_rate < 0.70:  return "compress_tool_outputs"
    if fill_rate < 0.85:  return "evict_low_value_history"
    return "archive_and_spawn_subtask"     # 别硬撑

# 用法:每轮拼上下文前算一次 fill_rate,按返回值决定本轮回不回收
action = partition_response(current_tokens / WINDOW_SIZE)

关键点:分区阈值写死在代码里,团队才有统一的行为预期。 否则"什么时候压缩"永远取决于是谁在值班——这正是事故来源。

3.2 先测量,再优化 ​

不做测量就调上下文,几乎一定在做无用功。一个把一次调用拆成各类 token 的小工具:

python
def profile_context(ctx: dict) -> dict:
    return {
        "system":   count_tokens(ctx["system"]),
        "tools":    sum(count_tokens(t["schema"]) for t in ctx["tools"]),
        "retrieval":count_tokens(ctx["retrieval"]),
        "history":  count_tokens(ctx["history"]),
        "tool_outputs": sum(count_tokens(r) for r in ctx["tool_results"]),
    }
# 跑一周,你会发现自己以为"很有用"的那段,常常占了 40% 却从没被模型引用

四、失败模式:先会诊断,再谈开药 ​

上下文出问题时,表现都是"模型变笨了",但病因不同。四种失败模式(Drew Breunig 的分类,业内常用):

模式症状根因对策
污染 Poisoning一个幻觉被后续轮次当作事实反复引用错误内容进了上下文并留存注入前校验;对关键事实做二次确认
分心 Distraction模型执着于历史里的某个细节,忘了当前任务历史过长淹没了训练所得的判断力压缩历史;把目标前置重申
混淆 Confusion输出格式乱了、用了不该用的工具无关信息(尤其多余的工具定义)干扰了决策精简工具;结构化指令
冲突 Clash模型在两个说法之间摇摆,或输出自相矛盾上下文中存在互相矛盾的信息检索去重;标注来源与时效

上下文腐化(Context Rot) 是这四种的统称表现:随着上下文变长,模型准确率持续下降, 而且下降是静默的——没有报错,只是答案慢慢变差。这正是它危险的地方。

诊断口诀:先问"变长了还是变脏了"。 变长了 → 压缩与驱逐;变脏了(有错、有冲突)→ 校验与去重。两者常同时发生。


五、压缩:三层级联,从便宜到贵 ​

压缩不是只有"让模型总结一下"这一招。按代价从低到高:

层手段代价适用
第 1 层工具输出截断:只返回必要字段、限制行数、给"静默模式"几乎为零应该永远先做这层
第 2 层输入驱逐:按规则淘汰旧轮次/旧片段,保留骨架零(纯规则)通用
第 3 层LLM 摘要:让模型压缩历史一次额外调用 + 有损前两层兜不住时

很多团队一上来就做第 3 层,结果既贵又丢信息。第 1、2 层能解决大部分压力, 第 3 层是兜底,不是默认。

摘要时要用结构化字段,不要让它写散文。推荐字段:

会话意图 / 已完成的步骤 / 关键决策与理由 / 当前进度 / 未决问题 / 下一步

💡 一个反直觉但有效的做法(来自编码 Agent 的实践): 保留完整的"行动与推理骨架",只把旧的"工具输出"替换成占位符。 比整体摘要更省、也更不容易丢关键细节。[有争议] 不同场景效果有差异,建议自己测。

5.1 三层级联的可运行实现 ​

理论落到代码。注意顺序:永远先做第 1、2 层,第 3 层是兜底。

python
# 第 1 层:工具输出截断(零成本,应该永远先做)
def truncate_tool_results(results, max_lines=20, keep_fields=("status", "id")):
    cleaned = []
    for r in results:
        # 只留必要字段、限制行数,别把整个文件塞回上下文
        cleaned.append({k: r[k] for k in keep_fields if k in r})
    return [truncate_lines(c, max_lines) for c in cleaned]

# 第 2 层:输入驱逐(纯规则,零调用)
def evict_old_turns(history, keep_recent=6, keep_decisions=True):
    recent = history[-keep_recent:]
    if keep_decisions:
        # 把"已做的决策"抽成一行摘要留着,细节丢给旧轮次
        decisions = [t for t in history[:-keep_recent] if t.get("is_decision")]
        return decisions + recent
    return recent

# 第 3 层:LLM 摘要(有损,前两层兜不住才用)
def summarize_history(history, llm):
    skeleton = "会话意图 / 已完成的步骤 / 关键决策与理由 / 当前进度 / 未决问题 / 下一步"
    return llm.complete(f"按字段压缩:{skeleton}\n{history}")

很多团队一上来就写第 3 层(最"高级"),结果既贵又丢信息。 正确顺序是 1 → 2 → 3:前两层能解决大部分压力,第 3 层只在"还得挤"时兜底。


六、记忆:别把所有"记住"都塞进上下文 ​

"让 Agent 记住"不等于"放进上下文窗口"。记忆应当分层外置:

层存什么放哪生命周期
工作记忆当前任务的中间状态上下文窗口任务结束即弃
情景记忆之前几次会话发生了什么外部存储,按需检索短期,会过期
语义记忆稳定的事实(用户偏好、架构决策)外部存储,常驻少量摘要长期,需人工确认
程序记忆做事的套路(怎么发布、怎么迁移)技能/脚本,按需加载长期

关键原则:

  • 常驻上下文的只放"索引"(几十到几百 token 的清单),具体内容按需取——这是"能力按需加载"的核心收益
  • 记忆必须有过期机制。不朽的记忆会变成污染:三个月前的决定今天已经作废,但模型会照旧执行
  • 写入前先判断:这条值得长期记住吗?还是只是本次任务的中间态?

💡 落点建议:语义/情景记忆的持久化,已有 Postgres 的团队可直接用向量库——把"记忆条目"存成带元数据(用户/时效/类型)的向量行,按需检索回填上下文。 索引、过滤与过期清理的具体做法见 DB11 · pgvector。


七、工具与上下文:少即是多 ​

工具定义占据上下文,而且每个工具都会参与模型的决策——工具越多,选错的概率越高。

建议理由
同时暴露 5~8 个工具超过这个数,各模型上的表现普遍下降
工具职责不重叠重叠会让模型难以抉择,是"混淆"的主要来源
按需加载而非全量预置用不到却挂在上下文里,是纯浪费
描述要写清"什么时候用我"含糊的描述导致该调不调、不该调乱调

一个被反复引用的案例:某团队删掉了一批工具后,任务准确率从 80% 升到 100%。 这说明上下文里的"噪音"代价被系统性低估了——减法常常比加法有效。


八、自检清单 ​

  • [ ] 能在不看资料的情况下列出一次调用的 7 类上下文组成,并指出本项目里哪一类最容易膨胀
  • [ ] 给自己的系统设了分段的 Token 预算,而不是只有一个总上限
  • [ ] 有填充率分区响应策略(70% 时做什么、85% 时做什么)
  • [ ] 压缩按三层级联实施,且第 1 层(工具输出截断)已做
  • [ ] 记忆是分层外置的,上下文里只放索引
  • [ ] 记忆有过期/淘汰机制,不存在"不朽记忆"
  • [ ] 工具数量在 8 个以内,职责不重叠
  • [ ] 能区分四种失败模式,并说出各自的对策
  • [ ] 有上下文的回归测试:已知 query 能否召回对的文档、是否超预算、PII 是否脱敏

九、常见误区 ​

误区事实
"窗口够大,资料全塞进去最稳妥"填充率过高反而降准确率;聚焦的检索结果稳定优于大段堆料
"模型变笨了就换个更强的模型"先查上下文:变长了还是变脏了。换模型常常只是把问题推迟
"压缩就是让模型总结一下"那是第 3 层。先做工具输出截断与规则驱逐,便宜且无损
"记忆越多 Agent 越聪明"过期记忆是污染的主要来源。记忆要能死
"工具挂全一点,需要时它自己会选"工具越多越容易选错。删工具的收益常被低估
"上下文是免费的"长上下文的边际成本更高,且有延迟代价
"一次性把系统提示写全就一劳永逸"超长系统提示里,靠后的约束会被忽略。原则是写原则,不要写剧本
"压缩完就不用管了"压缩有损,可能丢掉后面要用到的细节。关键产物要外置,不要只活在上下文里

十、动手练习 ​

练习 A:给现有系统做一次"上下文体检" ​

挑一个你在跑的(或 demo)Agent,记录每一轮的 token 构成,回答:

  1. 哪一类的增长最快?
  2. 有没有"完全没被模型用到"的 token?(业内经验是常有 30%~50% 是浪费的 [易变])
  3. 到第几轮开始明显变笨?

这个练习的价值在于先测量再优化。凭感觉调上下文,几乎一定是在做无用功。

练习 B:把"加法"改成"减法" ​

找一个效果不好的 Agent 任务,先不要加提示词、加示例、加资料。 而是删:删掉一半工具、删掉历史里最旧的几轮、把系统提示砍到原来的 1/3。再测一次。

多数人会发现效果变好。这个练习是为了建立"减法优先"的肌肉记忆。

练习 C:设计三层级联 ​

为一个"会调用 5 个工具、跑 20 轮以上"的 Agent 设计压缩策略: 每层的触发条件是什么?各保留什么?第 3 层的摘要用哪些结构化字段?


延伸方向 ​