尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Spring AI RAG 全链路观测实战:从埋点到根因分析

发布时间:2026/9/28 23:06:39

资讯中心
01
ARTICLE

Spring AI RAG 全链路观测实战:从埋点到根因分析

Spring AI RAG 全链路观测实战:从埋点到根因分析
1. 先说清楚这不是给“监控系统”加个埋点那么简单Spring AI RAG 接上观测云做全链路观测这个标题里藏着三个被严重低估的认知偏差——第一很多人以为“观测”就是看几个指标曲线把 Spring Boot Actuator 的 /actuator/metrics 拉进 Grafana 就算完事第二RAG 流程常被当成黑盒调用只关心 final answer 对不对却从不追问“为什么召回了这三段文档”“Embedding 相似度阈值设成 0.72 是怎么来的”第三观测云不是数据管道终点而是诊断决策的起点。我去年在一家做智能客服知识引擎的团队落地这套方案时踩的第一个坑就是把 RAG 的 trace 当成普通 HTTP 请求 trace 去打点结果发现 83% 的 span 标签全是空的根本没法定位是检索慢、重排逻辑卡顿还是 LLM 调用超时。核心关键词必须前置说透Spring AI是 Spring 官方推出的 AI 应用开发抽象层它不绑定任何模型厂商通过统一的 PromptTemplate、ChatClient、EmbeddingClient 接口屏蔽底层差异RAG在这里特指基于 Spring AI 构建的检索增强生成流程包含文档切片、向量入库、语义检索、上下文注入、LLM 生成四阶段闭环观测云指代具备 OpenTelemetry 原生支持能力的可观测性平台如阿里云 ARMS、腾讯云 CODING Observability、火山引擎 APM重点在于其对 Span 关系、Log-Trace 关联、Metric-Span 下钻的深度支持能力而全链路观测的本质是让每一次用户提问User Query都能在观测云中还原出完整的决策路径从 Controller 层接收请求 → EmbeddingClient 计算查询向量 → VectorDB 执行近似最近邻搜索 → Retriever 返回原始文档片段 → Reranker 重新排序 → LLM 输入构造 → ChatClient 发起大模型调用 → Response 解析与流式返回。这条链路上每个环节的耗时、错误率、输入输出样本、关键参数如 topK5、similarityThreshold0.68都必须可追溯、可对比、可归因。适合谁来读如果你正在用 Spring Boot Spring AI 开发 RAG 应用但遇到这些问题线上用户反馈“回答变慢了”你却只能看到整体接口 P95 从 1.2s 升到 3.8s无法判断是向量库响应延迟升高还是 LLM token 生成速率下降或者 QA 测试发现某类问题命中率骤降你想查“为什么没召回那篇关键 SOP 文档”却发现日志里只有 final answer没有检索过程的原始 chunk 内容又或者想对比不同 Embedding 模型bge-m3 vs text-embedding-ada-002在真实业务 query 上的召回质量却缺乏结构化埋点支撑。那么这篇就是为你写的——它不讲 OpenTelemetry 基础概念不教 Grafana 怎么画图只聚焦 Spring AI RAG 场景下如何让观测云真正成为你的“RAG 系统 CT 机”。2. 观测云接入前必须完成的三道硬门槛很多团队失败的根本原因是把观测云当成“锦上添花”的可视化工具而不是 RAG 系统的“神经系统”。在往 Spring AI RAG 流程里注入观测能力之前有三道技术门槛必须跨过缺一不可。这三道门槛不是可选项而是观测能否真正起效的分水岭。2.1 必须启用 Spring AI 的原生 Tracing 支持Spring AI 1.0.0-M3 版本开始内置了 OpenTelemetry Tracing 集成但默认是关闭的。你不能依赖 Spring Boot Actuator 的通用 tracing因为 Spring AI 的核心组件EmbeddingClient、Retriever、ChatClient内部有大量异步操作、流式处理和自定义回调通用 tracing 会漏掉关键 span。正确做法是在 application.yml 中显式开启spring: ai: tracing: enabled: true # 关键配置必须指定 tracer 名称否则 span 会被丢弃 tracer-name: spring-ai-rag-tracer # EmbeddingClient 的 tracing 配置 embedding: client: openai: tracing: enabled: true # 这里要填你观测云提供的 OTLP endpoint otlp-endpoint: http://your-observability-cloud:4317 # ChatClient 的 tracing 配置 chat: client: openai: tracing: enabled: true otlp-endpoint: http://your-observability-cloud:4317提示otlp-endpoint必须指向观测云提供的 OTLP gRPC 或 HTTP 端点不能写 localhost。我们曾因误配为http://localhost:4317导致所有 span 数据丢失排查了两天才发现是容器网络策略阻断了本地回环地址。更关键的是Spring AI 的 tracing 默认只记录顶层 span如chatClient.call而 RAG 流程中的中间环节如retriever.retrieve、embeddingClient.embed需要手动注入。你必须在自定义的 RAG Service 中使用TracerBean 显式创建子 spanService public class RAGService { private final Tracer tracer; private final EmbeddingClient embeddingClient; private final RetrieverDocument retriever; private final ChatClient chatClient; public RAGService(Tracer tracer, EmbeddingClient embeddingClient, RetrieverDocument retriever, ChatClient chatClient) { this.tracer tracer; this.embeddingClient embeddingClient; this.retriever retriever; this.chatClient chatClient; } public String generateAnswer(String userQuery) { // 创建根 span名称必须体现业务语义 Span rootSpan tracer.spanBuilder(rag-process) .setAttribute(user.query, userQuery.substring(0, Math.min(50, userQuery.length()))) .startSpan(); try (Scope scope rootSpan.makeCurrent()) { // 步骤1Embedding 计算 Span embedSpan tracer.spanBuilder(embedding-client.embed) .setAttribute(query.length, userQuery.length()) .startSpan(); try (Scope embedScope embedSpan.makeCurrent()) { ListDouble queryVector embeddingClient.embed(userQuery); embedSpan.setAttribute(vector.dimension, queryVector.size()); } finally { embedSpan.end(); } // 步骤2文档检索 Span retrieveSpan tracer.spanBuilder(retriever.retrieve) .setAttribute(top-k, 5) .startSpan(); try (Scope retrieveScope retrieveSpan.makeCurrent()) { ListDocument retrievedDocs retriever.retrieve(userQuery); retrieveSpan.setAttribute(retrieved.count, retrievedDocs.size()); // 关键记录召回的文档 ID用于后续分析 retrievedDocs.stream() .map(Document::getId) .forEach(id - retrieveSpan.setAttribute(retrieved.doc.id, id)); } finally { retrieveSpan.end(); } // 步骤3LLM 生成 Span chatSpan tracer.spanBuilder(chat-client.call) .setAttribute(model.name, qwen2-72b) .startSpan(); try (Scope chatScope chatSpan.makeCurrent()) { String prompt buildPrompt(userQuery, retrievedDocs); Message response chatClient.call(new Prompt(List.of(new UserMessage(prompt)))); chatSpan.setAttribute(response.tokens, response.getContent().length()); return response.getContent(); } finally { chatSpan.end(); } } finally { rootSpan.end(); } } }这段代码的价值在于它把 RAG 的四个核心阶段Embedding → Retrieve → Rerank → Generate全部暴露为独立 span并且为每个 span 注入了业务关键属性query 内容、召回文档 ID、token 数量。这些属性是后续做精准下钻分析的基础——没有它们你在观测云里看到的只是一堆名字叫chat-client.call的扁平化 span根本无法区分是哪次用户提问触发的。2.2 必须改造 VectorDB 的客户端使其支持 Span 关联RAG 流程中向量数据库如 Milvus、Weaviate、Qdrant是性能瓶颈最集中的环节但它的调用往往游离在 Spring AI tracing 之外。默认情况下当你调用retriever.retrieve()时Spring AI 只负责组装查询参数真正的向量搜索是由底层 VectorDB Client 执行的这部分耗时不会自动关联到当前 trace。我们必须手动将 VectorDB 的调用 span “嫁接”到 RAG 主链路上。以 Milvus 为例其 Java SDK 提供了MilvusClient但原生不支持 OpenTelemetry。解决方案是使用OpenTelemetrySdk的TracerProvider创建一个代理客户端Configuration public class MilvusConfig { Bean public MilvusClient milvusClient(Tracer tracer) { // 获取当前 tracer 的 span context Span currentSpan Span.current(); Context currentContext Context.current(); return new MilvusClient() { private final io.milvus.client.MilvusClient delegate new MilvusClientImpl(ConnectParam.newBuilder() .withHost(milvus-server) .withPort(19530) .build()); Override public SearchResults search(SearchParam searchParam) { // 创建子 span父 span 为当前 RAG 流程的 span Span dbSpan tracer.spanBuilder(milvus.search) .setParent(Context.current().with(currentSpan)) .setAttribute(collection.name, searchParam.getCollectionName()) .setAttribute(top-k, searchParam.getTopK()) .startSpan(); try (Scope scope dbSpan.makeCurrent()) { long startTime System.nanoTime(); SearchResults results delegate.search(searchParam); long durationNs System.nanoTime() - startTime; dbSpan.setAttribute(search.duration.ns, durationNs); dbSpan.setAttribute(results.count, results.getSearchResults().size()); return results; } finally { dbSpan.end(); } } // 其他方法同理... }; } }注意setParent(Context.current().with(currentSpan))这行代码至关重要。它确保 Milvus 的 search span 成为当前 RAG trace 的子节点而不是孤立的 span。否则你在观测云的 trace 图里会看到两条平行线一条是 Spring AI 的 RAG 流程另一条是 Milvus 的搜索两者毫无关联。对于 Weaviate你需要在其Client初始化时注入OpenTelemetry实例Bean public Client weaviateClient(Tracer tracer) { return new Client.Builder() .url(http://weaviate:8080) .openTelemetry(tracer) // 关键传入 Spring AI 的 tracer .build(); }实测下来这一步改造能让 RAG 流程的总耗时分解精度提升 70% 以上。以前你只能看到retriever.retrieve耗时 850ms现在能清晰看到其中milvus.search占 720msreranker.rerank占 110msdocument.parser占 20ms——这才是真正的“全链路”。2.3 必须建立 RAG 特有的 Metrics 指标体系而非复用通用指标观测云里的 Metrics 不是越多越好而是越精准越有用。RAG 场景下以下四个指标是必须采集的核心黄金信号它们直接反映系统健康度和业务效果指标名称类型采集方式业务意义告警阈值建议rag_retrieval_hit_rateGauge每次检索后计算retrieved_docs_with_correct_answer / total_retrieved_docs衡量检索模块是否召回了真正相关的文档 0.65 持续 5 分钟rag_llm_input_token_countHistogram在 ChatClient 调用前统计注入的 prompt token 数量监控上下文长度是否失控避免 LLM 截断P95 32000rag_response_latency_msHistogram从 Controller 接收请求到返回 final answer 的总耗时端到端用户体验核心指标P95 5000msrag_embedding_similarity_scoreHistogramEmbeddingClient 返回的相似度分数分布判断 Embedding 模型质量是否退化P50 0.45这些指标不能靠日志解析生成必须在代码中主动打点。例如rag_retrieval_hit_rate的计算逻辑// 在 RAGService 的 generateAnswer 方法中 ListDocument retrievedDocs retriever.retrieve(userQuery); // 假设你有一个业务规则SOP 文档 ID 以 SOP- 开头且用户问题明确要求 SOP boolean hasSopInQuery userQuery.toLowerCase().contains(sop); int hitCount 0; for (Document doc : retrievedDocs) { if (hasSopInQuery doc.getId().startsWith(SOP-)) { hitCount; } } // 打点注意这里是 Counter不是 Gauge meter.counter(rag.retrieval.hit.count) .register(meter) .increment(hitCount); meter.gauge(rag.retrieval.hit.rate, () - (double) hitCount / Math.max(1, retrievedDocs.size()));经验教训我们最初只打了rag.response.latency结果线上出现一次故障——LLM 生成耗时飙升但总耗时没超阈值因为检索阶段耗时大幅下降缓存命中率 100%。后来补上rag_llm_input_token_count后才发现是前端传入的 query 被恶意拼接了 1000 个重复问句导致 prompt token 爆涨到 42000触发了 LLM 的截断机制。没有这个指标你永远不知道“回答不准”是因为模型不行还是输入有问题。3. 观测云里真正有用的四大分析场景接入完成后观测云不是用来“看图”的而是用来“做决策”的。以下是我们在生产环境高频使用的四个分析场景每个都对应一个具体问题、一套标准排查路径、以及一个可立即落地的优化动作。3.1 场景一定位“回答变慢”的根因——不是看平均值而是看分布当运营同学反馈“客服机器人响应变慢”第一反应不是查 P95而是打开观测云的 Trace 列表按rag_response_latency_ms降序排列找出耗时最长的 10 个 trace。然后逐个点击重点观察三个位置EmbeddingClient.span 的耗时占比如果超过总耗时的 60%说明 Embedding 模型推理是瓶颈。此时要检查是否启用了 GPU 加速如 vLLM 部署Embedding 模型是否过大bge-large-zh 有 1.2B 参数bge-base-zh 只有 110M是否开启了批处理batch_size 1VectorDB.span 的耗时占比如果超过 70%说明向量库压力过大。此时要检查Milvus 的index_type是否为IVF_FLAT适合中小规模还是HNSW适合高精度nlist和nprobe参数是否合理nlist1000,nprobe10是常见起点是否启用了缓存Milvus 的cache.cache_sizeChatClient.span 的耗时占比如果超过 80%说明 LLM 是瓶颈。此时要检查是否启用了流式响应streamtrue避免前端等待整个 response是否设置了max_tokens限制防止生成过长内容是否启用了temperature0.3降低随机性提升生成稳定性我们曾遇到一个典型案例P95 从 1.2s 升到 3.8s但查看 Top 10 trace 发现其中 7 个的milvus.search耗时都在 2.1s 左右而其他 span 都很稳定。进一步下钻到 Milvus 的 metrics发现query_queue_length持续高于 5说明查询队列积压。最终定位是nprobe从 10 被误调为 100导致单次搜索耗时翻倍。修复后 P95 回落至 1.4s。3.2 场景二分析“回答不准”的真相——不是看答案而是看召回内容当 QA 测试发现某类问题如“如何申请退款”回答准确率从 92% 降到 65%传统做法是看日志里的 final answer。但在观测云里你应该在 Trace 列表中筛选user.query包含 “退款” 的 trace进入任意一个失败 trace找到retriever.retrievespan展开该 span 的Attributes找到retrieved.doc.id列表复制这些 ID在文档管理系统中查找对应原文对比发现召回的文档都是“退款政策”但缺失了最关键的“退款操作步骤”文档ID: SOP-REFUND-STEP。这就引出了关键问题为什么没召回 SOP-REFUND-STEP继续下钻查看embedding-client.embedspan 的query.vector属性十六进制字符串在向量库中执行query_vector的相似度搜索发现 SOP-REFUND-STEP 的相似度分数只有 0.32远低于阈值 0.6检查该文档的原始文本发现它被切片时关键步骤被分到了第二个 chunk而第一个 chunk 只有标题“退款操作”导致 Embedding 表征不完整。解决方案调整文档切片策略将“退款操作步骤”这类关键 SOP 文档设置为chunk_size512而非默认的 256并启用overlap64确保步骤描述不被截断。上线后该类问题的召回率回升至 89%。3.3 场景三评估 Embedding 模型升级效果——不是跑 benchmark而是看线上分布团队决定从text-embedding-ada-002升级到bge-m3常规做法是用 MTEB 数据集跑评测。但在观测云里你可以用真实流量验证在观测云中创建两个对比视图modelada和modelbge-m3对比指标rag_retrieval_hit_rate的 7 日趋势重点看rag_embedding_similarity_score的分布直方图bge-m3 的 P50 从 0.52 提升到 0.68且长尾0.4比例从 12% 降至 3%更关键的是查看rag_llm_input_token_count由于 bge-m3 召回更精准平均注入的文档数从 5.2 降至 3.8token 数量减少 18%LLM 生成耗时同步下降。这个数据比任何 benchmark 都有说服力。它证明了模型升级不仅提升了理论分数更直接降低了线上资源消耗和用户等待时间。3.4 场景四识别“幽灵流量”——不是看 QPS而是看 Query 语义聚类某天发现 RAG 接口 QPS 突增 300%但业务侧没做任何推广。在观测云中对user.query字段做高频词云分析发现大量 query 包含 “test”、“123”、“aaaa”进一步用user.query的 Embedding 向量做聚类观测云通常提供 PCA 降维KMeans 功能发现 87% 的新增流量聚集在 3 个语义簇里抽样查看这些 query 的rag_response_latency_ms发现平均耗时仅 80ms远低于正常值 1200ms且retrieved.doc.id全为空结论这是自动化测试脚本或爬虫在刷接口未传有效 query。应对措施在 Controller 层增加轻量级 query 质量校验如中文字符占比 30%、长度 5、包含非业务关键词拦截此类请求。上线后 QPS 回落至基线水平且rag_retrieval_hit_rate提升 5 个百分点——因为有效 query 的资源分配更充分了。4. 那些官方文档绝不会告诉你的实战陷阱再完美的方案落地时也会撞上一堆“意料之外”的墙。这些坑只有亲手部署过 3 次以上 RAG 观测系统的团队才会懂。4.1 Span 名称冲突Spring AI 的默认命名会污染你的业务语义Spring AI 自动创建的 span 名称是chatClient.call、embeddingClient.embed这看起来很规范。但问题在于你的业务里可能有多个 ChatClient Bean比如一个用于客服一个用于内部知识问答它们的 span 全叫chatClient.call在观测云里完全无法区分。解决方案在 Bean 定义时通过Qualifier指定唯一名称并在 tracing 配置中引用Bean Qualifier(customerServiceChatClient) public ChatClient customerServiceChatClient(OpenAiChatModel openAiChatModel) { return ChatClient.builder() .chatModel(openAiChatModel) .build(); } Bean Qualifier(internalKnowledgeChatClient) public ChatClient internalKnowledgeChatClient(QwenChatModel qwenChatModel) { return ChatClient.builder() .chatModel(qwenChatModel) .build(); }然后在 application.yml 中分别配置spring: ai: chat: client: openai: # 对应 customerServiceChatClient tracing: enabled: true tracer-name: customer-service-rag-tracer qwen: # 对应 internalKnowledgeChatClient tracing: enabled: true tracer-name: internal-knowledge-rag-tracer这样观测云里就会出现customer-service-rag-tracer.chatClient.call和internal-knowledge-rag-tracer.chatClient.call两个独立的 span可以分别做告警和分析。4.2 Log-Trace 关联失效日志里找不到 traceId很多团队发现虽然 trace 数据上了观测云但对应的日志却无法关联。根源在于Spring AI 的异步操作如流式响应会切换线程导致 MDCMapped Diagnostic Context中的traceId丢失。标准解法是使用OpenTelemetry的Context传递机制而不是依赖 MDC// 在 Controller 中 GetMapping(/ask) public ResponseEntityResponseBody ask(RequestParam String query) { // 获取当前 trace context Context context Context.current(); // 异步处理但保留 context CompletableFutureString future CompletableFuture.supplyAsync(() - { // 在新线程中恢复 context try (Scope scope context.makeCurrent()) { return ragService.generateAnswer(query); } }); // 流式返回时同样要保证 context 传递 return ResponseEntity.ok() .contentType(MediaType.TEXT_EVENT_STREAM) .body(future.thenApply(answer - SseEmitter.event() .name(answer) .data(answer) .build())); }同时在 logback-spring.xml 中确保 pattern 包含%X{trace_id}appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %X{trace_id} - %msg%n/pattern /encoder /appender实测经验我们曾因忘记在CompletableFuture中makeCurrent()导致 90% 的日志丢失 traceId。排查方法是在观测云中找一个 trace复制其trace_id然后在日志系统中搜索该 ID如果搜不到就说明 context 传递断了。4.3 观测云采样率陷阱不是所有 trace 都值得保留全量上报 trace 会产生海量数据成本高昂且无必要。Spring AI 默认采样率是 1.0100%必须调整。但简单设为 0.110%会丢失关键异常 trace。最佳实践是动态采样对成功 trace 低采样对失败 trace 全采样。OpenTelemetry 提供TraceIdRatioBasedSampler但我们需要自定义Bean public Sampler customSampler() { return new Sampler() { Override public SamplingResult shouldSample(SamplingParameters parameters) { // 如果 span 有 ERROR 属性100% 采样 if (parameters.getParentContext() ! null parameters.getParentContext().getSpan() ! null parameters.getParentContext().getSpan().getSpanContext().isSampled()) { // 检查是否为 error if (parameters.getAttributes().get(AttributeKey.stringKey(error.type)) ! null) { return SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE); } } // 否则按 5% 采样 return Math.random() 0.05 ? SamplingResult.create(SamplingDecision.RECORD_AND_SAMPLE) : SamplingResult.create(SamplingDecision.DROP); } Override public String getDescription() { return custom-rag-sampler; } }; }这样所有报错的 trace 都会被完整保留而正常流量只保留 5%成本降低 95%关键问题一个不漏。4.4 RAG 特定字段的索引爆炸别让观测云变成你的数据库观测云为了支持快速检索会对 span 的 Attributes 建立倒排索引。但 RAG 流程中有些字段如retrieved.doc.content、llm.prompt内容极长且高度唯一会导致索引体积暴增查询变慢。安全做法是只索引业务关键字段其余转为非索引属性。在观测云控制台中将以下字段设为not indexedretrieved.doc.content文档原始内容太大llm.prompt完整 prompt可能含敏感信息user.query如果 query 很长只索引前 100 字符而必须索引的字段只有user.query.keywordquery 的关键词哈希用于聚类retrieved.doc.id文档 ID用于关联分析rag.stage标识当前是 embedding/retrieve/generate 阶段我们曾因未限制llm.prompt索引导致观测云每天多产生 2TB 索引数据集群负载飙升。调整后索引体积下降 80%查询响应时间从 8s 降至 0.3s。5. 最后一点个人体会观测不是终点而是新循环的起点做完这套全链路观测我最大的感受是它彻底改变了我们迭代 RAG 系统的方式。过去优化一个环节比如换 Embedding 模型我们得先跑 offline benchmark再灰度发布等一周看业务指标变化整个周期至少 10 天。现在只要模型一上线我打开观测云5 分钟内就能看到rag_embedding_similarity_score的分布变化、rag_retrieval_hit_rate的提升幅度、甚至rag_llm_input_token_count的下降趋势——所有数据都来自真实用户流量毫秒级刷新。更重要的是观测数据开始反哺模型训练。我们把高频失败 trace 中的user.query和retrieved.doc.id导出作为负样本加入到 Embedding 模型的微调数据集中把rag_response_latency_ms高的 trace 中的llm.prompt提取出来分析哪些 prompt 结构导致生成慢进而优化 prompt engineering 模板。观测云不再是一个“看”的工具而成了 RAG 系统的“感知神经”和“反馈回路”。如果你刚起步我的建议是不要追求一步到位。先确保rag_response_latency_ms和rag_retrieval_hit_rate这两个指标能稳定采集再逐步加上 span 关联和字段索引。记住目标不是把所有数据都塞进观测云而是让每一次用户提问都能在观测云里留下一条可解读、可归因、可行动的数字足迹。这条路没有捷径但每一步踩实RAG 系统的确定性就会提升一分。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。