Spring AI企业级应用实战(7):可观测性、Token成本、超时重试与熔断降级

文章摘要

前6篇已经完成Spring AI统一调用层、流式输出、Chat Memory、Tool Calling和MCP治理。本篇进入生产稳定性:使用Micrometer与OpenTelemetry观测ChatClient、ChatModel、Advisor、Tool和VectorStore;从ChatResponse累计Usage并计算成本;使用Resilience4j实现超时、限流、重试、熔断和舱壁隔离;根据业务等级在Sol、Terra、Luna或规则服务之间降级。同时解决Tool Calling场景中“重试导致重复执行”的问题,形成可直接复用的企业AI调用网关。

一、本篇目标架构

Controller
→ EnterpriseAiService
→ Budget Guard
→ Model Router
→ Resilience Policy
→ ChatClient
→ Advisor Chain
→ ChatModel
→ Provider

同时:
ChatClient/Model/Tool/VectorStore
→ Micrometer
→ Prometheus

Trace
→ OpenTelemetry
→ Trace Backend

Usage
→ Cost Calculator
→ Usage Database

二、项目结构

spring-ai-enterprise
├── config
│   ├── AiObservabilityConfig.java
│   ├── ResilienceConfig.java
│   └── ModelClientConfig.java
├── context
│   └── AiRequestContext.java
├── gateway
│   ├── EnterpriseAiGateway.java
│   └── SpringAiEnterpriseGateway.java
├── routing
│   ├── ModelRouter.java
│   ├── ModelTier.java
│   └── RoutingDecision.java
├── budget
│   ├── BudgetService.java
│   └── BudgetDecision.java
├── cost
│   ├── UsageSnapshot.java
│   ├── AiCostCalculator.java
│   └── UsageRecorder.java
├── resilience
│   ├── AiErrorClassifier.java
│   ├── AiFallbackService.java
│   └── ToolIdempotencyService.java
└── web
    └── AiController.java

三、依赖

    org.springframework.ai
    spring-ai-starter-model-openai



    org.springframework.boot
    spring-boot-starter-actuator



    io.micrometer
    micrometer-registry-prometheus



    org.springframework.cloud
    spring-cloud-starter-circuitbreaker-reactor-resilience4j



    io.github.resilience4j
    resilience4j-micrometer

具体版本应通过Spring Boot、Spring Cloud和Spring AI BOM统一管理。

四、Actuator配置

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus

  tracing:
    sampling:
      probability: 0.1

  metrics:
    tags:
      application: spring-ai-enterprise

生产环境不要默认开放所有Actuator端点到公网。

五、Spring AI观测层次

Spring AI 2.0提供:

spring.ai.chat.client
spring.ai.advisor
gen_ai.client.operation
spring.ai.tool
db.vector.client.operation

它们分别覆盖:

观测 作用
ChatClient 端到端调用
Advisor Memory、RAG、日志等链路
ChatModel Provider模型调用
Tool 工具执行
VectorStore add、delete、query

六、自定义ChatClient必须保留自动配置

@Configuration
public class ModelClientConfig {

    @Bean("powerfulChatClient")
    ChatClient powerfulChatClient(
            @Qualifier("powerfulChatModel")
            ChatModel chatModel,
            ChatClientBuilderConfigurer configurer
    ) {
        ChatClient.Builder builder =
                ChatClient.builder(chatModel);

        configurer.configure(builder);

        return builder
                .defaultSystem(
                        "你是企业级AI助手"
                )
                .build();
    }
}

如果直接构建客户端而不应用Configurer,可能丢失观测和Customizer。

七、请求上下文

public record AiRequestContext(
        String requestId,
        String tenantId,
        String userId,
        String conversationId,
        String businessScene,
        String promptVersion,
        RiskLevel riskLevel
) {
}

该上下文用于:

  • 路由;
  • 预算;
  • Trace;
  • 审计;
  • Tool权限;
  • 成本归属。

不要把高基数字段全部写成Meter标签。

八、模型层级

public enum ModelTier {
    POWERFUL,
    BALANCED,
    FAST,
    RULE_BASED,
    BLOCKED
}

映射示例:

POWERFUL → GPT-5.6 Sol
BALANCED → GPT-5.6 Terra
FAST → GPT-5.6 Luna
RULE_BASED → 模板或搜索

九、路由决策

@Component
public class ModelRouter {

    public ModelTier route(
            AiRequestContext context,
            BudgetDecision budget,
            int complexity
    ) {
        if (budget == BudgetDecision.BLOCK) {
            return ModelTier.BLOCKED;
        }

        if (context.riskLevel() == RiskLevel.HIGH) {
            return ModelTier.POWERFUL;
        }

        if (budget == BudgetDecision.WARN) {
            return ModelTier.FAST;
        }

        if (complexity >= 8) {
            return ModelTier.POWERFUL;
        }

        if (complexity >= 4) {
            return ModelTier.BALANCED;
        }

        return ModelTier.FAST;
    }
}

