01-系统架构设计-LangGraph状态机与多源异构RAG

RAG 系统架构设计:基于 LangGraph 的多源异构 RAG 系统

📚 知识库项目实战系列:

  1. RAG 系统架构设计:基于 LangGraph 的多源异构 RAG 系统 ← 本篇
  2. 多路召回与融合排序:RRF + Rerank + 动态 TopK 工程实践
  3. 跨库链路检索:Neo4j 图数据库桥接文档与代码
  4. RAG 检索链路实战:从 ItemName 过滤陷阱到引用去重的工程修复
  5. HyDE 假设性文档检索:原理、陷阱与工程化反思
  6. RAG 系统的可控性设计:从 HyDE 到联网搜索的按需启停架构
  7. 从知识库到信息流产品AI助手:多源数据血缘构建与全链路溯源
  8. 代码仓库 RAG 全链路:从 Gitee 抓取、AST 分块到跨库检索

前言

在企业级知识管理场景中,知识来源往往是多元异构的------本地 PDF 文档、语雀在线文档、Gitee 代码仓库,它们格式各异、存储分散、语义空间不同。如何将这些异构知识统一管理,并提供精准的智能问答能力,是 RAG(Retrieval-Augmented Generation)系统面临的核心挑战。

O-RAG 是一个面向多源异构知识场景的 RAG 系统,支持本地文档、语雀文档、代码仓库三类知识源的统一导入与联合检索。本文将从系统架构、工作流编排、状态管理等维度,深入剖析 O-RAG 的设计与实现。

一、技术栈选型

组件类型 技术选型 选型理由
Web 框架 FastAPI + Uvicorn 高性能异步框架,原生支持 SSE 流式输出
RAG 编排 LangGraph 基于状态图的 DAG 工作流,支持条件路由和并行分支
向量数据库 Qdrant 原生支持稠密+稀疏双向量混合检索,RRF 融合
图数据库 Neo4j 构建跨知识源的语义关系图谱
对象存储 MinIO 存储原始文件和图片资源
历史记录 MySQL 持久化对话历史
Embedding 模型 BGE-M3 原生支持稠密+稀疏向量,L2 归一化
Rerank 模型 BGE-Reranker 精排模型,支持动态 TopK
前端 UI Element Plus Vue 组件库,三标签页设计

二、系统整体架构

O-RAG 采用双服务分离架构:

yaml 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                      用户界面(Element Plus)                      │
│         三标签页:本地文档 | 语雀文档 | 代码仓库                      │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│              query_process 服务(端口 8001)                       │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                    查询工作流(LangGraph)                  │  │
│  │  item_name_confirm → search_embedding ─┐                │  │
│  │                    → search_hyde       ├→ RRF → Rerank │  │
│  │                    → web_search        │  → Answer      │  │
│  │                    → query_kg ─────────┘                │  │
│  └──────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│             import_process 服务(端口 8000)                       │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                    导入工作流(LangGraph)                  │  │
│  │  entry → pdf_to_md → md_img → document_split            │  │
│  │       → item_name_recognition → bge_embedding           │  │
│  │       → import_qdrant → import_kg                       │  │
│  └──────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│                        存储层                                      │
│  ┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐  │
│  │ Qdrant │  │ Neo4j  │  │ MinIO  │  │ MySQL  │  │  本地  │  │
│  │ 向量库 │  │ 图谱库 │  │ 对象库 │  │ 历史库 │  │ 文件  │  │
│  └────────┘  └────────┘  └────────┘  └────────┘  └────────┘  │
└─────────────────────────────────────────────────────────────────┘

核心设计理念

  1. 导入与查询分离import_process 专注知识导入流水线,query_process 专注检索与生成,职责清晰
  2. 统一前端入口 :所有 UI 页面统一在 query_process 服务管理,避免前端分散
  3. 三模数据源隔离 :Qdrant 按数据源分集合(kb_docskb_yuquekb_code),语义空间独立、生命周期独立

三、LangGraph 状态机编排

LangGraph 是 LangChain 生态中的工作流编排框架,基于状态图(StateGraph) 实现 DAG 流程控制。O-RAG 用它来编排导入和查询两条核心流水线。

3.1 状态定义:TypedDict 类型安全

每个工作流都有一个 TypedDict 状态类,定义了流程中流转的所有数据字段:

