03-跨库链路检索-Neo4j图数据库桥接文档与代码

跨库链路检索:Neo4j 图数据库桥接文档与代码

📚 知识库项目实战系列:

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

前言

在企业研发场景中,知识往往分散在多个系统:需求文档存储在语雀或 Confluence,代码实现托管在 Gitee 或 GitHub。当开发者提问"登录功能的设计方案和代码实现"时,传统的向量检索只能分别在文档集合和代码集合中搜索,无法建立"设计文档 → 代码实现"的关联。

O-RAG 系统引入了跨库链路检索(Cross-KB Search) ,通过 Neo4j 图数据库构建跨知识源的语义关系图谱,实现"文档检索 → 关系桥接 → 代码取回"的两阶段精准检索。本文将从架构设计、图谱构建、检索策略、关系回写等维度,深入剖析这一特性的实现。

一、问题背景

1.1 单集合方案的局限

最初,O-RAG 考虑将所有知识源导入同一个 Qdrant 集合,通过 source_type 字段过滤。但这种方案存在明显问题:

问题 说明
语义空间混淆 文档和代码的向量空间差异大,混合检索效果差
生命周期耦合 无法单独清理某个数据源的数据
关系缺失 无法表达"文档 A 实现了代码 B"这类跨源关系
检索低效 每次检索都要扫描全量数据

1.2 分集合 + 图谱桥接方案

O-RAG 最终采用Qdrant 分集合 + Neo4j 跨库关系桥接的双模共存方案:

css 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        Qdrant 向量库                             │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐                      │
│  │  kb_docs │  │ kb_yuque │  │ kb_code  │  语义空间隔离          │
│  │ 本地文档 │  │ 语雀文档 │  │ 代码仓库 │  生命周期独立          │
│  └──────────┘  └──────────┘  └──────────┘                      │
└─────────────────────────────────────────────────────────────────┘
                              ↕
┌─────────────────────────────────────────────────────────────────┐
│                        Neo4j 图谱库                              │
│  ┌────────────────────────────────────────────────────────┐    │
│  │  DocChunk ──[IMPLEMENTS]──> CodeFunction               │    │
│  │  YuqueChunk ──[MENTIONS]──> CodeClass                  │    │
│  │  CodeFile ──[CONTAINS]──> CodeFunction                 │    │
│  │  CodeClass ──[INHERITS]──> CodeClass                   │    │
│  └────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘

核心设计理念

  1. 向量库分集合:语义空间隔离、生命周期独立
  2. 图数据库桥接:通过关系边建立跨集合节点的语义关联
  3. 双通道检索:优先走 Neo4j 关系桥接,回退到 LLM 符号抽取 + 向量检索

二、代码图谱构建

跨库检索的基础是构建高质量的代码图谱。O-RAG 通过 AST 解析提取代码符号,然后导入 Neo4j 构建图谱。

2.1 AST 多语言解析

O-RAG 支持 Python、Java、JavaScript、Go、TypeScript 五种语言的 AST 解析:

python 复制代码
# app/import_process/processors/ast_parser.py
class CodeSymbol:
    """代码符号:函数/类/方法"""
    def __init__(self, name: str, symbol_type: str, language: str,
                 start_line: int = 0, end_line: int = 0,
                 parameters: str = "", docstring: str = "",
                 body: str = "", parent: str = "", decorators: List[str] = None):
        self.name = name
        self.symbol_type = symbol_type  # "function", "class", "method"
        self.language = language
        self.start_line = start_line
        self.end_line = end_line
        self.parameters = parameters
        self.docstring = docstring
        self.body = body
        self.parent = parent  # 所属类名(方法才有)
        self.decorators = decorators

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)
        else:
            return self._parse_regex(code, language, file_path)  # 通用正则兜底

Python AST 解析示例

