本文 AI 產出,尚未審核

LangChain 教學:Output Parsers(輸出解析)— 自訂輸出解析器

簡介

在使用大型語言模型(LLM)時,模型的原始回傳往往是自由文字(free‑form text),這對於後續程式邏輯的處理會造成障礙。Output Parser(輸出解析器)是 LangChain 提供的機制,能把模型的文字回應轉換成結構化資料(如 JSON、Pydantic 物件、列表…),讓程式碼可以直接使用。

自訂輸出解析器的價值在於:

  1. 提升可靠性:避免因文字微小差異(例如多餘的換行、標點)導致的解析錯誤。
  2. 降低耦合度:將「文字格式」的規範與業務邏輯分離,讓 Prompt 與程式碼可以各自演進。
  3. 支援複雜回應:當需求需要回傳多個欄位、嵌套結構或自訂型別時,內建的 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 的基本步驟

  1. 繼承 BaseOutputParser(或 BaseLLMOutputParser
  2. 實作 parse 方法:將文字轉成目標型別,必要時拋出 OutputParserException
  3. (可選)實作 get_format_instructions:提供給 Prompt 的格式說明,讓 LLM 知道該如何回傳。
  4. 在 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.loadsstrict=False(若支援)
欄位順序不一致 JSON 欄位順序不保證,直接使用 dict 取值即可 不要依賴順序;若需固定順序,使用 OrderedDictPydantic
型別不符合 例如 price 被模型寫成字串 "12.5" 在 Parser 中加入型別轉換或使用 Pydantic 的自動 coercion
解析失敗未捕捉 直接拋出例外會讓整條 Chain 中斷 try/except 包裹 parse,在失敗時返回預設值或重新呼叫 LLM
Prompt 與 Parser 不同步 Parser 要求的格式與 Prompt 指示不一致 使用 parser.get_format_instructions() 動態產生 Prompt 的格式說明,避免手動維護

最佳實踐

  1. 先定義 Schema,再產生 Prompt:利用 PydanticOutputParserStructuredOutputParserget_format_instructions(),確保兩端一致。
  2. 單元測試每個 Parser:寫測試案例涵蓋「正常回傳」與「雜訊回傳」兩種情況,確保未來模型升級不會破壞解析。
  3. 分層處理:先用輕量 Parser(如 TrimOutputParser)清理文字,再交給較重的結構化 Parser。
  4. 使用 Runnable 連接:保持 Chain 的可組合性,未來想換掉 LLM 或 Parser 時,只需要改一個節點。
  5. 記錄失敗樣本:將解析失敗的文字存入資料庫,作為微調或 Prompt 優化的依據。

實際應用場景

場景 需求 建議的 Parser 組合
客服自動回覆 LLM 必須回傳 客戶問題類型建議解決方案(兩欄) PydanticOutputParserCustomerTicket model) + TrimOutputParser
金融報表產出 需要回傳多筆交易的 日期、金額、幣別,且金額必須是 Decimal ListOfDictParser + Decimal 轉換 lambda
多語言翻譯服務 LLM 回傳原文與翻譯結果的對照表(列表) JsonOutputParser + RunnableLambda 進行清理
資料標註平台 LLM 產生標註結果(entity: label),需要即時驗證 StructuredOutputParser(自訂 schema) + Pydantic 進行驗證
聊天機器人指令解析 使用者輸入指令文字,LLM 必須回傳指令名稱與參數字典 RegexParser(自訂)或 PydanticOutputParserCommand 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 的開發旅程中玩得開心、寫得順利!