python 复制代码
class QueryGraphState(TypedDict):
    """查询流程状态"""
    session_id: str           # 会话唯一标识
    original_query: str       # 用户原始问题
    
    # 检索类型控制(三模支持)
    search_type: str          # auto/doc/code/all/cross_kb
    source_types: List[str]   # 指定数据源类型列表
    
    # 检索过程中的中间数据
    embedding_chunks: list    # 普通向量检索结果
    hyde_embedding_chunks: list  # HyDE 检索结果
    web_search_docs: list     # 网络搜索结果
    kg_docs: list             # 知识图谱查询结果
    cross_kb_docs: list       # 跨库链路检索结果
    
    # 排序与生成
    rrf_chunks: list          # RRF 融合排序后
    reranked_docs: list       # Rerank 精排后
    answer: str               # 最终答案

设计亮点

  • 类型安全:TypedDict 提供代码补全和类型检查
  • 状态共享 :所有节点通过 state 字典读写数据,无需全局变量
  • 深拷贝隔离create_default_state() 使用 deepcopy 避免状态污染

3.2 导入工作流:从 PDF 到向量库

导入工作流处理知识入库的完整链路:

markdown 复制代码
entry → pdf_to_md → md_img → document_split → item_name_recognition 
      → bge_embedding → import_qdrant → import_kg → END

条件路由:根据文件类型选择不同路径

python 复制代码
def route_after_entry(state: ImportGraphState) -> str:
    """根据文件类型路由"""
    if state["is_pdf_read_enabled"]:
        return "node_pdf_to_md"   # PDF → 转 Markdown
    elif state["is_md_read_enabled"]:
        return "node_md_img"      # MD → 直接处理图片
    else:
        return END                # 无效文件 → 结束

核心节点职责

节点 职责 技术实现
node_pdf_to_md PDF 转 Markdown MinerU 模型
node_md_img 提取/下载 MD 中的图片 MinIO 存储
node_document_split 文档切分 标题切割 + 长度控制
node_item_name_recognition 提取商品名 LLM 识别
node_bge_embedding 生成向量 BGE-M3 模型
node_import_qdrant 向量入库 Qdrant 集合
node_import_kg 知识图谱导入 Neo4j 节点/关系

3.3 查询工作流:多路召回与融合

查询工作流是 O-RAG 的核心,实现了多路并行召回 + 融合排序 + 精排生成的完整链路:

markdown 复制代码
item_name_confirm ─┬→ search_embedding ────┐
                   ├→ search_embedding_hyde ├→ RRF → Rerank → Answer
                   ├→ web_search_tavily     │
                   ├→ query_kg ─────────────┘
                   └→ cross_kb_search ──────┘ (条件路由)

条件路由 :根据 search_type 分流

python 复制代码
def route_after_confirm(state: QueryGraphState) -> str:
    """根据检索类型路由"""
    search_type = state.get("search_type", "all")
    if search_type == "cross_kb":
        return "cross_kb"   # 跨库链路检索
    return "normal"         # 普通多路检索

并行分支:普通检索路径下,4 个检索节点并行执行

bash 复制代码
# 普通检索路径:多路并行召回
builder.add_edge("node_search_embedding", "node_rrf")
builder.add_edge("node_search_embedding_hyde", "node_rrf")
builder.add_edge("node_web_search_tavily", "node_rrf")
builder.add_edge("node_query_kg", "node_rrf")

四、三模数据源管理

O-RAG 支持三类知识源,每类知识源在 Qdrant 中有独立的集合:

ini 复制代码
class SourceType(str, Enum):
    LOCAL_DOC = "local_doc"      # 本地上传文档
    YUQUE_DOC = "yuque_doc"      # 语雀在线文档
    CODE_REPO = "code_repo"      # 代码仓库

# 数据源类型到 Qdrant 集合的映射
SOURCE_TYPE_COLLECTION_MAP = {
    SourceType.LOCAL_DOC: "kb_docs",
    SourceType.YUQUE_DOC: "kb_yuque",
    SourceType.CODE_REPO: "kb_code",
}

分集合设计的优势

  1. 语义空间隔离:不同数据源的向量空间独立,避免语义混淆
  2. 生命周期独立:可以单独清理某个数据源的数据,不影响其他
  3. 灵活检索:支持单集合、多集合、跨库链路三种检索模式

4.1 语雀文档导入

