Spring AI RAG接上观测云怎么做全链路观测?

做 RAG 应用时,最让人头疼的问题往往不是"彻底不能用",而是"偶尔有点慢"。

RAG 可以先理解成"先找相关资料,再让大模型带着资料回答"。Embedding 负责把文字问题变成数字向量,Milvus 则保存这些向量并找出最相近的内容。

用户发来一个问题,页面转了几秒才开始输出。应用日志告诉我,这次请求总共用了三秒多,但三秒花在哪里,它没有说。

可能是把问题转成向量的 Embedding 模型慢了,可能是 Milvus 相似度检索慢了,也可能是大模型生成首字慢了。还有一种更容易被忽略的情况:Milvus 本身没问题,慢的是应用到 Milvus 之间的网络。

如果只有一个总耗时,这几种情况看起来几乎一样。排查只能靠猜:重启服务、换模型、调线程池,甚至先把锅甩给向量库。

这次我没有做一个空项目演示,而是直接拿自己的 Spring AI Alibaba RAG 应用改造。项目使用 Spring Boot 3.2.0、Spring AI 1.0.0、Spring AI Alibaba 1.0.0.2,Embedding 模型是 text-embedding-v4,生成模型是 qwen-plus,向量库是部署在服务器上的 Milvus。

我在 Windows 本机安装 DataKit,把应用的链路和指标送进观测云,同时从本机跨网络抓取远程 Milvus 的 Prometheus 指标。接入后,我又主动给"应用到 Milvus"这段网络增加延迟,跑完故障和恢复两组对照。

最后的结果很明确:这次慢点出现在向量检索的客户端链路,但 Milvus 服务端处理时间没有跟着升高。也就是说,问题在网络路径,不在大模型,也不能简单写成"Milvus 变慢"。

先说清楚:我想看到的不是一堆曲线

接入监控之前,我先给这次实践定了一个验收标准:观测页面必须能回答具体问题,而不是只证明"数据传上去了"。

对一次 RAG 请求,我至少要看到下面这条调用链:

text 复制代码
用户请求
  └─ ChatClient
      ├─ Advisor / 记忆处理
      ├─ EmbeddingModel
      ├─ Milvus VectorStore query
      └─ ChatModel

Trace 是一次请求经过各步骤的完整调用记录,其中每个计时片段叫 Span。它回答"这一次请求的时间去了哪里";应用指标回答"同类操作最近是不是普遍变慢";Milvus 指标回答"向量库内部是否同时出现压力"。这三层合起来,才有机会区分模型、向量库和网络。

我还加了一条隐私要求:不能为了看耗时,把用户问题、模型答案和召回文档一起上传。对定位性能来说,操作名称、耗时、状态和必要标签已经够用,正文内容没有必要进入观测平台。

我的真实链路长什么样

应用运行在 Windows 本机,Milvus 在远程服务器,观测云是 SaaS。DataKit 是负责采集和转发数据的程序,我把它装在应用所在的本机,而不是 Milvus 服务器。

这样放有两个原因。

第一,应用把 OTLP Trace 发到本机地址,不需要把接收端口暴露到公网。第二,DataKit 可以同时抓本机应用的 Prometheus 端点和远程 Milvus 的指标端点,再统一上传。

实际链路是:

text 复制代码
Spring AI Alibaba RAG
  ├─ OTLP Trace ───────────────┐
  └─ /actuator/prometheus ──┐  │
                            ↓  ↓
                     Windows DataKit ──→ 观测云
                            ↑
远程 Milvus /metrics ───────┘

这也回答了一个常见问题:Milvus 在服务器上,本机 DataKit 能不能监控?可以,前提是本机能访问 Milvus 的指标端点,并且服务器防火墙只向必要来源开放。我的实践就是这种部署方式。

DataKit 2.13.0 安装后以 Windows 服务运行,启动类型是 Automatic。观测云基础设施页面已经能看到这台主机,说明最基础的数据通路正常。

应用侧只加四块东西

项目本来已经使用 Spring AI 的 ChatClient、Advisor、EmbeddingModel 和 MilvusVectorStore。这一点很重要,因为 Spring AI 已经为这些组件提供了 Micrometer Observation。接入时不需要在每个方法外面手写计时器,先把现成的观测能力打开即可。