ini 复制代码
def _parse_python(self, code: str, file_path: str) -> List[CodeSymbol]:
    """使用 Python ast 模块解析"""
    tree = ast.parse(code)
    symbols = []
    
    for node in ast.walk(tree):
        if isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef):
            # 提取参数
            args = [a.arg for a in node.args.args]
            params = ", ".join(args)
            
            # 提取 docstring
            docstring = ast.get_docstring(node) or ""
            
            # 提取装饰器
            decorators = []
            for dec in node.decorator_list:
                if isinstance(dec, ast.Name):
                    decorators.append(dec.id)
                elif isinstance(dec, ast.Attribute):
                    decorators.append(ast.unparse(dec))
            
            # 获取代码体
            body_lines = code.split("\n")[node.lineno - 1:node.end_lineno]
            body = "\n".join(body_lines)
            
            symbols.append(CodeSymbol(
                name=node.name,
                symbol_type="function",
                language="python",
                start_line=node.lineno,
                end_line=node.end_lineno or node.lineno,
                parameters=params,
                docstring=docstring,
                body=body,
                decorators=decorators
            ))
        
        elif isinstance(node, ast.ClassDef):
            # 类解析...
            # 提取类方法
            for item in node.body:
                if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
                    # 方法解析...
                    symbols.append(CodeSymbol(
                        name=item.name,
                        symbol_type="method",
                        language="python",
                        parent=node.name  # 所属类名
                    ))
    
    return symbols

2.2 Neo4j 图谱构建

CodeGraphBuilder 负责将 AST 解析结果导入 Neo4j:

python 复制代码
# app/import_process/processors/code_graph_builder.py
class CodeGraphBuilder:
    """代码图谱构建器"""
    
    def __init__(self, repo_slug: str, branch: str = "main"):
        self.repo_slug = repo_slug
        self.branch = branch
    
    def build_graph(self, symbols: List[CodeSymbol], file_path: str) -> Dict[str, int]:
        """根据解析出的符号构建代码图谱"""
        stats = {"files": 0, "classes": 0, "functions": 0, "relationships": 0}
        
        driver = get_neo4j_driver()
        with driver.session() as session:
            # 1. 创建文件节点
            self._create_file_node(session, file_path)
            stats["files"] += 1
            
            # 2. 创建类/函数节点
            for symbol in symbols:
                if symbol.symbol_type == "class":
                    self._create_class_node(session, symbol, file_path)
                    stats["classes"] += 1
                elif symbol.symbol_type in ("function", "method"):
                    self._create_function_node(session, symbol, file_path)
                    stats["functions"] += 1
            
            # 3. 建立 CONTAINS 关系(文件包含类/函数)
            for symbol in symbols:
                self._create_contains_rel(session, file_path, symbol)
                stats["relationships"] += 1
            
            # 4. 建立 INHERITS 关系(类继承)
            for symbol in symbols:
                if symbol.symbol_type == "class" and symbol.parameters:
                    for base in symbol.parameters.split(","):
                        base = base.strip()
                        if base and base not in ("object", "Object"):
                            self._create_inherits_rel(session, symbol.name, base)
                            stats["relationships"] += 1
            
            # 5. 建立 CALLS 关系(函数调用)
            calls = self._extract_calls(symbols)
            for caller, callee in calls:
                self._create_calls_rel(session, caller, callee)
                stats["relationships"] += 1
        
        return stats

2.3 图谱节点与关系类型

节点类型

节点类型 属性 说明
CodeFile path, name, repo, branch 代码文件
CodeClass node_id, name, file_path, repo, language, docstring
CodeFunction node_id, name, qualified_name, file_path, parameters, docstring 函数/方法
CodeModule name 模块(导入关系)
DocChunk chunk_id, content, title, item_name 文档切片
YuqueChunk chunk_id, content, title, url 语雀文档切片

关系类型

关系类型 方向 说明
CONTAINS CodeFile → CodeClass/CodeFunction 文件包含类/函数
INHERITS CodeClass → CodeClass 类继承关系
CALLS CodeFunction → CodeFunction 函数调用关系
IMPORTS CodeFile → CodeModule 模块导入关系
IMPLEMENTS DocChunk/YuqueChunk → CodeFunction 文档实现代码(跨库)
MENTIONS DocChunk/YuqueChunk → CodeClass 文档提及代码(跨库)

Cypher 语句示例

ini 复制代码
// 创建文件节点
MERGE (f:CodeFile {path: $path, repo: $repo})
ON CREATE SET
    f.name = $name,
    f.branch = $branch,
    f.created_at = timestamp()
