跳到主要内容

第 1 章 · 检索主线:图谱先发言,再去翻书

这一章讲编排:一次检索请求被拆成哪几步、谁调谁。两个主角:RetrieveFlow(纯检索)和 ChatFlow(检索 + 答题的完整对话)。

1.1 RetrieveFlow:检索的三步

RetrieveFlow.retrieve 是整个检索的入口,逻辑短得惊人——三步:

# 真实源码节选,retrieve_flow.py:66
def retrieve(self, user_question: str) -> List[NodeWithScore]:
if self.engine_config.refine_question_with_kg:
# 1. 先在知识图谱里检索与问题相关的子图
_, knowledge_graph_context = self.search_knowledge_graph(user_question)
# 2. 用图谱上下文 + 聊天历史,把用户问题改写得更精准
self._refine_user_question(user_question, knowledge_graph_context)
# 3. 用(可能被精化过的)问题去向量库捞 chunk
return self.search_relevant_chunks(user_question=user_question)

这就是 Graph RAG 的核心套路:让知识图谱在向量检索之前发言,用它改写问题。refine_question_with_kg 默认是 True(chat/config.py:83)。

一个真实存在的细节(诚实标注): 上面第 2 步 self._refine_user_question(...) 的返回值没有被接住——retrieve 里调用它但丢掉了返回的 refined_question,第 3 步仍然用原始 user_question。也就是说在 RetrieveFlow.retrieve 这条路径上,精化的副作用(图谱检索本身、LLM 调用)发生了,但精化后的问题并没真正用于 chunk 检索。真正用上 refined_question 的是 ChatFlow(见 1.4),它自己重新编排了这几步。

1.2 第一步:知识图谱检索 + 转成上下文文本

search_knowledge_graph(retrieve_flow.py:81)做两件事:

  1. 建一个 KnowledgeGraphFusionRetriever,调 retrieve_knowledge_graph(user_question) 拿到结构化的实体/关系(算法细节在第 3 章)。
  2. 把结构化结果渲染成一段文本 knowledge_graph_context,供 LLM 阅读。

渲染分两种模式,取决于 using_intent_search(意图搜索,默认开):

模式何时用渲染什么模板
intentusing_intent_search=True按子问题组织的子图(to_subqueries_dict())llm.intent_graph_knowledge
normal否则扁平的 entities + relationships 列表llm.normal_graph_knowledge
# 真实源码节选,retrieve_flow.py:101 _get_knowledge_graph_context
if self.engine_config.knowledge_graph.using_intent_search:
kg_context_template = RichPromptTemplate(self.engine_config.llm.intent_graph_knowledge)
return kg_context_template.format(sub_queries=knowledge_graph.to_subqueries_dict())
else:
kg_context_template = RichPromptTemplate(self.engine_config.llm.normal_graph_knowledge)
return kg_context_template.format(
entities=knowledge_graph.entities,
relationships=knowledge_graph.relationships,
)

to_subqueries_dict()(retrievers/knowledge_graph/schema.py:137)把各个子图按它的 query(子问题)聚合成 {子问题: {entities, relationships}} 的字典——这样喂给 LLM 的上下文是按意图分组的,而不是一锅乱炖。

1.3 第二步:用图谱上下文精化问题

_refine_user_question(retrieve_flow.py:120)就是一次 LLM 调用:把图谱上下文 + 当前日期塞进 condense_question_prompt,让 fast LLM(便宜的小模型)吐出改写后的问题。

# 真实源码节选,retrieve_flow.py:126
refined_question = self._fast_llm.predict(
prompt_template,
graph_knowledges=knowledge_graph_context, # 图谱给的上下文
question=user_question,
current_date=datetime.now().strftime("%Y-%m-%d"), # 注入「今天」,处理时效性问题
)
return refined_question.strip().strip(".\"'!")

为什么注入 current_date? 像「最新版本」这种问题,LLM 不知道「最新」是哪天。把今天的日期喂进去,精化时就能落到具体时间。

1.4 第三步:向量检索 chunk

search_relevant_chunks(retrieve_flow.py:134)建一个 ChunkFusionRetriever 并执行检索。注意这里 use_query_decompose=False 写死——主对话路径不在这步再拆子问题(子问题分解的位置在第 3 章/第 2 章细说)。

# 真实源码节选,retrieve_flow.py:134
def search_relevant_chunks(self, user_question: str) -> List[NodeWithScore]:
retriever = ChunkFusionRetriever(
db_session=self.db_session,
knowledge_base_ids=self.knowledge_base_ids,
llm=self._llm,
config=self.engine_config.vector_search,
use_query_decompose=False, # 主路径不再二次分解
)
return retriever.retrieve(QueryBundle(user_question))

检索回来的 NodeWithScore 还能进一步映射回文档:get_documents_from_nodes(retrieve_flow.py:144)从每个 node 的 metadata["document_id"] 取文档 id,去库里捞文档,并保持相似度排序(sorted(..., key=lambda x: document_ids.index(x.id)))。

1.5 ChatFlow:完整对话循环(检索只是其中一段)

RetrieveFlow 只管检索。真正面向用户的对话由 ChatFlow._builtin_chat(chat_flow.py:200)编排,它把检索拆开重新串,并补上澄清和答题:

ChatFlow._builtin_chat 的 5 步:
1. _search_knowledge_graph → 图谱检索 + 上下文
2. _refine_user_question → 精化问题(这里真的接住了 refined_question)
3. _clarify_question (可选) → 上下文不够就反问用户,直接结束
4. _search_relevance_chunks → 用 refined_question 做向量检索
5. _generate_answer → 把图谱上下文 + chunk 喂大模型,流式吐答案

对照 chat_flow.py:208-247 能看到这五步的真实调用。和 RetrieveFlow.retrieve 的关键差别:ChatFlow 第 4 步用的是第 2 步精化后的 refined_question,所以图谱精化在对话里是真正生效的。

流式与可观测: 每一步都 yield 一个 ChatEvent(如 KG_RETRIEVAL 状态),前端能实时显示「正在搜索知识图谱…」;整个流程包在 Langfuse trace 里(chat_flow.py:165 起的 _trace_manager.observe)。

1.6 小结

  • 主线 = KG 检索 → 精化问题 → 向量检索,这三步是 Graph RAG 的骨架。
  • RetrieveFlow 是纯检索编排;ChatFlow 在外面套澄清/答题/落库,并且真正用上精化后的问题
  • 图谱的产物以文本上下文形式回流,既用于精化问题、也用于最终答题。

下一章进入向量检索的内部:多 KB 并发、子问题分解、融合去重、TiDB oversampling、reranker。