外观
第 7 章 上下文工程
智能体接上工具之后,干得越久,上下文越乱:窗口是它唯一的工作记忆,每推进一步、每返回一条工具结果,都要往里面追加内容,追加得越多,模型对单条信息的把握反而越弱;把窗口当作无限大的缓冲区,什么都往里放,是智能体从 demo 走向生产时最先暴露的错误之一。第 6 章解决的是知识不在模型参数里的问题,这一章解决的是注意力不够用,思路是把上下文当作一份预算来管理,这笔费用从第 2 章 2.4 讲成本核算时就开始细算。7.1 回答窗口开大了为什么记忆力并没有随之变好,上下文腐烂既有实证研究也有机理解释;7.2 列出五类管理策略,把“每条信息应该放在哪里”变成一道可以逐项选择的题,再对照 Anthropic 和 LangChain 两家的官方说法理清术语;7.3 深入压实与摘要这一类工程量最大的手段,配一组真实的对比实验,说明什么是健康的 token 曲线、什么是抖动、怎么用重复读取率做诊断;7.4 写代码设计 ExecutionContext,用存储与呈现分离的思路把策略放进一个清晰的架构,同时建立挂点、回调、中间件和钩子这套框架词汇,第 8、13、15 章的代码都要用到它。
7.1 上下文塞满的代价
上下文从三千 token 涨到九万,模型的判断力会持续下滑,这个现象测得出、机理也讲得清。给公关团队做舆情简报的智能体就是一个典型例子:前十分钟一切正常,按品牌搜索微博、阅读贴吧热帖、统计负面占比、往表格里填数据;到第四十分钟,它开始反复读同一个帖子,把“只统计上周”这条要求整个忘了,品牌 A 的提及量又错记到了品牌 B 名下。模型没换,提示词没改,变化的只有上下文,从三千 token 涨到了九万。
九万 token 是什么概念?第 1 章讲过换算率,中文一个汉字大约 1.5 到 2 个 token,反推回来约合四万五千到六万字,差不多是一部中篇小说的篇幅。模型每推理一步,背景里都要带着这整部书的内容。
工程师遇到这种情况,第一反应往往是换更强的模型,或者在提示词里加一句“请仔细”,我第一次遇到时,也是这两个办法轮流试。这两个方向都不对,问题不在模型够不够强,而在信息环境。
模型每生成一个 token,都要回头参考前面的全部内容,注意力就是这种参考强度的分配规则。在 Transformer 的结构里,上下文中每个 token 都要与其他所有 token 两两计算关系,n 个 token 的关系数量随 n 的平方增长。输入变长后,每组关系分到的权重就被摊薄,答案真正依赖的那几组关系,参考强度也随之变弱,依赖它们才能答对的问题就开始出错。
Anthropic 的工程博客把这个现象概括为注意力预算(attention budget):预算有限,每进入一个新 token 都要消耗一部分,而且越往后,新 token 带来的收益越小。窗口里的 token 越多,模型从上下文中准确回忆信息的能力越差。向量数据库公司 Chroma 给它起了另一个名字,叫上下文腐烂(context rot),并把它作为 2025 年 7 月一份技术报告的标题。
这个结论有实验依据。Chroma 的报告把任务刻意做得极简,只保留输入长度这一个变量,测出 18 个主流模型的性能都随输入变长而下降,各自退化的幅度不同。
即使是在大海捞针(needle in a haystack)测试里接近满分的模型,也躲不开。报告把针与问题的相似度调低,再往草堆里加入干扰项,退化幅度又增大了一截。
更早的证据在 2023 年就有了。有一项研究测量过模型使用信息的规律:提示开头和结尾的部分用得最好,长文本中间的信息基本用不上,仅位置这一个因素就能造成性能差距,与模型是否支持长上下文无关。
顺着这个思路,多半会想到一个问题:窗口再大一些不就好了?厂商确实在这么做。截至 2026-08,DeepSeek v4 系列 API 把上下文标到 1M token,Kimi 旗舰 K3 同样标 1M,K2.6 则是 256K。
窗口尺寸早就不是短板了,真正的问题是腐烂。标称数只说明装得下装不下,腐烂说的是装进去之后模型还剩多少判断力,这是两回事,不能混为一谈。
退化不必等到窗口快满才出现。几万 token 的一条搜索结果读进来,一次就能让模型偏离方向。Anthropic 在那篇文章里指出:就算窗口继续加大,无关内容混进上下文、模型挑不出真正相关的信息,这两个问题短期内也解决不了,追求最强性能时尤其如此。
Google Gemini 团队用一个玩宝可梦的游戏智能体做过基准测试。上下文大约到 125K token 时,性能就出现退化,而模型标称窗口是 500K,连四分之一都没用到,判断力已经开始下降。
退化既能测出来,也能修复。这个团队没有改动模型,只改了三处上下文工程:检索只取过滤后的前 K 条,不再把相关信息全量放进去;增加一个修剪工具,把无关内容从上下文里移除;把上下文存成结构化版本,每次调用模型前现拼一段干净的字符串。三处改完,准确率从 34% 提到 90% 以上,并稳定下来。
腐烂之外,长上下文还带来其他开销。费用按 token 计,首 token 延迟也随输入长度增加,九万 token 的一次调用,大部分钱都花在早已用不上的信息上。输出也要占用窗口,额度不是由输入单方面决定的,要把输入和输出合起来计算。
四种失效各有各的表现。工具结果里混进幻觉文本,模型把编造的成分当作事实继续使用,这是污染。无关内容一多,模型干脆绕开上下文,按训练数据里的旧印象作答,这是分心。冗余信息互相争夺注意力,把回答引向错误的方向,这是混淆。上下文里前后矛盾,两条搜索结果对同一件事说法相反,模型不知道该信哪条,这是冲突。
这套分类由工程师 Drew Breunig 最先归纳,LangChain 的官方博客转述过。四个名字不同,原因是同一个:上下文一变长,质量风险就随之增加。
换个角度,按来源分类,还有一类损伤值得单独说明:为了帮助智能体而添加的信息,自己成了病因。把全量 API 文档整个放进提示,想图个齐全,模型反而从无关章节里编造出端点;把三周的 git 历史全部带上,真正的 bug 反而被埋得更深。这类帮倒忙的内容有个名字,叫医源性上下文(iatrogenic context),医学里把治疗本身引起的疾病叫医源性疾病,这里借用了同一个词根。它背后的剂量关系也有一个形状:信息太少,模型缺少材料;放得太满,注意力被摊薄;判断力的峰值在中等填充的那一段。所以上下文工程的目标不是把窗口清空,而是把用量控制在峰值附近。
噪声占满窗口,影响的不只是当次调用,还有长期学习。数据质量差的环境里,成千上万条同类的小故障反复涌入,真正能带来教训的大失败反而没有位置,复盘时分不清哪次失败值得深究;智能体整天忙着应付这些小故障,能力一直提升不了。第 8 章 8.3 的失败日志解决的就是前半个问题:值得深究的失败单独成库,才不会被日常噪声淹没。
再看增长速度,这就是上下文爆炸(context explosion)。回路每运行一轮都要往里添加新内容,这个速度可以用一组数字量出来:系统提示加任务起步约 2,600 token,第一轮工具调用往返增加 3,150,累计到 5,750;接下来五次带多条结果的工具调用,再增加约 15,000,总量到 20,750。搜索工具一次返回几万 token 是常事,读一个代码文件占用的更多。
套回开头的舆情智能体:读一页搜索结果就增加一万多,四十分钟涨到九万,完全在正常范围内。
非要涨到九万才需要警觉吗?不必,更早就有判断依据。社区把上下文利用率总结成一条经验线:四成左右还能守住,超过六成就该收缩,先把当前进展保存下来,再换一个新窗口继续,具体做法 7.3 讲。这是工程师总结的实践经验,不是厂商给出的技术规格,只当作趋势线使用。一旦越过这条线,回忆就开始走样,写代码的智能体会凭空生成不存在的库名。
比利用率更早出现的,是行为层面的信号。同一个工具调用连续三轮一字不改地重复;原先严格遵守的规则突然失守;任务目标丢了也没察觉,被派去修改鉴权中间件,它却跑去日志库里翻找;改出来的内容越来越粗糙,遗漏越来越多。最明确的一条是无条件附和,人说什么它都先回一句“你说得对”,社区把这当作智能体已经失效的证据。到了这一步,纠正、提示都不起作用,只剩换新窗口这一条路。
注意力在窗口里的分布有一个形状:两头高,中间低。
2023 年那项研究还给出另一个做法,说的是信息放在哪里。模型对提示的开头和结尾使用得最好,对中间使用得最差,所以关键内容不要埋在中间。需要常驻的规则放在头部,系统提示就是干这个的位置;当前这一步最重要的约束,放在紧贴任务的那条消息里。给信息分级不能只看优先级,还要看位置。一条最高优先级的约束,如果排在几万 token 的中段,标注的级别再高,也处于注意力最弱的位置。
回到开头那句“请仔细”,为什么没有用?注意力被摊薄是数值层面的稀释,与态度无关。指令写得再恳切,每组关系分到的权重也不会改变。问题既然出在这一层机理上,就只能用工程手段解决,措辞起不了作用。换更强的模型,只是把起点抬高,退化仍然随长度出现,Chroma 报告里的 18 个模型没有一个例外。真正有效的方向只有一个:少往窗口里放,放进去的内容尽快用掉,把位置让给后面的步骤。
上下文是一种稀缺资源。智能体能记住多少,不取决于窗口的标称大小,而取决于工程上如何分配这份预算。怎么分配,Anthropic 给过一句可以直接使用的话:找到能达成目标的最小高信号 token 集合。这句话反过来理解更实用:凡是删掉也不影响下一步决策的内容,都不应该留在上下文里。
7.2 上下文管理的五大策略
注意力是一份预算,接下来的问题是:每条信息该放在哪里?答案只有三个地方:上下文里、上下文外的存储和另一个独立的上下文。业界这些年积累的策略,都围绕这三个去处展开。
管理上下文有一套通用的策略清单,归纳起来是五类:Generation、Retrieval、Write、Reduce 和 Isolate。这一节用这套五分法作为框架,它覆盖全面、类别之间又不重叠,适合先把整体框架建立起来。另外还有两家官方的分法值得对照。Anthropic 的工程文章给出四项:压实、结构化笔记、子智能体架构、即时检索;LangChain 官方博客则归为四类:write、select、compress、isolate。三套说法是对同一件事的不同划分,放在一起对照,就不容易混淆。
| 五分法 | 一句话机制 | Anthropic 说法对应 | LangChain 说法对应 |
|---|---|---|---|
| Generation | 模型自己把内容生成出来写进上下文,比如计划与反思 | 无单独对应项 | 无单独对应项 |
| Retrieval | 从外部来源把信息取进上下文 | 即时检索(just-in-time) | select |
| Write | 把信息从上下文里存到外面 | 结构化笔记(structured note-taking) | write |
| Reduce | 把上下文压缩、删减 | 压实(compaction) | compress |
| Isolate | 把任务或工具挪进独立的环境 | 子智能体架构 | isolate |
先讲 Retrieval,因为第 6 章已经做过铺垫。区别在于视角:第 6 章把检索当作知识库能力来讲,这里把它当作预算手段来用。这一类的重点是即时检索:不预先把资料放进上下文,只在上下文里保留轻量的引用,比如文件路径、查询语句、链接,模型工作时按需去取。
Anthropic 用 Claude Code 举过这套做法的例子:靠 glob、grep 这类基本操作在文件系统里按需查找文件,不会一开始就把整个代码库倒进窗口。连文件名、目录结构和时间戳这些元数据,也在帮助模型判断什么值得读取。
工具清单也属于这项策略。第 4 章 4.1 算过:工具定义随每一轮请求上行,按 token 计费,工具挂得越多,单轮请求越贵。对策正是即时检索的思路:第 4 章 4.5 借助向量检索挑出当轮用得上的几份工具定义随请求发送,其余不进上下文,这就是五大策略在工具清单上的应用。
拿这一章的舆情智能体再对照一遍。品牌词表、统计口径和输出表格格式这类开工就要用的静态资料,值得一次性读入并常驻上下文;帖子正文则留给即时检索,窗口里只保留链接和查询词,等模型决定统计哪一批时再去取。预取是用预算换速度,即时检索是用速度换预算。真实系统多半两种都用:静态资料走预取,动态探索走即时检索。
即时检索的那一半写成代码很短,关键就在窗口里留什么。
python
import json
# 模拟舆情库:帖子正文留在库外,窗口里从头到尾看不到全文
ARCHIVE = {
"品牌A": ([{"text": "空气炸锅到手就坏,客服还踢皮球。", "negative": True}] * 1440
+ [{"text": "性价比不错,家人都喜欢。", "negative": False}] * 10560),
"品牌B": ([{"text": "物流太慢,等了五天才到。", "negative": True}] * 950
+ [{"text": "第二天就到了,包装完好。", "negative": False}] * 7690),
}
def fetch_summary(brand: str) -> str:
"""按品牌取回统计摘要。窗口里平时只留品牌名,正文一条都不进窗口。"""
posts = ARCHIVE[brand]
negative = sum(p["negative"] for p in posts)
return json.dumps(
{"brand": brand, "mentions": len(posts), "negative": negative,
"negative_ratio": round(negative / len(posts), 3)},
ensure_ascii=False)
# 常驻窗口的只有这行引用,几十个 token;两万条帖子的正文全在库外
REFERENCE_NOTE = "舆情数据用 fetch_summary(品牌名) 现查,不要凭印象报数字。"两万条帖子的全文折合几十万 token,一股脑塞进窗口,费用和 7.1 讲的注意力摊薄都躲不开;窗口里放一行引用,模型要数字时调用工具,进入上下文的只有返回的几行统计。
静态资料读进窗口之前,还有一道成本很低的工序:排版。同一份内容,用标题分节、条目分列的结构化 Markdown 写出来,比一整段平文本可靠得多,模型查找数据、引用、按多步指令执行都更稳定,在法律文书推理这类任务上实测过明显的准确率提升,出处见本章参考资料。这个收益有清楚的来源:分了节,背景和指令就分开了,长输入里混在一起的指令和语境,正是容易误读的地方。
动态探索这一头也有现成的路子可以参考。在陌生环境里查找信息,最常见的问题有两个:一是一上来就把大文件整个读进来,二是在目录里找不到入口,白白消耗一轮轮调用。一套好用的做法分三段:先广泛扫描,只做低成本操作,读 README、依赖清单这类用来摸底的文件,用 glob 扫文件名,用 grep 搜索任务关键词,目的是弄清这个环境里有什么;接着定点读取,从扫描得到的线索里挑出少数几个高价值文件,读取全文;最后顺着引用继续深入,代码里的 import、文档里指向的规范,把直接相关的外围内容补齐。越往后,单次操作越贵,次数就应越少,所以三段各设次数上限,扫描那一段放宽,后两段收紧,探索的预算才能控制住。前面说的 Claude Code 按需查找的做法,走的就是这条路,只是没有把阶段和上限明确说出来。
这套做法有一个前提:环境要有结构。文件名随机、没有清单、命名没有惯例的项目,扫描那一段得到的多半是噪声,对策是少用 glob、多用 grep,搜内容而不是搜名字。放到舆情智能体上,第一次接手某个品牌的历史目录,同样按三段来:先看目录和说明文件,再定点读几份近期简报了解格式,最后顺着简报里引用的口径文档补充细节。
Write 处理的是这样一类信息:上下文装不下,但后面还会用到。做法是让智能体把关键状态写到上下文外面,比如草稿文件、待办清单和笔记。
舆情智能体又是一个好例子。三个品牌各统计完一轮,与其让三段结果都堆在上下文里,不如写进中间文件,窗口里只保留文件路径,到汇总那一步再读回来。同一份信息放在文件里,不占注意力预算;取回它,也只是一次工具调用的开销。
落实成代码,其实就两个函数,一个写出去,一个取回来:
python
import json
import os
def save_note(name: str, data: dict) -> str:
"""把一段结果写到上下文外面,返回留给窗口的全部信息:一个路径。"""
os.makedirs("./notes", exist_ok=True)
path = f"./notes/{name}.json"
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
return path
def load_note(path: str) -> dict:
"""汇总阶段取回,一次读文件换回全部数据。"""
with open(path, encoding="utf-8") as f:
return json.load(f)
# 每个品牌统计完一轮,存一份;窗口里只留返回的路径
note_a = save_note("品牌A", {"mentions": 12000, "negative": 1440})
note_b = save_note("品牌B", {"mentions": 8640, "negative": 950})
note_c = save_note("品牌C", {"mentions": 4210, "negative": 630})
# 最后汇总时才取回
final = {"品牌A": load_note(note_a), "品牌B": load_note(note_b), "品牌C": load_note(note_c)}
print(json.dumps(final, ensure_ascii=False, indent=2))三段结果从头到尾没有同时出现在窗口里。窗口里出现的只有三行路径,几个 token 的量;直到汇总那次调用,它们才重新汇集。取回的动作就是一次文件读取,路径由命名规则固定,内容是结构化的 JSON。
Claude Code 的待办列表就是 Write 的实例。Anthropic 开放平台 2025 年又推出了基于文件的官方记忆工具。这一类的关键不在“写”这个动作,而在“取”。写出去的内容必须能低成本取回:路径要稳定、命名要可预期、内容要结构化,做不到这几点,写出去就等于丢了。
Generation 很少有人留意,因为它看起来什么都没有“存”。开工时先让模型生成一份计划写进上下文,几十步之后它仍然在按这份计划工作;每完成一个阶段再生成一段反思,把后面的路线修正一下。它产生的是新信息,不是搬运已有信息,与 Write 的区别就在这里。生成信息的成本很低,一份计划几百 token,就能让几十步不偏离方向。
这个做法也有副作用。计划生成得太早,环境随后一变,过时的计划会把模型限制在错误的路径上。长任务里的计划因此要随着进度不断更新,而更新这个动作本身仍然是 Generation。第 3 章讲的思维链与反思、Anthropic 的 think 工具,本质上都属于这一类。
Generation 写成代码,就是开工多一次调用,让模型把计划写出来常驻窗口头部。
python
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url="<DeepSeek 兼容端点,见官方文档>",
)
def make_plan(task: str) -> str:
"""开工先让模型把计划写出来,返回的文本将常驻窗口头部。"""
resp = client.chat.completions.create(
model="deepseek-v4-flash",
messages=[
{"role": "user",
"content": f"为舆情简报任务拟一份执行计划:{task}。"
"分三到五个阶段,每个阶段写清目标、要调用的工具、完成的判据,"
"总共不超过 200 字,只输出计划本身。"},
],
)
return resp.choices[0].message.content
PLAN_NOTE = make_plan("统计品牌A、品牌B上周的负面提及量,产出对比表格")
# 生成的计划作为一条消息放进列表开头,后面每一轮的工具调用都以它为前提
messages = [
{"role": "system", "content": "你是舆情简报智能体,按计划逐阶段推进。"},
{"role": "user", "content": PLAN_NOTE},
]一次调用、几百 token 的计划,换后面几十步都有一份明文的路线;它常驻窗口头部,不埋进历史中段,7.1 讲的位置效应在这里正好用上。
Reduce 负责把上下文本身变小,压实和摘要都属于它,工程细节也最多,7.3 专门用一节来讲。
Isolate 要解决的是职责混杂的问题:与其把所有工作都放进同一份上下文,不如让每份上下文只负责一件事。代码放进沙箱执行,长文本分析交给专门的分析智能体,主智能体只接收它返回的摘要。数字最能说明问题,一组三版本对比实验里,不做管理的基线用了 21,633 token,压实版 7,276,隔离版 5,892。隔离省得最多,因为它根本没有让膨胀发生,从架构上就切断了膨胀的路径。
Anthropic 的多智能体研究系统也采用这条路:子智能体在内部消耗几万 token,返回给主智能体的只是一份简短摘要。隔离要付出的代价是协调,值不值得升级成多份上下文,第四部分展开说明。
五类各有分工。Retrieval 负责进,把外部的信息取回来;Write 负责出,把窗口里的信息移出去保存;Reduce 负责瘦身,让窗口内部不再臃肿;Isolate 负责转移,把整个任务移进另一份窗口;Generation 最特别,它既不进也不出,而是生成需要长期留在窗口里的计划和反思。五个动作,对应三个去处。做选型时先问这条信息打算去哪里,答案自然指向某一类。
把舆情智能体完整走一遍,五类都用上了。开工先让模型生成一份统计计划,写明三个品牌、口径和表格格式,这是 Generation;品牌词表和统计口径是静态资料,开工时读一次就常驻窗口,这是 Retrieval 的预取一端;每个品牌统计完,结果写进中间表格文件,窗口里只保留文件名,这是 Write;运行四十分钟后对话本身变长,超过阈值先压实搜索结果,再摘要旧过程,这是 Reduce。
真要通读二十万字的长文,就派一个子智能体去读,主窗口只接收它返回的摘要,这是 Isolate。
五类策略之间不是单选题。一个像样的生产系统,常常同时启用四五类:检索负责进入,Write 和 Reduce 负责输出和节省,Isolate 作为兜底。启用的顺序也有讲究。Generation 和 Write 成本最低,先用;Reduce 随后;Isolate 的协调成本最高,最后才使用。第 8 章的记忆系统,就是把 Write 和 Retrieval 整理成一套带生命周期管理的完整子系统。
7.3 压实与摘要
压实和摘要是 Reduce 这一类里用得最多的两种手段,本身都不复杂,但要用好并不容易,效果好坏也不能凭感觉判断,要看信号。
压实(compaction)的做法是:信息一旦处理完,就用一条占位引用把它替换掉。文件读完、分析做完,原文对后面的步骤几乎没有价值,这时保留一句“文件内容已压实,需要时可重新读取”就够了。
压实处理的对象有两类。一类是写操作的调用参数,比如 create_file 传入的一万字报告,既然已经存盘,上下文里就不必再保留全文,压成“内容已存入文件”即可。另一类是读操作的结果,read_file 返回的整个文件、search_web 返回的一整页搜索结果都算,这才是撑大上下文的主要来源,同样压成一句带参数的引用,等模型真正需要时再调用一次工具取回。
压实的好处,全部在“确定”这两个字上。哪个工具的结果要压、压成什么样,规则事先写好,不消耗成本、不增加延迟,还随时可逆,想拿回原始数据就再调用一次工具。Anthropic 把“工具结果清理”直接做成了平台能力。Claude Code 的压实同样在清理上下文,只是做法属于摘要那一类:把消息历史交给模型总结,保留架构决策和尚未解决的 bug,再带上最近访问过的五个文件继续工作。
压实替换的是整段结果,在这之前还有一档更细的做法:按语义选择保留哪些内容。网页搜索返回的一批原文,动不动就是几万 token,模型真正用得上的常常只有其中几段。具体做法是先把结果切块,逐块计算嵌入,再用当前任务做向量检索,只把最相关的几段放进上下文。
这条流水线有人实际运行过。一批搜索结果原文 136448 个字符、折合 37312 个 token,切块嵌入后按相似度取前三段,真正进入上下文的只剩 304 个 token,节省了 99.2%,保留下来的还是语义最贴合的部分,不是从头尾硬性截取。现成的服务已经把这做成了内建功能,Tavily 的搜索接口带有 chunks_per_source 参数,可以按来源取片段,另有 search_depth 档位在延迟和相关度之间做权衡,一次调用直接返回过滤后的片段。
语义筛选和占位引用各管一头。占位引用替换的是已经处理完的结果,语义筛选裁剪的是还没处理完的大结果。没有被选中的段落不进入上下文,模型需要全文时重新调用工具取回,做法与占位引用一样。
另一种手段是摘要(summarization):把大段旧过程整个交给模型,换回一份语义上的浓缩。压实够不着的情况归它处理,比如对话和推理本身太长,又没有工具结果可以作为压缩对象,只能靠语义压缩。
代价同样要看清。压实可逆,摘要不可逆,原始推理一旦被压缩就再也找不回来;压实不花钱,摘要要多付一次模型调用;压实结果确定,摘要质量则完全取决于模型当时的表现。
还有一种更激进的压缩:词级压缩这类自动压缩组件,不靠模型写摘要,而是按算法直接从原文里删词,把上下文强行压短。这类组件节省的空间有限,误删关键信息的风险却是实实在在的,长文档场景先算这笔开销,再决定用不用。
所以排顺序时总是压实在前,摘要只作为压不动时的备选。摘要也不能频繁触发,每触发一次,就是用完整的上下文跑一趟模型。任务中途多次做摘要,节省的 token 最后会以摘要费用的形式付出去。
摘要费用还有一个节省办法:摘要这项工作,用不着旗舰模型。把这个思路推广开,就是流水线内部按步骤配置模型档位:批量摘要、抽取这类机械性工作固定交给低价的小模型,中间的归并与合成用中档,只有最终成稿和拿不准的判断才交给旗舰。步骤的难度在设计流水线时就已确定,所以档位可以写死,这一点与第 14 章 14.3 的模型路由正好互补:路由按这条请求难不难现场选择档位,配档按这一步是什么工作事先固定。第 8 章 8.2 的记忆抽取、8.3 的入库前压缩,都把这类辅助调用交给了低价档模型。
摘要的提示模板如下,可以直接使用。
text
你是智能体执行过程的记录员。把下面的执行历史压缩成一份进度摘要,
供同一个智能体在后续步骤里接着干活用。摘要分四部分,关键发现、
已用工具、已试过且失败的路、当前进度与剩余工作,逐条列出,每条
不超过一行,只保留对继续工作有用的信息,用中文输出。
### 示例
[历史]
[user]: 统计三个品牌上周的负面提及量
[工具调用] search_web({"query": "品牌A 负面 提及 上周"})
[工具结果] 微博约 12000 条,负面占 12%,其余平台数据略
[助手] 品牌A 已统计完成,继续查品牌B
示例输出
关键发现:品牌A 上周提及约 12000 条、负面占 12%
已用工具:search_web
已试过且失败的路:暂无
当前进度:品牌A 已完成,剩余品牌B、品牌C 与汇总表
### 输入数据
[历史]
{history}模板存成变量接上调用,就是完整的一段,摘要单独配一个客户端,模型走低价档。
python
import os
from openai import OpenAI
summarizer = OpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url="<DeepSeek 兼容端点,见官方文档>",
) # 摘要走低价档,与主对话的模型分开配置
SUMMARIZE_PROMPT = "<上面那份模板原样存进这个变量>"
def render_history(messages: list) -> str:
"""按模板示例的行文渲染执行历史,每个角色一行。"""
return "\n".join(f"[{m['role']}]: {m['content']}" for m in messages)
def summarize_history(messages: list) -> str:
"""把执行历史压成进度摘要,返回的文本替换旧历史,进下一次请求。"""
resp = summarizer.chat.completions.create(
model="deepseek-v4-flash",
messages=[
{"role": "system", "content": SUMMARIZE_PROMPT},
{"role": "user", "content": render_history(messages)},
],
)
return resp.choices[0].message.content压实和摘要都要遵守同一条规则:失败留下的痕迹不能压缩。调用成功,压成占位、摘成一行都可以;一旦失败,整条调用链要原样保留。试过什么、败在哪里、为什么走不通,这些是智能体下一步避免重复出错的依据。生产环境里出过真实的事故:一条近五百 token 的报错堆栈,被压实系统缩成一句“认证出错”,智能体不知道这条路已经走过而且走不通,照原路又白白运行了几个小时。
给每条工具结果打一个出错标志,压实规则遇到它就跳过;做摘要时单独设一栏“已试过且失败的路”,随摘要一起保留。错误与普通上下文不是一回事,把它压掉,最容易导致原路重踩。社区实践早已把这条豁免定为硬性规则,出处见本章参考资料。
压实和摘要谁先谁后,靠分层管理来决定。第一步永远是量 token。消息条数不能作为依据,一次文件读取算一条消息,却可能占一万 token,二十句短对话加起来也许只有五百。量 token 用分词器直接计算,OpenAI 开源的 tiktoken 是常见选择,用于中文至少能给出稳定的相对值。量完后按三档处理:低于阈值不动,超过阈值先压实,仍然超出再做摘要。每做完一级再量一次,达标就停。
这里还有一层费用上的冲突,涉及前缀缓存。各家 API 几乎都能缓存多次请求开头相同的部分,命中部分按远低于未命中的费率收费。便宜靠的是复用:前缀在预填充阶段算过的结果留在服务端,同样一段再来,这部分就不必重算。OpenAI 的提示缓存按模型分了档:GPT-5.6 及以后满 1,024 token 自动开启,更早的模型要累积到 2,048。命中条件也很严格,复用要一字不差,整段渲染后的前缀必须逐字节一致,前面任何内容稍有改动,缓存就作废。命中与未命中的价差不小,按 DeepSeek 官方定价,V4-Flash 的缓存命中价不到未命中的三十分之一。
压实改动的偏偏是中间的历史消息,这一改,后面整段缓存就白费了。所以阈值不是摆设,而是两笔费用之间的平衡点:上下文膨胀造成的损失还没有超过缓存作废造成的损失时,历史一个字都不应该动。长任务里这个权衡常常推翻只看 token 数得出的判断,有些地方该压却忍着不压,反而才是对的。
先看 token 曲线的三种形状:健康锯齿、单调上涨、贴线平走。
管理得好不好,看 token 曲线的形状就能知道。健康的形态是锯齿(sawtooth):上下文增长到预算线,压实把它降下来,再增长再降下来。只涨不降,说明根本没有在管理;紧贴着预算线走成平线,说明预算设得太紧,每一步都在被迫削减。
更细一些,还有一个更硬的指标:重复读取率,即同一个文件或同一个资源被反复读取的次数占全部读取次数的比例,它可以直接暴露是不是压缩过度。前面两个信号推到极端,就是抖动(thrashing)。预算一紧,压实刚清掉的内容转眼又要用,智能体把成本花在重新获取已有信息上,比不压实还费钱。
实验让智能体对一个含 44 个 Python 文件的仓库做代码评审,五种配置各运行一遍。表格里的头尾压实(HeadTail)保留两头、压缩中间:开头的系统提示和任务指令要保留,最近的工作要保留,中间那些已经处理完的旧消息直接裁掉。还有一条硬性规则:工具调用与它的结果绑成一个原子组,要么一起保留,要么一起压缩。一旦拆开,模型会发现自己调用过工具却看不到结果,行为当场就会混乱。
| 配置 | 输入 token | 重复读取率 | 文件覆盖 | 质量分 |
|---|---|---|---|---|
| 不压实 | 915,591 | 27% | 16/44 | 6.0/10 |
| 头尾压实,预算 8K | 276,390 | 55% | 14/44 | 4.0/10 |
| 头尾压实 8K,无完成检查钩子 | 144,136 | 15% | 17/44 | 5.5/10 |
| 头尾压实,预算 15K | 402,102 | 46% | 14/44 | 5.1/10 |
| 隔离(子智能体分摊) | 118,824 | 0% | 48(子智能体合计) | 4.5/10 |
(质量分这一栏,是实验里 LLM 裁判给完整性和可操作性打的平均分。文件覆盖那列要分开看:其余各行是“44 个文件里读过几个”,隔离行的 48 则是几路子智能体读文件的合计,不同路可能读同一个文件,所以会超过 44。这只是一次实验,看趋势可以,不要当作普适常数。)
表里最显眼的是第二行。预算压到 8K,token 总量从 91 万降到 27 万,但重复读取率猛增到 55%,质量分不升反降,从 6.0 降到 4.0。抖动的典型表现就是这样:省下来的 token 又原样花在重复读取上。
第三行反映更深一层的问题。预算同为 8K,把完成检查钩子拿掉后,重复读取率降到 15%。钩子存在的意义是推动没干完的智能体继续工作,但压实刚把它的工作记忆清掉,它只能回头重读。配置之间会互相牵制,这正是上下文工程与普通调参不同的地方。
经验法则给出了一组起步值:压实预算按单步工作集的 2 到 3 倍设置,多文件任务从 25K 到 50K 起步,重复读取率一旦超过 15%,先放宽预算,其他的先不谈。
压实与摘要之外还有第三种手段:重开窗口。长对话走到后半程,压实压不动、摘要也摘不干净的时候,最省事的办法就是换窗口。先让智能体写一份交接文件,记录目标、已完成的进展、没有处理完的部分、卡住的位置,再结束当前会话开一个新窗口,让它读着交接文件继续。Anthropic 的工程文章说得很直接:压实只是长时程任务的第一个手段,压得太狠会把当时不起眼、后来才重要的信息丢掉。因此对于更长的任务,各家编码工具最后都加入了重开这个办法。
要不要重开,判断依据 7.1 已经给出:利用率超过六成、行为信号开始出现时,就不要在旧窗口里继续耗着了,花几分钟写交接文件、换新窗口更划算。交接文件本身就是写入策略用到极致的产物,状态全部移出窗口保存到磁盘,新窗口里只放一份浓缩过的交接内容。这套动作做久了会形成节奏:工作一段、写交接、换窗口,长任务的稳定就靠这个节奏。第 21 章 21.5 讲压缩时该保留什么,与它是同一个问题。
这一节讲的压实与摘要,压缩的对象都是不断变长的执行历史;还有一类超出窗口的情况避不开:材料本身就超长。一份二十万字的长文、三十份行业报告,哪个窗口都装不下,而任务又要求读完全部,这时要先选择路线。装得下就整段填入,配合前面的语义筛选取相关段;装不下有两条路。一条是分块逐份摘要、再把各份摘要归并成终稿,各块独立、便于并行,扛得住大批量,代价是接受一定的内容损失。另一条是滚动精炼,带着当前摘要读下一块,读完最后一块,终稿就得到了,每份材料的要点都能带进终稿,代价是只能串行执行,早期读到的信息还会被反复改写。7.2 说通读二十万字长文可以派子智能体,但子智能体也不应该把二十万字整个读进自己的窗口,它读材料时同样要在这两条路线里选择。
回头对照 7.1 的剂量曲线:Reduce 要做的不是把上下文压到最小,而是压到刚好,刚好让模型保持判断力,又不至于把 token 浪费在重新查找信息上。这套方法要用起来,还有一个前提没有交代:修改呈现内容时不能破坏存储。
7.4 存储与呈现分离
前三节做的所有事,底层都依赖同一个前提:先有一份不动的完整记录,要修改的只是发给模型的那一份,压实、摘要、裁剪无一例外。这个前提有个名字,叫存储与呈现分离(separating storage from presentation)。存储层负责记录发生过的全部事实,呈现层决定模型这一步能看到什么。两层职责分开,策略才有调整的余地。
先确定存储层,它叫 ExecutionContext,只包含两个东西。events 是一条执行事实的流水,用户消息、助手消息、工具调用和工具结果都按顺序排列,只允许追加,不允许修改和删除;state 保存运行时状态,比如压实进度、待审批的工具调用。这是本次执行的完整事实记录,智能体凭什么得出某个结论,答案只能到这里找。呈现层叫 LlmRequest,每次调用模型前从 events 投影生成,压实和摘要都作用在这份投影上,用完就丢弃。
存储与呈现分开,可以直接得到三点好处。第一是复盘。想知道“为什么从那批搜索结果得出错误结论”,需要翻看当时的原文,压实后的占位符看不出任何信息。第二是复用。后面的工具常常要引用前面步骤的产出,原文在存储层始终没有改动,引用时直接使用即可。第三是替换。今天用滑动窗口、明天换成压实,要改的只是投影函数,原始记录一个字都不用动。
下面给出一份最小实现。它是示意代码,省略了真实框架里的异步和序列化,但两层的分界、压实挂载的位置都是真实的。
python
"""示意代码:存储与呈现分离的最小骨架,python3 可直接运行。"""
from dataclasses import dataclass, field, replace
@dataclass
class Message:
role: str # 取 "user" 或 "assistant"
content: str
@dataclass
class ToolCall:
tool_call_id: str
name: str
arguments: dict
@dataclass
class ToolResult:
tool_call_id: str
name: str
content: str
ContentItem = Message | ToolCall | ToolResult
@dataclass
class ExecutionContext:
"""存储层:只追加、不修改的执行事实流水。"""
events: list[ContentItem] = field(default_factory=list)
state: dict = field(default_factory=dict) # 运行时状态,如压实进度
def add(self, item: ContentItem) -> None:
self.events.append(item)
@dataclass
class LlmRequest:
"""呈现层:发给模型的投影,每次调用前重新计算。"""
system_prompt: str
contents: list[ContentItem]
def build_llm_request(context: ExecutionContext, system_prompt: str) -> LlmRequest:
# 投影就是复制:后续优化只作用在这份副本上
return LlmRequest(system_prompt=system_prompt, contents=[replace(i) for i in context.events])
COMPACTION_RULES = {
# 工具名 -> 压实后的占位文本
"read_file": "文件内容已压实:{file_path}。需要时可重新读取。",
"search_web": "搜索结果已压实,查询词:{query}。需要时可重新搜索。",
}
def apply_compaction(request: LlmRequest) -> int:
"""压实呈现层里的工具结果,返回省下的字符量。"""
call_args: dict[str, dict] = {} # 工具调用先记下参数,结果好引用
saved = 0
for item in request.contents:
if isinstance(item, ToolCall):
call_args[item.tool_call_id] = item.arguments
elif isinstance(item, ToolResult) and item.name in COMPACTION_RULES:
args = call_args.get(item.tool_call_id, {})
saved += len(item.content)
item.content = COMPACTION_RULES[item.name].format(
file_path=args.get("file_path", "未知"),
query=args.get("query", "未知"),
)
saved -= len(item.content)
return saved
def rough_tokens(request: LlmRequest) -> int:
# 示意估算:中文按 1 字符约 1.5 token,生产实现应换 tiktoken
# ToolCall 没有 content 字段,用 getattr 兜住,只统计文本部分
return int(sum(len(getattr(i, "content", "")) for i in request.contents) * 1.5)
def before_llm(context: ExecutionContext, system_prompt: str,
threshold: int = 2000) -> LlmRequest:
"""模型调用前的钩子:低于阈值不动历史,超了才压实。"""
request = build_llm_request(context, system_prompt)
if rough_tokens(request) < threshold:
return request # 不动,前缀缓存保持命中
apply_compaction(request)
return request
if __name__ == "__main__":
context = ExecutionContext()
context.add(Message("user", "汇总三个品牌上周的负面舆情"))
context.add(ToolCall("c1", "search_web", {"query": "品牌A 负面 上周"}))
context.add(ToolResult("c1", "search_web", "微博约 12000 条、贴吧约 3000 条。" * 300))
context.add(Message("assistant", "品牌A 已统计,继续查品牌B。"))
req = before_llm(context, "你是舆情简报智能体。")
print(f"呈现层约 {rough_tokens(req)} token")
print(f"存储层完整事件数不变,共 {len(context.events)} 条")实际运行一遍,差别立刻显现:呈现层的估算 token 从一万多降到一百以内,存储层那四条事件一条没少。
还没压实时,模型这一步收到的是完整的四条:user 的一句任务、search_web 的调用参数、三百行重复正文作为结果、assistant 的一句进度。压实之后,第三条换成一行文字:“搜索结果已压实,查询词:品牌A 负面 上周。需要时可重新搜索”。任务和进度不变,模型下一步的判断不受干扰,这一步的输入却从一万 token 级别缩小到一百以内。存储层没有任何改动,想复盘原始的三百行,随时可以取到。
压实这个动作,从头到尾没有触碰历史,改动的只是本次请求的副本。before_llm 函数承载的正是 7.3 那套分层管理:先投影、再计量,低于阈值原样放行,超过才压实。在真实框架里,它一般注册成模型调用前的回调。7.3 的摘要一级也挂在这条链上,压实完重新计量一遍,仍然超标的才轮到摘要。
另一个关键步骤是 apply_compaction。它沿着流水线走一遍,遇到工具调用就把参数记入 call_args,遇到工具结果,先查工具名是否在 COMPACTION_RULES 里登记,登记过就把结果正文替换成占位引用。工具调用总是排在结果前面,参数先到手,这一遍遍历就够用。占位里带的 file_path、query 不是摆设,它们是留给模型的取回线索,看到查询词,模型就知道该重新搜索什么。规则表以工具名作为键。压实之所以能零成本、零延迟,正因为它是写死的确定规则;凡是需要模型判断的事,都不应该进入这张表。
有不少团队图省事,直接在 messages 列表上原地压缩,压缩完再把版本存回会话存储。第二天用户问“你昨天引用的那段原文”,智能体翻遍历史只剩一句占位符,回答不出来;评测要复现某次失败,摘要早已覆盖了原始轨迹,想查都无处可查。这些问题可以总结成一句话:凡是打算修改历史的操作,先问一句原始记录在哪里。如果根本没有原始记录,就先把原始记录建立起来,再谈压缩。
代码之外,还有两个设计上的取舍值得说明。一个是,投影每次都从 events 重新计算,不维护一份可变的副本,这样同样的 events 永远得到同样的请求前缀;阈值没到就一字不动,前缀缓存照常命中。缓存和压实的矛盾并没有消失,只是交给 7.3 的阈值去权衡。另一个是,state 放在存储层而不是请求里,压实进度、检查点这类跨步骤状态,要在一次次投影之间保留下来。顺着这两条延伸,把 ExecutionContext 的生命周期从单次执行扩展到多次执行,外面再包上会话和持久化,第 8 章记忆系统的骨架就出来了。
投影的最后一站还没有交代:这份消息列表真正发给模型之前,还有一道拼装工序,各个模型服务提供商各不相同。同样一串角色加正文的消息,落到具体模型上,要按它训练时见过的样子渲染成输入,说话人的起止标记、系统提示的位置,各家的拼法不同,这就是聊天模板(chat template)。这道工序平时藏在 API 后面看不见,问题在两处显现:一处是自己动手拼原始文本,把历史消息串成一大段直接交给模型,格式偏离训练时的样子,效果悄悄下降,还不报错;另一处是跨厂商迁移,同一串消息经两家的模板渲染出来的输入并不一样,差异不会报错,只体现在回答质量稍差。守住的办法只有一条:消息始终以结构化的形式存入 events、放入 contents,渲染交给目标厂商的模板或客户端库,不要自己拼接裸文本。
智能体的每一轮执行,沿途预留了五个挂点:LLM 调用前、LLM 调用后、工具执行前、工具执行后和整次运行结束。挂在上面的回调返回 None 还是别的值,决定这次是放行还是短路。
把 before_llm 放到更大的框架里看,它并不是孤立的技巧,只是一族挂点(框架预留的插入位置)里最常用的一个。这族挂点总共五处:LLM 调用前,做注入、压缩、缓存判断;LLM 调用后,校验输出;工具执行前,审批、改参数;工具执行后,压缩结果、脱敏改写;整次运行结束,把存档写入记忆。把要做的事写成函数,注册到对应位置,每经过一次就执行一次,这类函数叫回调(callback)。
所有回调共同遵守一条规则:返回 None 就放行,返回别的值就短路。能短路的位置不同,后续动作也不同。工具执行前的回调如果有返回值,真实执行直接跳过,返回值当作结果使用;工具执行后的回调如果有返回值,它会替换掉原始结果;整条回调链按列表顺序执行,停在第一个非 None 的位置。
回调的能力还有一条边界,体现在时序上。真实框架里,模型调用前的回调拿到的常常是已经构建好的请求,这时再往记录里补充事件已经晚了,补的内容这次请求看不到,最早要到下一轮才生效。所以“先改记录、再投影”这类工作,比如把审批结果先记成事件再让模型看到,必须放在投影之前的环节,做进框架内部,不能指望回调来兜底。另一方面,ExecutionContext 不只提供给投影函数,也会随调用传给工具:框架执行工具时把上下文作为参数传过去,需要读取执行状态的工具,比如动手前查权限的那种,自己接收使用,用不到的直接忽略。这样的传递还顺便统一了方法签名,工具之间要共享执行状态,不必各自另建通道。
这套语义在本书里先后出现三处。这一节的 before_llm,在 LLM 调用前做压缩;第 8 章 8.1 的 manager.save,会在执行结束时把会话保存到磁盘,属于运行结束挂点上的存档;第 15 章 15.2 的 GATED 审批,则是靠工具执行前返回一个挂起对象,把真实执行短路掉。
挂点之外,循环的边界上还有另一类插入位置,挂在这里的函数叫钩子。钩子和中间件分工不同:中间件是五个挂点上的那条回调链,把每一次 LLM 调用和工具执行都包一层;钩子则在循环的起点和出口触发。起始钩子在首次 LLM 调用前注入一条规划指令,它与 LLM 调用前挂点的区别全在“首次”两个字上,挂点每一轮都会经过,起始钩子只在第一轮之前运行一次。结束钩子在循环想要退出时,用一次独立的模型调用判断任务是否真的完成了,7.3 实验里那个“完成检查钩子”指的就是它;没有完成,就把继续执行的指令作为用户消息注回对话,再配一个重启次数上限作为保险,避免循环无限运行。
这四个词合起来:挂点是五个插入位置,回调是挂在上面的函数,中间件是挂点上的回调链,钩子管循环的起点与出口。
钩子也遵守一份两句话的约定:返回字符串,就把它当作用户消息注入;返回 None,则什么都不做。第 13 章讲的三层防御、各家框架的护栏机制,都在这五个挂点和循环边界的钩子上,中间件那一半留到 13.3 讲。
本章小结
- 7.1 腐烂的代价:18 个模型的长输入实验全部退化,标称 500K 窗口的模型大约 125K 就开始掉判断力;利用率四成以内还算安全、到六成就该收缩;注意力两头高、中间低,关键内容不埋中段。
- 7.2 五大策略:Retrieval 进、Write 出、Reduce 瘦身、Isolate 转移、Generation 生成计划与反思,生成的计划与反思同样算上下文的一等成员;成本最低的先用,Isolate 协调成本最高、最后用。
- 7.3 压实与摘要:压实可逆、免费,排在前,摘要不可逆、要付费,作为备选;失败的调用链不压缩;预算按单步工作集的 2 到 3 倍设置,重复读取率超过 15% 先放宽预算;健康的 token 曲线是锯齿,不是只升不降,也不是贴线平走。
- 7.4 存储与呈现分离:events 只追加、LlmRequest 随算随丢,压实和摘要怎么改都伤不到原始记录;五个挂点是 LLM 调用前后、工具执行前后、运行结束,回调返回 None 放行、非 None 短路;挂点是位置、回调是函数、中间件是回调链、钩子管循环的起点与出口,第 8、13、15 章的代码都建立在这套词汇上。
做到这一步,上下文这笔最不起眼的隐性开销,就变成了一项工程指标,能量出来,诊断的时候有依据,也改得动。智能体有没有做长任务的能力,先看这一项是否过关。
设计题:找一个会越聊越长的多轮任务,从五大策略里至少选两种放进自己的回路,比如历史压实加外部检索,再把每条信息放在哪里写清楚。度量方法:同一批任务,改造前后各运行一遍,比较输入 token 均值和任务通过率。预期交付:一份投影函数,加一组前后对照的数字。只有 token 降下来而通过率没有下降,这一章才算真正用上了。
参考资料
- Chroma《Context Rot: How Increasing Input Tokens Impacts LLM Performance》技术报告(2025-07-14,18 个模型的长输入退化实验),https://www.trychroma.com/research/context-rot ,核实于 2026-08。
- Anthropic《Effective context engineering for AI agents》(2025-09,注意力预算、压实、结构化笔记、子智能体、即时检索;压实是长时程连贯的第一根杠杆、过度压实会丢当时不起眼后来才显出关键的信息),https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents ,核实于 2026-08,2026-09-01 复核。
- LangChain《Context Engineering for Agents》(write、select、compress、isolate 四桶归纳,转引 Karpathy 与 Drew Breunig 的说法),https://www.langchain.com/blog/context-engineering-for-agents ,核实于 2026-08。
- Nelson F. Liu 等《Lost in the Middle: How Language Models Use Long Contexts》(TACL 2023),https://arxiv.org/abs/2307.03172 ,核实于 2026-08。
- OpenAI 官方文档《Prompt caching》(GPT-5.6 及之后 1,024 token 起自动缓存、更早模型 2,048 起,前缀逐字节一致才命中),https://developers.openai.com/api/docs/guides/prompt-caching ,核实于 2026-08。
- DeepSeek 官方文档《定价》(v4 系列 1M 上下文、缓存命中与未命中价差),https://api-docs.deepseek.com/quick_start/pricing ,核实于 2026-08。
- Kimi 开放平台文档(K3 标称 1M token 上下文、K2.6 为 256K;platform.moonshot.cn 已整体迁至 kimi.com)https://platform.kimi.com/docs/pricing/chat-k3 ,核实于 2026-08。
- OpenAI tiktoken 仓库(开源分词器),https://github.com/openai/tiktoken ,核实于 2026-08。
- Younghee Song, Jungjun Hur《Build an AI Agent (From Scratch)》(Manning MEAP V3,2026)第 1 章(五大策略)、第 4 章(ExecutionContext 与存储呈现分离的首次出现)、第 5 章(工具结果语义筛选:切块嵌入取相关段,37312 到 304 token 演示与回调挂点五位置)、第 6 章(压实、摘要、分层管理、存储与呈现分离)。见 txt-ref-zh/build-ai-agent-from-scratch/。
- Victor Dibia《Designing Multi-Agent Systems》(2025)第 4 章(上下文腐烂与管理策略对比、中间件与钩子的分工)、第 15 章(压实预算与重复读取率实验)。见 txt-ref-zh/designing-multi-agent-systems/。
- 《Patterns for Building AI Agents》(Sam Bhagwat、Michelle Gienow,O'Reilly 2025)第 7 章(上下文失效模式归纳;宝可梦智能体基准测试:约 125K token 起性能退化、窗口 500K,三处上下文工程改动后准确率指标 34% 提升到稳定超过 90%,转述 Google Gemini 团队的测试)。译文见 txt-ref-zh/patterns-for-building-ai-agents/。
- Anthropic 工程博客《The "think" tool: Enabling Claude to stop and think》(think 工具,给模型的一个专用推理暂存工具,在动作之间停下来想),https://www.anthropic.com/engineering/claude-think-tool ,核实于 2026-08-29。
- Anthropic 官方文档《Memory tool》(2025 年 9 月推出的基于文件的官方记忆工具:/memories 目录、六种文件操作、客户端执行存储、自动注入的记忆协议),https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool ,核实于 2026-09-02(机制口径另见第 8 章参考段同链接条目,核实于 2026-08)。
- Tavily 文档 Search API(
chunks_per_source按源取段、search_depth调节延迟与相关度权衡,每段上限 500 字符),https://docs.tavily.com/documentation/api-reference/endpoint/search ,核实于 2026-08。 - Addy Osmani《Agentic Engineering》(O'Reilly,Early Release 2026)第 4 章(context rot 的操作判据:利用率守约四成、过六成收工;过崖信号清单——同一工具调用连续三轮无变化、守着的规则突然破戒、目标丢失、编辑变糙、“You are absolutely right”式反射附和是停摆铁证;压实、卸载、重开三板斧与交接文件续跑),见 txt/agentic-engineering.txt(英文原文,第 4 章,行 1746-1806、1808-1855)
- Jia Huang《Designing AI Agents》(Manning,MEAP)第 3 章(attention zone coverage:关键条目要落进窗口首尾的高注意力区,分诊只看优先级不够;错误踪迹对压实豁免、is_error 标志与“抹掉失败等于抹掉证据”),见 txt/designing-ai-agents.txt(英文原文,第 3 章,行 3149、4139、3442-3450)
- Manus 工程博客《Context Engineering for AI Agents: Lessons from Building Manus》(错误踪迹不得压实:五百 token 堆栈摘要成一句报错后智能体原路重干数小时、is_error 条目豁免压缩防重试循环),https://manus.im/blog/Context-Engineering-for-AI-Agents-Lessons-from-Building-Manus ,核实于 2026-09-01。
- Jia Huang《Designing AI Agents》(Manning,MEAP)第 3 章(渐进式探索三段程序:广撒网读清单与目录、定点全文读、顺引用深追,各段设次数上限;医源性上下文与峰形剂量曲线:为帮忙而添的信息自己成病因、判断力峰值在中等填充段),见 txt/designing-ai-agents.txt(英文原文,第 3 章,行 2999-3003、3536-3591)。
- Roberto Infante《AI Agents and Applications with LangChain, LangGraph and MCP》(Manning,2026 中译)第 3 章(多文档摘要选路:装得下整段填充,装不下分块归并或滚动精炼及两者取舍)、第 4 章(研究摘要引擎流水线按步骤配不同档位模型,批量摘要与抽取走低价档)、第 10 章(最终合成与回答选完整版模型而非小档的取舍说明),见 txt-ref-zh/agents-langchain-langgraph-mcp/。
- Louise Macfadyen《Designing AI Interfaces》(O'Reilly,2026)第 3 章(输入先排版:结构化 Markdown 比平面文本更可靠,转述 Braun 等的法律文书排版实验),见 txt/designing-ai-interfaces.txt(英文原文)。
- Christian Braun、Alexander Lilienbeck、D. Mentjukov《The Hidden Structure —— Improving Legal Document Understanding Through Explicit Text Formatting》(2025,法律推理任务上显式排版对准确率的提升),https://arxiv.org/abs/2505.12837 ,核实于 2026-09-16。
- 《GenAI on Google Cloud》(Ayo Adedeji、Lavi Nigam、Sarita Joshi 等四位 Google Cloud 从业者,O'Reilly,2026)第 2 章(数据故障淹没学习:重复小故障灌满智能体上下文、战略性失败无从分辨、长期学习饿死),见 txt/genai-google-cloud.txt(英文原文)。
- Younghee Song, Jungjun Hur《Build an AI Agent (From Scratch)》(Manning MEAP V3,2026)第 4 章(上下文传播:ExecutionContext 作为工具首参数随调用传递、需要的工具选择性接收)、第 6 章(回调时序限制:模型调用前的回调拿到的是已构建请求,补事件最早下一轮才可见、框架内部逻辑不进回调),见 txt-ref-zh/build-ai-agent-from-scratch/。
- HuggingFace Transformers 文档《Chat templates》(消息列表经聊天模板渲染成模型训练时格式的输入,控制记号各模型不同,格式错配会明显掉性能,apply_chat_template 的用法),https://huggingface.co/docs/transformers/chat_templating ,核实于 2026-09-16。