National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 72 24°C PM2.5 13
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 19

對照 LangChain

自己寫過的流程,用框架怎麼寫、差在哪裡
Interaction實作 20 minute(s)

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 用寫死的字串代替回應,這也是測試時常用的做法。

你的輸出
預期輸出

小測驗:框架的取捨

1.

prompt | model | StrOutputParser() 實際上做了什麼?

2.

實測中,LangChain 版與純 Python 版的 RAG 輸入 token 都是 269。這代表什麼?

3.

你照著一篇網路文章寫 from langchain.chains import LLMChain,在 langchain 1.4.8 出現 ModuleNotFoundError。最合理的做法是?

4.

下面哪個情況,最適合不用框架、自己寫?

5.

改用框架之後,下面哪件事仍然要你自己決定?

第 1 次作答

重點整理

  • LangChain 的鏈、向量庫、create_agent,對應到你寫過的函數組合、numpy 檢索與工具迴圈。
  • 送給模型的內容相同時,回答的品質與費用不會因為框架而改變:實測輸入 token 完全相同。
  • 代價是抽象層(實測同一個錯誤從 6 層變成 17 層)、相依套件,以及版本變動讓舊文章不能照抄。
  • 流程單純、要精確掌控時自己寫;要接很多現成整合時用框架。

下一課: 不管怎麼寫,改了之後怎麼知道有沒有變好?下一課建立測試集,用數字比較兩個版本。

這一課完成了!
Next lesson:用測試集評估應用
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。