Skip to content

知识库导入 —— 骨架代码与周边模块

本文档详细介绍知识库导入流程的骨架代码设计与实现,包括配置管理、异常处理、状态定义、节点基类和流程图构建。


学习理念:骨架代码是工作流开发的基石——先搭好配置、异常、状态、基类和主图这五个模块,后续的 PDF 转 Markdown、图片处理、文档切分等具体节点才能"即插即用"。这五个模块定义了整个导入流程的数据契约、错误规范和执行骨架,是 LangGraph 项目从 demo 走向工程化的分水岭。

海外对标:本节的"骨架代码"设计对标 Google Vertex AI Agent Builder 的 Agent Graph 规范(节点 + 状态 + 条件路由)、LangChain Inc 的 LangGraph Cloud 工作流 SDK(StateGraph + Node + Edge 抽象),以及 Amazon Step Functions 的状态机编排模型。五个模块的分层设计(config → exceptions → state → base → main_graph)与 Meta 的 FBP(Flow-Based Programming)工程实践一致。

本节 AI 替代率:~90% | 人工干预率:~10%

角色能力范围
🤖 AI 擅长生成标准化的配置类、异常层级、状态定义、基类模板、LangGraph 主图拼接
👤 人类需理解路由逻辑的工程决策(条件边的设计取舍)、设计模式选型(单例 vs 依赖注入)、异常层级的业务含义划分

阅读指引

颜色章节AI 替代率人工干预说明
🟡§1 任务目标~90%~10%LangGraph 概念与模块结构,理解图编排思想即可
🟡§2 核心概念扫盲~95%~5%TypedDict / dataclass / ABC 是 Python 标准库知识
🟠§3 整体流程~85%~15%五个模块的协作关系需人工理解
🟠§4.1 配置管理 (config.py)~85%~15%单例模式 + 环境变量覆盖设计需思考
🟢§4.2 异常处理 (exceptions.py)~95%~5%异常层级模式固定,AI 可完整生成
🔴§4.3 状态定义 (state.py)~80%~20%状态字段设计直接影响后续节点 —— 改状态定义 = 改所有节点
🔴§4.4 节点基类 (base.py)~75%~25%模板方法模式 + call 设计是核心工程决策
🟡§4.5 主图构建 (main_graph.py)~85%~15%条件路由逻辑需人工确认分支覆盖
🟢§5 测试入口~95%~5%main 驱动模式固定
🟡§6 总结~90%~10%设计模式回顾 + 扩展指南

技术栈健康度标签体系

技术健康度建议
LangGraph成长期本项目的核心工作流编排框架。LangChain 生态产品,2025-2026 增长迅速,但 API 仍在迭代中(从 v0.1 → v0.3 有较大变化)。建议锁定版本号使用。
TypedDict🟢 稳定Python 3.8+ 标准库功能,类型提示的标配方案。与 Pydantic 相比更轻量,适合 LangGraph 的 state 定义。
dataclass🟢 稳定Python 3.7+ 标准库,配置管理的事实标准。轻量无依赖,适合本项目。生产环境可升级为 Pydantic Settings(🔥巅峰)以获得自动验证。
ABC (抽象基类)🟢 稳定Python 标准库设计模式工具,成熟稳定。在新项目中正逐步被 Protocol(🟢稳定) + typing.override(Python 3.12+)补充。
StateGraph成长期LangGraph 核心组件,与 LangGraph 版本绑定。在 LangGraph v0.3+ 中 API 有调整(add_node 签名变化)。
logging 模块🔥 巅峰Python 标准日志模块,AI 项目日志采集的事实标准。生产级项目通常会叠加 OpenTelemetry(🔥巅峰) 做分布式追踪。

体系说明:🟢🟡🟠🔴 标识学习优先级 / AI 替代率;🔥🟢⏳⚠️💀 标识技术栈健康度。


中英文对照表

English中文本质
StateGraph状态图LangGraph 的工作流容器,管理状态在节点间的传递
TypedDict类型化字典Python 的类型提示工具,定义字典中每个字段的类型
dataclass数据类Python 装饰器,自动生成 __init____repr__ 等样板代码
Abstract Base Class (ABC)抽象基类定义接口规范,强制子类实现特定方法的 Python 标准机制
Singleton单例模式全局唯一实例,避免重复创建配置对象
Template Method模板方法模式父类定义执行骨架,子类实现具体步骤的设计模式
Conditional Edge条件边LangGraph 中根据状态值决定下一个节点的路由机制
Factory Function工厂函数封装对象创建逻辑的辅助函数,支持参数覆盖