语雀文档通过 API 客户端拉取,经过清洗后入库:

python 复制代码
# app/import_process/processors/yuque_cleaner.py
def clean_yuque_content(content: str) -> str:
    """语雀内容清洗"""
    # 去除 HTML 注释
    content = re.sub(r'<!--.*?-->', '', content, flags=re.DOTALL)
    # 去除 CDN 参数
    content = re.sub(r'?x-oss-process=.*?$', '', content, flags=re.MULTILINE)
    # 去除锚点标记
    content = re.sub(r'<a name=".*?"></a>', '', content)
    return content

4.2 代码仓库导入

代码仓库通过 Gitee API 抓取,经过 AST 解析后构建代码图谱:

python 复制代码
# app/import_process/processors/ast_parser.py
class ASTParser:
    """多语言 AST 解析器"""
    def parse(self, code: str, language: str, file_path: str) -> List[CodeSymbol]:
        if language == "python":
            return self._parse_python(code, file_path)  # 使用 ast 模块
        elif language in ("javascript", "typescript"):
            return self._parse_js(code, language, file_path)  # 正则解析
        elif language == "java":
            return self._parse_java(code, file_path)
        elif language == "go":
            return self._parse_go(code, file_path)

代码图谱节点类型

  • CodeFile:代码文件节点
  • CodeClass:类节点
  • CodeFunction:函数/方法节点

代码图谱关系类型

  • CONTAINS:文件包含类/函数
  • INHERITS:类继承关系
  • CALLS:函数调用关系
  • IMPORTS:模块导入关系

五、文档切分策略

文档切分是 RAG 系统的关键环节,直接影响检索质量。O-RAG 采用两阶段切分策略

5.1 第一阶段:语义粗切(按标题)

python 复制代码
def step_2_split_by_title(md_content, file_title):
    """根据 Markdown 标题进行语义切割"""
    title_pattern = r'^\s*#{1,6}\s+.+'
    lines = md_content.split('\n')
    
    is_code_block = False
    for line in lines:
        strip_line = line.strip()
        # 判断代码块状态(避免误识别代码注释为标题)
        if strip_line.startswith('```') or strip_line.startswith('~~~'):
            is_code_block = not is_code_block
        
        # 判断是否为标题
        is_title = (not is_code_block) and re.match(title_pattern, strip_line)
        
        if is_title:
            # 保存当前标题的内容,开始新标题
            if current_title:
                sections.append({"title": current_title, "content": ...})
            current_title = strip_line
            current_lines = [current_title]

设计亮点

  • 代码块感知:通过状态标记避免将代码注释误识别为标题
  • 语义完整:按标题切割保证每个 Chunk 语义完整

5.2 第二阶段:精细控制(长度+重叠)

python 复制代码
def step_3_refine_chunks(sections, max_length, min_length):
    """精细控制 Chunk 大小"""
    # 1. 超长段落二次切割
    for section in sections:
        sub_section = split_long_section(section, max_length)
        final_sections.extend(sub_section)
    
    # 2. 过短段落合并(同一 parent_title)
    final_sections = merge_short_sections(final_sections, min_length)

参数配置

  • DEFAULT_MAX_CONTENT_LENGTH = 2000:单个 Chunk 最大字符数
  • MIN_CONTENT_LENGTH = 500:短 Chunk 合并阈值
  • chunk_overlap = 100:二次切割的重叠长度

六、BGE-M3 向量化

O-RAG 采用 BGE-M3 模型生成稠密+稀疏混合向量,这是实现混合检索的基础:

python 复制代码
# app/lm/embedding_utils.py
def generate_embeddings(texts):
    """生成稠密+稀疏混合向量"""
    model = get_bge_m3_ef()  # 单例模式
    embeddings = model.encode_documents(texts)
    
    # 处理稀疏向量(CSR 格式 → 字典格式)
    processed_sparse = []
    for i in range(len(texts)):
        sparse_indices = embeddings["sparse"].indices[...].tolist()
        sparse_data = embeddings["sparse"].data[...].tolist()
        sparse_dict = {k: v for k, v in zip(sparse_indices, sparse_data)}
        processed_sparse.append(sparse_dict)
    
    return {
        "dense": [emb.tolist() for emb in embeddings["dense"]],
        "sparse": processed_sparse
    }

