AI 教學 14 min read

LangGraph LLM Node:結構化輸出 + Few-shot + 業務驗證

把真正的 LLM 接進 LangGraph node,三個關鍵:結構化輸出(用 Pydantic schema 強制 LLM 吐固定格式,不要 parse 自由文字)、few-shot(在 prompt 裡塞範例把輸出品質拉穩)、業務驗證(LLM 吐的東西不能照單全收,過一道規則檢查)。這篇示範怎麼用 with_structured_output 在決策 node 裡把這三件事兜起來,並講最容易出事的地方:LLM 的結構對了不代表內容對。「用真實專案學 LangGraph」系列 EP4。

本文目錄 · 9
TL;DR:前三篇 node 都是純算的假資料,這篇把真正的 LLM 接進 node。三個關鍵:結構化輸出(用 Pydantic schema 強制 LLM 吐固定格式 JSON,不要 parse 自由文字)、few-shot(在 prompt 裡塞幾個範例,把輸出品質拉穩)、業務驗證(LLM 吐的東西不能照單全收,過一道規則檢查)。這篇示範怎麼在血脂決策 node 裡把這三件事兜起來,並講一個最容易出事的地方:LLM 的結構對了不代表內容對。系列 EP4。

EP3 教完分流跟重試,但那個 llm_decide 是假的——它沒真的呼叫模型。這篇把它換成真的,並把「怎麼讓 LLM 的輸出可控、可驗」講清楚。

📌 目錄

為什麼不能直接 parse LLM 的自由文字

最天真的做法:丟一段 prompt,LLM 回一坨自由文字,你自己用正則 / 字串切割去撈你要的欄位。

這做法在 demo 能跑,上 production 一定爆。因為 LLM 的自由文字格式每次都可能不一樣

  • 這次回「建議:升級用藥」,下次回「我的建議是升級用藥喔!」
  • 你要的數字它有時加單位、有時不加
  • 它偶爾在前面加一段「好的,讓我來分析…」的廢話
  • 換個 model、換個版本,格式整個變
你的正則永遠追不上。正解是結構化輸出——直接叫 LLM 吐固定 schema 的 JSON,由框架幫你保證格式。

🧠 結構化輸出:用 schema 綁死格式

先用 Pydantic 定義你要的輸出長什麼樣:

from pydantic import BaseModel, Field
from typing import Literal

class LipidDecision(BaseModel):
"""血脂處置建議的結構化輸出"""
action: Literal["維持現狀", "調整生活型態", "升級用藥"] = Field(
description="處置方向,只能是這三個之一"
)
reason: str = Field(description="這個處置的臨床理由,必填,不可空白")
target_ldl: int = Field(description="建議的 LDL 目標值 (mg/dL)")

重點在三個地方:

  • Literal[...] 把 action 鎖死成枚舉——LLM 不能亂創一個第四種處置。
  • Field(description=...) 是給 LLM 看的——這些描述會變成 schema 的一部分送給模型,等於是 inline 的指令。
  • 型別就是契約——target_ldl: int 保證你拿到的是整數,不是 "100 mg/dL" 字串。
然後用 LangChain 的 with_structured_output 把 schema 綁到 model 上:
from langchain.chat_models import init_chat_model

model = init_chat_model("openai:gpt-4o-mini") # 換成你的 provider
structured = model.with_structured_output(LipidDecision)

result = structured.invoke("LDL 160、有家族史的中年男性,給處置建議")
print(result.action) # → "升級用藥"(保證是三個枚舉之一)
print(result.reason) # → 臨床理由字串
print(type(result)) # → <class 'LipidDecision'>,不是 str

with_structured_output(LipidDecision) 之後,invoke 回的直接是 LipidDecision 物件,不是文字。底層它用 model 的 function calling / JSON mode 去保證格式,parse 失敗框架會幫你處理。你再也不用寫正則撈欄位。

📝 Few-shot:把範例塞進 prompt 拉穩品質

結構對了,但內容品質還是飄。同樣的 case,LLM 給的理由有時很到位、有時很空泛。拉穩品質最有效的手段是 few-shot——在 prompt 裡塞幾個「輸入 → 理想輸出」的範例,讓模型照著學:

from langchain_core.prompts import ChatPromptTemplate

SYSTEM = """你是血脂處置決策助手。根據病人指標給出結構化建議。
理由必須引用具體數值,不可空泛。參考以下範例的詳細程度:"""

