(第二篇)Spring AI 架构设计与优化:可观察性体系,打造全链路可视化的 AI 运维方案

摘要

大家好,我是深耕 AI 服务架构的后端开发。在上一篇文章里,我给大家分享了 Spring AI 从单实例到万级 QPS 分布式架构的演进方案。但架构搭完只是开始,线上运维才是真正的考验:用户反馈 AI 回答慢,却分不清是网关、向量检索还是模型调用拖了后腿;月底模型 API 账单超支,却查不到是哪个接口、哪个场景消耗了最多 Token;偶尔出现的模型调用异常,复现不了也找不到根因,只能干着急。

这些问题的核心,就是 AI 服务缺少一套专属的可观察性体系。通用的 Java 服务监控只能看 JVM、CPU 这些基础指标,完全覆盖不了 AI 服务的核心痛点。这篇文章我会结合半年的生产环境实战经验,从指标监控、全链路追踪、规范化日志三大维度,给大家讲透 Spring AI 可观察性体系的搭建,最后带大家一步步落地一套可直接复用的 AI 服务监控大屏,附完整 Grafana 模板。全文都是生产环境验证过的干货,配套手绘 SVG 插图,看完就能直接落地。

1. 引言:为什么 AI 服务必须做专属可观察性体系?

先给大家看看我半年前的运维噩梦:

  • 线上用户反馈问答接口响应慢,我翻遍了 JVM 监控,CPU、内存、GC 全正常,完全不知道问题出在哪;
  • 月底 OpenAI 账单直接超了预算 3 倍,翻了半天日志,也算不清是哪个业务场景、哪个模型版本消耗的 Token 最多;
  • 凌晨收到告警,有 10% 的模型调用失败,等我爬起来看的时候,异常又消失了,没有完整的 Prompt 和响应日志,根本没法复现。

相信做 AI 服务的兄弟们都遇到过类似的问题。为什么通用的 Spring Boot 监控方案解决不了这些问题?核心原因是AI 服务的核心链路和瓶颈点,和普通 Java 服务完全不一样:

  1. 核心链路更长:用户请求→网关→业务服务→向量检索服务→模型调用服务→第三方大模型 API,任何一个环节出问题,都会影响最终体验;
  2. 核心指标特殊:除了常规的耗时、成功率,AI 服务最核心的是 Token 消耗量、模型版本适配、向量检索命中率、Prompt 合规性这些专属指标;
  3. 问题排查更复杂:模型调用失败、响应慢,可能是 Prompt 的问题、模型限流的问题、网络的问题,甚至是向量检索返回的上下文太多的问题,没有全链路数据根本定位不了。

所以,我们必须给 Spring AI 服务搭建一套专属的可观察性体系,从指标、链路、日志三个维度,实现全链路可视化,让线上的每一个请求、每一次模型调用都可追溯、可排查、可审计。

2. 核心架构概览:Spring AI 全链路可观察性体系长啥样?

先给大家看一下我们最终落地的可观察性架构拓扑图,这套体系稳定运行了半年,线上 90% 的问题都能在 5 分钟内定位根因:

整个体系分为四大核心模块,从上到下层层递进:

  1. 业务层:Spring AI 分布式服务集群,是可观察性数据的来源;
  2. 采集层:分为三大核心 ------Micrometer 指标采集、Sleuth 链路追踪、Logback 规范日志,覆盖可观察性的三大支柱;
  3. 存储层:Prometheus 存储时序指标数据,ELK 存储日志数据,Zipkin 存储链路数据;
  4. 可视化层:Grafana 统一做可视化展示、告警配置、根因分析,实现全链路一站式运维。

接下来我会给大家拆解每个模块的落地细节,从代码实现到踩坑经验,全部分享给大家。

3. 指标监控:Micrometer 集成模型调用专属 Metrics

3.1 AI 服务到底要监控哪些指标?通用监控远远不够

很多人做 AI 服务监控,上来就搭一套 JVM、CPU、内存的基础监控,结果线上出问题还是两眼一抹黑。对于 Spring AI 服务来说,我们要监控的是和业务、成本、体验强相关的专属指标,我把它分为四大类:

表格

