iT邦幫忙

2026 iThome 鐵人賽

DAY 2
0

前言

Day 1 先談了為什麼 AI Agent 需要被測試、追蹤與驗證。

從今天開始進入實作。不過在 Trace、Eval、Guardrails 出現之前,我們需要先有一個最小可用的 Agent。它不用很聰明,也不用一開始就會使用工具。先做到一件事就好:

接收一個任務,呼叫 LLM,回傳答案。

這個最小 Agent 會是後面所有功能的起點。之後要記錄 trace、跑 eval、比較 prompt、加入 retry,都會圍繞這個 Agent Runner 展開。


今天要完成什麼?

今天的目標很單純:

User Task -> Agent Runner -> LLM Client -> Final Answer

會完成:

  1. 建立最小專案結構。
  2. 建立 baseline system prompt。
  3. 實作 SimpleAgent
  4. 實作一個 FakeLLMClient 來確認流程。
  5. app.py 跑一次最小 Agent。

今天先不做:

  • Tool Calling
  • Trace Recorder
  • SQLite
  • Evaluation
  • Guardrails

如果一開始就把所有功能塞進來,很容易看不清楚系統邊界。今天先把 Agent Runner 做乾淨,後面再一層一層加功能。


什麼是 Agent Runner?

在這個系列裡,Agent Runner 負責執行 Agent。

它至少要處理三件事:

  1. 接收使用者任務。
  2. 組合 prompt。
  3. 呼叫 LLM 並回傳結果。

目前可以先把它想成一個很薄的 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 會比較輕鬆。


建立 baseline system prompt

新增 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,也先把版本管理的意識放進來。


建立 SimpleAgent

新增 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,之後再慢慢加入:

  • session id
  • latency
  • token usage
  • status
  • error
  • trace steps

第三,定義 SimpleAgent.run()。它接收 user_task,組成 messages,呼叫 llm_client.chat(),最後回傳 AgentResult

這裡刻意把 llm_client 從外部傳進來,而不是在 SimpleAgent 裡寫死特定 API。

這樣拆有幾個好處:

  • 之後可以替換不同 LLM provider。
  • 可以用 fake LLM 測試流程。
  • 避免 Agent 邏輯和 API 細節混在一起。
  • 後續 evaluation runner 只要呼叫同一個 agent.run() 介面。

建立 Fake LLM Client

新增 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 的好處是:

  • 不需要 API key。
  • 不會產生成本。
  • 輸出穩定,方便測試。
  • 可以先專注在架構,不被模型回覆品質干擾。

建立 package 初始化檔

新增 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 已經能正常運作。


為什麼今天先不用真實 LLM API?

今天的實作重點是:

  • 建立 SimpleAgent
  • 設計基本 system prompt
  • 呼叫 LLM API
  • 輸入 user task
  • 顯示 final answer

其中「呼叫 LLM API」在實作上可以先用 FakeLLMClient 代表 LLM client 的介面。真正接 OpenAI、Claude 或 Gemini API 可以放到後面,因為那不會改變 SimpleAgent 的主要設計。

這樣拆,是為了降低 Day 2 的複雜度。今天先確認:

  • Agent 如何被呼叫?
  • Prompt 放在哪裡?
  • LLM client 介面長什麼樣?
  • 回傳結果格式怎麼設計?

等這些邊界穩定後,再把 fake client 換成真實 LLM client。


今天完成後的系統狀態

今天完成後,系統具備:

  • 一個最小的 SimpleAgent
  • 一個可替換的 LLMClient 介面
  • 一個 baseline system prompt
  • 一個 FakeLLMClient
  • 一個命令列入口 app.py

目前還沒有:

  • 工具呼叫
  • trace 紀錄
  • 資料庫
  • 自動評測
  • dashboard

這是刻意留下的空白。

Agent Runner 是平台的第一塊地基。只要 agent.run(user_task) 這個介面穩定,後面所有功能都可以圍繞它擴充。


今天的重點整理

今天建立了最小可用 Agent Runner。

主要設計是:

SimpleAgent.run(user_task) -> AgentResult

並且把 LLM 呼叫抽成 LLMClient 介面,讓 Agent 邏輯和模型供應商解耦。

這樣做之後:

  • 後續可以替換不同模型。
  • 可以用 fake LLM 測試流程。
  • 可以在 LLM client 外層加入 trace 紀錄。
  • evaluation runner 也能重複呼叫同一個 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 更有意思,因為我們不只要記錄模型回答,還要記錄工具呼叫與工具結果。


上一篇
Day 1|為什麼 AI Agent 需要被測試?
下一篇
Day 3|替 Agent 加上第一個工具
系列文
從黑盒到可驗證:30 天打造 AI Agent 的 Trace、Eval 與 Guardrails 系統8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言