(第二篇)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(向量库名)、topKhit(是否命中缓存)

重点提醒绝对不要把 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 架构优化的实战干货,感谢大家的支持!

相关推荐
Awu12271 小时前
🍎Claude Code Playground:我愿称之为「前端调参神器」
前端·人工智能·aigc
果汁华1 小时前
LangGraph:构建状态化 AI 代理的革命性编排框架
大数据·人工智能
熊猫钓鱼>_>1 小时前
AR游戏的“轻”与“深”:当智能体接管眼镜,游戏逻辑正在发生什么变化?
人工智能·游戏·ai·ar·vr·game·智能体
dinl_vin2 小时前
LangChain 系列·(四):RAG 基础——给大模型装上“外脑“
人工智能·算法·langchain
深念Y2 小时前
哈希与向量:计算机理解现实的两座桥梁
人工智能·数学·机器学习·向量·hash·哈希·空间
TImCheng06092 小时前
AI认证等级体系深度对比:能力与应用场景
人工智能
掘金安东尼2 小时前
谁才真正拥有 Agent Loop?从 OpenClaw、Claude Code 到 LangGraph、Temporal 的一次工程级拆解
人工智能
隔壁大炮2 小时前
Day06-08.CNN概述介绍
人工智能·pytorch·深度学习·算法·计算机视觉·cnn·numpy
白云千载尽2 小时前
前馈与反馈——经典控制理论中的基础概念
人工智能·算法