在 AI 模型调用的前、后插入自己的业务逻辑—— 日志记录、数据脱敏、性能监控。本项目手把手教你用 ObservationHandler 打造专属的可观测性拦截器。


一、自定义观测处理器

在前面的项目中,我们演示了如何将 AI 调用的可观测数据导出到 ARMS、Zipkin、LangFuse 等外部系统。这固然很好,但有时你需要更灵活的实时处理

  • 记录请求和响应的详细日志,便于本地调试
  • 在调用前添加自定义标签(如用户 ID、业务线)
  • 过滤敏感信息,防止 API Key 或用户隐私泄漏
  • 根据调用耗时触发告警或降级逻辑
  • 在调用成功后触发某些业务钩子(如积分奖励)

这些场景无法仅靠导出器实现,你需要一个能够嵌入调用生命周期的拦截器 —— 这就是 ObservationHandler 的价值所在。


二、什么是 ObservationHandler?

ObservationHandler 是 Spring AI 基于 Micrometer Observation 提供的扩展接口。你可以把它理解为 AI 调用过程中的**“观察者”** —— 在观测开始和结束时,你都能插上一段自定义代码。

2.1 接口定义

public interface ObservationHandler<N extends Observation.Context> {
    void onStart(N context);          // 观测开始(AI 调用前)
    void onStop(N context);           // 观测结束(AI 调用后)
    boolean supportsContext(Observation.Context context); // 此 Handler 关心哪些上下文?
}

💡 打个比方:就像你去餐厅吃饭(AI 调用),onStart 是服务员记录你点餐前的状态(你坐在几号桌、点了什么菜),onStop 是你吃完后结账并记录用餐感受。而 supportsContext 则用于区分你吃的是中餐还是西餐 —— 只处理你自己关心的类型。

2.2 完整的调用流程图

下面这张 mermaid 图清晰地展示了从用户请求到 AI 返回的全过程,以及 onStart / onStop 在何处介入:

通义千问 API ChatModel ObservationHandler Controller 用户 通义千问 API ChatModel ObservationHandler Controller 用户 添加自定义标签 记录开始时间 预处理请求 获取响应结果 记录 token 用量 脱敏/日志/告警 POST /chat (message) onStart(context) 继续调用 发送请求 返回响应 onStop(context) 返回结果 最终回答

三、项目结构一览

observationhandler-example/
├── pom.xml                                 # 依赖管理
├── src/main/java/.../observation/
│   ├── ObservationHandlerExampleApplication.java  # 启动类
│   ├── handler/
│   │   └── CustomerObservationHandler.java       # 自定义 Handler
│   └── controller/
│       └── ChatModelController.java              # REST 控制器
└── src/main/resources/
    └── application.yml                            # 配置文件

四、核心依赖解析(pom.xml)

以下是我们项目使用的主要依赖,我会逐行解释其作用,并指出需要修正的问题。

<!-- 通义千问大模型 Starter(Spring AI Alibaba)-->
<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>

<!-- Spring Boot Actuator:提供 /actuator 端点,暴露健康、指标、链路信息 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- 🌟 Micrometer 链路追踪桥接(Brave 实现)-->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-brave</artifactId>
    <!-- ⚠️ 原项目使用了 1.5.0-M2(里程碑版),不稳定!推荐改为稳定版 -->
    <version>1.3.4</version>
    <exclusions>
        <exclusion>
            <artifactId>slf4j-api</artifactId>
            <groupId>org.slf4j</groupId>
        </exclusion>
    </exclusions>
</dependency>

<!-- Zipkin 上报器(可选,用于将链路数据发送到 Zipkin UI)-->
<dependency>
    <groupId>io.zipkin.reporter2</groupId>
    <artifactId>zipkin-reporter-brave</artifactId>
    <version>3.4.3</version>
</dependency>

🔧 修正说明:原项目使用的 1.5.0-M2 属于里程碑版本,可能存在不稳定风险。生产环境强烈建议使用稳定版本,如 1.3.4


五、配置文件详解(application.yml)

spring:
  application:
    name: observationhandler-example   # 应用名,会在 Zipkin 中显示

ai:
  dashscope:
    api-key: ${AI_DASHSCOPE_API_KEY}   # 从环境变量读取,更安全

# 自定义观测开关(非 Spring 标准,是我们自己加的)
observations:
  log-completion: true    # 是否打印 AI 的完整响应
  log-prompt: true        # 是否打印用户的原始提问

server:
  port: 8080

# Actuator 与链路采样配置
management:
  tracing:
    sampling:
      probability: 1.0     # 100% 采样(生产环境建议 0.1~0.5)

# Zipkin 上报地址(如果启动了 Zipkin 服务)
zipkin:
  tracing:
    endpoint: http://localhost:9411/api/v2/spans
