RAG 實戰筆記

LangChain 在 RAG 中的角色定位:官方列了六大元件,我的專案只留下一個

2026-08-17 #RAG#Qdrant#LangChain#LangGraph

查 LangChain 的資料,十篇有九篇告訴你它是「打造 LLM 應用的完整框架」。AWS 的〈什麼是 LangChain〉把它整理成六大元件:LLM 介面、提示範本、代理、擷取模組、記憶體、回呼——聽起來 RAG 該有的它全包了。

但回頭看我實際跑起來的企業 RAG 專案(就是這個系列前四篇一直在講的那個),requirements.txt 裡沒有 langchain。六大元件裡,最後真正留在專案裡的角色只有一個:編排——而且用的是它的姊妹專案 LangGraph。其餘五個元件,全部被更直接的東西取代。

這篇不是勸退文。我想用一個真實專案逐一對照:LangChain 宣稱的每個元件,在需求變具體之後發生了什麼事、為什麼被換掉、換掉的代價是什麼——以及什麼情境下你反而應該用它。

一、AWS 怎麼描述 LangChain

先把官方視角擺出來。AWS 那篇文章的核心概念是鏈(chain):「從使用者查詢到模型輸出的一系列自動化行動」,鏈由一個個**連結(link)**組成——格式化輸入、查詢 LLM、檢索資料,各是一個連結。開發者用宣告式的方式把連結串成鏈,不用自己寫膠水程式碼。

支撐這個概念的是六大元件:

元件 AWS 說它做什麼
LLM 介面 統一的 API 連接各家公有/專有模型,不用自己處理各家 SDK 差異
提示範本 預先結構化的查詢格式,確保一致性、可重複使用
代理(Agents) 讓 LLM 自行決定最佳行動序列的特殊鏈
擷取模組 支援 RAG 的檢索元件:document loaders、text splitters、vector stores、retrievers
記憶體 儲存與調用過去的互動,支援多輪對話
回呼(Callbacks) 監控與記錄鏈執行過程中的事件

這個清單本身沒有錯。問題在於:它隱含了一個假設——你的 RAG 每一層都適合用通用抽象來蓋。我的專案就是這個假設的反例。

二、對照真實專案:六個元件的實際下場

專案背景簡述(細節在系列第一篇):企業內部文件問答,中文為主、含大量掃描 PDF 與表格型公告,embedding 用本機 bge-m3(文件不出本機),向量庫 Qdrant 混合檢索,生成走 Claude、但機密文件必須路由到地端模型。

把六大元件跟專案裡實際負責那件事的程式碼放在一起:

LangChain 元件 專案裡實際用的東西 位置
LLM 介面 自製 LLMProvider 抽象類別 + anthropic 官方 SDK rag/llm.py
提示範本 Python 字串常數 + f-string rag/graph.py
代理 沒有——流程是固定的,不需要 LLM 決定走哪步 —
擷取模組 qdrant-client 直接呼叫 + pypdf/pytesseract + 自製切塊 rag/vectorstore.py、rag/loaders.py、rag/chunking.py
記憶體 history 就是一個 list[dict],配 query rewriting rag/graph.py
回呼 Python 標準 logging 各模組
(編排) LangGraph StateGraph ✓ rag/graph.py

唯一存活的是表格最後一列——編排。整個問答流程是一個三節點的狀態機:

flowchart LR
    S([問題進來]) --> RW[rewrite
追問改寫成獨立問題] RW --> RT[retrieve
Qdrant 混合檢索
含權限過濾] RT --> GEN[generate
先決定路由再生成
機密→地端 · 非機密→雲端] GEN --> E([帶來源編號的答案])

節點是 LangGraph 的,節點裡面全是直接呼叫:retrieve 直接打 qdrant-client,generate 直接用 anthropic SDK。框架只負責「狀態怎麼流過這三步」,不碰任何一步的內容。

三、為什麼五個元件被換掉:每個都有具體理由

不是為了炫技才自己寫。每一個被換掉的元件,背後都是一個通用抽象包不住的具體需求。

LLM 介面:因為路由邏輯要長在這一層

專案的硬需求是 FR-60:任何一個檢索到的切塊標記為機密,這次生成就必須走地端模型,雲端連看都不能看到。這代表「選哪個模型」不是啟動時設定一次就好,而是每一次問答、在生成之前依檢索結果動態決定。

自製的抽象層小到只有一個方法:

1
2
3
4
5
6
class LLMProvider(ABC):
"""所有生成模型共用的介面。接地端或雲端都實作同一個介面。"""

@abstractmethod
def generate(self, *, system: str, user: str, max_tokens: int) -> str:
...

路由就建在這層上:route_provider(has_conf) 依機密與否回傳不同的 provider 實例,呼叫端介面完全不變。LangChain 的 ChatModel 抽象當然也做得到,但那是在別人的抽象上再包一層去實作自己的路由;而這裡的需求本質上只是「給 system 和 user、拿回文字」,20 行 ABC 就收工。當你需要的介面比框架提供的小很多,框架就是負資產。

順帶一提,連 query rewriting 用哪個模型都經過這層:改寫時會把對話歷史送給模型,而歷史裡可能含前一輪由機密文件生成的答案——所以只要地端可用,改寫一律走地端。這種安全推理要求你對「哪個字串會流向哪個模型」有完全的掌握,抽象層愈厚愈難確認。

提示範本:中文 prompt 用 f-string 就夠了

專案的 system prompt 是一段中文規則(只根據參考資料回答、逐句標來源編號、繁體中文作答),外加依使用者偏好注入的詳細程度條款。全部就是字串常數加字典查表:

1
2
3
4
5
6
_VERBOSITY = {
"concise": "4. 簡潔作答,直接給結論,避免鋪陳。",
"standard": "4. 適度說明,必要時分點,但不冗長。",
"detailed": "4. 詳細說明,分點列出條件與例外,並指出各項對應的來源。",
}
system = _SYSTEM_PROMPT + "\n" + _VERBOSITY.get(verbosity, _VERBOSITY["concise"])

PromptTemplate 解決的問題(變數插值、範本重用、部分填值)在 Python 裡本來就有語言級的解法。範本元件真正划算的場景是 prompt 要進版本管理系統、要跨團隊共用、要 A/B 測試——單一專案內就是多學一套 API。

擷取模組:這裡是差距最大的地方

這是六大元件裡我最想展開講的,因為「擷取」恰好是這個專案最不通用的部分,前面三篇系列文各講過一段:

  • 載入:企業 PDF 一半是掃描檔。loaders.py 先抽文字層,平均每頁不到 20 個字元就判定為掃描檔、逐頁丟 tesseract OCR(chi_tra+eng)。這個「先試文字層、不行才 OCR」的回退邏輯是針對這批文件調出來的。
  • 切塊:中文以字元數計(700 字、重疊 120),切點優先落在換行與中文句末標點;表格型公告走「表格列感知」切塊——一筆案件不被拆散、每塊自帶表頭與日期(第一篇講過這個對命中率的影響)。LangChain 的 text splitter 家族以英文與程式碼為主場,中文標點與表格列的邏輯終究要自己寫。
  • 檢索:dense top-20 + sparse top-20 → RRF 融合 → 30 個候選 → bge-reranker-v2-m3 精排取 15(完整拆解在第三篇),而且權限過濾直接下在 Qdrant 查詢條件裡——沒權限的切塊連進候選的機會都沒有(第四篇的主題)。這條管線的每個環節都要碰 qdrant-client 的原生 API(named vectors、sparse vectors、payload filter),retriever 抽象在這裡不是幫忙,是隔了一層玻璃。

公道地說:這也是 LangChain 最有價值的地方的反面。它的 document loaders 有數百個現成整合(Notion、Slack、S3、各種資料庫),如果你的需求是「快速接上十種資料源看看效果」,自己寫是傻的。我的專案只需要六種本機檔案格式,而且每種都有在地化的處理細節——需求愈具體,現成整合的價值愈低。

記憶體:多輪對話的重點不是「記住」,是「改寫」

這是做階段 B 時最大的體悟。使用者追問「那它呢?」,把這句連同歷史一起塞進 prompt(LangChain memory 的典型用法)只解決了生成端;檢索端還是拿「那它呢?」去查向量庫,撈回來的全是雜訊。

真正的解法是 query rewriting:檢索前先用 LLM 把追問改寫成可獨立理解的問題(「那它呢?」→「115年偵字第5413號的移送機關是哪裡?」),再拿改寫後的查詢去檢索。所以專案的「記憶體」就是 state 裡一個 list[dict],真正做事的是 rewrite 節點。框架的 memory 元件解決的是儲存與注入,而 RAG 多輪的痛點在檢索查詢的品質——問題根本不在那個元件的守備範圍裡。

代理與回呼:沒有那個問題,就不需要那個解法

