LangGraph Rag Agent 学习 LangGraph实战用状态图构建多步Agent工作流-腾讯云开发者社区-腾讯云用StateGraph实现分支、循环与人工审批LangGraph的StateGraph是如何用一张有向图把分支判断、循环重试、人工审批、断点续传这四件Chain做不了的事一次解决的。企业级Agent的本质不是调用LLM是编排一个可靠的工作流。而工作流本质上是一个状态机。StateGraph核心四件套LangGraph的核心是StateGraph——一个有向图状态机。① State状态一个共享的TypedDict所有节点都读它、写它。是图的血液。② Node节点一个函数输入State输出State的部分更新。是图的背肌。③ Edge边节点之间的连接。可以是硬边A一定到B。④ Conditional Edge条件边一个路由函数读当前State决定下一个节点是谁。这是Chain跟Graph拉开差距的结构。# 第一步定义状态 - 这是「血液」 from typing import TypedDict from langgraph.graph import StateGraph, ENDclass DocState(TypedDict): raw_text: str extracted: dict confidence: float needs_review: bool final_status: str# 第二步定义节点函数 def extract_node(state: DocState): result llm.invoke(state[raw_text]) return { extracted: result.data, confidence: result.score } def check_node(state: DocState): threshold 0.85 return {needs_review:state[confidence] threshold}Conditional Edge让文档走不同路径# 第三步路由函数关键 def route_after_check(state: DocState): if state[needs_review]: return human_review return write_to_db # 第四步装配图 builder StateGraph(DocState) builder.add_node(extract, extract_node) builder.add_node(check, check_node) builder.add_node(human_review, hitl_node) builder.add_node(write_to_db, db_node) builder.set_entry_point(extract) builder.add_edge(extract, check) builder.add_conditional_edges(check, route_after_check) builder.add_edge(write_to_db, END) graph builder.compile()add_conditional_edges接收的路由函数返回的是节点名字不是节点本身。这让整个图的路径变成运行时决定的而不是定义时硬编码的。意味着LLM本身可以决定走哪里——这才是Agent而不是工作流。文档入库 → LLM抽取 → 质量检查 → 人工审批 → 写入向量库文档处理工作流拓扑START↓ingest文档入库↓llm_extractLLM抽取↓quality_check质量检查↓ Conditional Edgehuman_review人工审批|write_vector_db写入向量库↓END低置信文档自动走人工审批分支高置信直接写库第四章 Checkpointing让Agent断点续跑每次节点执行结束自动把State快照写到持久化存储。流程中断后从最近一个checkpoint恢复跳过已执行的节点。这就把Agent的重启重跑变成了重启接上。从开发到生产Checkpoint有三档配置▸ 开发期InMemorySaver— 内存里存进程退出就丢单元测试用。▸ 单机生产SqliteSaver— 本地SQLite文件进程重启不丢数据。▸ 集群生产PostgresSaver— 多进程共享支持高并发企业首选。# 生产配置Postgres持久化 from langgraph.checkpoint.postgres \ import PostgresSaverDB_URI postgresql://user:pwdlocalhost:5432/agent_state checkpointer PostgresSaver.from_conn_string(DB_URI) checkpointer.setup() # 创建 schemagraph builder.compile(checkpointercheckpointer,interrupt_before[human_review]) # 用thread_id标识一次会话 config {configurable: {thread_id: doc-2026-001}} # 第一次运行跑到human_review暂停 result graph.invoke({raw_text: doc}, config) # 进程崩溃也没关系下次 # 用同一个thread_id继续 result graph.invoke(None, config) # LangGraph自动从checkpoint恢复这里的interrupt_before[human_review]就是HITLHuman-in-the-Loop的入口图执行到human_review节点前会暂停把当前State持久化等待人工接入。这是企业Agent上线的硬门槛——没有HITL就别谈生产环境。还有一个被低估的特性时间旅行调试。你可以遍历某个thread_id的所有checkpoint回到任意历史状态重跑。加上Postgres Checkpoint后最常见的两类问题——进程被k8s重启导致执行中断和用户中途修改输入导致需要回滚——都从架构层面消失了。第五章 Human-in-the-LoopAI暂停等人第四章的interrupt_before是HITL的入门款。LangGraph还有更细粒度的interrupt()函数让节点内部主动暂停。这一点对企业级场景至关重要——很多审批不是前置审批而是过程中决策点。from langgraph.types import interrupt from langgraph.types import Commanddef human_review_node( state: DocPipelineState): # 把决策上下文交给人 decision interrupt({ doc_id: state[doc_id], meta: state[extracted_meta], confidence: state[confidence], prompt: approve / reject / edit }) return {review_decision: decision}# 前端拿到interrupt后渲染审批面板 # 用户决定后用Command恢复 graph.invoke(Command(resumeapproved),config)interrupt()抛出的不是异常是一个暂停信号当前State自动持久化前端拿到interrupt数据渲染审批UI用户决定后通过Command(resume...)把结果送回节点函数继续执行——就好像那个interrupt调用刚返回一样。四种HITL经典模式企业级Agent里基本都见过模式一 · 审批确认低置信度结果暂停等审批approve则继续reject则回炉。模式二 · 内容编辑把LLM抽取结果交给人编辑编辑后的内容回写到State继续往下走。模式三 · 多选决策LLM给出几个候选方案让人选一个。最常见于工具选择、动作规划。模式四 · 异常上报遇到无法处理的情况主动停下来附上上下文给人避免Agent硬跑出错。第六章 SubGraph与选型对比SubGraph是LangGraph的模块化机制——当工作流变大时把文档解析质量校验索引写入等子任务封装成可复用的子图主图只关心编排逻辑。# 子图文档解析 parse_subgraph build_parse_graph() # 子图质量校验 validate_subgraph build_validate_graph() # 主图直接把子图作为节点 main StateGraph(MainState) main.add_node(parse, parse_subgraph) main.add_node(validate, validate_subgraph) main.add_edge(parse, validate)框架核心抽象最佳场景不适合LangGraph有向状态图可控、可中断、可观测的企业工作流完全开放式的多智能体涌现CrewAI角色任务角色化协作产品经理工程师QA需要严格状态机控制的流程AutoGen / AG2多Agent对话研究、头脑风暴、群聊式协作需要确定性输出的生产场景Claude Agent SDK原生工具循环Anthropic生态深度集成多模型、跨Provider场景OpenAI Agents SDKHandoff交接OpenAI模型为主的轻量场景需要细粒度状态持久化Agent工程的三层抽象第一层 · 调用Chain时代我能让LLM跑起来。第二层 · 编排StateGraph时代我能让多个LLM按图协作。第三层 · 治理CheckpointHITL时代我能让生产Agent可中断、可审批、可观测。混合检索RAG多路召回Reranker重排模型实战-腾讯云开发者社区-腾讯云向量检索和关键词检索用的是两套完全不同的评分体系向量检索给的是余弦相似度取值在 01 之间ES 的 BM25 分数理论上没有上界可能是 3.2也可能是 27.8。这两个分数直接放一起比大小就跟拿身高的厘米数跟体重的公斤数去排序一样压根不是一个量纲谁排在前面全看运气。单路召回为什么不够用向量检索的盲区语义漂移向量检索擅长语义相近但对精确匹配特别不敏感。用户问CVE-2024-3094 影响哪些版本向量检索会召回一堆讲漏洞影响范围安全补丁的相关文档但很可能漏掉那篇标题里精确写着这个 CVE 编号、内容却是纯表格没什么语义描述的公告文档——因为向量模型对编号、型号这类字符串的语义表达能力天生就弱编号本身在向量空间里几乎是噪声。专有名词、编号、精确术语向量检索天生吃力。用户问服务突然挂了怎么排查文档里写的是进程异常退出后的故障定位方法两句话意思一样但共同出现的关键词几乎为零BM25 直接抓瞎。纯关键词检索在口语化提问场景下的 Top-5 召回率只有向量检索的 60% 左右这个差距在客服场景尤其致命因为用户很少会用文档里的官方措辞来提问。向量检索管意思相近关键词检索管字面精确两者互补而不是互相替代。多路召回架构谁跟谁并行查最终采用的是三路并行召回并行分发路径A →Milvus向量检索 Top 30管语义相近路径B →ES BM25检索 Top 30管字面精确路径C →知识图谱检索 Top 10管实体关系三路结果汇总RRF融合排序Reranker精排Top 5 送入LLM前面提到分数量纲不一致的问题业界的标准解法是RRFReciprocal Rank Fusion倒数排名融合。它的核心思路特别聪明不看原始分数只看排名rank排名是天然可比的第 1 名不管在哪一路都是最好不存在量纲问题。公式很简单RRF_score(d) Σ 1 / (k rank_i(d))对文档 d把它在每一路召回结果里的排名 rank_i 取倒数再累加k 是个平滑常数通常取 60防止排名靠前的文档权重过大导致分数差距失真。排名越靠前1/(krank) 越大贡献越高一篇文档如果同时在向量检索和关键词检索里都排前几名它的融合分数会明显高于只在一路里靠前的文档——这正是我们想要的效果两路都认可的结果可信度更高。混合检索RAG实战多路召回Reranker重排模型_多路检索后还需要rerank吗,为什么rerank后反而把标准答案排到了后面-CSDN博客def rrf_fusion(vector_results: list[str],bm25_results: list[str],k: int 60) - dict[str, float]: 对两路召回结果做RRF融合 scores: dict[str, float] {} for rank, doc_id in enumerate( vector_results, start1 ): scores[doc_id] scores.get(doc_id, 0) 1 / (k rank)for rank, doc_id in enumerate(bm25_results, start1): scores[doc_id] scores.get(doc_id, 0) 1 / (k rank) return dict(sorted(scores.items(),keylambda x: x[1],reverseTrue))单路向量检索 Top-5 命中率 76%单纯拼接两路结果不做融合直接各取一半是 79%用 RRF 融合之后到了 87%。业界经验值 k60 是从信息检索领域的大量实验里得出的没有特殊场景不建议改。为什么还需要 RerankerReranker重排模型解决的正是这个问题它是一个专门训练用来判断query 和某段文档到底有多相关的模型直接吃 query 和候选文档的原始文本输出一个精确的相关性分数Bi-Encoder vs Cross-Encoder为什么 Rerank 不能用向量检索代替向量检索用的 Embedding 模型和 Reranker 模型都是判断相关性为什么不能只用向量检索答案在于两者的模型架构完全不同。Embedding 模型是Bi-Encoder架构query 和文档分别独立编码成向量之后算个余弦相似度。这种架构的好处是可以提前把文档向量算好存库里检索时只需要编码 query速度极快能支撑百万级候选集的检索。模型只能靠两个独立向量的几何距离去近似相关性精度天然有损失。Reranker 用的是Cross-Encoder架构query 和文档拼接成一个整体输入模型让模型在自注意力层里充分交互逐词判断相关性精度明显更高。代价是没法预计算每个候选文档都要跟 query 现场拼接一次做推理计算量比 Bi-Encoder 高一个量级这也是为什么 Reranker 只能用在粗筛之后的少量候选上不可能拿去做百万级的初筛。Bi-EncoderEmbeddingquery和doc独立编码算余弦距离 速度快可预计算 适合百万级初筛Cross-EncoderRerankerquery和doc拼接后一起编码 自注意力充分交互精度更高 速度慢只能用于精排少量候选模型Top-5准确率单次延迟(20候选)部署方式仅RRF无Rerank87.0%--BGE-Reranker-v2-m393.4%约80ms私有化部署Cohere Rerank 394.1%约200ms含网络API调用业务数据微调交叉编码器96.7%约60ms私有化部署Rerank 这一步比 Embedding 更值得投入微调成本。mbedding 微调是给通用向量空间做局部调整收益有天花板而 Reranker 本身就是专门判断相关性的模型用业务真实的 query-doc 对做微调相当于直接教它认识你的业务语言收益立竿见影。BGE-Reranker-v2-m3 打底拿线上积累的用户反馈数据点赞/点踩人工复核持续微调数据敏感、有一定 ML 工程能力选 BGE-Reranker 私有化部署持续微调长期收益最大批量推理是唯一解批量推理Batching把 20 个 query-doc 对拼成一个 batch 一次性丢进模型from FlagEmbedding import FlagRerankerreranker FlagReranker(BAAI/bge-reranker-v2-m3,use_fp16True) def rerank(query: str, docs: list[str]) - list[float]: 一次批量推理而非逐个调用 pairs [[query, d] for d in docs] scores reranker.compute_score(pairs, batch_size32) return scores线上有大量高频重复问题客服场景尤其明显怎么退款这类问题一天能问几百遍。对这种情况我们加了一层基于 query 语义相似度的缓存——不是精确字符串匹配缓存用户措辞千变万化命中率极低而是拿 query 的 Embedding 向量做近似去重语义高度相似的 query 直接复用之前的 Rerank 结果async def rerank_with_cache(query: str, docs: list[str]): q_vec await embed_query(query) # 查缓存语义相似度0.95视为同一query cached await semantic_cache.get(q_vec, threshold0.95) if cached: return cachedscores await rerank(query, docs) await semantic_cache.set(q_vec, scores, ttl3600) return scores上线之后统计缓存命中率大概在 35% 左右也就是三分之一的 Rerank 请求直接省掉了GPU 负载明显降下来了。这里有个细节要提醒相似度阈值设 0.95 是我们业务场景客服问答措辞相对集中跑出来的经验值如果你的场景 query 多样性很高这个阈值可能需要调得更宽松否则缓存命中率会很低白白多一层查询开销。方案Recall5MRRP99延迟纯向量检索76.2%0.6845ms纯BM25检索71.5%0.6320ms双路RRF融合87.0%0.7970ms双路RRFReranker96.7%0.94155ms从单路到双路RRFRerankerRecall5 提升了 20 个百分点MRR衡量正确答案排名靠前程度的指标提升了近 40%代价是延迟从 45 毫秒涨到 155 毫秒。