Learning Objectives
- 讀懂 LangChain 呼叫模型、組鏈、RAG 與工具呼叫的現行寫法
- 把 LangChain 的每一段對應到你已經會寫的純 Python
- 從抽象層、版本變動與除錯說明使用框架的代價
- 依專案情況判斷該用框架或自己寫
你已經會的東西,框架怎麼包
前面幾課沒有用任何框架,RAG 和工具呼叫都是普通的 Python。實務上你會常看到 LangChain:一個幫你把模型、提示詞、檢索與工具接起來的套件。這一課不是要你改用它,而是讓你讀得懂別人寫的程式,並判斷自己要不要用。
這一課的程式有兩種,請分清楚:
- LangChain 的程式是一般的程式碼區塊,不能在這個瀏覽器裡執行。區塊下面的輸出,是我們 2026-11-03 在
langchain 1.4.8、langchain-openai 1.7.3實際執行、真的呼叫模型的結果。這個套件更新很快,版本變了可能要改寫,請以官方文件為準。 - 逐步執行的範例是同一件事的純 Python 寫法,回應和前幾課一樣是預錄的真實輸出。
執行環境的附註:我們的電腦封鎖了 xxhash、uuid_utils 兩個編譯好的套件,執行時換成純 Python 替代品。它們只用來產生內部編號,不影響送給模型的內容。
在自己的電腦上安裝:
pip install langchain langchain-openai
LangChain:呼叫模型與組鏈
最短的呼叫:
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-5.6-luna", reasoning_effort="none", max_completion_tokens=100)
response = model.invoke([
{"role": "system", "content": "你是大學通識課的助教,用一句話回答。"},
{"role": "user", "content": "什麼是二進位?"},
])
print(response.content)
print(response.usage_metadata)
實際輸出(langchain 1.4.8):
二進位是一種只使用「0」和「1」兩個數字來表示數值的計算方式,也是電腦儲存與處理資料的基礎。
{'input_tokens': 32, 'output_tokens': 45, 'total_tokens': 77, 'input_token_details': {'audio': 0, 'cache_read': 0, 'cache_creation': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}
輸入 32 token,和第 7 課用 OpenAI SDK 送同樣內容時相同:底下送出的是同樣的請求。
LangChain 的招牌寫法是用 | 把幾個步驟接成一條鏈(chain):
from langchain.chat_models import init_chat_model
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", "你是大學通識課的助教,用一句話回答。"),
("user", "什麼是{topic}?"),
])
model = init_chat_model("openai:gpt-5.6-luna", reasoning_effort="none", max_completion_tokens=100)
chain = prompt | model | StrOutputParser()
print(chain.invoke({"topic": "二進位"}))
實際輸出(langchain 1.4.8):
二進位是一種只使用「0」和「1」兩個數字來表示數值的計數系統,也是電腦儲存與處理資料的基礎。
逐步看:鏈就是函數一個接一個
同一條鏈的純 Python 寫法。請對照上面三段:prompt、model、StrOutputParser(),各自對應哪個函數。
LangChain:RAG
第 13 課的 RAG。向量庫用 LangChain 內建、存在記憶體的 InMemoryVectorStore,它幫你呼叫 embedding API、存向量、算相似度:
import json
from langchain.chat_models import init_chat_model
from langchain_core.documents import Document
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings
with open("handbook.json", encoding="utf-8") as f:
sections = json.load(f)
docs = [Document(page_content=s["title"] + ":" + s["text"], metadata={"id": s["id"]}) for s in sections]
store = InMemoryVectorStore(OpenAIEmbeddings(model="text-embedding-3-small", dimensions=128))
store.add_documents(docs)
question = "停修的課會算進平均成績嗎?"
hits = store.similarity_search_with_score(question, k=3)
for doc, score in hits:
print(doc.metadata["id"], round(score, 3))
data = "\n".join(f"[{n}] {doc.page_content}" for n, (doc, _) in enumerate(hits, start=1))
model = init_chat_model("openai:gpt-5.6-luna", reasoning_effort="none", max_completion_tokens=200)
response = model.invoke([
{"role": "system", "content": "只根據資料回答,用一句話,句尾用 [編號] 標示來源。資料裡沒有答案就回答「手冊沒有寫」。"},
{"role": "user", "content": "資料:\n" + data + "\n\n問題:" + question},
])
print(response.content)
print(response.usage_metadata)
實際輸出(langchain 1.4.8,usage 只留前三項):
course-2 0.695
leave-3 0.548
grade-1 0.495
不會,停修課程不列入平均成績計算。[1]
{'input_tokens': 269, 'output_tokens': 21, 'total_tokens': 290, ...}
注意提示詞還是我們自己組的。框架省掉的是向量庫這部分;資料與指令怎麼分開、要不要引用、查無資料怎麼說,一樣要你決定。
逐步看:同一件事的純 Python
向量搜尋放在 rag_utils.py,和第 13 課相同。請比較輸出的分數、回答與 token 數。
LangChain:工具呼叫與 Agent
第 15、16 課自己寫的工具迴圈,LangChain 1.x 用 create_agent 提供。函數加上 @tool,它會從參數型別與 docstring 產生工具的描述:
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import tool
@tool
def get_course_room(course: str) -> str:
"""查某門課的上課教室。"""
rooms = {"微積分": "E1-101", "資料結構": "E2-305"}
return rooms.get(course, "查無此課")
model = init_chat_model("openai:gpt-5.6-luna", reasoning_effort="none", max_completion_tokens=200)
agent = create_agent(model=model, tools=[get_course_room], system_prompt="你是選課助手,用一句話回答。")
result = agent.invoke({"messages": [{"role": "user", "content": "資料結構在哪間教室上課?"}]})
for m in result["messages"]:
print(type(m).__name__, repr(m.content))
實際輸出(langchain 1.4.8),每則訊息的工具呼叫與 token 數是我們另外加印後整理的:
HumanMessage '資料結構在哪間教室上課?'
AIMessage '' (呼叫 get_course_room,參數 course=資料結構;輸入 152、輸出 20)
ToolMessage 'E2-305'
AIMessage '資料結構在 E2-305 教室上課。' (輸入 188、輸出 16)
這裡的模型一定要明確帶 reasoning_effort="none":這個模型在 chat.completions 使用工具時,不帶這個參數會回傳 400。框架不會替你處理各家模型的這類規定。
逐步看:Agent 迴圈裡發生的事
create_agent 替你跑的就是這個迴圈。請看 messages 怎麼變長,以及和上面四則訊息的對應。
框架替你做了什麼、代價是什麼
| 工作 | 自己寫 | LangChain |
|---|---|---|
| 換模型供應商 | 各家 SDK 寫法不同,要自己包一層 | 改模型字串,統一的介面 |
| 向量庫 | 小資料 numpy 就夠;大資料要接資料庫 | 內建多種向量庫的介面 |
| 工具描述 | 自己寫 JSON Schema | 從函數產生 |
| Agent 迴圈 | 自己寫,約二十行 | create_agent,另有中介層可以加限制與紀錄 |
代價一:多了抽象層。 我們用一個不存在的模型名稱故意觸發同一個錯誤,數從自己的程式到丟出例外的呼叫層數(實測,langchain 1.4.8):
| 寫法 | 例外類別 | 呼叫層數 |
|---|---|---|
| OpenAI SDK | NotFoundError |
6 |
LangChain 模型 invoke |
OpenAIModelNotFoundError |
8 |
| LangChain 鏈 | OpenAIModelNotFoundError |
9 |
create_agent |
OpenAIModelNotFoundError |
17 |
層數本身不是大問題,這個例子的錯誤訊息也很清楚。比較麻煩的是想知道「模型實際收到什麼」時,要穿過這些層才看得到;自己寫時 print(messages) 就好。另外,安裝 langchain 和 langchain-openai 後,我們的環境裡有 46 個套件(含 pip 與另外裝的 numpy),大多是要更新與檢查的相依項目。
代價二:版本變動。 網路上和舊教材常見的寫法,現在不一定能用。我們在 langchain 1.4.8 實際執行:
from langchain.chains import LLMChain → ModuleNotFoundError: No module named 'langchain.chains'
from langchain.chains import ConversationChain → ModuleNotFoundError: No module named 'langchain.chains'
from langchain.memory import ConversationBufferMemory → ModuleNotFoundError: No module named 'langchain.memory'
依官方的遷移指南,舊的鏈移到另一個套件 langchain-classic。官方的版本政策說 1.0 之後的破壞性變更只會發生在主要版本,算是比較穩定了;但你查到的教學文章可能還是舊版,先看文章用的版本。用 AI 工具產生 LangChain 程式碼時也要注意這點。
什麼時候值得用: 要同時支援多家模型、要接多種向量庫、團隊已經在用它、要用它生態系裡現成的整合。
什麼時候自己寫比較好: 流程單純(幾次呼叫加一個迴圈)、要精確控制提示詞與每一步的紀錄、相依套件要少、正在學原理。兩者也可以混用,例如只用它的向量庫介面。
不管選哪一個,提示詞怎麼寫、檢索準不準、回答怎麼評估,框架都不會替你決定。
實作:自己寫一個 chain
把「鏈」的概念寫成函數組合。完成 make_chain(*steps):回傳一個新函數,這個函數收到輸入後,依序把每個步驟的輸出交給下一個步驟,回傳最後的結果。
這一題不需要模型,fake_model 用寫死的字串代替回應,這也是測試時常用的做法。
小測驗:框架的取捨
prompt | model | StrOutputParser() 實際上做了什麼?
實測中,LangChain 版與純 Python 版的 RAG 輸入 token 都是 269。這代表什麼?
你照著一篇網路文章寫 from langchain.chains import LLMChain,在 langchain 1.4.8 出現 ModuleNotFoundError。最合理的做法是?
下面哪個情況,最適合不用框架、自己寫?
改用框架之後,下面哪件事仍然要你自己決定?
重點整理
- LangChain 的鏈、向量庫、
create_agent,對應到你寫過的函數組合、numpy 檢索與工具迴圈。 - 送給模型的內容相同時,回答的品質與費用不會因為框架而改變:實測輸入 token 完全相同。
- 代價是抽象層(實測同一個錯誤從 6 層變成 17 層)、相依套件,以及版本變動讓舊文章不能照抄。
- 流程單純、要精確掌控時自己寫;要接很多現成整合時用框架。
下一課: 不管怎麼寫,改了之後怎麼知道有沒有變好?下一課建立測試集,用數字比較兩個版本。