iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0

https://ithelp.ithome.com.tw/upload/images/20260817/20161290soMxgPlZmF.png

生產環境不是只看最終答案,而是看每一步的證據。

在展示 Demo 或 POC 階段,大家往往只關心 AI「有沒有回答出看似合理的答案」;然而一旦進入生產環境(Production),企業最常面臨的追問從來不是「它答得像不像人」,而是:

  • 為什麼這次給了這位客戶 30% 的折扣?(合規與審計)
  • 為什麼這個查詢花了 8 秒才回應?(效能瓶頸定位)
  • 為什麼本月的 LLM API 費用突然暴增 3 倍?(維運成本歸因)
  • 模型輸出格式錯誤時,到底是在哪一個 Action 壞掉的?(即時除錯與回溯)

如果你的 AI Agent 是個黑盒子,發生問題時除了看著一片狼藉的終端日誌乾瞪眼之外,根本無法向業務端交代。今天我們就要利用 Embabel 的可觀測性機制,將 Agent 的每一次思考、工具調用與 Action 執行,轉化為具備時間戳、Token 消耗與成本依據的 ActionAudit 工程事件


1. 今天要解決的痛點與核心觀念

痛點背景:黑盒 Agent 在生產環境的四大災難

  1. 死無對證:客戶投訴被推薦了不合規的合約方案,但日誌裡只有最終輸出的文字,完全查不到中間是哪一步 Prompt 誘導或哪筆 CRM 資料餵錯。
  2. 費用黑洞:月末帳單爆表,無法分辨是哪個 Action 瘋狂消耗 Token,還是有 Action 陷入了未預期的重試迴圈。
  3. 性能盲區:使用者抱怨系統變慢,但無法精確拆解究竟是本地 Java 資料庫查詢慢、第三方 API 延遲,還是 LLM 推理卡住。
  4. 前端體驗空白:在等待 Agent 跑完 4~5 個步驟的 10 秒內,前端只能掛著單調的轉圈圈(Loading Spinner),使用者完全不知道系統是不是當機了。

觀念圖解:ActionAudit 與事件觀測體系

https://ithelp.ithome.com.tw/upload/images/20260817/20161290SjRHtklyE0.png

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

https://ithelp.ithome.com.tw/upload/images/20260817/20161290GFqH6v7JPu.jpg

  • 步驟 1 (Plan Created):A* 演算法依據目標生成最優 Action 執行序列。
  • 步驟 2 (Action Started & Completed):監聽純 Java Action(如查詢 DB 耗時 12ms,成本 $0)。
  • 步驟 3 (LLM Token & Cost Attribution):即時捕捉 LLM 的 Prompt 與 Completion Token,換算精確維運費用(如 $0.002)。
  • 步驟 4 (Event Dispatcher 雙向分流):一端將 ActionAudit 寫入資料庫/監控系統,另一端透過 SSE 推播給前端展示即時思維鏈。

2. 官方核心技術依據與架構深度

在 Embabel 官方架構中,可觀測性主要由三個核心介面與模型支撐:

1. AgentProcessEvent 事件體系

Embabel 定義了精細的流程事件抽象,涵蓋 Agent 的完整生命週期:

  • PlanCreatedEvent:包含 A* 演算法規劃出的完整 Action 執行清單與預估 Cost。
  • ActionStartedEvent:記載即將執行的 Action 名稱、傳入的強型別 Input Object。
  • LlmCallCompletedEvent:底層 Spring AI 呼叫完畢後觸發,封裝了 PromptMetadataTokenUsage(Prompt Tokens / Completion Tokens)與實際使用的模型名稱。
  • ActionCompletedEvent:包含產出的 Output Object、執行毫秒數(durationMs)與執行狀態(SUCCESS / FAILED)。

2. AgenticEventListener 全域監聽介面

任何實作了 AgenticEventListener 的 Spring Bean 都會被自動註冊至事件匯流排(Event Bus)。這允許我們無侵入性地將觀測邏輯(日誌記錄、指標累加、SSE 廣播)從業務 Action 中徹底解耦。

