Agnes-2.5-Flash 长文档一次喂不进去 / 先估算 token 再决定怎么切块

文章导读
长文档一次喂不进去,通常分两种情况:一是整篇超出 Agnes-2.5-Flash 单次可接收的上下文长度,直接被截断或报错;二是没有超限,但文档太长,模型把注意力分散到无关章节,答案丢三落四。两种情况的处理不一样,所以先别急着改提示词,而是先把文档换算成一个可比较的规模指标,再决定切几块、在哪切、块与块之间怎么衔接。判断顺序可以是:先粗估总 token 量级,对比你所用接口的上下文上限留出余量;再
📋 目录
  1. Ⅰ 先量出手里文档的实际规模
  2. Ⅱ 找出切块的自然边界
  3. Ⅲ 给每块加上位置标记与重叠
  4. Ⅳ 验证切块后答案是否还连贯
A A

长文档一次喂不进去,通常分两种情况:一是整篇超出 Agnes-2.5-Flash 单次可接收的上下文长度,直接被截断或报错;二是没有超限,但文档太长,模型把注意力分散到无关章节,答案丢三落四。两种情况的处理不一样,所以先别急着改提示词,而是先把文档换算成一个可比较的规模指标,再决定切几块、在哪切、块与块之间怎么衔接。判断顺序可以是:先粗估总 token 量级,对比你所用接口的上下文上限留出余量;再按自然边界切块并加位置标记;最后用同一组问题分别问整篇和切块结果,看丢失是从哪里开始的。

先量规模再切块,通常比反复调提示词更有效:用字符数与中英混排比例粗估 token,按标题层级和问答轮次找边界,块首写清序号、总块数和上一块结尾,相邻块留一两句重叠,最后拿同一组问题对比整篇与切块的答案差异。规模估算只是判断依据,不是精确计数,实际仍要结合模型分词器、上下文上限和返回日志确认。

先量出手里文档的实际规模

凭感觉判断“这份文档大概挺长”没有可操作性。建议先把文档统计成几个能放在一起比较的指标:总字符数、段落数、换行数、标点数,以及中文与英文字符各自占多少。中英文混排时,两者的 token 折算比例差别不小,通常中文一个汉字大致在 1 个 token 上下浮动,英文约 4 个字符折 1 个 token,所以混排文档如果只按总字符数估,容易偏得比较厉害。

下面这段脚本放在本地跑,读的是纯文本或已抽取的正文文件,输出的是量级参考,不是精确 token 数。替换项只有文件名和用于分段的空行规则。

import re

text = open("doc.txt", encoding="utf-8").read()

cjk   = len(re.findall(r"[\u4e00-\u9fff]", text))
ascii_chars = len(re.findall(r"[A-Za-z0-9]", text))
punct = len(re.findall(r"[,。;:、!?,.!?;:]", text))
lines = text.count("\n") + 1
paras = len([p for p in re.split(r"\n\s*\n", text) if p.strip()])

print("总字符", len(text))
print("汉字", cjk, "英数", ascii_chars, "标点", punct)
print("行数", lines, "段落", paras)
print("中文占比", round(cjk / max(len(text), 1), 2))

rough = cjk + punct + ascii_chars / 4
print("粗估 token 量级", int(rough))
print("按每块 2000 量级估算块数", max(1, int(rough / 2000) + 1))

把粗估结果和你所用接口的上下文上限放在一起看,留出提示词、历史轮次和回答本身要占的部分,剩下的才是能放正文的空间。这段估算是为了决定“切几块”,不能拿来当精确预算;如果接口返回里带 usage 或 token 计数字段,可以拿一次真实请求的结果回校这个粗估值,偏得多就调整中英文折算系数。记录时建议把汉字数、英数字符数、段落数分开写,后面判断丢失原因时用得上。

找出切块的自然边界

