本文 AI 產出,尚未審核

LangChain 教學 – Output Parsers:RetryOutputParser


簡介

在使用 LLM(大型語言模型)時,我們常會把模型的原始文字答案交給 Output Parser 進一步轉換成結構化資料(JSON、Pydantic 物件、Enum 等)。然而,LLM 的回應偶爾會因為上下文、提示語或模型的隨機性而產生格式錯誤、缺漏欄位或不符合預期的類型。這時如果直接把錯誤拋給上層程式,整個工作流就會中斷。

RetryOutputParser 正是為了解決這類「臨時格式錯誤」而設計的。它會在解析失敗時自動 重試 LLM,重新取得答案,直到解析成功或達到預設的重試上限。透過這個機制,我們可以大幅提升對話式 AI 應用的穩定性與使用者體驗,而不需要在每一次失敗時手動寫重試邏輯。

本文將從概念說明、實作範例、常見陷阱與最佳實踐,最後延伸到真實業務情境,完整帶你掌握 RetryOutputParser 的使用方法。


核心概念

1. 為什麼需要 RetryOutputParser?

  • LLM 輸出不一定符合格式:即使提示語寫得再清楚,模型仍可能漏掉括號、少一個欄位或把字串寫成數字。
  • 重試成本低:相較於重新設計 Prompt,直接再呼叫一次模型的成本(API 時間、金錢)通常較低。
  • 提升容錯率:在大量使用者同時對話的情境下,少一次失敗就能避免大量錯誤回報。

重點:RetryOutputParser 只在 解析失敗 時重試,而不會在模型產生正確格式但語意錯誤時介入。

2. 工作流程概覽

  1. Prompt → LLM 產生文字回應。
  2. Parser(例如 JsonOutputParser)嘗試將文字轉成結構化資料。
  3. 若解析成功 → 回傳結果。
  4. 若解析失敗 → RetryOutputParser 捕捉例外,重新呼叫 LLM(可選擇同樣的 Prompt 或加入 重試提示),重複第 2 步。
  5. 重試次數達上限仍失敗 → 拋出最終例外供上層處理。

3. 主要參數說明

參數 型別 說明
parser BaseOutputParser 真正執行解析的子解析器(如 JsonOutputParserPydanticOutputParser)。
max_retries number 最大重試次數,預設 3
retry_prompt PromptTemplate(可選) 若提供,重試時會把原 Prompt 包裝在此模板中,加入「請再次以正確 JSON 格式回覆」等指示。
on_retry (attempt: number, error: Error) => void(可選) 每次重試發生錯誤時的回呼,可用來記錄 log 或調整 Prompt。
timeout number(毫秒) 單次 LLM 呼叫的逾時時間,避免無限等待。

4. 與其他 Parser 的關係

  • 單層 Parser:如 JsonOutputParser 只負責一次性的解析,失敗即拋錯。
  • 組合模式RetryOutputParserDecorator(裝飾者)模式的實作,將重試行為「包裝」在任意解析器外層。
  • 可堆疊:可以把 RetryOutputParser 再包在 RetryOutputParser 內部,實現「不同層級的重試策略」——例如先針對 JSON 結構重試,若仍失敗再改用更寬鬆的文字解析。

程式碼範例

以下範例採用 LangChain.js(Node.js)語法。若你使用 Python,只需要把類別名稱與型別換成對應的 langchain 套件即可,概念完全相同。

範例 1:最簡單的 RetryOutputParser

import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/prompts";
import { JsonOutputParser } from "@langchain/output_parsers";
import { RetryOutputParser } from "@langchain/output_parsers";

// 1. 建立 LLM 實例
const llm = new ChatOpenAI({ modelName: "gpt-4o-mini", temperature: 0 });

// 2. 定義 Prompt
const prompt = PromptTemplate.fromTemplate(`
請依照以下格式回傳 JSON,欄位說明如下:
{
  "name": "使用者姓名",
  "age": "使用者年齡(數字)",
  "email": "使用者電子信箱"
}
請直接回傳 JSON,**不要**加上任何說明文字。
{input}
`);

// 3. 基本的 JSON 解析器
const jsonParser = new JsonOutputParser();

// 4. 包裝成 RetryOutputParser,最多重試 2 次
const retryParser = new RetryOutputParser({
  parser: jsonParser,
  max_retries: 2,
  // optional: 在每次失敗時印出 log
  on_retry: (attempt, error) => {
    console.warn(`第 ${attempt} 次解析失敗:${error.message}`);
  },
});

// 5. 執行
async function run(input) {
  const chain = prompt.pipe(llm).pipe(retryParser);
  const result = await chain.invoke({ input });
  console.log("最終結果:", result);
}

// 測試
run("我的名字是阿明,30 歲,email: ami@example.com");

