LangChain 教學:Output Parsers(輸出解析)— 自訂輸出解析器
簡介
在使用大型語言模型(LLM)時,模型的原始回傳往往是自由文字(free‑form text),這對於後續程式邏輯的處理會造成障礙。Output Parser(輸出解析器)是 LangChain 提供的機制,能把模型的文字回應轉換成結構化資料(如 JSON、Pydantic 物件、列表…),讓程式碼可以直接使用。
自訂輸出解析器的價值在於:
- 提升可靠性:避免因文字微小差異(例如多餘的換行、標點)導致的解析錯誤。
- 降低耦合度:將「文字格式」的規範與業務邏輯分離,讓 Prompt 與程式碼可以各自演進。
- 支援複雜回應:當需求需要回傳多個欄位、嵌套結構或自訂型別時,內建的
StrOutputParser已不足以應付,必須自行實作。
本篇文章將從概念說明出發,帶領讀者一步步建立、測試與最佳化自訂的 Output Parser,並提供多個實用範例,適合剛接觸 LangChain 的初學者以及想要深化應用的中階開發者。
核心概念
1. Output Parser 的角色
在 LangChain 的執行流程中,Chain(或 Runnable) 的最後一步通常是 解析 LLM 的文字回覆。
flowchart LR
Prompt --> LLM --> RawText --> OutputParser --> StructuredData
OutputParser 必須實作 parse(text: str) -> Any 方法,將 RawText 轉成程式可直接使用的物件。
LangChain 已內建以下幾種常見 parser:
| Parser | 產出 | 典型用途 |
|---|---|---|
StrOutputParser |
str |
直接回傳文字 |
JsonOutputParser |
dict |
解析 JSON |
PydanticOutputParser |
BaseModel 子類別 |
產生驗證過的資料模型 |
StructuredOutputParser |
dict (根據 schema) |
自訂欄位與類型 |
然而,實務上常會遇到 「模型常常多說一句」、「欄位順序不固定」、或 「需要回傳混合型別」 等情況,這時就需要 自訂 Parser。
2. 建立自訂 Parser 的基本步驟
- 繼承
BaseOutputParser(或BaseLLMOutputParser) - 實作
parse方法:將文字轉成目標型別,必要時拋出OutputParserException。 - (可選)實作
get_format_instructions:提供給 Prompt 的格式說明,讓 LLM 知道該如何回傳。 - 在 Chain 中掛上 parser:使用
|(pipe)或RunnableLambda連接。
小技巧:若只要簡單的後處理(例如去除前後空白、拆分列表),可以直接使用
RunnableLambda,不必寫完整的類別。
3. 為什麼要提供 format_instructions?
LLM 並不會自動遵守我們的期望格式。format_instructions 會被插入 Prompt,告訴模型:
- 必須回傳 JSON、YAML、或特定的欄位名稱
- 不可出現額外的文字說明(如「以下是結果」)
提供清晰的指示能大幅提升解析成功率,減少後續錯誤處理的成本。
程式碼範例
以下範例均以 Python(LangChain 官方語言)撰寫,使用 langchain 套件。若您偏好 JavaScript,可將概念對應到 langchainjs,語法類似。
範例 1:最簡單的自訂 Parser(去除前後空白)
from langchain.schema import BaseOutputParser
class TrimOutputParser(BaseOutputParser):
"""只負責去除回傳文字的前後空白與換行。"""
def parse(self, text: str) -> str:
return text.strip()
# 使用方式
trim_parser = TrimOutputParser()
raw = "\n Hello LangChain! \n"
print(trim_parser.parse(raw)) # => "Hello LangChain!"
重點:即使是最簡單的需求,也建議寫成 Parser,讓 Chain 的結構保持一致。
範例 2:JSON 解析器 + 錯誤容忍
LLM 有時會漏掉最後的 },這裡示範如何在解析失敗時自動補齊。
import json
from langchain.schema import BaseOutputParser, OutputParserException
class RobustJsonParser(BaseOutputParser):
"""嘗試解析 JSON,若失敗則嘗試自動補齊大括號。"""
def parse(self, text: str) -> dict:
try:
return json.loads(text)
except json.JSONDecodeError as exc:
# 嘗試補全大括號
fixed = text.strip()
if not fixed.endswith('}'):
fixed += '}'
try:
return json.loads(fixed)
except json.JSONDecodeError:
raise OutputParserException(f"JSON 解析失敗: {exc}")
# 示例
parser = RobustJsonParser()
raw = '{"name": "Alice", "age": 30' # 缺少結尾 }
print(parser.parse(raw)) # => {'name': 'Alice', 'age': 30}
技巧:在 parse 中捕捉例外並自行修正,能讓 Chain 更具彈性。
範例 3:使用 Pydantic 建立驗證模型
from pydantic import BaseModel, Field, ValidationError
from langchain.output_parsers import PydanticOutputParser
class ProductInfo(BaseModel):
"""產品資訊的結構化模型"""
name: str = Field(..., description="商品名稱")
price: float = Field(..., gt=0, description="商品價格(美元)")
in_stock: bool = Field(..., description="是否有庫存")
# 建立 parser
pydantic_parser = PydanticOutputParser(pydantic_object=ProductInfo)
# Prompt 中的說明(自動產生)
print(pydantic_parser.get_format_instructions())
產出(示範):
The output should be a JSON object that conforms to the following JSON schema:
{
"properties": {
"name": {"title": "Name", "type": "string"},
"price": {"title": "Price", "type": "number"},
"in_stock": {"title": "In Stock", "type": "boolean"}
},
"required": ["name", "price", "in_stock"]
}
使用方式:把
pydantic_parser串在 Chain 後端,LLM 回傳的文字會自動被轉成ProductInfo物件,若驗證失敗會拋出例外。
範例 4:自訂多層結構(列表 + 內嵌字典)
此範例展示如何解析「多筆資料」的回傳,回傳類型為 list[dict]。
from typing import List, Dict
from langchain.schema import BaseOutputParser, OutputParserException
import json
import re
class ListOfDictParser(BaseOutputParser):
"""解析 LLM 回傳的多筆 JSON 列表,允許前後雜訊文字。"""
def parse(self, text: str) -> List[Dict]:
# 取出最內層的 [...],忽略前後說明文字
match = re.search(r'\[.*\]', text, re.DOTALL)
if not match:
raise OutputParserException("找不到 JSON 陣列")
json_str = match.group(0)
try:
data = json.loads(json_str)
if not isinstance(data, list):
raise OutputParserException("解析結果不是列表")
return data
except json.JSONDecodeError as exc:
raise OutputParserException(f"JSON 解析失敗: {exc}")
# 示例
parser = ListOfDictParser()
raw = """
以下是今天的天氣預報:
[
{"city": "Taipei", "temp": 28},
{"city": "Kaohsiung", "temp": 30}
]
祝您有美好的一天!
"""
print(parser.parse(raw))
# => [{'city': 'Taipei', 'temp': 28}, {'city': 'Kaohsiung', 'temp': 30}]
關鍵點:使用正規表達式抓取最外層的 [ ],避免因 LLM 多說一句「以下是結果」而失敗。
範例 5:結合 RunnableLambda 的輕量化 Parser
若只需要簡單的「字串拆分」或「型別轉換」,不必寫完整類別,直接使用 RunnableLambda:
from langchain.schema import RunnableLambda
# 假設 LLM 回傳 "apple, banana, cherry"
split_parser = RunnableLambda(lambda txt: [s.strip() for s in txt.split(",")])
# 示範
raw = "apple, banana, cherry"
print(split_parser.invoke(raw)) # => ['apple', 'banana', 'cherry']
建議:在 Chain 中盡量使用
RunnableLambda處理一次性、無需錯誤捕捉的簡單轉換,保持程式碼可讀性。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解法 |
|---|---|---|
| LLM 加入額外說明 | 常見模型會在 JSON 前加上「以下是結果」或「Here is the JSON」 | 在 Parser 中使用正規表達式或 json.loads 的 strict=False(若支援) |
| 欄位順序不一致 | JSON 欄位順序不保證,直接使用 dict 取值即可 |
不要依賴順序;若需固定順序,使用 OrderedDict 或 Pydantic |
| 型別不符合 | 例如 price 被模型寫成字串 "12.5" |
在 Parser 中加入型別轉換或使用 Pydantic 的自動 coercion |
| 解析失敗未捕捉 | 直接拋出例外會讓整條 Chain 中斷 | 以 try/except 包裹 parse,在失敗時返回預設值或重新呼叫 LLM |
| Prompt 與 Parser 不同步 | Parser 要求的格式與 Prompt 指示不一致 | 使用 parser.get_format_instructions() 動態產生 Prompt 的格式說明,避免手動維護 |
最佳實踐
- 先定義 Schema,再產生 Prompt:利用
PydanticOutputParser或StructuredOutputParser的get_format_instructions(),確保兩端一致。 - 單元測試每個 Parser:寫測試案例涵蓋「正常回傳」與「雜訊回傳」兩種情況,確保未來模型升級不會破壞解析。
- 分層處理:先用輕量 Parser(如
TrimOutputParser)清理文字,再交給較重的結構化 Parser。 - 使用
Runnable連接:保持 Chain 的可組合性,未來想換掉 LLM 或 Parser 時,只需要改一個節點。 - 記錄失敗樣本:將解析失敗的文字存入資料庫,作為微調或 Prompt 優化的依據。
實際應用場景
| 場景 | 需求 | 建議的 Parser 組合 |
|---|---|---|
| 客服自動回覆 | LLM 必須回傳 客戶問題類型、建議解決方案(兩欄) | PydanticOutputParser(CustomerTicket model) + TrimOutputParser |
| 金融報表產出 | 需要回傳多筆交易的 日期、金額、幣別,且金額必須是 Decimal |
ListOfDictParser + Decimal 轉換 lambda |
| 多語言翻譯服務 | LLM 回傳原文與翻譯結果的對照表(列表) | JsonOutputParser + RunnableLambda 進行清理 |
| 資料標註平台 | LLM 產生標註結果(entity: label),需要即時驗證 |
StructuredOutputParser(自訂 schema) + Pydantic 進行驗證 |
| 聊天機器人指令解析 | 使用者輸入指令文字,LLM 必須回傳指令名稱與參數字典 | RegexParser(自訂)或 PydanticOutputParser(Command model) |
透過自訂 Parser,我們能把「自然語言」與「程式邏輯」之間的鴻溝縮小,讓 LLM 成為真正可程式化的元件。
總結
- Output Parser 是 LangChain 讓 LLM 輸出可程式化的關鍵環節。
- 自訂 Parser 能解決模型常見的「雜訊、格式不符、型別錯誤」等問題,提升系統穩定性。
- 建議 先定義結構化 Schema(Pydantic / JSON Schema),再以
get_format_instructions()產生 Prompt,最後實作對應的 Parser。 - 實務上,將 Parser 與 Chain 以 Runnable 方式串接,保持高度模組化與可測試性。
- 常見陷阱包括模型加入額外說明、欄位順序不定、型別不符等,透過正規表達式、型別轉換與例外捕捉即可有效化解。
掌握了自訂 Output Parser,您就能在 LLM 與業務邏輯之間建立堅實的橋樑,讓 AI 功能更可靠、更易於維護,並快速擴展到各種實務應用。祝您在 LangChain 的開發旅程中玩得開心、寫得順利!