查 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 | class LLMProvider(ABC): |
路由就建在這層上:route_provider(has_conf) 依機密與否回傳不同的 provider 實例,呼叫端介面完全不變。LangChain 的 ChatModel 抽象當然也做得到,但那是在別人的抽象上再包一層去實作自己的路由;而這裡的需求本質上只是「給 system 和 user、拿回文字」,20 行 ABC 就收工。當你需要的介面比框架提供的小很多,框架就是負資產。
順帶一提,連 query rewriting 用哪個模型都經過這層:改寫時會把對話歷史送給模型,而歷史裡可能含前一輪由機密文件生成的答案——所以只要地端可用,改寫一律走地端。這種安全推理要求你對「哪個字串會流向哪個模型」有完全的掌握,抽象層愈厚愈難確認。
提示範本:中文 prompt 用 f-string 就夠了
專案的 system prompt 是一段中文規則(只根據參考資料回答、逐句標來源編號、繁體中文作答),外加依使用者偏好注入的詳細程度條款。全部就是字串常數加字典查表:
1 | _VERBOSITY = { |
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 | graph = StateGraph(QAState) |
三個節點都是普通 Python 函式,吃 QAState(一個 TypedDict)、回傳要更新的欄位。這個骨架給了三樣自己寫迴圈拿不到的東西:
- 狀態的形狀是顯式的。
QAState把整條管線會流動的資料(原始問題、改寫後查詢、權限條件、檢索結果、路由結果)寫成一個型別,每個節點只宣告自己改了哪幾欄。事後回來加欄位(階段 C 加perm_filter、FR-60 加route/model)時,影響範圍一眼可見。 - 加節點不用動別人。 rewrite 節點是階段 B 才加的——單輪問答時代這張圖只有 retrieve → generate。插一個節點進來,改的是兩行
add_edge,既有節點一行不動。 - 可注入、可測試。
build_graph(client=None)讓測試與評測共用同一個 Qdrant 連線,整張圖 compile 出來就是一個可以invoke的物件,黃金問答集評測直接打它。
同樣公道地說:三節點的線性流程,用三個函式呼叫串起來也死不了。LangGraph 在這個規模是「舒服」而不是「必要」;它真正的護城河在流程長出分支與迴圈之後——條件路由(檢索品質差就改寫重查)、人工介入點、checkpoint 恢復。專案選它是為了那個方向的演進空間,而不是現在就非它不可。
五、一個誠實的註腳:你其實躲不開 langchain-core
最後補一個查環境時發現的事實。專案的 requirements.txt 明明沒有任何 langchain 開頭的套件,但 pip list 長這樣:
1 | langchain-core 0.3.86 |
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 差在哪、混合檢索深入拆解、檢索層權限實作與應用。
留言