指标分类 核心监控项 指标意义
模型调用核心指标 调用 QPS、成功率、平均耗时 / P95/P99 耗时、Token 总消耗量(Prompt/Completion 拆分)、模型版本调用占比 直接决定用户体验和 API 成本,是 AI 服务最核心的指标
向量检索指标 检索 QPS、成功率、平均耗时、向量缓存命中率、Milvus 查询耗时、TopK 匹配度 定位检索瓶颈,优化 RAG 效果的核心依据
业务指标 会话数、用户提问频次、接口调用排行、异常请求占比 掌握业务健康度,定位异常用户和接口
基础资源指标 JVM 堆内存 / GC、CPU / 磁盘使用率、线程池活跃度、数据库连接池状态 保障服务基础稳定性

3.2 Micrometer 集成 Spring AI:核心代码实现

Spring Boot 默认集成了 Micrometer,它是一套标准化的指标采集门面,可以无缝对接 Prometheus、InfluxDB 等时序数据库。我们要做的,就是给 Spring AI 的模型调用、向量检索等核心操作,加上自定义的指标采集。

首先,先引入核心依赖(Spring Boot 3.x+ 版本):

XML 复制代码
<!-- Micrometer 核心依赖 -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-core</artifactId>
</dependency>
<!-- Prometheus 对接依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<!-- Spring AI 核心依赖 -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

接下来,最核心的一步:给 Spring AI 的 ChatClient 加上调用拦截器 ,在模型调用前后采集指标。Spring AI 1.0 + 版本提供了ChatClientBuilder的自定义拦截器能力,我们可以直接实现:

java 复制代码
@Component
public class ModelMetricsInterceptor implements ChatClientInterceptor {

    private final MeterRegistry meterRegistry;

    // 定义核心指标
    private final Timer modelCallTimer;
    private final Counter modelCallSuccessCounter;
    private final Counter modelCallFailCounter;
    private final Counter tokenUsageCounter;

    public ModelMetricsInterceptor(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        // 模型调用耗时计时器,带标签:模型名称、接口名称
        this.modelCallTimer = Timer.builder("ai.model.call.duration")
                .description("模型调用耗时")
                .publishPercentiles(0.5, 0.75, 0.95, 0.99)
                .register(meterRegistry);
        // 模型调用成功计数器
        this.modelCallSuccessCounter = Counter.builder("ai.model.call.success")
                .description("模型调用成功次数")
                .register(meterRegistry);
        // 模型调用失败计数器
        this.modelCallFailCounter = Counter.builder("ai.model.call.fail")
                .description("模型调用失败次数")
                .register(meterRegistry);
        // Token消耗计数器,拆分Prompt和Completion
        this.tokenUsageCounter = Counter.builder("ai.model.token.usage")
                .description("模型Token消耗量")
                .register(meterRegistry);
    }

    @Override
    public ClientResponse intercept(ClientRequest request, Chain chain) {
        // 记录开始时间
        long startTime = System.currentTimeMillis();
        String modelName = request.model();
        try {
            // 执行模型调用
            ClientResponse response = chain.next(request);
            // 计算耗时,记录Timer指标,带上模型名称标签
            long duration = System.currentTimeMillis() - startTime;
            modelCallTimer.record(Duration.ofMillis(duration));
            // 记录成功次数,带上模型名称标签
            modelCallSuccessCounter.tag("model", modelName).increment();
            // 记录Token消耗,拆分Prompt和Completion
            if (response.metadata() != null && response.metadata().usage() != null) {
                long promptTokens = response.metadata().usage().getPromptTokens();
                long completionTokens = response.metadata().usage().getCompletionTokens();
                tokenUsageCounter.tag("model", modelName).tag("type", "prompt").increment(promptTokens);
                tokenUsageCounter.tag("model", modelName).tag("type", "completion").increment(completionTokens);
            }
            return response;
        } catch (Exception e) {
            // 记录失败次数,带上模型名称和异常类型标签
            modelCallFailCounter.tag("model", modelName).tag("exception", e.getClass().getSimpleName()).increment();
            throw e;
        }
    }
}

然后,把这个拦截器注入到 Spring AI 的 ChatClient 中,让它生效:

java 复制代码
@Configuration
public class SpringAIConfig {

    @Bean
    public ChatClient chatClient(OpenAiChatModel openAiChatModel, ModelMetricsInterceptor metricsInterceptor) {
        return ChatClient.builder(openAiChatModel)
                .defaultInterceptor(metricsInterceptor)
                .build();
    }
}

最后,在 application.yml 中开启 Actuator 的 Prometheus 端点:

bash 复制代码
management:
  endpoints:
    web:
      exposure:
        include: prometheus,health,info
  metrics:
    tags:
      application: ${spring.application.name}
    export:
      prometheus:
        enabled: true