高风险任务使用强模型不代表可以自动执行高风险动作,仍要审批。

十、统一Gateway接口

public interface EnterpriseAiGateway {

    AiResult call(
            AiTask task,
            AiRequestContext context
    );

    Flux stream(
            AiTask task,
            AiRequestContext context
    );
}

结果:

public record AiResult(
        String requestId,
        String content,
        String provider,
        String model,
        UsageSnapshot usage,
        BigDecimal estimatedCost,
        long durationMs,
        boolean fallback
) {
}

十一、Usage快照

public record UsageSnapshot(
        long inputTokens,
        long outputTokens,
        long totalTokens,
        long cacheWriteTokens,
        long cacheReadTokens,
        int modelCallCount,
        int toolCallCount
) {
}

Tool Calling可能包含多轮模型调用,最终Usage应按框架返回的累计值处理,并记录轮次。

十二、成本计算

@Component
public class AiCostCalculator {

    public BigDecimal calculate(
            UsageSnapshot usage,
            ModelPrice price
    ) {
        return perMillion(
                usage.inputTokens(),
                price.inputPrice()
        ).add(perMillion(
                usage.outputTokens(),
                price.outputPrice()
        )).add(perMillion(
                usage.cacheWriteTokens(),
                price.cacheWritePrice()
        )).add(perMillion(
                usage.cacheReadTokens(),
                price.cacheReadPrice()
        ));
    }

    private BigDecimal perMillion(
            long tokens,
            BigDecimal price
    ) {
        if (price == null) {
            return BigDecimal.ZERO;
        }

        return BigDecimal.valueOf(tokens)
                .multiply(price)
                .divide(
                        BigDecimal.valueOf(1_000_000),
                        10,
                        RoundingMode.HALF_UP
                );
    }
}

价格从版本化配置表读取。

十三、错误分类

public enum AiErrorType {
    RATE_LIMIT,
    INSUFFICIENT_QUOTA,
    AUTHENTICATION,
    INVALID_REQUEST,
    TIMEOUT,
    PROVIDER_5XX,
    CONTENT_BLOCKED,
    INVALID_OUTPUT,
    UNKNOWN
}

分类决定是否重试:

public boolean retryable(AiErrorType type) {
    return switch (type) {
        case RATE_LIMIT,
             TIMEOUT,
             PROVIDER_5XX -> true;
        default -> false;
    };
}

但Tool副作用场景还要额外判断当前检查点。

十四、Resilience4j配置

resilience4j:
  retry:
    instances:
      powerfulModel:
        max-attempts: 3
        wait-duration: 1s
        enable-exponential-backoff: true
        exponential-backoff-multiplier: 2

  circuitbreaker:
    instances:
      powerfulModel:
        sliding-window-type: count_based
        sliding-window-size: 50
        minimum-number-of-calls: 20
        failure-rate-threshold: 50
        slow-call-duration-threshold: 15s
        slow-call-rate-threshold: 70
        wait-duration-in-open-state: 30s
        permitted-number-of-calls-in-half-open-state: 5

  timelimiter:
    instances:
      powerfulModel:
        timeout-duration: 60s
        cancel-running-future: true

  bulkhead:
    instances:
      powerfulModel:
        max-concurrent-calls: 50
        max-wait-duration: 0

参数必须通过压测调整,不能直接照搬。

十五、为什么要舱壁隔离

如果旗舰模型响应变慢,所有请求都占用线程和连接,会拖垮整个应用。

按模型隔离:

Sol并发池
Terra并发池
Luna并发池

还可以按场景隔离:

客服
批量报告
后台评测

后台任务不能抢占在线客服全部资源。

十六、Gateway实现骨架

@Service
public class SpringAiEnterpriseGateway
        implements EnterpriseAiGateway {

    private final ModelRouter router;
    private final BudgetService budgetService;
    private final ChatClientRegistry clients;
    private final UsageRecorder usageRecorder;
    private final AiFallbackService fallbackService;

    @Override
    public AiResult call(
            AiTask task,
            AiRequestContext context
    ) {
        long start = System.nanoTime();

        BudgetDecision budget =
                budgetService.check(context);

        ModelTier tier = router.route(
                context,
                budget,
                task.complexity()
        );

        if (tier == ModelTier.BLOCKED) {
            return fallbackService.blocked(
                    task,
                    context
            );
        }

        try {
            ChatClient client = clients.get(tier);

            ChatResponse response = client.prompt()
                    .advisors(spec -> spec
                            .param(
                                    "requestId",
                                    context.requestId()
                            )
                            .param(
                                    "tenantId",
                                    context.tenantId()
                            )
                    )
                    .user(task.prompt())
                    .call()
                    .chatResponse();

            UsageSnapshot usage =
                    UsageMapper.from(response);

            AiResult result = buildResult(
                    response,
                    usage,
                    start,
                    false
            );

            usageRecorder.record(
                    context,
                    task,
                    result
            );

            return result;
        }
        catch (RuntimeException ex) {
            return fallbackService.handle(
                    task,
                    context,
                    tier,
                    ex
            );
        }
    }
}

