ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SmolDocling 实战:用超紧凑视觉语言模型做端到端多模态文档转换

SmolDocling 实战:用超紧凑视觉语言模型做端到端多模态文档转换 1. 为什么我要在本地跑 SmolDocling 做文档转换如果你手头有一堆 PDF、扫描件、技术报告想把它们变成结构化数据传统做法通常是拼一条流水线OCR 模型负责认字布局分析模型负责找表格和标题表格识别模型再单独处理单元格合并最后写一堆后处理逻辑把结果拼起来。这条链路能跑但调起来很痛苦任何一个环节出错都会往后累积而且每个模型都要单独部署显存和依赖加起来并不轻。SmolDocling 吸引我的地方在于它把这件事收敛成了一个端到端模型。它是一个超紧凑的视觉语言模型参数量只有 256M基于 SmolVLM-256M 改造输入一张文档页面图像直接输出一种叫 DocTags 的结构化标记序列。DocTags 里同时编码了元素类型、页面位置和内容表格用 OTSL 标签描述结构公式保留 LaTeX代码保留缩进和语言分类。换句话说一次推理就拿到了内容、结构和布局不需要再串多个模型。它适合谁我觉得三类人值得试一是做文档解析、知识库入库的工程师想找一个能在本地或单卡上跑的轻量方案二是做 RAG 的开发者需要把 PDF 转成带结构的 Markdown 或 JSON三是想研究超紧凑视觉语言模型实际效果的人256M 这个量级在消费级显卡甚至 CPU 上都能跑试错成本低。这篇我会按真实落地流程走一遍先把模型和 ONNX 权重准备好再给出可复制的推理配置然后跑一张样例文档看 DocTags 输出最后把常见的报错和排查动作列清楚。全程以本地文档解析为目标不涉及任何网络访问工具。2. SmolDocling 前置准备模型权重、ONNX 会话与 DocTags 依赖在写推理代码之前得先把运行环境和模型文件理清楚。SmolDocling 的官方权重在 Hugging Face 上叫ds4sd/SmolDocling-256M-preview它提供了两种用法一种是直接用 transformers 加载 PyTorch 权重另一种是用 ONNX Runtime 加载导出的三个 ONNX 文件。ONNX 路线在 CPU 和 GPU 上都能跑显存占用小我实测下来更适合本地部署所以这篇以 ONNX 为主。先装依赖。核心是onnxruntimeCPU或onnxruntime-gpuCUDA加上transformers用来加载 processor 和 configdocling_core用来把 DocTags 转成 DoclingDocument再导出 Markdown 或 JSON。pip install torch pip install --upgrade transformers pip install --upgrade docling_core pip install onnxruntime # CPU 版本 # 或者有 NVIDIA 显卡时用 pip install onnxruntime-gpu然后是三个 ONNX 权重文件它们分别对应视觉编码器、token 嵌入和合并后的解码器wget https://huggingface.co/ds4sd/SmolDocling-256M-preview/resolve/main/onnx/vision_encoder.onnx wget https://huggingface.co/ds4sd/SmolDocling-256M-preview/resolve/main/onnx/embed_tokens.onnx wget https://huggingface.co/ds4sd/SmolDocling-256M-preview/resolve/main/onnx/decoder_model_merged.onnx下载完把这三个文件放到同一个目录比如/content/或你本地的models/smoldocling/。注意decoder_model_merged.onnx是合并了 KV cache 的版本推理循环里要手动维护past_key_values这也是后面代码里那段循环的由来。processor 和 config 仍然从 Hugging Face 拉因为 ONNX 只导出了计算图分词和图像预处理逻辑还在 transformers 里from transformers import AutoConfig, AutoProcessor model_id ds4sd/SmolDocling-256M-preview config AutoConfig.from_pretrained(model_id) processor AutoProcessor.from_pretrained(model_id)这里有个容易忽略的点config.text_config里藏着推理循环需要的几个关键参数比如num_key_value_heads、head_dim、num_hidden_layers、eos_token_id。另外image_token_id和end_of_utterance的 token id 也要提前取出来前者用来把图像特征塞回嵌入序列后者是生成结束的标志之一。num_key_value_heads config.text_config.num_key_value_heads head_dim config.text_config.head_dim num_hidden_layers config.text_config.num_hidden_layers eos_token_id config.text_config.eos_token_id image_token_id config.image_token_id end_of_utterance_id processor.tokenizer.convert_tokens_to_ids(end_of_utterance)环境准备好之后目录结构大概是这样三个.onnx文件、一个测试图片目录、一个输出目录。测试图片建议先用一张清晰的单页文档比如技术报告或带表格的 PDF 转成的 PNG分辨率别太低否则小字和表格线容易糊。注意ONNX 路线不需要 GPU 也能跑只是慢一些。如果你用 CUDAExecutionProvider记得确认 onnxruntime-gpu 的版本和本机 CUDA 匹配否则会回退到 CPU 或者直接报 provider 找不到。3. 可复制配置SmolDocling 推理脚本与 DocTags 字段映射这一节给出完整的推理脚本分两个版本一个导出 Markdown一个导出 JSON。两者前半部分完全一样区别只在最后怎么处理 DocTags。先看公共部分。加载三个 ONNX 会话CPU 和 GPU 只差 providers 参数import os import numpy as np import onnxruntime from transformers import AutoConfig, AutoProcessor from transformers.image_utils import load_image from docling_core.types.doc.document import DoclingDocument, DocTagsDocument os.environ[OMP_NUM_THREADS] 1 os.environ[ORT_CUDA_USE_MAX_WORKSPACE] 1 model_id ds4sd/SmolDocling-256M-preview config AutoConfig.from_pretrained(model_id) processor AutoProcessor.from_pretrained(model_id) # CPU 版本 # vision_session onnxruntime.InferenceSession(vision_encoder.onnx) # embed_session onnxruntime.InferenceSession(embed_tokens.onnx) # decoder_session onnxruntime.InferenceSession(decoder_model_merged.onnx) # CUDA 版本 vision_session onnxruntime.InferenceSession( /content/vision_encoder.onnx, providers[CUDAExecutionProvider]) embed_session onnxruntime.InferenceSession( /content/embed_tokens.onnx, providers[CUDAExecutionProvider]) decoder_session onnxruntime.InferenceSession( /content/decoder_model_merged.onnx, providers[CUDAExecutionProvider])然后是输入构造。SmolDocling 的提示词很固定就是一句Convert this page to docling.配合图像一起送进 processormessages [ { role: user, content: [ {type: image}, {type: text, text: Convert this page to docling.} ] }, ] image load_image(image_path) prompt processor.apply_chat_template(messages, add_generation_promptTrue) inputs processor(textprompt, images[image], return_tensorsnp)接下来是自回归生成循环。这段是整个脚本的核心逻辑是先用 embed 会话把 input_ids 转成嵌入如果是第一步就把视觉特征算出来并替换掉图像占位 token 的嵌入然后送进 decoder 拿到 logits 和新的 KV cache取最后一个 token 作为下一步输入循环直到遇到 eos 或 end_of_utterance。batch_size inputs[input_ids].shape[0] past_key_values { fpast_key_values.{layer}.{kv}: np.zeros( [batch_size, num_key_value_heads, 0, head_dim], dtypenp.float32) for layer in range(num_hidden_layers) for kv in (key, value) } image_features None input_ids inputs[input_ids] attention_mask inputs[attention_mask] position_ids np.cumsum(inputs[attention_mask], axis-1) max_new_tokens 8192 generated_tokens np.array([[]], dtypenp.int64) for i in range(max_new_tokens): inputs_embeds embed_session.run(None, {input_ids: input_ids})[0] if image_features is None: image_features vision_session.run( [image_features], { pixel_values: inputs[pixel_values], pixel_attention_mask: inputs[pixel_attention_mask].astype(np.bool_) } )[0] inputs_embeds[inputs[input_ids] image_token_id] \ image_features.reshape(-1, image_features.shape[-1]) logits, *present_key_values decoder_session.run(None, dict( inputs_embedsinputs_embeds, attention_maskattention_mask, position_idsposition_ids, **past_key_values, )) input_ids logits[:, -1].argmax(-1, keepdimsTrue) attention_mask np.ones_like(input_ids) position_ids position_ids[:, -1:] 1 for j, key in enumerate(past_key_values): past_key_values[key] present_key_values[j] generated_tokens np.concatenate([generated_tokens, input_ids], axis-1) if (input_ids eos_token_id).all() or (input_ids end_of_utterance_id).all(): break doctags processor.batch_decode(generated_tokens, skip_special_tokensFalse)[0].lstrip()拿到doctags字符串之后用DocTagsDocument.from_doctags_and_image_pairs把它和原图配对再load_from_doctags装进DoclingDocument。导出 Markdown 就一行doctags_doc DocTagsDocument.from_doctags_and_image_pairs([doctags], [image]) doc DoclingDocument(nameDocument) doc.load_from_doctags(doctags_doc) result str(doc.export_to_markdown())导出 JSON 则用doc.save_as_json(out_file)它会保留 DocTags 里的层级和位置信息。DocTags 的字段映射值得单独说一下因为后面验证结果时要对着看。文档块类型里text是普通文本title和section_header是标题list_item配合ordered_list或unordered_list表示列表code带_programming-language_分类formula里是 LaTeXotsl是表格结构。位置标签loc_x1loc_y1loc_x2loc_y2给出边界框。表格内部用fcel表示有内容单元格、ecel空单元格、ched列标题、rhed行标题、srow表格行。图片用picture加image_class比如pie_chart、bar_chart、natural_image。如果你要把 DocTags 转成别的格式映射关系大致是formula转 LaTeXotsl转 HTML 表格text、list_item、title转 Markdown化学分子结构可以转 SMILES。docling_core已经帮你做了大部分转换所以直接用export_to_markdown或save_as_json就行。4. 验证请求用样例文档对比 SmolDocling 转换结果配置写好了得跑一张真实文档看效果。我用的是一张带标题、段落、一个表格和一段代码的技术文档截图分辨率 1600 左右。把图片路径填进main函数运行后会在当前目录生成同名.txt或.json。先看 Markdown 输出。理想情况下标题会变成#或##段落是普通文本表格会渲染成 Markdown 表格代码块保留缩进。我实测下来标题和段落的还原度不错表格结构基本能对上代码缩进也保住了。但有几个地方要留意如果表格有合并单元格OTSL 的ched和rhed能表达但转成 Markdown 时可能丢一部分语义这时候 JSON 输出更完整。再看 JSON 输出。save_as_json出来的结构里每个元素带label、text和prov位置信息。你可以用下面这段快速检查关键字段import json with open(test.json, r, encodingutf-8) as f: data json.load(f) for item in data.get(texts, [])[:5]: print(item.get(label), item.get(text, )[:60])如果label里出现section_header、table、code这些说明 DocTags 的类型识别生效了。位置信息在prov里能看到bbox和page_no这对做版面还原很有用。验证的时候我建议做三组对比第一组是纯文本页看 OCR 准确率和换行第二组是带表格的页看 OTSL 是否正确区分了表头和数据行第三组是带公式或代码的页看 LaTeX 和缩进有没有丢。每组都同时导出 Markdown 和 JSONMarkdown 看可读性JSON 看结构完整性。有个细节生成循环里max_new_tokens设的是 8192长文档可能不够如果发现输出被截断可以调大这个值但要注意显存和耗时。另外skip_special_tokensFalse是为了保留 DocTags 标签如果你只想看纯文本可以改成True但那样就丢了结构信息。提示第一次跑建议用 CPU 版本确认流程通不通再切 GPU 提速。ONNX 的 CUDA provider 在首次加载时会有一定初始化开销之后单页推理会快很多。5. 本篇常见错排查401、local proxy failed 与 reading choices 报错跑 SmolDocling 的过程中我踩过几个典型的坑这里按报错现象列出来方便你对照排查。第一个是401 Unauthorized或Cannot access gated repo。这通常发生在从 Hugging Face 拉ds4sd/SmolDocling-256M-preview的 config 或 processor 时。虽然这个模型本身是公开的但如果你本地配置了需要鉴权的镜像或代理就可能返回 401。排查动作先确认AutoConfig.from_pretrained和AutoProcessor.from_pretrained能单独跑通如果报 401检查环境变量里有没有残留的HF_ENDPOINT或 token 配置清掉再试。ONNX 文件是直接 wget 的不走鉴权所以如果只有 config 报错问题就在 transformers 这一侧。第二个是local proxy failed或Connection error。这个报错一般出现在下载 ONNX 权重或拉 processor 的时候。如果你在受限网络环境里wget 和 transformers 的请求可能都出不去。排查动作先确认三个.onnx文件是否已经完整下载到本地如果已经下载好就把代码里的路径改成绝对路径避免运行时再去联网。processor 和 config 如果拉不下来可以提前在能联网的机器上缓存好再把缓存目录拷过来用cache_dir参数指定。第三个是reading choices相关的报错比如KeyError: choices或IndexError: list index out of range。这个多半不是模型本身的问题而是你在用某个 API 封装层调用时返回结构和你预期的不一样。SmolDocling 的 ONNX 推理是本地计算不涉及 API 返回所以如果你看到choices字样说明代码里混进了别的调用逻辑。排查动作检查你的脚本里有没有多余的 HTTP 请求或 SDK 调用把推理路径收敛到本文给的 ONNX 循环上。第四个是OAuth或token invalid。如果你在别的地方配置过需要 OAuth 的服务环境变量可能被污染。排查动作在脚本开头打印os.environ里和 token、auth 相关的键确认没有意外注入的值。第五个是生成结果为空或只有几个 token。这通常是image_token_id没对上导致图像特征没塞进嵌入序列。排查动作打印image_token_id和inputs[input_ids]确认图像占位 token 确实出现在序列里并且inputs_embeds[inputs[input_ids] image_token_id]这行的替换形状能对上。第六个是 CUDA provider 报NotFound。这说明 onnxruntime-gpu 没装好或 CUDA 版本不匹配。排查动作先pip show onnxruntime-gpu看版本再确认onnxruntime.get_available_providers()里有没有CUDAExecutionProvider没有就回退到 CPU 版本先跑通。把这几类报错对照一遍基本能覆盖本地部署 SmolDocling 时八成以上的问题。剩下的多半是图片质量或max_new_tokens设置导致的输出异常调这两个参数通常能解决。6. 从本地推理到稳定调用SmolDocling 的接入与验证路径本地把 SmolDocling 跑通之后下一步通常是把它接进你的文档处理流程。如果你只是偶尔转几页直接跑脚本就够了但如果要做批量入库或在线服务就需要考虑调用方式和密钥管理。一个务实的做法是本地 ONNX 推理负责重活把 DocTags 或 Markdown 结果缓存下来需要调用云端模型做补充识别或对比时再走 API。TaoToken 在这里可以作为一个统一的模型调用入口它的 API 地址是 https://taotoken.net/api你可以在控制台里创建密钥然后按文档接入。模型对话入口适合做单页验证和效果对比Coding Plan 适合把文档转换接进长期的编码或 Agent 流程API Keys 页面用来管理你的调用凭证。具体操作上先在 API Keys 页面生成一个 key然后参考接入文档把 Base URL 配成https://taotoken.net/apiModel ID 按你实际要调的模型填。如果你用的是 Claude Code 这类工具做文档润色或结构化后处理可以在配置里把 Base URL、Key 和 Model ID 三件套写全避免只填一半导致鉴权失败。验证的时候先用模型对话入口发一张测试图或一段 DocTags 文本确认返回正常再切到批量流程。我自己的习惯是SmolDocling 本地跑出 DocTags 之后把结构化的 JSON 存下来需要进一步理解或摘要时再走 API。这样既保留了本地推理的低成本和隐私优势又能在需要更强语义能力时补上云端模型。整个链路里本地模型负责“看清结构和内容”云端模型负责“理解和再加工”分工明确排查也容易。如果你要长期跑文档转换任务建议把 ONNX 会话做成常驻进程避免每次请求都重新加载模型。三个 ONNX 文件加载一次大概几百 MB 内存常驻之后单页推理的延迟会稳定很多。批量处理时按页拆分每页独立生成 DocTags再统一转成 Markdown 或 JSON 入库这样单页失败不会影响整批。
返回列表