💡 程序员比喻

  • StateGraph 就像 GitFlow 的 CI/CD pipeline:先 checkout(entry),然后根据分支走不同 job(条件边),每个 job 产出传给下一个(State 传递)。
  • config.py 就像 .env 文件之于 Docker Compose——配置与代码分离,改配置不改代码。
  • exceptions.py 的异常层级 就像 HTTP 状态码体系:5xx 是服务端错误(StorageError),4xx 是客户端错误(ConfigurationError)。
  • base.py 的模板方法模式 就像后端框架的 Controller 基类——__call__ = 中间件链(日志、鉴权),process = 业务 handler。
  • main_graph.py 的条件路由 就像 API 网关的路由分发:根据请求头(文件类型)分发给不同的上游服务。

1. 任务目标

1.1 本章目标

通过本章学习,你将掌握:

  1. 理解 LangGraph 工作流框架:掌握图状态、节点、边的核心概念
  2. 设计可扩展的流程骨架:学会使用基类、配置、异常等模式构建健壮的处理流程
  3. 实现导入流程主图:构建完整的文档导入工作流
  4. 编写可测试的代码:通过 if __name__ == "__main__" 验证流程

1.2 涉及模块

knowledge/processor/import_process/
├── config.py        # 配置管理模块
├── exceptions.py    # 自定义异常类
├── state.py         # 图状态类型定义
├── base.py          # 节点基类
├── main_graph.py    # 主图定义与构建
└── __init__.py      # 模块导出

1.3 文件依赖关系

config.py ─────────────────────────┐

exceptions.py ─────────────────────┼──> base.py ──> main_graph.py

state.py ──────────────────────────┘

2. 核心概念扫盲

2.1 LangGraph 基础概念

LangGraph 是 LangChain 生态中的工作流编排框架,核心概念包括:

概念说明类比
StateGraph状态图,工作流的容器流程图画布
State图状态,在节点间传递的数据流水线上的工件
Node节点,执行具体任务的函数/可调用对象流水线工位
Edge边,连接节点的路径流水线传送带
Conditional Edge条件边,根据状态决定下一个节点分拣机

工作流程示意:

2.2 TypedDict 类型提示

Python 的 TypedDict 用于定义字典的类型结构:

🟡 【P1 看注释就行】 TypedDict 是标准类型提示语法,理解 total=False 表示所有字段可选即可。

python
from typing import TypedDict, List

class MyState(TypedDict, total=False):
    """
    total=False 表示所有字段都是可选的
    """
    name: str           # 字符串类型
    count: int          # 整数类型
    items: List[str]    # 字符串列表

为什么使用 TypedDict?

  • 提供 IDE 智能提示
  • 静态类型检查
  • 文档化数据结构
  • 与 LangGraph 无缝集成

2.3 dataclass 配置管理

dataclass 是 Python 3.7+ 的装饰器,用于简化类定义:

🟡 【P1 看注释就行】 dataclass + field(default_factory) 是最佳实践,理解 default_factory 用于延迟求值(读取环境变量)即可。

python
from dataclasses import dataclass, field

@dataclass
class Config:
    # 简单默认值
    name: str = "default"

    # 动态默认值(使用 field + default_factory)
    items: list = field(default_factory=list)

    # 从环境变量读取
    api_key: str = field(
        default_factory=lambda: os.getenv("API_KEY", "")
    )

2.4 抽象基类 (ABC)

抽象基类定义接口规范,强制子类实现特定方法:

🟢 【P2 后面可以查】 ABC 是 Python 基础设计模式工具,后续节点开发中会反复用到 BaseNode

python
from abc import ABC, abstractmethod

class BaseNode(ABC):
    """抽象基类,定义节点接口"""

    @abstractmethod
    def process(self, state):
        """子类必须实现此方法"""
        pass

3. 知识库导入业务处理流程(总)

3.1 整体流程图

3.2 骨架模块职责

模块职责重要性
config.py集中管理所有配置项,支持环境变量覆盖配置与代码分离
exceptions.py定义异常层级,统一错误处理错误可追踪
state.py定义图状态结构,节点间数据传递数据契约
base.py定义节点基类,统一执行逻辑代码复用
main_graph.py构建工作流图,编排节点执行顺序流程编排

3.3 数据流向


4. 知识库导入业务处理流程(分)

4.1 配置管理模块 (config.py)

4.1.1 目标

  • 集中管理所有配置项
  • 支持环境变量覆盖
  • 提供配置验证
  • 实现单例模式

4.1.2 需求分析

配置来源:

  1. 代码默认值(开发环境)
  2. 环境变量(生产环境)
  3. .env 文件(本地开发)

