評估與除錯:agent 為什麼失敗
你的 agent 跑了十幾輪,最後輸出「I was unable to complete the task」——然後程式結束。你看著終端機,完全不知道它在中間做了什麼、在哪一步走偏了、是工具出了問題還是 prompt 根本沒說清楚。你再跑一次,得到不一樣的失敗。再跑一次,又不一樣。
這就是 agent 開發和傳統軟體開發最痛苦的差異:傳統程式壞了,你加一行 print 就知道在哪;agent 壞了,你面對的是模型的內部決策過程——看不見、不確定、每次不一樣。
評估和除錯不是開發完成後才加的補丁,是 agent 開發的核心工作方法。這堂課教你從第一行 code 就把觀察能力做進去,讓「agent 為什麼失敗」變成一個可以系統性回答的問題。
這堂學什麼
- Trace 記錄系統:讓每一步工具呼叫、決策、輸入輸出都可以回放,不靠記憶猜
- 四大失敗模式:工具誤用、迷路(lost in loop)、過早放棄、無限迴圈——辨識症狀與根因
- 評估任務集:如何建立一組任務測量 agent 的真實能力,而不是跑一次看感覺
- 迭代決策:面對失敗,怎麼判斷要改 prompt、改工具設計,還是動架構
- 實戰:對自己的 agent 跑評估,修復兩個具體失敗案例
觀念一:沒有 trace 就沒有除錯
「你可以 debug 的,只有你看得見的東西。」這是 2026 年 agent 可觀察性的核心原則。
沒有刻意做觀察,你只能看到 agent 的最終輸出——但你看不見它做了幾次工具呼叫、每次傳了什麼參數、工具回傳了什麼、模型在哪一步改變決策方向。這跟開車時後照鏡完全黑掉是同一件事。

