Day 3 我們替 Agent 加上了第一個工具 calculator。
目前 Agent 已經可以根據任務呼叫工具,例如:
User Task
-> LLM Client
-> Tool Call
-> Tool Result
-> Final Answer
但現在還有一個問題:這些過程只會印在終端機上,程式執行完就消失。
如果 Agent 回答錯誤,我們很難回頭檢查:
所以 Day 4 要開始處理這個系列的第一個重點:Trace。
Trace 要做的事很單純:
把一次 Agent 執行過程中的重要步驟記錄下來,讓 Agent 不再只是輸入與輸出的黑盒。
今天要設計一個最小可用的 Trace 格式,並讓 SimpleAgent 在執行時產生 trace。
會完成:
TraceStep。AgentTrace。SimpleAgent,在執行過程中記錄 step。app.py,把 trace 印出來。今天先不做:
這些放到後面幾天。今天先把資料格式設計好。
Trace 可以想成 Agent 的執行紀錄。
如果只看 final answer,我們只會知道:
Answer:
The result is 3780
但如果有 trace,我們可以看到:
Step 1: user_input
使用者要求計算 135 * 28
Step 2: llm_response
LLM Client 判斷需要呼叫 calculator
Step 3: tool_call
呼叫 calculator,輸入 135 * 28
Step 4: tool_result
calculator 回傳 3780
Step 5: final_answer
Agent 回答 The result is 3780
這些資訊對後面的除錯與評測很有用。
例如未來某次測試失敗時,如果只看到:
Expected: 3780
Actual: The result is 13528
我們不知道錯在哪裡。
但如果 trace 顯示:
tool_input: "13528"
就可以知道問題可能出在「LLM Client 抽取算式時把 135 * 28 解析錯了」,而不是 calculator 算錯。
這就是 Trace 派上用場的地方。
今天新增 tracing/ 資料夾,專門放 trace 相關資料模型。
agent-testing-platform/
app.py
agents/
__init__.py
simple_agent.py
prompts.py
fake_llm.py
tools/
__init__.py
calculator.py
tracing/
__init__.py
models.py
新增:
| 檔案 | 用途 |
|---|---|
tracing/__init__.py |
讓 tracing 成為 Python package |
tracing/models.py |
定義 TraceStep 與 AgentTrace |
修改:
| 檔案 | 修改內容 |
|---|---|
agents/simple_agent.py |
在 Agent 執行過程中建立 trace |
app.py |
顯示 trace JSON |
新增 tracing/models.py:
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any
from uuid import uuid4
@dataclass
class TraceStep:
step_type: str
name: str
input_data: dict[str, Any] = field(default_factory=dict)
output_data: dict[str, Any] = field(default_factory=dict)
error: str | None = None
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
TraceStep 代表 Agent 執行過程中的一個步驟。
這裡先放幾個欄位:
| 欄位 | 說明 |
|---|---|
step_type |
步驟類型,例如 user_input、llm_response、tool_call |
name |
步驟名稱,方便閱讀 |
input_data |
這一步的輸入資料 |
output_data |
這一步的輸出資料 |
error |
如果這一步失敗,記錄錯誤訊息 |
timestamp |
這一步發生的時間 |
這個設計先保持簡單。
等後面接上 SQLite,這些欄位會變成資料表中的欄位。
繼續修改 tracing/models.py,在 TraceStep 下方新增 AgentTrace:
@dataclass
class AgentTrace:
session_id: str = field(default_factory=lambda: str(uuid4()))
steps: list[TraceStep] = field(default_factory=list)
def add_step(
self,
step_type: str,
name: str,
input_data: dict[str, Any] | None = None,
output_data: dict[str, Any] | None = None,
error: str | None = None,
) -> None:
self.steps.append(
TraceStep(
step_type=step_type,
name=name,
input_data=input_data or {},
output_data=output_data or {},
error=error,
)
)
def to_dict(self) -> dict[str, Any]:
return {
"session_id": self.session_id,
"steps": [
{
"step_type": step.step_type,
"name": step.name,
"input_data": step.input_data,
"output_data": step.output_data,
"error": step.error,
"timestamp": step.timestamp,
}
for step in self.steps
],
}
AgentTrace 代表一次 Agent 執行。
它有兩個主要欄位:
session_id:每次執行都有一個唯一 ID。steps:這次執行中的所有步驟。add_step() 是輔助方法,讓我們可以用比較乾淨的方式加入步驟。
例如:
trace.add_step(
step_type="user_input",
name="User Task",
input_data={"content": user_task},
)
to_dict() 則留給後面輸出 JSON 或存進資料庫時使用。
新增 tracing/__init__.py:
這個檔案今天可以先留空,不需要放任何程式碼。
它的用途是讓 Python 把 tracing 資料夾視為 package,這樣其他檔案才能匯入:
from tracing.models import AgentTrace
接著修改 agents/simple_agent.py:
from dataclasses import dataclass, field
from typing import Protocol
from agents.prompts import SYSTEM_PROMPT
from tools.calculator import calculator
from tracing.models import AgentTrace
class LLMClient(Protocol):
def chat(self, messages: list[dict]) -> dict:
...
@dataclass
class ToolCallRecord:
tool_name: str
tool_input: str
tool_output: str
@dataclass
class AgentResult:
answer: str
tool_calls: list[ToolCallRecord] = field(default_factory=list)
trace: AgentTrace = field(default_factory=AgentTrace)
class SimpleAgent:
def __init__(self, llm_client: LLMClient):
self.llm_client = llm_client
self.tools = {
"calculator": calculator,
}
def run(self, user_task: str) -> AgentResult:
trace = AgentTrace()
trace.add_step(
step_type="user_input",
name="User Task",
input_data={"content": user_task},
)
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_task},
]
response = self.llm_client.chat(messages)
trace.add_step(
step_type="llm_response",
name="LLM Response",
input_data={"messages": messages},
output_data={"response": response},
)
if response["type"] == "final_answer":
answer = response["content"]
trace.add_step(
step_type="final_answer",
name="Final Answer",
output_data={"answer": answer},
)
return AgentResult(answer=answer, trace=trace)
if response["type"] == "tool_call":
return self._handle_tool_call(response, trace)
trace.add_step(
step_type="error",
name="Unknown Response Type",
input_data={"response": response},
error=f"Unknown response type: {response['type']}",
)
raise ValueError(f"Unknown response type: {response['type']}")
def _handle_tool_call(self, response: dict, trace: AgentTrace) -> AgentResult:
tool_name = response["tool_name"]
tool_input = response["tool_input"]
trace.add_step(
step_type="tool_call",
name="Tool Call",
input_data={
"tool_name": tool_name,
"tool_input": tool_input,
},
)
if tool_name not in self.tools:
trace.add_step(
step_type="error",
name="Unknown Tool",
input_data={"tool_name": tool_name},
error=f"Unknown tool: {tool_name}",
)
raise ValueError(f"Unknown tool: {tool_name}")
tool_output = self.tools[tool_name](tool_input)
trace.add_step(
step_type="tool_result",
name="Tool Result",
input_data={"tool_name": tool_name},
output_data={"tool_output": tool_output},
)
tool_call = ToolCallRecord(
tool_name=tool_name,
tool_input=tool_input,
tool_output=tool_output,
)
answer = f"The result is {tool_output}"
trace.add_step(
step_type="final_answer",
name="Final Answer",
output_data={"answer": answer},
)
return AgentResult(
answer=answer,
tool_calls=[tool_call],
trace=trace,
)
這次主要改了幾個地方。
第一,AgentResult 多了一個 trace 欄位:
trace: AgentTrace = field(default_factory=AgentTrace)
也就是說,每次 Agent 執行完成後,不只會回傳答案,也會回傳執行紀錄。
第二,在 run() 一開始建立 trace:
trace = AgentTrace()
然後立刻記錄使用者輸入:
trace.add_step(
step_type="user_input",
name="User Task",
input_data={"content": user_task},
)
第三,呼叫 LLM Client 後,也把 response 記錄起來。
Agent 後續行為是根據 LLM response 決定的。如果沒有記錄這一步,之後就很難知道 Agent 為什麼會呼叫某個工具。
第四,工具呼叫被拆成兩個 step:
tool_call:記錄工具名稱與工具輸入。tool_result:記錄工具執行後的輸出。未來如果工具結果錯誤,我們可以分辨是「工具輸入錯」還是「工具執行錯」。
最後修改 app.py:
import json
from agents.fake_llm import FakeLLMClient
from agents.simple_agent import SimpleAgent
def main():
agent = SimpleAgent(llm_client=FakeLLMClient())
user_task = input("Task: ")
result = agent.run(user_task)
print("\nAnswer:")
print(result.answer)
if result.tool_calls:
print("\nTool Calls:")
for tool_call in result.tool_calls:
print(f"- tool_name: {tool_call.tool_name}")
print(f" tool_input: {tool_call.tool_input}")
print(f" tool_output: {tool_call.tool_output}")
print("\nTrace:")
print(json.dumps(result.trace.to_dict(), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
這次 app.py 多做一件事:
print(json.dumps(result.trace.to_dict(), ensure_ascii=False, indent=2))
ensure_ascii=False 是為了讓中文正常顯示,不會被轉成 Unicode escape。
indent=2 則是讓 JSON 比較好閱讀。
在專案根目錄執行:
python3 app.py
輸入:
請計算 135 * 28
預期會看到類似結果:
Task: 請計算 135 * 28
Answer:
The result is 3780
Tool Calls:
- tool_name: calculator
tool_input: 135 * 28
tool_output: 3780
Trace:
{
"session_id": "e76471be-7f4c-460c-80c9-f42dddfb9c18",
"steps": [
{
"step_type": "user_input",
"name": "User Task",
"input_data": {
"content": "請計算 135 * 28"
},
"output_data": {},
"error": null,
"timestamp": "2026-09-05T10:30:00.000000"
},
{
"step_type": "llm_response",
"name": "LLM Response",
"input_data": {
"messages": [
{
"role": "system",
"content": "..."
},
{
"role": "user",
"content": "請計算 135 * 28"
}
]
},
"output_data": {
"response": {
"type": "tool_call",
"tool_name": "calculator",
"tool_input": "135 * 28"
}
},
"error": null,
"timestamp": "2026-09-05T10:30:00.000000"
}
]
}
實際輸出的 session_id 和 timestamp 每次都會不同,這是正常的。
後面還會有 tool_call、tool_result、final_answer 等 step。上面的範例只截取前幾段,避免文章太長。
假設今天輸入:
請計算 135 28
這不是一個合法算式,因為中間沒有 +、-、* 或 /。
依照 Day 3 的 FakeLLMClient,它會嘗試抽取算式,但找不到符合格式的內容,最後丟出錯誤。
如果沒有 trace,我們可能只看到:
ValueError: No arithmetic expression found
有了 trace 設計後,我們就知道接下來應該把錯誤也記錄成 step。
目前 Day 4 的錯誤紀錄還不完整,這是刻意留下來的範圍。後面做 Failure Analysis 時,我們會把錯誤整理成更穩定的分類,例如:
今天先把正常執行流程記錄起來。
今天完成後,系統具備:
TraceStep 資料模型。AgentTrace 資料模型。session_id。user_input。llm_response。tool_call。tool_result。final_answer。app.py 可以把 trace 以 JSON 形式印出。目前還沒有:
今天開始把 Agent 從黑盒往可觀察的系統推進。
Day 2 的 Agent 只能回傳答案。
Day 3 的 Agent 可以呼叫工具。
Day 4 的 Agent 開始能說清楚自己執行過程中發生了哪些步驟。
目前最重要的設計是:
AgentTrace
-> session_id
-> steps[]
TraceStep
-> step_type
-> name
-> input_data
-> output_data
-> error
-> timestamp
有了這個結構,後面才有辦法做:
Day 5 會把今天的 Trace 寫進 SQLite。
目前 trace 只存在記憶體中,程式執行結束就會消失。下一篇會建立最小資料庫結構,包含:
sessions tablesteps table讓每次 Agent 執行都能被保存下來。之後才可以查詢歷史紀錄、做 Trace Viewer,並和 Eval 結果連結。