EXAMPLES = """
範例 1:
輸入:LDL 100、無危險因子、首次檢查
輸出:action=維持現狀, reason=LDL 100 已達一般族群目標 (<130),無危險因子,建議維持並一年後複檢
範例 2:
輸入:LDL 160、糖尿病史、已用 statin
輸出:action=升級用藥, reason=糖尿病屬高風險族群目標 <70,現值 160 嚴重未達標,建議在現有 statin 上加 ezetimibe
"""

prompt = ChatPromptTemplate.from_messages([
("system", SYSTEM + EXAMPLES),
("human", "{patient_summary}"),
])

chain = prompt | model.with_structured_output(LipidDecision)

兩個範例就能讓「理由要引用具體數值」這個要求從「模型有時記得」變成「模型穩定做到」。few-shot 的範例數量不是越多越好——通常 2-4 個有代表性的就夠,太多會吃掉 context 又拉高成本。挑邊界 case 當範例(剛好達標 / 嚴重未達標)比挑普通 case 有用。

⚙️ 把 LLM 包成一個 node

現在把上面這條 chain 塞進 LangGraph 的 node。記得 EP2 教的:node 收 state、回傳只含改動欄位的 dict

def llm_decide(state: AnalyzeState) -> dict:
    patient = state["patient"]
    summary = f"LDL {patient['ldl']}、危險因子 {patient['risk_factors']}"

decision: LipidDecision = chain.invoke({"patient_summary": summary})

# 把 Pydantic 物件轉成 state 要存的形狀
return {
"decision": decision.action,
"reason": decision.reason,
"target_ldl": decision.target_ldl,
}

這個 node 就能直接接進 EP3 那張帶 conditional edge 的 graph——llm_decide 跑完進 validate,驗過往前、不過退回重試。LLM 在這裡只是 graph 裡的一個 node,跟其他純算的 node 平起平坐。這就是 LangGraph 的價值:把 AI 步驟跟確定性步驟用同一套 state / edge 串在一起。

🎯 業務驗證:結構對 ≠ 內容對

這是這篇最重要的一段,也是最多人栽的地方。

with_structured_output 保證的是結構合法:action 一定是三個枚舉之一、target_ldl 一定是整數、reason 一定有字串。但它完全不保證內容正確

  • action 是合法枚舉,但對這個病人是錯的處置
  • reason 有字串,但寫的是「建議升級用藥」配上「維持現狀」的 action(自相矛盾)
  • target_ldl 是整數 50,但臨床上不合理
所以 LLM node 後面一定要接一道業務規則驗證(就是 EP3 的 validate node),用你自己寫死的規則去檢查 LLM 的輸出在業務上合不合理:
def validate(state: AnalyzeState) -> dict:
    action = state["decision"]
    target = state["target_ldl"]
    reason = state.get("reason", "")

checks = [
bool(reason.strip()), # 理由不可空白
70 <= target <= 190, # 目標值要在合理範圍
not (action == "維持現狀" and "升級" in reason), # 不可自相矛盾
]
passed = all(checks)
out = {"validation": {"passed": passed}}
if not passed:
out["retry_count"] = 1
return out

這道驗證是你系統的最後一道防線。LLM 再聰明都可能出系統性錯誤,業務規則是確定性的、可審計的、不會幻覺。永遠不要讓 LLM 的輸出未經驗證就流到下游——尤其醫療、金融這種高風險場景。結構化輸出解決「格式可控」,業務驗證解決「內容可信」,兩個都要。

🔥 我踩過的坑

坑一:以為結構化輸出就不用驗了

最常見的誤會。with_structured_output 只管格式,不管對錯。我一開始也以為「都用 Pydantic 綁死了還驗什麼」,結果 LLM 給出結構完美但臨床上矛盾的建議——action 跟 reason 對不上。結構化輸出跟業務驗證是兩件事,缺一不可。

坑二:few-shot 範例挑錯,反而帶歪

few-shot 是雙面刃。如果你的範例本身品質不一致,或都挑簡單 case,模型會學到「原來不用很仔細」。範例要挑你最在意的邊界情況,而且每個範例的輸出都要是你心目中的滿分答案。範例就是你對模型的隱性規格。

坑三:把 schema 描述寫得太簡略

Field(description=...) 不是註解,是會送給 LLM 的指令。寫 Field(description="理由") 跟寫 Field(description="臨床理由,必填,需引用具體 LDL 數值,不可空泛"),模型表現差很多。description 越具體,輸出越穩。把它當成 prompt 的一部分來寫,不要當成程式註解。

