
1. 先拿 TaoToken KeyLlamaIndex 多模态配置的入口检查在 LlamaIndex 里接OpenAIMultiModal或封装好的VLMPredictor时最先卡住你的往往不是 OCR 提示词而是api_base和 Key 没对齐先去 TaoToken 官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentjustin_time_ocr_intro 拿 Key再把 Base URL 设为 https://taotoken.net/api。很多同学把多模态模型名、温度、最大 token 都调完了结果请求仍然返回401 Invalid API key或404 Not Found原因通常只有两个Key 没有真正写进 LlamaIndex 读取的环境变量或者把 Base URL 写成了聊天补全的默认地址而不是 TaoToken 的 OpenAI 兼容入口。这篇按 just-in-time 调度视角拆解 LlamaIndex 的 Agentic OCR 两遍式文档处理第一遍用 LiteParse 这类低成本解析器对全部文件做粗读建立可检索的页级索引第二遍只对检索命中的页面调用 VLM 做 OCR 精读。目标不是把所有 PDF 一次性变成高精度文本而是让“贵的 VLM 调用”发生在查询真正需要的那几页上。你会看到可复现的 just-in-time 配置、调用记录样例以及api_base、模型 ID、超时、429 的排查顺序。先明确一个入口原则只要你要在 LlamaIndex 中调用多模态模型无论是OpenAIMultiModal、OpenAIVision风格封装还是项目里自己命名的VLMPredictor底层通常都要拿到三样东西api_key、api_base、model。其中api_key用YOUR_API_KEY占位api_base用https://taotoken.net/apimodel到模型对话页确认。不要先把 Key 硬编码进 notebook再到处复制先让它在一个.env或环境变量里生效后面的检索、OCR、缓存、重试才能统一。如果你还没有 Key去 TaoToken 官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentjustin_time_ocr_key 完成创建然后回到本地做一次最小验证。验证方式可以先用curl或 Python 请求确认 Key 和 Base URL 可用再进入 LlamaIndex。这样可以避免把 Key 问题误判成 LlamaIndex 解析器问题。export TAOTOKEN_API_KEYYOUR_API_KEY export OPENAI_API_KEY$TAOTOKEN_API_KEY export OPENAI_API_BASEhttps://taotoken.net/api注意OPENAI_API_BASE只是很多 OpenAI 兼容客户端会读取的环境变量名并不代表你只能调用 OpenAI 模型。TaoToken 的 Base URL 是统一入口具体模型 ID 以控制台和模型对话页为准。LlamaIndex 显式传api_base时优先级通常高于环境变量所以在代码里写清楚更稳。2. just-in-time OCR 的调度模型LiteParse 粗读与 VLM 精读的边界LlamaIndex 这篇 just-in-time Agentic OCR 思路最值得借鉴的地方是把文档处理拆成两个成本级别。第一遍不是 OCR而是“粗读”用 LiteParse 这类免费或低成本解析器把目录下所有文件跑一遍提取可读文本、页码、文件名、路径等元数据写入向量索引或关键词索引。这个阶段允许少量噪声因为它只负责“找到相关页”不负责“精确抄录”。第二遍才是“精读”当用户问题到来检索器先定位到候选页再只对这些页面调用 VLM 做 OCR提取表格、印章、手写批注、复杂版式等粗读阶段容易丢失的信息。这就是 just-in-time 调度视角的关键不是文档入库时全量 VLM而是查询发生时按需 VLM。它的成本模型更接近“检索命中页数 × 单页 VLM 调用成本”而不是“文档总页数 × 单页 VLM 调用成本”。对于几百页的合同、产品手册、扫描件、研报差距会非常明显。精度上粗读负责召回VLM 负责精排和抽取。如果粗读阶段把目标页漏掉了后面 VLM 再强也救不回来所以第一遍的目标不是完美 OCR而是让关键词、章节标题、页眉页脚、可提取文本尽可能进入索引。可以把两遍式流程写成调度伪代码用户提问 - 粗读索引检索 top_k 页 - 对命中页计算 score 与 doc/page 元数据 - 命中页渲染为图片或保留原页引用 - 仅命中页调用 VLM OCR - 合并粗读文本 VLM 精读文本 - 写入调用记录与缓存 - 返回答案或继续追问这里有一个常见误区把 LiteParse 粗读结果直接当成最终答案。对于普通文本 PDF这样做可能够用但对于扫描件、双栏排版、表格跨页、发票、合同附件粗读文本常常词序错乱或缺失。just-in-time 模式的价值就在于先用低成本文本保证“找得到”再用 VLM 保证“读得准”。两者不是替代关系而是召回与精读的分工。在 LlamaIndex 中实现时你可以把粗读节点设计成TextNodemetadata 至少包含doc_id、page_no、source_path、file_type、coarse_text_len。检索命中后根据source_path和page_no重新渲染该页图片再构造ImageDocument交给多模态模型。这样索引里存的是轻量文本VLM 只处理当前查询相关的页面。调用记录里要同时记录检索分数和 VLM 结果后续才能判断是召回问题还是精读问题。3. 可复现配置在 LlamaIndex 中接上 TaoToken 的 VLM 端点下面给一份可复现的 just-in-time 配置示例。思路是先用pypdf对 PDF 做粗读建立VectorStoreIndex检索命中后用PyMuPDF将对应页导出 PNG再通过 LlamaIndex 的OpenAIMultiModal调用 TaoToken 的 VLM 端点。代码中的YOUR_API_KEY、YOUR_VLM_MODEL_ID需要替换。如果你使用VLMPredictor封装核心也是向底层客户端传api_base和api_key不要只改模型名。安装依赖pip install llama-index llama-index-multi-modal-llms-openai pypdf pymupdf配置与调用示例import os import json import time from pathlib import Path from pypdf import PdfReader import fitz # PyMuPDF from llama_index.core.schema import TextNode, ImageDocument from llama_index.core import VectorStoreIndex from llama_index.multi_modal_llms.openai import OpenAIMultiModal BASE_URL https://taotoken.net/api API_KEY YOUR_API_KEY VLM_MODEL YOUR_VLM_MODEL_ID os.environ[OPENAI_API_KEY] API_KEY os.environ[OPENAI_API_BASE] BASE_URL vlm OpenAIMultiModal( modelVLM_MODEL, api_keyAPI_KEY, api_baseBASE_URL, max_new_tokens2048, temperature0.1, timeout120, ) def coarse_read_pdf(pdf_path: str): reader PdfReader(pdf_path) nodes [] for page_no, page in enumerate(reader.pages, start1): text page.extract_text() or nodes.append( TextNode( texttext[:4000], metadata{ doc_id: Path(pdf_path).name, page_no: page_no, source_path: str(Path(pdf_path).resolve()), file_type: pdf, coarse_text_len: len(text), }, ) ) return nodes def build_coarse_index(docs_dir: str): nodes [] for pdf_path in Path(docs_dir).glob(*.pdf): nodes.extend(coarse_read_pdf(str(pdf_path))) return VectorStoreIndex(nodes) def render_page_to_png(pdf_path: str, page_no: int, out_dir: str ./ocr_cache): Path(out_dir).mkdir(parentsTrue, exist_okTrue) doc fitz.open(pdf_path) page doc.load_page(page_no - 1) pix page.get_pixmap(dpi180) out_path Path(out_dir) / f{Path(pdf_path).stem}_p{page_no}.png pix.save(str(out_path)) doc.close() return str(out_path) def vlm_ocr_page(pdf_path: str, page_no: int, question: str): image_path render_page_to_png(pdf_path, page_no) image_doc ImageDocument(image_pathimage_path) prompt ( f请对第 {page_no} 页做 OCR 精读。 f只输出与问题相关的原文、表格字段和页码。问题{question} ) response vlm.complete(promptprompt, image_documents[image_doc]) return response.text if __name__ __main__: index build_coarse_index(./docs) retriever index.as_retriever(similarity_top_k5) query 付款周期、逾期利息和争议解决条款分别在哪一页 hits retriever.retrieve(query) records [] for hit in hits: meta hit.node.metadata start time.time() ocr_text vlm_ocr_page(meta[source_path], meta[page_no], query) records.append( { trace_id: jit-ocr-001, doc_id: meta[doc_id], page_no: meta[page_no], retrieval_score: hit.score, vlm_model: VLM_MODEL, latency_ms: int((time.time() - start) * 1000), ocr_preview: ocr_text[:300], } ) print(json.dumps(records, ensure_asciiFalse, indent2))这份代码把“粗读”和“精读”分开build_coarse_index只做低成本文本提取vlm_ocr_page只对命中页调用 VLM。api_base固定为https://taotoken.net/apiKey 用YOUR_API_KEY。如果你在项目里使用VLMPredictor包装类建议把api_key、api_base、model作为显式参数传进去不要依赖隐式默认值。这样日志里才能明确记录每次 VLM 调用走了哪个入口、哪个模型。官网入口再确认一次TaoToken 官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentjustin_time_ocr_config 。创建 Key 后建议先用模型对话页确认模型 ID 是否可用再写进 LlamaIndex 配置。很多人把模型名写错比如把聊天模型 ID 填到多模态字段最后得到的是模型不存在或能力不支持而不是 Key 错误。4. 调用记录两遍式文档处理的检索命中、VLM 页级 OCR 与缓存just-in-time OCR 能不能持续优化取决于调用记录是否完整。建议每次查询记录一条 trace里面至少包含查询文本、检索 top_k、命中页列表、每页检索分数、是否触发 VLM、VLM 模型 ID、耗时、返回摘要、缓存命中状态。下面是一份脱敏后的调用记录样例{ trace_id: jit-ocr-20240521-001, query: 付款周期、逾期利息和争议解决条款分别在哪一页, retrieval: { top_k: 5, hits: [ { doc_id: contract_a.pdf, page_no: 17, score: 0.83, coarse_text_len: 1280 }, { doc_id: contract_a.pdf, page_no: 18, score: 0.79, coarse_text_len: 1150 }, { doc_id: contract_a.pdf, page_no: 42, score: 0.74, coarse_text_len: 960 } ] }, vlm_calls: [ { doc_id: contract_a.pdf, page_no: 17, model: YOUR_VLM_MODEL_ID, latency_ms: 4210, status: ok, cache_hit: false }, { doc_id: contract_a.pdf, page_no: 18, model: YOUR_VLM_MODEL_ID, latency_ms: 3980, status: ok, cache_hit: false }, { doc_id: contract_a.pdf, page_no: 42, model: YOUR_VLM_MODEL_ID, latency_ms: 4450, status: ok, cache_hit: false } ], skipped_pages: 126, total_vlm_calls: 3, total_latency_ms: 12640 }这个记录能直接回答几个问题粗读召回是否命中正确页VLM 调用是否过多哪些页可以加缓存延迟主要出现在检索还是 OCR模型 ID 是否填对。缓存键建议用doc_id page_no question_hash model_id prompt_version。如果用户反复问同一份合同的付款条款第二次就可以直接命中缓存不必重复调用 VLM。缓存示例import hashlib from pathlib import Path CACHE_DIR Path(./vlm_cache) CACHE_DIR.mkdir(exist_okTrue) def make_cache_key(doc_id: str, page_no: int, question: str, model_id: str): raw f{doc_id}|{page_no}|{question}|{model_id}|v1 return hashlib.sha256(raw.encode(utf-8)).hexdigest() def read_cache(doc_id: str, page_no: int, question: str, model_id: str): key make_cache_key(doc_id, page_no, question, model_id) path CACHE_DIR / f{key}.txt return path.read_text(encodingutf-8) if path.exists() else None def write_cache(doc_id: str, page_no: int, question: str, model_id: str, text: str): key make_cache_key(doc_id, page_no, question, model_id) (CACHE_DIR / f{key}.txt).write_text(text, encodingutf-8)在 just-in-time 模式里缓存不是可选优化而是成本控制的一部分。因为 VLM 调用通常比文本检索慢一个数量级重复调用同一页不仅浪费成本也会拖慢响应。把缓存和调用记录放在一起才能判断“这次没命中缓存是因为问题变了、页码变了还是模型 ID 变了”。5. 排障手册api_base、模型 ID、超时与 429 的定位顺序当 LlamaIndex 调用 VLM 报错时按下面顺序排查不要同时改五个参数。第一401或Invalid API key。检查YOUR_API_KEY是否真的被代码读取而不是 shell 里设置了但 notebook 内核没重启。显式传参时检查api_keyAPI_KEY环境变量方式检查OPENAI_API_KEY。如果 Key 有空格、换行也容易失败。第二404或Not Found。优先检查api_base。正确值是https://taotoken.net/api不要自行拼接/v1、/chat/completions或多余路径除非对应文档明确要求。很多 OpenAI 兼容客户端会自动补路径手动再加一层就会 404。第三模型不存在或能力不匹配。到模型对话页确认模型 ID再填到VLM_MODEL。聊天模型和多模态模型不是一回事。如果你把只支持文本的模型 ID 传给OpenAIMultiModal可能会得到参数不支持或图片被忽略的结果。第四超时。timeout120只是客户端等待上限不是根因。常见根因是图片太大、DPI 太高、单页内容过密。可以先降到 150 到 180 DPI或者把大图切成上下两半。调用记录里的latency_ms能帮你判断是所有页都慢还是某一页特别慢。第五429。这通常表示并发过高或短时间请求太多。just-in-time 模式本身已经减少了 VLM 调用量但仍可能因为批量查询导致并发飙升。建议给 VLM 调用加信号量失败时指数退避并记录retry_count。import asyncio import random async def vlm_call_with_retry(fn, max_retries3): for attempt in range(max_retries): try: return await asyncio.to_thread(fn) except Exception as exc: if attempt max_retries - 1: raise wait min(2 ** attempt random.random(), 8) await asyncio.sleep(wait) raise RuntimeError(unreachable)第六返回空文本或胡编。检查提示词是否要求“只输出原文和表格字段”并降低温度。对于印章、手写、低质量扫描件可以在粗读命中后把同一页的多个区域分别裁剪再 OCR而不是整页一次性识别。调用记录里保存ocr_preview方便回看是图片问题还是提示词问题。6. 成本与精度怎么调阈值、并发、页码映射与失败回退just-in-time OCR 的调参目标不是“每页都识别”而是“在可接受精度下把 VLM 调用压到最少”。可以从四个维度调。第一检索 top_k 和分数阈值。top_k 太小会漏页太大则 VLM 调用暴涨。建议先用similarity_top_k5到10再根据调用记录观察命中页是否稳定。如果第 5 到第 10 名长期低分且从未被引用可以加分数阈值过滤。不要只看分数绝对值要结合文档类型。扫描件粗读文本短分数可能整体偏低。第二粗读文本长度与分块。页级索引适合 just-in-time OCR因为 VLM 按页调用最自然。但如果一页内容很长可以把粗读文本按段落分块metadata 仍然保留page_no。检索命中多个块时按doc_id page_no去重再调用一次 VLM避免同一页被多次 OCR。第三页码映射。PDF 物理页码、打印页码、扫描件页码常常不一致。粗读时记录的是阅读器物理页用户提问可能说“第 12 页”。如果文档内部页码偏移需要在 metadata 里加printed_page_no或页码映射表。调用记录里同时保存物理页码和识别到的打印页码后续排障会轻松很多。第四失败回退。VLM 调用失败时不要让整个查询失败。可以用粗读文本先返回一个低置信答案并标记need_vlm_retrytrue等并发下降或网络恢复后再补 OCR。对于金额、日期、条款编号建议在答案中保留原文片段和页码方便人工复核。def answer_with_fallback(hit, question): coarse_text hit.node.text try: ocr_text vlm_ocr_page( hit.node.metadata[source_path], hit.node.metadata[page_no], question, ) return {source: vlm, text: ocr_text, page_no: hit.node.metadata[page_no]} except Exception as exc: return { source: coarse, text: coarse_text[:800], page_no: hit.node.metadata[page_no], error: str(exc), need_vlm_retry: True, }这套回退机制的价值在于just-in-time 模式本身已经是按需调度如果某次 VLM 不可用至少保留粗读召回结果。等下一次查询同一页时再走缓存或重试。这样系统不会因为单个页面的 VLM 超时而整体不可用。7. 同一把 Key 的周边配置Claude Code、Codex 与 CC Switch 三件套如果你不仅用 LlamaIndex 做文档 OCR还在本地用 Claude Code、Codex 或其他 CLI建议统一从 TaoToken 官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentjustin_time_ocr_multitool 创建 Key然后按工具分别配置。不要把 Claude Code 的ANTHROPIC_*变量复制到 Codex也不要把 Codex 的config.toml格式套到 Claude Code。两者读取的配置文件和变量名不同。Claude Code 可以用settings.json或环境变量。示例{ 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按对应文档替换字段不要同时混用多个鉴权变量。配置文件可以放在用户级设置或项目级设置中具体路径以你的 Claude Code 版本为准。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_KEYYOUR_API_KEY如果你用 CC Switch 管理多个 CLI 配置建议把它当成“三件套”来维护Claude Code 一套ANTHROPIC_*变量Codex 一套config.toml TAOTOKEN_API_KEY第三个 CLI 客户端按它自己的官方变量单独设。每个 profile 的名字、Base URL、模型 ID、Key 来源都写清楚。切换时只切 profile不要手改全局环境变量否则很容易把 A 工具的 Key 带到 B 工具里。8. 文末 CTA模型对话、Coding Plan、创建 Key 与 Claude Code 文档如果你已经准备好把 LlamaIndex 的 just-in-time OCR 跑起来建议按下面顺序完成接入先在模型对话页验证 Key 和模型 ID 是否可用https://taotoken.net/models/detail/chat?utm_sourcetaotoken_aicg_blog_endutm_contentjustin_time_ocr_chat如果你还要长期写代码、跑文档流水线查看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentjustin_time_ocr_plan到 API Keys 页面创建或轮换 Key把YOUR_API_KEY替换掉https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentjustin_time_ocr_keys需要 Claude Code 的详细接入方式看 Claude Code 文档https://taotoken.net/doc/ClaudeCodeAnthropic?utm_sourcetaotoken_aicg_blog_endutm_contentjustin_time_ocr_claude_doc回到 LlamaIndex 这条线just-in-time 模式最重要的不是“把模型换掉”而是把调度做对LiteParse 负责全量粗读和召回TaoToken 的 VLM 负责命中页精读Base URL 固定为https://taotoken.net/apiKey 用YOUR_API_KEY调用记录和缓存跟着查询走。这样你既能保住复杂文档的 OCR 精度又不会在无关页面上消耗多模态调用。