
上周有个做设备运维的朋友找我说他用 Dify 搭了一个维修知识库把设备图纸、现场照片、说明书扫描件都传了上去结果问“这张图纸里的管径标注是多少”Dify 只回了一句“未找到相关资料”。他以为是模型太笨我让他去知识库后台看一眼才发现这些图片要么格式不被接受要么进去了也检索不到。这个场景我非常熟悉——很多人对 Dify 的期待是“能传图片”但 Dify 知识库对图片的处理并不是把图片当成图片理解而是要把其中的信息转换成一种可检索的结构。要让大模型知识库在 Ubuntu 上真正支持图片召回关键不在上传入口而在整条数据链路的改造。这篇文章我会从原理讲到落地覆盖 Ubuntu 环境准备、Dify 部署、图片处理脚本编写、知识库分段设计、工作流返图以及我踩过的各种坑。适合两类人看一类是已经在用 Dify、想让知识库支持图片检索的开发者另一类是准备从零搭一个“图文混合”知识库、但还没想清楚技术路线的朋友。1. 先弄明白Dify 知识库里的“图片召回”到底是什么问题1.1 图片在 RAG 链路中的位置它不是一个能直接分词的“文档”RAG 知识库的基本链路是文档导入 → 分块 → Embedding 向量化 → 存入向量库 → 检索召回 → 交给大模型生成回答。这条链路里所有环节都在处理“文本”分块要对文本切分Embedding 模型接收的是 token向量库里比较的也是文本向量。图片不一样。一张 PNG 在计算机里是一堆像素点没法直接分词也没有天然对应的 token。你把它拖进知识库系统不知道该对它做什么。如果只是把图片二进制存入数据库检索时没有任何语义锚点自然“召不回”。所以“图片召回”本质上是一个翻译问题要么把图片翻译成文字让文本链路能够索引要么把图片翻译成向量让向量链路能够比较。前者更贴近 Dify 现有架构后者需要额外的多模态向量库。想清楚这一点后面所有方案选择都会清晰很多。1.2 三种实现路径OCR、图生文、多模态向量我为什么选图生文我接触到的图片入库方案大体有三类各有适用场景。方案基本思路召回质量实现成本Dify 集成难度纯 OCR用 OCR 提取图中文字入库文本对扫描件、票据尚可对图形化信息基本无效低低图生文多模态模型描述图片生成结构化文本入库高能描述物体、属性、场景、文字中中多模态向量用 CLIP 等模型把图文映射到同一向量空间高但文本与图片向量不易对齐高高Dify 无原生支持纯 OCR 的问题在于它丢失了图片里最关键的“视觉语义”。比如一张设备外观图OCR 只能抽出图上的几行字但“白色外壳、左侧有散热孔、正面带一块液晶屏”这些信息全部丢失。用户如果问“哪个设备是白色带液晶屏的”纯 OCR 方案根本召回不了。多模态向量方案理论最优但 Dify 目前没把这类模型内置到知识库链路里。你要自己搭一个独立的图片向量库再写插件或服务去桥接工程量大而且图片向量和文本向量如果不在同一空间检索效果照样扑街。除非你有明确的“以图搜图”需求否则我不建议第一步就上这个方案。我最终选择的是“图生文 文本 RAG 外链返图”的组合用视觉大模型把图片翻译成一段高质量描述文本这段文本进入 Dify 知识库参与常规检索图片本身放在 Nginx 或对象存储上在描述文本里保留图片的绝对 URL。用户问到一个东西时检索命中的是“图片描述”回答时把原图链接带出来。这样既绕开了 Dify 的原生限制又实现了真正意义上的“图片召回”。1.3 Dify 原生能力边界与“曲线救国”的整体设计所谓“曲线救国”就是承认 Dify 知识库本质上是文本仓库然后围绕它做外围改造。整个架构由四部分组成图片存储层图片文件放在统一目录由 Nginx 提供 HTTP 访问能力。图片描述层写一个 Python 脚本调用视觉大模型把图片转成结构化 Markdown 文本。知识库层把生成的 Markdown 按“一张图一个片段”的方式导入 Dify。应用层在工作流里做知识检索从命中片段中提取图片 URL 并展示给用户。这个设计的好处是每一层都能独立替换。视觉模型想换就换Nginx 可以换成 MinIO 或 S3Dify 知识库检索策略也能按效果调整。我在后面的章节里会按这条链路逐步展开。2. Ubuntu 上准备环境与部署 Dify 时最容易忽略的细节2.1 检查 Docker 环境与容器编排工具Dify 官方推荐用 Docker Compose 部署Ubuntu 上最基础的一件事是把 Docker 环境整干净。先检查两个命令是否可用docker --version docker compose version如果 docker 没装可以走 apt 安装sudo apt update sudo apt install -y docker.io docker-compose-v2 sudo systemctl enable --now docker这里有个小坑Ubuntu 的 docker.io 包版本可能偏旧但 Docker Compose 插件的 v2 基本够用。装完后记得把当前用户加入 docker 组避免每条命令都要 sudosudo usermod -aG docker $USER newgrp docker我遇到过不少人在docker compose命令上卡住报Unknown command compose基本都是因为只装了旧版 docker-composePython 版。现在 Dify 的部署脚本已经按 v2 插件写了直接用docker compose而不是docker-compose。2.2 克隆 Dify 仓库并完成初始配置Dify 的 Docker 编排文件都在独立目录里拉取方式如下git clone https://github.com/langgenius/dify.git cd dify/docker cp .env.example .env复制完.env后有几项我每次都会检查SECRET_KEY默认值能跑但生产环境最好改成随机长字符串。POSTGRES_PASSWORD、REDIS_PASSWORD如果是公网服务器这些默认密码一定要改。存储类型如果你后续要把图片或文件交给 Dify 管理需要配置 S3/MinIO 相关变量如果图片只是放在 Nginx 上Dify 本地存储就够。检查完直接启动docker compose up -d启动后可以通过docker compose ps看状态。服务比较多首次启动可能要等一两分钟等nginx、api、worker三个容器都显示 healthy 了再打开http://服务器IP/install初始化管理员账号。2.3 镜像拉取失败和启动卡住的排查顺序见评论区经常有人发 Dify 拉取镜像失败的截图我在 Ubuntu 上排查的顺序基本固定第一步先看是不是网络波动导致的超时docker compose pull如果报dial tcp: lookup registry-1.docker.io之类的解析错误大概率是 DNS 或镜像源问题。可以在/etc/docker/daemon.json里配置国内常见的 Docker 镜像加速地址{ registry-mirrors: [ https://docker.m.daocloud.io ] }然后重启 Dockersudo systemctl restart docker第二步看磁盘空间。Dify 全套镜像加起来好几个 GB/var/lib/docker所在分区如果满了启动会报no space left on device。第三步看容器日志。启动卡住最常见的是 API 容器起不来多半和数据库初始化有关。用docker compose logs api | tail -100能看到具体报错。我遇到过一次是.env里的POSTGRES_PASSWORD含特殊字符比如、#导致数据库连接串解析异常。建议密码只用字母和数字。还有一点如果你是在国内服务器上部署Dify 的镜像拉取偶发失败是正常的不要反复up -d而是先docker compose pull确认所有镜像都成功再执行启动。2.4 模型供应商接入视觉模型和 Embedding 模型都要配Dify 部署好之后第一件事是接模型。这里需要两类模型Embedding 模型负责把文本变成向量比如text-embedding-3-small、bge-m3。选型时注意向量维度要和知识库一致中途换模型会导致已入库存量无法检索。视觉模型负责给图片写描述比如 GPT-4o、Qwen-VL、Ollama 上的 llava。这个模型不直接参与 Dify 知识库检索但在图片预处理阶段决定了描述质量。在 Dify 的“设置 → 模型供应商”里可以把视觉模型也注册进去后面做 Agent 或工作流时可以直接调用。不过我的习惯是图片描述脚本独立于 Dify 跑不占用 Dify 的模型配额省得把知识库 API 的并发打满。3. 图片入库的完整链路从图片目录到可检索的知识库文档3.1 图片目录规划与外链存储设计图片不是“传进 Dify”就完事它必须有一个可被浏览器访问的地址。最简单的做法是 Nginx 挂一个静态目录。我在服务器上习惯这么规划/data/images/ /product/ product_a_001.png product_a_002.jpg /manual/ manual_01.pngNginx 配置如下server { listen 8080; server_name _; root /data/images; autoindex off; location / { expires 30d; add_header Cache-Control public; } }启动后图片地址就是http://服务器IP:8080/product/product_a_001.png。记得在 Ubuntu 防火墙里放行端口sudo ufw allow 8080/tcp这里有个容易被忽略的问题如果你把 Dify 和 Nginx 用 Docker 部署在同一台机器容器里的localhost和宿主机不是一回事。Nginx 如果是容器root路径要映射到宿主机目录图片 URL 里也要写服务器对外 IP 或域名不能写localhost否则用户在浏览器里看到图片时访问的是他自己的电脑。3.2 写一个“图生文”预处理脚本把图片变成结构化 Markdown这是整条链路的核心。图片描述的质量直接决定后面检索命中的质量。描述写得越结构化检索时越容易被语义命中。我用的脚本逻辑是读取图片 → Base64 编码 → 调用视觉模型 → 得到结构化描述 → 拼成 Markdown 文件。import base64 import glob import os from openai import OpenAI client OpenAI( base_urlhttp://your-model-api/v1, api_keyyour-api-key ) IMAGE_DIR /data/images/product OUTPUT_DIR /data/dify_input/product IMAGE_BASE_URL http://your-server-ip:8080/product SYSTEM_PROMPT 你是一个图片信息提取专家。请用结构化方式描述图片 1. 一句话核心摘要30字以内 2. 图中主体对象包含品牌、型号、颜色、形状、材质等可检索属性 3. 图中所有可见文字逐行输出 4. 构图、背景、环境氛围 5. 可能的用途或使用场景 不要编造图中不存在的内容。 def image_to_markdown(img_path: str) - str: with open(img_path, rb) as f: b64 base64.b64encode(f.read()).decode() ext img_path.rsplit(., 1)[-1].lower() if ext jpg: ext jpeg data_url fdata:image/{ext};base64,{b64} resp client.chat.completions.create( modelgpt-4o, messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: [ {type: text, text: 请描述这张图片}, {type: image_url, image_url: {url: data_url}} ]} ], temperature0.2 ) description resp.choices[0].message.content filename os.path.basename(img_path) url f{IMAGE_BASE_URL}/{filename} md f--- source: {filename} image_url: {url} --- # {filename} {description}  return md def main(): os.makedirs(OUTPUT_DIR, exist_okTrue) for img_path in glob.glob(os.path.join(IMAGE_DIR, *.png)) \ glob.glob(os.path.join(IMAGE_DIR, *.jpg)): md image_to_markdown(img_path) out_path os.path.join(OUTPUT_DIR, os.path.basename(img_path) .md) with open(out_path, w, encodingutf-8) as f: f.write(md) print(fprocessed: {img_path}) if __name__ __main__: main()这段代码有几个设计细节temperature0.2图片描述要尽量稳定准确温度太高会编造内容。YAML 头里的image_url单独存放后面从知识库结果里提取图片地址时直接查这个字段比从正文正则匹配稳定得多。Markdown 正文里再带一次图片链接这样用户在 Dify 的文档预览里也能看到图。脚本跑完后/data/dify_input/product下每个图片对应一个 Markdown 文件。文件名我建议在脚本里顺手做一下规范化比如转小写、空格换下划线避免中文和特殊字符。3.3 知识库分段与元数据设计避免图片 URL 被切碎Dify 知识库导入文档时默认按固定长度切块。问题来了如果分段长度太小图片 URL 可能被从中间切断变成 - dict: urls [] for item in result: content item.get(content, ) # 匹配 Markdown 图片语法 found re.findall(r!\[.*?\]\((http[^)])\), content) urls.extend(found) # 兼容 YAML 头里单独存放的 image_url m re.search(rimage_url:\s*(http\S), content) if m: urls.append(m.group(1).strip()) return {urls: list(dict.fromkeys(urls))[:3]}在 Dify 的代码节点里输入变量名和你配置的“输入变量”映射有关。比如把知识检索节点的result映射为result代码节点出入口都要在变量类型里定义清楚。LLM 节点更聪明但偶尔会“过度理解”。我试用过直接让大模型从content中提取图片链接并输出质量不稳定。一次性给我 8 张图、漏选重要图片、或者把不相干的图片也塞进来都遇到过。所以现在生产环境我偏好代码节点做初筛再用 LLM 节点做排序。4.3 提示词设计让大模型把图片当作答案的一部分如果只是把图片 URL 塞给用户体验很生硬。更合理的回答方式是先用文字说明依据再把相关图片以 Markdown 形式展示出来。我工作流里 LLM 节点的提示词大致如下你是一名熟悉设备资料库的助理。请根据检索到的文档片段回答用户问题。 要求 1. 如果片段中有相关图片链接在回答末尾用 Markdown 图片格式展示 2. 每张图片对应一行图片描述要简短准确来自文档内容 3. 不要编造文档片段中不存在的图片 4. 如果文档片段没有图片只输出文字回答这个提示词里的“不要编造图片”很关键。LLM 看到图片 URL 后如果上下文里有多个 URL它可能会自己“联想”出一张并不存在的图。加了上面这句之后返图准确率高了很多。4.4 检索质量优化混合检索与 Rerank我在第 3 章的测试中发现单靠向量检索图片描述里的一些精确型号、编号容易漏召回。Dify 知识库支持三种检索模式向量检索、全文检索、混合检索。向量检索适合语义相近但用词不同的情况比如“红色的盒子”匹配“外壳为红色”。全文检索适合精确词命中比如“HK-300”。图片描述里既有语义描述又有具体型号所以我推荐用混合检索。如果检索精度还是不够加一层 Rerank 模型。Dify 支持配置 Rerank 模型比如bge-reranker-v2-m3。它的作用是把召回的前 2050 个候选重新排序把真正相关的片段排到最前面。我实测下来加了 Rerank 之后图片描述片段排名明显上浮Top1 命中率提升明显。检索参数我建议这样调TopK812候选多一点给 Rerank 留足空间Score 阈值先设 0.3如果误召回太多再往上调Rerank TopN最终返回给模型 35 条。5. 避坑指南这些“图片召回”相关的坑我一个个踩过来5.1 图片“传上去了”但知识库检索不到格式与编码问题很多人的第一反应是问“Dify 知识库能不能传图片”。实际情况是Dify 把图片当文档处理的路径非常有限即便你通过某些方式把图片文件塞进了文档检索时也不会对图片本身建立索引。图片要进知识库必须先经过“图生文”转换。另外图片文件本身也可能有问题。PNG、JPG 通常没问题但 WebP 在某些模型服务里不支持直接 Base64 传输需要先转码。还有一次我处理批量图片时脚本读出来发现是 0 字节原因是有几张图片在 Windows 下编辑后扩展名是.png实际内容是 JPEG后端解析失败。脚本里最好加一步文件头检查或者统一转成 RGB 模式的 JPEG/PNG。图像大也是隐患。一张 10MB 的图片 Base64 后约 13MB直接塞进 API 请求很容易触发网关超时或 413 错误。我在脚本里加了一个预处理步骤超过 2MB 的图先压缩到 1024px 宽度质量 85 再发送。这个尺寸对视觉模型来说足够识别细节请求量也小很多。5.2 图片链接在分段后被截断的经典问题这个坑我前面提过一次但因为太典型值得再展开。Dify 默认分段长度是 500 字符如果 Markdown 的 YAML 头、标题、描述、图片链接连在一起超过 500默认分段方式会从第 500 个字符硬切。图片链接长的话极易被切在中间。我当时遇到的现场是检索结果里的content变成了... : try: md image_to_markdown(img_path) break except Exception as e: print(fretry {attempt} for {img_path}: {e}) time.sleep(2 ** attempt) else: print(ffailed: {img_path})这里的time.sleep(2 ** attempt)是退避策略第一次失败等 2 秒第二次等 4 秒第三次等 8 秒。能有效缓解瞬时限流。5.6 改完 Embedding 模型后老数据全部失效这个坑在图片入库场景特别容易踩。因为做图片召回时你会反复尝试不同 Embedding 模型想找效果最好的一个。但 Dify 知识库一旦用了某个 Embedding 模型之前入库的向量就全部锁死在这个模型的向量空间里。中途换模型旧向量和新查询向量不在同一空间相似度计算就是错的。我有一次把text-embedding-3-small换成了本地bge-m3结果所有旧文档全部召回异常分数普遍掉到 0.1 以下。最后只能清空知识库重新导入。所以建议是在图片预处理脚本写好之后先用 1020 张图做一轮完整测试确认描述质量和检索效果都满意了再正式批量入库。Embedding 模型的选型最好第一批测试时就定下来后期不要轻易换。6. 最终效果与几点个人建议这套链路跑通之后最终效果是这样的用户问“红色外壳带液晶屏的设备”工作流先通过知识检索命中对应图片描述文本代码节点从命中片段提取出image_urlLLM 节点在回答末尾展示图片用户看到的是一条带文字的说明加一张真实的产品图片。整个过程从提问到出图大概两三秒。如果让我重新搭一遍我会在第一步就把图片描述模板固定下来并且用 10 张不同类型的图先做召回测试确认描述风格稳定后才开始批量。描述模板里的“口语化说法”和“属性结构”两段一定要保留这直接影响向量召回的命中率。另外图片 URL 不要只留在正文里单独存到 metadata 或 YAML 头这种结构化字段里能省掉后面 80% 的提取麻烦。Nginx、防火墙、文件名规范这些基础工作看似和“大模型知识库”无关但图片召回的体验 90% 都卡在这些基础环节上。把这套链路跑熟之后你会发现图片知识库和文本知识库的差别其实只差一个“翻译”步骤。