ON MATCH SET
    f.updated_at = timestamp()

// 创建函数节点
MERGE (fn:CodeFunction {node_id: $node_id})
ON CREATE SET
    fn.name = $name,
    fn.qualified_name = $qualified_name,
    fn.file_path = $file_path,
    fn.repo = $repo,
    fn.language = $language,
    fn.parameters = $parameters,
    fn.docstring = $docstring,
    fn.start_line = $start_line,
    fn.end_line = $end_line,
    fn.created_at = timestamp()

// 创建 CONTAINS 关系
MATCH (f:CodeFile {path: $file_path, repo: $repo})
MATCH (s:CodeFunction {node_id: $node_id})
MERGE (f)-[r:CONTAINS]->(s)
ON CREATE SET r.created_at = timestamp()

// 创建跨库 IMPLEMENTS 关系
MATCH (d:DocChunk {chunk_id: $doc_chunk_id})
MATCH (c:CodeFunction {node_id: $code_node_id})
MERGE (d)-[r:IMPLEMENTS]->(c)
ON CREATE SET r.created_at = timestamp()

三、跨库检索策略

跨库检索是 O-RAG 的核心特性,采用两阶段检索 + 回退通道的设计:

vbnet 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        跨库检索流程                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  第一阶段:文档检索                                               │
│  ┌────────────────────────────────────────────────────────┐     │
│  │  Query → 向量检索 kb_docs + kb_yuque → doc_chunks      │     │
│  └────────────────────────────────────────────────────────┘     │
│                            ↓                                     │
│  第二阶段:Neo4j 关系桥接(优先)                                  │
│  ┌────────────────────────────────────────────────────────┐     │
│  │  doc_chunks → Neo4j 查询 IMPLEMENTS/MENTIONS 关系       │     │
│  │            → code_chunks                               │     │
│  └────────────────────────────────────────────────────────┘     │
│                            ↓                                     │
│  回退通道:无关系边时 LLM 符号抽取                                 │
│  ┌────────────────────────────────────────────────────────┐     │
│  │  doc_content → LLM 抽取代码符号                          │     │
│  │             → 向量检索 kb_code                          │     │
│  │             → code_chunks                               │     │
│  └────────────────────────────────────────────────────────┘     │
│                            ↓                                     │
│  关系回写:积累跨库关系                                           │
│  ┌────────────────────────────────────────────────────────┐     │
│  │  doc_chunks + code_chunks → 创建 IMPLEMENTS 关系边       │     │
│  └────────────────────────────────────────────────────────┘     │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

3.1 第一阶段:文档检索

从文档和语雀集合中检索设计相关内容:

ini 复制代码
# app/query_process/agent/nodes/node_cross_kb_search.py
def _search_doc_collections(query: str, limit: int = 5) -> List[Dict[str, Any]]:
    """从文档/语雀集合中检索设计相关内容"""
    doc_collections = [
        qdrant_config.kb_docs_collection,
        qdrant_config.kb_yuque_collection
    ]
    
    embeddings = generate_embeddings([query])
    qdrant_client = get_qdrant_client()
    all_results = []
    
    for collection in doc_collections:
        try:
            results = hybrid_search(
                client=qdrant_client,
                collection_name=collection,
                dense_vector=embeddings["dense"][0],
                sparse_vector=embeddings["sparse"][0],
                limit=limit,
                with_payload=True
            )
            if results:
                for hit in results:
                    chunk = {
                        "chunk_id": hit.id,
                        "score": hit.score,
                        "collection": collection,
                        **(hit.payload or {})
                    }
                    all_results.append(chunk)
        except Exception as e:
            logger.error(f"文档集合 [{collection}] 检索失败: {e}")
            continue
    
    # 按分数排序取 top-k
    all_results.sort(key=lambda x: x.get("score", 0), reverse=True)
    return all_results[:limit]

3.2 第二阶段:Neo4j 关系桥接

通过文档节点的 chunk_id 查找关联的代码块:

python 复制代码
def _neo4j_bridge(doc_chunks: List[Dict]) -> List[Dict[str, Any]]:
    """Neo4j 关系桥接:通过文档节点查找关联的代码块"""
    if not doc_chunks:
        return []
    
    # 提取文档节点的 chunk_id
    chunk_ids = [c["chunk_id"] for c in doc_chunks if c.get("chunk_id")]
    
    code_results = []
    
    # 分别查询 DocChunk 和 YuqueChunk 标签的关系
    for label in ["DocChunk", "YuqueChunk"]:
        try:
            results = query_cross_kb_code_chunks(
                source_label=label,
                source_chunk_ids=chunk_ids,
                rel_types=["IMPLEMENTS", "MENTIONS"],
                limit=10
            )
            code_results.extend(results)
        except Exception as e:
            logger.error(f"Neo4j 桥接查询 [{label}] 失败: {e}")
            continue
    
    logger.info(f"Neo4j 桥接结果: {len(doc_chunks)} 个文档节点 -> {len(code_results)} 个代码块")
    return code_results

Neo4j 查询 Cypher

vbnet 复制代码
// 查询文档节点关联的代码块
MATCH (d:DocChunk)-[r:IMPLEMENTS|MENTIONS]->(c:CodeFunction)
WHERE d.chunk_id IN $chunk_ids
RETURN c.node_id AS chunk_id,
       c.name AS name,
       c.docstring AS docstring,
       c.file_path AS file_path,
       c.language AS language,
       type(r) AS rel_type
ORDER BY c.start_line
LIMIT $limit

3.3 回退通道:LLM 符号抽取

当 Neo4j 没有关系边时(新导入的文档/代码),回退到 LLM 抽取代码符号:

python 复制代码
def _llm_extract_symbols(doc_content: str) -> Dict[str, Any]:
    """回退通道:LLM 从文档内容中抽取代码符号"""
    prompt = load_prompt("extract_code_symbols", context=doc_content[:3000])
    llm = get_llm_client(json_mode=True)
    response = llm.invoke([HumanMessage(content=prompt)])
    
    content = response.content
    if content.startswith("```json"):
        content = content.replace("```json", "").replace("```", "").strip()
    symbols = json.loads(content)
    
    return symbols  # {"functions": [...], "classes": [...], "modules": [...]}

Prompt 示例

diff 复制代码
你是一个代码符号抽取专家。请从以下文档内容中提取可能涉及的代码符号:

{context}

请以 JSON 格式返回,包含以下字段:
- functions: 函数名列表
- classes: 类名列表
- modules: 模块名列表
- files: 文件名列表

符号向量检索

python 复制代码
def _search_code_by_symbols(symbols: Dict[str, Any], query: str, limit: int = 5) -> List[Dict[str, Any]]:
    """回退通道:用抽取的符号名称去代码集合做向量检索"""
    keywords = []
    for key in ("functions", "classes", "modules", "files"):
        items = symbols.get(key, [])
        if items:
            keywords.extend(items)
    
    if not keywords:
        return []
    
    # 拼接搜索文本:符号名 + 原始查询
    search_text = f"{query} {' '.join(keywords)}"
    embeddings = generate_embeddings([search_text])
    
    qdrant_client = get_qdrant_client()
    results = hybrid_search(
        client=qdrant_client,
        collection_name=qdrant_config.kb_code_collection,
        dense_vector=embeddings["dense"][0],
        sparse_vector=embeddings["sparse"][0],
        limit=limit,
        with_payload=True
    )
    
    code_chunks = []
    if results:
        for hit in results:
            code_chunks.append({
                "chunk_id": hit.id,
                "score": hit.score,
                "source": "symbol_search",
                **(hit.payload or {})
            })
    
    return code_chunks

3.4 关系回写:逐步积累

检索完成后,O-RAG 会回写 IMPLEMENTS 关系边,逐步积累跨库关系:

python 复制代码
def _write_back_relationships(doc_chunks: List[Dict], code_chunks: List[Dict]):
    """回写 IMPLEMENTS 关系边(逐步积累跨库关系)"""
    if not doc_chunks or not code_chunks:
        return
    
    for doc in doc_chunks[:3]:  # 最多回写 3 个文档节点
        for code in code_chunks[:3]:  # 最多回写 3 个代码节点
            doc_source_type = doc.get("source_type", "local_doc")
            source_label = "YuqueChunk" if doc_source_type == "yuque_doc" else "DocChunk"
            
            try:
                create_cross_kb_relationship(
                    source_label=source_label,
                    source_id=doc["chunk_id"],
                    target_label="CodeFunction",
                    target_id=code["chunk_id"],
                    rel_type="IMPLEMENTS"
                )
            except Exception as e:
                logger.debug(f"回写关系失败: {e}")

设计亮点

  1. 渐进式积累:每次检索都可能发现新的跨库关系,图谱越来越完善
  2. 限制回写数量:每次最多回写 3×3=9 条关系,避免图谱膨胀
  3. 静默失败:回写失败不影响主流程

四、完整检索节点

将上述策略整合到 node_cross_kb_search 节点:

python 复制代码
# app/query_process/agent/nodes/node_cross_kb_search.py
def node_cross_kb_search(state: QueryGraphState) -> Dict[str, Any]:
    """跨库链路检索节点"""
    logger.info(f">>> [node_cross_kb_search] 开始跨库链路检索")
    
    try:
        query = state.get("rewritten_query") or state.get("original_query", "")
        
        # 第一阶段:文档检索
        doc_results = _search_doc_collections(query, limit=5)
        logger.info(f"文档检索结果: {len(doc_results)} 条")
        
        if not doc_results:
            logger.info("文档检索无结果,跳过跨库检索")
            return {"cross_kb_docs": [], "extracted_symbols": {}}
        
        # 第二阶段:Neo4j 关系桥接(优先)
        code_results = _neo4j_bridge(doc_results)
        
        extracted_symbols = {}
        
        # 回退通道:无关系边时 LLM 抽取符号
        if not code_results:
            logger.info("Neo4j 无关系边,回退到 LLM 符号抽取")
            doc_content = "\n\n".join([d.get("content", "") for d in doc_results[:3]])
            extracted_symbols = _llm_extract_symbols(doc_content)
            
            if extracted_symbols:
                code_results = _search_code_by_symbols(extracted_symbols, query, limit=5)
        
        # 回写关系边(积累跨库关系)
        if code_results:
            _write_back_relationships(doc_results, code_results)
        
        # 组装跨库结果:文档 + 代码
        cross_kb_docs = []
        
        # 添加文档结果
        for doc in doc_results:
            cross_kb_docs.append({
                "chunk_id": doc.get("chunk_id"),
                "content": doc.get("content", ""),
                "title": doc.get("title", ""),
                "source_type": doc.get("source_type", "local_doc"),
                "score": doc.get("score", 0),
                "role": "design_doc"  # 标记为设计文档
            })
        
        # 添加代码结果
        for code in code_results:
            cross_kb_docs.append({
                "chunk_id": code.get("chunk_id"),
                "content": code.get("content", code.get("docstring", "")),
                "title": code.get("name", code.get("title", "")),
                "source_type": "code_repo",
                "source": code.get("file_path", ""),
                "score": code.get("score", 0.9),
                "role": "code_block",  # 标记为代码块
                "language": code.get("language", ""),
                "rel_type": code.get("rel_type", "")  # IMPLEMENTS/MENTIONS
            })
        
        logger.info(f"跨库检索完成: {len(doc_results)} 文档 + {len(code_results)} 代码")
        
        return {
            "cross_kb_docs": cross_kb_docs,
            "extracted_symbols": extracted_symbols
        }
    
    except Exception as e:
        logger.error(f"[node_cross_kb_search] 跨库检索失败: {e}", exc_info=True)
        return {"cross_kb_docs": [], "extracted_symbols": {}, "error": str(e)}

五、检索路由与融合

跨库检索结果需要融入主查询流程:

5.1 条件路由

根据 search_type 决定走普通检索还是跨库检索:

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

builder.add_conditional_edges(
    "node_item_name_confirm",
    route_after_confirm,
    {
        "cross_kb": "node_cross_kb_search",
        "normal": "node_search_embedding"
    }
)

5.2 RRF 融合

跨库检索结果以权重 1.2 参与 RRF 融合:

