為了解放每日的工作任務,我們可以使用 LangChain 開發一個在本地端執行的小程式。使用提示詞模版、解析器等各種工具。今天示範旅遊景點與天氣生成器。
我寫電子報的宗旨,是服務給非資訊本科的一般人,提供各位最簡單的方式,使用 AI 讓日常生活雜務或工作能夠自動化。
所以上週我在開始介紹 LangChain 這個 Python 套件時,就在猶豫,我要怎麼敘述,才不會讓大家覺得門檻太高。
於是我決定只談 LangChain 有什麼工具可以使用,以及可能的應用領域有哪些。過程中僅用少量的程式說明。
畢竟我鼓勵大家直接使用如 Windsurf 這類的 AI 工具來開發系統,只需要懂得怎麼運作即可,不需要硬背。如果你需要閱讀程式碼,在電子報最後我會提供我的學習筆記。
如果你還不知道什麼是 LangChain,也不知道如何連到 AI 公司如 OpenAI 、 Anthropic 提供的 API 應用程式開發介面,請先閱讀 EP-7 AI 技術應用:新手小白要開發 AI 工具,需要學什麼?什麼又是 LangChain ?
在今天的電子報,介紹 LangChain 開發必備的工具:提示詞模版、解析器、Chain 。
什麼是提示詞模版
使用 LangChain 套件開發,一定會用到提示詞模版(PromptTemplate)。
當我們直接使用 ChatGPT 網頁時,提示詞是我們自己輸入的。例如每天你必須挑選某個城市,請 ChatGPT 生成旅遊資訊介紹,以及目前天氣概況。你就必須不斷地寫提示詞。
如果你需要每天做類似的工作,這不是一件很惱人的事嗎?我強烈建議你寫一個小程式,讓程式每天幫你做這件事。當然你也可以用Make、n8n這種自動化工具,可能要額外花錢就是了。
為了串接工作流程,你可以使用提示詞模版,它支持使用參數來動態插入內容,格式如下:
from langchain_core.prompts import PromptTemplate
weather_template = """
### 任務說明 ###
請描述以下城市在 {month} 月的典型氣候特徵,包含溫度、降雨機率等資訊。
請直接描述,不要加入任何額外的開場白或結束語。
### 城市名稱 ###
{city}
"""
weather_prompt = PromptTemplate(
input_variables=["city", "month"],
template=weather_template
)
提示詞模版的目的就是先寫好提示詞,放入可能的變數,在這個例子就是城市及月份。
假設你是想每天發送不同城市的旅遊資訊,就可以用隨機的方式抓取城市名字,讓程式抓取系統的當月月份,就能送出提示詞給語言模型。
建議大家使用時,請 AI 把提示詞模版另外放,不要一起放在同一個程式檔,不然程式行數會過多,後續維護會比較麻煩。
什麼是解析器?
在上述的例子中,我們沒有要特別讓語言模型以特定格式輸出旅遊資訊,因此用 StrOutputParser()
這個預設的解析器即可。
讓我們繼續前面的程式:
import os
from dotenv import load_dotenv
# 載入環境變數及 API Key
load_dotenv()
openai_api_key = os.getenv("OPENAI_API_KEY")
# 定義語言模型
llm = ChatOpenAI(api_key=openai_api_key, temperature=0.7)
# 定義文字解析器,StrOutputParser()會把語言模型回傳的文字完整不動的送回。
parser = StrOutputParser()
# 建立處理鏈
chain = weather_prompt | llm | parser
有時候為了配合前端網頁介面,我們會希望讓語言模型用指定的格式送回,常見的作法是要求語言模型以 JSON 格式輸出,如以下範例:
weather_template = """
### 任務說明 ###
請描述以下城市在 {month} 月的典型氣候特徵,包含溫度、降雨機率。
請使用以下 JSON 格式輸出:
{{
"city": "<城市名稱>",
"month": "<月份>",
"temperature": "<平均溫度描述>",
"rain_probability": "<降雨機率描述>"
}}
### 城市名稱 ###
{city}
僅輸出 JSON,不要加入任何額外文字。
"""
既然是以 JSON 格式輸出,那麼使用 StrOutputParser
就不是很合適,可以改用 JsonOutputParser
。如果語言模型沒有輸出 JSON 格式,則 JsonOutputParser
會報錯。
為什麼叫做 LangChain?
LangChain 之中的 Lang
,當然代表的是 Language Models 意思, Chain
又是什麼意思?
Chain
是指將多個步驟「鏈接」在一起,形成一個可執行的流程。包括:
預處理輸入,例如提示詞模版
調用語言模型
後處理語言模型的輸出,例如讓語言模型輸出 JSON 格式,再後續處理資料內容
整合多個模型或邏輯單元,形成一個複雜的管道。
一個常見的 Chain 表達形式,就像是之前的示範:
chain = weather_prompt | llm | parser
在這裡使用 |
表示,是 LangChain 中 RunnableSequence
方法的一種簡化語法。意思是從 weather_prompt 這個提示詞模版,送到 llm 語言模型,語言模型生成資料後回傳,交給 parser 解析輸出。
一個複雜的系統可能會有數十個這樣的 Chain ,一串抓起來像是綁肉粽一樣。
最後,我們會使用 invoke()
方法,執行這個工作鏈。如以下程式說明。
from datetime import datetime
# 取得當前月份
current_month = datetime.now().month
# 測試範例輸入
input_data = {"city": "台北", "month": current_month }
result = chain.invoke(input_data)
# 印出語言模型輸出的資料
print(result)
讓程式依據需求執行不同工作鏈
如果我們要同時存在兩條以上的工作鏈,讓使用者自行選擇不同的鏈。例如我們希望有一個介面,如果使用者詢問某個城市的在特定月份的氣候,就回答氣候相關答案,如果詢問當地景點,就回答旅遊相關資訊。
此時我們就會需要用到 RunnableBranch
方法,除了我們之前的天氣描述提示詞模版以外,我們可以增加 2 個不同的提示詞模版,一個用來判斷使用者的問題類型,一個是旅遊資訊模版。
# 定義問題分類的提示詞模板
classification_template = """
### 任務說明 ###
根據使用者的問題,判斷問題的類型。
可用類型有:
- weather: 與天氣相關,例如天氣如何、會不會下雨。
- travel: 與旅遊相關,例如景點、美食或最佳旅遊季節。
- both: 同時包含天氣和旅遊的問題。
- none: 無法判斷。
請僅輸出以下格式:
{{ "type": "<類型>" }}
### 使用者問題 ###
{question}
"""
classification_prompt = PromptTemplate(
input_variables=["question"],
template=classification_template
)
# 定義旅遊提示詞模板
travel_template = """
### 任務說明 ###
請為以下城市提供一份簡要的旅遊指南,包含必遊景點、特色美食和最佳旅遊季節。
請直接描述,不要加入任何額外的開場白或結束語。
### 城市名稱 ###
{city}
"""
travel_prompt = PromptTemplate(
input_variables=["city"],
template=travel_template
)
接著你可以請 AI 幫你寫一個程式,類似像是下面這樣。如果問題跟天氣有關,執行天氣處理鏈,跟旅遊有關,就執行旅遊處理鏈,如果兩個都是,就分別執行兩條鏈。
當然程式沒這麼簡單,還會需要一些判斷式,如底下的 process_question
。你看不懂程式也沒關係,反正你只要記得可以這麼做就好。細節讓 AI 去幫你寫。
# 建立主要處理分支
main_branch = RunnableBranch(
(lambda x: x["type"] == "weather", weather_chain),
(lambda x: x["type"] == "travel", travel_chain),
(lambda x: x["type"] == "both", RunnablePassthrough() | both_chain | format_response),
lambda _: "抱歉,我無法理解您的問題。請試著詢問天氣或旅遊相關的問題。"
)
def process_question(question: str) -> None:
print("\\n問題:", question)
print("-" * 50)
try:
# 判斷問題類型
classification_result = classification_chain.invoke({"question": question})
question_type = classification_result.get("type", "none")
if "error" in classification_result:
print("無法理解問題類型")
print(classification_result["error"])
return
print(f"問題類型:{question_type}")
# 提取資訊
info = extraction_chain.invoke({"question": question})
print(f"已提取資訊 - 城市:{info['city']},月份:{info['month']}")
# 使用 RunnableBranch 處理問題
result = main_branch.invoke({
"type": question_type,
"city": info["city"],
"month": info["month"]
})
print(result)
except Exception as e:
print(f"處理錯誤:{str(e)}")
print("-" * 50)
最後在這支程式,我還使用了 RunnableLambda
以及 RunnableWithFallbacks
。
# 提取資訊的函式
def extract_info(text: str) -> Dict[str, Any]:
try:
info = eval(text)
return {
"city": info["city"],
"month": info["month"]
}
except Exception as e:
print(f"資訊提取錯誤:{str(e)}")
return {"city": "未知", "month": datetime.now().month}
base_extraction_chain = extraction_prompt | llm | parser | RunnableLambda(extract_info)
extraction_chain = RunnableWithFallbacks(
runnable=base_extraction_chain,
fallbacks=[RunnableLambda(lambda _: {"city": "未知", "month": datetime.now().month})]
)
RunnableLambda
可以把某個自定義函式,轉成可以在 LangChain 工作流程中使用的組件。在這裡就把 extract_info
函數包裹起來,就可以放在工作鏈使用。
RunnableWithFallbacks
用來添加錯誤處理機制,如果一切正常使用 base_extraction_chain
這條工作鏈, fallbacks=[...]
是指當主要鏈失敗時的備用處理方案。
上述詳細且完整的程式與說明,可以讀我的筆記 LangChain奇幻旅程筆記(5):電子報 EP-8 示範案例 。
今天示範的程式都沒有記憶能力,你一開始問台中在 2 月的天氣如何,有哪些地方好玩?語言模型會正確回應你,例如它可能舉出彩虹眷村、台中國家歌劇院。
你如果接著問「還有其他的嗎」,它就無法理解你的問題了,因為這支程式沒有設定上下文記憶,根本不知道你上一個問題是什麼。
所以下一個章節,將會介紹如何引入 LangChain 的記憶模組。