昨天我們談到 Prompt 系統化:包括模板化、版本管理、測試與整合。這些設計,讓我們能像管理程式碼一樣管理 Prompt。
但在真實應用中,單一 Prompt 常常不夠,我們需要更靈活的方式,來設計、組合與控制 Prompt:
這就是今天(Day16)的主題:Prompt Template 與 Prompt Chain。
我們會進一步介紹兩個常見的工具:LangChain 與 Guidance。
⚠️ 提醒:本篇介紹的工具偏進階,如果你剛接觸 Prompt,建議先熟悉 Day15 的基礎(模板化、版本管理、測試),再回來閱讀這一篇。
⚠️ 版本相容性提醒
本文撰寫與測試版本如下,若遇到 API mismatch,請先檢查套件版本是否不同:
> LangChain 0.2.11
> LangChain-Core 0.2.21
> LangChain-OpenAI 0.1.7
> Guidance 0.1.14
今天的 Demo Code 僅會擷取部分片段,完整程式碼放在 GitHub Repo。
LangChain 是一個在 2022 年後迅速爆紅的 LLM 應用開發框架,它的定位是「讓開發者更容易把大語言模型整合進應用程式」。 它的設計哲學是:把 LLM 當成一個元件,並透過 Chain 與 Agent 來編排多步驟流程。
LangChain:Prompt + Chain + Agent(Optional) 流程圖
優勢:
缺點:
首先,把 Prompt 抽象化,用 {}
插入動態變數,可以統一管理不同場景下的 Prompt(FAQ、摘要、推理)。再來,將 Day 15 的 prompt template
- prompts_v1.yaml
載入後,轉成 LangChain 需要的 {}
風格,把「文件內容」與「使用者問題」參數化,再建立 Prompt:
# 讀 YAML(Day15 的 Prompt Registry)
YAML_PATH = "prompts/prompts_v1.yaml"
if not os.path.exists(YAML_PATH):
raise FileNotFoundError(f"找不到 {YAML_PATH},請確認路徑與工作目錄。")
with open(YAML_PATH, "r", encoding="utf-8") as f:
yml = yaml.safe_load(f) or {}
# 取出模板字串,並轉成 LangChain 可用格式
try:
raw_summary_template = yml["prompts"]["summary"]["template"]
raw_faq_template = yml["prompts"]["faq"]["template"]
except KeyError as e:
raise KeyError(
f"YAML 結構不符,缺少 {e}. 期待結構為 prompts.summary.template / prompts.faq.template"
)
summary_template = normalize_template(raw_summary_template) # 需要 {context}
faq_template = normalize_template(raw_faq_template) # 需要 {context}、{question}
# Step 1️⃣: 摘要 Prompt(吃 {context})
summary_prompt = PromptTemplate(
input_variables=["context"],
template=summary_template
)
# dict(context=...) → Prompt → LLM → str
summary_chain = summary_prompt | llm | StrOutputParser()
# Step 2️⃣: FAQ Prompt(吃 {context} 與 {question})
faq_prompt = PromptTemplate(
input_variables=["context", "question"],
template=faq_template
)
# --- 用法 A:直接 QA(context + question → faq) ---
# [IN] {"text": <原始內容>, "question": <問題>}
# [OUT] str(答案)
qa_chain = (
{
# 將原始輸入映射到 Prompt 需要的鍵
"context": RunnableLambda(lambda x: x["text"]),
"question": RunnableLambda(lambda x: x["question"]),
}
| faq_prompt # dict → Prompt 字串
| llm # Prompt → 模型輸出
| StrOutputParser() # → 純文字
)
執行結果:
❯ python langchain_chain_demo.py
=== 用法 A:直接 QA(context + question → faq) ===
- 步驟 1:安裝軟體
- 步驟 2:設定帳號
- 步驟 3:連線
...
有時候我們的需求不只是一個 Prompt,而是需要「多步驟推理」。官方定義的常見流程模式包括 Sequential(依序執行)、Router(依條件分流)、Map-Reduce(並行處理後彙總)。
類型 | 流程 | 適用情境 | 真實案例 |
---|---|---|---|
Sequential Chain | Prompt1 → Prompt2 → … | 需要逐步推理,前一步的結果是後一步的輸入 | 文件先摘要,再交給 LLM 生成 FAQ(例如內部知識庫建置) |
Router Chain | 根據條件分派到不同 Prompt | 問題類型不同需走不同流程 | 公司內部 Chatbot:若問題包含「價格」→ 進 Pricing Prompt;若是技術問題 → 進 FAQ Prompt |
Map-Reduce Chain | 多個 Prompt 分塊處理 → 彙整 | 大文件、多人分工 | 法律文件分析:先把 200 頁契約切成 20 份 → 每份做摘要(Map)→ 再彙整成一份重點摘要(Reduce) |
💡 完整的三種模式程式碼實作也放在 GitHub Repo -
langchain_chain_router_demo.py
, 有興趣的讀者可以自行研究看看。
最常見的模式就是「先做 A,再做 B」。
在我們的 Demo Code (langchain_chain_demo.py
) 中,用法 B 就是典型的 Sequential Chain:
...
...
# --- 用法 B:先摘要再 FAQ(summary → faq) ---
# Step 0️⃣: 定義一個「自動問題產生器」
# 在這裡我們固定輸出一段文字:「請依據以上摘要產生三個常見問題並回答,條列式。」
# 實際應用中,你也可以改成根據情境動態產生不同問題。
# [IN] {"text": <原始內容>}
# [OUT] str(條列 FAQ)
auto_question = RunnableLambda(
lambda _: "請依據以上摘要產生三個常見問題並回答,條列式。"
)
summary_to_faq_chain = (
{
# Step 1: 從輸入資料結構中取出 text,包裝成 {context}
# 並先丟給 summary_chain,產生「文件摘要」
"context": RunnableLambda(lambda x: {"context": x["text"]}) | summary_chain,
"question": auto_question, # Step 2: 生成問題(固定輸出三個常見問題的要求)
}
| faq_prompt # Step 3: 把 {context, question} 套用到 faq_prompt,組裝完整的 Prompt
| llm # Step 4: 把組裝好的 Prompt 丟給 LLM,產生回答
| StrOutputParser() # Step 5: 解析 LLM 輸出,只留下純文字(去掉多餘 metadata)
)
if __name__ == "__main__":
doc = "VPN 設定文件:步驟 1 安裝軟體,步驟 2 設定帳號,步驟 3 連線。"
print("=== 用法 A:直接 QA(context + question → faq) ===")
out_qa = qa_chain.invoke({"text": doc, "question": "如何設定公司 VPN?"})
print(out_qa)
print("\n=== 用法 B:先摘要再 FAQ(summary → faq) ===")
out_summary_faq = summary_to_faq_chain.invoke({"text": doc})
print(out_summary_faq)
執行結果:
...
=== 用法 B:先摘要再 FAQ(summary → faq) ===
1. **如何安裝 VPN 軟體?**
- 請參考文件中的第一步驟,安裝 VPN 軟體。
2. **我該如何設定帳號資訊?**
- 請依據文件中的第二步驟,設定帳號資訊。
3. **如何進行連線操作?**
- 請根據文件中的第三步驟,進行連線操作。
這種方式適合 需要逐步推理 的場景,例如「先做摘要,再問答」或「先翻譯,再潤稿」。很多初學者看到這段程式碼時,會覺得像「黑箱」:到底哪一段是處理輸入的?哪一段是組裝 Prompt?哪一段才是整條 Chain?
👉 我們可以把 langchain_chain_demo.py
拆解成三個角色來理解:
元件 | 功能定位 | 程式碼裡的例子 |
---|---|---|
RunnableLambda | 小型轉接器:負責從輸入中取值或轉換資料,傳給下一步 | lambda x: x["text"] 、lambda x: x["question"] |
PromptTemplate | 模板生成器:把變數代入 Prompt 模板,產生完整輸入給 LLM | summary_prompt 、faq_prompt |
Chain | 流水線:把多個元件串起來,形成一個可執行的流程 | qa_chain 、summary_to_faq_chain |
有了對照表,就會發現整條 Chain 是由小元件逐步拼起來的流水線。
除了 Template 與 Chain,LangChain 還有 Agent 概念,讓 LLM 可以根據指令「自己決定」要不要查資料庫、呼叫 API。
這個功能比較進階,我們會在之後的章節再深入介紹。
Guidance 是由 Microsoft Research 團隊開源的 Prompt 程式設計框架。與強調「組合流程」的 LangChain 不同,Guidance 的核心理念是 在生成過程中就施加約束,確保輸出結果 一定符合指定格式(如 JSON、SQL、Markdown 表格),而不是事後再去解析與清洗。
在真實應用中,LLM 輸出「多了一句廢話」就可能讓整個流程掛掉:
好的,以下是 JSON: {"answer": "VPN 設定步驟…"}
Guidance 的解法是 強制 LLM 在生成時就遵守 Schema,不會多一個字,輸出一定可解析。
功能面向 | 說明 |
---|---|
Prompt 內建邏輯 | 支援 if 、for 、變數等控制結構,讓 Prompt 更像「程式碼」 |
格式輸出保證 | 可指定輸出必須是 JSON、Markdown 表格,甚至 SQL 語句;適合需要下游 API 消化的情境 |
多模型支援 | 不侷限 OpenAI,也能在 HuggingFace 模型上使用 |
優點 | 缺點 | |
---|---|---|
Guidance | 適合 需要結構化輸出(例如 API 回傳 JSON、生成 SQL)。 | 生態較小、文件不如 LangChain 完善 |
Prompt + 邏輯更緊密,開發者能掌控每一個 token 的生成過程。 | 偏向工程師導向,入門曲線比 LangChain 陡。 |
# ---------- Guidance 程式(要求只輸出 JSON) ----------
@guidance
def faq_json_program(lm, draft):
lm += f"""
你會收到一段 FAQ 草稿,請轉成 JSON 陣列,每個元素必須包含 "q" 與 "a" 欄位。
嚴格要求:只輸出合法 JSON,不要前後多任何文字、說明或標記。
FAQ 草稿:
{draft}
輸出:
{{{{gen 'json' temperature=0 max_tokens=800}}}}
"""
return lm
執行結果:
❯ python guidance_faq_json_demo.py
=== FAQ JSON ===
[{'q': '公司 VPN 怎麼設定?', 'a': '請先安裝軟體,接著輸入帳號密碼,最後按下連線。'}]
Guidance 也允許在 Prompt 中內嵌程式邏輯,確保輸出更符合需求。
舉例來說,用 regex
約束生成格式(例如「一定是數字」):
import os
from dotenv import load_dotenv
import guidance
from guidance import models, gen
load_dotenv()
llm = models.OpenAI(model="gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY"))
@guidance
def ask_age(lm):
lm += "You are a teenager.\nHow old are you?\nAge: "
lm += gen("age", regex=r"\d{1,2}") # 只允許 1~2 位數字
return lm
out = ask_age(model=llm)
print("age =", out["age"]) # 例如 "15"
很多團隊會把這兩者搭配起來用:
程式碼請參照 GitHub Repo 的 combined_demo.py
。
❯ python combined_demo.py
=== LangChain → FAQ 草稿(文字) ===
1. **如何安裝VPN軟體?**
- 請參考文件中的第一步驟:安裝VPN軟體。
2. **如何設定使用者帳號?**
- 請參考文件中的第二步驟:設定使用者帳號。
3. **如何進行連線?**
- 請參考文件中的第三步驟:進行連線。
=== Guidance → FAQ JSON ===
[{'q': '如何安裝VPN軟體?', 'a': '請參考文件中的第一步驟:安裝VPN軟體。'}, {'q': '如何設定使用者帳號?', 'a': '請參考文件中的第二步驟:設定使用者帳號。'}, {'q': '如何進行連線?', 'a': '請參考文件中的第三步驟:進行連線。'}]
特性 | LangChain | Guidance | LangChain + Guidance |
---|---|---|---|
定位 | LLM 應用框架,偏向工作流編排 | Prompt + 程式邏輯整合,偏向嚴格輸出 | 先編排流程,再收斂格式,兩者互補 |
核心功能 | PromptTemplate、Chain、Agent | 嵌入 if/for/變數,格式控制(JSON、Markdown 等) | 流程用 LangChain 串接,輸出用 Guidance 保證結構 |
優勢 | 社群大、模組多、容易做快速原型 | 控制力強,能確保輸出格式正確 | 快速開發 + 穩定上線,降低出錯率 |
缺點 | 功能龐雜,生產環境常需裁切 | 生態小,入門門檻較高 | 複雜度提高,需要同時維護兩層 |
適合場景 | 多步驟推理、原型開發、複雜工作流 | API 輸出需要結構化(SQL、JSON、報表) | 內部 FAQ Bot → 前端需要 JSON:LangChain 串流程,Guidance 收斂成 JSON |
失敗 fallback 策略 | — | — | 若 Guidance 輸出失敗 → 重試;仍失敗 → 回退正則清洗,避免下游爆炸 |
比喻 | 「管弦樂指揮」:協調多步驟任務 | 「演奏家」:保證輸出符合規格 | 「合作演出」:指揮協調全場演奏家,演奏者把每一個音節演奏完美 |
今天學到的 Prompt Workflow
工具,讓我們能建立穩健的 Prompt
輸出系統。 明天我們會聊聊不同規模的 LLM 應用專案應該選擇哪種部署策略。
🤔 延伸問題:如果你只有一天時間學,怎麼選?
你的目標 | 建議工具 | 原因 |
---|---|---|
⚡ 想快速做 Demo / Hackathon | LangChain | 提供現成的 PromptTemplate / Chain,能快速把多個 Prompt 串成可用流程。 |
🛡️ 想要上線 API,需要穩定輸出 | Guidance | 能嚴格控制輸出格式(JSON、Markdown、SQL),避免下游系統爆炸。 |
LangChain 官方文件
Guidance 相關資源
其他