Skip to content

阶段一:导入流程 — 文档处理链路(对应课程 day02~05)

当前状态:骨架已就绪(3 节点)→ 新增 3 个文档处理节点

知识标记总览

知识点出现次数扮演的角色
LangGraph StateGraph 节点扩展🟢 第 2 次在现有图上追加 3 个节点
MinerU PDF 解析🔴 第 1 次PDF→Markdown 转换引擎
VLM 图片摘要(视觉模型)🔴 第 1 次用 VL API 理解图片内容
MinIO 对象存储🔴 第 1 次图片的 S3 兼容存储服务
文档切分策略🟢 第 2 次(Ch16→Ch19)按 Markdown 标题层级切分

1.1 阶段起始状态:3 个灰色节点(已有骨架)


1.2 本阶段新增文件

shopkeeper_brain/knowledge/processor/import_process/nodes/
├── __init__.py                  ← 已存在
├── entry_node.py                ← 已存在
├── pdf_to_md_node.py            ← 🔴 新增:PDF→Markdown
├── md_img_node.py               ← 🔴 新增:图片处理
└── document_split_node.py       ← 🔴 新增:文档切分

main_graph.py 修改:追加 3 个节点的 add_node + 对应的 add_edge


1.3 核心代码骨架

① PdfToMdNode:PDF→Markdown(🔴 新知识:MinerU)

python
class PdfToMdNode(BaseNode):
    """使用 MinerU 将 PDF 解析为 Markdown"""
    
    name = "pdf_to_md"

    def process(self, state: ImportGraphState) -> ImportGraphState:
        pdf_path = state.get('pdf_path')
        md_dir = Path(state['file_dir']) / "mineru_output"
        
        # MinerU 命令行解析(两种方式任选)
        # 方式 1:CLI 命令
        # subprocess.run(["mineru", "--pdf", pdf_path, "--output", str(md_dir)])
        
        # 方式 2:Python SDK
        from mineru import MinerU
        mineru = MinerU(config_path="mineru.json")
        result = mineru.parse(pdf_path)
        md_path = md_dir / f"{state['file_title']}.md"
        
        state['md_path'] = str(md_path)
        return state

🔑 MinerU 简介:开源 PDF 解析引擎,比 PyMuPDF / pdfplumber 等库更智能——能识别标题层级、表格、公式、图片。输出结构化的 Markdown。缺点是大模型环境依赖较重(需下载 OCR 模型)。

② MarkDownImageNode:VLM 图片处理(🔴 新知识:VLM + MinIO)

python
class MarkDownImageNode(BaseNode):
    """扫描 MD 中的图片引用,用 VLM 生成摘要,上传 MinIO"""
    
    name = "md_img"

    def process(self, state: ImportGraphState) -> ImportGraphState:
        md_path = Path(state['md_path'])
        md_content = md_path.read_text(encoding='utf-8')
        
        # 1. 正则提取所有图片路径
        img_pattern = r'!\[.*?\]\((.*?)\)'
        img_paths = re.findall(img_pattern, md_content)
        
        # 2. VLM 生成每张图的摘要(带滑动窗口限流)
        summaries = {}
        for img_path in img_paths:
            # VLM API 调用(与普通 LLM 同格式,但传图片 URL)
            summary = self._vlm_describe(img_path)  # "包含电路图的接线示意图"
            summaries[img_path] = summary
        
        # 3. 替换 MD 引用:原引用 → 新引用(含摘要 + MinIO URL)
        for old_path, summary in summaries.items():
            minio_url = self._upload_to_minio(old_path)
            md_content = md_content.replace(
                f'![{old_path}]', 
                f'![{summary}]({minio_url})'
            )
        
        # 4. 写回 MD
        md_path.write_text(md_content, encoding='utf-8')
        state['md_content'] = md_content
        return state
    
    def _vlm_describe(self, img_path: str) -> str:
        """调用 VLM 模型生成图片摘要"""
        # 请求格式(以 OpenAI-compatible API 为例):
        # POST {base_url}/chat/completions
        # { "model": "qwen-vl-plus", "messages": [
        #     {"role": "user", "content": [
        #         {"type": "image_url", "image_url": {"url": f"file://{img_path}"}},
        #         {"type": "text", "text": "请用一句话描述这张图片"}
        #     ]}
        # ]}
        return "图片摘要内容"

    def _upload_to_minio(self, file_path: str) -> str:
        """上传文件到 MinIO,返回可访问的 URL"""
        from minio import Minio
        client = Minio(
            self.config.minio_endpoint,
            access_key=self.config.minio_access_key,
            secret_key=self.config.minio_secret_key,
            secure=self.config.minio_secure,
        )
        bucket = self.config.minio_bucket
        if not client.bucket_exists(bucket):
            client.make_bucket(bucket)
        object_name = f"images/{Path(file_path).name}"
        client.fput_object(bucket, object_name, file_path)
        return f"http://{self.config.minio_endpoint}/{bucket}/{object_name}"

