夜雨聆风学习资料网

ARTICLE · 1117587

Spring AI 源码阅读(四)——Observation:一次调用在观测里长什么样

Spring AI 源码阅读(四)——Observation:一次调用在观测里长什么样

配套仓库:https://github.com/iweidujiang/spring-ai-source-notes

对照源码:Spring AI 2.0.1(与当前 main 主干类名一致)

样例:LayerLoggingObservationHandler、/observation/meters;请求用 /chat 与 /chat/tools

本文导航

前三篇已经把一次请求拆成:构建与执行、Advisor 链、Tool 循环。

跑样例时控制台里还会冒出另一类日志,例如:

[obs:start] kind=ChatClient ...
[obs:start] kind=Advisor ...
[obs:start] kind=ChatModel ...
[obs:stop]  kind=ChatModel ...
...

很多人会默认:

框架在最外层包了一个大 span,里面发生什么黑盒即可。

本文的重点是:

Observation 打在哪几层、属性谁定、observe 包在调用链的哪一句,以及 prompt 正文为何默认不进观测。

  • 看见一次 /chat,先问:有几层 start/stop,Context 类型分别是什么?
  • 看见标签名,先问:**gen_ai.* 与 spring.ai.* 各管什么?**
  • 看见 observe(...),先问:它包住的是 content、nextCall、chatModel.call,还是工具执行?

一、它要解决什么问题

AI 调用和普通 HTTP 不一样:一次用户请求里可能有 多轮模型、多次工具、多段 Advisor。

若观测只有一个“总耗时”,线上只能知道慢,不知道慢在模型、工具还是某条 Advisor。

如果只有业务里手打日志,常见麻烦是:

  • 分层对不齐:ChatClient / Advisor / Model / Tool 各写各的,对不上同一次请求
  • 语义不统一:token、模型名、厂商字段各项目自创
  • 隐私:prompt 全文默认进日志,容易泄密
  • 后端对接:无法稳定接到 OTel / Prometheus / Langfuse 一类后端

Spring AI 的选择是:在既有 Micrometer Observation 上,为每一层提供约定好的 Documentation + Convention + Context,需要时再挂 Handler(打日志、记指标、导出 trace)。

二、打在哪几层,Context 叫什么

实际设计

一次同步 call().content()(无工具)至少三层嵌套:

  • ChatClient:Context 为 ChatClientObservationContext;在 DefaultCallResponseSpec 里包住整段 nextCall
  • Advisor:Context 为 AdvisorObservationContext;在 DefaultAroundAdvisorChain.nextCall 里每个 Advisor 一截
  • ChatModel:Context 为 ChatModelObservationContext;如 OllamaChatModel.call 里包住单次推理

有 Tool Calling 时再加:

  • Tool:ToolCallingObservationDocumentation.TOOL_CALL;在 DefaultToolCallingManager 执行某个工具时打开

结合第(三)篇 Tool Calling:谁在跑循环、谁在调本地方法:ToolCallingAdvisor 的 while 每转一轮,内侧的 ChatModel(以及需要时的 Tool)Observation 会再起停一轮;外层 ChatClient 通常仍是包住整次 content() 的那一次。

样例 LayerLoggingObservationHandler 只订阅 org.springframework.ai.* 的 Context,把 kind(类名去掉 ObservationContext)和 name 打到控制台 —— 用来对照层,不代替正式导出。

源码落点

位置 1:各层 Documentation 枚举

  • ChatClientObservationDocumentation.AI_CHAT_CLIENT
  • AdvisorObservationDocumentation.AI_ADVISOR
  • ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
  • ToolCallingObservationDocumentation.TOOL_CALL

位置 2:样例 LayerLoggingObservationHandler#onStart

三、gen_ai.* 与 spring.ai.* 如何分工

实际设计

跨层通用 AI 语义集中在:

org.springframework.ai.observation.conventions.AiObservationAttributes

注释写明灵感来自 OpenTelemetry GenAI Semantic Conventions。常见键包括:

  • gen_ai.operation.name
  • gen_ai.system(provider;较新的 OTel 规格里对应关系在演进,阅读时以当前枚举字符串为准)
  • gen_ai.request.model
  • gen_ai.usage.input_tokens / output_tokens / total_tokens 等

指标名见 AiObservationMetricNames(如 gen_ai.client.operation.duration、gen_ai.client.token.usage)。样例里 GET /observation/meters 过滤的就是这类名字。

Spring AI 结构补充用 spring.ai.*,例如 ChatClient 高基数标签(ChatClientObservationDocumentation):

  • spring.ai.chat.client.advisors
  • spring.ai.chat.client.conversation.id
  • spring.ai.chat.client.tool.names
  • 以及低基数里的 spring.ai.kind、spring.ai.chat.client.stream 等