配置完成后,启动服务,访问http://服务IP:端口/actuator/prometheus,就能看到我们自定义的 AI 服务指标了,Prometheus 可以直接抓取这些指标。

3.3 指标标签设计:细粒度定位问题的关键

指标能不能帮你快速定位问题,标签设计是核心。很多人只加了指标,没加标签,结果只能看到整体的耗时,却不知道是哪个模型、哪个接口出了问题。

给大家分享我们生产环境在用的标签设计规范,核心原则是低基数、高维度、可过滤:

  1. 必加基础标签 :application(服务名)、env(环境:dev/test/prod)、host(主机 IP)
  2. 模型调用指标标签 :model(模型名称,如 gpt-3.5-turbo)、interface(业务接口名)、exception(异常类型)
  3. Token 消耗指标标签 :model(模型名称)、type(token 类型:prompt/completion)、business(业务场景)
  4. 向量检索指标标签 :index(向量库名)、topK、hit(是否命中缓存)

重点提醒:绝对不要把 userId、requestId 这种高基数的字段加到标签里,我之前踩过这个坑,把 userId 加到了标签里,结果一周后 Prometheus 的时序数据直接爆了,内存占用飙升了 10 倍,最后直接 OOM 了。高基数标签会导致时序数据量呈指数级增长,一定要避免。

3.4 踩坑实录:高基数标签把 Prometheus 搞崩了

刚才提到的这个坑,我单独拿出来给大家详细说说,避免大家踩同样的坑。

当时为了统计每个用户的 Token 消耗,我直接把userId加到了ai.model.token.usage指标的标签里。一开始用户量少,没什么问题,后来用户量涨到了几万,Prometheus 的内存占用每天涨几个 G,最后直接 OOM 崩溃了。

后来查了官方文档才知道,Prometheus 的每一组标签组合,都会生成一条独立的时序数据。比如有 10 个模型,2 种 token 类型,5 万个用户,就会生成10*2*50000=100万条时序数据,Prometheus 根本扛不住。

解决方案:

  1. 去掉标签里的高基数字段(userId、requestId 等),只保留低基数的维度;
  2. 用户级别的统计,放到日志里做,用 ELK 来统计,不要用 Prometheus;
  3. 给指标设置最大时序数限制,避免异常情况打爆 Prometheus。

4. 链路追踪:Sleuth+Zipkin 精准定位模型调用瓶颈

4.1 AI 服务的长链路痛点:到底是谁拖慢了响应?

先看一个 AI 服务的典型请求链路:

用户请求 → Spring Cloud Gateway → 业务服务 → 向量检索服务(Feign 调用)→ Milvus 向量库 → 模型调用服务(Feign 调用)→ 第三方大模型 API → 返回结果

这个链路里,任何一个环节的耗时增加,都会导致用户最终的响应变慢。之前我们线上出现过一次 P99 耗时从 300ms 飙升到 2s 的问题,我翻遍了每个服务的监控,都没找到明确的瓶颈,因为每个服务的平均耗时都是正常的。

这就是没有全链路追踪的痛点:你只能看到每个服务孤立的指标,却看不到一个请求在全链路里的完整流转,不知道每个环节的耗时占比,自然找不到根因。

而 Spring Cloud Sleuth + Zipkin,就是解决这个问题的最佳方案。Sleuth 负责给每个请求生成全局唯一的 traceId,在全链路里透传,给每个环节生成 span;Zipkin 负责把链路数据可视化,让你一眼看到每个环节的耗时占比。

4.2 Sleuth 集成 Spring AI:全链路 traceId 透传

首先,引入 Sleuth 和 Zipkin 的依赖(Spring Cloud 2023.x 版本):

XML 复制代码
<!-- Spring Cloud Sleuth 核心依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<!-- Zipkin 对接依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>

然后在 application.yml 中配置 Sleuth 和 Zipkin:

bash 复制代码
spring:
  sleuth:
    sampler:
      # 采样率,生产环境建议设为0.1(10%),测试环境设为1.0(100%)
      probability: 1.0
    # 开启异步、Feign、RestTemplate的链路追踪
    async:
      enabled: true
    feign:
      enabled: true
    web:
      client:
        enabled: true
  zipkin:
    # Zipkin服务地址
    base-url: http://你的Zipkin服务IP:9411
    sender:
      type: web