❓ 常見問題 FAQ

Q:with_structured_output 每個 model 都支援嗎?
A:主流的(OpenAI、Anthropic、Google 等)都支援,但底層機制不同(function calling vs JSON mode)。LangChain 幫你抽象掉差異,但冷門或本地小模型可能支援度差、parse 失敗率高。換 model 時務必實測結構化輸出的穩定度。

Q:結構化輸出失敗會怎樣?
A:如果 LLM 吐的東西無法 parse 成你的 schema,LangChain 預設會丟錯。你可以在 node 裡 try/except 接住,當成驗證失敗走 EP3 的 retry 流程。不要讓 parse 錯誤直接炸掉整張 graph。

Q:few-shot 跟 fine-tune 差在哪,我該用哪個?
A:few-shot 是在 prompt 裡塞範例,零成本、即改即生效,適合範例少、需求常變的情況——大部分業務場景用 few-shot 就夠。fine-tune 是真的去訓練模型,成本高、週期長,只有在 few-shot 怎麼調都救不動、且量大到划算時才考慮。先用 few-shot,撞牆再說。

Q:業務驗證規則寫在 node 裡,會不會跟 LLM 邏輯混在一起難維護?
A:建議拆開。LLM node 只負責「呼叫模型拿結構化輸出」,validate node 只負責「用規則檢查」。EP3 就是這樣分的——兩個獨立 node、中間用 conditional edge 連。職責分清楚,驗證規則改起來不會動到 LLM 那段。

Q:可以讓 LLM 自己驗證自己的輸出嗎(LLM-as-judge)?
A:可以當補充,不能當唯一防線。LLM 驗 LLM 一樣會幻覺,兩個都錯的機率不是零。高風險場景的最後一道驗證一定要是確定性規則(你寫死的 if-else),LLM-as-judge 適合用在規則難明確表達的軟性品質檢查(語氣、完整度)。

🔗 延伸資源

author
陳彥彤

AI 工程師 · AI 顧問。Java 後端 8 年、AI 工程師 2 年。AI 內訓 · AI 導入顧問 · 前後端與雲端培訓。

support

覺得文章有用可以到 GitHub 給個 star,或是透過信箱聊聊 AI 內訓、AI 導入顧問或前後端 / 雲端培訓。

related

相關文章

[AI 教學] · 13min
LangChain vs LangGraph 是什麼?2026 該學哪個(完整比較)
LangChain 跟 LangGraph 不是二選一,是上下層:LangGraph 是底層 runtime(管 state、流程、重試),LangChain 是建在它上面的高階 API。2025-10-22 兩者都上 1.0 正式版。這篇用比較表、三個情境判斷、三個真實踩坑,講清楚標準 agent loop 該用 LangChain、要精細控制流程分流與重試該用 LangGraph,以及完全新手該從哪個開始學。「用真實專案學 LangGraph」系列第一篇。
[AI 工具] · 12min
我原本用 Hermes Agent CLI,後來為什麼換成 LangGraph(遷移覆盤)
系列番外。我這套醫療決策 pipeline 最早不是用 LangGraph,是用 Hermes Agent CLI(NousResearch 2026 年初開源的自主 agent 框架)。Hermes 很強,但強在通用對話助理、會自我學習、接 20+ 平台;而我要的是確定性步驟為主、流程可畫成圖、每步可審計、出錯能精準退件的決策管線。方向不對再強也是錯的工具。這篇覆盤這個選型決策:Hermes 哪裡好、為什麼對我的場景不對、換到 LangGraph 解決了什麼。「用真實專案學 LangGraph」系列 EP6(完結)。
[AI 教學] · 13min
LangGraph 完整 Pipeline + Human-in-the-loop:8 步決策管線全貌
前四篇的零件——state、node、edge、conditional edge、reducer、LLM 結構化輸出——這篇全部組起來,串成一條完整的醫療 AI 決策 pipeline:載入資料 → 補衍生指標 → 載入規則 → 組 few-shot → LLM 出建議 → 業務驗證 → 適配輸出 → 持久化,中間帶分流重試。然後加上 LangGraph 的殺手級功能 human-in-the-loop:用 interrupt() 在高風險決策點暫停整張 graph,等人點頭才繼續,靠 durable state 讓這件事變簡單。「用真實專案學 LangGraph」系列 EP5。