掌柜智库项目 Day 03|图片处理节点与 MinIO 对象存储
实现图片处理节点 node_md_img,用视觉模型识别图片内容、上传 MinIO 替换为网络地址,并完成对象存储与 LLM 的 infra 封装。
本篇要点
- 梳理当前图的节点状态,明确图片处理要解决的问题
- 实现
node_md_img:scan_images取图片上下文 → 视觉模型summarize_images→ 上传 MinIO 并替换 md 内容 - 掌握 MinIO 的 docker 部署、可视化操作、桶权限 policy 与 Python 增删查
- 用
MinioGateway与LLMProvider把对象存储和模型能力封装进 infra - 回顾正则:
re.escape、search/match/findall/finditer、贪婪与非贪婪
内容回顾
1、上一天主题(Day02)
- 定义
ImportGraphState(TypedDict),共 12 个属性,配套create_default_state/get_default_state - 节点与业务分层:
@node_log装饰器 +add_running_task/add_done_task - 主图构建:builder → add_node → set_entry_point → 条件边 → compile
- 任务分发节点
node_entry,PDF 解析节点node_pdf_to_md(MinerU 上传/轮询/下载解压重命名)
2、基础工具小结
Path属性:.name、.stem、.suffix、.parent、.parentsPath方法:.is_dir、.exists、.mk_dir、.read_text、.read_bytes、write_text、write_bytes、.rename、.with_name、.rglob、.globshutil方法:rmtree(文件夹)、unpack_archive(解压的文件, 目标文件夹)、make_archive("压缩文件名","压缩格式",压缩文件或者文件夹地址)requests:请求用requests.get/post/put/delete(url, headers, params, json, data, timeout);响应用.status_code、.json()、.text、.content,以及requests.Session()- MinerU 的使用:申请上传地址和
batch_id(POST)→ 向指定的预签名地址请求 → 轮询获取响应结果
今天内容
1、当前节点状态
项目主图目前已串起两个节点,节点状态如下:
1 | -------------------> |
2、node 中的图片处理
2.1 节点作用
将 md 中的图片进行处理:
1 | 处理前:  |
这样处理有两方面优势:
- 优势一:让图片真正参与到内容回答中(图片内容会被模型识别)。对应面试/追问点:你们的 RAG 是做的图文混合检索的么?
- 优势二:本地地址换成网络地址,客户端在任何位置都可以访问和回显图片,回答问题的时候可以直接带上图片。
2.2 参数说明
- 使用:
md_path→md_content→images(图片文件夹) - 生成:
md_content;文件由文件名.md→文件名_new.md
2.3 准备工作
- 准备文件服务器 / 文件服务器的使用
- 自己搭建:
minio搭建文件服务器(需要考虑服务器、搭建维护成本) - 直接买:阿里、腾讯、华为等 OSS 对象存储服务器(需要花钱)
- 自己搭建:
- 准备视觉模型对象
- 在
infra层封装:minio 的操作 | 模型的使用
2.4 实现步骤总览
- 安装了 minio 软件
- 准备 minio 的公共类(桶名、图片前缀、client、构建访问路径)
- 准备 llm 的公共类(获取大语言模型 / 获取视觉模型)
- 获取数据并且校验(state → md_path)→
md_content、images_path_obj、md_path_obj - 判断 images 文件夹是否为空
- 为后续步骤做准备,提取图片的信息:每张图片 →
(图片名.jpg, 图片地址, (上文, 下文)) - 使用视觉模型进行图片的意图识别
- 将图片传递到 minio,并且完成 md_content 内容的替换
- 替换 state 内容
md_content、md_path
3、step 4:参数获取与校验
获取 md_content、images_path_obj、md_path_obj 三个对象:
- 获取
md_path属性(state) - 非空和文件存在性校验
- 读取
md_content内容(通过md_path),md_content→ state
注意:md_content必须存进 state,因为后面可能没有图片,后续步骤仍然要用到它 - 获取图片文件夹地址:
md_path_obj → .parent / 'images' → images_path_obj - 返回,供后续步骤使用
4、step 6:scan_images 提取图片上下文
图片文件夹与 md 内容的对应关系大致如下:
1 | images 文件夹对象 md_content |
目标函数签名:
1 | scan_images(images_path_obj: Path, md_content: str) -> list[tuple(图片名.xx, 图片地址, (上, 下))] |
实现细节:
- 遍历图片文件夹 images,获取每一个文件
- 判断文件是不是图片 → 通过后缀判断,命中则加入
list[] - 使用图片的名字
name→ 在 md 中查找是否被引用(正则回顾,见第 7 节) - 若被引用,获取引用信息
start、end - 取上文
[start-100:start]、下文[end:end+100] - 拼接本次数据
(图片名, str(obj), (上文, 下文))→ append 到结果列表
最终产物:
1 | md_content, images_path_obj, md_path_obj, image_context_list = ... |
5、step 7:summarize_images 视觉模型识别
1 | dict[str -> name, str -> 图片的描述] = summarize_images(image_context_list, 文件夹的名字 md_path_obj.stem) |
实现细节:
- 获取模型对象
vision_chat() - 封装提示词(图片 / 文本)
- 封装一个调用链
StrOutputParser() - 执行调用链获取返回结果
- 封装一个 dict,并且返回即可
细节(面试/坑):
- base64
- 视觉模型的数据格式
- 滑动窗口限速器
- langchain 的 chains 是
|串联
6、step 8:上传 MinIO 并替换 md_content
1 | md_content_new: str = upload_images_and_replace( |
实现细节:
- 获取 minio 客户端对象
- 先删除当前文件图片(minio 上的)
- 循环
image_context_list向 minio 上传文件 →dict[str: image_name, str: 网络地址] - 循环”图片 → 网络地址”对应的字典,逐一替换 md_content 内容
- 返回
md_content_new
细节(面试/坑):
- 查询
prefix不能以/开头(处理方式[1:]) - 查询到的对象要转成
DeleteObject,即[DeleteObject(o.object_name) ...] - 删除
errors:for ... yield - 上传用
fput(bucket_name, object_name, file_path, content_type=guess_type[0]) - 拼接访问地址
- 替换:
md_content = re.sub(lambda _: f"![]()", md_content)
7、step 9:回写 state
- 把
md_content备份到原文件名对应的文件名_new.md文件中 - 替换 state 中的
md_content、md_path
8、MinIO 的使用
8.1 搭建和安装(docker minio)
方案一:使用 docker —— 一行命令
1 | docker run -d --name minio \ |
注意:
- docker 方式拉取的 minio 镜像是标准的社区版本;minio 企业版本需要认证(认证费)
- 2025 年 5 月的时候,社区版本的功能被阉割了,去掉了权限修改的功能(只是阉割了 9001 的可视化管理端,使用代码都一样)
方案二:手工搭建
- 这个版本提供了权限修改的功能
8.2 可视化操作和使用(minio 的核心概念)
- 访问 minio 客户端:
ip : 9001 - 登录和认证:
minioadmin / minioadmin - 创建一个桶:一个项目对应一个桶
- 上传文件或者文件夹,桶内结构例如:
1 | 桶 |
访问地址规则
- minio 部署的时候可以添加 ssl 证书(安全)
- 地址格式:
http | https→ 端点 + 桶 + 桶内全路径(对象名) - 例如
http://ip:9000/0318tong/mv.jpg、http://ip:9000/0318tong/images/mv.jpg
拒绝访问问题
- minio 文件服务器默认:上传需要认证(登录),访问也需要认证(直接访问会被拒绝)
- 解决思路:1)访问时添加认证;2)修改桶权限——上传需要认证,但访问可以匿名
修改桶权限的三种模式
| 权限 | 上传 | 访问 |
|---|---|---|
private |
需要认证 | 需要认证 |
public |
不需要认证 | 不需要认证 |
custom |
自定义 | 自定义 |
custom 自定义策略示例:
1 | { |
8.3 Python 中使用 minio 进行文件 CRUD
核心概念:
- 端点 →
ip地址:9000,具体的文件服务器地址 - 桶 → 名称,主要是区分不同项目
- 对象 → 具体存储的文件,如
xx.zip、xx/xxx/xxx.zip - 访问地址 →
http | https://+ 端点 + 桶 + 对象
使用 Python 进行 minio 客户端登录:
1 | from minio import Minio |
创建桶并设置权限:
1 | found = client.bucket_exists(bucket_name) |
上传 / 删除 / 查看文件:
1 | # 上传 |
list_objects 返回的层级大致是:
1 | 桶名 |
8.4 封装一个 minio 使用的工具类
- 专门创建客户端和返回客户端对象,隐藏桶的创建和权限的设置
- 位置:
app/shared/clients/minio_utils(读,不需要写) - 再封装一个公共对外访问类,位置:
app/infra/object_stroage/minio_gateway(写)
1 | from app.shared.clients.minio_utils import get_minio_client |
9、模型工具类和 infra 封装
9.1 解读模型初始化工具
模型初始化的公共工具(app/shared/model/lm_utils)负责按配置创建模型客户端,上层的 LLMProvider 只是对它做一层项目级封装。
9.2 封装一个模型对应的 infra
1 | from app.infra.config.providers import infra_config |
10、正则使用回顾
10.1 基本用法
1 | import re |
10.2 贪婪匹配与非贪婪匹配
1 | text = "A123B456B" |
要点小结:
re.escape()用来转义图片名中的字面字符(如demo(1).png的括号),避免被当成元字符search找第一个、match只从开头、findall返回字符串列表、finditer返回 Match 对象Match对象可通过.group()、.start()、.end()、.span()获取内容与位置.*贪婪、.*?非贪婪,图片语法必须用非贪婪避免跨越多个图片
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 𝐺ℎ𝑎𝑜 の 𝐵𝑙𝑜𝑔!
评论