配置完成后,Sleuth 会自动给所有的 HTTP 请求、Feign 调用、异步任务加上 traceId 和 spanId,并且自动透传到下游服务。同时,traceId 会自动放到 MDC 里,我们可以直接在日志里打印出来,实现日志和链路的联动。

这里有个重点:Sleuth 默认不会给 Spring AI 的 ChatClient 调用生成 span,因为 ChatClient 的底层是 RestTemplate/WebClient,需要我们手动加链路埋点,才能把模型调用纳入到全链路里。

4.3 自定义 Span:给模型调用、向量检索加链路埋点

我们可以基于上一节的拦截器,给模型调用加上自定义的 Span,把模型调用的全流程纳入到链路追踪里:

java 复制代码
@Component
public class ModelTraceInterceptor implements ChatClientInterceptor {

    private final Tracer tracer;

    public ModelTraceInterceptor(Tracer tracer) {
        this.tracer = tracer;
    }

    @Override
    public ClientResponse intercept(ClientRequest request, Chain chain) {
        String modelName = request.model();
        // 给模型调用创建一个独立的Span,纳入当前的trace链路
        Span modelSpan = tracer.nextSpan()
                .name("ai.model.call")
                .tag("model", modelName)
                .tag("prompt.length", String.valueOf(request.messages().size()))
                .start();
        
        try (Tracer.SpanInScope scope = tracer.withSpan(modelSpan)) {
            // 执行模型调用
            ClientResponse response = chain.next(request);
            // 给Span加上Token消耗标签
            if (response.metadata() != null && response.metadata().usage() != null) {
                long totalTokens = response.metadata().usage().getTotalTokens();
                modelSpan.tag("total.tokens", String.valueOf(totalTokens));
            }
            return response;
        } catch (Exception e) {
            // 异常时给Span加上错误标签
            modelSpan.tag("error", "true");
            modelSpan.tag("error.message", e.getMessage());
            throw e;
        } finally {
            // 结束Span
            modelSpan.end();
        }
    }
}

同样的,我们可以给向量检索、文档向量化这些操作,也加上自定义的 Span,这样整个 AI 服务的核心操作,都会被纳入到全链路追踪里。

然后把这个拦截器也注入到 ChatClient 里:

java 复制代码
@Bean
public ChatClient chatClient(OpenAiChatModel openAiChatModel, 
                              ModelMetricsInterceptor metricsInterceptor,
                              ModelTraceInterceptor traceInterceptor) {
    return ChatClient.builder(openAiChatModel)
            .defaultInterceptors(metricsInterceptor, traceInterceptor)
            .build();
}

4.4 实战案例:用 Zipkin 定位线上 P99 耗时飙升问题

给大家看一个我们线上的真实案例,用 Zipkin1 分钟定位到了耗时瓶颈。

当时线上出现了问答接口 P99 耗时从 300ms 飙升到 2s 的问题,我们打开 Zipkin,按耗时排序,找到一个耗时最长的 trace,打开后看到了完整的链路:

从链路图里一眼就能看到,整个请求总耗时 2000ms,其中模型调用就占了 1500ms,而模型调用的耗时里,95% 都是第三方 OpenAI API 的耗时。进一步看 Span 的标签,发现这些慢请求用的都是gpt-4模型,而gpt-3.5-turbo模型的耗时完全正常。

后来我们查了 OpenAI 的状态页,发现当时gpt-4模型的 API 出现了限流和延迟波动,我们立刻给gpt-4模型加上了降级策略,切换到备用模型,P99 耗时立刻就恢复了正常。

如果没有全链路追踪,我们根本不可能这么快定位到问题,可能还在排查自己的服务是不是出了问题。这就是链路追踪的核心价值:把黑盒的请求链路变成白盒,精准定位瓶颈点。

5. 日志设计:Prompt / 响应 / 耗时全量日志规范

5.1 AI 服务日志和普通 Java 日志的核心区别

对于 AI 服务来说,日志是可观察性体系的最后一道防线。线上出现的很多问题,比如模型返回异常、RAG 效果不好、合规问题,都需要靠完整的日志来排查。

但 AI 服务的日志,和普通 Java 服务的日志有本质的区别:

  1. 核心内容特殊 :除了常规的异常堆栈,AI 服务最核心的是Prompt 内容、模型响应内容、Token 消耗量、耗时这些专属字段;
  2. 审计需求强:很多行业的 AI 服务需要满足合规要求,所有的 Prompt 和响应都需要留存审计,日志必须可追溯、不可篡改;
  3. 数据敏感:Prompt 里可能包含用户的隐私数据、企业的内部文档,必须做脱敏处理,不能明文存储;
  4. 数据量大:一个完整的 Prompt + 响应可能有几千甚至上万字,如果全量打印,很容易把磁盘打满。

