LangChain 教學 – Output Parsers:RetryOutputParser
簡介
在使用 LLM(大型語言模型)時,我們常會把模型的原始文字答案交給 Output Parser 進一步轉換成結構化資料(JSON、Pydantic 物件、Enum 等)。然而,LLM 的回應偶爾會因為上下文、提示語或模型的隨機性而產生格式錯誤、缺漏欄位或不符合預期的類型。這時如果直接把錯誤拋給上層程式,整個工作流就會中斷。
RetryOutputParser 正是為了解決這類「臨時格式錯誤」而設計的。它會在解析失敗時自動 重試 LLM,重新取得答案,直到解析成功或達到預設的重試上限。透過這個機制,我們可以大幅提升對話式 AI 應用的穩定性與使用者體驗,而不需要在每一次失敗時手動寫重試邏輯。
本文將從概念說明、實作範例、常見陷阱與最佳實踐,最後延伸到真實業務情境,完整帶你掌握 RetryOutputParser 的使用方法。
核心概念
1. 為什麼需要 RetryOutputParser?
- LLM 輸出不一定符合格式:即使提示語寫得再清楚,模型仍可能漏掉括號、少一個欄位或把字串寫成數字。
- 重試成本低:相較於重新設計 Prompt,直接再呼叫一次模型的成本(API 時間、金錢)通常較低。
- 提升容錯率:在大量使用者同時對話的情境下,少一次失敗就能避免大量錯誤回報。
重點:RetryOutputParser 只在 解析失敗 時重試,而不會在模型產生正確格式但語意錯誤時介入。
2. 工作流程概覽
- Prompt → LLM 產生文字回應。
- Parser(例如
JsonOutputParser)嘗試將文字轉成結構化資料。 - 若解析成功 → 回傳結果。
- 若解析失敗 →
RetryOutputParser捕捉例外,重新呼叫 LLM(可選擇同樣的 Prompt 或加入 重試提示),重複第 2 步。 - 重試次數達上限仍失敗 → 拋出最終例外供上層處理。
3. 主要參數說明
| 參數 | 型別 | 說明 |
|---|---|---|
parser |
BaseOutputParser |
真正執行解析的子解析器(如 JsonOutputParser、PydanticOutputParser)。 |
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只負責一次性的解析,失敗即拋錯。 - 組合模式:
RetryOutputParser是 Decorator(裝飾者)模式的實作,將重試行為「包裝」在任意解析器外層。 - 可堆疊:可以把
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 永遠不會觸發,因為它永遠成功。 |
確保使用的解析器會在格式錯誤時拋例外(如 JsonOutputParser、ZodOutputParser)。 |
| 未考慮非格式錯誤 | 有時模型回傳的 JSON 格式正確,但內容不符合業務規則。 | 先用結構化解析器,再加上業務驗證(如 Zod/Pydantic),必要時在 on_retry 中自行拋出錯誤讓 Retry 觸發。 |
| 忘記傳遞 AbortSignal | 長時間的重試可能卡住服務,尤其在伺服器端。 | 使用 AbortController 並在 invoke 時傳入 signal。 |
| 日誌過度 | 每次失敗都印出大量文字會淹沒日誌。 | 在 on_retry 中只記錄關鍵資訊(attempt、error.message、簡短 Prompt)。 |
最佳實踐
- 先寫好驗證 Schema:使用 Zod / Pydantic 定義清晰的資料結構,讓解析器自動拋錯。
- 設定
max_retries與timeout:避免無止盡的呼叫與成本飆升。 - 使用
retry_prompt:在每次失敗時加入「請務必使用正確 JSON」等提示,可大幅提升成功率。 - 監控與日誌:將
on_retry的資訊寫入外部監控(如 Datadog、ELK),方便事後分析 Prompt 改進方向。 - 分層重試:先針對格式錯誤重試,若仍失敗再退回到「寬鬆文字解析」或「人工介入」的流程。
實際應用場景
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 產品! 🚀