【第29篇】自定义观测处理器
在 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 在何处介入:
三、项目结构一览
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
- 访问 阿里云百炼控制台
- 登录后,在「API-KEY 管理」中创建一个新的 API Key
- 复制该 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,但注册顺序有误(不会影响,只要注册了就会全部尝试)。
排查步骤:
- 在
onStart方法第一行加上System.out.println("Handler invoked!");(临时) - 确认
DashScopeChatModelBean 是通过我们定义的配置类创建的,且传入了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 应用中自由地添加日志、监控、安全过滤等能力了。如果有任何疑问,随时欢迎探讨!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)