所以,我们必须给 AI 服务设计一套专属的日志规范,既要满足排查和审计的需求,又要控制日志量,还要保证数据安全。

5.2 全量日志字段规范:可追溯、可检索、可审计

我们生产环境在用的 AI 服务日志规范,把日志分为两类:请求摘要日志 和全量明细日志,分别对应 INFO 和 DEBUG 级别,既保证了核心信息的留存,又控制了日志量。

1. 请求摘要日志(INFO 级别,100% 打印)

用于核心指标的统计和快速排查,每条模型调用都会打印,字段如下:

字段名 说明 必选
traceId 全链路追踪 ID,和 Sleuth 联动 是
spanId 当前环节的 SpanID 是
userId 用户 ID,用于定位用户问题 是
businessScene 业务场景,用于拆分统计 是
modelName 调用的模型名称 是
promptTokens Prompt 消耗的 Token 数 是
completionTokens 响应消耗的 Token 数 是
totalTokens 总 Token 数 是
callDuration 模型调用耗时(ms) 是
success 调用是否成功 是
errorMsg 异常信息(失败时必填) 否
2. 全量明细日志(DEBUG 级别,采样打印)

用于问题排查和合规审计,包含完整的 Prompt 和响应内容,字段如下:

字段名 说明 必选
traceId 全链路追踪 ID 是
userId 用户 ID 是
fullPrompt 完整的 Prompt 内容,脱敏后打印 是
fullResponse 完整的模型响应内容,脱敏后打印 是
systemPrompt 系统提示词 否
ragContext RAG 检索的上下文内容 否

5.3 日志实现:Logback 集成 + 脱敏 + 分级采样

首先,给大家看一下我们的 Logback 配置文件,实现了日志分级、滚动策略、traceId 自动打印、脱敏等核心能力:

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds">
    <!-- 引入Spring Boot默认的日志配置 -->
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>

    <!-- 自定义日志格式,自动打印traceId和spanId -->
    <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId:-},%X{spanId:-}] %-5level %logger{36} - %msg%n"/>

    <!-- 业务日志文件输出,按天滚动,保留30天 -->
    <appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/ai-service-info.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/ai-service-info.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
            <totalSizeCap>30GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 只打印INFO级别日志 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 全量明细日志文件输出,按天滚动,保留7天 -->
    <appender name="FILE_DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/ai-service-debug.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/ai-service-debug.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>7</maxHistory>
            <totalSizeCap>50GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 只打印DEBUG级别日志 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>DEBUG</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 根日志配置 -->
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE_INFO"/>
        <appender-ref ref="FILE_DEBUG"/>
    </root>

    <!-- AI服务核心包的日志级别,可动态调整 -->
    <logger name="com.ai.service" level="INFO" additivity="false">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE_INFO"/>
        <appender-ref ref="FILE_DEBUG"/>
    </logger>
</configuration>

然后,我们基于 ChatClient 拦截器,实现规范化的日志打印,同时加上敏感信息脱敏和采样逻辑:

java 复制代码
@Component
@Slf4j
public class ModelLogInterceptor implements ChatClientInterceptor {

    // 采样率:10%的请求打印全量DEBUG日志
    private static final double SAMPLING_RATE = 0.1;
    private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();

