版权与内容来源声明
本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容,均在附表 A 中标注来源;引用官方原文保持原样,不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准,标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式,也不对任何收益结果作承诺。转载请注明出处。
把一页 A4 按 3×3 网格切、重叠给到 30%,30 行正文里能完整落在某一块内的行数是 0。同一组参数下,只按上下边界算的纵向完整率是 1.00,横向却是 0 —— 被劈开的不是行高,是行宽。
这是按机制构造的演示,不是某次事故的原始日志:页面用 Pillow 现画、行等距、参数自己扫。但它复现的机制是真的,任何一个矩形网格切块器,切到列的时候都会撞上同一件事。
一、结论先行:九块切下去,整行完整率是 0,不是 90%
先把结论摆出来:「切得更细 = 每块更聚焦 = 识别更准」这条链条,在文档图像上断在第二环。细切确实提高了每块的有效分辨率,但同时把每一行从中间切开,而视觉模型读一行字,需要的是完整的一行,不是两半各带一次的残句。
重叠从 0 调到 30%,有一列数字全程没动
一页 1240×1754(A4 比例,按 150 像素/英寸渲染)的文档图,按 3×3 网格切,块宽 413 像素;正文行宽 1040 像素。413 装不下 1040,所以任何一块都不可能包含完整的一行,重叠从 0 加到 30%、块数从 9 涨到 16、冗余面积从 1.00 倍涨到 1.78 倍,整行完整率那一列始终是 0.00。
这不是调参没调到位,是参数无效。重叠的作用是让相邻两块共享一条带,它能让一个被切开的物体在某一侧重新变完整 —— 前提是这块本身的尺寸大于那个物体。当块宽小于行宽时,共享带再宽也只是把「半行」复制到两块里,两边都是半行。
真正被重叠救回来的,只有纵向那一列
同一个脚本里我还单独统计了纵向完整率:只看一行的上下边界有没有落在同一块的纵向区间内,不管左右。这一列会动,而且动得有规律:N=3 时从 0.93 抬到 1.00,N=4 时从 0.90 抬到 1.00,N=2 时从 0.97 抬到 1.00。
两个方向的反差,正好划出了重叠的作用域:它只在「块的尺寸大于被切对象」的那个轴上有用。行是横着排的,行高 48 像素远小于块高,所以纵轴上重叠有效;行宽 1040 像素大于块宽,所以横轴上重叠无效。判断该不该加重叠、加多少,先看被切对象在哪一轴上被切短了,而不是先拍一个百分比。
二、口径先钉死:什么叫「一行完整」
之所以很多团队在这件事上吵不出结果,是因为大家报的指标根本不是一个东西。有人报「块的平均字符数」,有人报「重叠百分比」,有人报「OCR 回来的字准不准」—— 前两个是输入参数,第三个是端到端结果,中间那个真正决定成败的量被跳过了。
一行完整的判定式:一个块同时盖住它的上下边界和左右边界
本文把「整行完整率」定义成一句话:一行的外接矩形,完整落在至少一个块的矩形内,这个行才算完整。写成判定式就是,存在某个块(x0,y0,x1,y1),使得该行的外接框(l,t,r,b)同时满足 x0 <= l、r <= x1、y0 <= t、b <= y1 四条。
这个定义看起来很严,但它对应的正是模型侧的真实约束:一次前向里,模型看到的是这一块像素,块外的东西它不知道。行被切开之后,上半截和下半截分别落在两次调用里,中间那部分笔画谁都看不见。你要它「把这两块的内容拼起来理解」,那是你在应用层做的事,不是模型在块内做的事。
为什么不用「块大小」和「重叠百分比」当指标
因为同一个百分比,在不同切法下对应的绝对像素能差一倍多。本文的页面高 1754:切成 2 行时块高 877,10% 重叠是 87.7 像素;切成 3 行时块高 585,10% 是 58.5 像素;切成 4 行时块高 438,10% 只剩 43.9 像素。而行高是恒定的 48 像素。
于是出现一个很别扭的局面:同样是「10% 重叠」,2 行切法给的余量接近两个行高,4 行切法给的余量还不到一个行高。同一个百分比,落到 87.7、58.5、43.9 三个完全不同的绝对余量上 —— 百分比被当成了不变量,而真正该当不变量的,是行高。
| 整行完整率 | 外接矩形完整落在某一块内的行,占全部行的比例 | 逐行判断四条边界,命中任一块即计 1 | 主指标 |
| 纵向完整率 | 只看上下边界是否落在同一块的纵向区间内 | 忽略左右边界,单独诊断纵轴 | 诊断指标 |
| 块数 | 实际切出的块数量 | m = 1 + ceil((N-1)/(1-o)),总数 m*m | 成本指标 |
| 冗余面积比 | 所有块面积之和 ÷ 页面面积 | 块尺寸固定时等于 (m/N)^2 | 成本指标 |
| 行高 h | 行盒高度,含行距 | 由字号与行距决定,本文固定 48 像素 | 换算基准 |
三、最小复现:30 行、48 像素行高,扫 20 组参数
复现不需要任何 OCR 库、不需要模型、不需要联网。要证明的只是一件几何事实,用 Pillow 画一页、按网格切、数一数就行。这也意味着你可以把这套判定搬到自己的真实文档上,只换掉「造页」那几行。
造页与切块:一段自包含的脚本
页面按 A4 比例取 1240×1754,左边距 100、右边距 1140,行宽 1040;30 行等距文本,行盒高 48 像素,首行顶距 120。块尺寸固定为 W/N × H/N,不做「均分带余数」的处理,最后一行一列贴边对齐 —— 这是绝大多数切块库的实际行为,也避免切出宽度只有几十像素的碎条。
重叠按块尺寸的百分比取,步长 s = T*(1-o)。块数不是 N,是 m = 1 + ceil((N-1)/(1-o)),这一点后面会单独算账。
🧪 实测环境:Python 3.13.12 / macOS 27.0.1(arm64)/ Pillow 12.3.0 / 字体 Songti.ttc;口径:1240×1754 的 A4 比例页面、30 行等距文本、行盒高 48 像素,块尺寸固定为 W/N × H/N,重叠按块尺寸的百分比取,末行末列贴边对齐;「整行完整」= 该行外接框完整落在某一块内。
#!/usr/bin/env python3
"""e01:在内存里造一页 A4 文档图,按 N×N + 重叠 o 切块,统计整行完整率。
用法: python e01_tiling_grid.py
依赖: Pillow / 系统字体 Songti.ttc(失败则退回默认位图字体并在输出里说明)
"""
import math
from PIL import Image, ImageDraw, ImageFont
W, H = 1240, 1754 # A4 @150dpi
MARGIN_L, MARGIN_R = 100, 1140
TOP, LINE_H, N_LINES = 120, 48, 30 # 行盒高 48px,共 30 行
FONT_PATH = "/System/Library/Fonts/Supplemental/Songti.ttc"
try:
font = ImageFont.truetype(FONT_PATH, 32)
font_note = FONT_PATH
except Exception as e:
font = ImageFont.load_default()
font_note = "PIL default bitmap (%s)" % e
page = Image.new("RGB", (W, H), "white")
d = ImageDraw.Draw(page)
seed_text = "切块边界落在哪里,决定这一行还能不能被完整读出来。"
lines = []
for i in range(N_LINES):
y0 = TOP + i * LINE_H
txt = ("第%02d行 " % (i + 1)) + seed_text[i % 8:] + str(i)
d.text((MARGIN_L, y0 + 6), txt, fill="black", font=font)
d.line([(MARGIN_L, y0), (MARGIN_R, y0)], fill=(225, 225, 225))
lines.append((MARGIN_L, y0, MARGIN_R, y0 + LINE_H))
def grid(n, o):
"""返回 n×n 网格的块矩形列表:块尺寸固定 W/n × H/n,最后一行/列贴边对齐。"""
tw, th = W / n, H / n
sx, sy = tw * (1 – o), th * (1 – o)
mx = 1 + math.ceil((W – tw) / sx – 1e-9)
my = 1 + math.ceil((H – th) / sy – 1e-9)
out = []
for j in range(my):
for i in range(mx):
x0, y0 = min(i * sx, W – tw), min(j * sy, H – th)
out.append((x0, y0, x0 + tw, y0 + th))
return out
print("font=%s page=%dx%d line_h=%d lines=%d" % (font_note, W, H, LINE_H, N_LINES))
print("%-2s %-5s %-5s %-7s %-8s %-8s" % ("N", "o", "tiles", "area/页", "整行完整率", "纵向完整率"))
for n in (1, 2, 3, 4):
for o in (0.0, 0.05, 0.10, 0.20, 0.30):
tiles = grid(n, o)
area = sum((x1 – x0) * (y1 – y0) for x0, y0, x1, y1 in tiles) / (W * H)
full = sum(1 for (a, b, c, e) in lines
if any(x0 <= a and c <= x1 and y0 <= b and e <= y1
for x0, y0, x1, y1 in tiles))
vert = sum(1 for (a, b, c, e) in lines
if any(y0 <= b and e <= y1 for x0, y0, x1, y1 in tiles))
print("%-2d %-5.2f %-5d %-7.2f %-8.2f %-8.2f" % (n, o, len(tiles), area, full / N_LINES, vert / N_LINES))
font=/System/Library/Fonts/Supplemental/Songti.ttc page=1240×1754 line_h=48 lines=30
N o tiles area/页 整行完整率 纵向完整率
1 0.00 1 1.00 1.00 1.00
1 0.05 1 1.00 1.00 1.00
1 0.10 1 1.00 1.00 1.00
1 0.20 1 1.00 1.00 1.00
1 0.30 1 1.00 1.00 1.00
2 0.00 4 1.00 0.00 0.97
2 0.05 9 2.25 0.00 1.00
2 0.10 9 2.25 0.00 1.00
2 0.20 9 2.25 0.00 1.00
2 0.30 9 2.25 0.00 1.00
3 0.00 9 1.00 0.00 0.93
3 0.05 16 1.78 0.00 0.97
3 0.10 16 1.78 0.00 1.00
3 0.20 16 1.78 0.00 1.00
3 0.30 16 1.78 0.00 1.00
4 0.00 16 1.00 0.00 0.90
4 0.05 25 1.56 0.00 0.93
4 0.10 25 1.56 0.00 1.00
4 0.20 25 1.56 0.00 1.00
4 0.30 36 2.25 0.00 1.00
20 组读数:整行完整率只在 N=1 时是 1.00
把上面这段输出整理成表,20 组参数按台阶合并成 10 行,看一眼就够。注意 N=2 之后那一列全是 0.00,而块数和冗余面积在往上跳。
| 1 | 0.00 ~ 0.30 | 1 | 1.00 | 1.00 | 1.00 |
| 2 | 0.00 | 4 | 1.00 | 0.00 | 0.97 |
| 2 | 0.05 ~ 0.30 | 9 | 2.25 | 0.00 | 1.00 |
| 3 | 0.00 | 9 | 1.00 | 0.00 | 0.93 |
| 3 | 0.05 | 16 | 1.78 | 0.00 | 0.97 |
| 3 | 0.10 ~ 0.30 | 16 | 1.78 | 0.00 | 1.00 |
| 4 | 0.00 | 16 | 1.00 | 0.00 | 0.90 |
| 4 | 0.05 | 25 | 1.56 | 0.00 | 0.93 |
| 4 | 0.10 ~ 0.20 | 25 | 1.56 | 0.00 | 1.00 |
| 4 | 0.30 | 36 | 2.25 | 0.00 | 1.00 |
读这张表有三个台阶值得记住。第一个台阶在 N=2、o 从 0 到 0.05 之间:块数从 4 跳到 9,冗余面积从 1.00 跳到 2.25 —— 你以为加了 5% 的重叠,实际付出了 125% 的额外面积,因为 2×2 的网格为了容纳那 5% 的共享带,直接变成了 3×3。
第二个台阶是台阶内部的平坦段:N=3 时 o 从 0.05 一路加到 0.30,块数一直是 16,冗余一直是 1.78,纵向完整率从 0.97 走到 1.00 就停了。也就是说在这个区间里,继续加重叠不改变任何成本,也不改变任何收益,纯粹是把同一批像素重复送进模型。
具体到哪几行被劈开,可以对上账:N=3、o=0 时横切线落在 585 和 1170 像素两处,压住的是第 10 行(y 从 552 到 600)和第 22 行(y 从 1128 到 1176),28 除以 30 等于 0.93,跟输出里 N=3、o=0 那一行的 9 块 / 1.00 / 0.00 / 0.93 逐项对得上。标题里说的「被劈成两半」,压住的就是这一类行,具体是第几行取决于你的页边距 —— 这也是为什么它不能靠「多给点重叠」蒙过去。
第三个台阶在 N=4、o 从 0.20 到 0.30:块数从 25 跳到 36,冗余从 1.56 跳到 2.25。台阶的位置由取整决定,不由百分比决定,所以「把重叠从 20% 调到 25%」这种微调,很可能一步都没迈过台阶。
切细的代价:冗余面积是台阶式膨胀,不是线性增长
代价的数字在这张表里:N 从 1 走到 4,块数按 1 → 4 → 9 → 16 走,冗余面积比在 o=0 时恒为 1.00(因为此时块刚好铺满页面,不重不漏)。一旦 o 大于 0,冗余不是「多 o」,而是 (m/N)^2:N=2 时 2.25 倍,N=3 时 1.78 倍,N=4 时 1.56 倍。
连续近似下的公式是 (1/(1-o))^2,o=0.10 时给 1.23 倍,看着很温和。实测是 1.56 到 2.25 倍,差了一倍。原因不复杂:块数是整数,(N-1)/(1-o) 只要比整数多一点点,ceil 就多给你一整列,而一整列在二维里就是一整列加一整行的面积。所以做预算的时候不能用连续公式,要用 m = 1+ceil((N-1)/(1-o)) 再平方。
四、为什么「切细更准」这句话有一半是对的
到这里必须承认一件事:说「切细更准」的人不是瞎说,他们在分辨率这一半上是对的。错的是把分辨率当成唯一变量,忽略了切块同时在做另一件事 —— 破坏结构。
上游也在按网格切:512 像素方块与 1568 长边
截至 2026-10-31,OpenAI 的图像输入文档在描述 detail: high 时给的是四个步骤:先把图缩进 2048×2048 的方框并保持长宽比(小图不放大);若最短边超过 768 像素,就把最短边缩到 768 像素、另一边向下取整;然后数覆盖这张图需要多少个 512 像素方块,每个方块按该模型的 tile tokens 计费;最后再加上该模型的 base tokens。文档里 gpt-4o / gpt-4.1 那一行写的是 base 85、tile 170。
这段话说明的是计费口径,不是内部实现,我按原文理解,不往外推。但它至少说明一件事:在「一张文档图值多少 token」这件事上,上游的态度就是固定尺寸的方格 + 每格固定单价,方格之间没有重叠这个概念。你在应用层怎么切,都改变不了「它自己也按格子在算」这一层。
Anthropic 那一侧的口径不同但结论相似。截至 2026-10-31,视觉文档给的是两条限制:一条是长边上限(标准档 1,568 像素,高分辨率档 2,576 像素),一条是视觉 token 预算(标准档 1,568),token 的算法是 ceil(w/28) * ceil(h/28);超过任一条就先缩,缩完再把右边和下边补到 28 的整数倍。
切细换来的清晰度是真的:整页会被缩到 0.74 倍
拿本文这页 1240×1754 套上面那条公式:切成 45×63 共 2835 个 patch,远超 1,568 的预算,缩放系数 sqrt(1568/2835) 约 0.7437,整页被缩到约 922×1304,再补到 924×1316。行高从 48 像素变成 35.7 像素,字号从 32 像素变成 23.8 像素。
这就是「切细更准」的真实来源:把同一页切成 3×3,每块 413×585,按公式算是 15×21 共 315 个 patch,远在预算之内,不会触发缩放,行高保持 48 像素。相对整页送进去,等效分辨率提高了约 1.34 倍。
所以那条直觉不是空穴来风,它只是漏掉了另一半账:清晰度从 0.74 倍回到 1.00 倍,代价是整行完整率从 1.00 掉到 0.00。把两个数放在一起看,净收益是负的 —— 除非你把切法换掉。
重叠按行高算:o ≥ h·N/H,而 10% 是个运气值
能复算的关系式不长,每一项都能在页面尺寸上指出来。设行高 h、块高 T、重叠比例 o,相邻两块的间距就是 s = T*(1-o)。一块内能「完整容纳」一行的纵向安全带,是块顶往下 T-h 那一段;两块的安全带之间会留一道缝,缝宽是
缝宽 = s – (T – h) = h – o*T
缝宽大于 0,就存在一行正好落在这道缝里、被两块各切一半。要让它对任何相位都成立,必须 o*T >= h,也就是 o >= h/T = h*N/H。这就是「重叠要按行高定」的算式:它不是按页面百分比定,也不是按块百分比拍脑袋,而是行高除以块高。
把这个式子代进本文的页面(h=48,H=1754):N=2 需要 5.47%,N=3 需要 8.21%,N=4 需要 10.95%。切得越细,同一个行高要求的重叠比例越高 —— 这就是「固定 10% 重叠」这个经验值在细切时失效的原因:它在 N=2 时有 1.8 倍余量,到 N=4 时余量已经是负的。
🧪 实测环境:Python 3.13.12 / macOS 27.0.1(arm64)/ 仅标准库(math);口径:与上一段同一页几何、独立重算,重叠细扫步长 0.001,相位扫描把首行顶距在 0~47 像素间平移。
#!/usr/bin/env python3
"""e02:重叠阈值的公式核对(同一页几何,独立重算,不依赖 e01 的变量)。
关系式: 相邻两块的「整行安全带」要接上,需 o*T >= h,即 o >= h*n/H。
"""
import math
W, H = 1240, 1754
TOP, LINE_H, N_LINES = 120, 48, 30
L, R = 100, 1140
def grid(n, o, top):
tw, th = W / n, H / n
sx, sy = tw * (1 – o), th * (1 – o)
mx = 1 + math.ceil((W – tw) / sx – 1e-9)
my = 1 + math.ceil((H – th) / sy – 1e-9)
return [min(i * sx, W – tw) for i in range(mx)], [min(j * sy, H – th) for j in range(my)]
def vert_rate(n, o, top):
xs, ys = grid(n, o, top)
th = H / n
boxes = [(t + top, t + top + LINE_H) for t in (i * LINE_H for i in range(N_LINES))]
hit = sum(1 for a, b in boxes if any(y <= a and b <= y + th for y in ys))
return hit / N_LINES
print("line_h=%d page_h=%d (公式 o* = h*n/H)" % (LINE_H, H))
print("%-2s %-8s %-8s %-8s %-8s" % ("n", "理论o*", "o*·T(px)", "实测o*", "实测o*·T"))
for n in (2, 3, 4):
t = H / n
theo = LINE_H * n / H
emp = next(o / 1000 for o in range(0, 301) if vert_rate(n, o / 1000, TOP) == 1.0)
print("%-2d %-8.4f %-8.1f %-8.4f %-8.1f" % (n, theo, theo * t, emp, emp * t))
print("\\n相位敏感性: n=4, o=0.10, 首页顶距 0..47 px 平移")
rates = [vert_rate(4, 0.10, t) for t in range(0, 48)]
print("min=%.3f max=%.3f 低于1.0的偏移数=%d/48" % (min(rates), max(rates), sum(1 for r in rates if r < 1.0)))
print("偏移0..11:", " ".join("%.3f" % r for r in rates[:12]))
print("\\n块数: 实测 vs 连续公式 (n/(1-o))^2")
print("%-2s %-5s %-6s %-8s %-7s" % ("n", "o", "实测", "公式", "实测/公式"))
for n in (2, 3, 4):
for o in (0.05, 0.10, 0.20, 0.30):
xs, ys = grid(n, o, TOP)
print("%-2d %-5.2f %-6d %-8.2f %-7.2f" % (n, o, len(xs) * len(ys), (n / (1 – o)) ** 2,
len(xs) * len(ys) / (n / (1 – o)) ** 2))
line_h=48 page_h=1754 (公式 o* = h*n/H)
n 理论o* o*·T(px) 实测o* 实测o*·T
2 0.0547 48.0 0.0430 37.7
3 0.0821 48.0 0.0560 32.7
4 0.1095 48.0 0.0700 30.7
相位敏感性: n=4, o=0.10, 首页顶距 0..47 px 平移
min=0.967 max=1.000 低于1.0的偏移数=12/48
偏移0..11: 1.000 1.000 1.000 1.000 1.000 1.000 1.000 0.967 0.967 0.967 0.967 1.000
块数: 实测 vs 连续公式 (n/(1-o))^2
n o 实测 公式 实测/公式
2 0.05 9 4.43 2.03
2 0.10 9 4.94 1.82
2 0.20 9 6.25 1.44
2 0.30 9 8.16 1.10
3 0.05 16 9.97 1.60
3 0.10 16 11.11 1.44
3 0.20 16 14.06 1.14
3 0.30 16 18.37 0.87
4 0.05 25 17.73 1.41
4 0.10 25 19.75 1.27
4 0.20 25 25.00 1.00
4 0.30 36 32.65 1.10
输出中间那三行就是阈值对照:理论值 5.47% / 8.21% / 10.95%,实测值 4.30% / 5.60% / 7.00%,两边换算成绝对像素分别是 48.0 对 37.7、48.0 对 32.7、48.0 对 30.7 —— 理论那一列恒等于行高 48,这不是巧合,是算式本身要求的安全带宽度。
实测阈值比公式低 1 到 4 个百分点,这个差不是公式错了,是两件事叠在一起:公式算的是「最坏相位下也保证成立」,实测扫的是「这一页、这个页边距下的最小值」;另外最后一行贴边对齐,末端的缝被压掉了一部分。做预算要按公式那个大的数取,按实测那个小的数取,等于把排版当成了不变量。
10% 到底够不够,看块高就知道了:N=4 时块高 438,10% 只有 43.9 像素,而安全带要求 48 像素,缝宽 48 – 43.9 = 4.1 像素。这 4.1 像素就是全部余量。上面那段输出的相位扫描把首行顶距在 0 到 47 像素之间平移,48 个相位里有 12 个掉到 0.967 —— 也就是说,同一份参数、同一页内容,只把上边距改几像素,就会出现一行被劈开。
「我们线上重叠设的 10%,跑下来没问题」,这句话的可信度大致等于「这次没有一行落在那 4 像素里」。它不是结论,是一次抽签。
这份资料是什么:文档解析链路的工程化清单,从切块、版面分析到结果合并的落地顺序,和本章讲的「切法怎么定」是同一件事的不同阶段。放在资料包里,扫码即可获取:

五、以后怎么定:三条可以写进配置的规矩
结论落到可执行的判断上,就三句话:能不竖切就不竖切;必须重叠时按行高算并向上取整到台阶;上线前把整行完整率做成回归项。
规则一:竖切之前,先问块宽能不能放下一个语义单元
对文档图像来说,语义单元是「一整行」,对表格来说是「一整行单元格」,对双栏排版来说是「一整栏」。块宽小于这个单元,重叠加多少都没用,因为重叠只能复制,不能扩容。
最省事的做法是横条切:保持整页宽度、只在纵向切。同样是 3 条,1240×585 的横条按 Anthropic 那条公式算是 45×21 共 945 个 patch,在 1,568 的预算内,不触发缩放;整行完整率直接是 1.00,因为块宽等于行宽。你唯一要处理的就剩纵向那一道缝,而那道缝有现成的算式。
| 单栏正文、行宽接近版心 | 整宽横条,2~4 条 | 块宽 = 行宽,整行完整率天然是 1.00,且不触发缩放 |
| 双栏论文 / 杂志 | 先按栏切,再在栏内横切 | 语义单元是「一栏宽」,块宽要 ≥ 栏宽,不能按页宽折算 |
| 跨页表格 | 按行组切,别按行切 | 表头要和数据同块,切在表头下面等于把每块都变成无表头数据 |
| 图注混排、公式多 | 网格切 + 大重叠,并接受重试 | 这类版面本来就没有稳定行结构,靠整行完整率救不回来,只能靠二次识别 |
规则二:重叠按行高算,并且向上取整到台阶
具体的算法是三步。先量行高 h(不是字号,是行盒高度,含行距);再按 o* = h*N/H 算出下限;然后把 o 往上取到让 m 刚好跨过台阶的值,因为块数是整数,中间那一段加了等于白加。
拿本文的页面举例:行高 48、页高 1754,N=3 时下限 8.21%,取 10% 就够(此时 m=4,块数 16);N=4 时下限 10.95%,10% 不够,要取到 12% 以上,而 12% 和 30% 落在同一个台阶里,块数都是 25 —— 所以直接取 12%,别取 30%。多出来的那 18 个百分点不会让任何一行变完整,只会让每块的 token 账单变厚。
这里有个反直觉的推论:重叠不是越大越安全,超过台阶就是纯浪费。判据是「再加一档,m 会不会变」,不变就是浪费。
规则三:把整行完整率做成上线前的回归项
这套判定最大的价值不在调参,在于它能当验收。把线上真实文档抽 50 页,跑一遍整行完整率,低于 0.99 就说明切法有问题,先改切法再谈模型;0.99 以上再去比较识别结果,否则你调的是模型,坏的是切块,两边互相甩锅。
判定阈值我给具体的:整行完整率低于 0.95,直接判定切法不可用,别进 A/B;0.95 到 0.99 之间,允许上线但要盯住「跨块行」的合并逻辑;0.99 以上,切块这一环可以从怀疑名单里划掉。另外两个数一起看:冗余面积比超过 2.0 就要问一句「这些重复的像素买到了什么」,块数超过预算的 1.5 倍就要回头查一次 m 的取整。
最后一句实话:这套数是在一页合成文档上跑出来的,行等距、无图无表、单栏。真实文档里行高会变、会有跨栏标题、会有嵌在正文里的公式,整行完整率一定比这里低。所以要做的不是把这几个数抄进配置,而是把这段脚本改成读你自己的 PDF → 渲染 → 切块 → 统计,跑一遍你手上的 50 页,用你自己的行高去算那个 o*。
这份资料是什么:视觉模型输入尺寸与 token 预算的对照整理,含各家长边上限、缩放规则与常见版面的估算示例,和本章「怎么按行高反推重叠」配合着看更省事。放在资料包里,扫码即可获取:

