1. 链路拆解:LiteParse 粗读与 VLM 精读在 LlamaIndex 中的边界
LlamaIndex 的 just-in-time Agentic OCR 把文档处理拆成 LiteParse 粗读与 VLM 精读两遍;在配置 VLMPredictor 之前,先到 TaoToken 官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=llamaindex_ocr_intro 获取 Key,并把 Base URL 设为 https://taotoken.net/api。很多文档问答项目一开始会把 PDF、DOCX、图片全部送进视觉模型,结果索引构建慢,成本随页数上涨,而检索本身并不需要每一页都做到像素级 OCR。两遍式思路更接近工程上的分层:第一遍用 LiteParse 这类低成本解析器,把全部文件粗读成可检索文本;第二遍只把检索命中的相关页面交给 VLM 做 OCR,补回表格、印章、公式、手写或扫描质量差的区域。
从链路拆解视角看,关键不是“选哪个模型”这么简单,而是把粗读结果、页码定位、检索命中、精读回写、缓存与失败重试串起来,并且让 VLM 客户端稳定指向 TaoToken 的兼容接口。本文按可复现目标来写:先给出两段式调用链图,再拆第一遍和第二遍的配置片段,最后把 VLMPredictor、Claude Code、Codex、CC Switch 的配置边界讲清楚。你能拿到的不是概念图,而是一条可以落到本地脚本和工具配置里的路径。
两段式调用链可以先用文本图表示:
文件集(PDF/扫描件/图片) -> 第一遍:LiteParse 粗读全量页面 -> 产出:doc_id、page_no、raw_text、source_path、hash -> 建索引:文本索引/向量索引/关键词索引 -> 用户查询 -> 检索:返回命中的 page_id 列表 -> 去重与排序:按页码、分数、文档聚合 -> 第二遍:仅对命中页面调用 VLM OCR -> 产出:refined_text、table_json、layout_notes -> 回写合并:粗文本 + 精读文本 -> 生成回答或二次检索这个链路里,LiteParse 负责“广撒网”,VLM 负责“精修”。如果第一遍没有保存页码,第二遍就不知道要渲染哪一页;如果第二遍没有缓存,每次查询都会重复 OCR 同一页;如果 VLM 的 Base URL 和 Key 配置错,第一遍再便宜也跑不到最终答案。因此先把 TaoToken 的 Key 和 Base URL 准备好,再写 LlamaIndex 侧配置,是最省排障时间的顺序。
2. 第一遍 LiteParse 粗读:全量文件如何变成可检索的页码索引
第一遍的目标不是“完美还原文档”,而是“让检索能工作”。LiteParse 适合处理文本型 PDF、可提取文字的 DOCX、部分版面规整的扫描件。它输出得快,成本低,适合全量跑一遍。你需要强制保留几个字段,否则第二遍会缺定位信息:
doc_id:文档唯一标识,建议用文件内容哈希,避免同名文件覆盖。page_no:页码,统一从 1 开始,后续渲染和回写都用它。raw_text:粗读文本,用于全文检索和向量化。source_path:原始文件路径,第二遍渲染页面时要用。page_hash:页面图像或页面文本的哈希,用于缓存 VLM OCR 结果。parser_name:例如liteparse,方便排查不同解析器差异。
一个简化后的粗读入库片段可以这样写。实际导入名请按你安装的 LiteParse SDK 调整,重点看字段结构:
import hashlib from pathlib import Path from typing import Iterable def file_hash(path: Path) -> str: h = hashlib.sha256() with path.open("rb") as f: for chunk in iter(lambda: f.read(1024 * 1024), b""): h.update(chunk) return h.hexdigest() def page_hash(doc_id: str, page_no: int, text: str) -> str: raw = f"{doc_id}:{page_no}:{text[:2000]}".encode("utf-8") return hashlib.sha256(raw).hexdigest() def build_coarse_pages(paths: Iterable[str]): pages = [] for p in paths: path = Path(p) doc_id = file_hash(path) # 这里替换为你的 LiteParse 解析调用。 # 目标是拿到 per-page 文本,而不是只拿整篇文本。 parsed_pages = liteparse_to_pages(str(path)) for item in parsed_pages: page_no = int(item["page_no"]) raw_text = item.get("text", "") pages.append({ "doc_id": doc_id, "page_no": page_no, "raw_text": raw_text, "source_path": str(path), "page_hash": page_hash(doc_id, page_no, raw_text), "parser_name": "liteparse", }) return pages入库后,检索层可以同时支持关键词和向量。对于两遍式 OCR,关键词检索很重要,因为合同编号、发票号、条款号、表格标题往往靠精确匹配命中。向量检索适合语义召回。你可以把raw_text切成 page 级 chunk,也可以再做 512 到 1024 token 的细分,但建议保留page_no元数据。命中后,第二遍只需要拿到一个去重后的页码集合。
这里有一个容易忽略的点:第一遍粗读不要过早丢弃低质量页面。扫描页可能文字很少,但检索分数低并不代表它不重要。更稳妥的做法是把低文本密度页面也保留,在检索阶段用“文档级召回 + 页面级排序”处理。例如先按文档召回,再把该文档内文本密度低、包含表格关键词、包含签章位置的页面加入候选。这样第二遍 VLM 才不会漏掉关键页。
粗读完成后,建议做一次质量统计:每页字符数、是否包含乱码、是否包含表格符号、是否几乎为空。它可以作为第二遍的触发条件。比如:
def should_refine(page: dict, retrieved_score: float) -> bool: text = page.get("raw_text", "") if retrieved_score < 0.65: return False if len(text.strip()) < 80: return True if any(k in text for k in ["表", "金额", "合计", "签字", "盖章", "附件"]): return True if "�" in text or "□" in text: return True return False这段逻辑不是固定规则,但它体现了两遍式的成本控制思想:VLM 不是默认全开,而是被检索和页面质量触发。此时第一遍的 LiteParse 已经完成全量覆盖,第二遍只做“just-in-time”的精读补充。
3. 第二遍 VLM 精读:只对命中页面调用 OCR 的路由与提示词
第二遍的入口不是文件,而是page_id列表。你要做四件事:去重、排序、限流、渲染。去重按doc_id + page_no,排序按检索分数和页码顺序,限流用最大页数上限,渲染把页面转成图像或可被视觉模型读取的输入。不要直接把整份 PDF 再次传给 VLM,否则又回到全量 OCR 的老路。
页面渲染时要注意分辨率。太低会丢失小字和表格线,太高会增加图像 token 和超时概率。实践上先按 150 到 200 DPI 渲染,再根据失败重试调整。如果页面主要是文字,可以适当降低;如果包含密集表格或印章,可以局部裁剪。裁剪区域最好来自 LiteParse 或其他版面分析结果中的 bbox,如果没有 bbox,就按整页处理。
VLM OCR 的提示词要明确输出格式,否则后续合并很麻烦。推荐让模型输出 JSON,字段固定:
{ "page_no": 12, "full_text": "本页完整转写文本", "tables": [ { "title": "费用明细", "rows": [["项目", "金额"], ["服务费", "1000"]] } ], "layout_notes": ["右上角有盖章", "底部有手写签名"], "uncertain_parts": ["第三行第二列数字可能为 8"] }对应的 Python 调用可以这样组织。这里不绑定具体视觉模型,模型 ID 从 TaoToken 控制台可见的视觉模型里选:
import base64 import json from pathlib import Path from openai import OpenAI client = OpenAI( api_key="YOUR_API_KEY", base_url="https://taotoken.net/api", ) VLM_OCR_PROMPT = """你是文档 OCR 引擎。请转写当前页面,保留表格结构。 只输出 JSON,不要输出 Markdown 代码块。 字段:page_no, full_text, tables, layout_notes, uncertain_parts。 无法确认的内容放入 uncertain_parts,不要编造。""" def image_to_data_url(image_path: str) -> str: suffix = Path(image_path).suffix.lower().replace(".", "") mime = "image/jpeg" if suffix in {"jpg", "jpeg"} else "image/png" data = base64.b64encode(Path(image_path).read_bytes()).decode("utf-8") return f"data:{mime};base64,{data}" def vlm_ocr_page(image_path: str, page_no: int, model_id: str) -> dict: resp = client.chat.completions.create( model=model_id, messages=[ { "role": "user", "content": [ {"type": "text", "text": VLM_OCR_PROMPT}, { "type": "image_url", "image_url": {"url": image_to_data_url(image_path)}, }, {"type": "text", "text": f"当前页码:{page_no}"}, ], } ], temperature=0.1, max_tokens=2048, ) content = resp.choices[0].message.content.strip() return json.loads(content)这段代码的重点是:base_url用https://taotoken.net/api,Key 用YOUR_API_KEY,模型 ID 用你在 TaoToken 模型列表里确认支持图像输入的模型。不要凭记忆填一个模型名,也不要把 Anthropic 的环境变量套到 OpenAI 兼容客户端上。先跑通一页,再批量跑。
第二遍完成后,把refined_text回写到页面记录,并保留uncertain_parts。最终回答可以优先使用精读文本,粗读文本作为兜底。对于表格,可以把tables转成 Markdown 或 JSON 再进入索引。对于layout_notes,可以单独存为元数据,不参与正文拼接。这样既提升精度,又不破坏原有检索结构。
4. 把 VLMPredictor 接到 TaoToken:Key、Base URL 与模型名的最小配置
在 LlamaIndex 里,视觉模型通常落在 MultiModal LLM 或 VLMPredictor 这一层。不管你用哪种封装,核心参数都是三个:API Key、Base URL、模型 ID。配置前先到 TaoToken 官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=llamaindex_ocr_key_setup 完成登录并创建 Key。不要在其他地方找来源不明的 Key,也不要把 Key 写进公开仓库。
推荐用环境变量保存 Key,代码里只读环境变量:
export TAOTOKEN_API_KEY="YOUR_API_KEY" export TAOTOKEN_BASE_URL="https://taotoken.net/api" export TAOTOKEN_VISION_MODEL="YOUR_VISION_MODEL_ID"如果 LlamaIndex 版本提供 OpenAI 兼容的多模态客户端,可以这样初始化。不同版本的导入路径可能不同,按你本地版本调整:
import os from llama_index.multi_modal_llms.openai import OpenAIMultiModal vlm = OpenAIMultiModal( model=os.environ["TAOTOKEN_VISION_MODEL"], api_key=os.environ["TAOTOKEN_API_KEY"], api_base=os.environ["TAOTOKEN_BASE_URL"], max_new_tokens=2048, temperature=0.1, )如果你使用的是自定义VLMPredictor,思路相同:把它的底层客户端指向 OpenAI 兼容接口,并传入base_url。伪配置如下:
from llama_index.core.multi_modal_llms import MultiModalLLM class TaoTokenVLMPredictor(MultiModalLLM): def __init__(self, model: str, api_key: str, api_base: str): super().__init__() self.model = model self.api_key = api_key self.api_base = api_base def complete(self, prompt: str, image_documents, **kwargs): # 在这里调用 OpenAI 兼容的 chat.completions 或 responses 接口。 # 请求地址的 base 使用 self.api_base。 # 图片转成 image_url 或等价的多模态消息结构。 ...注意,Base URL 统一写https://taotoken.net/api,不要手动附加 UTM,也不要在代码里写带查询参数的地址。UTM 只用于官网和 deep link 的访问统计。模型 ID 必须从 TaoToken 控制台或模型对话页面确认。先拿一张包含表格的发票页测试,确认返回 JSON 能解析,再接入 LlamaIndex 的文档处理流。
这里再给一个验证客户端是否配置正确的片段:
from openai import OpenAI client = OpenAI( api_key="YOUR_API_KEY", base_url="https://taotoken.net/api", ) models = client.models.list() for m in models.data[:20]: print(m.id)如果这个片段能列出模型,说明 Key 和 Base URL 基本正确。接下来再排查视觉模型是否支持图片输入。不要把“文本模型可用”等同于“视觉 OCR 可用”,两者要分开验证。
5. 两段式调用链的可复现编排:从文件集到答案合并
把前面的片段串起来,就是一个可复现的 just-in-time Agentic OCR 流程。下面给出编排骨架,重点不是库的具体名字,而是每一步的输入输出边界:
from typing import List, Dict MAX_VLM_PAGES = 8 def run_two_pass_ocr(paths: List[str], query: str) -> Dict: # 第一遍:全量粗读 coarse_pages = build_coarse_pages(paths) # 建索引:可以分别建关键词索引和向量索引 index = build_page_index(coarse_pages) # 检索:返回页面级命中 hits = retrieve_pages(index, query, top_k=30) # 去重、排序、限流 candidates = dedup_by_page(hits) candidates = sorted(candidates, key=lambda x: x["score"], reverse=True) refined_pages = [] for item in candidates[:MAX_VLM_PAGES]: page = item["page"] if not should_refine(page, item["score"]): continue image_path = render_page_to_image( source_path=page["source_path"], page_no=page["page_no"], dpi=180, ) cache_key = f'{page["doc_id"]}:{page["page_no"]}:{page["page_hash"]}' cached = load_ocr_cache(cache_key) if cached: refined_pages.append(cached) continue refined = vlm_ocr_page( image_path=image_path, page_no=page["page_no"], model_id="YOUR_VISION_MODEL_ID", ) refined["cache_key"] = cache_key save_ocr_cache(cache_key, refined) refined_pages.append(refined) merged_pages = merge_coarse_and_refined(coarse_pages, refined_pages) answer = answer_with_pages(merged_pages, query) return { "answer": answer, "coarse_page_count": len(coarse_pages), "refined_page_count": len(refined_pages), "refined_pages": refined_pages, }这段代码里有几个工程点值得单独强调:
MAX_VLM_PAGES必须设上限。没有上限,一次查询可能触发几十页 OCR,延迟不可控。should_refine要把检索分数和页面质量结合起来。不是所有命中页都值得精读。cache_key要包含文档哈希、页码、页面哈希。文档更新后旧缓存自动失效。merge_coarse_and_refined要以页码为主键合并,精读文本优先,粗读文本兜底。answer_with_pages要保留引用页码,方便回溯是哪一页提供了答案。
如果你要把这条链路做成 Agent 工具,也建议保持“检索在前、OCR 在后”的顺序。不要让 Agent 直接对全量文件做 OCR,更不要让 Agent 绕过检索随意选择页面。工具调用的参数应该是doc_id、page_no、query,返回结构化 OCR 结果。这样既可审计,也能缓存。
6. 排障清单:401、404、图片不支持、页码错位与超时
配置两遍式 OCR 时,最容易卡在接口和页面定位上。下面按具体报错排查。
报错一:401 Unauthorized 或 Incorrect API key provided。优先检查YOUR_API_KEY是否已经替换,环境变量是否在当前终端生效。Claude Code、Codex、Python 脚本可能读取不同环境变量,不要假设终端里设置了就一定能被 IDE 或后台进程继承。还要检查 Base URL 是否误写成带 UTM 的官网地址。接口 Base URL 是https://taotoken.net/api,不是官网首页。
报错二:404 Not Found 或 model not found。通常是模型 ID 写错,或者把 Anthropic 风格的模型名直接用在 OpenAI 兼容客户端里。去 TaoToken 控制台或模型对话页面确认可用模型 ID,再填入配置。LlamaIndex 封装如果默认拼接了/v1,也要确认它和 Base URL 的组合是否符合平台文档。不要盲目在代码里硬编码完整端点。
报错三:Unsupported content type image_url 或 invalid image。这说明当前模型或请求格式不支持图片输入。先确认模型是否具备视觉能力,再检查图片是否转成了data:image/png;base64,...或data:image/jpeg;base64,...。如果图片过大,可以降低分辨率或裁剪区域。不要把 PDF 二进制直接塞进 image_url。
报错四:页码错位。LiteParse 输出的页码可能从 1 开始,渲染库可能从 0 开始,VLM 返回的页码又可能按当前图片重新编号。统一策略是:所有内部记录使用从 1 开始的page_no,渲染时再转换。合并时以内部page_no为准,不信任模型返回的页码。
报错五:Read timed out 或 rate limit。第二遍 OCR 要加超时、重试和退避。建议单页超时 60 到 120 秒,失败重试 2 次,并在重试时降低 DPI。批量处理时加并发上限,不要一次开几十个请求。对于限流,按指数退避并记录失败页,下一轮只补失败页。
报错六:返回内容不是 JSON。在提示词里明确“只输出 JSON”,并在代码里做容错解析。如果模型返回了 Markdown 代码块,可以先剥离首尾标记再json.loads。仍然失败就记录原始响应,不要静默丢弃。
排障时建议单独维护一个failed_pages.jsonl,记录doc_id、page_no、错误类型、原始响应摘要和重试次数。下一次运行时先补失败页,再跑新查询。这样两遍式链路才是可运维的。
7. Claude Code、Codex、CC Switch 的配套配置不能混
TaoToken 的 Key 可以服务多种工具,但不同工具的配置格式不同。最常见的错误是把 Claude Code 的ANTHROPIC_*环境变量套到 Codex 的config.toml里,或者反过来把 OpenAI 风格配置塞进 Claude Code。下面分开写。
Claude Code 使用settings.json和ANTHROPIC_*系列变量。示例:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "YOUR_API_KEY", "ANTHROPIC_MODEL": "YOUR_CLAUDE_MODEL_ID" } }如果你的 Claude Code 版本使用ANTHROPIC_AUTH_TOKEN作为认证头,也可以按官方文档替换对应字段。关键是 Base URL 指向https://taotoken.net/api,Key 使用YOUR_API_KEY,模型 ID 使用 TaoToken 控制台可见的 Claude 兼容模型。
Codex 使用config.toml,不要写ANTHROPIC_*。示例:
model = "YOUR_CODEX_MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat"同时设置环境变量:
export TAOTOKEN_API_KEY="YOUR_API_KEY"Codex 的env_key指向的是环境变量名,不是 Key 本身。不要把YOUR_API_KEY直接写进config.toml的env_key字段,否则工具会去读取一个名为YOUR_API_KEY的环境变量,而不是使用真实值。
CC Switch 可以理解成三件套:供应商配置、API Key、默认模型映射。你在 CC Switch 里新增一个供应商时,填:
- 供应商标识:
taotoken - Base URL:
https://taotoken.net/api - API Key:
YOUR_API_KEY - 模型映射:把 Claude Code 和 Codex 分别映射到 TaoToken 控制台可见的模型 ID
不要在一个工具里配置完成后,直接把同一份配置文件复制给另一个工具。Claude Code、Codex、CC Switch 的字段名和读取路径不同。先分别跑通最小请求,再统一管理。
8. 成本与精度平衡:第二遍 VLM 应该开多大
两遍式 OCR 的核心收益来自“把 VLM 从全量变成按需”。成本可以粗略理解为:第一遍粗读覆盖所有页面,成本低且可预测;第二遍精读只覆盖命中页面,成本取决于命中页数和每页图像 token。影响第二遍成本的主要因素是:
- 每次查询允许精读的最大页数。
- 页面渲染 DPI 和图片尺寸。
- 是否缓存已 OCR 页面。
- 是否把表格、印章、手写区域单独裁剪。
- 是否对低价值命中页做了过滤。
精度方面,VLM 对表格、印章、手写、复杂版面的还原通常优于纯文本解析器,但也会受图像质量、提示词和模型能力影响。建议先设置一个保守上限,例如每次查询最多精读 5 到 8 页,并且只对检索分数高或粗读质量差的页面触发。跑一段时间后,看哪些查询最终答案确实引用了精读页,再调整上限。
缓存是最直接的省钱手段。同一页在不同查询中可能被反复命中,如果没有缓存,每次都要重新渲染和调用 VLM。缓存键建议使用doc_id:page_no:page_hash。当文档更新时,page_hash变化,旧缓存自然不再命中。对于表格页,可以额外缓存table_json,避免每次重新解析。
另一个技巧是分级精读。第一级只让 VLM 转写文本;第二级才要求表格 JSON 和版面说明。如果某页只是普通段落,第一级就够。如果某页包含金额、合计、签字位置,再升到第二级。这样可以在保持答案质量的同时,减少每页输出 token。
最后,不要把“成本低”理解成“完全不调用 VLM”。两遍式的意义是让 VLM 在正确的时间、正确的页面上出现。第一遍保证召回,第二遍保证精度。链路里每一层都有明确职责,才不会在成本和精度之间反复摇摆。
9. 文末 CTA:从模型对话到 API Key 的落地顺序
如果你还没开始配置,建议按下面顺序落地。先到 TaoToken 官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=llamaindex_ocr_final 登录并创建 Key,然后把本文的 Base URL 填成https://taotoken.net/api。先用模型对话验证视觉模型能读取一张测试图片,再去创建 API Key,最后接 Claude Code 文档完成命令行工具配置。
模型对话:https://taotoken.net/models/detail/chat?utm_source=taotoken_aicg_blog_end&utm_content=llamaindex_ocr_chat
先用对话页验证图片输入和 OCR 提示词,确认返回 JSON 可解析。Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=llamaindex_ocr_coding_plan
如果你要把两段式 OCR 接到日常开发工作流,可以先了解 Coding Plan 的配置方式。API Keys:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=llamaindex_ocr_api_keys
创建独立 Key,填入本文所有YOUR_API_KEY占位符,不要把 Key 提交到仓库。Claude Code 文档:https://taotoken.net/doc/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_content=llamaindex_ocr_claude_code_doc
按文档完成settings.json和ANTHROPIC_*配置,再回到两遍式 OCR 链路中做端到端测试。
把 LiteParse 粗读、检索命中、VLM 精读、缓存回写这四步串起来,你就得到了一个可复现的 just-in-time Agentic OCR 工作流。Key 交给 TaoToken,Base URL 固定为https://taotoken.net/api,剩下的就是按页面、按查询、按成本上限去精读真正重要的内容。