🔑 设计决策:为什么图片处理要单独做一个节点,而不是合并在 PDF 节点里?

  • VLM 调用是 I/O 密集型(网络请求 + 图片传输),PDF 解析是 CPU 密集型(OCR 识别)。两个节点独立可以各自选择不同的重试策略和资源限制
  • VLM 调用有速率限制(requests_per_minute=15),需要独立的滑动窗口队列控制。

③ DocumentSplitNode:文档切分(🟢 第 2 次,Ch16 回顾)

python
class DocumentSplitNode(BaseNode):
    """按 Markdown 一级标题切分文档,短内容合并"""
    
    name = "document_split"

    def process(self, state: ImportGraphState) -> ImportGraphState:
        md_content = state.get('md_content', '')
        
        # 1. 按一级标题切分(# 标题)
        sections = re.split(r'(?=^# )', md_content, flags=re.MULTILINE)
        
        # 2. 构建面包屑路径(标题层级 -> 每个 chunk 保留全局标题路径)
        chunks = []
        for section in sections:
            if not section.strip():
                continue
            # 提取标题路径
            title_path = self._extract_title_path(section)
            # 组装 chunk
            chunks.append({
                "title_path": title_path,
                "content": section.strip(),
                "metadata": {"file_title": state['file_title']}
            })
        
        # 3. 短 chunk 合并(<500 字与相邻合并)
        merged = []
        for chunk in chunks:
            if merged and len(chunk['content']) < 500:
                merged[-1]['content'] += "\n" + chunk['content']
            else:
                merged.append(chunk)
        
        state['chunks'] = merged
        return state

1.4 设计决策

决策选项 A选项 B选择理由
PDF 引擎选型MinerUPyMuPDF / pdfplumberMinerU 能自动识别标题层级和表格结构,输出 Markdown——后续切分直接复用标题结构
VLM 调用方式LLM API(OpenAI-compatible)本地部署 VLMAPI 方式零 GPU 成本,使用滑动窗口限流控制频率
图片存储方式MinIO(S3 兼容)本地文件系统MinIO 提供 HTTP 可访问的 URL,后续查询前端直接引用
切分粒度一级标题二级标题 / 固定长度一级标题粒度适中(每个 section 500-2000 字),二级标题太碎
短内容策略与相邻合并丢弃合并保留信息不丢失

1.5 main_graph 追加代码

python
# → 在 create_import_graph() 中追加
from knowledge.processor.import_process.nodes.pdf_to_md_node import PdfToMdNode
from knowledge.processor.import_process.nodes.md_img_node import MarkDownImageNode
from knowledge.processor.import_process.nodes.document_split_node import DocumentSplitNode

def create_import_graph():
    builder = StateGraph(ImportGraphState)
    
    # 已有节点
    builder.add_node("entry_node", EntryNode())
    # 本阶段新增 3 个节点
    builder.add_node("pdf_to_md_node", PdfToMdNode())
    builder.add_node("md_img_node", MarkDownImageNode())
    builder.add_node("document_split_node", DocumentSplitNode())
    
    # 已有边
    builder.add_conditional_edges("entry_node", import_router, ...)
    # 本阶段新增边
    builder.add_edge("entry_node", "pdf_to_md_node")
    builder.add_edge("pdf_to_md_node", "md_img_node")
    builder.add_edge("md_img_node", "document_split_node")
    
    return builder.compile()

1.6 阶段结束时的演进架构图

本阶段已搭建的节点数:4/8(entry + pdf + md + document_split) 本阶段新增的文件:3 个(pdf_to_md_node.py, md_img_node.py, document_split_node.py) 新增的知识:🔴 MinerU 配置 / 🔴 VLM API 调用 / 🔴 MinIO 上传 / 🟢 文档切分 下一阶段将新增:3 个节点(商品名识别 → 批量嵌入 → Milvus 入库)

📂 对应的原始代码快照day02/3_code/shopkeeper_brain/day05/ 之间的逐日增量

OPC 超级个体实战指南