手上有一份一万五千字的产品规范,想让 AI 干三件事:出一版目录、每章写一段摘要、把散在正文里的参数表汇总成一张对比表。
最常见做法是整篇复制进去,然后祈祷。结果通常是三种:要么超出上下文被截断,后半篇直接消失;要么模型"读了但没读全",摘要里漏掉两章你事后才发现;要么它很努力地把表格重新排版了一遍,但漏了三张。
问题不在模型,在于你把"喂进去"这件事当成了一次性动作。长文档要的是流水线:先看清结构,再切成能消化的块,逐块处理,最后用机器校验有没有漏——最后这一步最容易被跳过,也最要命。
长文档处理的正确顺序是:目录 → 分块 → 逐块摘要 → 覆盖校验。 其中"覆盖校验"是唯一能保证"AI 没漏章节"的手段,靠人眼读摘要发现不了漏。
适用边界:本文全部实测于 macOS + Python 3.14.3,脚本零第三方依赖、Python 3.9+ 可跑。只处理 Markdown;docx / PDF 需先转成 md。它做的是结构层面的切分与校验,不做语义相似度检索,也不替代你判断摘要写得对不对。
# 环境:macOS,Python 3.9+,零第三方依赖
mkdir -p longdoc-demo && cd longdoc-demo
curl -O https://example.com/longdoc_kit.py # 换成你自己的存放路径
python3 make_longdoc_demo.py # 生成一份虚构示例文档make_longdoc_demo.py 的作用很朴素:造一份 15233 字符 / 429 行 / 59 个标题的虚构规范文档,里面故意埋了 8 类坑(代码块里的 # 注释、Setext 标题、跳级编号、超长单行段落、frontmatter 等)。用固定随机种子,任何人跑出来的文件字节完全一致,方便复现我的实测数字。
所有示例内容均为虚构,与任何真实系统、真实业务数据无关。
python3 longdoc_kit.py outline longdoc-demo/spec_v23.md --stats
echo $? # 有跳级返回 1真实输出(本机,节选):
文件: longdoc-demo/spec_v23.md
总行数: 429 字符数: 15233
识别标题: 59 个(朴素 ^# 扫描: 77 个,含围栏内误判)
层级分布: H1=1 H2=13 H3=30 H4=15
------------------------------------------------------------
H1 L 7 智能办公助手部署与运维规范(示例文档)
H2 L 12 1. 部署前检查
H3 L 16 1.1 环境依赖
...
------------------------------------------------------------
[WARN] 检测到 1 处层级跳级,目录可能编号有误:
H2 → H4 8.0 跳级小节(示例:H2 下直接 H4)这一步的价值在于"先发现文档自己的问题"。跳级、编号断裂、章节重名,这些在原文里看不出来,一旦切成目录就一目了然。目录同时也是后面分块的锚点。
python3 longdoc_kit.py chunk longdoc-demo/spec_v23.md --max-chars 3000 --overlap 200
python3 longdoc_kit.py hardcut longdoc-demo/spec_v23.md --max-chars 3000 # 对照组按标题切(14 块)与纯按字符硬切(6 块)的差别,不在块数,在切坏了什么:
--- 硬切 max_chars=3000 ---
块数: 6
被切断的代码块: 2 处(该块内围栏数为奇数,AI 会看到半个代码块)
被切断的表格: 1 处(表头与数据行分家)代码块被切成一半、表格表头和数据行分家,AI 拿到的就是残缺信息,它会非常自信地基于残缺内容作答。按标题切永远不会切在围栏中间,因为标题本身就是语义边界。
python3 longdoc_kit.py pack longdoc-demo/spec_v23.md --max-chars 3000 --overlap 150 --out packs/
# 输出:packs/chunk-01.md … chunk-14.md每个块文件自带三样东西:标题路径(这个块属于哪一章)、字符数与行区间、以及可选的"上文衔接"片段。逐块喂给 AI 时,指令要统一,别让模型自己发挥:
只摘要下面这一个块,不要臆测块外内容。
输出三行:① 本块讲了什么(≤60 字)② 关键参数或结论(有就列,没有写"无")
③ 本块是否包含表格(是/否,是则原样保留表头)
块内容:
<粘贴 chunk-XX.md>最后把所有"①"按顺序拼起来就是全文摘要,把"③=是"的块单独拎出来就是对比表的素材。摘要的合并顺序由脚本保证,不靠模型记忆。
python3 longdoc_kit.py verify longdoc-demo/spec_v23.md --max-chars 3000
echo $? # 0=通过,1=有遗漏或重复,可直接挂流水线做门禁覆盖校验: 块数=14 应覆盖正文行=264 实际覆盖=264 耗时=0.1 ms
覆盖率: 100.0%
[OK] 无遗漏
[OK] 无重复覆盖这里校验的不是"摘要质量",而是原文的每一行正文是否恰好落在一个块里。漏行 = AI 根本没看到那部分,摘要必然缺失;重复行 = 同一段内容被算了两次。这两个问题用眼睛看摘要都发现不了。
python3 longdoc_kit.py tables longdoc-demo/spec_v23.md共 11 张表
# 行区间 列 数据行 表头
1 L42-48 3 5 | # | 操作 | 示例阈值 |
...
11 L423-427 3 3 | # | 版本 | 说明 |先知道有 11 张表、每张几列,再让 AI 汇总成一张对比表,你就能用"表数对不对"来验货。让 AI 从长文里"找表再合并",等于把计数和改写两件事同时交出去,出错率翻倍。
全部为本机真实执行,示例文档 15233 字符 / 429 行,三次取 min/avg/max:
max_chars | 块数 | 最大块字符 | 耗时 min/avg/max (ms) | 覆盖率 |
|---|---|---|---|---|
500 | 44 | 497 | 0.21 / 0.23 / 0.24 | 100.0% |
800 | 30 | 800 | 0.21 / 0.21 / 0.21 | 100.0% |
1500 | 17 | 1475 | 0.05 / 0.05 / 0.05 | 100.0% |
3000 | 14 | 2500 | 0.05 / 0.05 / 0.05 | 100.0% |
6000 | 14 | 2500 | 0.05 / 0.05 / 0.05 | 100.0% |
对照组(同一份文档,纯按字符硬切):
max_chars | 块数 | 切断的代码块 | 切断的表格 |
|---|---|---|---|
800 | 20 | 0 | 2 |
1500 | 13 | 0 | 1 |
3000 | 6 | 2 | 1 |
目录识别对照:朴素 ^#{1,6}\s 扫描命中 77 条,真标题只有 59 条——20 条是代码块围栏里的 # 注释被误判,另有 2 条 Setext 标题(=== / --- 下划线式)被漏掉。
分块耗时在 0.05–0.24 ms 量级,相对于一次模型调用可以忽略,所以分块策略应该按"模型能消化多少"来定,不用考虑脚本性能。
踩坑(全是真跑出来的,不是想象):
len(line) 累加,块实际比上限大(行数−1)个字节——实测 800 的上限切出 830 字符的块。改成 len(line) + 1。# 是最强噪音源,一份文档贡献 20 条假标题。先标记围栏状态再找标题,顺序不能反。文字 + ===)朴素扫描必漏。同时要注意 frontmatter 结尾的 --- 不能被当成 Setext 下划线——必须先圈出 frontmatter 行再找标题。errors="replace" 会变成一堆问号,先 file -I 确认编码再读。边界(这些情况别用):
--depth 决定切多细:结构清晰的文档用 H2(14 块),单章特别长的改用 H3 边界(实测同文档、同样 3000 上限会切出 44 块)。长文档的问题从来不是"模型读不完",而是你没法证明它读全了。目录、分块、摘要这三步大家都会做,真正拉开差距的是第四步——用脚本按行校验覆盖率,把"没漏"从感觉变成可验证的事实。整套流程零依赖、0.05 ms 级别,成本远低于一次重试。
完整脚本见 assets/longdoc_kit.py(含 outline / chunk / pack / verify / hardcut / tables / selftest 七个子命令、内置自检 3/3 通过)。下面是三个最关键的函数:
# ① 先标围栏与 frontmatter,再找标题(顺序反了就全错)
ATX_RE = re.compile(r"^(#{1,6})[ \t]+(\S.*?)[ \t]*#*\s*$")
FENCE_RE = re.compile(r"^[ \t]{0,3}(```+|~~~+)")
def find_headings(doc):
out = []
for i, ln in enumerate(doc.lines):
if doc.in_fence[i] or doc.is_frontmatter[i] or doc.is_table[i]:
continue # 围栏内、frontmatter、表格行一律不看
m = ATX_RE.match(ln)
if m:
out.append((len(m.group(1)), i, m.group(2).strip())); continue
if i + 1 < doc.n and doc.lines[i].strip():
if re.match(r"^=+[ \t]*$", doc.lines[i + 1]):
out.append((1, i, doc.lines[i].strip())) # Setext H1
elif re.match(r"^-{3,}[ \t]*$", doc.lines[i + 1]):
out.append((2, i, doc.lines[i].strip())) # Setext H2
return out
# ② 分块主循环:+1 是换行符;超长行切成片段且各自成块
for i in seg:
ln = doc.lines[i]
if cur and cur_len + len(ln) + 1 > max_chars:
flush()
if split_long and len(ln) > max_chars:
flush()
for piece in split_sentences(ln, max_chars):
raw.append((path, [(i, piece)], {i})) # 片段单独成块
continue
cur.append((i, ln)); cur_len += len(ln) + 1
# ③ 覆盖校验:按行号,不按文本
expect = set(doc.content_line_idxs()) # 非空、非 frontmatter 的行
covered = {}
for c in chunks:
for i in set(c.lines):
covered[i] = covered.get(i, 0) + 1
missing = sorted(expect - set(covered)) # 漏
dup = {i: n for i, n in covered.items()
if n > 1 and i not in partial_lines} # 重复(排除句子切分的行)快速自检:
python3 longdoc_kit.py selftest
# [PASS] 目录提取 / 对照组 / 覆盖校验 —— 全部通过(3 项)WorkBuddy、AI 办公、效率工具关联阅读:本文处理的是单篇长文档;如果你要管理的是整个笔记库(几百篇 md 的索引与断链体检),思路不同,那是另一篇的内容。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。