1.本文中提到的checkpoints,指的是需要审核的检查点。
2.区别于多数 VL 模型惯用的 PDF 转图片解析思路,本文未使用该方案。PDF 转图时的 DPI 调节会导致图像质量波动,内容密集的文件更易出现羽化、模糊现象。
你有没有遇到过这样的场景?
一份 50 页的采购合同,让 AI 检查“付款条款是否合规”,结果它漏看了第 37 页附件里的关键约定。
一份带签章的扫描件 PDF ,经过 OCR 以后表格结构乱掉,金额、日期、签章位置都变得不可靠。
一份嵌套多层附件的技术标书,向量检索召回了一堆看起来相似的段落,却迟迟找不到真正该看的那一页。
很多方案的第一反应,还是把 PDF 全部读进去,再把用户问题一起塞给模型,希望它一次性完成阅读、定位、判断和总结。
短文档这样做问题不大;一旦 PDF 变长,附件漏读、签章漏看、跨页表格断裂、封底证明材料被忽略,就会变成高频问题。
PDF Agent 真正要解决的,不是“让模型看 PDF”,而是建立一套稳定、可复用、可追溯的阅读流程。
在谈这套方案之前,先看几个常见路线的问题。
1. 一次性阅读:注意力和上下文都不稳定
一次性把整份 PDF 扔给模型,本质上是在赌模型能同时完成全局阅读、局部定位和业务判断。
常见结果是:
更麻烦的是,索引和判断被混在了一次调用里。只要检查点变了,前面的阅读结果就很难复用。
2. 纯 OCR :识别文字不等于阅读理解
扫描件 PDF 很容易让人把问题简化成 OCR :只要把图片里的字识别出来,后面交给模型就行。
但 OCR 本身没有阅读理解能力。它更像一个“字符转写器”,主要回答“页面上有哪些字”,很难稳定回答“这些字之间是什么关系”。
真正做审核时,麻烦往往出在关系上:
•表格行列、合并单元格、金额单位和备注的对应关系;所以 OCR 结果看起来有字,后续审核仍然可能出问题:金额和单位错位,签章页被当成普通图片,附件里的关键说明没有和正文关联起来。
PDF Agent 需要的不只是文字提取,而是把页面里的业务对象、版式线索、证据位置和上下文关系一起保留下来。
3. 向量检索:相似不等于相关
LlamaIndex 这类 RAG 框架很适合做知识库,但向量检索有一个天然问题:它召回的是语义相似内容,不一定是业务相关证据。
你要找“2024 年 Q4 的营收数据”,它可能召回“2023 年 Q4 的营收数据”,因为语义太像。
你要核验“合同是否完成签署”,它可能召回“签署条款”,但真正应该看的往往是签署页、盖章页、授权代表签字页,甚至附件里的确认函。
审核系统要的不是“最像的一段话”,而是“足以支持判断的一组页面”。
4. 多轮递归:准确率提升了,链路也变重了
PageIndex 这类文档树路线很有价值,它提醒我们:长文档不应该只按 token 切块,而应该先形成文档结构。
但如果每次查询都依赖多轮 Prompt 递归筛选,企业审核系统会遇到另一个问题:链路变长了。
一个查询可能触发多次模型调用。每一轮都有新的上下文和中间输出,随之带来几个生产问题:
所以我只吸收它的“先建地图”思想,不把整条审核主链路交给动态递归检索。
我把 PDF Agent 拆成两件事:先生成索引,再根据索引查询。
总流程是:
•第一轮: PDF 页面进入视觉模型,生成可复用的 doc + nodes;PDF → 获取物理页数 → 按页切chunk → Seed VL并行读取 → DeepSeek分层merge → 输出pdf-node-index.json
•第二轮:检查点进入计划器,根据索引选择页面,再合批执行判断;这样拆开以后, PDF 不再是每来一个问题就重读一遍。第一轮解决“内容在哪里”,第二轮解决“这个问题该看哪里、能不能下结论”。
这也是我没有直接做“一次性全文问答”的原因。企业审核不是聊天问答,它更像现场核验:先知道材料分布,再带着检查点去看证据。
第一轮不是审核,而是建地图。
它的目标只有一个:把 PDF 变成一份稳定的文档索引。这份索引不应该被具体用户问题污染,只回答三件事:
所以第一轮只保留两个核心对象:doc + nodes。
doc 描述整份文档,比如文件名、页数、文档类型和一句话概览。
nodes 描述文档结构,比如正文、附件、签署页、表格组、截图证明、付款申请、验收材料等。
每个 node 只需要有:title、pages、summary、children。
这里有一个关键取舍:第一轮不要输出审核结论,不要抽字段级 evidence 。
不要输出 anchors 、坐标框、完整表格行列、签章 OCR 、风险状态、规则命中结果。
这些东西会让索引绑定某一次具体审核任务,降低复用性。第一轮只做“哪里有什么”,第二轮才做“这个检查点应该看哪里,以及能不能得出判断”。
为什么索引不能被问题污染
把“建索引”和“审核判断”混在一次模型调用里,最大的问题是目标冲突。
建索引需要模型保持全局视角,尽量完整地描述文档结构;审核判断则需要模型围绕某个检查点聚焦,判断哪些证据相关、哪些证据无关。
如果把用户检查点提前塞进第一轮,模型很容易只关注当前问题相关的页面,忽略其他附件、表格、签署页和视觉证明材料。
这样生成的索引就不是文档地图,而是一份被问题过滤过的临时阅读笔记。下次换个规则,就得重读。
长 PDF 怎么建索引
第一轮不依赖传统 extract_text,也不按 token 切分(这是pageIndex的做法)。
因为很多 PDF 的关键内容并不只是文本,尤其是合同、付款申请、验收单、报价单、证明截图、签章页、扫描件。
更合理的方式是按物理页切 packet ,让视觉模型直接看页面。
短 PDF 可以一次性读完,比如 10 页以内直接生成完整 doc + nodes。长 PDF 则按 15 页左右切分,每个 chunk 只向前 overlap 少量页面,用来理解跨页延续关系。
每个 chunk 并行交给视觉模型读取,得到局部 nodes 。
然后再用文本模型做 merge ,把重叠页导致的重复节点合并,把跨 chunk 延续的章节拼起来,最终得到一份干净的全局索引。
这里的 merge 不是供应商 Batch API ,而是工程侧 map-reduce 。
比如先把 C1-C5 、 C6-C10 、 C11-C15 分别合并成局部结果,再把局部结果合成 Final 。
这样可以避免一次 merge 输入过大,也能降低长文档下的上下文压力。
node 的粒度要克制
PDF 索引最容易犯的错,是把每一页都拆成一个 node ,或者把每个条款、每个编号、每个列表项都拆出来。
这会让索引看起来很细,但第二轮反而更难用。
好的 node 应该是语义内容块,而不是页面块。
比如:合同正文、合同附件、签署页、付款申请材料、验收证明、技术要求、安全环保协议、装箱清单。
children 最多保留一层。
清晰的一级附件、一级章节、一级表单组可以成为 children 。
但是正文里的编号条款、列表项、小标题,不默认拆 children ,而是写进最近 node 的 summary 。
这样索引足够轻,第二轮 Plan 仍然能通过 summary 找到相关页。
第二轮才开始带着问题阅读。
它不再从整份 PDF 里盲读,而是拿着第一轮生成的索引,先判断每个检查点应该看哪些页面,再让视觉模型精读这些页面。
第二轮分三步:Plan -> Batch -> Execution。
Plan 阶段只看第一轮 index 和用户 checkpoints ,不读 PDF 。
它的任务是判断每个检查点应该看哪些物理页。
例如, CP1 需要核对签章,就选择签署页; CP2 需要核对付款条款,就选择前附表、正文付款条款和相关附件; CP3 需要核对交付验收,就选择交付、验收、收货证明等页面。
Plan 不输出结论,不抽 evidence ,只输出 checkpoint id、page ranges 和 reason。
然后进入 Batch。
Batch 是工程侧做的动态规划和装箱,把多个检查点合并成尽量接近 20 页的执行组。
这里要强调一点:合批只是为了减少视觉模型调用次数,不代表检查点共享证据池。
Execution 阶段虽然可以把多个 checkpoint 放进同一个 page packet ,但每个 checkpoint 只能使用自己的 allowedRanges。
不能拿 CP2 的页面去回答 CP1 。
这就是“物理合批,逻辑隔离”。
Execution 输出要短
很多审核系统喜欢让模型输出复杂 JSON :结论、依据、风险、证据、页码、不足、建议,全都想要。
但在这个架构里, Execution 不需要这么重。
因为页码、分组、检查点和允许页面范围,工程侧已经保存了。
Execution 只需要按 checkpoint id 输出简短结论。
例如, CP1 只输出“合同签署页可见双方盖章,并有授权代表签字”; CP2 只输出“合同包含付款、发票和验收相关约定,前附表列明支付方式,正文包含价格支付和验收条款”。
如果页面不足,就直接说页面不足。
不要再输出复杂 evidence 结构。
这会让结果更短,延迟更低,也更不容易格式漂移。
这套方案还要考虑 Prompt 的缓存友好性。
当前很多 Prompt 会在开头插入动态变量,比如 document name 、 total pages 、 current chunk pages 、 parent node 、 checkpoint list 、 index JSON 。
这会破坏 KV-cache 。
因为只要文档名、页码、 chunk 范围变化, Prompt 前半段就变了,模型很难复用前面的静态规则缓存。
更好的写法是:静态角色和规则放在前面,动态输入统一放在最后,不同调用之间尽量保持前缀一致。
动态变量包括 documentName 、 totalPages 、 chunk page range 、 primary page range 、 parentNode 、 attentionBrief 、 index 、 checkpoints 、 compactChunkResultsJson 、 execution group 、 allowedRanges 。
这些都应该尽量放在 Prompt 尾部。
如果某个变量不存在,也不要让 Prompt 中间结构大幅变化。可以在 Runtime Inputs 里标记为 null 或 not provided 。
这样才能让同类任务共享更长的静态前缀,提高缓存命中率。
在落地前,我对比过 LlamaIndex 、 PageIndex 、 Docling 这几条成熟路线。它们都有价值,但解决的问题层级不同。
如果放到“PDF 审核”这个场景里,可以这样看:
| | | | |
|---|
| 通用 RAG 框架,强在多数据源接入、索引、检索、生成 | | 文档解析工具,强在 PDF/Office/扫描件结构化转换 | 面向 PDF 审核的阅读流程,负责索引、选页、合批、隔离、执行 |
| | | | |
| | | | 合同、付款申请、验收材料、签章页等 PDF 附件审核 |
| 向量检索解决的是“语义相似”,不是“证据相关”;切块后跨页表格、签章页、附件关系容易断 | Prompt 调用次数多,章节越多、递归越深,调用越多;每一轮中间输出都增加审核、延迟和成本压力 | 定位在解析/转换层,业务检查点选页、合批、判断、追溯需要上层系统实现 | 不依赖一次性读全文,也不依赖向量相似召回,用页面索引和 allowedRanges 控制证据边界 |
| 可能召回“签署条款”,但漏掉真正要看的签署页、盖章页、授权代表签字页 | 能沿结构找内容,但每次查询路径动态变化,结果复现和审计更难 | 能提取文本、表格、 OCR ,但证据链编排需要上层审核流程 | 直接按物理页让视觉模型读页面,保留签章、截图、表格、扫描件等线索 |
| 依赖分块、向量库和解析工具,长单文档整体逻辑容易被切碎 | | | page packet 并行读取,再 map-reduce merge ,避免一次吃完整份 PDF |
| 检索阶段通常不调用 LLM ,但最终生成仍依赖一次或多次调用;复杂链路要自行编排 | 多轮 Prompt 调用是核心机制,成本和延迟随文档复杂度上升 | | Index 、 Plan 、 Batch 、 Execution 拆开,调用边界固定 |
| | | | 每个 checkpoint 有 allowedRanges ,物理合批但逻辑隔离 |
| | | | 主链路自控,页面选择、证据边界、审计追溯和成本控制都可设计 |
前面讲的是架构,最后落到 Prompt 。这里的 Prompt 是把流程边界写清楚:第一轮只建地图,第二轮才带问题读;可以物理合批,但不能共享证据;动态变量尽量后置,方便 KV-cache 复用。
下面是一组更适合 KV-cache 的 Prompt 模板。
注意:这里列的是模板类型,不是一次处理里固定调用 6 次 Prompt 。
第一轮也不是固定调用 4 次。
它有两条路径:
短 PDF :System Prompt + Single-Packet Index Prompt, 1 次模型调用,直接生成 doc + nodes。
长 PDF :System Prompt + Chunk Extraction Prompt 按 page packet 调用 N 次,生成局部 nodes ;DeepSeek Layer Merge Prompt 按 merge 层级调用 M 次,合成全局 doc + nodes。
也就是说,第一轮实际调用次数是:短文档 1 次,或长文档 N 次 chunk extraction + M 次 merge 。 System Prompt 是随每次模型调用一起使用的角色约束,不是单独一次业务调用。
1. 第一轮 System Prompt
You are a PDF document structure indexer.Your job is to build a lightweight PageIndex-style node tree for PDF documents.Return only one valid JSON object.Do not output Markdown, comments, or explanation.Do not include hidden reasoning in the final answer.The output must be a concise document map.It must not contain audit conclusions, compliance judgments, risk labels, evidence sections, coordinates, full table extraction, or field-level OCR details.2. 第一轮长文档路径: Chunk Extraction Prompt
Your job is to extract a lightweight hierarchical node structure from a PDF page packet.- A node is a semantic section, attachment group, form group, table group, screenshot/proof group, signing section, or other coherent content block.- Prefer the original visible title if there is one.- If there is no visible title, use a short neutral business title.- pages must use original physical PDF page numbers, not packet-local page numbers.- Use overlap pages only to understand content that continues into the primary pages.- Do not create standalone nodes that belong only to overlap pages before the primary range.- Do not split every page into a node unless every page is clearly a separate document or item.- If a node appears to continue beyond the visible pages, mention this naturally in summary.- If a node contains clear first-level subtitles or first-level subsections within this packet, include those direct children.- Output at most one level of children.- Do not output grandchildren or deeper nested children.- Do not expand tiny paragraphs, list items, every numbered clause, or every bullet into nodes.- If deeper subsection structure exists, mention important deeper subsection titles, numbered clause topics, or lower-level headings in the nearest node summary.- Do not represent non-contiguous content as one continuous pages range.- If a reference page and the actual referenced booklet or content are separated by unrelated pages, return two separate nodes.- A node.pages range must not skip across unrelated independent sections or attachments.- Mention important visible business objects naturally in summary, such as tables, images, screenshot groups, seals, signatures, QR codes, dates, amounts, codes, names, and key forms.- For tables, mention the table name and visible header or example row clues when useful.- For signing pages, mention visible seals and signatures for each party separately when visible.- Do not extract full table rows, signature OCR, image details, coordinates, or detailed field values.- If an attention brief is provided, use it only to preserve relevant location clues in summaries.- The attention brief is not an audit task.- Do not judge compliance.- Do not output risks or conclusions. "pages": [<start physical page>, <end physical page>], "summary": "<what this node is about; mention important visible business clues naturally>",- documentName: {{documentName}}- totalOriginalPdfPages: {{totalPages}}- packetOriginalPageRange: {{readPages}}- primaryResponsibilityPageRange: {{primaryPages}}- overlapPolicy: pages before the primary range are overlap context from the previous packet only- parentNode: {{parentNodeOrNull}}- attentionBrief: {{attentionBriefOrNull}}Directly return the final JSON object. Do not output anything else.3. 第一轮短文档路径: Single-Packet Index Prompt
Your job is to build a complete lightweight PageIndex-style index for the entire PDF.- Return a clean document summary and a concise node tree.- A node is a semantic section, attachment group, form group, table group, screenshot/proof group, signing section, or other coherent content block.- Prefer the original visible title if there is one.- If there is no visible title, use a short neutral business title.- pages must use original physical PDF page numbers.- Do not split every page into a node unless every page is clearly a separate document or item.- If a node contains clear first-level subtitles or first-level subsections, include those direct children.- Output at most one level of children.- Do not output grandchildren or deeper nested children.- Do not expand tiny paragraphs, list items, every numbered clause, or every bullet into nodes.- If deeper subsection structure exists, mention important deeper subsection titles, numbered clause topics, or lower-level headings in the nearest node summary.- Do not represent non-contiguous content as one continuous pages range.- If a reference page and the actual referenced booklet or content are separated by unrelated pages, return two separate nodes.- Mention important visible business objects naturally in summary, such as tables, images, screenshot groups, seals, signatures, QR codes, dates, amounts, codes, names, and key forms.- For tables, mention the table name and visible header or example row clues when useful.- For signing pages, mention visible seals and signatures for each party separately when visible.- Do not extract full table rows, signature OCR, image details, coordinates, or detailed field values.- If an attention brief is provided, use it only to preserve relevant location clues in summaries.- The attention brief is not an audit task.- Do not judge compliance.- Do not output risks or conclusions. "name": "<document name>", "type": "<document type if visible>", "summary": "<one sentence document summary>" "pages": [<start physical page>, <end physical page>], "summary": "<what this node is about; mention important visible business clues naturally>",- documentName: {{documentName}}- totalOriginalPdfPages: {{totalPages}}- packetScope: the attached PDF packet contains the entire original PDF in order- attentionBrief: {{attentionBriefOrNull}}Directly return the final JSON object. Do not output anything else.4. 第一轮长文档路径: DeepSeek Layer Merge Prompt
You are merging node trees extracted from overlapping PDF page packets.- Return one clean PageIndex-style node tree for the whole merge range.- Do not keep chunk boundaries.- Merge duplicate or clearly continuing nodes across packet boundaries.- Be conservative: if you are not sure two nodes are the same continuing content, keep them separate.- Do not invent detailed field values.- Reconstruct parent-child hierarchy during merge.- If a node is a clear first-level subtitle or subsection of a broader node, put it under the broader node's children.- If a node's pages are contained by a broader node and its summary is subordinate to the broader topic, use children instead of leaving it as a separate top-level node.- Do not leave child-level subsections as top-level nodes when a clear parent can be identified.- Output at most one level of children.- Do not output grandchildren or deeper nested children.- Do not expand tiny paragraphs, list items, every numbered clause, or every bullet into nodes.- If deeper structure is visible, summarize it in the nearest node summary instead of creating deeper children.- Do not represent non-contiguous content as one continuous pages range.- If a reference page and the actual referenced booklet or content are separated by unrelated pages, keep them as separate nodes.- Never expand a node pages range across unrelated independent sections or numbered attachments.- Preserve important visible business objects in node summaries, such as tables, images, screenshot groups, seals, signatures, QR codes, dates, amounts, codes, names, and key forms.- For tables, preserve the table name and visible header or example row clues when useful.- For signing pages, preserve visible seals and signatures for each party separately when visible.- Do not merge adjacent tables with different names, headers, or business purposes.- Do not attach seal or signature pages to unrelated preceding text.- Only merge signing pages when they clearly belong to the same signing or approval section.- If a node is too broad and clear subnodes are visible from the provided node list, place them under children.- If two nodes are adjacent but discuss different business content, keep them separate.- A child node's pages must stay within its parent node's pages.- pages must use original physical PDF page numbers. "name": "<document name>", "type": "<document type if visible>", "summary": "<one sentence document summary>"- documentName: {{documentName}}- totalOriginalPdfPages: {{totalPages}}- wholeMergePageRange: {{pageRange}}- outputDoc: {{outputDoc}}- parentNode: {{parentNodeOrNull}}- extractedPacketNodeLists: {{compactChunkResultsJson}}Directly return the final JSON object. Do not output anything else.5. 第二轮计划阶段: Plan Prompt
You are planning focused PDF reading tasks from a prebuilt PDF node index.Your only job is to decide which original physical PDF pages should be read for each checkpoint.- Use only the provided PDF index.- Do not read or infer from the original PDF.- Do not output audit conclusions.- Do not extract evidence.- Do not judge compliance.- Do not group checkpoints.- For each checkpoint, select the smallest sufficient page ranges based on the index.- Include signing pages, attachment pages, table pages, proof pages, or context pages when the checkpoint reasonably needs them.- If the index is insufficient to identify relevant pages, choose the most likely pages and state the uncertainty in reason.- Page ranges must use original physical PDF page numbers.- Keep reasons short and reviewable. "ranges": [[<start page>, <end page>]], "reason": "<why these pages should be read>"- pdfNodeIndex: {{pdfNodeIndexJson}}- checkpoints: {{checkpointListJson}}Directly return the final JSON object. Do not output anything else.6. 第二轮执行阶段: Execution Prompt
You are processing a batched PDF execution group.The batch is only for reducing model calls.Each checkpoint is logically isolated.- For each checkpoint, use only its allowedRanges.- Do not use pages assigned only to other checkpoints.- Do not borrow evidence across checkpoints.- Do not output page numbers.- Do not output evidence sections.- Do not output insufficiency sections.- Output one concise conclusion paragraph for each checkpoint id.- If the allowed pages are insufficient, state that directly in the conclusion.- Keep the answer short and business-readable.- Preserve the original checkpoint ids exactly.<one concise conclusion paragraph><one concise conclusion paragraph>- executionGroupId: {{groupId}}- groupInputPageRanges: {{inputRanges}}- checkpoints: {{checkpointsWithAllowedRanges}}- allowedRangesByCheckpoint: {{allowedRangesByCheckpoint}}Directly return the final answer. Do not output anything else.回到开头的问题: PDF Agent 的核心不是把模型调用堆上去,而是把阅读过程拆清楚。
第一轮不急着判断,第二轮不盲目重读, Execution 不输出过度复杂的结构, Prompt 不把动态变量塞在前面破坏缓存。
当 PDF Agent 从“一次性读全文”变成“先建地图,再带问题阅读”,系统才会真正稳定下来。