AG Day 9 觀測基礎:token、延遲與成本紀錄
執行需求:CPU+API key。如果暫時沒有 API key,本篇文章提供完整的離線 mock 與 dry-run 模式,不需要外部連線也能在本機完整驗證觀測資料庫建立、遙測封裝、指標計算與日誌審查流程。
引言
在上一篇 AG Day 8(原文連結)中,我們成功為「研究助理(research-agent)」建立了基於 Pydantic v2 與 JSON Schema 的強型別資料契約,消除了自然語言輸出的語法模糊性。然而,當我們開始讓代理自主執行多輪搜尋、多次呼叫工具並反覆抽取論點時,另一個更嚴肅的維運挑戰隨之而來:整個執行流程如同一個黑盒子。
傳統軟體系統在發生故障時,通常伴隨著明確的例外堆疊追蹤(Stack Trace)與 HTTP 狀態碼;但 AI Agent 的失敗往往無聲無息。例如,代理可能陷入了無效搜尋的死迴圈,在幾分鐘內發送了數十次請求,默默消耗了數十萬個權杖(token);或者某個外部工具的網路延遲飆升,導致整個分析任務嚴重逾時,而開發者卻無法得知時間究竟是耗費在模型思考、網路傳輸還是地端資料庫查詢;更嚴重的是,如果沒有精準的計費追蹤,營運成本可能會在不知不覺中失控超支。
要將原型概念轉化為可受控、可信賴的工程系統,我們必須在系統初期就植入可觀測性(Observability)機制。可觀測性的三大基石是權杖消耗(Tokens)、回應延遲(Latency)與財務成本(Cost)。在本文中,我們將在貫穿專案 research-agent 內部建立輕量、零外部服務依賴的本機觀測架構,利用 SQLite 資料庫(data/knowledge.db)的 runs 與 events 資料表,完整記錄代理每一步的生命週期事件,為後續邁向複雜的圖編排架構提供堅實的數據支撐。
為什麼需要觀測:代理黑盒子的維運挑戰
在傳統單次請求的對話介面中,觀測通常只需要記錄一次 HTTP 請求的狀態碼與往返時間。但在以 Agent 為核心的架構中,一次使用者任務的背後往往是由數個、甚至數十個連續的推理與工具互動所構成的狀態序列。這種分散、多步且具備自律分支特性的執行模式,帶來了三大關鍵痛點:
- 權杖消耗的隱蔽累積:在多輪對話中,每一次將歷史訊息重新送入模型時,輸入權杖量會隨著對話輪數呈線性甚至指數級增長。若文獻檢索節點帶入了龐大的網頁內容,一次呼叫就可能吃掉數萬個輸入權杖。如果沒有逐次回合的細緻度量,工程師很難發現究竟是哪一個步驟成為了「權杖吞噬怪獸」。
- 非均質延遲的定位困境:代理執行總耗時往往是由多個非同步環節疊加而成:包含模型首次權杖時間(Time to First Token, TTFT)、完整權杖解碼時間、網路傳輸耗時、以及地端執行搜尋、檔案讀寫或向量資料庫檢索的時間。若只記錄端到端總時間,一旦使用者反映「系統很慢」,維運團隊將完全無法區分是模型供應商的服務降級,還是地端 SQLite 查詢遭遇到鎖定阻塞。
- 非確定性推論的計費審計:不同模型(例如高階推理模型與輕量化模型)在計費價格上存在顯著差異,且快取命中權杖(Cached Tokens)與常規權杖的單價亦大不相同。建立即時的成本模型,不僅能提供清楚的營運帳單,更是實施「權杖配額熔斷(Circuit Breaker)」防範死迴圈無限花錢的前提條件。
三大觀測維度:Token、延遲與成本的計量原理
要建立專業的觀測架構,首先必須釐清這三個維度的底層定義與統計方式:
第一維度:權杖(Token)計量。主流大型語言模型 API 在其回應物件中均會附帶 usage 欄位,主要包含三項指標:輸入權杖數(prompt_tokens)、輸出權杖數(completion_tokens)以及總權杖數(total_tokens)。在 2025 年之後的主流商用 API 中,提示詞快取(Prompt Caching)已成為標準能力,usage 物件通常會額外提供 prompt_tokens_details.cached_tokens 欄位。精確區分快取命中數對於成本計算與效能評估至關重要。
第二維度:延遲(Latency)度量。在 Python 中測量精確的時間差,應始終採用 time.perf_counter(),而非容易受到系統時鐘調整影響的 time.time()。單次呼叫的延遲可細分為兩部分:連線建立至首個字元抵達的延遲(反映模型排隊與上下文載入速度),以及從首字到整體串流結束的解碼延遲(反映模型的生成吞吐率)。對於非串流呼叫,我們則精確度量從發起網路請求到完整物件反序列化完成的總毫秒數(ms)。
第三維度:成本(Cost)折算。成本計算並非寫死在程式碼中的常數,而是根據當前運作模型動態載入的計價設定檔。一般以每百萬權杖(Per Million Tokens, MTok)為單位進行換算。具體計價公式為:總成本 = (未快取輸入權杖 × 輸入單價) + (快取輸入權杖 × 快取單價) + (輸出權杖 × 輸出單價)。需要強調的是,各大模型供應商的定價策略隨市場動態調整,系統應提供外部設定注入介面,具體費率一律以各家官方文件為準。
資料儲存設計:runs 與 events 的階層架構
在我們的貫穿專案 research-agent 中,資料庫統一規劃在 data/knowledge.db(SQLite)。SPEC 規格中明確定義了兩張核心觀測資料表:runs 與 events。它們構成了經典的「父任務-子事件」階層關聯:
runs資料表(執行總體層級):記錄一次完整研究任務的生命週期。欄位包含全域唯一任務識別碼(run_id)、研究主題描述(topic)、執行狀態(status:如 RUNNING、COMPLETED、FAILED)、總輸入權杖、總輸出權杖、累計成本(美元)、總延遲時間(秒)、以及建立與完成時間戳記。events資料表(細粒度步階層級):記錄代理在該次執行中每一次與模型互動或工具調度的原子事件。欄位包含事件識別碼(event_id)、所屬任務識別碼(run_id)、執行步數(step_number)、事件型態(event_type:如 LLM_CALL、TOOL_EXECUTION)、使用模型名稱(model)、該步消耗的輸入與輸出權杖、該步耗時(毫秒)、該步計算之成本、以及儲存詳細入參和出參的 JSON 欄位(metadata_json)。
這種階層式資料模型使我們既能在巨觀上審視某次任務的總開銷,又能在微觀上逐步重播代理的思維軌跡與每一步的效能開銷。
完整實作:打造輕量級觀測封裝與事件儲存庫
現在,我們進入具體實作。我們將在 src/research_agent/ 模組目錄下,建立資料庫結構宣告、觀測記錄器以及兼具 API 與離線 dry-run 模式的度量封裝函式。
第一步,我們在 src/research_agent/storage.py 中實作資料表初始化邏輯。我們啟用 SQLite 的 WAL(Write-Ahead Logging)模式,以確保在多步驟並行記錄時具備極佳的並行寫入效能:
"""src/research_agent/storage.py:觀測資料表初始化與資料庫操作。"""
import sqlite3
from pathlib import Path
DEFAULT_DB_PATH = Path("data/knowledge.db")
def get_db_connection(db_path: Path = DEFAULT_DB_PATH) -> sqlite3.Connection:
"""取得已設定 WAL 模式與字典回傳的資料庫連線。"""
db_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(db_path))
# 啟用 WAL 模式以大幅改善多工寫入效能
conn.execute("PRAGMA journal_mode = WAL;")
conn.row_factory = sqlite3.Row
return conn
def init_observability_tables(conn: sqlite3.Connection) -> None:
"""初始化 runs 與 events 觀測資料表結構。"""
with conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS runs (
run_id TEXT PRIMARY KEY,
topic TEXT NOT NULL,
status TEXT NOT NULL,
total_prompt_tokens INTEGER DEFAULT 0,
total_completion_tokens INTEGER DEFAULT 0,
total_cost REAL DEFAULT 0.0,
total_latency_seconds REAL DEFAULT 0.0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
finished_at TIMESTAMP
);
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS events (
event_id TEXT PRIMARY KEY,
run_id TEXT NOT NULL,
step_number INTEGER NOT NULL,
event_type TEXT NOT NULL,
model TEXT,
prompt_tokens INTEGER DEFAULT 0,
completion_tokens INTEGER DEFAULT 0,
latency_ms REAL NOT NULL,
cost REAL DEFAULT 0.0,
metadata_json TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (run_id) REFERENCES runs(run_id)
);
""")
# 針對關聯查詢建立索引
conn.execute("CREATE INDEX IF NOT EXISTS idx_events_run_id ON events(run_id);")
第二步,我們在 src/research_agent/llm.py 中建立定價字典與計費計算器。請注意,此處數值僅供工程計算演示,實務上各模型最新單價一律以官方文件為準:
"""src/research_agent/llm.py:成本模型與費率計算器。"""
from dataclasses import dataclass
from typing import Dict, Tuple
# 模型定價資料結構(單位:美元 / 每百萬權杖 MTok)
# 注意:各家模型定價會隨時間調整,實務成本一律以官方文件為準
MODEL_PRICING: Dict[str, Tuple[float, float, float]] = {
# 格式: 模型名稱: (輸入單價, 快取輸入單價, 輸出單價)
"gpt-4o-mini": (0.15, 0.075, 0.60),
"gpt-4o": (2.50, 1.25, 10.00),
"claude-3-5-sonnet": (3.00, 0.30, 15.00),
}
@dataclass
class UsageMetrics:
"""單次執行的度量統計資料封裝。"""
prompt_tokens: int = 0
completion_tokens: int = 0
cached_tokens: int = 0
latency_ms: float = 0.0
cost_usd: float = 0.0
def calculate_cost(
model: str,
prompt_tokens: int,
completion_tokens: int,
cached_tokens: int = 0,
) -> float:
"""依據模型與權杖量計算單次呼叫的估計成本(美元)。"""
pricing = MODEL_PRICING.get(model, (0.50, 0.25, 2.00))
input_rate, cache_rate, output_rate = pricing
regular_prompt_tokens = max(0, prompt_tokens - cached_tokens)
cost = (
(regular_prompt_tokens / 1_000_000.0) * input_rate
+ (cached_tokens / 1_000_000.0) * cache_rate
+ (completion_tokens / 1_000_000.0) * output_rate
)
return round(cost, 6)
第三步,實作觀測裝飾器與事件記錄器 TelemetryRecorder。該類別負責封裝與 SQLite 資料庫的溝通,並能將單次呼叫的度量即時寫入 events,同時原子性地累積至 runs 摘要:
"""實作執行生命週期與事件追蹤記錄器。"""
import uuid
import json
import sqlite3
from typing import Optional, Dict, Any
class TelemetryRecorder:
"""負責將代理執行的每一步驟寫入本地 SQLite 觀測庫。"""
def __init__(self, conn: sqlite3.Connection, run_id: str):
self.conn = conn
self.run_id = run_id
self._step_counter = 0
def start_run(self, topic: str) -> None:
"""開啟新任務記錄。"""
with self.conn:
self.conn.execute(
"INSERT INTO runs (run_id, topic, status) VALUES (?, ?, ?);",
(self.run_id, topic, "RUNNING"),
)
def record_event(
self,
event_type: str,
latency_ms: float,
model: Optional[str] = None,
prompt_tokens: int = 0,
completion_tokens: int = 0,
cached_tokens: int = 0,
metadata: Optional[Dict[str, Any]] = None,
) -> UsageMetrics:
"""記錄單一步階事件,並累加總指標。"""
self._step_counter += 1
event_id = str(uuid.uuid4())
cost = calculate_cost(model or "default", prompt_tokens, completion_tokens, cached_tokens)
meta_str = json.dumps(metadata or {}, ensure_ascii=False)
with self.conn:
# 寫入原子事件
self.conn.execute(
"""
INSERT INTO events (
event_id, run_id, step_number, event_type, model,
prompt_tokens, completion_tokens, latency_ms, cost, metadata_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
""",
(
event_id, self.run_id, self._step_counter, event_type, model,
prompt_tokens, completion_tokens, latency_ms, cost, meta_str
),
)
# 原子更新母任務的統計總和
self.conn.execute(
"""
UPDATE runs SET
total_prompt_tokens = total_prompt_tokens + ?,
total_completion_tokens = total_completion_tokens + ?,
total_cost = total_cost + ?,
total_latency_seconds = total_latency_seconds + (? / 1000.0)
WHERE run_id = ?;
""",
(prompt_tokens, completion_tokens, cost, latency_ms, self.run_id),
)
return UsageMetrics(
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
cached_tokens=cached_tokens,
latency_ms=latency_ms,
cost_usd=cost,
)
def finish_run(self, status: str = "COMPLETED") -> None:
"""完成任務並標註時間戳記。"""
with self.conn:
self.conn.execute(
"UPDATE runs SET status = ?, finished_at = CURRENT_TIMESTAMP WHERE run_id = ?;",
(status, self.run_id),
)
第四步,實作具備觀測能力的 LLM 呼叫包裝函式 monitored_llm_call()。我們透過 Python 高精度的 time.perf_counter() 量測耗時,並支援 --dry-run 模式,讓沒有 API key 的讀者在離線環境下也能產出逼真的度量資料:
"""實作具備自動度量採集的高階模型呼叫包裝。"""
import os
import time
from typing import Tuple, Dict, Any
def monitored_llm_call(
prompt: str,
system_prompt: str,
recorder: TelemetryRecorder,
dry_run: bool = False,
) -> Tuple[str, UsageMetrics]:
"""執行模型呼叫,並自動將 token、延遲與費用記入觀測系統。"""
model_name = os.getenv("RESEARCH_AGENT_MODEL", "gpt-4o-mini")
api_key = os.getenv("OPENAI_API_KEY")
# 檢查是否走入離線 dry-run 模式
if dry_run or not api_key:
print("[提示] 啟用觀測離線 Mock 模式,產生模擬遙測指標。")
simulated_start = time.perf_counter()
# 模擬少許計算耗時
time.sleep(0.05)
simulated_latency = (time.perf_counter() - simulated_start) * 1000.0
simulated_text = "模擬回應:已成功分析研究主題,並提取相關技術架構論點。"
metrics = recorder.record_event(
event_type="LLM_CALL",
latency_ms=simulated_latency,
model=model_name,
prompt_tokens=420,
completion_tokens=180,
cached_tokens=128,
metadata={"mode": "dry-run", "prompt_snippet": prompt[:40]},
)
return simulated_text, metrics
from openai import OpenAI
client = OpenAI(api_key=api_key)
start_time = time.perf_counter()
response = client.chat.completions.create(
model=model_name,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt},
],
)
latency_ms = (time.perf_counter() - start_time) * 1000.0
output_text = response.choices[0].message.content or ""
usage = response.usage
prompt_tokens = usage.prompt_tokens if usage else 0
completion_tokens = usage.completion_tokens if usage else 0
cached_tokens = 0
if usage and hasattr(usage, "prompt_tokens_details") and usage.prompt_tokens_details:
cached_tokens = getattr(usage.prompt_tokens_details, "cached_tokens", 0)
metrics = recorder.record_event(
event_type="LLM_CALL",
latency_ms=latency_ms,
model=model_name,
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
cached_tokens=cached_tokens,
metadata={"prompt_snippet": prompt[:40]},
)
return output_text, metrics
第五步,將整套流程整合為一個多步驟的研究代理任務模擬腳本,展示從建立任務、多步推理、記錄事件到最後查詢 SQLite 匯總報表的完整工作流:
"""執行多步代理工作流並查詢觀測資料庫。"""
import uuid
from src.research_agent.storage import get_db_connection, init_observability_tables
from src.research_agent.llm import TelemetryRecorder, monitored_llm_call
# 1. 初始化本地資料庫
conn = get_db_connection()
init_observability_tables(conn)
# 2. 建立新任務記錄
task_run_id = f"run_{uuid.uuid4().hex[:8]}"
recorder = TelemetryRecorder(conn, task_run_id)
recorder.start_run(topic="評估 LangGraph 狀態檢查點的持久化效能")
print(f"=== 開始執行研究任務 [ID: {task_run_id}] ===")
# 模擬步驟一:搜尋規劃
print("\n--- 步驟 1: 擬定檢索關鍵字 ---")
_, step1_metrics = monitored_llm_call(
prompt="請針對 LangGraph 狀態檢查點擬定三組檢索詞。",
system_prompt="你是一位研究助理規劃專家。",
recorder=recorder,
dry_run=True,
)
print(f" 耗時: {step1_metrics.latency_ms:.2f} ms | 權杖: {step1_metrics.prompt_tokens} in / {step1_metrics.completion_tokens} out")
print(f" 花費: ${step1_metrics.cost_usd:.6f}")
# 模擬步驟二:文獻摘要提煉
print("\n--- 步驟 2: 提煉文獻論點 ---")
_, step2_metrics = monitored_llm_call(
prompt="請將檢索到的資料整合為結構化論點。",
system_prompt="你是一位嚴謹的文獻萃取助理。",
recorder=recorder,
dry_run=True,
)
print(f" 耗時: {step2_metrics.latency_ms:.2f} ms | 權杖: {step2_metrics.prompt_tokens} in / {step2_metrics.completion_tokens} out")
print(f" 花費: ${step2_metrics.cost_usd:.6f}")
# 3. 標註任務結束
recorder.finish_run(status="COMPLETED")
# 4. 查詢母表統計匯總
run_summary = conn.execute(
"SELECT * FROM runs WHERE run_id = ?;", (task_run_id,)
).fetchone()
print("\n" + "=" * 20 + " 任務執行觀測報表 " + "=" * 20)
print(f"任務狀態: {run_summary['status']}")
print(f"總耗時: {run_summary['total_latency_seconds']:.3f} 秒")
print(f"累計輸入權杖: {run_summary['total_prompt_tokens']}")
print(f"累計輸出權杖: {run_summary['total_completion_tokens']}")
print(f"總估計成本: ${run_summary['total_cost']:.6f} 美元")
# 輸出(範例輸出):
# === 開始執行研究任務 [ID: run_a1b2c3d4] ===
#
# --- 步驟 1: 擬定檢索關鍵字 ---
# 耗時: 52.30 ms | 權杖: 420 in / 180 out
# 花費: $0.000161
#
# --- 步驟 2: 提煉文獻論點 ---
# 耗時: 51.80 ms | 權杖: 420 in / 180 out
# 花費: $0.000161
#
# ==================== 任務執行觀測報表 ====================
# 任務狀態: COMPLETED
# 總耗時: 0.104 秒
# 累計輸入權杖: 840
# 累計輸出權杖: 360
# 總估計成本: $0.000322 美元
第六步,我們為觀測模組編寫一組自動化單元測試 tests/test_observability.py,使用 pytest 驗證資料表寫入正確性、權杖加總邏輯與計費公式運算:
"""tests/test_observability.py:驗證觀測資料庫與指標計算之正確性。"""
import sqlite3
import pytest
from src.research_agent.storage import init_observability_tables
from src.research_agent.llm import calculate_cost, TelemetryRecorder
@pytest.fixture
def memory_db():
"""建立暫存記憶體資料庫供單元測試獨立使用。"""
conn = sqlite3.connect(":memory:")
conn.row_factory = sqlite3.Row
init_observability_tables(conn)
yield conn
conn.close()
def test_calculate_cost_with_cache():
"""測試在快取命中時的精確計費折算。"""
# gpt-4o-mini 定價:未快取輸入 0.15/M, 快取 0.075/M, 輸出 0.60/M
cost = calculate_cost(
model="gpt-4o-mini",
prompt_tokens=1_000_000,
completion_tokens=1_000_000,
cached_tokens=500_000,
)
# (500k * 0.15) + (500k * 0.075) + (1M * 0.60) = 0.075 + 0.0375 + 0.60 = 0.7125
assert cost == 0.7125
def test_telemetry_recorder_lifecycle(memory_db):
"""測試任務建立、事件累計與狀態結案。"""
recorder = TelemetryRecorder(memory_db, "test_run_1")
recorder.start_run("測試自動化檢驗主題")
# 記錄第一個事件
recorder.record_event(
event_type="TOOL",
latency_ms=120.0,
model="gpt-4o-mini",
prompt_tokens=200,
completion_tokens=50,
)
# 記錄第二個事件
recorder.record_event(
event_type="LLM",
latency_ms=280.0,
model="gpt-4o-mini",
prompt_tokens=300,
completion_tokens=150,
)
recorder.finish_run("COMPLETED")
# 驗證 runs 表之累計結果
run_row = memory_db.execute("SELECT * FROM runs WHERE run_id = 'test_run_1';").fetchone()
assert run_row["status"] == "COMPLETED"
assert run_row["total_prompt_tokens"] == 500
assert run_row["total_completion_tokens"] == 200
assert run_row["total_latency_seconds"] == pytest.approx(0.4, 0.001)
# 驗證 events 表筆數
events_count = memory_db.execute("SELECT COUNT(*) FROM events WHERE run_id = 'test_run_1';").fetchone()[0]
assert events_count == 2
我們同樣可以透過命令列執行單元測試,確認所有資料庫交易與計算邏輯均符合預期:
# 執行觀測機制單元測試
uv run pytest tests/test_observability.py -v
常見錯誤與踩雷
在自建 AI Agent 的觀測與計量管線時,開發者經常遭遇以下幾個典型的架構陷阱:
- 串流輸出時遺失權杖用量資訊:在使用 OpenAI 原生相容的串流模式(
stream=True)時,預設最後一個 chunk 不會附帶usage物件。如果直接讀取response.usage會得到None,導致日誌中的 token 全部被記為 0。要修復此問題,必須在發起請求時明確傳入參數stream_options={"include_usage": True},模型才會在串流的最後一個資料塊中回傳完整的權杖統計。 - 忽略提示詞快取折扣:現代高階模型大多導入了自動提示詞快取機制(例如 Anthropic 的 Prompt Caching 或 OpenAI 的自動快取命中)。如果一律按照標準未快取價格計算成本,統計出來的費用可能會比實際帳單高出兩到三倍。實務上必須從
prompt_tokens_details中提取cached_tokens並依各家官方文件的折扣比例進行扣減。 - SQLite 並行寫入鎖定(database is locked):當代理系統進入平行處理(例如多個工具並行呼叫或背景非同步處理)時,若多個執行緒直接向預設設定的 SQLite 資料庫發起寫入,極易觸發
sqlite3.OperationalError: database is locked。解決關鍵在於初始化時宣告PRAGMA journal_mode = WAL;,並為連線設定充足的逾時時間(例如sqlite3.connect(path, timeout=30.0))。 - 誤用時鐘函式測量延遲:使用
time.time()測量程式碼執行時間是常見的壞習慣。因為time.time()採用的是作業系統的日曆時鐘(Wall-clock time),一旦伺服器執行 NTP 時間校準或閏秒調整,測量出來的時間差可能會變成負數或突增。測量微秒與毫秒級延遲,務必使用單調遞增的time.perf_counter()。
效能與實務提醒
觀測系統的存在是為了輔助核心業務,切忌讓觀測本身的開銷「喧賓奪主」:
第一點是度量寫入對代理主迴圈的阻礙。在本地開發或小規模驗證階段,直接同步寫入 SQLite 是最簡單透明的方案。但當系統擴展至高頻率並行運作時,頻繁的資料庫 I/O 會拖慢代理的整體回應速度。實務上的最佳化策略是採用記憶體非同步佇列(asyncio.Queue),由獨立的背景背景工作者(Worker)批次將日誌寫入磁碟;或者在更大型的架構中,直接將追蹤資料推送到像 Langfuse 這類專業的遠端觀測伺服器(此架構會在 AG Day 35 中深入實作)。
第二點是建立「成本熔斷防護網(Circuit Breaker)」。軟體開發中最恐怖的事故之一,是代理因為提示詞邏輯漏洞陷入無限重試迴圈,通宵執行燒光整個專案的預算配額。在 TelemetryRecorder 或代理迴圈的核心調度器中,應設定強制的硬性防護上限,例如透過環境變數 RESEARCH_AGENT_MAX_STEPS(預設限制 15 步)與 RESEARCH_AGENT_MAX_COST(例如單次任務不得超過 0.5 美元)。一旦累計數值觸及臨界值,立即中斷執行並拋出熔斷異常。
第三點是日誌記錄的隱私與敏感資料脫敏。在 metadata_json 中記錄提示詞與模型回應時,必須審查文獻中是否夾帶使用者個人識別資料(PII)或企業機密。在金融或醫療等高合規性場景中,敏感欄位在進入本地觀測庫前應執行雜湊遮罩處理,避免觀測庫反而成為資安外洩的破口。
小結
今天我們深入探討了 AI Agent 系統的觀測基石。我們從代理黑盒子的維運痛點切入,釐清了權杖、延遲與成本三大指標的計量原理與換算公式。我們在貫穿專案 research-agent 中,利用本地輕量、無依賴的 SQLite 資料庫設計了 runs 與 events 階層資料表,並透過 WAL 模式確保了高效穩定的寫入。
我們實作了兼顧精確性與離線 dry-run 相容的度量封裝函式,讓無金鑰的開發者也能順暢演練觀測流程,並提供了完善的單元測試。透過這套本機觀測機制,我們的研究助理第一次擁有了透明的心跳監視器,每一次思考與工具呼叫的資源消耗都將無所遁形。
結語
回顧至此,從 Day 1 到 Day 9,我們已經靠著純 Python 程式碼、基礎迴圈、Pydantic v2 契約以及 SQLite 觀測庫,打造出了一個功能完整的單體代理原型。我們能呼叫模型、調度工具、檢查錯誤、抽取結構化資料,還能詳細記錄每一次執行的時間與金錢成本。
然而,當我們試圖讓研究助理承擔更複雜的任務時——例如同時展開多條獨立的搜尋分支、在遇到歧異時回溯先前的狀態重新推理、或是暫停流程等待人類審查並隨後復原狀態——手刻的 while 迴圈與巨大字典狀態很快就會陷入義大利麵式的程式碼泥淖中。手刻 Agent 的架構極限究竟在哪裡?我們為什麼必須引入專業的圖編排框架?
明天,我們會進入「AG Day 10 為什麼要框架:手刻 Agent 的極限」,透過真實的工程痛點深度剖析狀態膨脹、分支控制、記憶體恢復與並行處理的極限,正式揭開從純腳本邁向 LangGraph 狀態圖編排架構的序幕。
延伸資源
- OpenAI API 官方文件(計費與使用量說明):
https://platform.openai.com/docs/api-reference/usage。掌握最新 token 統計與 prompt caching 規格。 - SQLite WAL(Write-Ahead Logging)官方文件:
https://www.sqlite.org/wal.html。深入理解並行寫入與交易日誌機制。 - Python 官方 time 模組性能基準指引:
https://docs.python.org/3/library/time.html。理解單調時鐘 perf_counter 與系統時鐘的差異。 - OpenTelemetry 語意慣例(Semantic Conventions for Generative AI):
https://opentelemetry.io/docs/specs/semconv/gen-ai/。現代分散式系統觀測 LLM 的國際標準規格。
留言
張貼留言