Trace 記錄要捕捉什麼?以下是 agent 除錯最有用的七個維度:
| 維度 | 記錄什麼 | 為什麼重要 |
|---|---|---|
| Turn 序號 | 目前是第幾輪迴圈 | 知道 agent 跑了多少步 |
| 模型的文字輸出 | 模型在工具呼叫前說了什麼 | 看它的「想法」 |
| Tool call 名稱 + 參數 | 呼叫了哪個工具、傳了什麼 | 找出工具誤用 |
| 工具回傳內容 | 工具回傳什麼給模型 | 判斷工具是否正常運作 |
| stop_reason | end_turn / tool_use / max_tokens |
找出異常終止原因 |
| token 用量 | input/output tokens per turn | 監控 context 壓力 |
| 執行時間 | 每一步花多久 | 找出異常慢的工具 |
以下是一個完整的 trace 記錄實作,插進任何自寫 loop 都能用:
import json
import time
from datetime import datetime
from pathlib import Path
class AgentTracer:
"""
在 agent loop 裡插入這個 tracer,每步都有完整紀錄。
輸出 JSONL 格式,每行一個事件,方便後續分析。
"""
def __init__(self, run_id: str = None, output_dir: str = "./traces"):
self.run_id = run_id or datetime.now().strftime("%Y%m%d_%H%M%S")
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
self.trace_file = self.output_dir / f"{self.run_id}.jsonl"
self.turn = 0
self.start_time = time.time()
def _write(self, event: dict):
event["run_id"] = self.run_id
event["elapsed_s"] = round(time.time() - self.start_time, 2)
with open(self.trace_file, "a", encoding="utf-8") as f:
f.write(json.dumps(event, ensure_ascii=False) + "\n")
def log_task(self, task: str, system_prompt: str):
self._write({
"type": "task_start",
"task": task,
"system_prompt_len": len(system_prompt),
})
def log_turn_start(self, input_tokens_estimate: int = 0):
self.turn += 1
self._write({
"type": "turn_start",
"turn": self.turn,
"input_tokens_estimate": input_tokens_estimate,
})
def log_model_output(self, stop_reason: str, text: str, usage: dict):
self._write({
"type": "model_output",
"turn": self.turn,
"stop_reason": stop_reason,
"text_preview": text[:300] if text else "", # 只存前 300 字,節省空間
"usage": usage,
})
def log_tool_call(self, tool_name: str, tool_id: str, tool_input: dict):
self._write({
"type": "tool_call",
"turn": self.turn,
"tool_name": tool_name,
"tool_id": tool_id,
"tool_input": tool_input,
})
def log_tool_result(self, tool_id: str, tool_name: str, result: dict, duration_ms: int):
self._write({
"type": "tool_result",
"turn": self.turn,
"tool_id": tool_id,
"tool_name": tool_name,
# 只存 success 狀態和前 500 字,避免大結果塞爆 trace
"result_success": result.get("success", True),
"result_preview": json.dumps(result, ensure_ascii=False)[:500],
"duration_ms": duration_ms,
})
def log_end(self, final_output: str, reason: str):
self._write({
"type": "task_end",
"total_turns": self.turn,
"reason": reason, # "completed" / "max_turns" / "error"
"final_output_preview": final_output[:300] if final_output else "",
})
print(f"[trace] 完整記錄存在 {self.trace_file}")
把 tracer 插進 agent loop:
import anthropic
client = anthropic.Anthropic()
def run_agent_with_trace(task: str, system: str, tools: list,
max_turns: int = 20) -> str:
tracer = AgentTracer()
tracer.log_task(task, system)
messages = [{"role": "user", "content": task}]
for _ in range(max_turns):
tracer.log_turn_start()
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=4096,
system=system,
tools=tools,
messages=messages
)
# 取出模型的文字輸出(如果有的話)
text_output = ""
for block in response.content:
if block.type == "text":
text_output += block.text
tracer.log_model_output(
stop_reason=response.stop_reason,
text=text_output,
usage={
"input_tokens": response.usage.input_tokens,
"output_tokens": response.usage.output_tokens,
}
)
if response.stop_reason == "end_turn":
tracer.log_end(text_output, "completed")
return text_output
if response.stop_reason == "tool_use":
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type == "tool_use":
tracer.log_tool_call(block.name, block.id, block.input)
t0 = time.time()
result = execute_tool(block.name, block.input)
duration_ms = int((time.time() - t0) * 1000)
tracer.log_tool_result(block.id, block.name, result, duration_ms)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result, ensure_ascii=False),
})
messages.append({"role": "user", "content": tool_results})
tracer.log_end("", "max_turns")
return "超過最大迴圈次數,任務未完成"
每次執行都會在 ./traces/ 目錄產出一個 JSONL 檔案。你可以用任何工具分析它——或是直接在除錯時打開來逐行讀。
觀念二:四大失敗模式
根據 2026 年 agent 社群的實際回報與研究,agent 的失敗有規律可循,可以分成四個清晰的類型。辨識出是哪種模式,才能對症下藥。