gen_ai.* 回答“对哪个模型做了什么、花了多少 token”;spring.ai.* 回答“Spring 这条管道上挂了谁”。

源码落点

位置 3:AiObservationAttributes

位置 4:DefaultChatClientObservationConvention

  • 看它往 ChatClient 观测上挂了哪些 low / high cardinality key

四、observe(...) 包在调用链的哪一句

不是整个应用一个大 observe,而是 每层在自己的入口各包一层。

对照前三篇:

  • **(一)content()**:拧开水龙头;AI_CHAT_CLIENT.observation(...).observe(() -> advisorChain.nextCall(...))
  • **(二)nextCall**:每个 Advisor;AI_ADVISOR.observation(...).observe(() -> advisor.adviseCall(...))
  • (三)工具执行:调 @Tool;TOOL_CALL.observation(...).observe(() -> 真正 invoke callback)
  • 模型单次:chatModel.call;CHAT_MODEL_OPERATION.observation(...).observe(() -> 推理 / HTTP)

因此:Tool 循环每多一轮,你就会多看到一轮 ChatModel(以及 Tool)的 start/stop;ChatClient 外层往往仍是一次。

源码落点

位置 5:DefaultCallResponseSpec 中 AI_CHAT_CLIENT + observe

位置 6:DefaultAroundAdvisorChain#nextCall 中 AI_ADVISOR

位置 7:OllamaChatModel#call 中 CHAT_MODEL_OPERATION

位置 8:DefaultToolCallingManager 中 TOOL_CALL

五、prompt / completion 为何默认不进观测

要解决的问题

Prompt 与模型输出经常含用户原文、订单号、密钥线索。默认写入 span/日志会带来:

  • 隐私与合规风险
  • 高基数与体量暴涨(观测后端与费用)

设计选择

配置默认关闭,需要时显式打开:

spring:
ai:
chat:
client:
observations:
log-prompt:true
log-completion:true
observations:
log-prompt:true
log-completion:true

打开后,自动配置注册专用的 ObservationHandler(ChatClient / ChatModel 的 Prompt、Completion Handler),在观测生命周期里把内容写进观测或日志。

样例里的 LayerLoggingObservationHandler只打 kind/name,不负责正文;正文是另一条可选 Handler 链路。

阅读源码时打开开关是为了看清机制;生产环境应默认关闭,或经脱敏后再开。

六、总览:一次带工具的调用在观测里的形状

/chat/tools → content()
  ChatClient span/obs
    ToolCallingAdvisor(while,本身也是 Advisor obs)
      第 1 轮:内侧 Advisor obs → ChatModel obs
      Tool obs(currentDateTime / add)
      第 2 轮:内侧 Advisor obs → ChatModel obs
  → 返回文本

可与熟悉概念对照:

  • Observation + Convention:Micrometer / OTel 的 span 约定
  • Context:span 上的属性袋
  • ObservationHandler:Exporter / 日志附录
  • **gen_ai.***:领域标准语义
  • **spring.ai.***:框架私有扩展标签

指标侧可通过 Actuator metrics 与 /observation/meters 看到 gen_ai.* 相关 meter;完整链路追踪还需配置 tracing 导出(本文只到 Observation 分层本身)。

七、读源码复盘

  1. 有几层、Context 叫什么?

    ChatClient / Advisor / ChatModel(+ Tool);对应 *ObservationContext。

  2. 属性谁定、如何分工?

    gen_ai.* 在 AiObservationAttributes;spring.ai.* 补充管道结构。

  3. observe 包住哪一句?

    分别包住 ChatClient 的 nextCall、每个 Advisor 的 adviseCall、chatModel.call、工具 callback。

  4. 正文日志为何默认关?

    隐私与体量;由配置打开的 ObservationHandler 写入,不是业务手打。

这几个问题搞懂了,再去做 Insight 式的 AI 观测或对接 Langfuse,就知道该认哪一层 span、哪些属性可信、哪些不该默认采集。

八、小结

  1. 分层观测 —— 与调用链同构:Client → Advisor → Model(→ Tool)。
  2. 双前缀属性 —— gen_ai.* 通用语义,spring.ai.* 框架结构。
  3. 就地 observe —— 每层入口各包一层,Tool 循环带来多轮内侧观测。
  4. 正文可选 —— prompt/completion 默认关闭,Handler 按需开启。

下期预告:回到装配 —— ChatClient.Builder 是谁创建的,Starter / AutoConfiguration / BOM 如何把 Model、Observation、ToolCallingAdvisor 接到一起。

相关学习资料