第一块:补齐依赖

我在 pom.xml 中加入了 Actuator、Prometheus 指标注册表、Micrometer 的 OpenTelemetry 桥接和 OTLP 导出器:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>

Actuator 暴露健康检查和 Prometheus 指标,也就是可以被持续抓取、统计和比较的数值;Tracing Bridge 把 Micrometer 的 Observation 转成链路;OTLP Exporter 再通过通用传输协议把链路发给 DataKit。

第二块:配置 OTLP 和 Prometheus

我的应用端口是 9166,管理端点单独放在本机 9167。核心配置如下:

yaml 复制代码
management:
  server:
    address: 127.0.0.1
    port: 9167
  endpoints:
    web:
      exposure:
        include: health,prometheus
  tracing:
    sampling:
      probability: 1.0
  otlp:
    tracing:
      endpoint: http://127.0.0.1:9529/otel/v1/traces
      timeout: 10s
  metrics:
    tags:
      application: ${spring.application.name}
      environment: practice

这里的 100% 采样只适合本次实践和小流量验证。生产环境如果请求量大,需要按成本和排障需要调整,否则链路量会迅速上涨。

管理端口绑定 127.0.0.1 也是刻意的。Prometheus 端点包含应用运行信息,不应该为了采集方便直接暴露到公网。本机 DataKit 能访问它,就没有必要扩大访问范围。

第三块:让 DataKit 抓两类指标

应用侧要抓的是:

text 复制代码
http://127.0.0.1:9167/actuator/prometheus

远程 Milvus 抓的是服务器上的 Prometheus 指标地址。我的采集配置没有把所有指标一股脑全收进来,而是先保留与这次排查直接相关的搜索、Proxy、QueryNode 和资源状态指标。

Windows 安装版 DataKit 的 Prom 配置放在 C:\Program Files\datakit\conf.d\prom。应用侧配置可以先用这一份:

toml 复制代码
[[inputs.prom]]
  urls = ["http://127.0.0.1:9167/actuator/prometheus"]
  interval = "15s"
  timeout = "10s"
  source = "clxzs-spring-ai"
  metric_types = ["counter", "gauge", "histogram", "summary"]
  metric_name_filter = [
    "^gen_ai_.*$",
    "^db_vector_.*$",
    "^http_server_requests_seconds.*$",
    "^jvm_memory_used_bytes$",
    "^process_cpu_usage$"
  ]
  measurement_name = "clxzs_spring_ai"
  keep_exist_metric_name = true
  election = false

远程 Milvus 另建一个配置,把地址换成自己的服务器内网地址或受控访问地址:

toml 复制代码
[[inputs.prom]]
  urls = ["http://<MILVUS_HOST>:9091/metrics"]
  interval = "15s"
  timeout = "10s"
  source = "milvus-remote"
  metric_types = ["counter", "gauge", "histogram"]
  metric_name_filter = [
    "^milvus_proxy_req_latency.*$",
    "^milvus_proxy_sq_latency.*$",
    "^milvus_querynode_sq_req_latency.*$",
    "^milvus_querynode_sq_req_count$"
  ]
  measurement_name = "milvus"
  keep_exist_metric_name = true
  election = false

保存后,用管理员 PowerShell 执行 Restart-Service datakit。先在本机打开两个指标地址,确认能返回文本;再看 DataKit 日志是否还有抓取错误;最后到观测云按 sourcemeasurement 查询。三步都通过,才算采集真正接上。

这是观测成本里很容易踩的坑。Milvus 指标数量不少,如果把高基数标签和所有序列直接放开,页面可能很丰富,账单也会很丰富。先围绕问题做白名单,后续遇到新场景再扩,比"全收再说"稳妥。

接入后,观测云中已经能查询到 Spring AI 的 gen_ai_client_operation_*db_vector_client_operation_* 等指标。

远程 Milvus 的搜索与组件耗时指标也进入了同一工作空间,后面会用它和应用侧耗时做交叉判断。

第四块:默认关闭敏感正文

我明确保留了下面三项关闭状态:

yaml 复制代码
spring:
  ai:
    chat:
      client:
        observations:
          log-prompt: false
          log-completion: false
    vectorstore:
      observations:
        log-query-response: false

同时移除了会把增强提示词写进日志的调试代码。整个实验中,请求正文只在内存里使用;留证时只记录响应字节数和 SHA-256,没有把用户问题、模型答案、召回文档写进证据目录。

这不是为了"看起来安全"。一旦提示词中包含业务资料、用户信息或内部知识,开启正文采集就会改变数据边界。性能排查通常不需要这些内容,默认关着更合适。

第一个坑:有 Span,不等于有完整 Trace

第一次跑请求时,观测云里已经出现了数据,但链路并不完整。

HTTP 请求、ChatClient 和模型生成在一条 Trace 里,Embedding 与 Milvus query 却像两条孤立链路。单看列表,好像每个步骤都有;点进一条请求,却无法把它们按父子关系串起来。

原因出在响应式上下文传播。这个项目使用流式生成,代码跨过 Reactor 异步边界后,观测上下文没有自动带过去。Span 产生了,但父 Trace 信息丢了。

修复只加了一项:

yaml 复制代码
spring:
  reactor:
    context-propagation: auto

重新启动后,同一次请求中出现了 12 个 Span:HTTP 入口、spring_ai_chat_client、消息记忆、向量检索 Advisor、Embedding、Milvus query、qwen-plus 流式生成都挂在同一条 Trace 下。

这一步比"能看到 Trace"更关键。链路没有串起来时,后面的耗时对比很容易拿错样本。监控平台不会自动替我们修正应用里的上下文传播,页面上有数据也不代表接入已经完成。

一条 RAG Trace,应该怎样读

第一次打开链路详情,很容易被十几个 Span 名称吓到。我的读法不是从上到下逐行研究,而是先找三块最影响用户等待时间的主干。

先看 ChatModel。它通常占据较长的一段,但流式生成有一个特殊点:总生成时间长,不一定等于用户一直看着空白页。生产排查时,最好把首字时间和完整生成时间分开。如果首字很快、后续持续输出,用户感受未必差;如果第一个字迟迟不来,即使总耗时一样,体验也会明显更糟。

再看 Embedding。它负责把用户问题转成向量,是进入 Milvus 前的一步。如果这一段突然升高,先检查模型服务的网络、限流、超时和批量策略。此时调整 Milvus 索引通常没有帮助,因为请求还没真正进入向量检索。

最后看 VectorStore query。它是应用侧看到的向量检索总耗时,里面可能包含连接建立、网络往返、服务端排队与实际查询。这个 Span 变长,只能把范围缩到"应用调用向量库这一段",还不能直接断言向量库内部变慢。要继续和 Milvus Proxy、QueryNode 指标对照。

Advisor、消息记忆和提示词组装也值得看,但优先级取决于占比。如果它们只占几毫秒,就先别急着优化;如果自定义 Advisor 中还有数据库查询、文件读取或远程接口,就要继续补更细的业务 Span。Trace 的作用不是让每一行都变成绿色,而是告诉我们下一步该把时间花在哪里。

另外,父子关系和时间轴要一起看。两个 Span 看起来都用了 500ms,如果它们并行执行,对总耗时的贡献可能仍接近 500ms;如果串行执行,才可能累加成 1 秒。只把列表中的耗时全部相加,往往会得到一个超过接口总耗时的错误数字。

到这里,应用 Trace、应用指标和远程 Milvus 指标已经通过本机 DataKit 汇到同一个工作空间。

然后,我主动制造了一次慢请求

我用 Toxiproxy 放在应用和远程 Milvus 之间,只对这段测试链路增加延迟:上行 350ms、下行 350ms,强度 100%。Embedding 模型和 ChatModel 的访问路径不变,Milvus 服务本身也没有做 CPU 限速或资源压测。

测试容器的启动命令如下,端口只绑定本机:

powershell 复制代码
docker run -d --name clxzs-milvus-toxiproxy `
  -p 127.0.0.1:8474:8474 `
  -p 127.0.0.1:19531:8666 `
  ghcr.io/shopify/toxiproxy:2.12.0

然后通过 Toxiproxy 的 8474 管理接口创建 0.0.0.0:8666 → <MILVUS_HOST>:19530 代理,再分别添加 upstream 和 downstream 两个 latency toxic,latency 都设为 350。应用测试时临时把 MILVUS_PORT 改成 19531。

这组操作只适合隔离的测试环境。看到结果后,先删除两个 toxic,再执行 docker rm -f clxzs-milvus-toxiproxy,把应用端口恢复为 19530 并重启。健康检查不为 UP、检索没有返回 200,或恢复后耗时没有回落,都不能算实验结束。

每个阶段使用相同接口和相同问题,记录固定次数,然后移除延迟,再跑恢复组。因此,后面看到向量检索耗时增加时,先把它叫作"到 Milvus 的网络变慢",不能直接说成"Milvus 服务器变慢"。

为了减少流式模型波动对结论的干扰,我同时看两组数据:完整问答请求用于观察一条真实 RAG Trace;独立检索端点连续调用三次,用于比较向量检索的中位数。再用 Micrometer 累计值计算 VectorStore、Embedding、ChatModel 的阶段平均耗时。

故障阶段,完整请求在观测云里显示总耗时 2.40 秒,其中 Milvus query 约 1.37 秒,ChatModel 约 1.02 秒。

恢复直连后,一条请求总耗时 1.84 秒,Milvus query 回到约 739 毫秒,ChatModel 约 1.05 秒。

单条 Trace 会受模型响应、网络抖动和缓存影响,所以我没有拿这两张图直接算百分比。更稳的对照来自固定次数的检索请求和阶段累计指标:

观察项 故障阶段 恢复阶段 变化
/debug/search 三次中位数 1595ms 1097ms 增加约498ms
VectorStore 平均耗时 1395ms 838ms 增加约557ms
Embedding 平均耗时 158ms 244ms 没有随故障升高
ChatModel 耗时 1023ms 1050ms 基本稳定
Milvus Proxy 服务端平均耗时 466ms 507ms 没有随故障升高
Milvus QueryNode 样本平均耗时 233ms 253ms 没有随故障升高

这个表就是整次排查最有价值的证据。

应用侧看到 VectorStore 多了约 557ms,但 Embedding 和 ChatModel 没有同步变慢。继续看 Milvus 服务端,Proxy 与 QueryNode 的处理时间也没有升高。客户端多花了时间,服务端却没有多做同等时间的工作,差值最合理的去向就是应用和 Milvus 之间的网络路径。

这里有两个数字看上去可能不够"整齐"。我注入的是上下行各 350ms,理论上是 700ms,但 /debug/search 中位数只增加约 498ms,VectorStore 平均值增加约 557ms。真实系统里的连接复用、采样窗口、远程服务自然波动都会影响结果,所以不能期待观测值与配置值毫秒不差地相等。

我更关心的是方向是否一致:故障阶段客户端向量检索明显抬高,恢复后回落;与这条路径无关的 Embedding 和 ChatModel 没有同方向上涨;Milvus 内部指标也没有出现对应增量。

这也是为什么完整问答和独立检索要分开测。大模型生成具有随机波动,如果只拿两次完整请求比较,很可能一次模型快、一次模型慢,反而遮住网络延迟。独立检索端点减少了模型变量,完整 Trace 则保留真实用户链路,两者各自回答不同问题。

如果只看应用总耗时,我只能说"RAG 变慢了"。如果只看 VectorStore Span,我可能会说"Milvus 变慢了"。把应用 Trace 和 Milvus 服务端指标放在一起,才有足够依据把范围缩到网络。

实验结束后,我删除了 Toxiproxy 的 toxic 和测试容器,应用切回远程 Milvus 直连。健康检查为 UP,向量检索返回 200,恢复组指标也回落。

把三层数据放在一起,少猜一轮

先说结论:观测云没有替我"自动诊断出网络问题"。它提供的是把不同层数据放到一起观察的条件,最后的判断仍然来自实验设计和交叉验证。

最直接的帮助是一条 Trace 把一次 RAG 请求拆开了。以前我只能看到接口总耗时,现在能点开某次请求,确认时间主要落在向量检索还是模型生成。对于偶发慢请求,这比只看平均值更有用。

Spring AI 指标和 Milvus 指标又补上了整体视角。Trace 告诉我"这一次"发生了什么,Prometheus 指标告诉我"这一类操作"是否持续异常。两种视角放在一起,才完成了这次网络与服务端的排除。

DataKit 则解决了数据怎么汇到一起:应用只向本机 OTLP 端点上报,管理端点也只监听本机;DataKit 再去访问远程 Milvus 指标。这种部署对 Windows 开发环境和小规模实践比较顺手。

但它也不是接上就万事大吉。

链路是否完整,取决于应用有没有正确传播上下文;自动 Span 覆盖多少,取决于代码是否走 Spring AI 的标准组件;指标量和成本,取决于采集范围与标签基数;真正能否定位问题,还取决于有没有基线和对照。

这次页面没有自动弹出一句"根因是网络"。它让我更快地把一次请求、应用指标和 Milvus 指标放在一起,但故障变量仍要由人控制,比较口径也要事先确定。少了这两步,同样一张图可能被解释成不同结论。

从这次体验看,它比较适合已经有多段调用、又不想分别维护链路平台和指标平台的团队。尤其是 RAG 这种跨模型服务、向量库和业务代码的流程,一条请求能下钻到各阶段,再切到同工作空间的服务指标,排查路径会短很多。

相应的代价也很现实。你需要维护 DataKit 配置,理解指标名称,处理采样和数据量,还要给服务补齐稳定的标签。页面不会替你决定哪些标签会造成高基数,也不会知道哪个业务字段敏感。对只有一个接口、几乎没有跨服务调用的小项目,这套投入未必划算;对已经出现偶发慢请求、并且靠日志难以复现的 RAG 应用,收益才更明显。

因此,在这次 Windows 本机应用加远程 Milvus 的实践中,它确实承接了 OTLP Trace、Spring AI 指标和 Milvus 指标,并支持我完成一次从故障到恢复的跨层对照。

如果项目直接调用 DashScope 原生 SDK、Milvus Java SDK,或者把整个 RAG 流程封装在自定义线程和异步任务里,部分步骤可能不会自动出现。这时需要在业务边界补自定义 Observation 或 OpenTelemetry Span,而不是认为换一个监控平台就会自动补齐。

把这套方法带到另一个项目

如果你也要给 RAG 应用加观测,更省力的起点是一条最小闭环,而不是先做十几张面板。

先选一个稳定请求,记录正常状态下的完整 Trace。确认 Embedding、VectorStore、ChatModel 都在同一条链路,父子关系正确。这个结果就是基线。

然后确认三个数据入口:应用 OTLP Trace、应用 Prometheus、Milvus Prometheus。任意一个缺失,先修采集,不急着做故障结论。

接着做一次可恢复的小故障。可以是测试环境中的网络延迟、受控超时或限流,但一次只改一个变量,并且提前写清恢复方法。不要直接在生产环境里试。

最后按下面的顺序判断:

看到的现象 优先怀疑 下一步
ChatModel Span 上升,向量检索稳定 模型服务或模型网络 看模型请求错误率、首字时间和供应商状态
Embedding Span 上升,Milvus稳定 Embedding 服务 看向量化请求、限流、超时和批量大小
VectorStore 客户端与 Milvus 服务端同时上升 Milvus内部或资源压力 看 Proxy、QueryNode、CPU、内存和磁盘
VectorStore 客户端上升,Milvus服务端稳定 应用到Milvus的网络路径 看跨机房、代理、连接池、DNS和丢包
总耗时上升,但各关键Span变化不大 未覆盖的业务代码或排队 补业务Span,检查线程池和异步边界

这张表不是通用真理,但它能避免一看到"向量检索慢"就立刻调 Milvus 参数。先判断慢时间发生在客户端、网络还是服务端,再决定改什么。

项目接入观测云,真正容易踩的是这几处

第一处是把 DataKit 的两个 OTLP 入口混在一起。4317 通常接收 gRPC,9529 提供 HTTP 接口。本项目使用 Spring Boot 的 HTTP 导出方式,所以地址写成 http://127.0.0.1:9529/otel/v1/traces。如果只照搬一个端口,协议和路径却没对上,DataKit 服务看起来正常,观测云里仍然收不到 Trace。

第二处是忽略 DataKit 装在哪里。本次 DataKit 和应用在同一台 Windows 电脑上,因此 Actuator 绑定 127.0.0.1:9167 仍能被采集。如果 DataKit 改装到服务器,这个地址就只代表服务器自己,抓不到开发电脑的管理端点。要么让采集器与应用同机,要么给管理端点配置受控的可访问地址,不能原样复制本次地址。

第三处是混淆 Milvus 的业务端口和指标端口。应用通过 19530 做向量查询,DataKit 抓取的是 9091/metrics。只测试 19530 能连通,并不能说明 Milvus 指标能采到;远程部署时还要确认 9091 对 DataKit 所在机器可达,并限制访问来源。

第四处出现在故障恢复。Toxiproxy 容器删掉以后,如果应用仍指向 19531,下一次请求会直接连接失败。恢复时要把 MILVUS_PORT 改回 19530,重启应用,再依次检查健康状态、检索响应和向量检索耗时。三个结果都恢复,才算测试环境真正收尾。

第五处是以为所有 RAG 代码都会自动产生 Span。ChatClient、Advisor、EmbeddingModel、VectorStore 走 Spring AI 标准组件时,自动观测比较完整;如果代码直接调用 DashScope 或 Milvus 原生 SDK,或者把检索放进自定义异步线程,缺失的步骤仍要补 Observation 或自定义 Span。

最后一处是为了"看得更详细"打开正文采集。log-promptlog-completionlog-query-response 对这次耗时定位没有必要,保持关闭也能区分模型、向量检索和网络。如果确实要打开,应该先确认用户问题、模型回答和召回文档允许进入观测平台。

最后的判断

做完这次实践后,我对"RAG 可观测"有了一个很朴素的标准:不是页面里出现几条 Trace、几张曲线就算完成,而是当一次回答变慢时,能沿着同一请求找到可疑阶段,再用另一层数据排除错误判断。

在这次实验里,完整 Trace 先把慢点指向 VectorStore;应用指标确认它不是单条请求的偶然波动;Milvus 服务端指标又排除了内部处理变慢。三层证据拼在一起,才得到"应用到 Milvus 的网络链路变慢"这个结论。

观测云在这里更像一个工作台:它把 Trace、应用指标和远程服务指标放到了同一个地方。工作台本身不替代判断,但能让判断有证据、有路径,也能在恢复后看到结果是否真的回落。

如果准备把这套方法用到自己的项目,先别急着做大屏。挑一个真实请求,确认链路完整,留一份正常基线,再制造一个可恢复的小故障。移除故障后,直到健康检查为 UP、检索返回 200、关键耗时回到基线附近,这次排查才算结束。

监控的终点不是"看见数据",而是少走一次错误的排查方向。对 RAG 来说,先把模型、向量检索和网络拆开看

相关推荐
loopne1 小时前
AI网文写作全景调研(十):合规篇——检测、平台政策、版权法律
经验分享·笔记·ai写作·智能写作
叠层归一研究院16 小时前
基于极限自指的叠层归一宇宙结构理论——无元外部封闭系统的内生区分模型
人工智能·经验分享·算法·agi
富唯智能机器人16 小时前
复合机器人一键标定技术|告别繁琐调试,实现快速部署投产|富唯智能
经验分享
恒锐丰科技林技术员17 小时前
EG1196S:高压宽输入降压 DC‑DC 芯片,电动车工业电源的优选方案
经验分享·嵌入式硬件·硬件工程
weixin_5372170620 小时前
工业设计网盘资料
经验分享
明天再做行么1 天前
AI电商资源合集
经验分享
小李不想当小白1 天前
PCB结构与组成(PCB设计入门学习笔记)
经验分享·笔记·单片机·嵌入式硬件·学习·pcb工艺·pcb
LuminousCPP1 天前
Linux系统编程(二):从目录树到命令执行|路径、环境变量、alias 与文件操作入门
linux·经验分享·笔记
AI袋鼠帝1 天前
WorkBuddy+仓颉.Skill 2.5,免费蒸馏飞书的任何内容!
人工智能·经验分享