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

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