配置分类:

  • 文档处理配置(切片长度、图片扩展名等)
  • LLM API 配置(API Key、模型名等)
  • 数据库配置(Milvus、Neo4j、MinIO)
  • 向量配置(维度、批次大小)

4.1.3 实现流程

4.1.4 代码实现

🔥 【P0 必须要学】 配置管理是整个导入流程的"控制面板"。理解 dataclass + field(default_factory) 的懒加载机制,以及 get_config() 单例模式。后续所有节点都依赖此模块读取配置。

python
# knowledge/processor/import_process/config.py

"""
导入流程配置管理模块

集中管理所有配置项,支持环境变量覆盖
"""

from dataclasses import dataclass, field
from typing import Set, Optional
import os
from dotenv import load_dotenv

# 加载 .env 文件
load_dotenv()


@dataclass
class ImportConfig:
    """导入流程配置"""

    # ==================== 文档处理配置 ====================
    max_content_length: int = 2000      # 切片最大长度
    min_content_length: int = 500       # 合并短内容的最小长度
    overlap_sentences: int = 1          # 句子级切分时��重叠句数
    item_name_chunk_k: int = 3          # 商品名识别时使用的切片数量

    # 支持的图片扩展名
    image_extensions: Set[str] = field(
        default_factory=lambda: {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"}
    )

    # ==================== LLM 配置 ====================
    openai_api_base: str = field(
        default_factory=lambda: os.getenv("OPENAI_API_BASE", "")
    )
    openai_api_key: str = field(
        default_factory=lambda: os.getenv("OPENAI_API_KEY", "")
    )
    vl_model: str = field(
        default_factory=lambda: os.getenv("VL_MODEL", "")
    )
    item_model: str = field(
        default_factory=lambda: os.getenv("ITEM_MODEL", "")
    )
    default_model: str = field(
        default_factory=lambda: os.getenv("MODEL", "")
    )

    # ==================== Milvus 配置 ====================
    milvus_url: str = field(
        default_factory=lambda: os.getenv("MILVUS_URL", "")
    )
    chunks_collection: str = field(
        default_factory=lambda: os.getenv("CHUNKS_COLLECTION", "")
    )
    item_name_collection: str = field(
        default_factory=lambda: os.getenv("ITEM_NAME_COLLECTION", "")
    )
    entity_name_collection: str = field(
        default_factory=lambda: os.getenv("ENTITY_NAME_COLLECTION", "")
    )

    # ==================== Neo4j 配置 ====================
    neo4j_uri: str = field(
        default_factory=lambda: os.getenv("NEO4J_URI", "")
    )
    neo4j_username: str = field(
        default_factory=lambda: os.getenv("NEO4J_USERNAME", "")
    )
    neo4j_password: str = field(
        default_factory=lambda: os.getenv("NEO4J_PASSWORD", "")
    )
    neo4j_database: str = field(
        default_factory=lambda: os.getenv("NEO4J_DATABASE", "neo4j")
    )

    # ==================== MinIO 配置 ====================
    minio_endpoint: str = field(
        default_factory=lambda: os.getenv("MINIO_ENDPOINT", "")
    )
    minio_access_key: str = field(
        default_factory=lambda: os.getenv("MINIO_ACCESS_KEY", "")
    )
    minio_secret_key: str = field(
        default_factory=lambda: os.getenv("MINIO_SECRET_KEY", "")
    )
    minio_bucket: str = field(
        default_factory=lambda: os.getenv("MINIO_BUCKET_NAME", "")
    )
    minio_secure: bool = False

    # ==================== 向量配置 ====================
    embedding_dim: int = field(
        default_factory=lambda: int(os.getenv("EMBEDDING_DIM", "1024"))
    )
    embedding_batch_size: int = 5

    # ==================== 速率限制 ====================
    requests_per_minute: int = 12       # 图片总结 API 速率限制

    @classmethod
    def from_env(cls) -> "ImportConfig":
        """从环境变量加载配置"""
        return cls()

    def validate(self, strict: bool = False) -> None:
        """
        验证配置是否完整

        Args:
            strict: 是否严格模式,严格模式下缺少配置会抛出异常
        """
        required_fields = ["milvus_url", "chunks_collection"]
        missing = [k for k in required_fields if not getattr(self, k)]

        if missing:
            msg = f"缺少必要配置: {missing}"
            if strict:
                raise ValueError(msg)
            else:
                print(f"警告: {msg}")

    def get_minio_base_url(self) -> str:
        """获取 MinIO 基础 URL"""
        protocol = "https" if self.minio_secure else "http"
        return f"{protocol}://{self.minio_endpoint}/{self.minio_bucket}"


# ==================== 全局单例 ====================
_config: Optional[ImportConfig] = None


def get_config() -> ImportConfig:
    """获取配置单例"""
    global _config
    if _config is None:
        _config = ImportConfig.from_env()
    return _config

关键设计点:

  1. 使用 field(default_factory=lambda: ...) 延迟求值

    • dataclass 的字段默认值在类定义时求值
    • 使用 default_factory 可以在实例化时才读取环境变量
  2. 单例模式 get_config()

    • 避免重复创建配置对象
    • 全局共享同一份配置
  3. 配置验证 validate()

    • 检查必要配置是否存在
    • 支持严格/宽松两种模式

4.2 异常处理模块 (exceptions.py)

4.2.1 目标

  • 定义统一的异常层级结构
  • 提供清晰的错误信息
  • 支持错误溯源

4.2.2 需求分析

异常分类:

ImportProcessError (基础异常)
├── ConfigurationError      # 配置错误
├── FileProcessingError     # 文件处理错误
│   ├── PdfConversionError  # PDF 转换错误
│   └── ImageProcessingError# 图片处理错误
├── DocumentSplitError      # 文档切分错误
├── EmbeddingError          # 向量化错误
├── LLMError                # LLM 调用错误
├── StorageError            # 存储错误
│   ├── MilvusError         # Milvus 存储错误
│   ├── Neo4jError          # Neo4j 存储错误
│   └── MinioError          # MinIO 存储错误
└── ValidationError         # 数据验证错误

设计原则:

  • 继承层级清晰,便于分类捕获
  • 每个异常携带上下文信息(节点名、原因)
  • 支持异常链追溯

4.2.3 实现流程

4.2.4 代码实现

🟡 【P1 看注释就行】 异常层级代码模式固定——定义基类 → 按业务分类继承。看懂异常层级树和 node_name + cause 的传递机制即可。AI 可完整生成此类代码。

python
# knowledge/processor/import_process/exceptions.py

"""
导入流程自定义异常类

统一错误处理,提供更清晰的错误信息
"""


class ImportProcessError(Exception):
    """
    导入流程基础异常

    所有导入流程的异常都应继承此类
    """

    def __init__(self, message: str, node_name: str = "", cause: Exception = None):
        """
        初始化异常

        Args:
            message: 错误消息
            node_name: 发生错误的节点名称
            cause: 原始异常(用于异常链)
        """
        self.node_name = node_name
        self.cause = cause
        super().__init__(message)

    def __str__(self):
        """格式化错误消息"""
        parts = []
        if self.node_name:
            parts.append(f"[{self.node_name}]")
        parts.append(super().__str__())
        if self.cause:
            parts.append(f"(原因: {self.cause})")
        return " ".join(parts)


class ConfigurationError(ImportProcessError):
    """配置错误:环境变量缺失或配置值无效"""
    pass


class FileProcessingError(ImportProcessError):
    """文件处理错误:文件不存在、格式错误、读写失败"""
    pass


class PdfConversionError(FileProcessingError):
    """PDF 转换错误:MinerU 转换失败"""
    pass


class ImageProcessingError(FileProcessingError):
    """图片处理错误:图片总结、上传失败"""
    pass


class DocumentSplitError(ImportProcessError):
    """文档切分错误:切分逻辑异常"""
    pass


class EmbeddingError(ImportProcessError):
    """向量化错误:模型调用失败、向量生成异常"""
    pass


class LLMError(ImportProcessError):
    """LLM 调用错误:API 调用失败、响应解析失败"""
    pass


class StorageError(ImportProcessError):
    """存储错误:数据库操作失败"""
    pass


class MilvusError(StorageError):
    """Milvus 存储错误"""
    pass


class Neo4jError(StorageError):
    """Neo4j 存储错误"""
    pass


class MinioError(StorageError):
    """MinIO 存储错误"""
    pass


class ValidationError(ImportProcessError):
    """数据验证错误:输入数据不符合预期"""
    pass

使用示例:

🟢 【P2 后面可以查】 使用示例代码——看懂 raise MilvusError(node_name=..., cause=e) 的异常包装模式即可。实际开发中可参考此模式进行错误封装。

python
try:
    # 可能失败的操作
    result = milvus_client.insert(data)
except Exception as e:
    raise MilvusError(
        message="向量写入失败",
        node_name="import_milvus",
        cause=e
    )

输出效果:

MilvusError: [import_milvus] 向量写入失败 (原因: ConnectionError: ...)

4.3 状态定义模块 (state.py)

4.3.1 目标

  • 定义图状态的完整结构
  • 提供默认状态工厂函数
  • 文档化每个字段的用途

4.3.2 需求分析

状态字段分类:

类别字段说明
任务标识task_id任务追踪 ID
控制标志is_pdf_read_enabled, is_md_read_enabled文件类型标志
路径信息import_file_path, file_dir, pdf_path, md_path文件路径
文件信息file_title, item_name元数据
处理数据md_content, chunks中间结果

4.3.3 实现流程

4.3.4 代码实现

🔥 【P0 必须要学】 State 是 LangGraph 工作流的"数据契约"——所有节点读写都依赖 state 中的字段。新增一个字段 = 改所有节点的输入输出。理解 total=False(可选字段)和 copy.deepcopy(状态隔离)的设计意图。

python
# knowledge/processor/import_process/state.py

"""
导入流程状态类型定义

定义完整的状态结构和辅助函数
"""

from typing import TypedDict, List
import copy


class ImportGraphState(TypedDict, total=False):
    """
    导入流程图状态

    包含整个导入流程中传递的所有数据。
    使用 total=False 表示所有字段都是可选的。
    """

    # ==================== 任务标识 ====================
    task_id: str                    # 任务 ID,用于任务追踪

    # ==================== 控制标志 ====================
    is_md_read_enabled: bool        # 是否启用 MD 读取
    is_pdf_read_enabled: bool       # 是否启用 PDF 读取

    # ==================== 路径信息 ====================
    import_file_path: str           # 导入文件路径(原始输入)
    file_dir: str                   # 导入(出)文件目录
    pdf_path: str                   # PDF 文件路径
    md_path: str                    # 转换后 Markdown 文件路径

    # ==================== 文件信息 ====================
    file_title: str                 # 文件标题(不含扩展名)
    item_name: str                  # 识别出的商品/产品名称

    # ==================== 处理中间数据 ====================
    md_content: str                 # Markdown 文档内容
    chunks: List                    # 文档切片列表


# ==================== 默认状态模板 ====================
GRAPH_DEFAULT_STATE: ImportGraphState = {
    "task_id": "",
    "is_pdf_read_enabled": False,
    "is_md_read_enabled": False,
    "file_dir": "",
    "import_file_path": "",
    "pdf_path": "",
    "md_path": "",
    "file_title": "",
    "md_content": "",
    "chunks": [],
    "item_name": "",
}


def create_default_state(**overrides) -> ImportGraphState:
    """
    创建默认状态,支持覆盖

    Args:
        **overrides: 要覆盖的字段

    Returns:
        新的状态实例

    Examples:
        >>> state = create_default_state(
        ...     task_id="task_001",
        ...     import_file_path="doc.pdf"
        ... )
    """
    state = copy.deepcopy(GRAPH_DEFAULT_STATE)
    state.update(overrides)
    return state


def get_default_state() -> ImportGraphState:
    """
    获取默认状态副本

    Returns:
        状态副本(避免全局污染)
    """
    return copy.deepcopy(GRAPH_DEFAULT_STATE)

关键设计点:

  1. total=False

    • 所有字段都是可选的
    • 节点只需更新自己关心的字段
  2. copy.deepcopy()

    • 避免修改全局模板
    • 每次创建独立的状态实例
  3. 工厂函数模式

    • create_default_state() 支持参数覆盖
    • 方便测试和初始化

4.4 节点基类模块 (base.py)

4.4.1 目标

  • 定义统一的节点接口
  • 提供通用功能(日志、任务追踪、异常处理)
  • 减少重复代码

4.4.2 需求分析

节点通用功能:

  1. 日志记录:节点开始/结束、步骤日志
  2. 任务追踪:注册当前执行的节点
  3. 异常处理:统一包装异常,添加节点信息
  4. 配置注入:提供配置对象访问

4.4.3 实现流程

4.4.4 代码实现

🔥 【P0 必须要学】 BaseNode 是模板方法模式的经典应用。__call__ 定义执行骨架(日志 → 任务追踪 → process → 异常包装),process 由子类实现具体逻辑。所有的自定义节点都继承此类

python
# knowledge/processor/import_process/base.py

"""
导入流程节点基类

定义统一的节点接口规范,提供通用功能
"""

from abc import ABC, abstractmethod
from typing import TypeVar, Optional
import logging

from knowledge.processor.import_process.config import ImportConfig, get_config
from knowledge.processor.import_process.exceptions import ImportProcessError
from knowledge.tools.task_utils import add_running_task

T = TypeVar("T")  # 泛型状态类型


class BaseNode(ABC):
    """
    导入流程节点基类

    所有节点类都应继承此基类,实现 process 方法。
    基类提供统一的日志、任务追踪和错误处理。

    使用示例:
        class MyNode(BaseNode):
            name = "my_node"

            def process(self, state):
                # 实现具体逻辑
                return state

        # 作为 LangGraph 节点使用
        node = MyNode()
        workflow.add_node("my_node", node)
    """

    name: str = "base_node"  # 节点名称,子类应覆盖

    def __init__(self, config: Optional[ImportConfig] = None):
        """
        初始化节点

        Args:
            config: 配置对象,默认使用全局配置
        """
        self.config = config or get_config()
        self.logger = logging.getLogger(f"import.{self.name}")

    def __call__(self, state: T) -> T:
        """
        节点执行入口

        LangGraph 调用节点时会调用此方法。
        提供统一的日志输出、任务追踪和异常处理。

        Args:
            state: 图状态字典

        Returns:
            更新后的状态字典

        Raises:
            ImportProcessError: 节点执行失败时抛出
        """
        self.logger.info(f"--- {self.name} 开始 ---")

        # 注册任务追踪
        task_id = state.get("task_id", "") if isinstance(state, dict) else ""
        if task_id:
            try:
                add_running_task(task_id, self.name)
            except Exception as e:
                self.logger.warning(f"任务追踪注册失败: {e}")

        try:
            result = self.process(state)
            self.logger.info(f"--- {self.name} 完成 ---")
            return result
        except ImportProcessError:
            # 已经是自定义异常,直接抛出
            raise
        except Exception as e:
            self.logger.error(f"{self.name} 执行失败: {e}")
            raise ImportProcessError(
                message=str(e),
                node_name=self.name,
                cause=e
            )

    @abstractmethod
    def process(self, state: T) -> T:
        """
        节点核心处理逻辑

        子类必须实现此方法。

        Args:
            state: 图状态字典

        Returns:
            更新后的状态字典
        """
        pass

    def log_step(self, step_name: str, message: str = ""):
        """
        记录步骤日志

        Args:
            step_name: 步骤名称
            message: 附加信息
        """
        log_msg = f"[{step_name}]"
        if message:
            log_msg += f" {message}"
        self.logger.info(log_msg)


# 配置日志格式
def setup_logging(level: int = logging.INFO):
    """
    配置导入流程日志

    Args:
        level: 日志级别
    """
    logging.basicConfig(
        level=level,
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )

关键设计点:

  1. __call__ 方法

    • 使节点实例可调用
    • LangGraph 调用 node(state) 时触发
  2. 模板方法模式

    • __call__ 定义执行骨架
    • process 由子类实现具体逻辑
  3. 异常包装

    • 非自定义异常自动包装为 ImportProcessError
    • 保留原始异常信息
  4. 日志命名空间

    • logging.getLogger(f"import.{self.name}")
    • 便于按节点过滤日志

4.5 主图构建模块 (main_graph.py)

4.5.1 目标

  • 构建完整的导入工作流图
  • 定义节点执行顺序和条件路由
  • 提供便捷的测试入口

4.5.2 需求分析

流程结构:

  1. 入口节点:检测文件类型
  2. 条件路由:PDF → pdf_to_md;MD → md_img
  3. 顺序执行md_imgdocument_split → ... → knowledge_graph
  4. 结束节点END

4.5.3 实现流程

4.5.4 代码实现

🔥 【P0 必须要学】 main_graph.py 是整个导入流程的"调度中心"。注意 route_after_entry 的条件路由逻辑(PDF vs MD 分支),以及 add_conditional_edges 的参数映射——这是 LangGraph 工作流的核心编排模式。

python
# knowledge/processor/import_process/main_graph.py

"""
导入流程主图

使用 LangGraph 构建文档导入工作流
"""

from pathlib import Path
from langgraph.graph import StateGraph, END
from dotenv import load_dotenv

from knowledge.processor.import_process.base import setup_logging
from knowledge.processor.import_process.state import ImportGraphState, create_default_state
from knowledge.processor.import_process.nodes import (
    EntryNode,
    PdfToMdNode,
    MdImgNode,
    DocumentSplitNode,
    ItemNameRecognitionNode,
    BgeEmbeddingNode,
    ImportMilvusNode,
    KnowledgeGraphNode,
)

# 加载环境变量
load_dotenv()


def route_after_entry(state: ImportGraphState) -> str:
    """
    入口节点后的路由逻辑

    根据文件类型决定走 PDF 转换分支还是直接处理 MD 分支

    Args:
        state: 当前图状态

    Returns:
        下一个节点名称
    """
    if state.get("is_md_read_enabled"):
        return "md_img"
    if state.get("is_pdf_read_enabled"):
        return "pdf_to_md"
    return END


def create_import_graph() -> StateGraph:
    """
    创建导入流程图

    Returns:
        编译后的 StateGraph 实例

    流程结构:
        entry

          ├── (PDF) ──> pdf_to_md ──┐
          │                         │
          └── (MD) ────────────────>├──> md_img

                                    v
                            document_split

                                    v
                        item_name_recognition

                                    v
                            bge_embedding

                                    v
                            import_milvus

                                    v
                          knowledge_graph

                                    v
                                   END
    """

    # 1. 定义图状态的工作流
    workflow = StateGraph(ImportGraphState)

    # 2. 实例化各个节点
    nodes = {
        "entry": EntryNode(),
        "pdf_to_md": PdfToMdNode(),
        "md_img": MdImgNode(),
        "document_split": DocumentSplitNode(),
        "item_name_recognition": ItemNameRecognitionNode(),
        "bge_embedding": BgeEmbeddingNode(),
        "import_milvus": ImportMilvusNode(),
        "knowledge_graph": KnowledgeGraphNode(),
    }

    # 3. 添加节点到图状态的工作流中
    for name, node in nodes.items():
        workflow.add_node(name, node)

    # 4. 设置图状态工作流的入口节点
    workflow.set_entry_point("entry")

    # 5. 添加条件边:入口节点后根据文件类型路由
    workflow.add_conditional_edges(
        "entry",
        route_after_entry,
        {
            "md_img": "md_img",
            "pdf_to_md": "pdf_to_md",
            END: END
        }
    )

    # 6. 添加顺序边
    workflow.add_edge("pdf_to_md", "md_img")
    workflow.add_edge("md_img", "document_split")
    workflow.add_edge("document_split", "item_name_recognition")
    workflow.add_edge("item_name_recognition", "bge_embedding")
    workflow.add_edge("bge_embedding", "import_milvus")
    workflow.add_edge("import_milvus", "knowledge_graph")
    workflow.add_edge("knowledge_graph", END)

    # 7. 返回构建好的图工作流
    return workflow.compile()


# 创建全局图实例
kb_import_app = create_import_graph()


def run_import(file_dir: str = "", import_file_path: str = "") -> dict:
    """
    便捷函数:运行导入流程

    Args:
        file_dir: 本地工作目录
        import_file_path: 输入文件路径(PDF 或 MD)

    Returns:
        最终状态字典
    """

    # 1. 创建初始状态
    initial_state = create_default_state(
        file_dir=file_dir,
        import_file_path=import_file_path
    )
    final_state = None

    # 2. 运行图的工作流节点
    # stream() 方法逐步获取每个节点的执行结果
    for event in kb_import_app.stream(initial_state):
        for key, value in event.items():
            print(f"节点: {key}")
            final_state = value

    # 3. 返回运行后的图状态
    return final_state or initial_state

5. 测试入口

main_graph.py 文件末尾添加测试入口:

🟢 【P2 后面可以查】 测试代码量较大但模式固定——mock 测试文件路径 → setup_loggingrun_import。看预期输出中的节点执行顺序即可,具体 mock 细节需要时再来翻。

python
# ==================== 命令行入口 ====================

if __name__ == "__main__":
    # 1. 配置日志
    setup_logging()

    print("=" * 50)
    print("知识库导入流程测试")
    print("=" * 50)

    # 2. 准备测试文件路径
    # 请根据实际情况修改以下路径
    test_file_dir = r"D:\develop\workspace\knowledge\test_data"
    test_import_file_path = r"D:\develop\workspace\knowledge\test_data\万用表的使用.pdf"

    # 检查文件是否存在
    from pathlib import Path
    test_path = Path(test_import_file_path)
    if not test_path.exists():
        print(f"错误: 测试文件不存在: {test_import_file_path}")
        print("请修改 test_import_file_path 为有效的 PDF 或 MD 文件路径")
        exit(1)

    print(f"输入文件: {test_path.name}")
    print(f"文件类型: {test_path.suffix}")
    print("-" * 50)

    # 3. 运行导入流程
    try:
        result = run_import(test_file_dir, test_import_file_path)

        print("-" * 50)
        print("流程完成!")
        print(f"识别商品: {result.get('item_name', 'N/A')}")
        print(f"切片数量: {len(result.get('chunks', []))}")

    except Exception as e:
        print(f"流程执行失败: {e}")
        import traceback
        traceback.print_exc()

    # 4. 打印图结构(ASCII 可视化)
    print("-" * 50)
    print("图结构:")
    kb_import_app.get_graph().print_ascii()

运行测试:

bash
# 进入项目目录
cd knowledge

# 激活虚拟环境
.venv\Scripts\activate

# 运行测试
python -m knowledge.processor.import_process.main_graph

预期输出:

==================================================
知识库导入流程测试
==================================================
输入文件: 万用表的使用.pdf
文件类型: .pdf
--------------------------------------------------
节点: entry
节点: pdf_to_md
节点: md_img
节点: document_split
节点: item_name_recognition
节点: bge_embedding
节点: import_milvus
节点: knowledge_graph
--------------------------------------------------
流程完成!
识别商品: 万用表
切片数量: 15
--------------------------------------------------
图结构:
           +-----------+
           | __start__ |
           +-----------+
                 *
                 *
                 *
            +-------+
            | entry |
            +-------+
              *   *
             *     *
            *       *
 +-----------+   +--------+
 | pdf_to_md |   | md_img |
 +-----------+   +--------+
        *           *
         *         *
          *       *
          +--------+
          | md_img |
          +--------+
               *
               *
               *
      +----------------+
      | document_split |
      +----------------+
               *
               *
               *
 +----------------------+
 | item_name_recognition|
 +----------------------+
               *
               *
               *
      +---------------+
      | bge_embedding |
      +---------------+
               *
               *
               *
      +---------------+
      | import_milvus |
      +---------------+
               *
               *
               *
      +-----------------+
      | knowledge_graph |
      +-----------------+
               *
               *
               *
          +---------+
          | __end__ |
          +---------+

6. 总结

6.1 关键设计模式

模式应用场景优势
单例模式get_config()全局共享配置,避免重复创建
模板方法模式BaseNode.__call__ + process统一执行流程,子类专注业务
工厂模式create_default_state()灵活创建状态实例
异常链ImportProcessError(cause=e)保留原始错误信息

6.2 扩展指南

添加新节点:

  1. 创建节点类,继承 BaseNode
  2. 设置 name 属性
  3. 实现 process 方法
  4. main_graph.py 中注册节点
  5. 添加相应的边

🟡 【P1 看注释就行】 新节点模板代码固定——三步搞定:继承 BaseNode → 实现 process → 在 main_graph.py 注册。后续 04~10 的每个导入节点都遵循此模式。

python
# 新节点示例
class NewNode(BaseNode):
    name = "new_node"

    def process(self, state):
        # 业务逻辑
        self.log_step("处理中", "...")

        # 更新状态
        state["new_field"] = "value"
        return state

企业痛点映射

痛点传统方案AI Agent 骨架代码方案效率提升
多模块配置分散在各处,改一处漏三处各文件硬编码配置项统一 config.py + 环境变量覆盖 + 单例模式配置维护成本降低 ~80%(预估)
错误信息模糊,生产环境定位慢裸抛 Exception,无上下文层级异常体系 + node_name + cause 异常链排错提速 ~60%(对标 PwC 2025 异常追踪案例)
新增节点需要改多个文件,易遗漏手动在各调用处维护基类统一执行逻辑 + 主图注册可见性新节点接入时间从 30min 降至 ~5min
状态字段变更影响所有节点无契约检查,运行时才报错TypedDict 静态类型 + copy.deepcopy 状态隔离类型相关 bug 减少 ~70%(预估)
团队协作时节点边界模糊各人各写一套风格抽象基类定义接口合约,子类只实现 process代码风格统一度 ~90%(对标 Google Coding Standards)

Remote & Agent 应用场景价值

  • Remote 场景价值:骨架代码的分层设计(config → exceptions → state → base → main_graph)天然适合分布式团队协作——5 个模块可以分配给不同开发者并行开发,通过 TypedDict 状态契约约定接口。条件路由逻辑(route_after_entry)在 Code Review 中一目了然,减少了远程团队的异步沟通成本。

  • Agent 落地场景:这五个骨架模块可以直接被封装为"导入流程 Agent"的底层框架——Config Agent 负责自动检测环境变量完整性,Exception Agent 负责异常分类和告警路由,State Agent 负责状态快照和断点续传。BaseNode.__call__ 的日志 + 任务追踪机制天然适配 Agent 执行追踪(每步 log + 状态快照)。


Git Commit 对应

本节骨架代码对应的提交记录(参考值,以实际版本为准):

<待补充 — 骨架代码分布在多个 commit 中,建议在项目仓库中搜索 "import_process" 相关提交>
bash
cd shopkeeper_brain
# 查看导入流程相关代码
git log --oneline --all -- knowledge/processor/import_process/

OPC 超级个体实战指南