附表 A:本文引用事实与出处对照表
| 1 | detail: high / auto 的处理顺序:先缩进 2048×2048 方框并保持长宽比(小图不放大);最短边超过 768 像素则缩到 768、另一边向下取整;数覆盖所需的 512 像素方块,每块计 tile tokens;再加上 base tokens | OpenAI《Images and vision》指南,OpenAI 发布,https://developers.openai.com/docs/guides/images-vision | 第四章第一节 |
| 2 | 瓦片计费表中 gpt-4o、gpt-4.1 为 base 85 / tile 170 | 同上 | 第四章第一节 |
| 3 | detail: low 时无论尺寸只计 base tokens | 同上 | 第四章第一节 |
| 4 | 标准档最长边上限 1,568 像素、视觉 token 预算 1,568;高分辨率档为 2,576 像素 / 4,784 | Anthropic《Vision》文档,Anthropic 发布,https://platform.claude.com/docs/en/build-with-claude/vision | 第四章第一、二节 |
| 5 | 图像 token 成本为 ceil(w/28) * ceil(h/28);超出任一限制会先缩放;缩放后补到 28 的整数倍(示例 924×1307 补成 924×1316);文档给的例子是 1075×1520 的 A4 扫描件被缩到 924×1307 | Anthropic《How Claude resizes and pads images》,Anthropic 发布,https://platform.claude.com/docs/en/build-with-claude/vision-coordinates | 第四章第二节 |
| 6 | 本文全部实测读数:1240×1754 合成页面、行高 48、30 行,N∈{1,2,3,4} × o∈{0,0.05,0.10,0.20,0.30} 的块数、冗余面积比、整行完整率、纵向完整率 | 本文自测,脚本见 e01_tiling_grid.py 与 e02_overlap_budget.py,输出为脚本原始 stdout | 第三章、第四章第三节 |
写在最后:这篇用到的资料
写这篇文章时,把相关的官方文档和源码又翻了一遍,顺手也整理了几份配套的东西:
- 大模型学习路线图:从零基础到能自己动手做 Agent,按阶段说明每一步该学什么、哪些可以先跳过
- 《LangChain + LangGraph + MCP 智能体开发实战》视频课:7 个模块,从私有化部署、Embedding+RAG 到 MCP+Agent 全流程
- AI 大模型知识库(在线可查):Agent Skills 从入门到落地、Claude Skills 完全指南等专题,按目录浏览即可
- 640 套 AI 大模型行业报告 + 经典 PDF 书籍:看行业落地案例和别人怎么做的时候用得上
- 大模型零基础到精通教学视频:跟着敲一遍,比只读文档快得多
资料是我自己整理的,放在下面这个码上,扫码即可获取:






添加时备注「AI」,优先通过。
资料按「先路线、再动手、最后查漏」的顺序整理好了,建议先看学习路线那一份,照着它挑一条适合自己当前基础的路径再往下看。




