Prometheus 和 Grafana 是一个标准化监控工作台。Prometheus 负责“生产数据”并暴露接口,Grafana 负责“消费数据”并渲染视图,两者通过标准的 HTTP API 和查询语言(PromQL)紧密耦合,形成了一个“存储-查询-展示”的闭环生态。本文主要介绍两者在Spring Boot + AI Agent 项目下的是如何接入的。

本文从一个 Spring Boot + ADK Agent 项目中,提炼一套可以复用的监控配置模板。

这套方案的目标很明确:用 Micrometer 采集业务指标,用 Spring Boot Actuator 暴露 Prometheus 指标,再用 Grafana 做可视化看板。

接入后,我们可以回答这些问题:

  1. Agent 是否真正运行过。
  2. 用户消息是否进入 Agent 执行链路。
  3. LLM 调用了多少次。
  4. LLM 每次调用耗时是多少。
  5. MCP 工具有没有被调用。
  6. 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;
    }
}

这里的标签要克制。推荐放 applicationagentagentIdmodeltoolstatus 这类稳定字段。不要把用户 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.060.07 ops/s 是正常现象。

问题五:Dashboard JSON 导入失败。

优先检查三件事:

  1. JSON 文件是否是 UTF-8。
  2. 中文标题是否乱码导致字符串不完整。
  3. PromQL 里是否漏了指标名。

十三、总结

这套模板的核心思路是:应用负责产生指标,Actuator 负责暴露指标,Prometheus 负责抓取指标,Grafana 负责展示指标。

对于 AI Agent 项目,最容易误解的一点是:启动不等于调用 LLM。

启动后能看到 ai_agent_assembly_total,说明 Agent 已经装配完成。只有真正执行 runner.runAsync(...),才会看到用户消息、Agent 运行、LLM 调用、工具调用这些业务指标。

一套实用的 AI Agent 监控看板,至少应该包含:

  1. Agent 装配数。
  2. 用户消息数。
  3. Agent 运行数。
  4. LLM 调用次数。
  5. LLM 调用耗时。
  6. MCP 工具调用次数。
  7. JVM 内存。
  8. HTTP 请求速率。

有了这些指标,我们的 AI Agent 项目就不再只是“能跑”,而是能被持续观察、定位和优化。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