Prometheus 和 Grafana 在 Agent 项目中的应用
Prometheus 和 Grafana 是一个标准化监控工作台。Prometheus 负责“生产数据”并暴露接口,Grafana 负责“消费数据”并渲染视图,两者通过标准的 HTTP API 和查询语言(PromQL)紧密耦合,形成了一个“存储-查询-展示”的闭环生态。本文主要介绍两者在Spring Boot + AI Agent 项目下的是如何接入的。
本文从一个 Spring Boot + ADK Agent 项目中,提炼一套可以复用的监控配置模板。
这套方案的目标很明确:用 Micrometer 采集业务指标,用 Spring Boot Actuator 暴露 Prometheus 指标,再用 Grafana 做可视化看板。
接入后,我们可以回答这些问题:
- Agent 是否真正运行过。
- 用户消息是否进入 Agent 执行链路。
- LLM 调用了多少次。
- LLM 每次调用耗时是多少。
- MCP 工具有没有被调用。
- JVM、HTTP 请求这些基础指标是否正常。
一、整体监控链路
这套监控链路可以理解为:
Spring Boot 应用
-> Micrometer 记录业务指标
-> Actuator 暴露 /actuator/prometheus
-> Prometheus 定时抓取
-> Grafana 使用 PromQL 展示看板
在 AI Agent 项目里,建议至少采集这些业务指标:
用户消息数:
Micrometer: ai.agent.user.message
Prometheus: ai_agent_user_message_total
Agent 运行数:
Micrometer: ai.agent.agent.run
Prometheus: ai_agent_agent_run_total
LLM 调用数:
Micrometer: ai.agent.llm.call
Prometheus: ai_agent_llm_call_total
LLM 调用耗时:
Micrometer: ai.agent.llm.duration
Prometheus: ai_agent_llm_duration_seconds_count
Prometheus: ai_agent_llm_duration_seconds_sum
Prometheus: ai_agent_llm_duration_seconds_max
MCP 工具调用数:
Micrometer: ai.agent.tool.call
Prometheus: ai_agent_tool_call_total
Agent 装配数:
Micrometer: ai.agent.assembly
Prometheus: ai_agent_assembly_total
Agent 装配耗时:
Micrometer: ai.agent.assembly.duration
Prometheus: ai_agent_assembly_duration_seconds_count
Prometheus: ai_agent_assembly_duration_seconds_sum
Prometheus: ai_agent_assembly_duration_seconds_max
这里有一个 Micrometer 到 Prometheus 的命名规则要注意:Micrometer 里的点号会变成下划线,Counter 会追加 _total,Timer 会生成 _seconds_count、_seconds_sum、_seconds_max 等序列。
二、引入依赖
在 Spring Boot 应用模块加入 Actuator 和 Prometheus Registry。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
如果采用DDD架构,业务指标封装在独立 domain 模块,可以在 domain 模块加入 Micrometer Core。
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>
三、暴露 Prometheus 指标端点
在 application.yml 中开启 Actuator 端点暴露。
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
tags:
application: your-ai-agent-app
启动应用后访问:
curl http://localhost:8091/actuator/prometheus
如果能看到 JVM、HTTP 等指标,说明基础监控链路已经生效。
jvm_memory_used_bytes{area="heap",...}
http_server_requests_seconds_count{...}
四、封装业务指标服务
建议把业务指标统一封装在一个服务里。这样业务代码不会到处散落指标名,后续维护也更轻松。
package com.example.agent.monitor;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;
@Service
public class AgentMetricsService {
private final MeterRegistry meterRegistry;
public AgentMetricsService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
public void recordUserMessage() {
meterRegistry.counter("ai.agent.user.message").increment();
}
public void recordAgentRun(String agentName) {
meterRegistry.counter("ai.agent.agent.run", "agent", safe(agentName)).increment();
}
public void recordLlmCall(String model) {
meterRegistry.counter("ai.agent.llm.call", "model", safe(model)).increment();
}
public Timer.Sample startLlmCall() {
return Timer.start(meterRegistry);
}
public void stopLlmCall(Timer.Sample sample, String model) {
sample.stop(meterRegistry.timer("ai.agent.llm.duration", "model", safe(model)));
}
public void recordToolCall(String toolName) {
meterRegistry.counter("ai.agent.tool.call", "tool", safe(toolName)).increment();
}
public void recordAssembly(String agentId) {
meterRegistry.counter("ai.agent.assembly", "agentId", safe(agentId)).increment();
}
public Timer.Sample startAssembly() {
return Timer.start(meterRegistry);
}
public void stopAssembly(Timer.Sample sample) {
sample.stop(meterRegistry.timer("ai.agent.assembly.duration"));
}
private String safe(String value) {
return value == null || value.isBlank() ? "unknown" : value;
}
}
这里的标签要克制。推荐放 application、agent、agentId、model、tool、status 这类稳定字段。不要把用户 ID、session ID、trace ID、完整 prompt、错误堆栈放到标签里,否则 Prometheus 的时间序列数量会暴涨。
五、用 ADK 插件采集 Agent 生命周期指标
如果项目使用 Google ADK,可以把监控封装成一个 BasePlugin。这样每次用户消息、Agent 执行、LLM 调用、工具调用都会自动进入监控逻辑。
package com.example.agent.monitor;
import com.google.adk.agents.BaseAgent;
import com.google.adk.agents.CallbackContext;
import com.google.adk.agents.InvocationContext;
import com.google.adk.models.LlmRequest;
import com.google.adk.models.LlmResponse;
import com.google.adk.plugins.BasePlugin;
import com.google.adk.tools.BaseTool;
import com.google.adk.tools.ToolContext;
import com.google.genai.types.Content;
import io.micrometer.core.instrument.Timer;
import io.reactivex.rxjava3.core.Maybe;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service("metricsPlugin")
public class MetricsPlugin extends BasePlugin {
private final AgentMetricsService metricsService;
private final ThreadLocal<LlmCallState> llmCallHolder = new ThreadLocal<>();
public MetricsPlugin(AgentMetricsService metricsService) {
super("MetricsPlugin");
this.metricsService = metricsService;
}
@Override
public Maybe<Content> onUserMessageCallback(InvocationContext invocationContext, Content userMessage) {
metricsService.recordUserMessage();
return Maybe.empty();
}
@Override
public Maybe<Content> beforeAgentCallback(BaseAgent agent, CallbackContext callbackContext) {
metricsService.recordAgentRun(agent.name());
return Maybe.empty();
}
@Override
public Maybe<LlmResponse> beforeModelCallback(CallbackContext callbackContext, LlmRequest llmRequest) {
String model = llmRequest.model().orElse("unknown");
metricsService.recordLlmCall(model);
llmCallHolder.set(new LlmCallState(metricsService.startLlmCall(), model));
return Maybe.empty();
}
@Override
public Maybe<LlmResponse> afterModelCallback(CallbackContext callbackContext, LlmResponse llmResponse) {
LlmCallState state = llmCallHolder.get();
if (state != null) {
llmCallHolder.remove();
metricsService.stopLlmCall(state.sample, state.model);
}
return Maybe.empty();
}
@Override
public Maybe<Map<String, Object>> beforeToolCallback(
BaseTool tool,
Map<String, Object> toolArgs,
ToolContext toolContext) {
metricsService.recordToolCall(tool.name());
return Maybe.empty();
}
private static class LlmCallState {
private final Timer.Sample sample;
private final String model;
private LlmCallState(Timer.Sample sample, String model) {
this.sample = sample;
this.model = model;
}
}
}
六、在 Agent 配置中注册插件
把 metricsPlugin 加到 runner 的插件列表里。
ai-agent:
tables:
your-agent:
app-name: yourAgentApp
agent:
agent-id: "100003"
agent-name: "单一智能体"
agent-desc: "示例智能体"
module:
agents:
- name: onlyAgent
description: 示例 Agent
instruction: |
你是一个帮助用户完成任务的智能体。
chat-model:
model: glm-4.5
tool-mcp-list: []
runner:
agent-name: onlyAgent
plugin-name-list:
- metricsPlugin
这里要特别注意:创建 ChatModel 和创建 Runner 不等于调用 LLM。只有真正执行 runner.runAsync(...),才会触发用户消息、Agent 运行、LLM 调用这些指标。
七、记录 Agent 装配指标
Agent 启动时一般会做配置装配、模型装配、工具装配和 Runner 装配。这个过程也可以记录。
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;
@Service
public class ArmoryService {
private final AgentMetricsService metricsService;
public ArmoryService(AgentMetricsService metricsService) {
this.metricsService = metricsService;
}
public void assembleAgent(String agentId, Runnable assembleAction) {
Timer.Sample sample = metricsService.startAssembly();
try {
assembleAction.run();
} finally {
metricsService.recordAssembly(agentId);
metricsService.stopAssembly(sample);
}
}
}
应用启动后,通常会先看到这些装配指标。
ai_agent_assembly_total
ai_agent_assembly_duration_seconds_count
ai_agent_assembly_duration_seconds_sum
ai_agent_assembly_duration_seconds_max
但是不会自动看到 LLM 调用指标。LLM 指标必须等真实 Agent 调用发生后才会出现。
八、提供一个开发环境验证接口
为了方便验证监控是否生效,可以提供一个手动触发 Agent 的接口。这个接口建议只在开发环境或测试环境打开。
package com.example.agent.web;
import com.google.adk.events.Event;
import com.google.adk.runner.InMemoryRunner;
import com.google.adk.sessions.Session;
import com.google.genai.types.Content;
import com.google.genai.types.Part;
import io.reactivex.rxjava3.core.Flowable;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/api/agent")
public class AgentTriggerController {
private final ApplicationContext applicationContext;
public AgentTriggerController(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@PostMapping("/run/{agentId}")
public String run(
@PathVariable String agentId,
@RequestParam(defaultValue = "给我一份学习计划") String message) {
AiAgentRegisterVO register = applicationContext.getBean(agentId, AiAgentRegisterVO.class);
String appName = register.getAppName();
InMemoryRunner runner = register.getRunner();
Session session = runner.sessionService()
.createSession(appName, "demo-user")
.blockingGet();
Content userMsg = Content.fromParts(Part.fromText(message));
Flowable<Event> events = runner.runAsync("demo-user", session.id(), userMsg);
List<String> outputs = new ArrayList<>();
events.blockingForEach(event -> outputs.add(event.stringifyContent()));
return String.join("\n", outputs);
}
}
调用示例:
curl -X POST "http://localhost:8091/api/agent/run/100003?message=把yonren小写转大写"
调用成功后,等待 Prometheus 下一次抓取,再看 Grafana 面板。
九、配置 Prometheus
在 Prometheus 的 prometheus.yml 中加入抓取配置。
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: ai-agent-scaffold
metrics_path: /actuator/prometheus
static_configs:
- targets:
- "<应用主机>:8091"
labels:
application: your-ai-agent-app
如果 Prometheus 在 Docker 中运行,而应用跑在宿主机,可以使用:
targets:
- "host.docker.internal:8091"
如果 Prometheus 和应用都直接运行在本机,可以使用:
targets:
- "localhost:8091"
验证 Prometheus 是否抓取成功:
curl http://localhost:9090/api/v1/targets
也可以打开 Prometheus 页面查看:
http://localhost:9090/targets
目标状态为 UP,才代表 Prometheus 已经成功抓到应用指标。
十、Grafana 常用 PromQL
Agent 装配总数:
sum(ai_agent_assembly_total) or vector(0)
近 1 小时用户消息数:
sum(increase(ai_agent_user_message_total[1h])) or vector(0)
LLM 调用次数,5 分钟速率:
sum by (model) (rate(ai_agent_llm_call_total[5m])) or vector(0)
LLM 最大耗时:
ai_agent_llm_duration_seconds_max or vector(0)
LLM 平均耗时:
(ai_agent_llm_duration_seconds_sum / ai_agent_llm_duration_seconds_count) or vector(0)
Agent 运行数,5 分钟速率:
sum by (agent) (rate(ai_agent_agent_run_total[5m])) or vector(0)
MCP 工具调用数,5 分钟速率:
sum by (tool) (rate(ai_agent_tool_call_total[5m])) or vector(0)
JVM 内存:
sum by (area) (jvm_memory_used_bytes)
HTTP 请求速率:
sum by (status) (rate(http_server_requests_seconds_count[5m]))
这些查询里经常会看到 or vector(0)。它的作用是兜底显示 0。因为应用刚启动时,如果还没有调用过 LLM,Prometheus 中就不会有 ai_agent_llm_call_total 这条序列。没有兜底时 Grafana 会显示 No data,很容易被误解为监控链路坏了。
十一、完整验证流程
第一步,确认应用暴露指标。
curl http://localhost:8091/actuator/prometheus
Windows PowerShell 可以这样看 AI Agent 指标:
curl.exe -s http://localhost:8091/actuator/prometheus | findstr /R "^ai_agent"
刚启动时,通常只会看到装配指标。
ai_agent_assembly_total{agentId="100003",application="your-ai-agent-app"} 1.0
ai_agent_assembly_duration_seconds_count{application="your-ai-agent-app"} 1
第二步,触发一次 Agent 调用。
curl -X POST "http://localhost:8091/api/agent/run/100003?message=把yonren小写转大写"
第三步,再查看指标。
curl http://localhost:8091/actuator/prometheus
正常情况下,此时会多出这些指标。
ai_agent_user_message_total
ai_agent_agent_run_total
ai_agent_llm_call_total
ai_agent_llm_duration_seconds_count
ai_agent_llm_duration_seconds_sum
第四步,在 Prometheus 中查询。
curl "http://localhost:9090/api/v1/query?query=ai_agent_llm_call_total"
或者打开 Prometheus Graph 页面:
http://localhost:9090/graph
十二、常见问题
问题一:Prometheus target 是 UP,但 Grafana 没数据。
先检查 PromQL。Grafana Explore 如果生成了下面这种表达式,说明没有选择指标名。
sum(rate([$__interval]))
正确写法应该带上指标名。
sum(rate(ai_agent_llm_call_total[$__interval]))
问题二:应用启动了,为什么 LLM 调用次数还是 0?
启动应用只会完成 Agent 装配,不等于调用 LLM。创建模型和创建 Runner 都不会触发 LLM 请求。
new OpenAiChatModel(...)
new InMemoryRunner(...)
真正触发 LLM 调用的是:
runner.runAsync(userId, sessionId, userMessage)
问题三:测试用例调用过 LLM,为什么 Grafana 还是 0?
Prometheus 抓的是常驻服务的 /actuator/prometheus。如果 LLM 调用发生在 JUnit 测试进程里,测试进程结束后,内存中的 Micrometer 指标也会消失。
要让 Grafana 看到数据,必须调用 Prometheus 正在抓取的那个应用实例。
问题四:HTTP 请求速率为什么不是 0?
Prometheus 自己会定时访问 /actuator/prometheus,这也会产生 HTTP 请求指标。假设抓取间隔是 15 秒,每分钟大约 4 次请求。
4 / 60 = 0.0667 ops/s
所以 Grafana 上看到 0.06 到 0.07 ops/s 是正常现象。
问题五:Dashboard JSON 导入失败。
优先检查三件事:
- JSON 文件是否是 UTF-8。
- 中文标题是否乱码导致字符串不完整。
- PromQL 里是否漏了指标名。
十三、总结
这套模板的核心思路是:应用负责产生指标,Actuator 负责暴露指标,Prometheus 负责抓取指标,Grafana 负责展示指标。
对于 AI Agent 项目,最容易误解的一点是:启动不等于调用 LLM。
启动后能看到 ai_agent_assembly_total,说明 Agent 已经装配完成。只有真正执行 runner.runAsync(...),才会看到用户消息、Agent 运行、LLM 调用、工具调用这些业务指标。
一套实用的 AI Agent 监控看板,至少应该包含:
- Agent 装配数。
- 用户消息数。
- Agent 运行数。
- LLM 调用次数。
- LLM 调用耗时。
- MCP 工具调用次数。
- JVM 内存。
- HTTP 请求速率。
有了这些指标,我们的 AI Agent 项目就不再只是“能跑”,而是能被持续观察、定位和优化。
更多推荐
所有评论(0)