大家好,我最近在整理 LangGraph 搭建的问答工作流中的一些知识点。本章主要介绍LangGraph 实现的知识检索节点,它是 RAG 链路核心执行单元,负责完成查询处理、权限过滤、文档检索与结果封装,为下游答案评估、内容生成节点提供标准结构化数据。这部分很重要,下面我仔细结合代码讲解一下这里,后面会连续更新这个系列。
0 LangGraph介绍
LangGraph是在LangChain之上构建的模块,它主要是用于更好地创建循环图。在之前的内容里面我们一直在用 LangChain 写链、写 Agent,今天开始介绍LangGraph ,LangGraph 可以独立使用,但它也能与任何 LangChain 产品无缝集成,为开发人员提供构建智能体所需的全套工具。LangGraph生态如下
- LangSmith --- 有助于智能体评估和可观察性。调试性能不佳的 LLM 应用程序运行、评估智能体轨迹、在生产环境中获得可见性,并随着时间的推移提高性能。
- LangGraph 平台 --- 使用专为长时间运行、有状态工作流设计的部署平台,轻松部署和扩展智能体。在团队之间发现、重用、配置和共享智能体 --- 并在LangGraph Studio中通过可视化原型快速迭代。
- LangChain -- 提供集成和可组合组件,以简化 LLM 应用程序开发。
理解
LangGraph最好的方式是先搞清楚它的三个基本概念:状态、节点和边。
State:一个共享数据结构,表示应用程序的当前快照。它可以是任何 Python 类型,但通常是TypedDict或 PydanticBaseModel。Nodes:编码代理逻辑的 Python 函数。它们接收当前的State作为输入,执行一些计算或副作用,并返回一个更新的State。Edges:Python 函数,根据当前的State决定接下来执行哪个Node。它们可以是条件分支或固定转换。
一、标准 RAG 执行流程(扫盲)
本节点调用统一检索流水线,完整执行行业通用六大 RAG 流程:
- Query Rewrite(查询改写) :优化用户原始语句,修正口语化表达,统一语义,提升检索匹配精度
- ACL(Access Control List 访问控制列表) :依据用户身份,过滤用户无访问权限的知识库内容
- Hybrid Retrieval(混合检索) :结合稠密向量检索 与稀疏关键词检索,批量召回相关文档
- RRF(Reciprocal Rank Fusion 倒数排名融合) :整合多路检索结果,完成去重并统一排序权重
- Rerank(结果重排) :对初筛文档二次相似度打分,筛选高关联有效文档
- Grounded Answer(事实锚定回答) :仅依托知识库原文生成答案,有效减少大模型幻觉问题
数据流交互
- 上游输入:从全局状态
state读取query(用户查询语句) 、user(用户身份对象) - 下游输出:将检索数据写入全局状态,供给grader_node(答案评估节点) 、generate_node(答案生成节点) 调用
- 兼容设计:同时输出新版结构化数据与项目旧版数据格式,(注意:适配项目架构迭代过渡期,无需改动原有业务调用逻辑)
二、版本兼容说明
我的旧链路是这样的:knowledge_rag_node → run_rag_chain → KnowledgeRetriever → knowledge_service.search() 旧链路的问题:
1. 只有 dense 向量检索,没有 sparse/BM25
- dense(向量检索) :把"年假怎么休"变成一串数字(向量),去向量库里找最接近的文档。适合语义匹配,比如"怎么休"能匹配到"休假制度"。
- sparse/BM25(关键词检索) :像百度搜索一样,直接匹配关键词。适合精确查询,比如查"PRJ-001"这种编码。
- 旧链路的问题:如果用户问了一个带精确编码的问题,向量检索可能"理解"错意思,搜不到。新链路两路一起查,互相兜底。
2. 没有 RRF 融合
- RRF:把 dense 搜到的结果和 sparse 搜到的结果合并成一份,去掉重复的。
- 旧链路的问题:旧链路只有 dense 一路结果,不需要合并。但这也意味着如果 dense 漏了,没有 sparse 来补。
3. 没有 rerank(重排序)
- rerank:第一次检索可能返回 50 条,rerank 用更精细的打分模型把这 50 条重新排一遍,把最相关的放前面。
- 旧链路的问题:直接拿 Milvus 返回的相似度分数排序,可能把不相关的东西排前面。新链路多了一步精排。
4. 没有 query rewrite、没有 ACL
- query rewrite:用户说"请问一下年假怎么休",rewrite 把它改成"年假 休假 流程",更适合检索。
- ACL :按用户身份过滤文档。比如
li.wei(技术部)看不到财务部的内部文档。 - 旧链路的问题:直接拿用户原话去搜,口语前缀干扰检索;所有人看到的东西一样,没有权限隔离。
5. 没有 citations,只有简单的 sources
- 旧 sources :
{"source_file": "休假制度.txt", "page_num": 1, "snippet": "..."}------ 信息少,前端只能显示"来自休假制度.txt"。 - 新 citations :
{"doc_id": "doc-2", "chunk_id": "doc-2-c1", "section_path": "总则", ...}------ 前端以后可以做"点击跳转到原文第3章第2节"。
新链路增加了citations(结构化更强,带 chunk_id/section_path)和 retrieval_debug(排障信息),节点接入项目新版统一检索方法 run_retrieval_pipeline ,调用后返回 KnowledgeAnswerPayload(统一检索结果实体) ,内置引用信息、检索追踪日志;为保障存量业务稳定运行,代码内部完成数据格式自动转换 ,兼容项目原有 draft_answer、sources、retrieved_docs 传统业务字段。
三、完整业务源码
我的完整项目工作流文件目录如下:
后面会逐一更新,下面介绍本文的源码:
ini
"""知识检索节点。
from __future__ import annotations
from langchain_core.documents import Document
from app.chains.rag_chain import run_retrieval_pipeline
from app.core.security import CurrentUser
async def knowledge_rag_node(state: dict) -> dict:
# 用户发起的检索提问,CurrentUser类型用户对象,用于解析访问权限,无参数则启用公开访问策略,返回封装检索文档、基础答案、来源引用、检索日志的状态字典
# 1.提取全局状态内的查询内容与用户信息
query = state["query"]
user = state.get("user")
# 2.校验用户对象类型,非法格式自动降级为匿名公开访问
if user is not None and not isinstance(user, CurrentUser):
user = None
# 3.异步调用统一RAG检索流水线,执行完整检索逻辑
payload = await run_retrieval_pipeline(query, user=user)
# 4.转换适配旧版来源数据格式
sources = [
{
"doc_id": c.doc_id,
"source_file": c.source_file,
"page_num": 1,
"department": "",
"score": 0.0,
"snippet": c.snippet,
}
for c in payload.citations
]
# 5.转换适配LangChain标准Document文档格式
retrieved_docs = [
Document(
page_content=c.snippet,
metadata={
"doc_id": c.doc_id,
"source_file": c.source_file,
"page_num": 1,
"department": "",
"doc_type": "",
"score": 0.0,
},
)
for c in payload.citations
]
# 6.统一返回多格式检索结果至工作流状态
return {
"retrieved_docs": retrieved_docs,
"draft_answer": payload.answer,
"sources": sources,
"citations": payload.citations,
"retrieval_debug": payload.retrieval_debug,
}
四、代码核心逻辑解析
1. 依赖导入释义
Document:LangChain 官方标准文档实体,统一项目知识库文本存储格式run_retrieval_pipeline:项目封装通用检索执行方法,整合全部 RAG 业务逻辑CurrentUser:项目自定义用户实体类,存储用户角色、访问范围等权限信息
2. 用户权限处理逻辑
内置用户实体强类型校验 ,(注意:仅识别规范CurrentUser对象);用户信息为空、格式异常时,自动关闭 ACL 权限过滤,仅检索公开无限制知识库内容,避免权限异常引发检索失败。
3. 异步执行特性
async 异步关键字定义,支持多用户请求并行处理,dddd。
4. 数据格式转换逻辑
从 payload(新版检索结果实体) 中读取引用数据,循环组装为项目旧版字典格式、LangChain 标准文档格式,实现新检索能力无缝对接存量业务。
五、全局状态返回字段说明
| 字段名 | 英文释义 | 数据用途 | 使用场景 |
|---|---|---|---|
| retrieved_docs | 检索完成文档集合 | 存储结构化知识库文本 | 传统答案生成节点读取文本素材 |
| draft_answer | 草稿答案 | 检索后生成的基础回答文本 | 快速输出简易问答结果 |
| sources | 来源信息列表 | 轻量化文档出处数据 | 旧版业务完成基础内容溯源 |
| citations | 引用数据源 | 标准化结构化引用信息 | 新版业务实现前端精准原文定位 |
| retrieval_debug | 检索调试日志 | 记录全流程检索运行信息 | 开发排错、线上检索效果排查 |
举个🌰
json
{
"draft_answer": "员工年假按累计工龄计算...",
"sources": [
{
"doc_id": "doc-2",
"source_file": "休假制度.txt",
"page_num": 1,
"department": "",
"score": 0.0,
"snippet": "年假休假制度说明"
}
],
"retrieved_docs": [Document对象],
"citations": [
{
"doc_id": "doc-2",
"chunk_id": "doc-2-c1",
"source_file": "休假制度.txt",
"section_path": "总则",
"snippet": "年假休假制度说明"
}
],
"retrieval_debug": {...}
}
六、开发实操注意要点
- 核心检索逻辑统一收口,节点只做适配 业务不参与这部分
所有检索规则(召回数量、排序权重、RRF 融合策略)全部封装在run_retrieval_pipeline中,
knowledge_rag_node只做「参数透传 + 结果格式适配」。
(注意:避免把检索逻辑散落在节点里,后期调优、改策略时,不用修改工作流节点代码,只维护公共流水线即可) - 预留业务拓展字段
retrieved_docs和sources中的page_num、score目前用默认值占位,没有直接绑定业务逻辑。(注意:后续如果要做「文档分页召回」或「相似度排序展示」,可以直接从payload的检索结果里读取真实值赋值,不用重构节点结构) - 权限校验做「容错兜底
节点内仅做CurrentUser类型校验,非法 / 异常用户直接降级为公开访问策略,不抛出异常打断流程。(注意:和上游auth_check_node的权限校验分层,核心权限过滤逻辑在run_retrieval_pipeline里执行,这里只做兜底,避免节点越权) - 新旧字段并行输出,分批升级
节点同时返回draft_answer/sources/retrieved_docs旧格式和citations/retrieval_debug新格式,下游grader_node、generate_node不用改代码就能对接新链路。(注意:可以分批升级下游节点,不用一次性全量改造,大幅降低线上发布风险)