python 复制代码
# app/query_process/agent/nodes/node_rrf.py
def node_rrf(state: QueryGraphState) -> QueryGraphState:
    """RRF 融合节点"""
    embedding_chunks = state.get("embedding_chunks") or []
    hyde_embedding_chunks = state.get("hyde_embedding_chunks") or []
    web_search_docs = state.get("web_search_docs") or []
    cross_kb_docs = state.get("cross_kb_docs") or []
    
    # 构建带权重的来源列表
    source_with_weight = []
    
    if embedding_chunks:
        source_with_weight.append((embedding_chunks, 1.0))
    if hyde_embedding_chunks:
        source_with_weight.append((hyde_embedding_chunks, 0.9))
    if web_search_docs:
        source_with_weight.append((web_search_docs, 0.5))
    if cross_kb_docs:
        source_with_weight.append((cross_kb_docs, 1.2))  # 跨库权重最高
    
    # 执行 RRF 融合
    rrf_response = step_3_reciprocal_rank_fusion(source_with_weight, top_k=5)
    
    state["rrf_chunks"] = rrf_response
    return state

六、实战场景

6.1 场景:从需求文档到代码实现

用户问题:"登录功能的设计方案和对应代码实现"

检索流程

markdown 复制代码
1. 第一阶段:文档检索
   Query → 向量检索 kb_docs + kb_yuque
   结果: 
     - doc_001: "登录功能需求文档 - 用户输入账号密码..."
     - doc_002: "认证模块设计文档 - JWT Token 验证..."

2. 第二阶段:Neo4j 关系桥接
   doc_001 (chunk_id=xxx) → Neo4j 查询 IMPLEMENTS 关系
   结果:
     - code_001: "LoginService.authenticate()" ← IMPLEMENTS
     - code_002: "AuthController.login()" ← IMPLEMENTS

3. 关系回写
   创建/更新 IMPLEMENTS 关系边

4. RRF 融合
   cross_kb_docs (权重 1.2) + 其他来源 → RRF 排序

5. Rerank 精排
   文档 + 代码混合精排

6. 答案生成
   LLM 基于文档和代码生成综合答案

6.2 场景:新文档无关系边

用户问题:"新导入的支付模块设计"

检索流程

vbnet 复制代码
1. 第一阶段:文档检索
   结果: doc_003 "支付模块设计文档..."

2. 第二阶段:Neo4j 关系桥接
   doc_003 → Neo4j 查询 → 无 IMPLEMENTS 关系(新文档)

3. 回退通道:LLM 符号抽取
   doc_003 内容 → LLM 抽取
   结果: {"functions": ["process_payment", "validate_order"], 
          "classes": ["PaymentService"]}

4. 符号向量检索
   "支付模块设计 process_payment validate_order PaymentService"
   → 向量检索 kb_code
   结果: code_003 "PaymentService.process_payment()"

5. 关系回写
   创建 IMPLEMENTS 关系边(下次检索可直接桥接)

七、性能优化

7.1 批量查询

Neo4j 查询使用批量 chunk_ids,减少网络往返:

ini 复制代码
# 批量查询,而非逐个查询
results = query_cross_kb_code_chunks(
    source_label=label,
    source_chunk_ids=chunk_ids,  # 批量传递
    rel_types=["IMPLEMENTS", "MENTIONS"],
    limit=10
)

7.2 索引优化

为 Neo4j 节点创建索引,加速查询:

scss 复制代码
// 创建索引
CREATE INDEX doc_chunk_id IF NOT EXISTS FOR (d:DocChunk) ON (d.chunk_id);
CREATE INDEX yuque_chunk_id IF NOT EXISTS FOR (y:YuqueChunk) ON (y.chunk_id);
CREATE INDEX code_function_node_id IF NOT EXISTS FOR (f:CodeFunction) ON (f.node_id);
CREATE INDEX code_file_path IF NOT EXISTS FOR (f:CodeFile) ON (f.path);

7.3 限制结果数量

跨库检索限制返回数量,避免结果过多:

ini 复制代码
# 文档检索限制
doc_results = _search_doc_collections(query, limit=5)

