
在展示 Demo 或 POC 階段,大家往往只關心 AI「有沒有回答出看似合理的答案」;然而一旦進入生產環境(Production),企業最常面臨的追問從來不是「它答得像不像人」,而是:
如果你的 AI Agent 是個黑盒子,發生問題時除了看著一片狼藉的終端日誌乾瞪眼之外,根本無法向業務端交代。今天我們就要利用 Embabel 的可觀測性機制,將 Agent 的每一次思考、工具調用與 Action 執行,轉化為具備時間戳、Token 消耗與成本依據的 ActionAudit 工程事件。

Embabel 透過內建的 Event Broker 與 生命週期攔截機制,將 Agent 執行過程中的每一個關鍵節點轉化為標準事件:

ActionAudit 寫入資料庫/監控系統,另一端透過 SSE 推播給前端展示即時思維鏈。在 Embabel 官方架構中,可觀測性主要由三個核心介面與模型支撐:
AgentProcessEvent 事件體系Embabel 定義了精細的流程事件抽象,涵蓋 Agent 的完整生命週期:
PlanCreatedEvent:包含 A* 演算法規劃出的完整 Action 執行清單與預估 Cost。ActionStartedEvent:記載即將執行的 Action 名稱、傳入的強型別 Input Object。LlmCallCompletedEvent:底層 Spring AI 呼叫完畢後觸發,封裝了 PromptMetadata、TokenUsage(Prompt Tokens / Completion Tokens)與實際使用的模型名稱。ActionCompletedEvent:包含產出的 Output Object、執行毫秒數(durationMs)與執行狀態(SUCCESS / FAILED)。AgenticEventListener 全域監聽介面任何實作了 AgenticEventListener 的 Spring Bean 都會被自動註冊至事件匯流排(Event Bus)。這允許我們無侵入性地將觀測邏輯(日誌記錄、指標累加、SSE 廣播)從業務 Action 中徹底解耦。
透過 Spring AI 的 Usage 物件,我們可以結合即時定價表(Pricing Matrix),為每次 LLM 交互精確計算出微美元(Micro-cents)級別的實際費用。
以下我們實作一套生產級別的稽核與觀測體系,包含:
package com.antechinus.travel.observability;
import java.time.Instant;
import java.util.List;
import java.util.Map;
/**
* Action 執行稽核紀錄物件
* 記錄單一 Action 執行的完整上下文、資源消耗與業務證據
*/
public record ActionAudit(
// 全域唯一追蹤識別碼 (Trace ID)
String traceId,
// 當前執行的 Agent 名稱
String agentName,
// 當前執行的 Action 方法名稱
String actionName,
// 輸入物件的型別名稱與內容快照
String inputType,
Object inputSnapshot,
// 輸出物件的型別名稱與內容快照
String outputType,
Object outputSnapshot,
// 實際使用的模型名稱 (若為純 Java Action 則為 NONE)
String modelName,
// 呼叫的工具清單與參數
List<ToolCallRecord> toolCalls,
// Prompt Token 消耗數
long promptTokens,
// Completion Token 消耗數
long completionTokens,
// 預估美元成本 (USD)
double estimatedCostUsd,
// 執行耗時 (毫秒)
long durationMs,
// 執行狀態 (SUCCESS, FAILED)
ExecutionStatus status,
// 若失敗時的錯誤原因
String errorMessage,
// 記錄時間戳記
Instant timestamp
) {
public enum ExecutionStatus { SUCCESS, FAILED }
/**
* 工具呼叫明細紀錄
*/
public record ToolCallRecord(
String toolName,
Map<String, Object> arguments,
Object result,
long durationMs
) {}
}
package com.antechinus.travel.observability;
import org.springframework.stereotype.Component;
/**
* LLM Token 費用計算組件
* 依據官方定價模型換算每次 Action 的實際維運成本
*/
@Component
public class TokenCostCalculator {
// 定義各模型的百萬 Token 單價 (Input / Output USD)
private static final double GPT_4O_INPUT_PER_M = 2.50;
private static final double GPT_4O_OUTPUT_PER_M = 10.00;
private static final double GPT_4O_MINI_INPUT_PER_M = 0.15;
private static final double GPT_4O_MINI_OUTPUT_PER_M = 0.60;
/**
* 依據模型與 Token 數量計算預估成本
*
* @param modelName 模型名稱
* @param promptTokens 輸入 Token 數
* @param completionTokens 輸出 Token 數
* @return 美元成本 (USD)
*/
public double calculateCost(String modelName, long promptTokens, long completionTokens) {
if (modelName == null || modelName.isBlank()) {
return 0.0;
}
double inputPricePerM;
double outputPricePerM;
if (modelName.contains("mini")) {
inputPricePerM = GPT_4O_MINI_INPUT_PER_M;
outputPricePerM = GPT_4O_MINI_OUTPUT_PER_M;
} else {
inputPricePerM = GPT_4O_INPUT_PER_M;
outputPricePerM = GPT_4O_OUTPUT_PER_M;
}
double inputCost = (promptTokens / 1_000_000.0) * inputPricePerM;
double outputCost = (completionTokens / 1_000_000.0) * outputPricePerM;
return inputCost + outputCost;
}
}
package com.antechinus.travel.observability;
import com.embabel.agent.event.AgentProcessEvent;
import com.embabel.agent.event.AgenticEventListener;
import com.embabel.agent.event.ActionStartedEvent;
import com.embabel.agent.event.ActionCompletedEvent;
import com.embabel.agent.event.LlmCallCompletedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.concurrent.ConcurrentHashMap;
/**
* Agent 流程可觀測性監聽器
* 負責收集生命週期事件、計算資源消耗並沉澱稽核日誌
*/
@Component
public class AgentObservabilityListener implements AgenticEventListener {
private static final Logger log = LoggerFactory.getLogger(AgentObservabilityListener.class);
private final TokenCostCalculator costCalculator;
// 用於暫存進行中 Action 的起始狀態 (Key: ActionExecutionId)
private final ConcurrentHashMap<String, Long> actionStartTimes = new ConcurrentHashMap<>();
public AgentObservabilityListener(TokenCostCalculator costCalculator) {
this.costCalculator = costCalculator;
}
/**
* 處理 Embabel 流程引擎發出的所有事件
*
* @param event 流程生命週期事件
*/
@Override
public void onProcessEvent(AgentProcessEvent event) {
if (event instanceof ActionStartedEvent started) {
handleActionStarted(started);
} else if (event instanceof ActionCompletedEvent completed) {
handleActionCompleted(completed);
}
}
/**
* 處理 Action 開始事件
*/
private void handleActionStarted(ActionStartedEvent event) {
actionStartTimes.put(event.executionId(), System.currentTimeMillis());
log.info("[Agent Trace: {}] Action [{}] 開始執行,傳入型別: {}",
event.traceId(), event.actionName(), event.input().getClass().getSimpleName());
}
/**
* 處理 Action 完成事件並產出 ActionAudit
*/
private void handleActionCompleted(ActionCompletedEvent event) {
Long startTime = actionStartTimes.remove(event.executionId());
long durationMs = (startTime != null) ? (System.currentTimeMillis() - startTime) : 0L;
// 計算費用
double cost = costCalculator.calculateCost(
event.modelName(),
event.promptTokens(),
event.completionTokens()
);
// 建構稽核實體
ActionAudit audit = new ActionAudit(
event.traceId(),
event.agentName(),
event.actionName(),
event.input().getClass().getSimpleName(),
event.input(),
event.output() != null ? event.output().getClass().getSimpleName() : "VOID",
event.output(),
event.modelName(),
List.of(), // 可擴充工具呼叫紀錄
event.promptTokens(),
event.completionTokens(),
cost,
durationMs,
event.isSuccess() ? ActionAudit.ExecutionStatus.SUCCESS : ActionAudit.ExecutionStatus.FAILED,
event.errorMessage(),
Instant.now()
);
// 記錄結構化日誌 (可用於 Logstash / Fluentd 收集至 Elasticsearch)
log.info("[Audit] Action: {}, 耗時: {}ms, Tokens: [in:{}, out:{}], 預估費用: ${}, 狀態: {}",
audit.actionName(), audit.durationMs(), audit.promptTokens(),
audit.completionTokens(), String.format("%.6f", audit.estimatedCostUsd()), audit.status());
}
}
System.currentTimeMillis() 與 Log 語法,業務邏輯嚴重污染,且一旦忘記寫就出現觀測斷層。AgenticEventListener 進行切面級(AOP)收集。promptTokens 指標,若單一 Action 超過 2,000 tokens 立即發出告警,評估是否改用 RAG 或 Summary 壓縮。ActionAudit 記錄 inputSnapshot 與 outputSnapshot 時,實作 Data Masker(脫敏處理器),自動過濾特定欄位。| 評估維度 | ❌ 傳統黑盒做法 (Bad) | ✅ Embabel ActionAudit 體系 (Good) |
|---|---|---|
| 排查問題依據 | 只能猜測 LLM 當時想了什麼 | 擁有精確的輸入、輸出、Prompt 快照與 Tool 呼叫序列 |
| 成本分析維度 | 只能看月底 API 總帳單 | 能精確拆解到「哪個 Action、哪個客戶等級、哪類業務」最花錢 |
| 前端進度反饋 | 單純的 Loading 動畫(死等 10 秒) | 透過 SSE 即時接收 ActionStarted / Completed 事件,動態展示思維進度 |
| 業務合規稽核 | 無法向稽核單位解釋為何產生該決策 | 提供完整的 Chain of Evidence(證據鏈條)供事後回放 |
ActionAudit Record 物件,確保至少包含 traceId、actionName、durationMs、promptTokens 與 estimatedCostUsd。AgenticEventListener 的 Spring Bean,並在終端機印出每次 Action 執行的結構化 JSON 稽核字串。ActionStartedEvent.actionName() 靜態映射,還是由後端事件動態推播?兩者有何優缺點?如果你也想進一步學習如何透過 AI 開發 Spring Framework 應用,讓 AI 協助理解框架、撰寫程式、除錯與驗>證,歡迎到 Hahow 看凱文大叔的最新課程【駕馭 AI 的全端實戰養成班:從零打造企業級智慧應用系統】。一起>學習如何駕馭 AI,提升 Spring 應用的開發效率與品質。
課程連結