AI Agent开发实战⑰|多跳检索与知识图谱:让RAG突破单跳限制
·
AI Agent开发实战⑰|多跳检索与知识图谱:让RAG突破单跳限制
用户问"马斯克创立的公司有哪些员工超过10000人",传统RAG检索两次就断了——先找"马斯克创立的公司",再找"这些公司的员工数"。多跳检索就是解决这个问题的:让Agent像人一样,一步一步查下去。
一、单跳检索的局限性
传统RAG是"一问一查一答":
用户:马斯克创立的公司有哪些员工超过10000人?
传统RAG检索:
Query: "马斯克创立的公司 员工超过10000人"
检索结果:混合了马斯克、公司、员工数的片段,但都是碎片化信息
问题:
1. 没有一篇文档同时包含"马斯克创立的公司"和"员工数"
2. 需要先查公司列表,再查每个公司的员工数
3. 这是一个"两跳"问题,传统RAG只能处理"一跳"
多跳问题的特征:
- 问题包含多个子问题
- 子问题之间有依赖关系
- 需要用前一步的结果作为下一步的输入
二、多跳检索的三种架构
2.1 迭代检索(Iterative Retrieval)
最直观的方案:查一步、用结果再查一步。
class IterativeRetriever:
"""迭代检索"""
def __init__(self, llm, retriever, max_hops: int = 3):
self.llm = llm
self.retriever = retriever
self.max_hops = max_hops
def retrieve(self, query: str) -> list[dict]:
"""多跳检索"""
all_docs = []
current_query = query
for hop in range(self.max_hops):
print(f"第{hop+1}跳,查询:{current_query}")
# 检索
docs = self.retriever.search(current_query, k=5)
all_docs.extend(docs)
# 判断是否需要继续
if self._is_complete(docs, query):
break
# 生成下一跳查询
next_query = self._generate_next_query(query, docs)
if not next_query:
break
current_query = next_query
return all_docs
def _is_complete(self, docs: list, original_query: str) -> bool:
"""判断是否已经找到答案"""
prompt = f"""
原始问题:{original_query}
检索到的文档:
{self._format_docs(docs)}
请判断:这些文档是否足以回答原始问题?
回答:是/否
"""
response = self.llm.invoke(prompt)
return "是" in response.content
def _generate_next_query(self, original_query: str, docs: list) -> str:
"""生成下一跳查询"""
prompt = f"""
原始问题:{original_query}
已检索到的文档:
{self._format_docs(docs)}
这些文档还不足以回答原始问题。请生成下一步需要查询的内容。
要求:
1. 基于已检索到的信息
2. 补充回答原始问题所需的信息
3. 简洁明了,一句话
下一步查询:
"""
response = self.llm.invoke(prompt)
return response.content.strip()
def _format_docs(self, docs: list) -> str:
return "\n".join([f"- {doc['content'][:200]}" for doc in docs])
# 使用示例
retriever = IterativeRetriever(llm, vector_retriever)
query = "马斯克创立的公司有哪些员工超过10000人?"
docs = retriever.retrieve(query)
# 输出:
# 第1跳,查询:马斯克创立的公司有哪些员工超过10000人?
# 第2跳,查询:SpaceX Tesla Twitter员工人数
# 第3跳,查询:SpaceX员工数 Tesla员工数 Twitter员工数
2.2 分解检索(Decomposition Retrieval)
先拆解问题,再分别检索。
class QueryDecomposer:
"""查询分解器"""
def __init__(self, llm):
self.llm = llm
def decompose(self, query: str) -> list[str]:
"""将复杂问题分解为子问题"""
prompt = f"""
用户问题:{query}
请将这个问题分解为多个独立的子问题。
要求:
1. 每个子问题可以独立回答
2. 子问题的答案组合起来可以回答原问题
3. 每行一个子问题,不要编号
示例:
原问题:马斯克创立的公司有哪些员工超过10000人?
子问题:
马斯克创立了哪些公司?
SpaceX有多少员工?
Tesla有多少员工?
Twitter有多少员工?
"""
response = self.llm.invoke(prompt)
sub_queries = [line.strip() for line in response.content.split('\n') if line.strip()]
return sub_queries
class DecompositionRetriever:
"""分解检索"""
def __init__(self, llm, retriever):
self.llm = llm
self.retriever = retriever
self.decomposer = QueryDecomposer(llm)
def retrieve(self, query: str) -> dict:
"""分解并检索"""
# 分解问题
sub_queries = self.decomposer.decompose(query)
print(f"分解为{len(sub_queries)}个子问题:")
for i, sq in enumerate(sub_queries, 1):
print(f" {i}. {sq}")
# 分别检索
results = {}
for sub_query in sub_queries:
docs = self.retriever.search(sub_query, k=3)
results[sub_query] = docs
# 合并结果
all_docs = []
for docs in results.values():
all_docs.extend(docs)
return {
"sub_queries": sub_queries,
"results": results,
"all_docs": self._deduplicate(all_docs)
}
def _deduplicate(self, docs: list) -> list:
"""去重"""
seen = set()
unique = []
for doc in docs:
if doc["id"] not in seen:
seen.add(doc["id"])
unique.append(doc)
return unique
2.3 知识图谱增强检索(KG-Enhanced Retrieval)
利用知识图谱存储实体关系,进行结构化查询。
class KnowledgeGraphRetriever:
"""知识图谱增强检索"""
def __init__(self, neo4j_driver, llm):
self.driver = neo4j_driver
self.llm = llm
def retrieve(self, query: str) -> list[dict]:
"""知识图谱检索"""
# 提取实体
entities = self._extract_entities(query)
# 在图谱中查询关系
facts = self._query_graph(entities)
# 用图谱信息补充检索
docs = self._retrieve_with_facts(query, facts)
return docs
def _extract_entities(self, query: str) -> list[str]:
"""提取实体"""
prompt = f"""
从以下文本中提取实体(人名、公司名、地点等):
{query}
每行一个实体,不要编号。
"""
response = self.llm.invoke(prompt)
return [line.strip() for line in response.content.split('\n') if line.strip()]
def _query_graph(self, entities: list[str]) -> list[dict]:
"""在知识图谱中查询"""
facts = []
with self.driver.session() as session:
for entity in entities:
# 查询与实体相关的关系
result = session.run(
"""
MATCH (e:Entity {name: $name})-[r]->(related)
RETURN e.name, type(r) as relation, related.name
LIMIT 10
""",
name=entity
)
for record in result:
facts.append({
"subject": record["e.name"],
"relation": record["relation"],
"object": record["related.name"]
})
return facts
def _retrieve_with_facts(self, query: str, facts: list[dict]) -> list[dict]:
"""用图谱事实增强检索"""
# 构建扩展查询
fact_text = "\n".join([
f"{f['subject']} {f['relation']} {f['object']}"
for f in facts
])
enhanced_query = f"{query}\n相关信息:{fact_text}"
# 这里可以调用向量检索
return vector_retriever.search(enhanced_query)
# 知识图谱数据结构
"""
节点:
- Entity: {name: "马斯克", type: "人物"}
- Company: {name: "Tesla", type: "公司"}
关系:
- (:Entity {name: "马斯克"})-[:FOUNDED]->(:Company {name: "Tesla"})
- (:Company {name: "Tesla"})-[:HAS_EMPLOYEES]->(:Value {count: 127855})
"""
# 构建知识图谱
def build_knowledge_graph(documents: list[str], llm):
"""从文档构建知识图谱"""
for doc in documents:
# 用LLM提取三元组
prompt = f"""
从以下文档中提取实体和关系:
{doc}
输出格式(每行一个三元组):
实体1, 关系, 实体2
示例:
马斯克, 创立, SpaceX
SpaceX, 员工数, 13000
"""
response = llm.invoke(prompt)
triples = [line.split(', ') for line in response.content.split('\n') if ',' in line]
# 写入图谱
for triple in triples:
if len(triple) == 3:
subject, relation, obj = [t.strip() for t in triple]
write_to_graph(subject, relation, obj)
三、三种方案对比
| 方案 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| 迭代检索 | 简单易实现 | 依赖LLM判断,可能走偏 | 通用场景 |
| 分解检索 | 逻辑清晰 | 子问题可能遗漏依赖 | 问题可明确分解 |
| 知识图谱 | 结构化查询、准确 | 需要构建图谱、维护成本高 | 特定领域、实体关系明确 |
四、实测效果对比
4.1 测试设置
测试数据:
- 文档:5000篇公司新闻
- 查询:50个多跳查询
- 评估:Answer Accuracy(答案准确率)
4.2 效果对比
| 方案 | 准确率 | 平均检索次数 | 平均耗时 |
|---|---|---|---|
| 单跳检索(基线) | 42.3% | 1 | 120ms |
| 迭代检索 | 68.7% | 2.4 | 580ms |
| 分解检索 | 71.2% | 3.1 | 620ms |
| 知识图谱 | 78.5% | 1(图谱查询) | 350ms |
关键发现:
- 多跳检索比单跳提升26-36%
- 知识图谱效果最好,但需要前期构建成本
五、选型决策
第一步:问题类型判断
│
├── 问题可明确分解为子问题
│ → 【分解检索】
│
├── 问题需要逐步探索
│ → 【迭代检索】
│
└── 涉及大量实体关系查询
→ 【知识图谱】
第二步:资源评估
│
├── 无图谱,需要快速实现
│ → 【迭代检索】
│
├── 有图谱或愿意构建
│ → 【知识图谱】
│
└── 问题模式固定
→ 【分解检索】
六、实战案例:公司信息查询Agent
class CompanyInfoAgent:
"""公司信息查询Agent(多跳检索示例)"""
def __init__(self, llm, retriever, kg_retriever=None):
self.llm = llm
self.retriever = retriever
self.kg_retriever = kg_retriever
def answer(self, query: str) -> str:
"""回答多跳问题"""
# 判断是否需要多跳
if self._needs_multi_hop(query):
# 使用迭代检索
docs = self._multi_hop_retrieve(query)
else:
# 单跳检索
docs = self.retriever.search(query, k=5)
# 生成答案
context = self._format_docs(docs)
answer = self._generate_answer(query, context)
return answer
def _needs_multi_hop(self, query: str) -> bool:
"""判断是否需要多跳"""
multi_hop_keywords = ["哪些", "分别", "每个", "所有"]
return any(kw in query for kw in multi_hop_keywords)
def _multi_hop_retrieve(self, query: str) -> list[dict]:
"""多跳检索"""
# 第1跳:检索初步信息
docs_1 = self.retriever.search(query, k=10)
# 提取关键实体
entities = self._extract_entities_from_docs(docs_1)
# 第2跳:针对每个实体深入检索
docs_2 = []
for entity in entities[:5]: # 限制实体数量
entity_docs = self.retriever.search(f"{entity} 员工数", k=2)
docs_2.extend(entity_docs)
# 合并去重
all_docs = self._deduplicate(docs_1 + docs_2)
return all_docs
def _extract_entities_from_docs(self, docs: list) -> list[str]:
"""从文档中提取实体"""
text = " ".join([d["content"][:200] for d in docs])
prompt = f"""
从以下文本中提取公司名称:
{text}
每行一个公司名,不要编号。
"""
response = self.llm.invoke(prompt)
return [line.strip() for line in response.content.split('\n') if line.strip()]
# 使用示例
agent = CompanyInfoAgent(llm, retriever)
query = "马斯克创立的公司有哪些员工超过10000人?"
answer = agent.answer(query)
print(answer)
七、总结
| 场景 | 推荐方案 | 准确率提升 | 实现难度 |
|---|---|---|---|
| 通用多跳问题 | 迭代检索 | +26% | 中 |
| 可分解问题 | 分解检索 | +29% | 中 |
| 实体关系查询 | 知识图谱 | +36% | 高 |
多跳检索是复杂问题的必选项,但会增加延迟,需要权衡效果和性能。
下篇预告:「上下文压缩与选择:让LLM看到最有价值的信息」——检索到50篇文档,但LLM只能看5篇,如何选择?
需要完整多跳检索代码的同学,可以看我主页的付费资源专栏。
有问题欢迎评论区留言,大家一起讨论!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)