系列文章 本篇是 AI Agent 深度解析系列的第 17 篇。以下是已发布的全部文章:
从零理解 RAG:检索增强生成完整指南
理解 AI Agent 的大脑:ReAct 模式从入门到实战
AI Agent 的记忆系统:从 ChatMemory 到持久化记忆的 Java 实战
MCP 模型上下文协议:AI 的万能接口与 MCP Server 实战
AI Agent 的工具箱:深入理解 Tool Use 与 Spring AI Function Calling 实战
AI Agent 的记忆力是怎么实现的——LangChain4j Memory 机制深度解析
让 AI 学会说人话——Spring AI 结构化输出实战
AI Agent 的规划大脑:从任务分解到自适应执行策略
AI Agent 的灵魂对话:Prompt Engineering 系统提示词设计的艺术与工程
AI Agent 团队协作:多 Agent 系统架构设计与 Java 实战
AI Agent 评估与优化:从基准测试到生产环境的质量守护实战
当 RAG 遇上知识图谱:GraphRAG 原理与 Java 实战
Embedding 向量化的魔法:从文本到向量的数学之旅与 Java 实战
当 RAG 遇到 Agent:Agentic RAG 的架构设计与 Java 实战
AI Agent 的知识检索引擎:从向量搜索到智能检索策略的 Java 实战
AI Agent 的安全防线:Prompt 注入防御与生产级安全防护实战
AI Agent 的可观测性:从链路追踪到成本监控的 Java 实战 (本文)
你的 Agent 上线了,然后呢? 想象一个场景:你精心打造的 AI Agent 客服系统上线了,用户开始用它处理退换货、查物流、咨询产品。第一天运行”看起来”很正常,直到老板拿着账单找到你——“这个月 API 调用费怎么 12 万?”
你开始排查:是哪个用户在疯狂调用?是哪个 Tool 调用链路特别长导致 Token 消耗暴增?是 Agent 陷入了推理循环反复调用同一个工具?还是模型返回了大量冗余内容?
你发现,Agent 系统和传统 Web 应用有一个根本区别:传统应用的每次请求消耗的资源基本固定,而 Agent 的一次用户交互可能触发 3-10 次 LLM 调用,每次调用消耗的 Token 数量差异巨大 。没有可观测性,你就是在盲飞。
在 AI Agent 评估与优化 一文中,我们聊了怎么评估 Agent 的质量。但评估是离线的、面向结果的;可观测性是在线的、面向过程的 ——它让你在 Agent 运行时实时看到”它在做什么、花了多少钱、哪里出了问题”。
这就是今天要聊的内容。
可观测性的三大支柱,在 Agent 世界里的新含义 传统后端的可观测性有三大支柱:Traces(链路追踪)、Metrics(指标)、Logs(日志) 。到了 Agent 世界,这三者的含义都发生了变化。
Traces:不只是 HTTP 调用链 在传统微服务中,一个 Trace 就是一条 HTTP 调用链:Gateway → Service A → Service B → Database。
但在 Agent 系统中,一次用户交互的 Trace 可能长这样:
1 2 3 4 5 6 7 8 9 10 11 用户输入: "我的订单 #12345 什么时候到?" ├── LLM Call #1 (ReAct 推理) — 1200 tokens, 800ms │ └── 决策: 调用 getOrderStatus tool ├── Tool Call: getOrderStatus("12345") — 50ms │ └── 返回: {"status": "已发货", "tracking": "SF1234567"} ├── LLM Call #2 (生成回复) — 800 tokens, 600ms │ └── 决策: 调用 getTrackingInfo tool ├── Tool Call: getTrackingInfo("SF1234567") — 80ms │ └── 返回: {"location": "深圳转运中心", "eta": "明天"} └── LLM Call #3 (最终回复) — 600 tokens, 500ms └── 输出: "您的订单已发货,目前在深圳转运中心..."
关键区别 :Agent 的 Trace 是多轮 LLM 调用 + 工具调用的复合链路 ,每一步都有独立的 Token 消耗和延迟。你需要看到的不只是”这个请求花了多久”,而是”Agent 推理了几轮、每轮消耗了多少 Token、调用了哪些工具”。
Metrics:Token 是新的 CPU 传统应用监控 CPU、内存、QPS。Agent 应用的核心指标变成了:
指标
传统应用等价物
含义
Token 消耗/请求
CPU 时间/请求
单次交互的资源消耗
LLM 调用次数/请求
RPC 调用次数/请求
Agent 的推理深度
LLM 延迟 (P50/P99)
RPC 延迟
用户等待时间
Tool 调用成功率
下游服务成功率
工具是否正常工作
每日 Token 成本
云计算费用
最直接的财务指标
缓存命中率
缓存命中率
相似问题是否复用
其中,Token 消耗 是 Agent 系统独有的、最重要的指标。它直接决定你的运营成本。
Logs:结构化是命 Agent 的日志必须是结构化的。一条普通的 logger.info("Agent responded") 毫无价值。你需要的是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { "timestamp" : "2026-06-19T10:30:00Z" , "traceId" : "abc-123" , "userId" : "user-456" , "agentType" : "customer-service" , "llmProvider" : "openai" , "model" : "gpt-4o" , "promptTokens" : 1200 , "completionTokens" : 800 , "totalTokens" : 2000 , "latencyMs" : 800 , "toolCalls" : [ "getOrderStatus" ] , "reasoning" : "User asked about order status, need to fetch from backend" , "cost" : 0.006 }
有了这样的结构化日志,你才能回答”哪个用户的请求最费钱”、”哪个 Tool 调用导致了延迟飙升”这类问题。
LangChain4j 的可观测性方案 LangChain4j 提供了两层可观测性能力:ChatModelListener(应用层) 和 OpenTelemetry 集成(基础设施层) 。
第一层:ChatModelListener——最灵活的观测点 ChatModelListener 是 LangChain4j 提供的回调接口,让你在 LLM 调用的各个生命周期节点插入自定义逻辑。它的设计思路类似于 Servlet 的 Filter 链——你可以在请求发出前、响应返回后、出错时分别执行逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 import dev.langchain4j.model.chat.listener.ChatModelErrorContext;import dev.langchain4j.model.chat.listener.ChatModelListener;import dev.langchain4j.model.chat.listener.ChatModelRequestContext;import dev.langchain4j.model.chat.listener.ChatModelResponseContext;@Component public class AgentObservabilityListener implements ChatModelListener { private static final Logger log = LoggerFactory.getLogger(AgentObservabilityListener.class); private final MeterRegistry meterRegistry; public AgentObservabilityListener (MeterRegistry meterRegistry) { this .meterRegistry = meterRegistry; } @Override public void onRequest (ChatModelRequestContext context) { context.attributes().put("startTime" , System.nanoTime()); String traceId = MDC.get("traceId" ); if (traceId != null ) { context.attributes().put("traceId" , traceId); } log.debug("LLM request initiated, model: {}" , context.chatRequest().modelName()); } @Override public void onResponse (ChatModelResponseContext context) { long startTime = (long ) context.attributes().getOrDefault("startTime" , 0L ); long latencyMs = (System.nanoTime() - startTime) / 1_000_000 ; var tokenUsage = context.chatResponse().tokenUsage(); String model = context.chatResponse().modelName(); meterRegistry.counter("llm.requests.total" , "model" , model, "status" , "success" ).increment(); meterRegistry.counter("llm.tokens.total" , "model" , model, "type" , "prompt" ).increment(tokenUsage.inputTokenCount()); meterRegistry.counter("llm.tokens.total" , "model" , model, "type" , "completion" ).increment(tokenUsage.outputTokenCount()); meterRegistry.timer("llm.latency" , "model" , model ).record(Duration.ofMillis(latencyMs)); double cost = calculateCost(model, tokenUsage.inputTokenCount(), tokenUsage.outputTokenCount()); meterRegistry.counter("llm.cost.total" , "model" , model ).increment(cost); log.info("LLM call completed: model={}, promptTokens={}, completionTokens={}, " + "latencyMs={}, cost=${}" , model, tokenUsage.inputTokenCount(), tokenUsage.outputTokenCount(), latencyMs, String.format("%.4f" , cost)); } @Override public void onError (ChatModelErrorContext context) { meterRegistry.counter("llm.requests.total" , "model" , "unknown" , "status" , "error" ).increment(); log.error("LLM call failed: {}" , context.error().getMessage()); } private double calculateCost (String model, long promptTokens, long completionTokens) { return switch (model) { case "gpt-4o" -> promptTokens * 2.5 / 1_000_000 + completionTokens * 10.0 / 1_000_000 ; case "gpt-4o-mini" -> promptTokens * 0.15 / 1_000_000 + completionTokens * 0.6 / 1_000_000 ; case "claude-3.5-sonnet" -> promptTokens * 3.0 / 1_000_000 + completionTokens * 15.0 / 1_000_000 ; default -> 0.0 ; }; } }
为什么 ChatModelListener 是第一选择? 因为它工作在 LangChain4j 的抽象层,不依赖任何外部系统。即使你还没有部署 OpenTelemetry Collector 或 Prometheus,也能立刻开始收集指标。
注册 Listener 的方式取决于你使用的是声明式还是编程式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Bean public ChatLanguageModel chatModel (AgentObservabilityListener listener) { return OpenAiChatModel.builder() .apiKey(System.getenv("OPENAI_API_KEY" )) .modelName("gpt-4o" ) .listeners(List.of(listener)) .build(); } AiService.builder(CustomerServiceAgent.class) .chatLanguageModel(chatModel) .chatMemoryProvider(memoryProvider) .tools(orderTools) .build();
第二层:OpenTelemetry 集成——生产级分布式追踪 当你需要把 Agent 的调用链路接入已有的 APM 系统(Jaeger、Grafana Tempo、Datadog)时,LangChain4j 的 OpenTelemetry 集成就派上用场了。
LangChain4j 提供了 langchain4j-opentelemetry 模块,它自动为每次 LLM 调用和 Tool 调用创建 Span:
1 2 3 4 5 <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-opentelemetry</artifactId > <version > 1.0.0-beta1</version > </dependency >
关键类是 OpenTelemetryChatModelListener,它把 ChatModel 生命周期事件映射为 OpenTelemetry Span:
1 2 3 4 5 6 7 8 9 10 11 12 13 import dev.langchain4j.opentelemetry.OpenTelemetryChatModelListener;import io.opentelemetry.api.OpenTelemetry;import io.opentelemetry.api.trace.Tracer;@Configuration public class ObservabilityConfig { @Bean public OpenTelemetryChatModelListener otelListener (OpenTelemetry openTelemetry) { Tracer tracer = openTelemetry.getTracer("langchain4j-agent" ); return new OpenTelemetryChatModelListener (tracer); } }
接入后,在 Jaeger 或 Grafana Tempo 中看到的 Trace 会是这样:
1 2 3 4 5 6 7 Span: agent-interaction (userId=user-456) ├── Span: llm-call (model=gpt-4o, prompt_tokens=1200) │ └── Attributes: llm.request.messages, llm.response.finish_reason ├── Span: tool-call (name=getOrderStatus) │ └── Attributes: tool.input, tool.output ├── Span: llm-call (model=gpt-4o, prompt_tokens=800) └── Span: llm-call (model=gpt-4o, prompt_tokens=600)
和 ChatModelListener 的区别是什么? ChatModelListener 是 LangChain4j 内部的回调机制,适合做自定义的指标计算和日志记录;OpenTelemetry 集成是标准化的分布式追踪协议,适合接入已有的 APM 基础设施。两者可以并存——Listener 做业务指标,OTel 做链路追踪。
别忘了 Agent 的另一半是 Tool 调用。LangChain4j 的 Tool 执行也会被自动创建 Span,但你可以在 Tool 方法中手动添加更丰富的上下文:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import io.opentelemetry.api.trace.Span;import io.opentelemetry.api.trace.Tracer;@Component public class OrderTools { private final OrderService orderService; private final Tracer tracer; public OrderTools (OrderService orderService, Tracer tracer) { this .orderService = orderService; this .tracer = tracer; } @Tool("根据订单号查询订单状态") public OrderStatus getOrderStatus (String orderId) { Span currentSpan = Span.current(); currentSpan.setAttribute("order.id" , orderId); long start = System.nanoTime(); try { OrderStatus status = orderService.queryStatus(orderId); currentSpan.setAttribute("order.status" , status.getStatus()); return status; } catch (Exception e) { currentSpan.setAttribute("error.type" , e.getClass().getSimpleName()); throw e; } finally { long duration = (System.nanoTime() - start) / 1_000_000 ; currentSpan.setAttribute("tool.duration_ms" , duration); } } }
Spring AI 的可观测性方案 如果你用的是 Spring AI 而不是 LangChain4j,好消息是 Spring AI 原生集成了 Micrometer Observation API ——这是 Spring 生态的标准可观测性抽象层。
自动配置的魔法 Spring AI 的可观测性是开箱即用的。只要你的 classpath 上有 micrometer-tracing-bridge-otel(或 Brave),Spring AI 会自动为每次 Chat Model 调用创建 Observation:
1 2 3 4 5 6 7 8 <dependency > <groupId > io.micrometer</groupId > <artifactId > micrometer-tracing-bridge-otel</artifactId > </dependency > <dependency > <groupId > io.opentelemetry</groupId > <artifactId > opentelemetry-exporter-otlp</artifactId > </dependency >
Spring AI 自动创建的 Observation 包含以下信息:
Span 名称 : chat(或 embedding、image 等)
高基数标签(High Cardinality Tags) : gen_ai.request.model, gen_ai.response.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens
低基数标签(Low Cardinality Tags) : gen_ai.operation.name, gen_ai.system
这里要解释一下”高基数”和”低基数”:高基数标签的值是动态的(比如具体的 token 数量、请求 ID),适合做 Span Attributes;低基数标签的值是固定的几个枚举(比如操作类型),适合做 Metric Tags。Spring AI 遵循了 OpenTelemetry 的 GenAI Semantic Conventions,这是一个专门为 LLM 应用定义的语义标准。
自定义 Observation:给 Agent 加业务维度 默认的 Observation 只覆盖了 LLM 调用层面。要追踪整个 Agent 交互,你需要自定义 Observation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import io.micrometer.observation.Observation;import io.micrometer.observation.ObservationRegistry;@Component public class AgentInteractionObserver { private final ObservationRegistry observationRegistry; public AgentInteractionObserver (ObservationRegistry observationRegistry) { this .observationRegistry = observationRegistry; } public <T> T observeAgentInteraction (String userId, String agentType, Supplier<T> interaction) { return Observation.createNotStarted("agent.interaction" , observationRegistry) .contextualName("agent-" + agentType) .lowCardinalityKeyValue("agent.type" , agentType) .highCardinalityKeyValue("user.id" , userId) .observe(() -> { long startTime = System.nanoTime(); try { T result = interaction.get(); long duration = (System.nanoTime() - startTime) / 1_000_000 ; Observation observation = Observation.start("agent.interaction" , observationRegistry); observation.highCardinalityKeyValue("duration_ms" , String.valueOf(duration)); observation.stop(); return result; } catch (Exception e) { throw e; } }); } }
使用方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Service public class CustomerServiceAgent { private final ChatClient chatClient; private final AgentInteractionObserver observer; public String handleInquiry (String userId, String message) { return observer.observeAgentInteraction(userId, "customer-service" , () -> { return chatClient.prompt() .system("你是客服 Agent,帮助用户处理订单问题" ) .user(message) .call() .content(); }); } }
Actuator 端点:实时指标查看 Spring Boot Actuator 自动暴露 AI 相关的指标。配置 management.endpoints.web.exposure.include=metrics 后,访问 /actuator/metrics/chat 就能看到:
1 2 3 4 5 6 7 8 9 10 11 12 { "name" : "chat" , "measurements" : [ { "statistic" : "COUNT" , "value" : 1234.0 } , { "statistic" : "TOTAL_TIME" , "value" : 987654.0 } , { "statistic" : "MAX" , "value" : 5678.0 } ] , "availableTags" : [ { "tag" : "gen_ai.request.model" , "values" : [ "gpt-4o" , "gpt-4o-mini" ] } , { "tag" : "gen_ai.system" , "values" : [ "openai" ] } ] }
配合 Grafana,你可以做出实时的 Agent 监控大盘。
构建成本监控系统:从指标到告警 可观测性的最终目的是让你能回答两个问题:**”花了多少钱”** 和 **”花得值不值”**。
成本计算的核心公式 1 单次请求成本 = (输入 Token 数 × 输入单价 + 输出 Token 数 × 输出单价) / 1,000,000
以 GPT-4o 为例(2026 年 OpenAI 定价):
输入:$2.50 / 1M tokens
输出:$10.00 / 1M tokens
一个典型的客服交互(3 轮 LLM 调用):
1 2 3 4 轮次1: 1200 prompt + 300 completion = $0.003 + $0.003 = $0.006 轮次2: 1500 prompt + 500 completion = $0.00375 + $0.005 = $0.00875 轮次3: 2000 prompt + 400 completion = $0.005 + $0.004 = $0.009 单次交互总成本: ~$0.024
如果日均 10,000 次交互,月成本 = $0.024 × 10,000 × 30 = $7,200/月 。
成本告警:防止”一夜破产” Agent 和传统应用不同,一个 Bug(比如推理死循环)可能在几小时内产生巨额费用。你需要设置多层告警:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 @Component public class CostAlertService { private final MeterRegistry meterRegistry; private final NotificationService notificationService; private static final double HOURLY_COST_LIMIT = 50.0 ; private static final double DAILY_COST_LIMIT = 500.0 ; private static final long TOKENS_PER_REQUEST_LIMIT = 10000 ; @Scheduled(fixedRate = 60_000) public void checkCostAlerts () { double hourlyCost = getMetricValue("llm.cost.total" , Duration.ofHours(1 )); if (hourlyCost > HOURLY_COST_LIMIT) { notificationService.sendAlert( AlertLevel.CRITICAL, String.format("⚠️ AI Agent 小时成本超限: $%.2f (阈值: $%.2f)" , hourlyCost, HOURLY_COST_LIMIT) ); } double dailyCost = getMetricValue("llm.cost.total" , Duration.ofDays(1 )); if (dailyCost > DAILY_COST_LIMIT) { notificationService.sendAlert( AlertLevel.CRITICAL, String.format("🚨 AI Agent 日成本超限: $%.2f (阈值: $%.2f)" , dailyCost, DAILY_COST_LIMIT) ); } } @Scheduled(fixedRate = 10_000) public void checkAnomalousRequests () { double maxTokens = meterRegistry.get("llm.tokens.per.request" ) .tag("statistic" , "MAX" ) .gauge().value(); if (maxTokens > TOKENS_PER_REQUEST_LIMIT) { notificationService.sendAlert( AlertLevel.WARNING, String.format("⚠️ 检测到异常大的请求: %d tokens (阈值: %d)" , (long ) maxTokens, TOKENS_PER_REQUEST_LIMIT) ); } } private double getMetricValue (String metricName, Duration duration) { return meterRegistry.get(metricName).counter().count(); } }
成本分析维度 有了结构化的指标数据,你可以从多个维度分析成本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 SELECT user_id, SUM (cost) as total_cost, COUNT (* ) as request_countFROM agent_interactionsWHERE created_at > NOW() - INTERVAL '7 days' GROUP BY user_idORDER BY total_cost DESC LIMIT 10 ; SELECT agent_type, AVG (cost) as avg_cost, AVG (total_tokens) as avg_tokensFROM agent_interactionsGROUP BY agent_type;SELECT model, COUNT (* ) as calls, SUM (cost) as total_cost, AVG (completion_tokens) as avg_output FROM llm_callsGROUP BY model;
这些分析会告诉你:是不是 80% 的请求其实用 GPT-4o-mini 就够了?是不是某个 Tool 的返回值太长导致 LLM 处理成本飙升?
生产环境的可观测性架构 把上面的组件组合起来,一个完整的 Agent 可观测性架构长这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 ┌─────────────────────────────────────────────────┐ │ AI Agent 应用 │ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ │ ChatModel│ │ Tools │ │ Memory/RAG │ │ │ │ Listener │ │ Tracing │ │ Metrics │ │ │ └────┬─────┘ └────┬─────┘ └──────┬───────┘ │ │ │ │ │ │ │ └──────┬───────┴───────────────┘ │ │ │ │ │ ┌────────▼────────┐ │ │ │ OpenTelemetry │ │ │ │ Collector │ │ │ └────────┬────────┘ │ └──────────────┼────────────────────────────────────┘ │ ┌──────────┼──────────┐ │ │ │ ▼ ▼ ▼ ┌──────┐ ┌────────┐ ┌────────┐ │Jaeger│ │Prometheus│ │ Loki │ │Trace │ │Metrics │ │ Logs │ └──┬───┘ └───┬────┘ └───┬────┘ │ │ │ └──────────┼────────────┘ │ ┌────▼────┐ │ Grafana │ │Dashboard│ └─────────┘
Grafana 大盘设计 一个实用的 Agent 监控大盘应该包含以下面板:
行 1:全局概览
今日总请求数(折线图)
今日总 Token 消耗(折线图)
今日总成本(单值面板,带阈值颜色)
P50/P99 响应延迟(折线图)
行 2:LLM 调用详情
各模型调用占比(饼图)
Token 消耗分布(直方图)
LLM 调用错误率(折线图)
平均推理轮次(折线图)
行 3:Tool 调用监控
各 Tool 调用次数(条形图)
Tool 调用延迟 Top 10(条形图)
Tool 调用失败率(折线图)
行 4:用户分析
Top 10 高消费用户(条形图)
按用户类型的成本分布(饼图)
异常请求列表(表格)
源码级解读:LangChain4j Listener 机制的内部实现 要真正用好 ChatModelListener,需要理解它在 LangChain4j 内部是怎么被触发的。我们看一下关键的调用链路。
当你通过 DefaultAiServices 调用 Agent 时,内部流程是:
1 2 3 4 5 6 7 AiService.invoke() → DefaultAiServices.create() // 生成代理对象 → ChatLanguageModel.generate() // 调用 LLM → AbstractChatModel.execute() // 统一的调用入口 → onRequest(Listeners) // 触发所有 Listener 的 onRequest → doGenerate() // 实际的 HTTP 调用 → onResponse(Listeners) // 触发所有 Listener 的 onResponse
核心逻辑在 AbstractChatModel(或具体实现如 OpenAiChatModel)中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public ChatResponse generate (ChatRequest request) { ChatModelRequestContext requestContext = new ChatModelRequestContext (request, attributes); for (ChatModelListener listener : listeners) { try { listener.onRequest(requestContext); } catch (Exception e) { log.warn("Error in ChatModelListener.onRequest" , e); } } try { ChatResponse response = doGenerate(request); ChatModelResponseContext responseContext = new ChatModelResponseContext ( request, response, attributes); for (ChatModelListener listener : listeners) { try { listener.onResponse(responseContext); } catch (Exception e) { log.warn("Error in ChatModelListener.onResponse" , e); } } return response; } catch (Exception e) { ChatModelErrorContext errorContext = new ChatModelErrorContext (e, attributes); for (ChatModelListener listener : listeners) { try { listener.onError(errorContext); } catch (Exception ex) { log.warn("Error in ChatModelListener.onError" , ex); } } throw e; } }
关键设计点 :
attributes Map 在整个生命周期中共享 :onRequest 阶段写入的数据,onResponse 和 onError 阶段能读到。这就是为什么我们可以用 context.attributes().put("startTime", ...) 来传递计时信息。
Listener 异常不影响主流程 :每个 Listener 的调用都被 try-catch 包裹,一个 Listener 报错不会阻断其他 Listener 或 LLM 调用。这很重要——你的监控代码不应该拖垮业务。
Listener 是同步执行的 :在当前版本(1.0.0-beta1)中,Listener 是在调用线程中同步触发的。如果你的 Listener 做了耗时操作(比如写数据库),会增加 LLM 调用的总延迟。最佳实践是 Listener 里只做内存操作(更新指标计数器),把持久化推到异步线程。
实战:从零搭建 Agent 监控系统 把上面的所有组件串起来,我们来构建一个完整的监控方案。
第一步:依赖配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-open-ai</artifactId > <version > 1.0.0-beta1</version > </dependency > <dependency > <groupId > dev.langchain4j</groupId > <artifactId > langchain4j-opentelemetry</artifactId > <version > 1.0.0-beta1</version > </dependency > <dependency > <groupId > io.micrometer</groupId > <artifactId > micrometer-registry-prometheus</artifactId > </dependency > <dependency > <groupId > io.micrometer</groupId > <artifactId > micrometer-tracing-bridge-otel</artifactId > </dependency > <dependency > <groupId > io.opentelemetry</groupId > <artifactId > opentelemetry-exporter-otlp</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency >
第二步:配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 management: endpoints: web: exposure: include: health, metrics, prometheus metrics: tags: application: ai-agent-service tracing: sampling: probability: 1.0 otel: exporter: otlp: endpoint: http://otel-collector:4317 service: name: ai-agent-service agent: cost: hourly-limit: 50.0 daily-limit: 500.0 per-request-token-limit: 10000
第三步:完整配置类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Configuration @EnableScheduling public class AgentObservabilityConfig { @Bean public ChatModelListener chatModelListener (MeterRegistry meterRegistry) { return new AgentObservabilityListener (meterRegistry); } @Bean public ChatLanguageModel chatModel (ChatModelListener listener) { return OpenAiChatModel.builder() .apiKey(System.getenv("OPENAI_API_KEY" )) .modelName("gpt-4o" ) .listeners(List.of(listener)) .build(); } @Bean public MeterFilter costTagFilter () { return MeterFilter.commonTags(Tags.of("component" , "ai-agent" )); } }
第四步:验证 启动应用后,验证指标是否正常采集:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 curl -s http://localhost:8080/actuator/prometheus | grep llm_ curl -s http://localhost:8080/actuator/health | jq . curl -X POST http://localhost:8080/api/chat \ -H "Content-Type: application/json" \ -d '{"userId": "test-user", "message": "你好"}'
生产环境最佳实践与踩坑指南 踩坑一:Listener 里的阻塞操作 问题 :在 ChatModelListener 的 onResponse 里做了数据库写入(记录每次 LLM 调用的详细信息),导致 P99 延迟从 2 秒飙升到 5 秒。
原因 :Listener 是同步执行的,数据库写入直接叠加在 LLM 调用延迟上。
解决 :Listener 里只做内存操作(更新 Micrometer 计数器),把持久化推到异步:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Component public class AgentObservabilityListener implements ChatModelListener { private final MeterRegistry meterRegistry; private final ExecutorService asyncWriter = Executors.newFixedThreadPool(2 ); @Override public void onResponse (ChatModelResponseContext context) { meterRegistry.counter("llm.tokens.total" ).increment(tokenUsage.inputTokenCount()); asyncWriter.submit(() -> { llmCallLogRepository.save(buildLogFromContext(context)); }); } }
踩坑二:高基数指标爆炸 问题 :把 userId 作为 Metric Tag,导致 Prometheus 时间序列数量暴增,内存占用飙升。
原因 :每个唯一的 userId 都会创建一组新的时间序列。10 万用户 × 5 个指标 = 50 万条时间序列。
解决 :userId 应该用在日志和 Trace 中(高基数数据),不要用在 Metric Tag 中。Metric Tag 只放有限枚举值(模型名、Agent 类型、状态码)。
踩坑三:采样率配置不当 问题 :生产环境 100% 采样,Trace 数据量太大,Jaeger 存储撑不住。
解决 :分层采样——错误请求 100% 采样,正常请求按比例采样:
1 2 3 4 5 6 7 @Bean public Sampler traceSampler () { return Sampler.parentBased( Sampler.traceIdRatioBased(0.1 ) ); }
踩坑四:成本计算模型不准确 问题 :用固定的 per-token 价格计算成本,但实际计费包含了缓存命中折扣、批量 API 折扣等,导致成本数据偏差 30%+。
解决 :定期(每天或每周)用 Provider 的 Billing API 拉取真实费用,和监控系统计算的费用做对比校准:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Scheduled(cron = "0 0 2 * * *") public void calibrateCost () { double providerCost = openAIBillingApi.getDailyCost(LocalDate.now().minusDays(1 )); double monitoredCost = getMonitoredCost(LocalDate.now().minusDays(1 )); double discrepancy = Math.abs(providerCost - monitoredCost) / providerCost; if (discrepancy > 0.1 ) { log.warn("Cost calibration off by {}%, provider: ${}, monitored: ${}" , String.format("%.1f" , discrepancy * 100 ), String.format("%.2f" , providerCost), String.format("%.2f" , monitoredCost)); } }
技术边界与未来展望 什么时候不需要这么重的方案?
开发/测试环境 :ChatModelListener + 日志就够了,不需要 OpenTelemetry
个人项目/低流量 :Micrometer + Prometheus 的基础指标足够
纯内部工具 :成本监控可以简化为每天看一眼 Provider 后台账单
当前方案的局限
**无法追踪 Token 的”质量”**:你能看到消耗了 1000 tokens,但不知道这些 token 有多少是”有效推理”、多少是”废话”
跨 Provider 的成本标准化困难 :OpenAI、Anthropic、Google 的计费模型不同,统一比较需要抽象层
**Agent 行为的”可解释性”**:Trace 能告诉你 Agent 调用了什么工具,但不能告诉你”为什么”做出这个决策
未来方向
AI-native 可观测性 :用 AI 分析 Agent 的行为模式,自动发现异常(比如”Agent 突然开始大量调用一个之前很少用的 Tool”)
成本预测模型 :基于历史数据预测未来成本,提前告警
Token 效率优化 :通过分析 Trace 数据,自动优化 Prompt 模板减少 Token 消耗
语义级 Trace :不仅追踪调用链路,还能追踪”推理链路”——Agent 的每一步推理逻辑
总结 Agent 系统的可观测性不是”锦上添花”,而是”生存必需”。没有它,你无法回答三个关键问题:
Agent 在干什么? ——链路追踪告诉你每一步推理和工具调用
Agent 花了多少钱? ——Token 指标和成本计算告诉你真实的财务影响
Agent 哪里出了问题? ——错误率、延迟、异常请求告诉你哪里需要优化
LangChain4j 的 ChatModelListener 是最灵活的观测入口,Spring AI 的 Micrometer 集成是 Spring 生态最自然的选择,OpenTelemetry 是连接一切的标准化协议。三者结合,你就能构建一个覆盖”开发 → 测试 → 生产”全链路的 Agent 可观测体系。
记住那句老话:你无法改善你无法度量的东西 。在 Agent 时代,这句话比以往任何时候都更重要。
下一篇我们聊聊 AI Agent 的流式输出与实时交互——当 Agent 需要实时”说话”而不是”说完再回复”时,架构该如何设计?