    @Override
    public ClientResponse intercept(ClientRequest request, Chain chain) {
        long startTime = System.currentTimeMillis();
        String traceId = MDC.get("traceId");
        String userId = MDC.get("userId");
        String modelName = request.model();
        String businessScene = MDC.get("businessScene");

        try {
            // 执行模型调用
            ClientResponse response = chain.next(request);
            long duration = System.currentTimeMillis() - startTime;
            boolean success = true;
            String errorMsg = null;

            // 获取Token消耗
            long promptTokens = 0;
            long completionTokens = 0;
            long totalTokens = 0;
            if (response.metadata() != null && response.metadata().usage() != null) {
                promptTokens = response.metadata().usage().getPromptTokens();
                completionTokens = response.metadata().usage().getCompletionTokens();
                totalTokens = response.metadata().usage().getTotalTokens();
            }

            // 1. 打印INFO级别摘要日志,100%打印
            log.info("AI模型调用摘要 | traceId:{} | userId:{} | businessScene:{} | modelName:{} | promptTokens:{} | completionTokens:{} | totalTokens:{} | duration:{}ms | success:{} | errorMsg:{}",
                    traceId, userId, businessScene, modelName, promptTokens, completionTokens, totalTokens, duration, success, errorMsg);

            // 2. 打印DEBUG级别全量日志,采样打印+异常100%打印
            boolean isSampled = Math.random() < SAMPLING_RATE;
            if (log.isDebugEnabled() && isSampled) {
                // 敏感信息脱敏:手机号、身份证、邮箱等
                String desensitizedPrompt = desensitize(request.messages().toString());
                String desensitizedResponse = desensitize(response.content());
                log.debug("AI模型调用全量明细 | traceId:{} | userId:{} | fullPrompt:{} | fullResponse:{}",
                        traceId, userId, desensitizedPrompt, desensitizedResponse);
            }

            return response;
        } catch (Exception e) {
            long duration = System.currentTimeMillis() - startTime;
            // 异常时100%打印ERROR日志和全量明细
            log.error("AI模型调用异常 | traceId:{} | userId:{} | businessScene:{} | modelName:{} | duration:{}ms | errorMsg:{}",
                    traceId, userId, businessScene, modelName, duration, e.getMessage(), e);
            // 异常时打印全量Prompt,用于排查问题
            log.debug("AI模型异常调用全量明细 | traceId:{} | userId:{} | fullPrompt:{}",
                    traceId, userId, desensitize(request.messages().toString()));
            throw e;
        }
    }

    // 敏感信息脱敏方法
    private String desensitize(String content) {
        if (content == null) {
            return null;
        }
        // 手机号脱敏
        content = content.replaceAll("(1[3-9]\\d)\\d{4}(\\d{4})", "$1****$2");
        // 身份证脱敏
        content = content.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");
        // 邮箱脱敏
        content = content.replaceAll("(\\w+)\\w{3}@(\\w+\\.\\w+)", "$1***@$2");
        return content;
    }
}

最后,把这个拦截器也注入到 ChatClient 里,这样所有的模型调用都会自动打印规范化的日志。

5.4 踩坑实录:全量日志把磁盘打满了怎么办?

这个坑几乎所有做 AI 服务的人都踩过:一开始为了排查问题,把所有的 Prompt 和响应都打印了 INFO 日志,结果线上高峰期一天就产生了几百 G 的日志,直接把服务器磁盘打满了,服务都挂了。

给大家分享我们的解决方案,完美平衡了日志留存和磁盘占用:

  1. 分级日志:INFO 级别只打印摘要日志,DEBUG 级别打印全量明细,线上默认 INFO 级别,出问题时再动态调整 DEBUG 级别;
  2. 采样打印:正常情况下只采样 10% 的请求打印全量日志,异常请求 100% 打印,既保证了排查问题的能力,又把日志量减少了 90%;
  3. 滚动策略:日志按天滚动,设置最大保留天数和总容量上限,自动删除过期日志,避免磁盘占满;
  4. 日志上云:把日志同步到 ELK / 阿里云 SLS,本地只保留最近 7 天的日志,长期留存的日志放到对象存储里;
  5. 动态调整:通过 Nacos/Apollo 动态调整日志级别和采样率,高峰期降低采样率,低峰期提高采样率。

6. 实战落地:从零构建 AI 服务监控大屏(附 Grafana 模板)

前面我们把指标、链路、日志都搭建好了,最后一步就是把这些数据可视化,做成一站式的 AI 服务监控大屏,让运维和开发同学一眼就能看到服务的健康状态。

6.1 监控大屏整体设计:核心看板规划

我们的监控大屏分为 6 大核心板块,覆盖了 AI 服务的所有核心维度,大家可以直接参考:

6.2 环境搭建:Prometheus + Grafana 集成

1. Prometheus 配置

首先,在 Prometheus 的配置文件prometheus.yml里,加上我们的 Spring AI 服务的抓取配置:

bash 复制代码
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'spring-ai-service'
    metrics_path: '/actuator/prometheus'
    static_configs:
      # 你的Spring AI服务地址,集群的话填所有实例
      - targets: ['服务IP1:端口', '服务IP2:端口']
        labels:
          env: 'prod'
          application: 'spring-ai-service'

配置完成后,重启 Prometheus,就能在 Prometheus 里看到我们的 AI 服务指标了。

