原文链接:SpringAI(GA):自定义指标接入Langfuse观测
[!TIP]
在 AI 应用开发过程中,系统观测是必不可少的一环,不管是链路中的时间损耗,还是核心指标的搜集 本期首先对 Langfuse 介绍,随后自定义指标并接入 Langfuse 观测系统中
本期带来自定义指标,接入 Langfuse 观测系统
- genai.prompt:AI 模型输入
- genai.completion:AI 模型输出
实战代码可见:github.com/GTyingzi/sp… 下的 observation 目录 observability-langfuse 模块
效果如下
Langfuse 介绍
Langfuse 是一个开源的 LLM(大型语言模型)工程平台,专为基于大语言模型(LLM)的应用程序设计,旨在提供 全生命周期的可观测性、测试、监控和提示管理 能力。它通过工具化的方式,帮助开发者更高效地调试、优化和扩展 LLM 应用,尤其适合处理复杂的多步骤工作流(如 RAG 系统、多代理协作、多模态任务等)。
核心功能
全链路追踪(Tracing)
- 记录请求生命周期:从用户输入到模型调用、工具执行、中间结果生成,直到最终输出的完整过程。
- 多阶段支持:适用于 RAG 系统中的检索(Retrieval)、生成(Generation)阶段,或 Agent 系统中的多跳推理。
- 可视化分析:通过 Web 界面查看每个步骤的耗时、输入/输出、上下文信息,快速定位性能瓶颈或错误点。
性能与成本监控(Metrics)
- 关键指标:实时监控响应延迟、API 调用成本(如 OpenAI 的 token 使用量)、错误率、重试次数等。
- 成本优化:通过 token 消耗统计,帮助开发者优化模型调用策略,降低运行成本。
- 自定义告警:设置阈值(如错误率 > 2%),触发告警通知。
评估体系(Assessments)
- 自动化评估:集成 LLM 作为“裁判”,对生成内容的准确性、毒性、连贯性等进行评分。
- 人工审核:提供手动评估界面,允许标注员对输出质量进行标注和反馈。
- RAGAS 集成:定量分析答案的忠实度(Faithfulness)和相关性(Answer Relevancy)
提示管理(Prompt Management)
- 版本控制:对提示词(Prompt)进行版本管理,支持回滚和对比不同版本的效果。
- A/B 测试:通过实验跟踪功能,比较不同提示模板在性能、成本和质量上的表现。
- Playground 测试:提供交互式环境,快速迭代和优化提示词。
实验跟踪(Experiment Tracking)
- A/B 测试:对不同模型配置、提示模板或工具链进行对比实验。
- 数据驱动优化:通过量化指标(如准确率、成本、延迟)选择最优方案。
技术优势
开源与灵活集成
- 开源协议为 MIT,支持本地化部署(Docker 安装)或云端服务。
- 提供 Python/JavaScript SDK,无缝集成 LangChain、LlamaIndex、OpenAI 等主流框架。
- 支持自定义 Instrumentation(如装饰器或回调接口)。
高性能架构
- 基于 PostgreSQL 实现毫秒级查询,支持大规模数据存储。
- 列式存储压缩技术降低 72% 的存储成本。
多模态支持
- 不仅支持文本,还可追踪图像等多模态数据类型。
适用场景
RAG 系统优化
- 跟踪检索与生成阶段的性能差异,优化向量数据库查询效率。
- 分析用户反馈与检索结果的关联性,提升生成质量。
模型调试与监控
- 快速定位低质量输出(如幻觉、毒性内容)的根源。
- 监控模型延迟和成本,平衡性能与经济性。
Agent 系统开发
- 可视化多代理协作的决策路径,优化工具调用逻辑。
- 通过 A/B 测试验证不同策略的有效性。
企业级 LLM 应用
- 提供数据安全和隐私保障(支持私有部署)。
- 通过仪表盘分析模型使用情况,辅助资源规划
密钥生成
创建项目
随后生成密钥,这里记住 sk 开头的 Secret Key、pk 开头的 Public Key
随后利用替换如下,生成对应的 base64 码
echo -n "pk-xxx:sk-xxx" | base64
SpringAI 集成 Langfuse
pom.xml
<properties>
<!-- Spring AI -->
<spring-ai.version>1.0.1</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-instrumentation-bom</artifactId>
<version>2.17.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<!-- Spring AI needs a reactive web server to run for some reason-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot Actuator for observability support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Micrometer Observation -> OpenTelemetry bridge -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<!-- OpenTelemetry OTLP exporter for traces -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
</dependencies>
application.yml
server:
port: 8080
spring:
application:
name: observability-langfuse
ai:
openai:
api-key: ${DASHSCOPEAPIKEY}
base-url: https://dashscope.aliyuncs.com/compatible-mode
chat:
options:
model: qwen-max
chat:
observations:
log-prompt: true # Include prompt content in tracing (disabled by default for privacy)
log-completion: true # Include completion content in tracing (disabled by default)
management:
tracing:
sampling:
probability: 1.0 # Sample 100% of requests for full tracing (adjust in production as needed)
observations:
annotations:
enabled: true # Enable @Observed (if you use observation annotations in code)
otel:
# configure exporter
traces:
exporter: otlp
sampler: alwayson
metrics:
exporter: otlp
# logs exportation inhibited for langfuse currently cannot support
logs:
exporter: none
exporter:
otlp:
endpoint: "https://cloud.langfuse.com/api/public/otel"
headers:
Authorization: "Basic ${YOURBASE64ENCODEDCREDENTIALS}"
protocol: http/protobuf
ObservationFilter
package com.spring.ai.tutorial.ovservation.config;
import io.micrometer.common.KeyValue;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationFilter;
import org.springframework.ai.chat.observation.ChatModelObservationContext;
import org.springframework.ai.content.Content;
import org.springframework.ai.observation.ObservabilityHelper;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* @author yingzi
* @since 2025/9/4
*/
@Component
public class ChatModelCompletionContentObservationFilter implements ObservationFilter {
@Override
public Observation.Context map(Observation.Context context) {
if (!(context instanceof ChatModelObservationContext chatModelObservationContext)) {
return context;
}
var prompts = processPrompts(chatModelObservationContext);
var completions = processCompletion(chatModelObservationContext);
chatModelObservationContext.addHighCardinalityKeyValue(new KeyValue() {
@Override
public String getKey() {
return "genai.prompt";
}
@Override
public String getValue() {
return ObservabilityHelper.concatenateStrings(prompts);
}
});
chatModelObservationContext.addHighCardinalityKeyValue(new KeyValue() {
@Override
public String getKey() {
return "genai.completion";
}
@Override
public String getValue() {
return ObservabilityHelper.concatenateStrings(completions);
}
});
return chatModelObservationContext;
}
private List<String> processPrompts(ChatModelObservationContext chatModelObservationContext) {
return CollectionUtils.isEmpty((chatModelObservationContext.getRequest()).getInstructions()) ? List.of() : (chatModelObservationContext.getRequest()).getInstructions().stream().map(Content::getText).toList();
}
private List<String> processCompletion(ChatModelObservationContext context) {
if (context.getResponse() != null && (context.getResponse()).getResults() != null && !CollectionUtils.isEmpty((context.getResponse()).getResults())) {
return !StringUtils.hasText((context.getResponse()).getResult().getOutput().getText()) ? List.of() : (context.getResponse()).getResults().stream().filter((generation) -> generation.getOutput() != null && StringUtils.hasText(generation.getOutput().getText())).map((generation) -> generation.getOutput().getText()).toList();
} else {
return List.of();
}
}
}
这里通过addHighCardinalityKeyValue方法增加观测的键和值
- gen_ai.prompt:值为AI模型的输入
- gen_ai.completion:值为AI模型的输出
controller
package com.spring.ai.tutorial.ovservation.controller;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/chat")
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder
.build();
}
@GetMapping("/call")
public String call(@RequestParam(value = "query", defaultValue = "你好,我的外号是影子,请记住呀!")String query) {
return chatClient.prompt(query).call().content();
}
}
效果
发起请求
在 Langfuse 中查看我们自定义的指标 genai.prompt、genai.completion。除此之外还有写内置指标可供查看,如所用模型,输入、输出 token 数量等
往期资料
Spring AI + Spring Ai Aliabba系统化学习资料
本教程将采用2025年5月20日正式的GA版,给出如下内容
- 核心功能模块的快速上手教程
- 核心功能模块的源码级解读
- Spring ai alibaba增强的快速上手教程 + 源码级解读
版本:
- JDK21
- SpringBoot3.4.5
- SpringAI 1.0.1
- SpringAI Alibaba 1.0.3+
免费渠道:
- 为Spring Ai Alibaba开源社区解决解决有效的issue or 提供有价值的PR,可免费获取上述教程
- 往届微信推文
收费服务:收费69.9元
- 飞书在线云文档
- Spring AI会员群教程代码答疑
- 若Spring AI、Spring AI Alibaba教程内容无法满足业务诉求,可定制提供解决方案,带价私聊
学习交流圈
你好,我是影子,曾先后在🐻、新能源、老铁就职,兼任Spring AI Alibaba开源社区的Committer。目前新建了一个交流群,一个人走得快,一群人走得远,另外,本人长期维护一套飞书云文档笔记,涵盖后端、大数据系统化的面试资料,可私信免费获取