SpringAI(GA):自定义指标接入Langfuse观测

150 阅读6分钟

原文链接: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版,给出如下内容

  1. 核心功能模块的快速上手教程
  2. 核心功能模块的源码级解读
  3. Spring ai alibaba增强的快速上手教程 + 源码级解读

版本:

  • JDK21
  • SpringBoot3.4.5
  • SpringAI 1.0.1
  • SpringAI Alibaba 1.0.3+

免费渠道:

  1. 为Spring Ai Alibaba开源社区解决解决有效的issue or 提供有价值的PR,可免费获取上述教程
  2. 往届微信推文

收费服务:收费69.9元

  1. 飞书在线云文档
  2. Spring AI会员群教程代码答疑
  3. 若Spring AI、Spring AI Alibaba教程内容无法满足业务诉求,可定制提供解决方案,带价私聊

学习交流圈

你好,我是影子,曾先后在🐻、新能源、老铁就职,兼任Spring AI Alibaba开源社区的Committer。目前新建了一个交流群,一个人走得快,一群人走得远,另外,本人长期维护一套飞书云文档笔记,涵盖后端、大数据系统化的面试资料,可私信免费获取