配置项 含义 推荐值
observations.log-prompt 开发时开启,方便查看用户问题 开发 true,生产 false
observations.log-completion 开启会打印 AI 完整回答,注意可能很长 按需开启
management.tracing.sampling.probability 采样率,1.0 代表每条请求都记录 生产建议 0.1

六、代码实现(优化后)

我们将用更优雅的方式实现自定义 Handler,并修正一些原项目中的设计缺陷。

6.1 启动类 —— 简洁至上

@SpringBootApplication
public class ObservationHandlerExampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ObservationHandlerExampleApplication.class, args);
    }
}

启动类保持干净,所有配置都交由 @Configuration 类处理。


6.2 自定义 Handler —— 核心拦截逻辑

原项目中的 CustomerObservationHandler 有两个主要问题:

  • 使用 System.out.println 打印日志,生产环境无法控制级别
  • 添加 KeyValue 的方式过于繁琐
  • 缺少高/低基数标签的区分

下面是优化后的代码,我逐一解释:

package com.example.observation.handler;

import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationHandler;
import io.micrometer.common.KeyValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.observation.ChatModelObservationContext;

public class CustomerObservationHandler 
        implements ObservationHandler<ChatModelObservationContext> {

    private static final Logger log = LoggerFactory.getLogger(CustomerObservationHandler.class);

    @Override
    public void onStart(ChatModelObservationContext context) {
        // 1. 低基数标签(枚举值有限,适合做 Group By)
        context.addLowCardinalityKeyValue(
            KeyValue.of("ai.model.provider", "dashscope")
        );
        context.addLowCardinalityKeyValue(
            KeyValue.of("user.role", extractUserRole())  // 假设你能从上下文中拿到用户角色
        );

        // 2. 高基数标签(值唯一或变化大,用于详情追踪)
        String promptContent = context.getPrompt().getContents().toString();
        context.addHighCardinalityKeyValue(
            KeyValue.of("ai.prompt.length", String.valueOf(promptContent.length()))
        );
        context.addHighCardinalityKeyValue(
            KeyValue.of("ai.prompt.preview", 
                promptContent.length() > 100 ? promptContent.substring(0, 100) : promptContent)
        );

        // 3. 打印开始日志(可结合配置开关)
        if (log.isInfoEnabled()) {
            log.info("🚀 AI 调用开始 | 提示长度: {}", promptContent.length());
        }
    }

    @Override
    public void onStop(ChatModelObservationContext context) {
        // 获取响应内容(注意:可能为 null 如果调用异常)
        var response = context.getResponse();
        String responseText = (response != null && response.getResult() != null) 
                ? response.getResult().getOutput().getText() 
                : "无响应";

        // 获取调用耗时
        long durationMs = context.getDuration().toMillis();

        // 记录结束日志
        log.info("✅ AI 调用完成 | 耗时: {}ms | 响应长度: {}", 
            durationMs, responseText.length());

        // 可选:根据耗时触发告警
        if (durationMs > 5000) {
            log.warn("⏱️ AI 调用耗时过长: {}ms, 请关注", durationMs);
        }

        // 可选:对响应做脱敏处理(这里仅示意)
        String maskedResponse = maskSensitiveInfo(responseText);
        // 你可以把脱敏后的内容存到某个上下文,或者打印出来
        log.debug("脱敏后响应: {}", maskedResponse);
    }

    /**
     * 声明这个 Handler 只关心 ChatModel 类型的观测上下文。
     * 如果不做限制,所有类型的观测(如 embedding、image 生成)都会经过这个 Handler。
     */
    @Override
    public boolean supportsContext(Observation.Context context) {
        return context instanceof ChatModelObservationContext;
    }

    // ---------- 辅助方法 ----------
    private String extractUserRole() {
        // 这里仅仅是示例,实际可以从 SecurityContext 或请求头中获取
        return "guest";
    }

    private String maskSensitiveInfo(String text) {
        // 简单脱敏:隐藏手机号、邮箱等
        return text.replaceAll("\\d{11}", "***********")
                   .replaceAll("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}", "***@***.***");
    }
}

👀 低基数 vs 高基数:低基数标签(如 ai.model.provider)只有少数几个可能的值,适合在监控仪表盘上做分组和聚合;高基数标签(如 ai.prompt.length)值几乎每个请求都不同,一般只用于详细的链路查询。


6.3 配置类 —— 注册 Handler 的最佳实践

原项目将 Handler 的注册放在了 Controller 的构造函数中,这会导致:

  • Handler 只对这一个 Controller 生效(如果你有多个 Controller 调用 ChatModel,就不会触发)
  • 违背了“关注点分离”原则

正确做法:创建一个独立的配置类,将 Handler 注册到全局 ObservationRegistry,并同时初始化 ChatModel 为 Spring Bean。