实际重试和熔断通过装饰器、注解或CircuitBreakerFactory接入。

十七、降级服务

@Service
public class AiFallbackService {

    public AiResult handle(
            AiTask task,
            AiRequestContext context,
            ModelTier failedTier,
            RuntimeException exception
    ) {
        AiErrorType error = classify(exception);

        if (error == AiErrorType.INSUFFICIENT_QUOTA) {
            return useLowerCostProvider(
                    task,
                    context
            );
        }

        if (task.allowRuleFallback()) {
            return ruleBasedAnswer(
                    task,
                    context
            );
        }

        return transferToHuman(
                task,
                context,
                error
        );
    }
}

降级优先级取决于业务,不应统一“失败就换模型”。

十八、Tool Calling重试隔离

错误:

整个Agent方法加Retry

正确:

模型选择工具
→ 可重试

工具写操作
→ 幂等执行

最终模型回答
→ 可重试并复用工具结果

检查点:

PLANNED
TOOL_RUNNING
TOOL_SUCCEEDED
ANSWER_RUNNING
COMPLETED

如果最终回答超时,从TOOL_SUCCEEDED恢复。

十九、Tool幂等上下文

public record ToolExecutionContext(
        String executionId,
        String idempotencyKey,
        String tenantId,
        String userId,
        String approvalId,
        int attempt
) {
}

模型不负责生成最终幂等键。

二十、自定义业务指标

@Component
public class EnterpriseAiMetrics {

    private final MeterRegistry registry;

    public void fallback(
            String scene,
            String reason
    ) {
        registry.counter(
                "enterprise.ai.fallback",
                "scene", scene,
                "reason", reason
        ).increment();
    }

    public void budgetBlocked(String scene) {
        registry.counter(
                "enterprise.ai.budget.blocked",
                "scene", scene
        ).increment();
    }
}

低基数标签列表由平台统一治理。

二十一、Trace中记录什么

可以记录:

requestId
conversationId
promptVersion
modelTier
fallback

谨慎记录:

完整Prompt
Completion
Tool参数
RAG文档内容

默认只保存Hash、长度和文档ID。

二十二、告警规则

模型错误率

5分钟错误率 > 5%

P95延迟

连续10分钟 > 15秒

Token突增

单请求Token超过基线3倍

模型轮次

平均模型调用次数突然翻倍

预算

月预算达到80%、90%、95%

熔断

任何生产模型Circuit Breaker打开

二十三、Dashboard

请求数|成功率|P95|当前并发
输入Token|输出Token|缓存命中|费用
429|超时|5xx|熔断
模型分布|降级分布|场景成本|租户成本
Tool调用|Tool失败|重复幂等命中|人工接管

二十四、测试策略

单元测试

  • 错误分类;
  • 价格计算;
  • 路由;
  • 预算;
  • 降级;
  • 幂等。

集成测试

  • 模型429;
  • 模型超时;
  • Usage为空;
  • Provider 5xx;
  • 熔断打开;
  • Tool成功后回答超时。

压测

  • 并发隔离;
  • 连接池;
  • 流式连接;
  • Prometheus序列;
  • 成本写入吞吐。

二十五、生产检查清单

□ ChatClient保留自动观测配置
□ Prometheus与Trace已接通
□ Provider Usage已验证
□ 模型价格版本化
□ 预算按组织、项目和租户设置
□ 429正确分类
□ 重试只覆盖可重试边界
□ 写工具支持幂等
□ Circuit Breaker按依赖隔离
□ 后台任务与在线流量舱壁隔离
□ 降级不绕过权限和审批
□ 高基数数据不进入Metric标签
□ 完整AI成本写入明细表

总结

企业AI生产治理需要把:

Micrometer指标
+OpenTelemetry轨迹
+Usage成本
+预算
+重试
+熔断
+舱壁
+降级
+工具幂等

组合成统一调用网关。

模型能力决定上限,而可观测性和韧性设计决定系统能否长期稳定运行。

下一篇将继续实现:

Spring AI企业级应用实战(8):结构化输出、Schema校验、自修复与业务结果可信化。

延伸阅读

如果你正在关注企业级 AI 应用、Spring AI、RAG、Agent 与 MCP 工程化落地,欢迎访问 智元界

https://www.zyentor.com/

智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。