失敗模式一:工具誤用(Tool Misuse)
症狀:agent 呼叫了工具,但傳了錯誤的參數——路徑格式錯、數字超出範圍、把 JSON 傳成字串。工具回傳錯誤,agent 可能嘗試幾次後放棄,也可能無視錯誤繼續跑而結果是錯的。
Trace 特徵:
[tool_call] tool_name=read_file, tool_input={"path": "data/report"}
[tool_result] result_success=false, result_preview={"error": "FILE_NOT_FOUND", ...}
[tool_call] tool_name=read_file, tool_input={"path": "data/report"} ← 完全一樣的參數,重試
[tool_result] result_success=false, ...
根因:工具的 description 不夠清楚,或參數的 description 沒有說明格式(例如是不是需要副檔名、相對路徑還是絕對路徑)。
修法:回到工具 schema,在 path 參數的 description 裡加範例:"路徑要包含副檔名,例如 './data/report.txt' 或 '/tmp/logs/app.log'"。
失敗模式二:迷路(Lost in Loop)
症狀:agent 沒有錯誤,只是原地繞圈——它做了 A,做了 B,又做一次 A,又做一次 B。每次工具都成功回傳,但任務毫無進展。或者更極端的版本:agent 一直說「讓我寫這份報告」,但從來沒有真的呼叫 write_file。
Trace 特徵:
[turn 3] tool_call=search_web, query="AI agent frameworks 2026"
[turn 4] tool_call=search_web, query="top AI agent frameworks" ← 幾乎一樣的查詢
[turn 5] tool_call=search_web, query="best AI agent tools 2026" ← 再一次
[turn 6] model_output: "Let me search for more information..." ← 說要做但不做
根因:system prompt 沒有明確定義「完成的標準」,或任務太模糊,讓 agent 陷入「再多查一點更好」的循環。另一個常見原因是搜尋結果不夠好,agent 不知道下一步可以做什麼,就繼續搜。
修法:在 system prompt 裡加入「完成標準」:"當你已經有足夠資訊回答問題時,直接輸出答案——不要為了讓答案更好而無限搜尋。"。同時設定硬性的工具呼叫上限。
失敗模式三:過早放棄(Early Termination)
症狀:agent 還沒完成任務就輸出最終答案。有時是工具報了一個不嚴重的錯誤,agent 就決定整個任務失敗;有時是 code 處理 stop_reason 的邏輯有 bug。
Trace 特徵:
[turn 2] tool_result: result_success=false, result_preview={"error":"PARTIAL_RESULT",...}
[turn 3] stop_reason=end_turn
[task_end] total_turns=3, reason="completed" ← 只跑了 3 輪就結束,任務根本沒完成
或是 harness 層的 bug:
# 常見的過早放棄 bug:response.content[0] 可能是 text block,不是 tool_use
if response.content[0].type == "text":
return response.content[0].text # ← 太早結束!可能 text 後面還有 tool_use block
根因:有兩個方向:①模型判斷失誤(工具錯誤被認為是「任務失敗」);②你寫的 harness 邏輯有 bug,在不該結束的時候結束了。
修法:模型判斷的問題用 system prompt 處理:"遇到工具錯誤時,根據 hint 修正後重試,最多重試 2 次再放棄單一工具——除非確認整個任務目標無法達成,否則繼續執行其他步驟。" harness bug 則要修 code,正確的終止判斷是 stop_reason == "end_turn" 加上確認 content 裡沒有 tool_use block。
失敗模式四:無限迴圈(Infinite Loop)
症狀:agent 就是不停,token 一直燒。2026 年有真實案例是 5 分鐘內燒掉 400 萬 token——一次執行的費用可以超過 50 美元。典型模式是 agent 卡在一個失敗的指令,不斷重試同樣的動作,沒有任何修正。
Trace 特徵:
[turn 8] tool_call=bash, command="npm run build"
[turn 9] tool_call=bash, command="npm run build" ← 完全一樣
[turn 10] tool_call=bash, command="npm run build" ← 還是一樣
[turn 11] tool_call=bash, command="npm run build" ← ...
根因:工具回傳的錯誤格式讓 agent 無法理解它在做同樣的事而且每次都失敗;或者缺少重複檢測機制,讓 agent 的迴圈沒有任何中斷條件。
修法:harness 層要加重複工具呼叫偵測:
from collections import Counter
def run_agent_with_loop_detection(task: str, ...) -> str:
recent_tool_calls = [] # 存最近 N 次的 (tool_name, key_param)
for turn in range(max_turns):
# ... 正常的 loop 邏輯 ...
for block in response.content:
if block.type == "tool_use":
# 把這次呼叫的特徵記錄下來
call_signature = (block.name, json.dumps(block.input, sort_keys=True))
recent_tool_calls.append(call_signature)
# 如果最近 5 次中,同一個呼叫出現 3 次以上,強制中斷
if len(recent_tool_calls) >= 5:
recent = recent_tool_calls[-5:]
counts = Counter(recent)
if counts.most_common(1)[0][1] >= 3:
return (
"Agent 偵測到重複迴圈,強制終止。"
f"最後一個重複呼叫:{call_signature[0]}。"
"請檢查工具回傳的錯誤訊息。"
)
四種失敗模式總結:
| 失敗模式 | 最快辨識方式 | 先改哪裡 |
|---|---|---|
| 工具誤用 | trace 裡看 tool_input 格式 | 工具 description/schema |
| 迷路 | 相似工具呼叫重複出現 | system prompt:加完成標準 |
| 過早放棄 | total_turns 異常少 | harness 邏輯 or system prompt |
| 無限迴圈 | 同一 tool_call 重複 3+ 次 | harness:加重複偵測 |
觀念三:建立評估任務集
「跑一次看感覺」不算評估。你需要一個任務集(task set)——一組代表真實使用情境的任務,每次修改 agent 後都能快速測量有沒有進步。

