【AI 工程化第四篇】Spring AI Agent 线上可观测实战:Token 成本、调用链、工具耗时、RAG 命中率怎么监控
系列定位:
第一篇:搭 Agent 服务。
第二篇:接 RAG 知识库。
第三篇:用 MCP / Tool Calling 让 Agent 调用业务系统。
第四篇:把 Agent 放到线上之后,如何知道它"慢在哪、贵在哪、错在哪"。
技术栈:Java 17 · Spring Boot 4.1.x / 3.5.x · Spring AI 2.0.x · Actuator · Micrometer · Prometheus · Grafana · OpenTelemetry
适用场景:AI 网关、企业知识助手、智能客服、订单 Agent、内部 Copilot
阅读前置:会用 Spring Boot Actuator 即可
为什么 AI 应用比普通后端更需要可观测
普通后端接口出问题,通常看三件事:
text
接口耗时
错误日志
数据库 / Redis / MQ 状态
AI Agent 上线后,问题会复杂很多:
text
为什么这次回答花了 12 秒?
为什么昨天 Token 成本突然翻倍?
为什么 RAG 明明有资料却没召回?
为什么模型重复调用同一个工具?
为什么用户说 AI 胡说八道,但日志里看不到上下文?
如果没有可观测体系,AI 应用上线后就会变成一个黑盒:
text
用户问了什么,不知道
模型用了多少 token,不知道
RAG 召回了什么,不知道
工具调用成功没,不知道
慢在模型、向量库还是 MCP,不知道
这篇文章就解决这个问题。
目标不是"打印几行日志",而是搭一个生产可用的观测闭环:
text
日志:能定位单次问题
指标:能看整体趋势
链路:能看一次请求经过哪些环节
审计:能回放 AI 当时为什么这么回答
告警:能在成本、错误、延迟异常时提前发现
一、先看最终效果
上线后,我们希望每次 Agent 请求都有这些数据:
json
{
"traceId": "b7f42f23c9d94b9e8d8a90d0c1f3a110",
"conversationId": "conv-001",
"scene": "order-agent",
"model": "gpt-4.1-mini",
"promptTokens": 1840,
"completionTokens": 286,
"totalTokens": 2126,
"ragTopK": 6,
"ragHitCount": 4,
"toolCallCount": 2,
"costMillis": 4238,
"success": true
}
Grafana 面板至少要有这些图:
text
QPS / 错误率 / P95 延迟
Token 总量 / 单次平均 Token / 按模型拆分成本
RAG 召回命中率 / 低分召回占比 / 向量检索耗时
工具调用次数 / 工具失败率 / 工具 P95 耗时
模型调用失败率 / 超时率 / 重试次数
这才是 Agent 能上线运营的基础。
二、Spring AI 已经内置了哪些观测能力
Spring AI 2.0.1 已经基于 Micrometer 提供了核心观测能力,覆盖:
| 组件 | 观测能力 |
|---|---|
| ChatClient | call() / stream() 调用耗时、Advisor、工具名、会话信息 |
| ChatModel | 模型调用耗时、输入/输出 Token、模型名 |
| EmbeddingModel | embedding 调用耗时、Token、向量维度 |
| VectorStore | query / add / remove 操作耗时、向量库信息 |
| Tool Calling | 工具名、工具类型、工具调用耗时、trace 上下文 |
官方文档里有几个关键点要记住:
- Spring AI 使用 Micrometer。
ChatModel提供gen_ai.client.token.usage指标,用于统计 Token。- Prompt、Completion、工具参数、工具结果默认不会导出,因为可能包含敏感信息。
spring.ai.tools.observations.include-content=true可以导出工具入参和结果,但生产环境要非常谨慎。- VectorStore 操作也有观测,名称是
db.vector.client.operation。
换句话说,Spring AI 已经帮我们打好了底座。
但只靠框架内置指标还不够。
业务侧还需要补充:
text
conversationId
userId
tenantId
业务场景 scene
RAG 命中情况
工具调用审计
单次请求总成本
业务失败原因
三、整体架构设计
3.1 功能模块图
text
┌────────────────────────────────────────────────────────────────────┐
│ 前端 / 调用方 │
│ Web · 企业微信 · 钉钉 · 内部客服系统 │
└─────────────────────────────┬──────────────────────────────────────┘
│
┌─────────────────────────────▼──────────────────────────────────────┐
│ AI Agent 应用 │
│ │
│ Controller │
│ - 生成 traceId │
│ - 参数校验 │
│ - MDC 日志上下文 │
│ │
│ AgentService │
│ - ChatClient │
│ - RAG Advisor / VectorStore │
│ - Tool Calling / MCP │
│ - AI 调用日志 │
│ │
│ Observability Layer │
│ - Micrometer Metrics │
│ - Observation / Tracing │
│ - Tool Audit │
│ - RAG Hit Log │
└───────────────┬────────────────────┬───────────────────────────────┘
│ │
┌───────────────▼────────────┐ ┌─────▼──────────────────────────────┐
│ Prometheus │ │ OpenTelemetry / Tempo │
│ 拉取 /actuator/prometheus │ │ Trace 链路追踪 │
└───────────────┬────────────┘ └─────┬──────────────────────────────┘
│ │
┌───────────────▼────────────────────▼──────────────────────────────┐
│ Grafana Dashboard │
│ 延迟 · 错误率 · Token 成本 · RAG 命中 · 工具耗时 · 告警 │
└────────────────────────────────────────────────────────────────────┘
3.2 单次请求链路
text
用户请求
-> Controller 生成 traceId
-> ChatClient observation
-> RAG Advisor observation
-> VectorStore query observation
-> ChatModel observation
-> Tool Calling observation
-> MCP / 业务工具
-> Usage 汇总
-> 业务日志表落库
-> Micrometer 指标上报
这条链路里,至少要能回答三个问题:
text
慢在哪:ChatModel / VectorStore / Tool / DB
贵在哪:哪个模型、哪个场景、哪个租户 token 高
错在哪:RAG 召回错、工具调用错、模型输出错、业务权限错
四、依赖配置
4.1 Maven 依赖
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<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>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>
</dependencies>
4.2 application.yml
yaml
spring:
application:
name: ai-agent-app
ai:
model:
chat: openai
embedding: openai
openai:
api-key: ${OPENAI_API_KEY}
base-url: ${OPENAI_BASE_URL:https://api.openai.com}
chat:
model: gpt-4.1-mini
temperature: 0.2
# 生产环境不建议打开 prompt / completion 全量日志
chat:
observations:
log-prompt: false
log-completion: false
include-error-logging: true
client:
observations:
log-prompt: false
log-completion: false
tools:
observations:
include-content: false
limits:
max-calls-per-tool-default: 3
max-total-tool-calls: 8
on-limit-exceeded: RETURN_ERROR_RESPONSE
management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
endpoint:
health:
probes:
enabled: true
metrics:
tags:
application: ${spring.application.name}
tracing:
sampling:
probability: 1.0
otlp:
tracing:
endpoint: http://localhost:4318/v1/traces
注意:如果你的 Prompt 里可能包含手机号、订单号、客户资料、合同内容,不要在生产环境开启
log-prompt和include-content。调试环境也建议脱敏后再打开。
五、核心表结构
内置 Micrometer 指标适合看趋势,业务表适合追溯单次请求。
这两个都要有。
5.1 AI 调用日志表
sql
CREATE TABLE t_ai_call_log (
id BIGSERIAL PRIMARY KEY,
trace_id VARCHAR(64) NOT NULL,
conversation_id VARCHAR(64),
tenant_id VARCHAR(64) NOT NULL DEFAULT 'default',
user_id VARCHAR(64),
scene VARCHAR(64) NOT NULL,
model VARCHAR(64),
prompt_tokens INT DEFAULT 0,
completion_tokens INT DEFAULT 0,
total_tokens INT DEFAULT 0,
rag_hit_count INT DEFAULT 0,
tool_call_count INT DEFAULT 0,
cost_millis BIGINT DEFAULT 0,
success BOOLEAN DEFAULT TRUE,
error_type VARCHAR(64),
error_msg VARCHAR(500),
create_time TIMESTAMP DEFAULT now()
);
CREATE INDEX idx_ai_call_trace ON t_ai_call_log(trace_id);
CREATE INDEX idx_ai_call_scene_time ON t_ai_call_log(scene, create_time DESC);
5.2 RAG 命中日志表
sql
CREATE TABLE t_ai_rag_hit_log (
id BIGSERIAL PRIMARY KEY,
trace_id VARCHAR(64) NOT NULL,
conversation_id VARCHAR(64),
kb_code VARCHAR(64) NOT NULL,
user_query TEXT NOT NULL,
chunk_id VARCHAR(64) NOT NULL,
document_id VARCHAR(64) NOT NULL,
score NUMERIC(8, 6),
rank_no INT NOT NULL,
create_time TIMESTAMP NOT NULL DEFAULT now()
);
CREATE INDEX idx_rag_hit_trace ON t_ai_rag_hit_log(trace_id);
5.3 工具调用日志表
sql
CREATE TABLE t_ai_tool_call_log (
id BIGSERIAL PRIMARY KEY,
trace_id VARCHAR(64) NOT NULL,
conversation_id VARCHAR(64),
tenant_id VARCHAR(64) NOT NULL,
user_id VARCHAR(64) NOT NULL,
tool_name VARCHAR(128) NOT NULL,
success BOOLEAN NOT NULL DEFAULT TRUE,
error_msg VARCHAR(500),
cost_millis BIGINT NOT NULL DEFAULT 0,
create_time TIMESTAMP NOT NULL DEFAULT now()
);
CREATE INDEX idx_tool_log_trace ON t_ai_tool_call_log(trace_id);
CREATE INDEX idx_tool_log_name_time ON t_ai_tool_call_log(tool_name, create_time DESC);
六、TraceId 过滤器
所有日志、指标、数据库记录都要能通过 traceId 串起来。
java
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.UUID;
@Component
public class TraceIdFilter extends OncePerRequestFilter {
public static final String TRACE_ID = "traceId";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String traceId = request.getHeader("X-Trace-Id");
if (traceId == null || traceId.isBlank()) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
try {
MDC.put(TRACE_ID, traceId);
response.setHeader("X-Trace-Id", traceId);
filterChain.doFilter(request, response);
} finally {
MDC.remove(TRACE_ID);
}
}
}
日志格式建议带上 traceId:
yaml
logging:
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%X{traceId}] [%thread] %logger{36} - %msg%n"
七、业务指标 Recorder
不要把所有维度都塞进 Prometheus 标签。
低基数标签可以放:
text
scene
model
result
toolName
kbCode
高基数字段不要放:
text
userId
conversationId
traceId
orderNo
message
这些应该进日志或数据库。
java
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
public class AiMetricsRecorder {
private final MeterRegistry meterRegistry;
public AiMetricsRecorder(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
public void recordCall(String scene,
String model,
boolean success,
int totalTokens,
Duration cost) {
String result = success ? "success" : "fail";
Counter.builder("ai_agent_requests_total")
.tag("scene", scene)
.tag("model", model)
.tag("result", result)
.register(meterRegistry)
.increment();
DistributionSummary.builder("ai_agent_tokens_total")
.tag("scene", scene)
.tag("model", model)
.register(meterRegistry)
.record(totalTokens);
Timer.builder("ai_agent_request_duration")
.tag("scene", scene)
.tag("model", model)
.tag("result", result)
.publishPercentileHistogram()
.register(meterRegistry)
.record(cost);
}
public void recordRag(String kbCode, int hitCount, Duration cost) {
DistributionSummary.builder("ai_rag_hit_count")
.tag("kbCode", kbCode)
.register(meterRegistry)
.record(hitCount);
Timer.builder("ai_rag_retrieve_duration")
.tag("kbCode", kbCode)
.publishPercentileHistogram()
.register(meterRegistry)
.record(cost);
}
public void recordTool(String toolName, boolean success, Duration cost) {
Timer.builder("ai_tool_call_duration")
.tag("toolName", toolName)
.tag("result", success ? "success" : "fail")
.publishPercentileHistogram()
.register(meterRegistry)
.record(cost);
Counter.builder("ai_tool_calls_total")
.tag("toolName", toolName)
.tag("result", success ? "success" : "fail")
.register(meterRegistry)
.increment();
}
}
八、AgentService 中记录 Token、耗时、RAG、工具
Spring AI 的 ChatResponse 可以拿到 Usage。
官方文档里有一个关键点:如果一次 ChatClient 请求触发了工具调用 loop,getUsage() 返回的是整个多步流程累计 Token,而不只是最后一次模型调用。
这对成本统计非常有用。
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.time.Instant;
@Service
public class ObservableAgentService {
private static final Logger log = LoggerFactory.getLogger(ObservableAgentService.class);
private final ChatClient chatClient;
private final JdbcTemplate jdbcTemplate;
private final AiMetricsRecorder metricsRecorder;
public ObservableAgentService(ChatClient.Builder chatClientBuilder,
JdbcTemplate jdbcTemplate,
AiMetricsRecorder metricsRecorder) {
this.chatClient = chatClientBuilder.build();
this.jdbcTemplate = jdbcTemplate;
this.metricsRecorder = metricsRecorder;
}
public String chat(String scene, String conversationId, String userId, String message) {
String traceId = MDC.get("traceId");
Instant start = Instant.now();
Usage usage = null;
boolean success = false;
String errorType = null;
String errorMsg = null;
try {
ChatResponse response = chatClient.prompt()
.system("你是企业 AI Agent,请基于知识库和工具结果回答。")
.user(message)
.call()
.chatResponse();
usage = response.getMetadata().getUsage();
success = true;
return response.getResult().getOutput().getText();
} catch (Exception ex) {
errorType = ex.getClass().getSimpleName();
errorMsg = ex.getMessage();
log.error("Agent调用失败 traceId={}, scene={}, conversationId={}",
traceId, scene, conversationId, ex);
throw new BizException("AI 服务暂时不可用,请稍后重试");
} finally {
long costMillis = Duration.between(start, Instant.now()).toMillis();
int promptTokens = usage == null ? 0 : usage.getPromptTokens();
int completionTokens = usage == null ? 0 : usage.getCompletionTokens();
int totalTokens = usage == null ? 0 : usage.getTotalTokens();
jdbcTemplate.update("""
INSERT INTO t_ai_call_log
(trace_id, conversation_id, tenant_id, user_id, scene, model,
prompt_tokens, completion_tokens, total_tokens,
cost_millis, success, error_type, error_msg)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
traceId,
conversationId,
"default",
userId,
scene,
"gpt-4.1-mini",
promptTokens,
completionTokens,
totalTokens,
costMillis,
success,
errorType,
errorMsg
);
metricsRecorder.recordCall(
scene,
"gpt-4.1-mini",
success,
totalTokens,
Duration.ofMillis(costMillis)
);
log.info("Agent调用完成 traceId={}, scene={}, conversationId={}, promptTokens={}, completionTokens={}, totalTokens={}, costMillis={}, success={}",
traceId, scene, conversationId, promptTokens, completionTokens, totalTokens, costMillis, success);
}
}
}
九、RAG 命中率怎么监控
RAG 的观测不能只看向量库耗时。
还要看:
text
topK 召回了几条
平均 score
最低 score
低于阈值的比例
最终引用了几条
用户是否追问"你答错了"
示例代码:
java
import org.springframework.ai.document.Document;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
@Service
public class RagObserveService {
private final JdbcTemplate jdbcTemplate;
private final AiMetricsRecorder metricsRecorder;
public RagObserveService(JdbcTemplate jdbcTemplate,
AiMetricsRecorder metricsRecorder) {
this.jdbcTemplate = jdbcTemplate;
this.metricsRecorder = metricsRecorder;
}
public void record(String traceId,
String conversationId,
String kbCode,
String userQuery,
List<Document> docs,
Instant start) {
int rank = 1;
for (Document doc : docs) {
jdbcTemplate.update("""
INSERT INTO t_ai_rag_hit_log
(trace_id, conversation_id, kb_code, user_query, chunk_id, document_id, score, rank_no)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
traceId,
conversationId,
kbCode,
userQuery,
String.valueOf(doc.getMetadata().get("chunkId")),
String.valueOf(doc.getMetadata().get("documentId")),
doc.getScore() == null ? BigDecimal.ZERO : BigDecimal.valueOf(doc.getScore()),
rank++
);
}
metricsRecorder.recordRag(kbCode, docs.size(), Duration.between(start, Instant.now()));
}
}
推荐告警:
text
RAG 命中数连续 10 分钟低于 1
RAG P95 检索耗时 > 1s
某知识库低分召回比例 > 30%
向量入库失败数 > 0
十、工具调用怎么监控
工具调用是 Agent 最危险的部分。
至少要监控:
text
工具调用次数
工具失败率
工具耗时
写操作次数
重复调用次数
工具调用限制触发次数
示例:
java
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.time.Instant;
@Service
public class ToolObserveService {
private final AiMetricsRecorder metricsRecorder;
private final ToolAuditRepository toolAuditRepository;
public ToolObserveService(AiMetricsRecorder metricsRecorder,
ToolAuditRepository toolAuditRepository) {
this.metricsRecorder = metricsRecorder;
this.toolAuditRepository = toolAuditRepository;
}
public <T> T observe(String traceId,
String toolName,
ToolSupplier<T> supplier) {
Instant start = Instant.now();
boolean success = false;
String errorMsg = null;
try {
T result = supplier.get();
success = true;
return result;
} catch (Exception ex) {
errorMsg = ex.getMessage();
throw ex;
} finally {
Duration cost = Duration.between(start, Instant.now());
metricsRecorder.recordTool(toolName, success, cost);
toolAuditRepository.save(traceId, toolName, success, errorMsg, cost.toMillis());
}
}
@FunctionalInterface
public interface ToolSupplier<T> {
T get();
}
}
十一、Grafana 面板建议
11.1 总览面板
text
AI 请求 QPS
AI 请求错误率
AI 请求 P95 / P99 延迟
Token 总消耗
平均每次 Token
模型调用成功率
11.2 RAG 面板
text
VectorStore query P95
RAG 平均命中数
RAG 低分召回比例
Top 知识库 Token 消耗
Top 文档命中次数
11.3 Tool 面板
text
工具调用总数
工具失败率
Top 慢工具
Top 失败工具
写操作工具调用次数
工具调用限制触发次数
11.4 成本面板
text
按模型统计 Token
按租户统计 Token
按场景统计 Token
按用户统计 Token(只在数据库里查,不做 Prometheus 标签)
每日预估成本
十二、PromQL 示例
12.1 Agent P95 延迟
promql
histogram_quantile(
0.95,
sum(rate(ai_agent_request_duration_seconds_bucket[5m])) by (le, scene)
)
12.2 Agent 错误率
promql
sum(rate(ai_agent_requests_total{result="fail"}[5m]))
/
sum(rate(ai_agent_requests_total[5m]))
12.3 Token 消耗趋势
promql
sum(rate(ai_agent_tokens_total_sum[5m])) by (scene, model)
12.4 工具失败率
promql
sum(rate(ai_tool_calls_total{result="fail"}[5m])) by (toolName)
/
sum(rate(ai_tool_calls_total[5m])) by (toolName)
十三、生产环境告警规则
建议第一版先做这些:
| 告警项 | 阈值 |
|---|---|
| Agent 错误率 | 5 分钟内 > 5% |
| Agent P95 延迟 | 5 分钟内 > 8s |
| Token 消耗突增 | 当前小时 > 过去 7 天同小时均值 2 倍 |
| 工具失败率 | 5 分钟内 > 10% |
| RAG 无命中率 | 10 分钟内 > 30% |
| 模型超时 | 5 分钟内 > 20 次 |
| MCP Server 不可用 | health check 失败 |
AI 应用最容易被忽略的是成本告警。
建议每天有一条成本日报:
text
昨日请求数:128,420
昨日 Token:1.82B
预估成本:xxx 元
Top 场景:客服问答、订单助手、研发文档
Top 用户/租户:xxx
异常增长:RAG 文档助手 +86%
十四、哪些内容不要进入观测系统
不要把这些作为 Prometheus 标签:
text
用户原始问题
手机号
身份证
订单号
邮箱
合同号
conversationId
traceId
原因:
- 基数太高,会打爆指标系统。
- 涉及隐私和合规。
- 查询性能会变差。
正确做法:
text
指标系统:低基数聚合趋势
日志系统:脱敏后的排查信息
数据库审计表:必要字段,按权限查询
对象存储:极少数问题样本,严格脱敏
十五、总结
这篇文章我们把 AI Agent 从"能跑"推进到了"可运营"。
核心结论:
- Spring AI 已经内置 Micrometer 观测,覆盖 ChatClient、ChatModel、Tool、VectorStore。
- Token 指标可以用
gen_ai.client.token.usage,业务侧还要记录单次调用 Usage。 - Prompt、Completion、工具参数默认不导出,生产环境不要轻易打开。
- RAG 观测重点是命中数、score、向量检索耗时和引用质量。
- Tool 观测重点是失败率、耗时、重复调用和写操作审计。
- Prometheus 看趋势,数据库审计表做追溯,两者缺一不可。
下一篇我准备写:
Spring AI Agent 生产治理实战:限流、熔断、降级、灰度、多模型路由
如果你想要完整工程骨架,可以在评论区留一句:
text
观测
我会继续把这个系列写完整。
可选标题
- Spring AI Agent 线上可观测实战:Token 成本、RAG 命中率、工具耗时一次讲透
- AI 应用别裸奔上线:Spring AI + Micrometer 监控体系实战
- Java AI Agent 第四篇:Prometheus + Grafana 监控 Token、RAG、MCP 工具调用
- Spring Boot 企业 AI 应用:从日志到链路追踪的生产级可观测方案
推荐标签
Spring AI Micrometer Prometheus Grafana AI Agent RAG Spring Boot
参考资料
- Spring AI Observability:https://docs.spring.io/spring-ai/reference/observability/
- Spring AI Usage Handling:https://docs.spring.io/spring-ai/reference/api/usage-handling.html
- Spring AI ChatClient:https://docs.spring.io/spring-ai/reference/api/chatclient.html
- Spring AI Tool Calling:https://docs.spring.io/spring-ai/reference/api/tools.html
- Spring Boot Observability:https://docs.spring.io/spring-boot/reference/actuator/observability.html