當退貨 Agent 執行完退貨任務後,系統能透過一般程式碼精確驗證後端 API 是否成功、資料庫是否更新,以及 Tool 是否只呼叫了一次。在傳統單元測試中,這些驗證全數通過,測試就會亮起綠燈。
但 Agent 最終產生給使用者的客服回覆,往往存在單元測試抓不到的盲區。例如後端剛建立退貨工單,Agent 卻對使用者說出這句話:
「已為您建立退貨案件 RET-901,款項已經退回您的信用卡帳戶囉!」
後端狀態完全正確,單號也確實存在,但這則回覆卻是嚴重的過度承諾與業務幻覺——款項明明還在等待驗收,Agent 卻向使用者保證已經退款。
回覆文字是否清楚、語氣是否符合客服專業、以及是否「忠於後端事實而沒有胡亂開支票」,無法用固定的字串完全比對。這類開放式的語意品質,需要由另一個模型——LLM Judge(評判模型)——依據明確的評分標準(Rubric)來評估。
這篇將說明如何為 Agent 回覆建立可靠的評判機制:劃分程式與 Judge 的檢驗邊界、縮小輸入資料、使用二元結構化輸出避免評分漂移,並將評分結果納入發布門檻。
建立評估流程的第一步,是劃清「程式檢查」與「LLM Judge」的職責邊界。
退貨能不能成立、Tool 軌跡是否安全、單號格式是否正確,後端狀態與資料庫已有確定答案。若把這些確定性邏輯丟給 Judge,不僅浪費 API 成本與時間,還會引入不必要的模型波動與誤判:
status 是否正確、Tool 呼叫順序與次數是否合規、必要代碼(如 RET-901)是否出現。這部分由本機測試程式直接驗證,任何一筆失敗都不允許發布。決定引入 LLM Judge 時,常見的直覺是將使用者的原始提問、System Prompt、Tool 執行日誌、Agent 內部思考過程全部打包丟給評判模型,並詢問「這個 Agent 表現好不好」。
這種做法會帶來三個問題:
因此,評估回覆品質時,輸入邊界必須遵循最小化原則:完全不看 Agent 的思考與除錯日誌,只傳入兩個核心欄位:
confirmed_facts:後端 Tool 執行後確認的結構化事實(例如退貨單號、處理狀態)。reply:Agent 最終產生給使用者的對話文字。輸入資料結構範例:
{
"confirmed_facts": {
"status": "completed",
"outcome": "created",
"return_id": "RET-901"
},
"reply": "已為您建立退貨案件 RET-901,商品寄回並完成驗收後將為您安排退款。"
}
將輸入嚴格限制在事實與回覆後,Judge 接收到相同的後端事實時,便能針對不同回覆給出明確的判定與理由:
passed=True)
passed=False)
在設計評判輸出時,使用二元是非判斷(passed: bool)通常比 1 到 5 分更穩定。數值評分容易讓評判模型在「給 3 分還是 4 分」之間產生隨機浮動;而 True / False 邊界非黑即白,只要出現捏造承諾或語意不清就判定不合格,能大幅降低評估的不確定性。
在 evaluators.py 中,使用 Pydantic 定義 Judge 必須回傳的結構化格式:
class ReplyQualityVerdict(BaseModel):
passed: bool = Field(
description="回覆是否合格(清楚、簡潔且忠於已確認事實,無虛假承諾)"
)
reason: str = Field(min_length=1, description="判斷合格或不合格的具體原因")
使用 LangChain 的 with_structured_output() 將 Schema 綁定至 Judge 模型:
def build_reply_quality_evaluator(
judge_model: BaseChatModel,
) -> Callable[..., Evaluation]:
structured_judge = judge_model.with_structured_output(ReplyQualityVerdict)
def reply_quality(*, output: Any, **kwargs: Any) -> Evaluation:
payload = {
"confirmed_facts": output.get("reply_facts"),
"reply": output.get("final_reply"),
}
try:
verdict = structured_judge.invoke(
[
SystemMessage(
content=(
"你只評估繁體中文客服回覆是否清楚、簡潔,並忠於"
" confirmed_facts。不要重新判斷退貨是否成立。"
"若回覆合格且無多餘承諾請回傳 passed=True;"
"若有幻覺、過度承諾或表達不當請回傳 passed=False。"
)
),
HumanMessage(content=json.dumps(payload, ensure_ascii=False)),
]
)
parsed = ReplyQualityVerdict.model_validate(verdict)
except Exception as error:
return Evaluation(
name="reply_quality",
value=False,
data_type="BOOLEAN",
comment=f"Judge 執行失敗:{type(error).__name__}。",
)
return Evaluation(
name="reply_quality",
value=parsed.passed,
data_type="BOOLEAN",
comment=parsed.reason,
)
return reply_quality
這段評分標準(Rubric)明確規範了三項原則:
confirmed_facts,嚴禁自行揣測或重新判斷退貨業務。Evaluator 收到 passed 布林值後,以 BOOLEAN 型別存入 Langfuse,並將評判原因寫入 comment。
在 experiment.py 中,評估流程將本機程式的確定性檢查函式與 reply_quality LLM Judge 一併放入 evaluators 清單,交由 Langfuse Experiment 統一執行:
evaluators: list[Callable[..., Any]] = list(DETERMINISTIC_EVALUATORS)
if judge_model_name is not None:
judge_model = ChatAnthropic(
model=judge_model_name,
temperature=0,
max_tokens=300,
)
evaluators.append(build_reply_quality_evaluator(judge_model))
當批次測試執行時,Langfuse 會為資料集裡的每一筆案例同時跑完所有檢查,並在後台報表統整兩邊的評分:
reply_facts 與最終客服文字 final_reply。final_result_matches、safe_tool_trajectory)。reply_quality 的布林判定(True / False)與 Judge 給出的具體評判原因。若 Judge 呼叫失敗(如網路超時或輸出格式解析錯誤),build_reply_quality_evaluator() 內的例外處理會直接記錄 value=False 並在 comment 註明錯誤類型,避免單一案例異常導致整個 Experiment 批次測試中斷。
離線評估不能只拿單一簡單對話做抽查,而是要用一組涵蓋各種異常邊界的黃金測試集(Dataset),檢驗 Agent 是否在每種情境下都能維持穩定的業務邏輯與回覆品質。
範例資料集包含 6 種典型退貨情境,分別檢驗不同的業務狀態與回覆要求:
| 案例 ID | 情境說明 | 預期業務結果 | 理想回覆重點 |
|---|---|---|---|
success |
正常退貨 | 建立成功(completed) |
明確告知單號 RET-901 與驗收流程 |
rejected |
超過 7 天鑑賞期 | 業務拒絕(rejected) |
委婉說明超期原因,不可答應退貨 |
unavailable_then_success |
後端短暫異常,重試後成功 | 建立成功(completed) |
正確重試並告知退貨單號 |
always_unavailable |
服務異常且達重試上限 | 轉人工客服(needs_human) |
請客人聯繫人工客服,嚴禁承諾已退款 |
timeout_after_create |
寫入後超時,冪等查詢確認已建單 | 建立成功(completed) |
避免重複扣款,告知單號 RET-901 |
timeout_unresolved |
超時且無法確認後端狀態 | 轉人工客服(needs_human) |
告知處理中並轉交人工,避免誤導客人 |
當執行評估命令時,系統會將每個案例逐一送入退貨流程,並同時進行兩組檢驗:
reply_quality LLM Judge:評判客服回覆是否簡潔、清楚,且完全忠於已確認的事實,沒有做出任何虛假承諾。進入範例目錄並安裝依賴:
$ cd ai-agent-sample/langgraph/langgraph-return-evaluation
$ uv sync
設定 Langfuse 與模型金鑰(可直接寫入專案根目錄的 .env,或匯出環境變數):
# 也可直接複製 .env.example 為 .env,或共用 langgraph-pydantic-intent-routing 的 .env
LANGFUSE_PUBLIC_KEY="..."
LANGFUSE_SECRET_KEY="..."
LANGFUSE_BASE_URL="http://localhost:3000"
ANTHROPIC_API_KEY="..."
同步 Prompt 與測試案例至 Langfuse:
$ uv run return-eval sync
Prompt version:1
Dataset cases:6
先單獨執行單一成功案例,驗證 Agent 回覆與 Judge 評判是否正常運作(未指定 --prompt-version 時,會自動採用最新同步的版本):
uv run return-eval run --case success
若要一次執行完整資料集的所有 6 筆案例,直接執行:
uv run return-eval run
在日常本機測試時可以省略版本號直接抓最新 Prompt。但在 CI/CD 或正式發布審查時,為了確保評估結果具備可重現性(避免 Prompt 改動導致歷史評估分數失去對照基準),建議明確加上 --prompt-version 釘選版本。
正式評估時,通常會針對被測 Agent 與 Judge 分別配置模型(例如 Agent 使用延遲與成本較低的 Haiku,Judge 則指定推理能力更強的 Sonnet 把關):
uv run return-eval run \
--prompt-version 1 \
--model claude-4-5-haiku-latest \
--judge-model claude-4-5-sonnet-latest \
--run-name return-v1-release-candidate
若只想快速確認程式邏輯,可加上 --skip-judge 略過評判模型:
uv run return-eval run --prompt-version 1 --skip-judge
發布門檻是團隊對「這批評估結果是否足以支持上線」的明確規則。它可以由 CI/CD 自動檢查,也可以由 QA、客服或產品負責人根據評估報告審核。高風險流程還可以結合兩者:安全條件失敗時自動阻擋,回覆品質落在邊界時要求人工核准。
這個退貨 Agent 將業務結果、Tool 軌跡與回覆契約設為零容忍條件,任何一筆失敗都不允許發布。啟用 LLM Judge 時,全體案例的 reply_quality 平均合格率還必須至少達到 80%。例如六筆案例中有兩筆回覆出現幻覺或過度承諾,合格率只有 66.7%,這個版本就不應上線。
範例的 release_gate.py 負責彙總所有案例並產生通過或失敗的判定。評估命令在門檻失敗時以 exit code 1 結束,CI/CD 才能據此停止後續部署。release_gate.py 本身不會發布或下架 Agent,它只是把品質政策轉成可重複執行的判定。
離線評估的標準答案來自測試集的 expected_output,Judge 的評分標準來自 Rubric,兩者都在上線前就已固定。Agent 上線後,真實使用者的問法會超出測試集;線上 Trace 只記錄使用者輸入與 Agent 的決策,沒有客人真正的意圖,所以程式無法自動算出意圖分類的正確率,Judge 也只能評估回覆的表達,無法判斷分流是否正確。
要取得線上正確率,必須從線上 Trace 抽樣,由客服或產品團隊人工標註標準答案。同一份人工標註也能和 Judge 的 Pass / Fail 比對,確認 Judge 的評判標準與團隊一致。下一篇會用 Langfuse 的 Annotation Queue 完成抽樣與標註,並把線上發現的失敗案例回補到離線測試集。