package com.example.observation.config;

import com.example.observation.handler.CustomerObservationHandler;
import com.alibaba.cloud.ai.dashscope.DashScopeApi;
import com.alibaba.cloud.ai.dashscope.DashScopeChatModel;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ObservationConfig {

    @Value("${spring.ai.dashscope.api-key}")
    private String apiKey;

    @Bean
    public ObservationRegistry observationRegistry() {
        // 创建 ObservationRegistry 并注册我们的自定义 Handler
        ObservationRegistry registry = ObservationRegistry.create();
        registry.observationConfig().observationHandler(new CustomerObservationHandler());
        return registry;
    }

    @Bean
    public DashScopeChatModel dashScopeChatModel(ObservationRegistry observationRegistry) {
        // 构建 DashScope API 客户端
        DashScopeApi dashScopeApi = DashScopeApi.builder()
                .apiKey(apiKey)
                .build();

        // 创建 ChatModel 并传入 ObservationRegistry
        return DashScopeChatModel.builder()
                .dashScopeApi(dashScopeApi)
                .observationRegistry(observationRegistry)
                .build();
    }
}

这样一来,任何地方注入的 DashScopeChatModel 都会自动触发我们的自定义 Handler,且 Handler 只会被注册一次。


6.4 控制器 —— 极简注入

package com.example.observation.controller;

import com.alibaba.cloud.ai.dashscope.DashScopeChatModel;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/custom/observation/chat")
public class ChatModelController {

    private final DashScopeChatModel chatModel;

    public ChatModelController(DashScopeChatModel chatModel) {
        this.chatModel = chatModel;
    }

    @GetMapping
    public String chat(@RequestParam(name = "message", defaultValue = "你好,请介绍一下自己") 
                       String message) {
        // 调用 AI 模型 —— 此时会自动触发 CustomerObservationHandler 的 onStart 和 onStop
        return chatModel.call(message);
    }
}

七、支持的其他观测上下文

Spring AI 不仅仅支持聊天模型,还支持多种 AI 能力。你可以针对不同的上下文实现多个 ObservationHandler

上下文类型 说明 常见场景
ChatModelObservationContext 对话补全 聊天机器人、问答
EmbeddingModelObservationContext 文本向量化 构建知识库、检索增强
ImageModelObservationContext 图片生成 文生图、图生图
VectorStoreObservationContext 向量数据库操作 相似性搜索、存储向量

多 Handler 组合示例

registry.observationConfig()
    .observationHandler(new ChatModelLoggingHandler())
    .observationHandler(new EmbeddingModelMetricsHandler())
    .observationHandler(new VectorStoreTracingHandler());

每个 Handler 通过 supportsContext 决定处理哪种上下文,彼此独立又协同工作。


八、与外部导出方案的关系

很多开发者会困惑:有了自定义 Handler,还需要 Zipkin / LangFuse 吗?

答案是:两者是互补关系,而非替代关系。

方案 目的 数据流向 典型用途
自定义 ObservationHandler 实时拦截、本地处理 JVM 内存 → 本地日志/告警 参数校验、脱敏、耗时告警、业务钩子
Zipkin / ARMS 链路数据导出与持久化 应用 → Zipkin Collector → 存储 历史调用查询、服务拓扑分析、性能瓶颈定位
LangFuse 专为 LLM 设计的可观测平台 应用 → LangFuse API Prompt 版本管理、Token 用量统计、用户反馈收集

🌟 最佳实践:同时使用两者!用 ObservationHandler实时过滤和脱敏,然后把过滤后的安全数据发送到 Zipkin 做长期存储和分析。


九、实操指南:从零跑通项目

下面咱们亲手把这个项目跑起来,感受一下自定义 Handler 的魅力。

9.1 环境准备

# 检查 Java 版本(需要 17+)
java -version

# 检查 Maven 版本
mvn -version

