PixelRAG 返回的页面截图和答案对不上原文,通常不是模型读错,而是三步中的某一步把输入换掉了:索引元数据里页码与图像文件名错位、渲染分辨率不足导致小字没被识别、重排把原本命中的结果挤出 top-k。排查顺序建议先查映射,再查渲染,最后查重排——前两步的偏差会一路传到重排,先把后面的参数调好也没有意义。
先核对索引元数据中页码、page_index 与图像文件名是否一一对应,这是最基础也最容易出现的错位;再用同一组查询、只提高一档渲染 DPI 重跑,比对返回的页面是否变化;最后把检索与重排两个阶段的 top-k 顺序分别打印出来。每轮只改一个变量并留日志,才能知道是哪一步改变了结果。DPI 是否影响命中与版式、扫描质量有关,需要结合环境确认。
在索引元数据里核对页码与图像文件名的映射
页码错位时,检索到的图块本身可能没错,只是被挂到了错误的页上,表现就是“检索像对的、截图是别处”。先把索引元数据读出来,人眼扫一遍。
import json
def load_meta(path):
with open(path, encoding='utf-8') as f:
for line in f:
yield json.loads(line)
for rec in load_meta('index/pages.jsonl'):
print(rec.get('doc_id'),
rec.get('page_index'),
rec.get('page_number'),
rec.get('image_path'))
核对这几类字段:doc_id 是否属于同一文档;page_index 与 page_number 是否差 1(渲染器常用 0 基,业务页码常用 1 基);image_path 文件名中嵌入的页码是否与 page_number 一致;chunk_id 能否反查到同一页。发现错位时,建议按原文页码重新生成 image_path 与元数据后重建索引,不建议只在答案侧做页码加减补偿,那样一旦换文档或换渲染器又会错位。
把渲染 DPI 提高一档,重跑同一组查询看返回是否变化
DPI 的修改位置一般在渲染配置或转图命令里,例如渲染配置中的 render.dpi,或 pdf 转图工具的 -r 参数。改之前先固定查询集,挑几条确定对不上原文的 query,记下当前返回的页面与图块。
# 只改渲染分辨率,其余参数保持一致
python render_pages.py `--input` docA.pdf `--dpi` 144 `--out` pages/dpi144/
python render_pages.py `--input` docA.pdf `--dpi` 216 `--out` pages/dpi216/
python run_query.py `--queries` q17,q42 `--index` pages/dpi144 `--out` runs/dpi144.jsonl
python run_query.py `--queries` q17,q42 `--index` pages/dpi216 `--out` runs/dpi216.jsonl
两次结果按 query 对齐后逐条记录:命中的 chunk_id、页码、分数,以及人工看截图是否与原文一致。描述变化时只写观察到的事实,例如“q17 的 top-1 从第 7 页变为第 12 页”,或“两条查询返回未变”,不要直接写成提高 DPI 提升了准确率。提高渲染 DPI 通常会改善小字识别,但是否真的改变命中,取决于页面版式和整条渲染链路,需要结合环境确认;同时分辨率上升会带来索引体积与耗时的变化,这属于代价,不是收益。
打印重排前后的 top-k 顺序,确认最终结果来自哪一步
结果对不上,也可能是真的检索到了,只是重排后顺序变了。要在流水线里把两个阶段的 top-k 都打出来,按行记录。
{'query_id':'q17','stage':'retrieve','rank':1,'chunk_id':'c03','page':7,'score':0.61}
{'query_id':'q17','stage':'rerank','rank':1,'chunk_id':'c11','page':12,'score':0.42}
每条至少保留五个字段:query_id、stage(retrieve 或 rerank)、rank(该阶段排名)、chunk_id、score(该阶段打分)。再补两个派生字段便于比对:原始排名(retrieve 阶段的 rank)和重排后排名(rerank 阶段的 rank)。对比时以 chunk_id 为主键对齐两张表,看三件事:哪些图块在重排后新进 top-k,哪些从 top-k 掉出,同一图块的排名移动了几位。如果答案引用的 chunk 在 retrieve 阶段就不在前列,问题在召回或渲染;如果它在 retrieve 里靠前、重排后掉出去,那更可能是重排顺序的问题,可以先关掉重排重跑同一组查询验证。
对小字号表格裁剪后单独提问,确认是否渲染导致的漏检
如果检索本身就没有命中,而目标内容是小字号表格、页脚或密集公式,怀疑点就落到渲染质量上。做法是把该区域按坐标裁出来单独提问,绕开整页缩放。
from PIL import Image
im = Image.open('pages/dpi216/docA_p7.png')
# bbox 可用版面分析结果,也可人工量取
box = (120, 640, 980, 900)
im.crop(box).save('crops/docA_p7_table.png')
# 再对裁剪图单独走一次提问流程
python ask_image.py `--image` crops/docA_p7_table.png `--query` q17
记录裁剪前后的差异:整页提问时是否有命中、命中页码是否正确、答案是否引用了错误区域;裁剪后是否能命中同一 chunk、答案是否覆盖表格内容。漏检常见两种表现,一是整页检索时该页完全不出现,二是出现了该页但抓到的是同页的其他区域。前一种更像缩放后文字不可读,后一种更像版面切分把区域划错了,后续处理方向不同,记录时要分开写。
固定其余变量逐项复跑,记录哪一步改动真正改变了结果
到这里不要一次改多个配置,把每轮复跑都压成单变量对照,结论才能重复出来。
- 第 0 轮:基线,原始 DPI + 开启重排 + 原始元数据,输出 r0_baseline.jsonl
- 第 1 轮:只改渲染 DPI(144 提到 216),其余不动
- 第 2 轮:DPI 回到 144,只关掉重排
- 第 3 轮:只重建索引元数据,修正页码映射
python run_query.py `--queries` q17,q42 `--dpi` 144 `--rerank` on `--meta` v1 `--out` runs/r0_baseline.jsonl
python run_query.py `--queries` q17,q42 `--dpi` 216 `--rerank` on `--meta` v1 `--out` runs/r1_dpi216.jsonl
python run_query.py `--queries` q17,q42 `--dpi` 144 `--rerank` off `--meta` v1 `--out` runs/r2_norerank.jsonl
python run_query.py `--queries` q17,q42 `--dpi` 144 `--rerank` on `--meta` v2 `--out` runs/r3_meta.jsonl
归因时把各轮输出按 query_id 与 chunk_id 对齐,逐个问题回答:这一轮改动后,返回的页码和顺序有没有变。哪一轮的变化与“对不上原文”消失同时发生,就先认定这一步是主因,再换第二组查询复跑一次看是否稳定。如果几轮结果都没变,说明当前样本触及不到这个变量,应该换更难的小字或跨页样本再试,而不是继续叠加配置。