Day 1 先談了為什麼 AI Agent 需要被測試、追蹤與驗證。
從今天開始進入實作。不過在 Trace、Eval、Guardrails 出現之前,我們需要先有一個最小可用的 Agent。它不用很聰明,也不用一開始就會使用工具。先做到一件事就好:
接收一個任務,呼叫 LLM,回傳答案。
這個最小 Agent 會是後面所有功能的起點。之後要記錄 trace、跑 eval、比較 prompt、加入 retry,都會圍繞這個 Agent Runner 展開。
今天的目標很單純:
User Task -> Agent Runner -> LLM Client -> Final Answer
會完成:
SimpleAgent。FakeLLMClient 來確認流程。app.py 跑一次最小 Agent。今天先不做:
如果一開始就把所有功能塞進來,很容易看不清楚系統邊界。今天先把 Agent Runner 做乾淨,後面再一層一層加功能。
在這個系列裡,Agent Runner 負責執行 Agent。
它至少要處理三件事:
目前可以先把它想成一個很薄的 wrapper:
run(user_task) -> answer
之後這個 wrapper 會慢慢長出更多責任:
run(user_task)
-> 建立 session
-> 記錄 input
-> 呼叫 LLM
-> 記錄 response
-> 驗證輸出格式
-> 評估結果
-> 回傳 answer 與 metadata
但 Day 2 先停在最小版本。
今天先建立最基本的資料夾結構:
agent-testing-platform/
app.py
agents/
__init__.py
simple_agent.py
prompts.py
fake_llm.py
各檔案先這樣分工:
| 檔案 | 用途 |
|---|---|
app.py |
程式進入點,負責接收任務並執行 Agent |
agents/__init__.py |
讓 agents 成為 Python package |
agents/simple_agent.py |
定義最小 Agent Runner |
agents/prompts.py |
放 system prompt,方便之後做 prompt versioning |
agents/fake_llm.py |
放假的 LLM client,先用來測試流程 |
今天先不串真正的 OpenAI、Claude 或 Gemini API。Day 2 的重點是 Agent Runner 的結構,不是 API 設定。介面先設計好,之後要換成真實 LLM client 會比較輕鬆。
新增 agents/prompts.py:
SYSTEM_PROMPT = """
You are a helpful AI agent.
Answer the user's task clearly and concisely.
If the task cannot be completed, explain why.
"""
這個 prompt 很普通,但剛好適合作為 baseline。
後面做 Prompt A/B Testing 時,我們會保留這個版本,再設計另一個更嚴格的 prompt,比較兩者在測試集上的差異。
所以今天雖然只寫一段 prompt,也先把版本管理的意識放進來。
新增 agents/simple_agent.py:
from dataclasses import dataclass
from typing import Protocol
from agents.prompts import SYSTEM_PROMPT
class LLMClient(Protocol):
def chat(self, messages: list[dict]) -> str:
...
@dataclass
class AgentResult:
answer: str
class SimpleAgent:
def __init__(self, llm_client: LLMClient):
self.llm_client = llm_client
def run(self, user_task: str) -> AgentResult:
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_task},
]
response = self.llm_client.chat(messages)
return AgentResult(answer=response)
這個檔案做了三件事。
第一,定義 LLMClient 介面。只要某個物件有 chat(messages) 方法,就可以被 SimpleAgent 使用。
第二,定義 AgentResult。目前只放 answer,之後再慢慢加入:
第三,定義 SimpleAgent.run()。它接收 user_task,組成 messages,呼叫 llm_client.chat(),最後回傳 AgentResult。
這裡刻意把 llm_client 從外部傳進來,而不是在 SimpleAgent 裡寫死特定 API。
這樣拆有幾個好處:
agent.run() 介面。新增 agents/fake_llm.py:
class FakeLLMClient:
def chat(self, messages: list[dict]) -> str:
user_message = messages[-1]["content"]
return f"Fake response for: {user_message}"
這個 fake client 不會真的呼叫 LLM API。
它只是取出最後一則 user message,包成一段假回覆。功能很簡單,但很適合今天的目標:確認 Agent Runner 的流程是通的。
用 fake client 的好處是:
新增 agents/__init__.py:
這個檔案今天可以先留空,不需要放任何程式碼。
它的作用是讓 Python 把 agents 資料夾視為 package,這樣 app.py 才能使用:
from agents.simple_agent import SimpleAgent
新增 app.py:
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 __name__ == "__main__":
main()
這個檔案是今天的執行入口。
流程如下:
app.py
-> 建立 FakeLLMClient
-> 建立 SimpleAgent
-> 讀取使用者輸入
-> 呼叫 agent.run(user_task)
-> 印出 AgentResult.answer
它現在還不像完整平台,但最重要的執行流程已經有了:
input -> agent.run() -> llm_client.chat() -> answer
在專案根目錄執行:
python app.py
輸入:
請簡單介紹 AI Agent
預期會看到類似結果:
Task: 請簡單介紹 AI Agent
Answer:
Fake response for: 請簡單介紹 AI Agent
如果看到這個結果,代表今天的最小 Agent Runner 已經能正常運作。
今天的實作重點是:
SimpleAgent
其中「呼叫 LLM API」在實作上可以先用 FakeLLMClient 代表 LLM client 的介面。真正接 OpenAI、Claude 或 Gemini API 可以放到後面,因為那不會改變 SimpleAgent 的主要設計。
這樣拆,是為了降低 Day 2 的複雜度。今天先確認:
等這些邊界穩定後,再把 fake client 換成真實 LLM client。
今天完成後,系統具備:
SimpleAgent
LLMClient 介面FakeLLMClient
app.py
目前還沒有:
這是刻意留下的空白。
Agent Runner 是平台的第一塊地基。只要 agent.run(user_task) 這個介面穩定,後面所有功能都可以圍繞它擴充。
今天建立了最小可用 Agent Runner。
主要設計是:
SimpleAgent.run(user_task) -> AgentResult
並且把 LLM 呼叫抽成 LLMClient 介面,讓 Agent 邏輯和模型供應商解耦。
這樣做之後:
Day 2 的目標不是做出完整 Agent,而是先把後續可以擴充的最小骨架立起來。
Day 3 會替 Agent 加上第一個工具。
我會先實作一個 calculator tool,讓 Agent 能處理需要明確計算的任務。
加入工具之後,Agent 的執行流程就會從:
User Task -> LLM Client -> Final Answer
變成:
User Task -> LLM Client -> Tool Call -> Tool Result -> Final Answer
這會讓後面的 Trace 更有意思,因為我們不只要記錄模型回答,還要記錄工具呼叫與工具結果。