說明

  • prompt.pipe(llm).pipe(retryParser) 形成一條「Prompt → LLM → RetryParser」的 pipeline。
  • 若 LLM 回傳的文字不是合法 JSON,JsonOutputParser 會拋出例外,RetryOutputParser 會捕獲並重新呼叫 LLM。

範例 2:加入自訂重試 Prompt

有時僅僅重試一次仍可能得到同樣錯誤,這時我們可以在每次重試時加入更明確的指示。

import { PromptTemplate } from "@langchain/prompts";

// 原始 Prompt
const basePrompt = PromptTemplate.fromTemplate(`
請依照以下 JSON 格式回覆:
{
  "product": "商品名稱",
  "price": "價格(數字)",
  "in_stock": "是否有庫存(true/false)"
}
{input}
`);

// 重試時使用的 Prompt,加入「請務必使用正確的 JSON」提醒
const retryPrompt = PromptTemplate.fromTemplate(`
先前的回覆格式不正確,請**務必**以正確的 JSON 格式回覆以下內容:
{
  "product": "...",
  "price": "...",
  "in_stock": "..."
}
以下是使用者的問題:
{input}
`);

// 建立 RetryOutputParser,指定 retry_prompt
const retryParser = new RetryOutputParser({
  parser: jsonParser,
  max_retries: 3,
  retry_prompt: retryPrompt,
  on_retry: (attempt, error) => {
    console.log(`重試 #${attempt} → ${error.message}`);
  },
});

// pipeline
const chain = basePrompt.pipe(llm).pipe(retryParser);

重點retry_prompt 會在每次失敗後 重新組合 Prompt,讓模型得到更強的「格式化」指示。


範例 3:結合 Pydantic(或 Zod)模型的解析

如果你希望得到更嚴格的類型驗證,可以先用 ZodOutputParser(JS)或 PydanticOutputParser(Python),再套上 Retry。

import { z } from "zod";
import { ZodOutputParser } from "@langchain/output_parsers";

// 定義資料結構
const OrderSchema = z.object({
  order_id: z.string(),
  amount: z.number(),
  currency: z.enum(["USD", "TWD", "EUR"]),
  items: z.array(z.object({
    sku: z.string(),
    qty: z.number(),
  })),
});

const zodParser = new ZodOutputParser({ schema: OrderSchema });

const retryZodParser = new RetryOutputParser({
  parser: zodParser,
  max_retries: 2,
  on_retry: (attempt, err) => console.warn(`Retry ${attempt}: ${err.message}`),
});

// Prompt
const orderPrompt = PromptTemplate.fromTemplate(`
請根據以下資訊產生一筆訂單 JSON,欄位說明請參照 schema:
{input}
`);

const orderChain = orderPrompt.pipe(llm).pipe(retryZodParser);

// 呼叫
orderChain.invoke({
  input: "客戶想買 2 件 SKU123,1 件 SKU456,總金額 1500 TWD。",
}).then(res => console.log("訂單資料:", res));

說明

  • ZodOutputParser 會先將文字轉成 JSON,再根據 OrderSchema 做型別驗證。
  • 若驗證失敗(例如 currency 拼寫錯誤),會觸發 Retry。

範例 4:自訂 on_retry 以記錄失敗原因與 Prompt 變化

在實務上,我們常需要把每次失敗的 Prompt、錯誤訊息寫入監控系統,以便事後分析。

import fs from "fs";

const logFile = "./retry_log.txt";

function logRetry(attempt, error, promptText) {
  const entry = `[${new Date().toISOString()}] Attempt ${attempt}
Prompt: ${promptText}
Error: ${error.message}
---\n`;
  fs.appendFileSync(logFile, entry);
}

const retryParser = new RetryOutputParser({
  parser: jsonParser,
  max_retries: 3,
  on_retry: (attempt, error) => {
    // 取得最近一次的 Prompt(LangChain 會在 error 中附帶 chainRunInfo)
    const lastPrompt = error?.chainRunInfo?.input?.input ?? "unknown";
    logRetry(attempt, error, lastPrompt);
  },
});

實務價值

  • 透過 log,我們可以發現模型在特定關鍵字或長度時容易產生格式錯誤,進而優化 Prompt。

範例 5:結合 Timeout 與 AbortSignal 防止無限重試

import { AbortController } from "node-abort-controller";

const controller = new AbortController();

setTimeout(() => {
  // 若 10 秒內仍未成功,就中斷整個 chain
  controller.abort();
  console.warn("執行逾時,已中止重試");
}, 10000); // 10 秒

const retryParser = new RetryOutputParser({
  parser: jsonParser,
  max_retries: 5,
  timeout: 3000, // 每次 LLM 呼叫最多 3 秒
});

const chain = prompt.pipe(llm).pipe(retryParser);

