ARTICLE · 1117587
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里包住整段nextCallAdvisor: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_CLIENTAdvisorObservationDocumentation.AI_ADVISORChatModelObservationDocumentation.CHAT_MODEL_OPERATIONToolCallingObservationDocumentation.TOOL_CALL
位置 2:样例 LayerLoggingObservationHandler#onStart
三、gen_ai.* 与 spring.ai.* 如何分工
实际设计
跨层通用 AI 语义集中在:
org.springframework.ai.observation.conventions.AiObservationAttributes
注释写明灵感来自 OpenTelemetry GenAI Semantic Conventions。常见键包括:
gen_ai.operation.namegen_ai.system(provider;较新的 OTel 规格里对应关系在演进,阅读时以当前枚举字符串为准)gen_ai.request.modelgen_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.advisorsspring.ai.chat.client.conversation.idspring.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 分层本身)。
七、读源码复盘
有几层、Context 叫什么?
ChatClient / Advisor / ChatModel(+ Tool);对应
*ObservationContext。属性谁定、如何分工?
gen_ai.*在AiObservationAttributes;spring.ai.*补充管道结构。observe 包住哪一句?
分别包住 ChatClient 的
nextCall、每个 Advisor 的adviseCall、chatModel.call、工具 callback。正文日志为何默认关?
隐私与体量;由配置打开的 ObservationHandler 写入,不是业务手打。
这几个问题搞懂了,再去做 Insight 式的 AI 观测或对接 Langfuse,就知道该认哪一层 span、哪些属性可信、哪些不该默认采集。
八、小结
分层观测 —— 与调用链同构:Client → Advisor → Model(→ Tool)。 双前缀属性 —— gen_ai.*通用语义,spring.ai.*框架结构。就地 observe —— 每层入口各包一层,Tool 循环带来多轮内侧观测。 正文可选 —— prompt/completion 默认关闭,Handler 按需开启。
下期预告:回到装配 —— ChatClient.Builder 是谁创建的,Starter / AutoConfiguration / BOM 如何把 Model、Observation、ToolCallingAdvisor 接到一起。