- 用
/augment/text-parser把 PDF 的文本抽出来 - 用 JSON schema 描述我们想要的记录
- 抽取它,让 schema 是强制约束而不是建议
- 处理那种根本没有可抽文本的文件
- 比较两条路线对同一页得到的结果
准备工作
你需要 Python 3.9 或更高版本、requests 包,以及一个 Venice API key。如果你还没有,请参见生成 API Key。
extract.py:
AUTH 和 JSON_HEADERS 是分开的。parser 接收 multipart 上传,如果你自己在 multipart 请求上设置了 Content-Type,requests 就不会再帮你加 boundary,然后失败的方式会让你很难排查。
1. 把文本抽出来
/augment/text-parser 接收 PDF、DOCX、XLSX 或纯文本文件(最大 25 MB),返回文本以及 token 数。文档在内存中处理,内容不会被保留。
tokens 计数才是真正有用的部分。它告诉你这份文档在下一次请求中会花你多少 token,可以在你发出请求之前就知道,这一点很重要,因为一份很长的 PDF 很容易超出你原本打算的预算。
2. 描述你想要的记录
请求模型返回 JSON,你拿到的 JSON 大致是你要求的形状。传一个 schema 进去,你拿到的 JSON 就会匹配它,因为 schema 是在约束生成,而不是建议它这样生成。additionalProperties: False 每一层都值得设。不设的话,模型一旦发现什么有意思的东西就可能加一个你从没打算要的 key,而读取这个结果的代码根本没准备好应付它。
3. 抽取
一次调用,response_format 带上 schema,把 strict 打开:
200 到达:
affiliation 是必填的,所以模型返回了一个空字符串,而不是把它省掉。这就是 schema 在严格按你告诉它的方式做事。
空字符串和缺失值是两件不同的事,而
required 把它们抹平了。如果你需要把”文档没这么说”和”文档在这里什么也没说”区分开,就把字段类型写成 {"type": ["string", "null"]},并在 system prompt 里要求返回 null。strict 模式接受这种联合类型,你就能拿到 null 而不是 ""。关掉思考
在那个请求里,disable_thinking 是最值得争论的一行,所以下面是论据。默认的文本模型在给出答案之前会先推理,而推理和 JSON 是从同一个 completion 预算里扣的。同样的抽取跑四次,看看模型都花了多少:
提高预算并不能解决第一个问题,只是把模型被允许触到的天花板抬高了。花掉 4003 个 token 那次运行返回时
finish_reason 为 length,内容是空字符串。
关掉思考让这次抽取便宜了五倍,更有用的是让它每次都一样。schema 已经在做本来推理才做的事——决定答案的形状。
4. 当没有文本可抽的时候
由扫描仪产出的 PDF 里装的是页面的图片,不是文本。文件名不会告诉你,文件大小也不会露馅。 你不用自己去检测,因为 parser 会替你检测:400 返回,而这与其说是失败,不如说是一个路由信号。文本路线对这个文件不可用,那就走另一条:把页面渲染成图像,让一个能看的模型来看它。
5. 两条路线的分歧
对同一张首页跑两条路线,返回的记录几乎一样。“几乎”才是有意思的部分:
文本路线保留了 Ł。视觉路线返回了 ASCII 的 L,因为它在读字形而不是字符编码,而变音符是个小小的视觉细节,很难幸存下来。如果你要用抽出来的名字去数据库里做匹配,这个差别决定了记录能不能被找到。
第八位作者的问题更重要。这一页没有为 Illia Polosukhin 写任何单位,文本路线每次都忠实地把它报告为空字符串。视觉路线在某些运行里,会用同一页上一个看起来合理的邻居来把这个字段填上。读像素给推断留出的空间,比读字符要多,而一个必填字段则是让它去填的邀请。当你没法用人工检查输出时,这就是一个在文档提供文本的地方优先走文本路线的理由。
成本的差距比看起来更小。两边都关掉思考之后,这两条路线在这一页上跑的 prompt 大小差不多:
那张图片是 923,732 个字符的 base64,但这部分你不用付费。图像是按尺寸计 token,而不是按编码后长度,所以一张很大的 PNG 并不会像它看起来那么贵。
当文档里有文本时,优先选用文本解析。它能保留精确的字符,超越第一页也不额外花钱,还不在乎页面是怎么排版的。只有当 parser 说没有东西可读、或者语义体现在版式上(比如图表、印章、签名)时,才伸手去拿视觉方案。
抽取别的东西
上面没有任何东西是论文特有的。换个 schema 和 system prompt,这条流水线就能抽取发票:description 字段是在真正干活的。日期只有在你说明想要哪种格式后才不再有歧义,03/04/2026 根据写的人是谁可以指两种不同的日期。
下一步
- 用
pydantic或jsonschema对结果按 schema 做校验,让一条格式错误的记录在边界处就失败,而不是在往后三层函数里才炸。 - 用嵌入把抽出的文本存起来,实现跨文档搜索,而不是每次都重新抽取。
- 当你想要的是答案而不是记录时,用文件输入把文档直接挂到一次 chat completion 里。
- 把这个抽取器当作工具交给一个智能体,参见使用函数调用构建能使用工具的智能体。
文档处理
text-parser endpoint 的参考文档。
结构化响应
json_schema 如何约束一次 completion。
视觉
向 chat 模型发送图像。
文件输入
附带一个文档,而不必自己解析它。