# 代码检索限制
code_results = _neo4j_bridge(doc_results)  # limit=10 in query

# 关系回写限制
for doc in doc_results[:3]:  # 最多 3 个文档
    for code in code_chunks[:3]:  # 最多 3 个代码
        _write_back_relationship(...)

八、图谱可视化

Neo4j Browser 可以直观展示跨库关系:

less 复制代码
// 查询某个文档的跨库关系
MATCH (d:DocChunk {chunk_id: "xxx"})-[r:IMPLEMENTS]->(c:CodeFunction)
RETURN d, r, c

// 查询代码调用链
MATCH (f1:CodeFunction)-[r:CALLS]->(f2:CodeFunction)
WHERE f1.name = "process_payment"
RETURN f1, r, f2

// 查询类继承关系
MATCH (c1:CodeClass)-[r:INHERITS]->(c2:CodeClass)
RETURN c1, r, c2

九、总结

跨库链路检索是 O-RAG 系统的核心差异化特性,其设计要点:

  1. 双模共存:Qdrant 分集合存储 + Neo4j 关系桥接,兼顾语义检索和关系推理
  2. 两阶段检索:文档检索 → Neo4j 桥接 → 代码取回,精准建立跨源关联
  3. 回退通道:无关系边时 LLM 抽取符号 + 向量检索,保证可用性
  4. 关系回写:检索过程中逐步积累跨库关系,图谱越来越完善
  5. 权重设计:跨库结果权重 1.2,在 RRF 融合中优先级最高

这套方案解决了传统 RAG 系统无法处理跨源关系的痛点,为"从需求到代码"的全链路知识检索提供了有力支撑。

附录:完整代码结构

bash 复制代码
app/
├── clients/
│   ├── neo4j_utils.py          # Neo4j 客户端工具
│   └── qdrant_utils.py         # Qdrant 客户端工具
├── import_process/
│   ├── agent/
│   │   ├── main_graph.py       # 导入工作流
│   │   └── nodes/
│   │       ├── node_import_kg.py      # 知识图谱导入
│   │       └── ...
│   └── processors/
│       ├── ast_parser.py       # AST 多语言解析
│       ├── code_graph_builder.py  # 代码图谱构建
│       ├── git_fetcher.py      # Gitee 仓库抓取
│       └── yuque_sync.py       # 语雀文档同步
├── query_process/
│   ├── agent/
│   │   ├── main_graph.py       # 查询工作流
│   │   └── nodes/
│   │       ├── node_cross_kb_search.py  # 跨库检索节点
│   │       ├── node_rrf.py     # RRF 融合节点
│   │       └── ...
└── utils/
    └── source_type.py          # 数据源类型枚举

系列文章导航

  1. O-RAG 系统架构设计:LangGraph 状态机与多源异构 RAG
  2. 多路召回与融合排序:RRF + Rerank + 动态 TopK 工程实践
  3. 跨库链路检索:Neo4j 图数据库桥接文档与代码(本文)

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

相关推荐
用户337922545681 小时前
基于 OKF + RAG 构建 Text2SQL 语义层:让 LLM 真正理解你的数据库
人工智能
把所有砖敲烂1 小时前
MiniMax M3 深度实测:单卡部署、代码生成与性能全解析
人工智能
沉默王二1 小时前
老板:“请说出一个录用你的理由。”我脱口而出:“每个月 AI 支出都超过我的生活费了!”老板愣了一下,随即哈哈大笑:“好吧,你被录用了。”
人工智能·ai编程·claude
这token有力气2 小时前
ReAct 循环中陷入"工具调用死循环"
人工智能
Mr_愚人派2 小时前
当"Claude"不再是 Claude:一次第三方 API 代理引发的 AI 身份伪造排查实录
人工智能·安全
Lee川2 小时前
Memory 模块深度解析(面试向)
人工智能·面试
MacroZheng2 小时前
Claude Code官方桌面端正式发布,夯爆了!
java·人工智能·后端
IT_陈寒3 小时前
React的useEffect依赖数组把我坑惨了,真相其实很简单
前端·人工智能·后端
Kapaseker3 小时前
什么?Stack Overflow 给 AI 做了个 Stack Overflow
人工智能