技术亮点

  1. 原生 L2 归一化 :开启 normalize_embeddings=True,单位化后内积等价于余弦相似度
  2. NumPy 类型转换np.int64 → intnp.float32 → float,解决序列化问题
  3. 单例模式:模型只加载一次,避免重复初始化

七、Qdrant 混合检索

Qdrant 原生支持稠密+稀疏双向量混合检索,通过 RRF 融合两路结果:

ini 复制代码
# app/clients/qdrant_utils.py
def hybrid_search(client, collection_name, dense_vector, sparse_vector, ...):
    """稠密+稀疏向量混合搜索"""
    # 构建预检索参数
    dense_prefetch = Prefetch(
        query=dense_vector,
        using="dense",
        limit=max(limit * 4, 20)  # 预召回 4 倍
    )
    sparse_prefetch = Prefetch(
        query=qdrant_sparse,
        using="sparse",
        limit=max(limit * 4, 20)
    )
    
    # 执行混合搜索,使用 RRF 融合
    results = client.query_points(
        collection_name=collection_name,
        prefetch=[dense_prefetch, sparse_prefetch],
        query=FusionQuery(fusion=Fusion.RRF),
        limit=limit,
        query_filter=query_filter
    )

多集合并行检索:支持同时检索多个集合

python 复制代码
def multi_collection_hybrid_search(client, collection_names, ...):
    """多集合并行混合搜索"""
    all_results = []
    for collection_name in collection_names:
        results = hybrid_search(...)
        # 标记来源集合
        for point in results:
            point.payload["_collection"] = collection_name
        all_results.extend(results)
    
    # 按分数降序排序,取 top-k
    all_results.sort(key=lambda x: x.score, reverse=True)
    return all_results[:limit]

八、SSE 流式输出

O-RAG 支持 SSE(Server-Sent Events)流式输出,提升用户体验:

python 复制代码
# app/query_process/agent/nodes/node_answer_output.py
def step_3_create_answer(state, prompt):
    """流式或非流式生成答案"""
    model = get_llm_client()
    is_stream = state.get("is_stream", False)
    
    if is_stream:
        # 流式输出:逐 token 推送到前端
        for chunk in model.stream(prompt):
            delta = chunk.content
            answer += delta
            push_to_session(state["session_id"], SSEEvent.DELTA, {"delta": delta})
    else:
        # 非流式:一次性返回
        response = model.invoke(prompt)
        set_task_result(state["session_id"], "answer", response.content)

SSE 事件类型

  • DELTA:增量文本
  • PROGRESS:节点执行进度
  • FINAL:最终答案(包含图片 URL)

九、总结

O-RAG 系统的核心设计思想:

  1. 双服务分离:导入与查询职责分离,独立扩展
  2. LangGraph 编排:基于状态图的 DAG 工作流,支持条件路由和并行分支
  3. 三模数据源隔离:Qdrant 分集合存储,语义空间独立
  4. 混合向量检索:BGE-M3 稠密+稀疏向量,Qdrant RRF 融合
  5. 多路召回融合:向量检索、HyDE、Web 搜索、知识图谱多路并行,RRF 融合排序

这套架构为后续的多路召回、跨库检索等高级特性打下了坚实基础。下一篇将深入探讨 RRF 融合算法和 Rerank 精排的工程实践。


作者正在寻找 AI 工程方向的机会,欢迎交流。

相关推荐
zzzzzz3102 小时前
假如我是掘金管理员,我先给评论区装个'代码审查'系统
python·程序员·机器人
砍材农夫2 小时前
python环境|conda安装和使用(2)
后端·python
程序员龙叔15 小时前
编写高质量 Skill 系列 -- 如何设计需求分析与用例生成的 SKILL
自动化测试·软件测试·python·软件测试工程师·接口测试·性能测试·skill·ai测试
用户83562907805118 小时前
使用 Python 操作 Word 内容控件
后端·python
码云骑士19 小时前
32-慢查询排查全流程(下)-索引优化实战与最左前缀原则
python
闵孚龙20 小时前
《PyTorch 深度修炼》Dataset 和 DataLoader:数据如何喂给模型
人工智能·pytorch·python
goldenrolan20 小时前
A公司物料替代测试系统 v1.7:从需求到 exe/apk 的 AI 辅助全链路实践
android·自动化测试·软件测试·python·ai
菜板春20 小时前
jupyter入门-手册-特征探索
python·jupyter