文档太长放不进上下文,怎么处理?

进阶场景题实践约 7 分钟读完

一句话回答

先看任务类型。需要通读全文的任务(总结、提取所有条款)用分块处理:Map-Reduce 每块独立处理再汇总,可以并行;Refine 按顺序逐块更新结果,更连贯但只能串行;特别长的文档用层级摘要逐层合并。只问局部问题时,用检索取出相关片段(RAG)就够了。每块都要保留出处,方便引用和核对;文档能放进长上下文模型时,直接放进去往往实现最简单、效果也好,但成本和延迟更高。

详细解析

先分清任务类型

  • 全局任务:总结全文、生成提纲、找出所有风险条款、对比各章节的观点,必须覆盖全文
  • 局部任务:某个条款怎么规定、某个数字是多少,只需要相关片段,用 RAG 即可,见 RAG 的完整链路
  • 两者都有:文档上传后同时做两件事,生成目录和摘要,并建立检索索引支持问答,见 超长文档的总结和问答服务

三种分块处理方法

文本
Map-Reduce:
  切块:   [块1] [块2] [块3] … [块n]
  Map:    每块并行处理 → 结果1 结果2 结果3 … 结果n
  Reduce: 合并所有结果 → 最终结果(结果太多放不下时,先分组合并,再合并一层)

Refine:
  结果 = 处理(块1)
  结果 = 根据块2 更新(结果)
  ……
  结果 = 根据块n 更新(结果)

层级摘要:
  段落 → 小节摘要 → 章节摘要 → 全文摘要(按文档结构逐层向上合并)

加上检索和直接使用长上下文,几种做法的对比:

方法 优点 缺点 适合
Map-Reduce 可以并行,速度快;单块失败只需重试这一块 块之间的联系会丢失;汇总时信息被二次压缩 摘要、逐块抽取信息
Refine 能利用前文,结果连贯 只能串行,慢;前面的内容在多次更新后可能被冲淡 前后关联紧密的文档
层级摘要 符合文档结构,中间结果可以复用(如章节导读) 依赖结构解析;层数多时误差会累积 书籍、手册、长篇报告
检索(RAG) 成本低、延迟低 不适合需要全文的任务;检索不到就答不出 针对局部内容的问答
直接用长上下文 实现最简单,能看到全文的联系 成本高、首 token 慢;中间部分可能被忽略 文档在窗口内,需要跨段落推理

实践要点

  • 按结构切块:按章节、段落切,块之间保留少量重叠,每块带上标题路径,见 文档怎么切分
  • Map 阶段的指令要具体:写清要提取什么(如"列出这部分提到的所有付款条款,没有就输出空列表"),输出用统一的结构,Reduce 时才好合并和去重
  • 保留出处:每个 Map 结果带上块编号、页码或章节,最终结论标注来源,方便用户核对
  • 控制并发、失败重试:几百个块同时请求很容易触发限流,见 限流、重试和降级
  • 同一份文档要问很多问题:如果能放进窗口,把文档放在固定的前缀里,利用 Prompt Caching 降低重复读取的成本

代码示例

Map-Reduce 总结(llm.chat 是通用伪接口,splitIntoChunks 按文档结构切块):

TypeScript
async function summarizeLongDoc(doc: string): Promise<string> {
  const chunks = splitIntoChunks(doc, { maxTokens: 3000, overlap: 200 })

  // Map:每块单独提取要点,带上块编号,方便最后标注出处
  const partials = await mapWithLimit(chunks, 5, async (chunk, i) => {
    const res = await llm.chat({
      messages: [{
        role: 'user',
        content: `下面是一份长文档的第 ${i + 1}/${chunks.length} 部分。列出这部分的关键要点,每条一行,只写原文中有的信息。\n\n<chunk>\n${chunk}\n</chunk>`,
      }],
    })
    return `[第 ${i + 1} 部分]\n${res.content}`
  })

  // Reduce:合并要点。要点总量仍然放不下时,应先分组合并,再合并一层
  const res = await llm.chat({
    messages: [{
      role: 'user',
      content: `下面是一份文档各部分的要点。合并成一份完整的摘要:去掉重复,保留关键数据,按主题组织,每条结论后标注来自第几部分。\n\n${partials.join('\n\n')}`,
    }],
  })
  return res.content
}

// 限制并发数,避免触发限流
async function mapWithLimit<T, R>(items: T[], limit: number, fn: (item: T, i: number) => Promise<R>) {
  const results: R[] = new Array(items.length)
  let next = 0
  const worker = async () => {
    while (next < items.length) {
      const i = next++
      results[i] = await fn(items[i], i)
    }
  }
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker))
  return results
}

面试官可能追问

汇总时各部分的结果加起来还是太长,怎么办?

做多层 Reduce:把结果分组,每组先合并成一份中间结果,再合并这些中间结果,直到能放进一次调用。每一层都要保留出处标记,否则最后无法追溯。

块与块之间有关联(前面定义、后面引用),分块处理会出问题吗?

会。比如合同在开头定义了"甲方",后面的条款只写"甲方"。缓解方法:每块的 Prompt 里附上文档级的背景(标题、目录、术语定义);块之间保留重叠;关联特别紧密的文档改用 Refine 或长上下文。

怎么检查总结有没有遗漏重要内容?

Map 阶段明确要抽取的信息类型,结构化输出后在代码里汇总,不要全靠模型"自由总结"。还可以根据原文自动出一批问题,看摘要能不能回答,再结合人工抽查。

处理一份几百页的文档要花多少钱、多长时间,怎么估算?

token:输入约等于全文(加上重叠部分和每次调用的指令),再加上 Reduce 阶段的输入;输出约等于块数乘以每块结果的长度。时间:块数除以并发数,乘以单次调用的耗时,再加上 Reduce 的耗时。单价按当前的官方价格代入,见 一次调用的成本怎么估算。

易错点

  • 用 RAG 做"总结全文",只检索到少数片段,总结以偏概全
  • Map 阶段的输出格式不统一,Reduce 时难以合并
  • 丢了出处,最终的结论没法核对
  • 不限制并发,大量请求同时发出,触发限流

AI 模拟面试官

用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮

登录后就可以和 AI 面试官对练,面试记录也会保存下来。登录

这道题你掌握了吗?

选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。

学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。