2. Grafana 对接 Prometheus
  1. 打开 Grafana,进入「Connections」→「Data sources」→「Add data source」;
  2. 选择 Prometheus,填入 Prometheus 的服务地址,点击「Save & test」,验证连接成功;
  3. 接下来就可以创建 Dashboard,添加我们需要的监控面板了。

6.3 核心面板 PromQL 语句:拿来就能用

给大家分享我们生产环境在用的核心面板 PromQL 语句,大家可以直接复制到 Grafana 里使用:

监控面板 PromQL 语句 说明
模型调用总 QPS sum(rate(ai_model_call_success_total[1m]) + rate(ai_model_call_fail_total[1m])) 统计所有模型调用的每秒请求数
模型调用成功率 sum(rate(ai_model_call_success_total[1m])) / sum(rate(ai_model_call_success_total[1m]) + rate(ai_model_call_fail_total[1m])) * 100 统计模型调用的成功率百分比
模型调用 P99 耗时 histogram_quantile(0.99, sum(rate(ai_model_call_duration_seconds_bucket[1m])) by (le, model)) 按模型拆分的 P99 耗时
当日 Token 总消耗量 sum(increase(ai_model_token_usage_total[1d])) 统计当日所有模型的 Token 总消耗
向量检索 QPS sum(rate(ai_vector_search_total[1m])) 向量检索的每秒请求数
向量缓存命中率 sum(rate(ai_vector_cache_hit_total[1m])) / sum(rate(ai_vector_search_total[1m])) * 100 向量缓存的命中率百分比
JVM 堆内存使用率 sum(jvm_memory_used_bytes{area="heap"}) / sum(jvm_memory_max_bytes{area="heap"}) * 100 JVM 堆内存的使用率

6.4 告警规则配置:异常提前感知

光有监控还不够,我们还要配置告警规则,当服务出现异常时,第一时间收到通知。给大家分享几个核心的告警规则,配置在 Prometheus 的rules.yml里:

bash 复制代码
groups:
- name: spring-ai-alert
  rules:
  # 模型调用成功率告警
  - alert: 模型调用成功率过低
    expr: sum(rate(ai_model_call_success_total[5m])) / sum(rate(ai_model_call_success_total[5m]) + rate(ai_model_call_fail_total[5m])) < 0.99
    for: 2m
    labels:
      severity: critical
    annotations:
      summary: "模型调用成功率低于99%"
      description: "当前成功率{{ $value | humanizePercentage }},请立即排查"

  # 模型调用P99耗时告警
  - alert: 模型调用耗时过高
    expr: histogram_quantile(0.99, sum(rate(ai_model_call_duration_seconds_bucket[5m])) by (le)) > 2
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: "模型调用P99耗时超过2s"
      description: "当前P99耗时{{ $value }}s,请排查模型API状态"

  # Token消耗异常告警
  - alert: Token消耗异常增长
    expr: sum(increase(ai_model_token_usage_total[1h])) / sum(increase(ai_model_token_usage_total[1h] offset 1d)) > 1.5
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "Token小时消耗环比增长超过50%"
      description: "当前增长幅度{{ $value | humanizePercentage }},请关注成本"

配置完成后,我们可以在 Grafana 里配置告警通知渠道,支持钉钉、企业微信、邮件、短信等,当告警触发时,就能第一时间收到通知了。

完整 Grafana 模板获取

我把我们生产环境在用的 Spring AI 监控大屏,做成了完整的 Grafana 模板,大家可以在评论区留言,我会把模板 JSON 文件发给大家,直接导入 Grafana 就能用,不用自己一个个配置面板。

7. 踩坑总结:可观察性体系搭建的避坑指南

最后,给大家总结一下我们搭建这套体系半年来,踩过的 8 个核心坑,大家可以直接避坑:

  1. 高基数标签打爆 Prometheus:绝对不要把 userId、requestId 这种高基数字段加到 Metrics 标签里,否则时序数据会指数级增长,直接把 Prometheus 搞崩;
  2. 异步调用 traceId 丢失:@Async 异步调用时,默认 MDC 里的 traceId 会丢失,需要用 TtlMDCAdapter 来透传,或者用 Sleuth 的异步支持;
  3. 全量日志打满磁盘:不要把完整的 Prompt 和响应都打印 INFO 级别日志,一定要做分级和采样,异常请求 100% 打印,正常请求采样打印;
  4. 敏感信息泄露:Prompt 和响应里可能包含用户隐私和企业敏感数据,一定要做脱敏处理,绝对不能明文存储和打印;
  5. 链路采样率设置不合理:生产环境不要设置 100% 的链路采样率,否则 Zipkin 的存储压力会非常大,建议设置 10% 的采样率,异常链路 100% 采集;
  6. 指标维度不够,无法定位问题:指标一定要加上模型名称、业务场景、接口名称这些标签,否则只能看到整体数据,无法细粒度定位问题;
  7. 日志和链路脱节:一定要把 traceId 加到日志里,这样通过 Zipkin 查到异常链路后,可以直接用 traceId 在 ELK 里查到完整的日志,实现一站式排查;
  8. 只监控不告警:监控的最终目的是提前发现问题,一定要配置合理的告警规则,不要等用户反馈了才知道服务出了问题。