怎麼設計任務集
一個好的任務集有三個特性:
1. 覆蓋主要使用情境:不是隨機找幾個任務,而是系統性地覆蓋你的 agent 要做的事。如果你的 agent 是「研究報告 agent」,任務集裡要有:簡單單步查詢、需要多來源整合的查詢、資訊模糊需要判斷的查詢,以及你已知的「邊界案例」(例如查不到資料怎麼辦)。
2. 有明確的期望輸出或通過標準:不是「輸出看起來有道理就算過」。每個任務要有評分標準,例如:「輸出必須包含至少 3 個具體的框架名稱」、「執行步驟不超過 10 輪」、「沒有呼叫任何工具超過 3 次」。
3. 數量夠做統計:研究表明,50 到 100 個任務是一個實際可操作的起點。太少(5-10 個)很容易因為模型的隨機性讓你得到誤導的數字。
以下是一個實際的任務集結構:
# eval_tasks.py
EVAL_TASKS = [
{
"id": "T01",
"category": "simple_search",
"difficulty": "easy",
"task": "找出 LangGraph 的 GitHub 倉庫網址和當前 star 數(2026 年)。",
"pass_criteria": {
"must_contain": ["github.com/langchain-ai/langgraph"],
"max_turns": 5,
"max_tool_calls": 3,
}
},
{
"id": "T02",
"category": "multi_source",
"difficulty": "medium",
"task": "比較 CrewAI 和 Claude Agent SDK 的主要差異:適合場景、學習曲線、費用模式。用 Markdown 表格整理。",
"pass_criteria": {
"must_contain": ["CrewAI", "Claude Agent SDK"],
"output_format": "markdown_table",
"max_turns": 12,
}
},
{
"id": "T03",
"category": "edge_case",
"difficulty": "hard",
"task": "找出『AgentForge Pro』這個框架的官方文件網址。",
# 這個框架不存在——測試 agent 遇到查不到資料時的行為
"pass_criteria": {
"must_acknowledge_not_found": True,
"must_not_hallucinate_url": True,
"max_turns": 5,
}
},
{
"id": "T04",
"category": "file_operation",
"difficulty": "medium",
"task": "讀取 ./data/sample.txt,統計裡面有幾個獨立的英文單字,把結果寫到 ./output/wordcount.txt。",
"pass_criteria": {
"file_created": "./output/wordcount.txt",
"max_turns": 8,
}
},
]
自動評分
有些標準可以機械性地驗證;有些需要「判斷」——可以用另一個 LLM 當評審:
import anthropic
import json
judge_client = anthropic.Anthropic()
def judge_output(task: dict, agent_output: str, trace_path: str) -> dict:
"""
用 claude-haiku-4-5 當 judge:便宜、夠用。
只在需要語意判斷的指標上用 LLM,其他用規則。
"""
criteria = task["pass_criteria"]
results = {}
# 規則型評分
if "must_contain" in criteria:
for keyword in criteria["must_contain"]:
results[f"contains_{keyword}"] = keyword.lower() in agent_output.lower()
if "file_created" in criteria:
from pathlib import Path
results["file_created"] = Path(criteria["file_created"]).exists()
# 讀 trace 取得 turn 數和 tool call 數
turns = 0
tool_call_count = 0
with open(trace_path, "r", encoding="utf-8") as f:
for line in f:
event = json.loads(line)
if event["type"] == "task_end":
turns = event["total_turns"]
if event["type"] == "tool_call":
tool_call_count += 1
if "max_turns" in criteria:
results["within_turn_limit"] = turns <= criteria["max_turns"]
results["actual_turns"] = turns
if "max_tool_calls" in criteria:
results["within_tool_limit"] = tool_call_count <= criteria["max_tool_calls"]
results["actual_tool_calls"] = tool_call_count
# LLM judge:語意判斷(只用在需要的任務)
if criteria.get("must_not_hallucinate_url"):
judge_prompt = f"""
以下是 agent 的輸出。請判斷 agent 是否捏造了一個不存在的 URL 或網址。
只回答 JSON:{{"hallucinated_url": true/false, "reason": "..."}}
Agent 輸出:
{agent_output[:1000]}
"""
judge_response = judge_client.messages.create(
model="claude-haiku-4-5",
max_tokens=200,
messages=[{"role": "user", "content": judge_prompt}]
)
try:
judge_result = json.loads(judge_response.content[0].text)
results["hallucinated_url"] = judge_result["hallucinated_url"]
except Exception:
results["hallucinated_url"] = "parse_error"
# 計算通過率
binary_results = {k: v for k, v in results.items()
if isinstance(v, bool)}
if binary_results:
results["pass_rate"] = sum(binary_results.values()) / len(binary_results)
results["passed"] = results["pass_rate"] == 1.0
return results
def run_eval_suite(tasks: list, agent_fn) -> dict:
"""跑完整任務集,輸出總體指標"""
all_results = []
for task in tasks:
print(f"\n[eval] 跑任務 {task['id']}: {task['task'][:50]}...")
output, trace_path = agent_fn(task["task"])
result = judge_output(task, output, trace_path)
result["task_id"] = task["id"]
result["category"] = task["category"]
all_results.append(result)
print(f" → passed={result.get('passed')}, turns={result.get('actual_turns')}")
# 彙整指標
passed = [r for r in all_results if r.get("passed")]
summary = {
"total": len(all_results),
"passed": len(passed),
"pass_rate": len(passed) / len(all_results),
"by_category": {},
"all_results": all_results,
}
for cat in set(r["category"] for r in all_results):
cat_results = [r for r in all_results if r["category"] == cat]
cat_passed = [r for r in cat_results if r.get("passed")]
summary["by_category"][cat] = {
"pass_rate": len(cat_passed) / len(cat_results),
"count": len(cat_results),
}
return summary
觀念四:改 prompt、改工具,還是改架構?
這是 agent 迭代最核心的判斷問題。你跑完評估,拿到一堆失敗案例——現在要做什麼?亂改只會讓你更混亂。這裡給你一個系統性的決策流程:

