数据截至 (上游 commit 1b7d2e80db9f)
压缩与容错:研究压缩、token 超限重试、最终报告生成
30 秒导读: 一个 researcher 搜完一堆资料后,消息历史会大到爆上下文窗口;把它们喂给模型"蒸馏成摘要""拼成最终报告"时,随时可能撞上"prompt 太长"这种错。本章讲这条流水线尾端的三处 LLM 产出——研究压缩、跨供应商 token 超限识别、最终报告生成——以及把它们裹起来的那套**"认出超限→砍上下文→重试"**的容错逻辑。这是本库工程含量最高、最能体现 bitter-lesson 式健壮性的部分。
前置:研究引擎怎么把 supervisor 委派、researcher 怎么跑 ReAct 循环并并行 fan-out,见 02-supervisor-researcher-loop.md;整张 LangGraph 的三层编排见 01-orchestration-graph.md。本章只讲这条链的收尾与容错。
1. 这是什么(零基础也能懂)
一句话定义: 研究做完之后,把"一大堆对话消息"变成"一段干净摘要",再变成"一篇报告"——而且做这两步的时候,假设它会因为内容太长而失败,并准备好一层层降级重试。
它要解决的真实痛点。 深度研究天然会产生海量文本:
- 一个 researcher 可能调了十几次搜索工具,每次工具返回几千字。
- 这些工具消息 + 模型的思考消息全堆在一个消息列表里。
- 到了要"总结"或"写报告"的时候,这个列表塞进 prompt,很容易超过模型的上下文窗口(比如 GPT-4o 的 128k、Claude 的 200k)。
- 一旦超,API 直接抛异常——如果不管,整个研究任务前功尽弃。
一句话直觉/类比。 把它想成做读书笔记再写论文:
- 压缩(compress) = 把你翻过的一摞书、划的线,浓缩成一页笔记(但底稿留着)。
- 写报告(final report) = 把所有人的笔记拼起来,写成一篇成文。
- 容错 = 如果笔记多到桌子摆不下,就先撤掉最近翻的那几本;如果论文素材多到写不动,就按比例先删掉一截再写。
为什么值得单独讲。 因为"调用模型"从来不是难点,"在真实供应商、真实长文本下不崩"才是。这一章的代码,大半不是在产出内容,而是在兜住失败。
2. 顶层全景(它大概怎么转)
三处 LLM 产出,分布在两个位置:
| 产出点 | 在哪 | 输入 | 输出 |
|---|---|---|---|
研究压缩 compress_research | researcher 子图收尾 | 一个 researcher 的全部消息 | compressed_research(摘要)+ raw_notes(底稿) |
最终报告 final_report_generation | 主图收尾 | 所有 notes 拼成的 findings | final_report(成文) |
| (第三处是"token 超限识别",不产内容,是前两处共用的容错判据) | utils.py | 一个异常 + 模型名 | 是/否超限 |
主线走一遍(高层,不进代码):
researcher 搜完
│ researcher_messages = [AI思考, Tool结果, AI思考, Tool结果, ...]
▼
┌───────────────────── ────────────────────────┐
│ compress_research(researcher 子图收尾) │
│ · 追加一句"请清理这些发现"的 human message │
│ · while 循环最多 3 次: │
│ 调压缩模型 → 成功则返回摘要+底稿 │
│ 失败且是 token 超限 → 砍掉最后一段 AI 之后│
│ 的消息,再试 │
│ · 3 次都败 → 返回一句错误串当摘要(不崩) │
└─────────────────────────────────────────────┘
│ compressed_research 汇成 supervisor 的 notes
▼ (多个 researcher 并行,notes 逐条累加)
┌─────────────────────────────────────────────┐
│ final_report_generation(主图收尾) │
│ · findings = 所有 notes 拼一起 │
│ · while 循环最多 3 次: │
│ 调报告模型 → 成功则返回报告 │
│ 失败且是 token 超限 → 渐进截断 findings │
│ 第1次:按 模型上限×4 字符 截 │
│ 之后:上次长度 ×0.9 再截 │
│ · 查不到模型上限 / 非超限错 → 立刻返回错误串 │
└─────────────────────────────────────────────┘
│
▼ final_report(总能返回一个字符串)
贯穿两处的同一条暗线: 每一次 LLM 调用都被 try/except + while 包住,except 里先问一句"这是不是 token 超限?"——是,就砍上下文重试;不是,就当普通失败处理。这句"是不是超限"的判断,就是第三处的主角。