// 呼叫時傳入 abort signal
chain.invoke({ input: "..." }, { signal: controller.signal })
  .then(res => console.log("最終結果", res))
  .catch(err => console.error("最終失敗", err));

說明

  • timeout 控制單次 LLM 呼叫的最長時間。
  • AbortSignal 可以在整體流程逾時時一次性中止,避免資源浪費。

常見陷阱與最佳實踐

陷阱 說明 解決方式
重試次數過高 若模型一直產生錯誤,無限制的重試會造成成本暴增。 設定合理的 max_retries(一般 2~4 次)。
Prompt 沒變化 重複使用相同 Prompt 時,模型往往會重複同樣錯誤。 使用 retry_prompt 加入「請重新格式化」的指示,或在 on_retry 中動態調整 Prompt。
解析器過於寬鬆 使用 StringOutputParser 會讓 Retry 永遠不會觸發,因為它永遠成功。 確保使用的解析器會在格式錯誤時拋例外(如 JsonOutputParserZodOutputParser)。
未考慮非格式錯誤 有時模型回傳的 JSON 格式正確,但內容不符合業務規則。 先用結構化解析器,再加上業務驗證(如 Zod/Pydantic),必要時在 on_retry 中自行拋出錯誤讓 Retry 觸發。
忘記傳遞 AbortSignal 長時間的重試可能卡住服務,尤其在伺服器端。 使用 AbortController 並在 invoke 時傳入 signal
日誌過度 每次失敗都印出大量文字會淹沒日誌。 on_retry 中只記錄關鍵資訊(attempt、error.message、簡短 Prompt)。

最佳實踐

  1. 先寫好驗證 Schema:使用 Zod / Pydantic 定義清晰的資料結構,讓解析器自動拋錯。
  2. 設定 max_retriestimeout:避免無止盡的呼叫與成本飆升。
  3. 使用 retry_prompt:在每次失敗時加入「請務必使用正確 JSON」等提示,可大幅提升成功率。
  4. 監控與日誌:將 on_retry 的資訊寫入外部監控(如 Datadog、ELK),方便事後分析 Prompt 改進方向。
  5. 分層重試:先針對格式錯誤重試,若仍失敗再退回到「寬鬆文字解析」或「人工介入」的流程。

實際應用場景

1. 客服聊天機器人 – 取得使用者資訊

在客服系統中,需要將使用者的「姓名、電話、問題類型」收集成 JSON,並存入 CRM。若模型偶爾漏掉逗號或把電話寫成文字,系統會卡住。使用 RetryOutputParser 可以自動補救,確保資料完整寫入。

2. 電子商務商品推薦 – 多層結構

商品推薦 API 需要返回「商品列表」 + 「每個商品的價格、庫存、折扣」等多層結構。使用 ZodOutputParser + RetryOutputParser,即使模型在某筆商品的 JSON 結構錯誤,整體回應仍能在有限次數內修正,避免前端顯示錯誤。

3. 金融風控 – 風險評分表

風控模型會產生「風險項目、分數、建議」的表格。若分數欄位被模型寫成字串「high」而非數字,會導致後續計算失敗。透過 RetryOutputParser,系統會要求模型重新提供正確的數字格式,降低手動校正成本。

4. 多語言問答系統 – 語言切換

在多語言平台,使用者可能同時混雜中文、英文。為了統一回傳結構,我們在 Prompt 中加入「請以 JSON 回覆,欄位名稱使用英文」。若模型因語言切換產生非英文欄位,RetryOutputParser 會重新提示模型「請使用英文欄位」再產生回應。

5. 內部流程自動化 – 表單填寫機器人

企業內部常有自動化機器人根據聊天內容自動填寫表單(HR 請假、IT 設備申請)。表單欄位必須符合嚴格的類型驗證,任何格式錯誤都會導致審批失敗。使用 RetryOutputParser 可在提交前自動校正格式,減少人工干預。


總結

  • RetryOutputParser 為 LangChain 的 Output Parser 系列提供了「自動容錯」的能力,讓開發者在面對 LLM 輸出不穩定時,能以 最小成本 取得正確的結構化資料。
  • 透過 重試次數、重試 Prompt、on_retry 回呼 等參數,我們可以靈活控制重試行為,兼顧 效能成本
  • 結合 Schema 驗證(Zod / Pydantic)Timeout / AbortSignal,以及 日誌監控,即可在實務專案中打造 高可靠性、易維護 的 AI 工作流。

關鍵訊息:在使用 LLM 時,不要把格式錯誤視為不可接受的失敗,而是把它當作可以透過 RetryOutputParser 重新嘗試的「暫時性」問題。只要把 Prompt、解析器與重試策略配合得當,就能讓你的 AI 應用在面對真實使用者時,保持穩定、流暢的體驗。

祝你在 LangChain 的開發旅程中玩得開心,寫出更可靠的 AI 產品! 🚀