news 2026/10/7 22:41:54

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 为主。

先装依赖。核心是onnxruntime(CPU)或onnxruntime-gpu(CUDA),加上transformers用来加载 processor 和 config,docling_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.,配合图像一起送进 processor:

messages = [ { "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_prompt=True) inputs = processor(text=prompt, images=[image], return_tensors="np")

接下来是自回归生成循环。这段是整个脚本的核心,逻辑是:先用 embed 会话把 input_ids 转成嵌入,如果是第一步就把视觉特征算出来并替换掉图像占位 token 的嵌入,然后送进 decoder 拿到 logits 和新的 KV cache,取最后一个 token 作为下一步输入,循环直到遇到 eos 或 end_of_utterance。

batch_size = inputs['input_ids'].shape[0] past_key_values = { f'past_key_values.{layer}.{kv}': np.zeros( [batch_size, num_key_value_heads, 0, head_dim], dtype=np.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([[]], dtype=np.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_embeds=inputs_embeds, attention_mask=attention_mask, position_ids=position_ids, **past_key_values, )) input_ids = logits[:, -1].argmax(-1, keepdims=True) 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_tokens=False)[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(name="Document") 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>里是 LaTeX,<otsl>是表格结构。位置标签<loc_x1><loc_y1><loc_x2><loc_y2>给出边界框。表格内部用<fcel>表示有内容单元格、<ecel>空单元格、<ched>列标题、<rhed>行标题、<srow>表格行。图片用<picture>加<image_class>,比如pie_chart、bar_chart、natural_image。

如果你要把 DocTags 转成别的格式,映射关系大致是:<formula>转 LaTeX,<otsl>转 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", encoding="utf-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 和 JSON,Markdown 看可读性,JSON 看结构完整性。

有个细节:生成循环里max_new_tokens设的是 8192,长文档可能不够,如果发现输出被截断,可以调大这个值,但要注意显存和耗时。另外skip_special_tokens=False是为了保留 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/api,Model ID 按你实际要调的模型填。如果你用的是 Claude Code 这类工具做文档润色或结构化后处理,可以在配置里把 Base URL、Key 和 Model ID 三件套写全,避免只填一半导致鉴权失败。验证的时候,先用模型对话入口发一张测试图或一段 DocTags 文本,确认返回正常,再切到批量流程。

我自己的习惯是:SmolDocling 本地跑出 DocTags 之后,把结构化的 JSON 存下来,需要进一步理解或摘要时再走 API。这样既保留了本地推理的低成本和隐私优势,又能在需要更强语义能力时补上云端模型。整个链路里,本地模型负责“看清结构和内容”,云端模型负责“理解和再加工”,分工明确,排查也容易。

如果你要长期跑文档转换任务,建议把 ONNX 会话做成常驻进程,避免每次请求都重新加载模型。三个 ONNX 文件加载一次大概几百 MB 内存,常驻之后单页推理的延迟会稳定很多。批量处理时按页拆分,每页独立生成 DocTags,再统一转成 Markdown 或 JSON 入库,这样单页失败不会影响整批。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 22:41:30

从零开始的嵌入式之旅:Day 1路线规划、环境搭建与STM32点灯实践

决定把接下来的学习过程用文字记录下来&#xff0c;今天算是正式启程。起因很朴素&#xff1a;工作里越来越多的项目涉及硬件&#xff0c;光会写上层逻辑已经有点不够用了&#xff0c;与其零敲碎打地查资料&#xff0c;不如系统性地把嵌入式这条路走一遍。于是有了这个“从零开…

作者头像 李华
网站建设 2026/10/7 22:41:29

Loop Engineering 实战:Claude Code、Codex、Cursor 环境搭建与工作流避坑指南

1. Loop Engineering 到底是什么&#xff0c;为什么现在值得花时间搞懂第一次听到 Loop Engineering 这个词&#xff0c;很多人会以为是某种新的编程语言或者框架。其实不是。它描述的是一套围绕 AI 编程工具构建的循环式工程工作流——你给 AI 一个任务&#xff0c;AI 执行&am…

作者头像 李华
网站建设 2026/10/7 22:40:02

自防御网络安全体系:从态势感知到闭环响应的工程落地

简介&#xff1a;本资源是一篇聚焦网络安全前沿实践的学术论文&#xff0c;面向高校网络空间安全专业师生、企业安全工程师及中小型机构IT运维人员&#xff0c;旨在解决当前规模化、复杂化网络攻击下防御响应滞后、协同不足的现实难题。论文提出基于网络安全态势感知的自防御体…

作者头像 李华
网站建设 2026/10/7 22:38:27

发动机力学模型实战:从火箭到电驱的差异化建模与坑点解析

力学模型这事&#xff0c;放在装备制造里永远是第一道门槛。发动机这种高速旋转、高载荷、高温高压的复杂系统&#xff0c;从图纸阶段到最后台架验证&#xff0c;每一个关键决策背后都站着一堆力学模型。这篇想聊的是火箭发动机、空天动力、潜艇推进、自动驾驶电驱、赛车燃油机…

作者头像 李华
网站建设 2026/10/7 22:35:51

n8n智能体开发:BambooHR+Bannerbear自动生成入职欢迎卡

干过几年n8n的人都知道&#xff0c;这工具表面上是个"连线拼积木"的自动化平台&#xff0c;真正玩进去之后你会发现&#xff0c;它最值钱的地方是"节点编排的思维能力"。这次想聊的是n8n智能体开发里一个很典型的组合&#xff1a;把BambooHR和Bannerbear这…

作者头像 李华
网站建设 2026/10/7 22:35:49

C语言递归全解:从函数调用栈到经典题型与优化

要我说&#xff0c;递归在C语言里就像一道“卡门槛”——没想通的时候觉得它玄乎&#xff0c;想通了之后会发现就那么回事。很多初学者拿着递归式能看懂&#xff0c;真让自己写却下不了笔&#xff0c;问题通常不在于语法不熟&#xff0c;而在于思维没切换到“递推边界”的模式。…

作者头像 李华