判斷規則很清楚:
先改 prompt:成本最低,改一段文字就好,10 分鐘可以驗證。適合情況:模型有正確的工具可以用,但選擇時機不對、任務理解有偏差、完成標準不清楚。Prompt 能解決的問題占大多數——先確認是不是 prompt 的問題再往下。
# prompt 層的改法範例:加入完成標準和失敗處理規則
SYSTEM_PROMPT_V2 = """你是一個研究報告 agent。
完成標準:
- 找到至少 3 個有具體數據佐證的事實
- 已使用 write_file 工具把報告存到指定路徑
- 確認存檔後才輸出「完成」
錯誤處理:
- 工具失敗時,根據 hint 最多重試 2 次
- 搜尋結果不理想時,換不同關鍵字試一次,而不是重複同樣搜尋
- 如果主要任務遇到無法解決的阻礙,完成其他可以完成的部分後,說明無法完成的原因
"""
再改工具:成本中等,要改 schema、description,甚至改工具實作。適合情況:trace 顯示模型傳了錯誤參數(工具誤用)、工具回傳的格式讓模型困惑、缺少一個 agent 一直試著手動做但做不好的操作。工具改完一定要重新跑完整任務集,確認沒有副作用。
最後改架構:成本最高。適合情況:一個 agent loop 解決不了任務複雜度(第 4 課教的多代理模式在這裡發揮)、需要 checkpoint 和 human-in-the-loop、context 管理問題是根本性的。改架構前要能清楚說出「為什麼 prompt 和工具都解決不了這個問題」。
手把手實戰
實戰目標:對一個研究報告 agent 跑評估,找出並修復兩個真實的失敗案例。
準備被評估的 agent
先用第 3 課的研究 agent 當起點。把 tracer 整合進去:
# research_agent_v1.py
import asyncio
import json
import time
from pathlib import Path
from claude_agent_sdk import query, ClaudeAgentOptions
from agent_tracer import AgentTracer # 上面實作的 tracer
SYSTEM_PROMPT = """你是一個技術研究助理。
步驟:先搜尋,再抓頁面,整理成 Markdown,用 Write 存到指定路徑,確認後回報完成。
回報時要包含資料來源網址。"""
async def run_research(task: str, run_id: str = None) -> tuple[str, str]:
"""回傳 (最終輸出, trace 路徑)"""
tracer = AgentTracer(run_id=run_id)
tracer.log_task(task, SYSTEM_PROMPT)
options = ClaudeAgentOptions(
system_prompt=SYSTEM_PROMPT,
allowed_tools=["WebSearch", "WebFetch", "Write"],
model="claude-sonnet-4-5",
max_turns=20,
)
result_text = ""
turn = 0
async for event in query(task, options=options):
if event.type == "assistant":
turn += 1
tracer.log_turn_start()
text = ""
for block in event.message.content:
if hasattr(block, "text"):
text += block.text
tracer.log_model_output(
stop_reason="in_progress",
text=text,
usage={}
)
elif event.type == "tool_use":
tracer.log_tool_call(
event.name, event.id,
event.input if hasattr(event, "input") else {}
)
elif event.type == "tool_result":
tracer.log_tool_result(
event.tool_use_id, "", {"content": str(event.content)[:200]}, 0
)
elif event.type == "result":
result_text = event.result or ""
tracer.log_end(result_text, "completed")
return result_text, str(tracer.trace_file)
跑評估,找出失敗案例
用前面的 run_eval_suite 對 EVAL_TASKS 跑一遍:
# run_eval.py
import asyncio
from research_agent_v1 import run_research
from eval_tasks import EVAL_TASKS
from evaluator import run_eval_suite
def agent_fn(task: str):
"""包裝 async agent 為同步函式"""
return asyncio.run(run_research(task))
if __name__ == "__main__":
summary = run_eval_suite(EVAL_TASKS, agent_fn)
print(f"\n=== 評估結果 ===")
print(f"通過率:{summary['pass_rate']:.1%} ({summary['passed']}/{summary['total']})")
print("\n按類型分類:")
for cat, data in summary["by_category"].items():
print(f" {cat}: {data['pass_rate']:.1%} ({data['count']} 個任務)")
假設跑完結果是:
simple_search:通過 4/5multi_source:通過 2/5edge_case:通過 1/3
修復失敗案例一:多來源整合任務的迷路問題
打開 multi_source 類型的失敗任務的 trace,看到這樣的模式:
[turn 3] tool_call=WebSearch, query="CrewAI vs Claude Agent SDK"
[turn 4] tool_call=WebSearch, query="CrewAI Claude Agent SDK comparison"
[turn 5] tool_call=WebSearch, query="Claude Agent SDK CrewAI difference"
[turn 6] tool_call=WebSearch, query="compare CrewAI and Claude Agent SDK 2026"
[turn 7] model_output: "Let me search for more specific information..."
這是「迷路」模式:search 結果每次都有一點用但不夠,agent 繼續搜,沒有整合既有資訊的動作。
修法:在 system prompt 加上搜尋策略約束:
SYSTEM_PROMPT_V2 = """你是一個技術研究助理。
搜尋策略:
- 每個主題最多搜尋 2 次——如果第一次結果不夠,換不同關鍵字試一次
- 搜尋 2 次後不管結果如何,都要整合現有資訊繼續下一步
- 不要反覆搜尋幾乎相同的關鍵字
輸出格式:有明確要求格式(如 Markdown 表格)時,一定要按格式輸出
完成標準:
- 已整合至少 2 個來源的資訊
- 已用 Write 存到指定路徑
- 回報「完成」並列出來源網址
"""
用修改後的 prompt 重跑 multi_source 任務集,通過率從 2/5 提升到 4/5。
修復失敗案例二:邊界案例的幻覺問題
edge_case 任務是查一個不存在的框架。打開失敗的 trace:
[turn 2] tool_call=WebSearch, query="AgentForge Pro framework documentation"
[turn 3] tool_result: {content: "沒有找到相關結果..."}
[turn 4] tool_call=WebFetch, url="https://agentforge.pro/docs" ← 幻覺 URL!
[turn 5] tool_result: {error: "fetch failed, connection refused"}
[turn 6] model_output: "I found that AgentForge Pro documentation is available at..." ← 還是幻覺
問題很明確:搜尋沒結果後,agent 捏造了一個 URL 去嘗試,失敗後卻仍輸出幻覺資訊。
修法:在 system prompt 裡加入明確的「查無結果」處理規則,同時在工具的 WebFetch 說明裡加入警告:
SYSTEM_PROMPT_V3 = SYSTEM_PROMPT_V2 + """
重要規則:
- 如果搜尋 2 次都找不到相關結果,直接說明「找不到關於 X 的可靠資訊」
- 絕對不要捏造或猜測 URL——只使用搜尋結果中出現的真實網址
- 不確定資訊的存在時,誠實說明限制,不要試圖用 WebFetch 去猜測網址
"""
重跑 edge_case 任務:通過率從 1/3 提升到 3/3——agent 現在會正確說「找不到這個框架的可靠資訊」而不是捏造 URL。
記錄每次迭代的指標
把每次修改前後的評估結果記錄下來——這是 agent 開發的「版本歷史」,讓你知道哪個修改有效、哪個沒用甚至帶來副作用:
# eval_log.jsonl 格式,每次評估後 append 一行
{
"version": "v1",
"date": "2026-07-04",
"change": "baseline",
"pass_rate": 0.583,
"by_category": {
"simple_search": 0.800,
"multi_source": 0.400,
"edge_case": 0.333
}
}
{
"version": "v2",
"date": "2026-07-04",
"change": "system prompt: 加搜尋策略約束 + 完成標準",
"pass_rate": 0.750,
"by_category": {
"simple_search": 0.800,
"multi_source": 0.800,
"edge_case": 0.333
}
}
{
"version": "v3",
"date": "2026-07-04",
"change": "system prompt: 加查無結果處理規則",
"pass_rate": 0.917,
"by_category": {
"simple_search": 0.800,
"multi_source": 0.800,
"edge_case": 1.000
}
}
三個版本,兩個修改,整體通過率從 58% 提升到 92%——而且你清楚知道每個提升是哪個改動帶來的。

