
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 毫秒。