按固定字符数硬切是最省事的做法,也是最容易把一句话、一张表、一段代码从中间断开的做法。切块的第一原则是:边界要落在读者也会认为是边界的地方。可用的自然边界通常有几种:Markdown 的标题层级(# / ## / ###)、中文的“第X章”“第X节”、列表前的引导句、以及问答文档里成对的“问—答”轮次。优先在上一级标题处切,只有在单个章节本身就超过预算时,才在该章节内部按小节或段落再切。

有两类内容必须整块保留,不能拆:表格和代码块。表格被切开后,表头和后续行分居两块,模型往往会把列对应关系理解错;代码块被截断后,缩进和括号不闭合,后续推理基本不可用。做法上可以先扫描出这些区间,切分点遇到区间就往前或往后让位。

import re

HEAD = re.compile(r"^(#{1,4}\s|第[一二三四五六七八九十百]+[章节])")
FENCE = re.compile(r"^```")

blocks, cur, in_fence = [], [], False
for line in text.splitlines():
    if FENCE.match(line):
        in_fence = not in_fence
    if not in_fence and HEAD.match(line) and cur:
        blocks.append("\n".join(cur)); cur = [line]
    else:
        cur.append(line)
if cur: blocks.append("\n".join(cur))

for i, b in enumerate(blocks, 1):
    print(i, len(b), b.splitlines()[0][:40])

跑完之后看每块的字符数,超预算的那块单独再处理:如果它内部还有下级标题就按下级标题切,没有的话按段落切,但段落之间至少保留完整的表格或代码块。切完把块数和每块首行打印出来,人工扫一眼有没有明显错位,这一步花的时间通常比事后排查答案丢失少。

给每块加上位置标记与重叠

切开的块单独送进模型时,模型并不知道自己在整篇文档的什么位置,也不知道前面刚讲过什么。解决办法是在每块正文前加一小段固定格式的位置标记,让块自身携带上下文。建议包含四项:本块序号与总块数、本块标题、上一块的结尾一句、以及一句关于重叠区的说明。

Agnes-2.5-Flash 长文档一次喂不进去 / 先估算 token 再决定怎么切块
【第 3/11 块】本块标题:3.2 增量同步策略
上一块结尾:……因此需要先建立基线快照。
说明:本块开头与上一块结尾有少量重叠,以本块正文为准,不要重复输出重叠内容。
--- 正文开始 ---
(本块正文)
--- 正文结束 ---

重叠区的长度以一两句为宜,作用是让跨块指代和连续步骤不至于断掉,不是让两块内容互相覆盖。留太多重叠会挤占本已紧张的上下文空间,还会诱导模型重复输出。重叠要放在块首,而不是块尾,因为读块的顺序是从前到后,块首的重叠能立刻接上上文。

如果是把多个块一起送进去做汇总,提示词里可以加一句限定范围,避免模型把本块没写的内容也“补”出来:

请只依据下面这一块内容回答。若答案需要其他章节,
请直接说明“需要第 N 块”,不要推测。

块序号和总块数建议同时写进块首标记和文件名或日志字段,后面对比答案时,能快速定位是哪一块出的问题。

验证切块后答案是否还连贯

判断信息丢失是切块造成的,还是问题本身超出文档范围,最直接的办法是准备一组固定问题,分别对整篇文档(或模型能完整接收的最大范围)和切块结果各问一遍,然后逐条比对。问题集要覆盖不同信息类型,才能看出丢失的规律:只在单块内出现的事实型问题、需要顺序的步骤型问题、需要跨两块以上才能回答的问题、以及文档里本来就没有写的问题。最后一类很关键,它能区分“切块丢了信息”和“文档根本没有这个信息”。

比对时按信息类型记录,而不是只记对错。可以简单列成一张表:

  • 事实型(名称、数值、条件):整篇答对、切块答错,通常是块被切在了该事实所在的段落中间。
  • 步骤型(顺序、前后依赖):切块后顺序错乱或漏步,通常是没有留重叠、或块首缺少“上一块结尾”。
  • 跨块型(需要两块以上拼接):切块后答“文档未提及”,通常是块首标记没写清总块数,模型不知道还有后续。
  • 文档外型:整篇和切块都应答“未提及”。如果整篇能编出内容而切块说没有,说明整篇那次是在硬凑,不能作为基准。

如果整篇和切块的差异集中在跨块型问题上,优先调边界和重叠;如果单块内的事实也丢了,先检查块是不是切在了句子或表格中间;如果切块后几乎每类都变差,那更可能是单块仍然太大,需要把块再切小一档。整个判断过程只依赖你自己准备的问题集和接口返回,不需要额外的评测工具,但结论要结合具体文档和模型配置确认,换一份文档、换一个上下文上限,边界位置通常也要重新看一遍。