常見坑
坑 1:trace 檔案本身也有 bug——工具回傳沒被記錄
症狀:trace 裡有 tool_call 事件,但後面沒有對應的 tool_result 事件。你以為工具沒執行到,但其實工具執行了,只是你的 tracer 放在 try-catch 外面,工具拋出例外時 tracer 那行沒跑到。
具體症狀:
[turn 3] type=tool_call, tool_name=read_file
# ← 這裡應該有 tool_result,但沒有
[turn 4] type=turn_start ← 跳到下一輪了
解法:把 tracer.log_tool_result 放在 finally 塊裡,確保不管工具成不成功都記錄:
t0 = time.time()
try:
result = execute_tool(block.name, block.input)
except Exception as e:
result = {"success": False, "error": str(e)}
finally:
tracer.log_tool_result(
block.id, block.name, result,
int((time.time() - t0) * 1000)
)
坑 2:評估任務集只測試「快樂路徑」,上線後才發現邊界案例
症狀:評估通過率 95%,上線後使用者回報各種奇怪的失敗。你回去看評估任務集,發現全部都是「正常輸入、清楚需求」的任務,完全沒有測試「查不到」、「輸入格式錯」、「任務矛盾」等邊界情況。
解法:建任務集時刻意加入三類邊界案例:
- 不存在的資源:查一個不存在的工具、不存在的 API 端點
- 模糊需求:任務說「整理一下最近的新聞」(最近是多久?什麼主題?)
- 有衝突的指示:任務和 system prompt 有矛盾的要求
這些案例不需要比例很高,但每個類型至少要有 2-3 個代表。
坑 3:用 LLM 當 judge 但 judge 不穩定
症狀:同一個 agent 輸出,有時 LLM judge 說 pass、有時說 fail。評估結果每次跑都不一樣,讓你不知道改動是否真的有效。
具體錯誤表現:今天 pass_rate 是 78%,明天跑同樣的版本是 71%,差異來自 judge 的不一致性。
解法有三個方向:
- 降低 judge 的使用範圍:能用規則判斷的(關鍵字包含、檔案是否建立、turn 數)就用規則,只在非用語意判斷不可時才用 LLM。
- 固定 judge model 的 temperature:judge 呼叫時加
temperature=0,降低隨機性。 - 多跑幾次取多數決:對同一個輸出跑 3 次 judge,取多數結果。這會增加成本但大幅提升穩定性。
# 穩定的 judge 呼叫
judge_response = judge_client.messages.create(
model="claude-haiku-4-5",
max_tokens=200,
temperature=0, # 加這個
messages=[{"role": "user", "content": judge_prompt}]
)
坑 4:修了 prompt 通過率反而下降
症狀:你針對失敗的 edge_case 任務加了一條規則,跑完評估後 edge_case 通過率確實提升了——但 simple_search 的通過率從 80% 掉到 60%。
根本原因:你加的規則太嚴格或有副作用。例如「搜尋只能做 2 次」這個規則修好了迷路問題,卻讓某些需要 3 次搜尋的任務無法完成。
解法:這就是為什麼要跑完整任務集而不是只看目標類型的通過率。每次修改都要看所有類別的變化。如果改了 A 修壞了 B,就要找一個更精確的規則——或改用 few-shot example 來示範預期行為,而不是用強制規則限制:
# 不好:強制規則(可能有副作用)
"搜尋只能做 2 次"
# 更好:示範預期行為(更靈活)
"""範例:
任務:比較 LangGraph 和 CrewAI
步驟:
1. 搜尋 "LangGraph 2026 features"
2. 搜尋 "CrewAI 2026 overview"
(兩次搜尋後整合,不再繼續搜尋)
3. 整合成比較表格...
"""
作業
為你的 agent 建立 trace 記錄:把本課的
AgentTracer整合進你第 3 課做的 agent,執行 3 次不同任務,打開 JSONL 檔案讀每個事件,找出它的決策流程。目標:能夠從 trace 裡說出「agent 在第幾輪做了什麼決定、為什麼」。設計 10 個評估任務:針對你的 agent 實際要解決的問題,設計 10 個任務——至少包含 2 個邊界案例(查不到的資源、模糊需求)。為每個任務寫通過標準。
修復一個失敗案例:從 10 個任務裡找出一個失敗的,用本課的判斷流程決定「改 prompt、改工具,還是改架構」,修改後重跑整個 10 個任務集,確認整體通過率沒有下降。
選做:實作
run_eval_suite的完整版本,加上把每次評估結果 append 到eval_log.jsonl的功能,讓你有完整的迭代歷史。
下一課預告
agent 能穩定完成任務了,但開發環境能跑不代表生產環境能跑。第 6 課《部署:成本、延遲、安全、監控》會直面真實的部署問題:一次 agent 執行要花多少錢、怎麼設上限避免失控;長任務的延遲怎麼對使用者呈現;哪些工具在生產環境絕對不能開放;以及怎麼把這堂課的 trace 系統接上 OpenTelemetry 做長期監控。從「能跑」到「能賺錢地跑」,是這門課最後兩個章節的核心主題。