3. Token 與成本模型(Cost Attribution Model)

透過 Spring AI 的 Usage 物件,我們可以結合即時定價表(Pricing Matrix),為每次 LLM 交互精確計算出微美元(Micro-cents)級別的實際費用。


3. 完整程式碼實戰(Production-Ready Code)

以下我們實作一套生產級別的稽核與觀測體系,包含:

  1. 強型別 ActionAudit 稽核實體
  2. 動態費用計算器(TokenCostCalculator)
  3. 非同步事件監聽器(AgentObservabilityListener)

1. ActionAudit 稽核紀錄定義

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
    ) {}
}

2. 精確費用計算器

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;
    }
}

3. 全域事件監聽與轉發器

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());
    }
}

4. 生產環境避坑指南與對比分析

常見踩雷與除錯秘訣

  1. 雷區一:在 Action 內部手動寫日誌與費用計算
    • 後果:每個 Action 都充斥著重複的 System.currentTimeMillis() 與 Log 語法,業務邏輯嚴重污染,且一旦忘記寫就出現觀測斷層。
    • 解法:統一交給全域 AgenticEventListener 進行切面級(AOP)收集。
  2. 雷區二:忽略 Context Window 造成的隱形費用
    • 後果:在多輪對話或大物件傳遞時,每一次 Action 都把整份歷史資料全部塞入 Prompt,導致 Prompt Tokens 指數型增長。
    • 解法:在 Audit 紀錄中監控 promptTokens 指標,若單一 Action 超過 2,000 tokens 立即發出告警,評估是否改用 RAG 或 Summary 壓縮。
  3. 雷區三:敏感個資(PII)在 Audit Log 中明文洩漏
    • 後果:客戶的信用卡號、身分證字號被完整寫入日誌系統,違反 GDPR / 金融資安規範。
    • 解法:在 ActionAudit 記錄 inputSnapshotoutputSnapshot 時,實作 Data Masker(脫敏處理器),自動過濾特定欄位。

觀測架構 Good vs Bad 對比表

評估維度 ❌ 傳統黑盒做法 (Bad) ✅ Embabel ActionAudit 體系 (Good)
排查問題依據 只能猜測 LLM 當時想了什麼 擁有精確的輸入、輸出、Prompt 快照與 Tool 呼叫序列
成本分析維度 只能看月底 API 總帳單 能精確拆解到「哪個 Action、哪個客戶等級、哪類業務」最花錢
前端進度反饋 單純的 Loading 動畫(死等 10 秒) 透過 SSE 即時接收 ActionStarted / Completed 事件,動態展示思維進度
業務合規稽核 無法向稽核單位解釋為何產生該決策 提供完整的 Chain of Evidence(證據鏈條)供事後回放

5. 今日動手實作任務與發文備註

🛠️ 今日實作任務

  1. 定義稽核結構:在你的專案中建立 ActionAudit Record 物件,確保至少包含 traceIdactionNamedurationMspromptTokensestimatedCostUsd
  2. 實作監聽器:建立一個實作 AgenticEventListener 的 Spring Bean,並在終端機印出每次 Action 執行的結構化 JSON 稽核字串。
  3. 思考題:如果系統要在前端展示「Agent 正在為您整理過去 1 年的旅遊活動...」,這個文字應該由前端依據 ActionStartedEvent.actionName() 靜態映射,還是由後端事件動態推播?兩者有何優缺點?

如果你也想進一步學習如何透過 AI 開發 Spring Framework 應用,讓 AI 協助理解框架、撰寫程式、除錯與驗>證,歡迎到 Hahow 看凱文大叔的最新課程【駕馭 AI 的全端實戰養成班:從零打造企業級智慧應用系統】。一起>學習如何駕馭 AI,提升 Spring 應用的開發效率與品質。
課程連結


上一篇
Day 16:需要資料時再去找
下一篇
Day 18:測試不要只看結果長什麼樣
系列文
讓 AI Agent 真的做事:用 Embabel 打造可控、可測試的智慧 Dashboard19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言