9.2 获取通义千问 API Key

  1. 访问 阿里云百炼控制台
  2. 登录后,在「API-KEY 管理」中创建一个新的 API Key
  3. 复制该 Key(格式类似 sk-xxxxx

9.3 启动 Zipkin(强烈推荐,以便可视化看到效果)

# 使用 Docker 一键启动 Zipkin
docker run -d -p 9411:9411 --name zipkin openzipkin/zipkin:latest

# 验证启动成功
curl http://localhost:9411/health

9.4 配置环境变量

# Linux / macOS
export AI_DASHSCOPE_API_KEY="sk-你的key"

# Windows (CMD)
set AI_DASHSCOPE_API_KEY=sk-你的key

9.5 修改配置文件(可选)

如果你想关闭请求/响应打印,编辑 application.yml

observations:
  log-completion: false   # 生产环境建议关闭,避免打印大量数据
  log-prompt: false

9.6 编译与启动

cd observationhandler-example
mvn clean compile
mvn spring-boot:run

看到如下日志表示启动成功:

Tomcat started on port 8080 (http) with context path ''
Started ObservationHandlerExampleApplication in 2.5 seconds

9.7 发送测试请求

# 打开另一个终端
curl "http://localhost:8080/custom/observation/chat?message=帮我写一句关于春天的诗"

你会立即在 应用的控制台 看到类似输出:

🚀 AI 调用开始 | 提示长度: 12
✅ AI 调用完成 | 耗时: 1243ms | 响应长度: 256

同时,如果你启动了 Zipkin,访问 http://localhost:9411 并点击「Run Query」,就能看到这条请求的完整链路,并且自定义的低/高基数标签也会出现在 Span 的 Tags 中。


十、常见问题与排查

Q1:Handler 没有被触发?控制台没有任何 onStart 日志?

可能原因

  • ObservationRegistry 没有正确注入到 DashScopeChatModel 中。检查配置类是否生效。
  • supportsContext 返回了 false。确认上下文类型是 ChatModelObservationContext
  • 你使用了多个 ObservationHandler,但注册顺序有误(不会影响,只要注册了就会全部尝试)。

排查步骤

  1. onStart 方法第一行加上 System.out.println("Handler invoked!");(临时)
  2. 确认 DashScopeChatModel Bean 是通过我们定义的配置类创建的,且传入了 observationRegistry 参数。

Q2:Zipkin 看不到自定义标签?

原因:Zipkin 默认只接收某些标准字段。自定义的 KeyValue 必须通过 addLowCardinalityKeyValue 添加,并且需要确保 Zipkin 上报器正常工作。

解决:检查 management.tracing.sampling.probability=1.0,并且确认 zipkin.tracing.endpoint 地址正确。

Q3:API Key 报错 InvalidApiKey

  • 检查环境变量名称是否写错(AI_DASHSCOPE_API_KEY 全大写)
  • 确认 API Key 没有过期,也没有前后空格
  • 如果是在 IDEA 中运行,可以在运行配置里手动设置环境变量

十一、生产环境部署

11.1 传统打包运行

mvn clean package -DskipTests
java -jar target/observationhandler-example-*.jar \
  --ai.dashscope.api-key=${AI_DASHSCOPE_API_KEY}

11.2 Docker 化部署

编写 Dockerfile

FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY target/observationhandler-example-*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

构建并运行:

docker build -t observation-handler:1.0 .
docker run -d -p 8080:8080 \
  -e AI_DASHSCOPE_API_KEY="sk-xxx" \
  observation-handler:1.0

11.3 生产环境配置建议

配置项 开发环境 生产环境
observations.log-prompt true false(避免打印敏感用户输入)
observations.log-completion true false(响应可能很大,打爆磁盘)
management.tracing.sampling.probability 1.0 0.1(10%采样)
zipkin.tracing.endpoint 本地地址 公司内部 Zipkin 集群地址

十二、进阶扩展:让 Handler 更强大

12.1 自动脱敏 + 异步告警

@Override
public void onStop(ChatModelObservationContext context) {
    String rawResponse = context.getResponse().getResult().getOutput().getText();
    String safeResponse = maskIdCard(rawResponse);  // 脱敏身份证号
    
    // 异步发送到告警系统(如钉钉、Slack)
    if (context.getDuration().toMillis() > 3000) {
        alertService.sendAsync("AI调用耗时超过3秒", context.getDuration());
    }
}

12.2 结合 Micrometer Metrics 记录自定义指标

private final MeterRegistry meterRegistry;

@Override
public void onStop(ChatModelObservationContext context) {
    meterRegistry.counter("ai.call.total",
        "model", "dashscope",
        "status", "success"
    ).increment();
    
    meterRegistry.timer("ai.call.duration")
        .record(context.getDuration());
}

12.3 动态开关(通过配置中心)

@Value("${observations.handler.enabled:true}")
private boolean handlerEnabled;

@Override
public void onStart(ChatModelObservationContext context) {
    if (!handlerEnabled) return;
    // 真正逻辑...
}

十三、总结

通过本项目的学习,你已经掌握了:

ObservationHandler 的核心原理 —— 实现 onStart / onStop 即可在 AI 调用前后织入自定义逻辑
正确的注册方式 —— 通过 @Configuration 注册到 ObservationRegistry,并注入 ChatModel
低基数 vs 高基数标签 —— 合理使用可提升监控与排障效率
与 Zipkin 等外部系统的协作 —— 形成“实时处理 + 持久化存储”的双层可观测体系
生产级改进 —— 日志框架、版本修正、脱敏、告警、Docker 部署

现在,你可以在自己的 AI 应用中自由地添加日志、监控、安全过滤等能力了。如果有任何疑问,随时欢迎探讨!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