8. 总结与展望

总结

AI 服务的可观察性体系,和普通 Java 服务有着本质的区别,它不是简单的搭一套监控、打几行日志就完事了,而是要围绕 AI 服务的核心痛点,从指标、链路、日志三个维度,构建一套专属的、全链路的可视化运维方案。

这篇文章里,我给大家完整分享了我们生产环境落地的 Spring AI 可观察性体系:

  1. 用 Micrometer 实现了 AI 服务专属的指标采集,覆盖了模型调用、Token 消耗、向量检索等核心指标;
  2. 用 Sleuth+Zipkin 实现了全链路追踪,把模型调用、向量检索都纳入了链路,1 分钟就能定位线上耗时瓶颈;
  3. 设计了 AI 服务专属的日志规范,实现了分级、采样、脱敏,既满足了排查和审计的需求,又控制了日志量;
  4. 从零搭建了 AI 服务监控大屏,给大家提供了可直接复用的 PromQL 语句和 Grafana 模板。

这套体系落地后,我们线上问题的平均排查时间从原来的 1 小时,缩短到了 5 分钟以内,服务可用性从 99.5% 提升到了 99.95%,同时也实现了 AI 服务成本的精细化管控。

展望

未来我们还会在这套体系的基础上,做进一步的优化:

  1. 接入 AIOps:用 AI 算法分析指标趋势,预测异常风险,实现故障的提前预警和自动止损;
  2. 根因分析自动化:当告警触发时,自动关联链路、日志、指标数据,给出根因分析和解决方案,不用人工排查;
  3. 合规审计体系:基于日志数据,构建 Prompt 和响应的合规审计体系,自动检测敏感内容和违规请求,满足等保和行业合规要求;
  4. 成本优化智能推荐:基于 Token 消耗指标,分析不同业务场景的模型使用效率,给出模型版本切换、Prompt 优化的建议,降低 API 成本。

参考文献


结语:以上就是 Spring AI 可观察性体系的全量实战分享,所有的代码和方案都是我们生产环境验证过的,大家可以直接落地。如果大家有任何问题,或者需要完整的代码和 Grafana 模板,欢迎在评论区留言,我会一一回复。如果觉得这篇文章对你有帮助,别忘了点赞、收藏、转发三连,后续我还会分享更多 Spring AI 架构优化的实战干货,感谢大家的支持!

相关推荐
sell-cloud3 小时前
Prompt Engineering要凉了?Fable 5.5能“猜“意图,但银行正在封杀AI Agent——企业AI的真正护城河是什么
人工智能·prompt
Navicat中国4 小时前
Navicat 18 内置 AI 助手让数据库开发从繁琐操作迈向智能协作
大数据·人工智能·数据库开发
4SAPI4 小时前
多模型请求路由怎么设计?成本、速度和能力如何平衡
人工智能·python·gateway·php
魔乐社区5 小时前
翼芯・智算测试与适配优化专区,让国产算力“选得准、跑得通、用得好“
人工智能·开源·大模型
XCDoris6 小时前
郴州logo设计机构
大数据·人工智能·python
Goboy7 小时前
一个人带三个Agent,我用全新TRAE从0到1搭了个资讯周刊站
人工智能·trae
俊哥V7 小时前
每日 AI 研究简报 · 2026-10-19
人工智能·ai
AI_AGENT_DEV_AI7 小时前
AI英语教培智能体的开发
人工智能
物联网软硬件开发-轨物科技7 小时前
【技术白皮书】光伏清检一体化机器人系统解决方案技术白皮书
大数据·运维·网络·人工智能·机器人
海宇数据7 小时前
零信任架构实战:基于海宇网上踪迹欺诈行为识别构建自动化信贷资信评估网关
运维·人工智能·架构·自动化