流程是固定的 rewrite → retrieve → generate,不存在「LLM 自行決定下一步」的空間——企業問答要的是可預測、可稽核,固定流程是特性不是限制。回呼要解決的觀測需求,標準 logging 目前夠用;等到要上分散式追蹤,接的也會是 OpenTelemetry 而不是框架自帶的 callback。

四、留下來的那一個:LangGraph 憑什麼

講完五個被換掉的,講講留下來的憑什麼。graph.py 的骨架長這樣:

1
2
3
4
5
6
7
8
9
graph = StateGraph(QAState)
graph.add_node("rewrite", rewrite)
graph.add_node("retrieve", retrieve)
graph.add_node("generate", generate)
graph.add_edge(START, "rewrite")
graph.add_edge("rewrite", "retrieve")
graph.add_edge("retrieve", "generate")
graph.add_edge("generate", END)
return graph.compile()

三個節點都是普通 Python 函式,吃 QAState(一個 TypedDict)、回傳要更新的欄位。這個骨架給了三樣自己寫迴圈拿不到的東西:

  1. 狀態的形狀是顯式的。 QAState 把整條管線會流動的資料(原始問題、改寫後查詢、權限條件、檢索結果、路由結果)寫成一個型別,每個節點只宣告自己改了哪幾欄。事後回來加欄位(階段 C 加 perm_filter、FR-60 加 route/model)時,影響範圍一眼可見。
  2. 加節點不用動別人。 rewrite 節點是階段 B 才加的——單輪問答時代這張圖只有 retrieve → generate。插一個節點進來,改的是兩行 add_edge,既有節點一行不動。
  3. 可注入、可測試。 build_graph(client=None) 讓測試與評測共用同一個 Qdrant 連線,整張圖 compile 出來就是一個可以 invoke 的物件,黃金問答集評測直接打它。

同樣公道地說:三節點的線性流程,用三個函式呼叫串起來也死不了。LangGraph 在這個規模是「舒服」而不是「必要」;它真正的護城河在流程長出分支與迴圈之後——條件路由(檢索品質差就改寫重查)、人工介入點、checkpoint 恢復。專案選它是為了那個方向的演進空間,而不是現在就非它不可。

五、一個誠實的註腳:你其實躲不開 langchain-core

最後補一個查環境時發現的事實。專案的 requirements.txt 明明沒有任何 langchain 開頭的套件,但 pip list 長這樣:

1
2
3
4
langchain-core         0.3.86
langgraph 0.2.62
langgraph-checkpoint 2.1.2
langsmith 0.8.5

langchain-core 是 langgraph 的相依套件,連 langsmith(LangChain 的觀測服務 SDK)都一起進來了。所以精確的說法不是「這個專案沒用 LangChain」,而是:用了 LangChain 生態的編排層與底層核心,跳過了它全部的高階抽象。這其實正是 LangChain 官方近年拆包(langchain-core / langgraph / 各 integration 套件分離)想支持的用法——生態系自己也承認,不是每個人都要整包吞。

結論:把 LangChain 當工具箱,不是當地基

回到標題的「角色定位」。走完這個專案,我對 LangChain 在 RAG 裡的定位是:

  • 原型期,它是加速器。 現成的 loaders、splitters、retrievers 讓你三天內看到端到端效果,這個價值是真的。
  • 需求變具體之後,抽象會一層層被換掉。 中文切塊、OCR 回退、混合檢索的原生 API、機密路由、檢索層權限——每個具體需求都在把你推向直接呼叫。這不是 LangChain 的失敗,是通用抽象的宿命。
  • 最後留下的是編排。 因為「狀態怎麼流過流程」這件事天然通用,跟你的文件是不是中文、向量庫是不是 Qdrant 無關。挑框架時值得反過來想:愈靠近資料與領域的層,愈早自己寫;愈靠近流程結構的層,框架愈留得住。

這篇是「RAG 實戰筆記」系列第五篇。前四篇:RAG 基礎邏輯與實戰教學、向量資料庫與 MongoDB 差在哪、混合檢索深入拆解、檢索層權限實作與應用。

Creative Commons 姓名標示 非商業性

本文採用 CC BY-NC 4.0 授權

歡迎轉載與引用,請標明出處(LangChain 在 RAG 中的角色定位:官方列了六大元件,我的專案只留下一個 — mur mur);禁止用於商業用途。

商業使用或合作提案,歡迎來信洽談:[email protected]

留言
分享

留言