跨库链路检索:Neo4j 图数据库桥接文档与代码
📚 知识库项目实战系列:
前言
在企业研发场景中,知识往往分散在多个系统:需求文档存储在语雀或 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 │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
核心设计理念:
- 向量库分集合:语义空间隔离、生命周期独立
- 图数据库桥接:通过关系边建立跨集合节点的语义关联
- 双通道检索:优先走 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}")
设计亮点:
- 渐进式积累:每次检索都可能发现新的跨库关系,图谱越来越完善
- 限制回写数量:每次最多回写 3×3=9 条关系,避免图谱膨胀
- 静默失败:回写失败不影响主流程
四、完整检索节点
将上述策略整合到 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 系统的核心差异化特性,其设计要点:
- 双模共存:Qdrant 分集合存储 + Neo4j 关系桥接,兼顾语义检索和关系推理
- 两阶段检索:文档检索 → Neo4j 桥接 → 代码取回,精准建立跨源关联
- 回退通道:无关系边时 LLM 抽取符号 + 向量检索,保证可用性
- 关系回写:检索过程中逐步积累跨库关系,图谱越来越完善
- 权重设计:跨库结果权重 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 # 数据源类型枚举
系列文章导航:
- O-RAG 系统架构设计:LangGraph 状态机与多源异构 RAG
- 多路召回与融合排序:RRF + Rerank + 动态 TopK 工程实践
- 跨库链路检索:Neo4j 图数据库桥接文档与代码(本文)
作者正在寻找 AI 工程方向的机会,欢迎交流。