第40章 Spring AI 实战:企业级 AI 应用架构

第40章 Spring AI 实战:企业级 AI 应用架构

大白话:前面39章教你怎么造零件,这章教你怎么组装一辆能上路的车。用Spring AI把架构、缓存、降级、安全、监控、迭代全部串起来,搭一个真正能上线的AI系统。


40.1 为什么是 Spring AI

前面6个阶段的代码示例,Python 占了大多数。但如果你是 Java 后端团队------团队会的是 Spring Boot,运维的是 JVM,积累的是 Spring Cloud 生态------那你的 AI 应用开发主战场应该是 Spring AI。

Spring AI 是什么

Spring AI 是 Spring 官方推出的 AI 应用开发框架,2024年正式发布1.0正式版。它的定位很明确:让 Java 开发者用 Spring 的方式开发 AI 应用

yaml 复制代码
# 一行依赖搞定
dependencies:
  - org.springframework.ai:spring-ai-openai-spring-boot-starter:1.0.0
  - org.springframework.ai:spring-ai-pgvector-spring-boot-starter:1.0.0
  - org.springframework.ai:spring-ai-rag:1.0.0

Spring AI vs LangChain(Python)

对比维度 Spring AI LangChain (Python)
语言生态 Java / Spring Boot Python
上手成本 会Spring就会用,零学习成本 需要学Python+LangChain概念
企业级能力 天然集成Spring Security/Cloud/Micrometer 需要额外搭
部署运维 JVM成熟生态,运维团队熟悉 Python部署+虚拟环境管理
社区生态 2024年起步,成长中 2023年爆发,生态最大
适合团队 Java后端团队 Python/全栈团队
RAG支持 内置完整(向量库/Embedding/检索) 内置完整
Agent支持 Function Calling原生支持 更灵活(LangGraph等)
生产稳定性 Spring生态背书,成熟稳定 需要自己搭基础设施

大白话:如果你团队是Java栈,Spring AI是当前最顺手的AI开发框架。它不是最灵活的,但是最"省心"的------Spring帮你把企业级该操心的安全、监控、配置、部署全部搞定了,你只需要关注AI业务逻辑。

Spring AI 核心能力一览

复制代码
Spring AI 核心模块
├── Chat Model        → 统一的对话模型抽象(OpenAI/Anthropic/Qwen/Ollama...)
├── Embedding Model    → 向量化模型抽象(BGE/OpenAI/智谱...)
├── Vector Store       → 向量数据库抽象(PgVector/Milvus/Chroma/Redis...)
├── RAG                → 检索增强生成完整支持
├── Function Calling   → 工具调用(原生Spring方法→AI工具)
├── Memory             → 对话记忆(短期/长期)
├── Structured Output  → 结构化输出(JSON→Java对象)
├── Evaluation         → 模型评估
├── Observation        → 可观测性(Micrometer集成)
└── Advisors           → 拦截器模式(类似Spring AOP,可插拔中间件)

大白话 :Spring AI 把AI应用需要的所有零件都做成了Spring Bean------你注入一个ChatClient就像注入一个RestTemplate一样自然。配合Advisors拦截器,你可以像写Spring拦截器一样给AI加缓存、限流、安全检查、日志。


40.2 项目搭建:从零开始

40.2.1 项目结构

复制代码
ai-enterprise-app/
├── pom.xml
├── src/main/java/com/example/ai/
│   ├── AiEnterpriseApplication.java          # 启动类
│   ├── config/
│   │   ├── AiConfig.java                     # AI核心配置
│   │   ├── VectorStoreConfig.java            # 向量库配置
│   │   ├── CacheConfig.java                  # 缓存配置
│   │   ├── SecurityConfig.java              # 安全配置
│   │   └── ObservabilityConfig.java          # 监控配置
│   ├── controller/
│   │   ├── ChatController.java               # 对话接口
│   │   ├── RAGController.java                # 知识库问答接口
│   │   ├── AgentController.java              # Agent接口
│   │   └── DocumentController.java           # 文档管理接口
│   ├── service/
│   │   ├── ChatService.java                  # 对话服务
│   │   ├── RagService.java                   # RAG服务
│   │   ├── AgentService.java                 # Agent服务
│   │   ├── CostControlService.java           # 成本控制
│   │   ├── SecurityService.java              # 安全防护
│   │   └── EvaluationService.java            # 评估服务
│   ├── advisor/
│   │   ├── CacheAdvisor.java                 # 缓存拦截器
│   │   ├── CostAdvisor.java                  # 成本监控拦截器
│   │   ├── SecurityAdvisor.java              # 安全拦截器
│   │   ├── ObservationAdvisor.java           # 可观测性拦截器
│   │   └── FallbackAdvisor.java             # 降级拦截器
│   ├── model/
│   │   ├── entity/                           # 实体
│   │   ├── dto/                              # DTO
│   │   └── enums/                            # 枚举
│   ├── tool/                                 # Agent工具
│   │   ├── OrderTool.java
│   │   ├── WeatherTool.java
│   │   └── SearchTool.java
│   └── util/
│       ├── TokenCounter.java                 # Token计数
│       └── ResponseCache.java                # 响应缓存
├── src/main/resources/
│   ├── application.yml
│   ├── prompts/                              # Prompt模板
│   │   ├── chat.st
│   │   ├── rag.st
│   │   └── summary.st
│   └── db/migration/                          # 数据库迁移
└── src/test/java/                            # 测试

40.2.2 Maven 依赖

xml 复制代码
<dependencies>
    <!-- Spring Boot 基础 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <!-- Spring AI 核心 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    </dependency>

    <!-- Spring AI 向量库 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-pgvector-spring-boot-starter</artifactId>
    </dependency>

    <!-- Spring AI RAG -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-rag</artifactId>
    </dependency>

    <!-- Spring AI 可观测性 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-observation</artifactId>
    </dependency>

    <!-- Micrometer 监控 -->
    <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.postgresql</groupId>
        <artifactId>postgresql</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
    </dependency>
    <dependency>
        <groupId>org.flywaydb</groupId>
        <artifactId>flyway-core</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

40.2.3 核心配置

yaml 复制代码
spring:
  application:
    name: ai-enterprise-app

  # ============ AI 模型配置 ============
  ai:
    openai:
      # 主模型 - 路由策略支持多模型
      api-key: ${OPENAI_API_KEY:sk-xxx}
      base-url: ${OPENAI_BASE_URL:https://api.openai.com}
      chat:
        options:
          model: gpt-4o-mini        # 默认用小模型省钱
          temperature: 0.7
          max-tokens: 2000
        # 备用模型配置
        fallback:
          model: gpt-4o
          temperature: 0.7

      # Embedding配置
      embedding:
        options:
          model: text-embedding-3-small

      # 向量库配置
      vectorstore:
        pgvector:
          index-type: HNSW
          distance-type: COSINE_DISTANCE
          dimensions: 1536

  # ============ 数据库配置 ============
  datasource:
    url: jdbc:postgresql://localhost:5432/ai_app
    username: ${DB_USER:postgres}
    password: ${DB_PASS:postgres}
  jpa:
    hibernate:
      ddl-auto: validate
    properties:
      hibernate:
        jdbc:
          batch_size: 100        # 批量插入向量
  flyway:
    enabled: true
    locations: classpath:db/migration

  # ============ Redis 缓存 ============
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
      timeout: 3s
      lettuce:
        pool:
          max-active: 50
          max-idle: 20

  # ============ Caffeine 本地缓存 ============
  cache:
    type: redis
    caffeine:
      spec: maximumSize=10000,expireAfterWrite=300s

  # ============ 监控 ============
  management:
    endpoints:
      web:
        exposure:
          include: health,info,metrics,prometheus,env
    metrics:
      distribution:
        percentiles-histogram:
          http.server.requests: true
          ai.chat.duration: true
      tracing:
        sampling:
          probability: 1.0       # 生产环境设0.1

# ============ 自定义AI配置 ============
ai:
  # 模型分级路由
  routing:
    enabled: true
    rules:
      - name: simple-query
        condition: "message.length < 50 && !message.contains('分析')"
        model: gpt-4o-mini
      - name: complex-query
        condition: "message.contains('分析') || message.contains('总结') || message.contains('报告')"
        model: gpt-4o
      - name: code-query
        condition: "message.contains('代码') || message.contains('编程')"
        model: gpt-4o

  # 缓存配置
  cache:
    enabled: true
    exact:
      ttl: 3600                  # 精确缓存1小时
    semantic:
      enabled: true
      ttl: 86400                 # 语义缓存24小时
      threshold: 0.95           # 相似度阈值

  # 成本控制
  cost:
    daily-budget-tokens: 1000000
    alert-threshold: 0.8
    fallback-threshold: 0.95

  # 安全配置
  security:
    prompt-injection:
      enabled: true
      block-patterns:
        - "ignore.*previous.*instruction"
        - "system.*prompt"
        - "reveal.*instructions"
    content-moderation:
      enabled: true
    max-conversation-length: 20

  # RAG配置
  rag:
    chunk-size: 400
    chunk-overlap: 60
    top-k: 5
    rerank:
      enabled: true
      model: bge-reranker-v2-m3

40.3 核心配置类

40.3.1 AI 主配置

java 复制代码
@Configuration
public class AiConfig {

    /**
     * 主对话客户端 - 默认走小模型
     */
    @Bean
    @Primary
    public ChatClient chatClient(ChatClient.Builder builder) {
        return builder
            .defaultSystem("你是一个专业的AI助手。请基于提供的上下文准确回答问题。"
                + "如果上下文中没有相关信息,请明确说'我没有找到相关信息',不要编造答案。"
                + "回答时请标注信息来源。")
            .defaultAdvisors(
                new SecurityAdvisor(),      // 安全检查(最先执行)
                new CacheAdvisor(),          // 缓存
                new CostAdvisor(),           // 成本监控
                new ObservationAdvisor()     // 链路追踪
            )
            .build();
    }

    /**
     * 高级模型客户端 - 复杂问题路由
     */
    @Bean("advancedChatClient")
    public ChatClient advancedChatClient(OpenAiChatModel chatModel) {
        return ChatClient.builder(chatModel)
            .defaultSystem("你是一个资深的分析专家。请进行深度分析,给出有洞察力的回答。")
            .defaultAdvisors(
                new SecurityAdvisor(),
                new ObservationAdvisor()
            )
            .build();
    }

    /**
     * Embedding模型
     */
    @Bean
    public EmbeddingModel embeddingModel(OpenAiEmbeddingModel model) {
        return model;
    }

    /**
     * 对话记忆 - 使用Redis持久化
     */
    @Bean
    public ChatMemory chatMemory(RedisChatMemoryRepository repository) {
        return MessageWindowChatMemory.builder()
            .maxMessages(20)                // 滑动窗口
            .chatMemoryRepository(repository)
            .build();
    }

    /**
     * 模型路由器 - 按问题复杂度路由到不同模型
     */
    @Bean
    public ModelRouter modelRouter(ChatClient defaultClient,
                                    @Qualifier("advancedChatClient") ChatClient advancedClient) {
        return new ModelRouter(defaultClient, advancedClient);
    }
}

40.3.2 模型路由器

java 复制代码
/**
 * 模型分级路由 - 简单问题走小模型,复杂问题走大模型
 * 参见第35章成本控制
 */
public class ModelRouter {

    private final ChatClient defaultClient;      // gpt-4o-mini
    private final ChatClient advancedClient;      // gpt-4o
    private final CacheManager cacheManager;

    // 路由规则
    private static final List<RoutingRule> RULES = List.of(
        // 复杂问题 → 大模型
        new RoutingRule("分析|总结|报告|对比|设计|方案|架构", "advanced", 0.8),
        // 代码相关 → 大模型
        new RoutingRule("代码|编程|实现|debug|bug|重构", "advanced", 0.9),
        // 简单问答 → 小模型
        new RoutingRule("你好|谢谢|再见|几点|天气", "default", 0.95)
    );

    /**
     * 路由决策
     */
    public String route(String userMessage) {
        // 1. 检查缓存 - 路由决策也缓存
        String cacheKey = "route:" + DigestUtils.md5Hex(userMessage);
        String cached = cacheManager.getCache("routeCache").get(cacheKey, String.class);
        if (cached != null) {
            return cached;
        }

        // 2. 规则匹配
        for (RoutingRule rule : RULES) {
            if (rule.matches(userMessage)) {
                cacheManager.getCache("routeCache").put(cacheKey, rule.model);
                return rule.model;
            }
        }

        // 3. 默认走小模型(省钱)
        return "default";
    }

    /**
     * 获取对应的ChatClient
     */
    public ChatClient getClient(String route) {
        return "advanced".equals(route) ? advancedClient : defaultClient;
    }

    /**
     * 路由 + 调用一体化
     */
    public String call(String userMessage, String conversationId) {
        String route = route(userMessage);
        ChatClient client = getClient(route);

        log.info("路由决策: message='{}...', route={}, model={}",
            userMessage.substring(0, Math.min(20, userMessage.length())), route,
            route.equals("advanced") ? "gpt-4o" : "gpt-4o-mini");

        return client.prompt()
            .user(userMessage)
            .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
            .call()
            .content();
    }

    record RoutingRule(String pattern, String model, double confidence) {
        boolean matches(String message) {
            return Pattern.compile(pattern, Pattern.CASE_INSENSITIVE)
                .matcher(message).find();
        }
    }
}

大白话:模型路由器就像医院的分诊台------感冒发烧去普通门诊(小模型),疑难杂症去专家门诊(大模型)。分对了,既省钱又高效。


40.4 Advisor 拦截器体系

Spring AI 的 Advisor 机制类似 Spring AOP,可以在AI调用前后插入自定义逻辑。这是实现缓存、安全、成本控制、监控的核心机制。

40.4.1 安全拦截器

java 复制代码
/**
 * 安全Advisor - 第34-37章安全防护的Spring AI实现
 */
public class SecurityAdvisor implements BaseAdvisor {

    // Prompt注入检测模式
    private static final List<Pattern> INJECTION_PATTERNS = List.of(
        Pattern.compile("ignore.*previous.*instruction", Pattern.CASE_INSENSITIVE),
        Pattern.compile("system.*prompt", Pattern.CASE_INSENSITIVE),
        Pattern.compile("reveal.*instructions", Pattern.CASE_INSENSITIVE),
        Pattern.compile("disregard.*above", Pattern.CASE_INSENSITIVE),
        Pattern.compile("you.*are.*now.*a.*", Pattern.CASE_INSENSITIVE)
    );

    // 敏感信息模式(防止泄露PII)
    private static final List<Pattern> PII_PATTERNS = List.of(
        Pattern.compile("\\d{15,18}"),        // 身份证
        Pattern.compile("\\d{16,19}"),         // 银行卡
        Pattern.compile("1[3-9]\\d{9}")       // 手机号
    );

    @Override
    public AdvisedRequest before(AdvisedRequest request) {
        String userMessage = request.userText();

        // 1. Prompt注入检测
        for (Pattern pattern : INJECTION_PATTERNS) {
            if (pattern.matcher(userMessage).find()) {
                log.warn("检测到Prompt注入尝试: {}", userMessage.substring(0, 50));
                throw new SecurityException("检测到潜在的安全风险,请正常提问");
            }
        }

        // 2. PII检测 - 脱敏
        String sanitized = userMessage;
        for (Pattern pattern : PII_PATTERNS) {
            sanitized = pattern.matcher(sanitized).replaceAll("[已脱敏]");
        }
        if (!sanitized.equals(userMessage)) {
            log.info("检测到敏感信息,已脱敏处理");
            request = AdvisedRequest.from(request).userText(sanitized).build();
        }

        // 3. 输入长度限制(防Token炸弹)
        if (userMessage.length() > 10000) {
            throw new SecurityException("输入过长,请控制在10000字以内");
        }

        // 4. 标记隔离 - 用户输入用特殊标记包裹
        String markedMessage = "<user_input>" + sanitized + "</user_input>";
        request = AdvisedRequest.from(request).userText(markedMessage).build();

        return request;
    }

    @Override
    public AdvisedResponse after(AdvisedResponse response) {
        String content = response.response().content();

        // 输出安全检查 - 防止泄露系统信息
        if (content.contains("System Prompt:") || content.contains("系统指令")) {
            log.warn("检测到模型可能泄露系统信息");
            return AdvisedResponse.from(response)
                .response(Response.from("抱歉,我无法提供此类信息。"))
                .build();
        }

        return response;
    }
}

40.4.2 缓存拦截器

java 复制代码
/**
 * 缓存Advisor - 精确缓存 + 语义缓存
 * 参见第35章成本控制
 */
public class CacheAdvisor implements BaseAdvisor {

    private final RedisTemplate<String, String> redisTemplate;
    private final EmbeddingModel embeddingModel;
    private final double semanticThreshold;

    @Override
    public AdvisedRequest before(AdvisedRequest request) {
        // 缓存检查在before阶段做
        String userMessage = request.userText();
        String cacheKey = "ai:cache:" + DigestUtils.md5Hex(normalize(userMessage));

        // 1. 精确缓存
        String cached = redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            log.debug("精确缓存命中");
            // 标记为缓存命中,跳过LLM调用
            return AdvisedRequest.from(request)
                .withCacheHit(cached)
                .build();
        }

        // 2. 语义缓存
        if (semanticThreshold > 0) {
            float[] queryVector = embeddingModel.embed(userMessage);
            String semanticKey = "ai:semantic:" + vectorToKey(queryVector);

            // 用Redis向量搜索找相似问题
            Set<String> similarKeys = redisTemplate.opsForSet()
                .members(semanticKey);
            if (similarKeys != null) {
                for (String key : similarKeys) {
                    String similarity = redisTemplate.opsForValue()
                        .get(key + ":sim");
                    if (similarity != null
                        && Double.parseDouble(similarity) >= semanticThreshold) {
                        String answer = redisTemplate.opsForValue().get(key + ":answer");
                        if (answer != null) {
                            log.debug("语义缓存命中, similarity={}", similarity);
                            return AdvisedRequest.from(request)
                                .withCacheHit(answer)
                                .build();
                        }
                    }
                }
            }
        }

        return request;
    }

    @Override
    public AdvisedResponse after(AdvisedResponse response) {
        // 缓存写入在after阶段做
        String userMessage = response.request().userText();
        String answer = response.response().content();

        // 只缓存高质量回答(非错误、长度适中)
        if (answer.length() > 10 && answer.length() < 5000) {
            String cacheKey = "ai:cache:" + DigestUtils.md5Hex(normalize(userMessage));

            // 精确缓存 - 1小时
            redisTemplate.opsForValue().set(cacheKey, answer, Duration.ofHours(1));

            // 语义缓存 - 24小时
            if (semanticThreshold > 0) {
                float[] vector = embeddingModel.embed(userMessage);
                String semanticKey = "ai:semantic:" + vectorToKey(vector);
                redisTemplate.opsForSet().add(semanticKey, cacheKey);
                redisTemplate.opsForValue().set(cacheKey + ":answer", answer,
                    Duration.ofHours(24));
                redisTemplate.opsForValue().set(cacheKey + ":sim", "1.0",
                    Duration.ofHours(24));
            }
        }

        return response;
    }

    /**
     * 文本归一化 - 去标点、统一大小写、去多余空格
     */
    private String normalize(String text) {
        return text.replaceAll("[\\p{Punct}\\s,。?!、;:""''()【】]+", "")
                   .toLowerCase()
                   .trim();
    }

    private String vectorToKey(float[] vector) {
        // 取前8维做粗略分桶(减少搜索范围)
        return String.format("%.2f_%.2f_%.2f_%.2f",
            vector[0], vector[1], vector[2], vector[3]);
    }
}

40.4.3 成本监控拦截器

java 复制代码
/**
 * 成本Advisor - Token计量 + 预算控制 + 自动降级
 * 参见第35章成本控制
 */
public class CostAdvisor implements BaseAdvisor {

    private final RedisTemplate<String, String> redisTemplate;
    private final double dailyBudgetTokens;
    private final double alertThreshold;
    private final double fallbackThreshold;

    // 模型价格表(每1K Token, 美元)
    private static final Map<String, ModelPricing> PRICING = Map.of(
        "gpt-4o-mini", new ModelPricing(0.00015, 0.0006),
        "gpt-4o", new ModelPricing(0.0025, 0.01),
        "gpt-4-turbo", new ModelPricing(0.01, 0.03)
    );

    @Override
    public AdvisedRequest before(AdvisedRequest request) {
        // 1. 检查今日Token用量
        String today = LocalDate.now().toString();
        String usageKey = "ai:tokens:" + today;
        String usageStr = redisTemplate.opsForValue().get(usageKey);
        double usedTokens = usageStr != null ? Double.parseDouble(usageStr) : 0;
        double ratio = usedTokens / dailyBudgetTokens;

        if (ratio >= fallbackThreshold) {
            // 预算超限 - 降级到最便宜模型
            log.warn("Token预算超限({}%), 强制降级", ratio * 100);
            request = AdvisedRequest.from(request)
                .withModel("gpt-4o-mini")
                .build();
            // 发告警
            sendAlert("TOKEN_BUDGET_EXCEEDED", ratio);
        } else if (ratio >= alertThreshold) {
            // 预算告警
            log.warn("Token预算告警({}%)", ratio * 100);
            sendAlert("TOKEN_BUDGET_WARNING", ratio);
        }

        return request;
    }

    @Override
    public AdvisedResponse after(AdvisedResponse response) {
        // 记录Token消耗
        Usage usage = response.response().usage();
        if (usage != null) {
            int inputTokens = usage.promptTokens();
            int outputTokens = usage.completionTokens();
            int totalTokens = usage.totalTokens();

            // 按天累计
            String today = LocalDate.now().toString();
            String usageKey = "ai:tokens:" + today;
            redisTemplate.opsForValue().increment(usageKey, totalTokens);

            // 按模型累计
            String model = response.request().model();
            String modelKey = "ai:tokens:" + today + ":" + model;
            redisTemplate.opsForValue().increment(modelKey, totalTokens);

            // 按用户累计(分账)
            String userId = response.request().context().get("userId");
            if (userId != null) {
                String userKey = "ai:tokens:" + today + ":user:" + userId;
                redisTemplate.opsForValue().increment(userKey, totalTokens);
            }

            // 计算成本
            ModelPricing pricing = PRICING.getOrDefault(model, PRICING.get("gpt-4o-mini"));
            double cost = (inputTokens / 1000.0 * pricing.inputPrice())
                       + (outputTokens / 1000.0 * pricing.outputPrice());

            // 记录到监控
            Metrics.counter("ai.tokens.total", "model", model).increment(totalTokens);
            Metrics.counter("ai.cost.usd", "model", model).increment(cost);
            Metrics.gauge("ai.budget.ratio", ratio);

            log.info("Token消耗: input={}, output={}, model={}, cost=${}",
                inputTokens, outputTokens, model, String.format("%.4f", cost));
        }

        return response;
    }

    private void sendAlert(String type, double ratio) {
        // 实际场景:调用钉钉/企业微信/邮件
        log.error("ALERT: {} - 预算使用率={}%", type, String.format("%.1f", ratio * 100));
    }

    record ModelPricing(double inputPrice, double outputPrice) {}
}

40.4.4 可观测性拦截器

java 复制代码
/**
 * 可观测性Advisor - 链路追踪 + 指标记录
 * 参见第38章可观测性
 */
public class ObservationAdvisor implements BaseAdvisor {

    private final MeterRegistry meterRegistry;
    private final Tracer tracer;

    @Override
    public AdvisedRequest before(AdvisedRequest request) {
        // 开始Span
        Span span = tracer.nextSpan().name("ai.chat.request").start();
        span.tag("ai.model", request.model());
        span.tag("ai.user.id", request.context().getOrDefault("userId", "anonymous"));
        span.tag("ai.conversation.id",
            request.context().getOrDefault("conversationId", "unknown"));
        span.tag("ai.input.length", String.valueOf(request.userText().length()));

        // 把Span存到上下文
        request = AdvisedRequest.from(request)
            .withContext(Map.of("trace.span", span))
            .build();

        // 计时开始
        long startTime = System.currentTimeMillis();
        request.context().put("startTime", startTime);

        return request;
    }

    @Override
    public AdvisedResponse after(AdvisedResponse response) {
        long startTime = (long) response.request().context().get("startTime");
        long duration = System.currentTimeMillis() - startTime;

        // 结束Span
        Span span = (Span) response.request().context().get("trace.span");
        if (span != null) {
            Usage usage = response.response().usage();
            if (usage != null) {
                span.tag("ai.tokens.input", String.valueOf(usage.promptTokens()));
                span.tag("ai.tokens.output", String.valueOf(usage.completionTokens()));
            }
            span.tag("ai.duration.ms", String.valueOf(duration));
            span.tag("ai.cache.hit",
                String.valueOf(response.request().context().getOrDefault("cacheHit", false)));
            span.end();
        }

        // 记录指标
        meterRegistry.timer("ai.chat.duration",
            "model", response.request().model(),
            "status", "success"
        ).record(Duration.ofMillis(duration));

        if (response.request().context().getOrDefault("cacheHit", false)) {
            meterRegistry.counter("ai.cache.hits").increment();
        } else {
            meterRegistry.counter("ai.cache.misses").increment();
        }

        return response;
    }
}

大白话:四个Advisor就像流水线上的四道质检工序------安全先检查(有没有危险品)、缓存先看看(有没有现货)、成本先算算(超预算没)、全程记录(出问题能追溯)。每一道都是独立可插拔的,不需要了直接去掉就行。


40.5 对话服务

40.5.1 ChatService

java 复制代码
@Service
public class ChatService {

    private final ModelRouter modelRouter;
    private final ChatMemory chatMemory;
    private final RedisTemplate<String, String> redisTemplate;

    /**
     * 普通对话 - 带模型路由 + 记忆
     */
    public ChatResponse chat(String userId, String conversationId, String message) {
        // 1. 记录对话历史
        chatMemory.add(conversationId, new UserMessage(message));

        // 2. 模型路由
        String route = modelRouter.route(message);
        ChatClient client = modelRouter.getClient(route);

        // 3. 调用(Advisor链自动执行安全/缓存/成本/监控)
        String response = client.prompt()
            .messages(chatMemory.get(conversationId))
            .user(message)
            .advisors(a -> a
                .param(ChatMemory.CONVERSATION_ID, conversationId)
                .param("userId", userId)
            )
            .call()
            .content();

        // 4. 记录AI回复
        chatMemory.add(conversationId, new AssistantMessage(response));

        // 5. 记录到持久化存储(用于审计和后续分析)
        saveChatHistory(userId, conversationId, message, response, route);

        return ChatResponse.builder()
            .content(response)
            .model(route.equals("advanced") ? "gpt-4o" : "gpt-4o-mini")
            .conversationId(conversationId)
            .timestamp(LocalDateTime.now())
            .build();
    }

    /**
     * 流式对话 - SSE
     */
    public Flux<String> chatStream(String userId, String conversationId, String message) {
        chatMemory.add(conversationId, new UserMessage(message));

        String route = modelRouter.route(message);
        ChatClient client = modelRouter.getClient(route);

        return client.prompt()
            .messages(chatMemory.get(conversationId))
            .user(message)
            .advisors(a -> a
                .param(ChatMemory.CONVERSATION_ID, conversationId)
                .param("userId", userId)
            )
            .stream()
            .content()
            .doOnComplete(() -> {
                // 流式完成后记录完整回复
                chatMemory.add(conversationId,
                    new AssistantMessage("[streamed response]"));
                log.info("流式对话完成: user={}, conv={}", userId, conversationId);
            });
    }

    /**
     * 多轮对话上下文摘要 - 防止上下文过长
     */
    @Scheduled(fixedRate = 300000)  // 每5分钟检查一次
    public void compressConversationContext() {
        // 找出消息超过15条的会话
        Set<String> activeConversations = redisTemplate.opsForSet()
            .members("ai:active:conversations");
        if (activeConversations == null) return;

        for (String convId : activeConversations) {
            List<Message> messages = chatMemory.get(convId);
            if (messages.size() > 15) {
                // 摘要前10条消息
                String summary = summarizeMessages(messages.subList(0, 10));
                // 保留摘要 + 最近5条
                chatMemory.clear(convId);
                chatMemory.add(convId, new SystemMessage("之前的对话摘要: " + summary));
                messages.subList(messages.size() - 5, messages.size())
                    .forEach(msg -> chatMemory.add(convId, msg));
                log.info("压缩对话上下文: conv={}, {}条→{}条", convId,
                    messages.size(), 6);
            }
        }
    }

    private String summarizeMessages(List<Message> messages) {
        String dialog = messages.stream()
            .map(m -> m.getMessageType() == MessageType.USER
                ? "用户: " + m.getContent()
                : "AI: " + m.getContent())
            .collect(Collectors.joining("\n"));

        return modelRouter.getClient("default")
            .prompt()
            .system("请将以下对话摘要为200字以内的总结,保留关键信息和上下文。")
            .user(dialog)
            .call()
            .content();
    }

    private void saveChatHistory(String userId, String convId,
                                  String message, String response, String route) {
        // 持久化到数据库,用于审计和后续优化
        // 实际场景:异步写入,不影响响应延迟
        CompletableFuture.runAsync(() -> {
            // chatHistoryRepository.save(...)
        });
    }
}

40.6 RAG 知识库服务

40.6.1 文档处理与入库

java 复制代码
@Service
public class RagService {

    private final VectorStore vectorStore;     // Spring AI统一向量库抽象
    private final EmbeddingModel embeddingModel;
    private final ChatClient chatClient;

    /**
     * 文档上传 - 切分 + 向量化 + 入库
     */
    @Async
    public CompletableFuture<Integer> ingestDocument(MultipartFile file,
                                                      String category,
                                                      String source) {
        // 1. 加载文档
        List<Document> documents = loadDocument(file);

        // 2. 数据清洗(参见第22章)
        documents = documents.stream()
            .map(this::cleanDocument)
            .filter(d -> !d.content.trim().isEmpty())
            .collect(Collectors.toList());

        // 3. 切分 - 递归切分,chunk_size=400, overlap=60
        List<Document> chunks = splitDocuments(documents, 400, 60);

        // 4. 添加元数据
        chunks = chunks.stream()
            .map(chunk -> Document.from(chunk.content)
                .withMetadata(Map.of(
                    "category", category,
                    "source", source,
                    "fileName", file.getOriginalFilename(),
                    "fileSize", String.valueOf(file.getSize()),
                    "uploadTime", LocalDateTime.now().toString(),
                    "chunkIndex", String.valueOf(chunk.metadata.get("chunkIndex")),
                    "totalChunks", String.valueOf(chunk.metadata.get("totalChunks"))
                ))
            )
            .collect(Collectors.toList());

        // 5. 向量化 + 入库(Spring AI一行搞定)
        vectorStore.add(chunks);

        log.info("文档入库完成: file={}, chunks={}, category={}",
            file.getOriginalFilename(), chunks.size(), category);

        return CompletableFuture.completedFuture(chunks.size());
    }

    /**
     * RAG问答 - 检索 + 生成
     */
    public RagResponse query(String userId, String question, String conversationId) {
        long startTime = System.currentTimeMillis();

        // 1. 查询改写(多轮对话场景)
        String rewrittenQuery = rewriteQuery(question, conversationId);

        // 2. 元数据过滤(按权限/类别)
        Expression expression = new Expression("category == '" + getUserCategory(userId) + "'");

        // 3. 向量检索(Spring AI内置)
        List<Document> retrieved = vectorStore.similaritySearch(
            SearchRequest.query(rewrittenQuery)
                .topK(5)
                .similarityThreshold(0.7)
                .filterExpression(expression)
        );

        // 4. 检查检索结果
        if (retrieved.isEmpty()) {
            return RagResponse.builder()
                .answer("抱歉,在知识库中没有找到与您问题相关的内容。"
                    + "您可以尝试换个问法,或联系人工客服。")
                .hasContext(false)
                .build();
        }

        // 5. 组装上下文(参见第26章生成优化)
        String context = buildContext(retrieved);

        // 6. 生成回答(带引用标注)
        String answer = chatClient.prompt()
            .system(ragSystemPrompt())
            .user(buildRagPrompt(question, context, retrieved))
            .advisors(a -> a.param("userId", userId)
                           .param("conversationId", conversationId))
            .call()
            .content();

        // 7. 幻觉检测(参见第26章)
        boolean hallucinationCheck = checkHallucination(answer, context);

        // 8. 构建引用
        List<RagResponse.Citation> citations = buildCitations(retrieved);

        long duration = System.currentTimeMillis() - startTime;

        return RagResponse.builder()
            .answer(answer)
            .hasContext(true)
            .citations(citations)
            .retrievedCount(retrieved.size())
            .hallucinationWarning(!hallucinationCheck)
            .durationMs(duration)
            .build();
    }

    /**
     * RAG流式问答
     */
    public Flux<String> queryStream(String userId, String question,
                                     String conversationId) {
        String rewritten = rewriteQuery(question, conversationId);

        List<Document> retrieved = vectorStore.similaritySearch(
            SearchRequest.query(rewritten).topK(5).similarityThreshold(0.7));

        if (retrieved.isEmpty()) {
            return Flux.just("抱歉,知识库中没有找到相关内容。");
        }

        String context = buildContext(retrieved);

        return chatClient.prompt()
            .system(ragSystemPrompt())
            .user(buildRagPrompt(question, context, retrieved))
            .stream()
            .content();
    }

    /**
     * RAG系统Prompt
     */
    private String ragSystemPrompt() {
        return """
            你是一个专业的知识库问答助手。请严格基于以下检索到的上下文回答问题。

            规则:
            1. 只使用<context>中的信息回答问题,不要使用外部知识
            2. 如果上下文中没有相关信息,明确说"根据知识库,我没有找到相关信息"
            3. 回答时标注引用来源,格式:[来源:文件名]
            4. 不要编造、猜测或推断上下文中没有的信息
            5. 保持回答准确、简洁、有条理
            """;
    }

    private String buildRagPrompt(String question, String context,
                                   List<Document> documents) {
        StringBuilder sb = new StringBuilder();
        sb.append("<context>\n");
        for (int i = 0; i < documents.size(); i++) {
            sb.append(String.format("[来源%d: %s]\n%s\n\n",
                i + 1,
                documents.get(i).metadata.get("fileName"),
                documents.get(i).content));
        }
        sb.append("</context>\n\n");
        sb.append("<question>").append(question).append("</question>\n");
        sb.append("请基于上述上下文回答问题,标注引用来源。");
        return sb.toString();
    }

    // ... 省略辅助方法(loadDocument/cleanDocument/splitDocuments等)
}

40.6.2 Controller

java 复制代码
@RestController
@RequestMapping("/api/ai")
public class ChatController {

    private final ChatService chatService;
    private final RagService ragService;

    /**
     * 普通对话
     */
    @PostMapping("/chat")
    public ChatResponse chat(@RequestBody ChatRequest request,
                             @RequestHeader("X-User-Id") String userId) {
        return chatService.chat(userId, request.conversationId(), request.message());
    }

    /**
     * 流式对话 - SSE
     */
    @PostMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> chatStream(@RequestBody ChatRequest request,
                                    @RequestHeader("X-User-Id") String userId) {
        return chatService.chatStream(userId, request.conversationId(), request.message());
    }

    /**
     * RAG知识库问答
     */
    @PostMapping("/rag/query")
    public RagResponse ragQuery(@RequestBody RagRequest request,
                                 @RequestHeader("X-User-Id") String userId) {
        return ragService.query(userId, request.question(), request.conversationId());
    }

    /**
     * RAG流式问答
     */
    @PostMapping(value = "/rag/query/stream",
                 produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> ragQueryStream(@RequestBody RagRequest request,
                                         @RequestHeader("X-User-Id") String userId) {
        return ragService.queryStream(userId, request.question(),
            request.conversationId());
    }

    /**
     * 文档上传
     */
    @PostMapping("/documents/upload")
    public CompletableFuture<UploadResponse> uploadDocument(
            @RequestParam("file") MultipartFile file,
            @RequestParam("category") String category,
            @RequestHeader("X-User-Id") String userId) {
        return ragService.ingestDocument(file, category, userId)
            .thenApply(chunks -> new UploadResponse(true,
                "文档入库成功,共" + chunks + "个片段", chunks));
    }

    /**
     * 健康检查
     */
    @GetMapping("/health")
    public Map<String, Object> health() {
        return Map.of(
            "status", "UP",
            "timestamp", LocalDateTime.now(),
            "models", Map.of("default", "gpt-4o-mini", "advanced", "gpt-4o")
        );
    }
}

40.7 Agent 工具集成

40.7.1 工具定义

Spring AI 的 Function Calling 非常简洁------一个 @Bean 就是一个工具:

java 复制代码
@Configuration
public class ToolConfig {

    /**
     * 工具1:查询订单
     */
    @Bean
    @Description("根据订单号查询订单状态、物流信息和商品详情")
    public Function<QueryOrderRequest, QueryOrderResponse> queryOrder() {
        return request -> {
            log.info("Agent调用queryOrder工具: orderId={}", request.orderId());
            // 实际查询逻辑
            Order order = orderRepository.findById(request.orderId())
                .orElseThrow(() -> new RuntimeException("订单不存在"));
            return new QueryOrderResponse(
                order.getId(),
                order.getStatus().toString(),
                order.getTrackingNo(),
                order.getEstimatedDelivery().toString(),
                order.getItems().stream()
                    .map(item -> item.getName() + " x" + item.getQuantity())
                    .collect(Collectors.toList())
            );
        };
    }

    /**
     * 工具2:申请退款
     */
    @Bean
    @Description("为指定订单申请退款。仅在订单状态为'已签收'且签收不超过7天时可用。")
    public Function<RefundRequest, RefundResponse> applyRefund() {
        return request -> {
            // 权限检查 + 业务校验
            Order order = orderRepository.findById(request.orderId())
                .orElseThrow(() -> new RuntimeException("订单不存在"));

            if (order.getStatus() != OrderStatus.DELIVERED) {
                return new RefundResponse(false, "订单状态不支持退款");
            }
            if (order.getDeliveredAt().isBefore(LocalDateTime.now().minusDays(7))) {
                return new RefundResponse(false, "已超过7天退款期限");
            }

            // 创建退款单
            Refund refund = refundService.createRefund(order, request.reason());
            return new RefundResponse(true, "退款申请已创建",
                refund.getId(), refund.getStatus().toString());
        };
    }

    /**
     * 工具3:搜索知识库
     */
    @Bean
    @Description("在企业知识库中搜索相关信息,返回相关文档片段")
    public Function<SearchKnowledgeRequest, SearchKnowledgeResponse> searchKnowledge() {
        return request -> {
            List<Document> results = vectorStore.similaritySearch(
                SearchRequest.query(request.query())
                    .topK(3)
                    .similarityThreshold(0.7)
            );
            return new SearchKnowledgeResponse(
                results.stream()
                    .map(doc -> Map.of(
                        "content", doc.content,
                        "source", doc.metadata.get("fileName")
                    ))
                    .collect(Collectors.toList())
            );
        };
    }

    // 请求/响应记录
    public record QueryOrderRequest(String orderId) {}
    public record QueryOrderResponse(String orderId, String status,
        String trackingNo, String estimatedDelivery, List<String> items) {}

    public record RefundRequest(String orderId, String reason) {}
    public record RefundResponse(boolean success, String message,
        String refundId, String status) {}

    public record SearchKnowledgeRequest(String query) {}
    public record SearchKnowledgeResponse(List<Map<String, Object>> results) {}
}

40.7.2 Agent 服务

java 复制代码
@Service
public class AgentService {

    private final ChatClient chatClient;
    private final ChatMemory chatMemory;

    /**
     * Agent对话 - 自动决策使用工具
     */
    public AgentResponse execute(String userId, String conversationId,
                                  String task) {
        chatMemory.add(conversationId, new UserMessage(task));

        // Spring AI自动处理Function Calling循环
        String response = chatClient.prompt()
            .system("""
                你是一个智能客服Agent。你可以使用提供的工具帮助用户解决问题。
                规则:
                1. 先理解用户需求,决定是否需要调用工具
                2. 调用工具后,基于工具返回的结果回答用户
                3. 如果工具返回失败,向用户解释原因并提供替代方案
                4. 高风险操作(如退款)必须向用户确认后才执行
                5. 不要编造订单信息,只使用工具查询到的真实数据
                """)
            .user(task)
            .functions("queryOrder", "applyRefund", "searchKnowledge")
            .advisors(a -> a
                .param(ChatMemory.CONVERSATION_ID, conversationId)
                .param("userId", userId)
            )
            .call()
            .content();

        chatMemory.add(conversationId, new AssistantMessage(response));

        return AgentResponse.builder()
            .content(response)
            .conversationId(conversationId)
            .build();
    }

    /**
     * Agent流式执行
     */
    public Flux<String> executeStream(String userId, String conversationId,
                                       String task) {
        return chatClient.prompt()
            .system("你是一个智能客服Agent...")
            .user(task)
            .functions("queryOrder", "applyRefund", "searchKnowledge")
            .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
            .stream()
            .content();
    }
}

大白话 :Spring AI 的 Function Calling 简单到什么程度?你写一个普通的 Spring Bean 方法,加个 @Description 注解告诉模型这个工具干什么,Spring AI 自动帮你处理"模型决定调用→生成参数→执行函数→结果返回模型→模型基于结果回答"的完整循环。你不需要写任何调度代码。


40.8 安全配置

40.8.1 Spring Security + AI 安全

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            // 1. 认证配置
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/ai/health").permitAll()
                .requestMatchers("/api/ai/**").authenticated()
                .anyRequest().authenticated()
            )
            // 2. JWT认证
            .oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()))
            // 3. 限流
            .addFilterBefore(new RateLimitFilter(), UsernamePasswordAuthenticationFilter.class)
            // 4. AI安全过滤
            .addFilterAfter(new AiSecurityFilter(), RateLimitFilter.class)
            // 5. CORS
            .cors(Customizer.withDefaults())
            // 6. CSRF(API场景关闭)
            .csrf(csrf -> csrf.disable())
            .build();
    }

    /**
     * 限流过滤器 - 每用户每分钟20次
     */
    static class RateLimitFilter extends OncePerRequestFilter {
        private final RedisTemplate<String, String> redisTemplate;

        @Override
        protected void doFilterInternal(HttpServletRequest request,
                HttpServletResponse response, FilterChain chain)
                throws ServletException, IOException {
            String userId = request.getHeader("X-User-Id");
            String key = "ratelimit:" + userId + ":" + System.currentTimeMillis() / 60000;

            Long count = redisTemplate.opsForValue().increment(key);
            if (count == 1) {
                redisTemplate.expire(key, Duration.ofMinutes(1));
            }

            if (count > 20) {
                response.setStatus(429);
                response.getWriter().write("{\"error\":\"请求过于频繁,请稍后再试\"}");
                return;
            }

            chain.doFilter(request, response);
        }
    }

    /**
     * AI安全过滤器 - Prompt注入检测
     */
    static class AiSecurityFilter extends OncePerRequestFilter {
        private static final List<Pattern> INJECTION_PATTERNS = List.of(
            // 参见第37章完整模式列表
            Pattern.compile("ignore.*previous.*instruction", Pattern.CASE_INSENSITIVE),
            Pattern.compile("system.*prompt", Pattern.CASE_INSENSITIVE)
        );

        @Override
        protected void doFilterInternal(HttpServletRequest request,
                HttpServletResponse response, FilterChain chain)
                throws ServletException, IOException {
            // 只检查POST请求体
            if ("POST".equalsIgnoreCase(request.getMethod())) {
                ContentCachingRequestWrapper wrappedRequest =
                    new ContentCachingRequestWrapper(request);
                chain.doFilter(wrappedRequest, response);

                // 检查请求体内容
                String body = wrappedRequest.getContentAsString();
                for (Pattern pattern : INJECTION_PATTERNS) {
                    if (pattern.matcher(body).find()) {
                        response.setStatus(403);
                        response.getWriter().write(
                            "{\"error\":\"检测到安全风险\"}");
                        log.warn("Prompt注入尝试: body={}", body.substring(0, 100));
                        return;
                    }
                }
            } else {
                chain.doFilter(request, response);
            }
        }
    }
}

40.9 监控与运维

40.9.1 自定义健康指标

java 复制代码
@Component
public class AiHealthIndicator implements HealthIndicator {

    private final ChatClient chatClient;
    private final VectorStore vectorStore;
    private final RedisTemplate<String, String> redisTemplate;

    @Override
    public Health health() {
        Health.Builder builder = Health.up();
        Map<String, Object> details = new HashMap<>();

        // 1. 检查模型可用性
        try {
            String test = chatClient.prompt()
                .user("ping")
                .call()
                .content();
            details.put("chatModel", "UP");
            details.put("chatModelResponse", test != null ? "ok" : "empty");
        } catch (Exception e) {
            builder = Health.down();
            details.put("chatModel", "DOWN");
            details.put("chatModelError", e.getMessage());
        }

        // 2. 检查向量库
        try {
            vectorStore.similaritySearch(SearchRequest.query("test").topK(1));
            details.put("vectorStore", "UP");
        } catch (Exception e) {
            builder = Health.down();
            details.put("vectorStore", "DOWN");
        }

        // 3. 检查Redis
        try {
            redisTemplate.opsForValue().get("health:check");
            details.put("redis", "UP");
        } catch (Exception e) {
            builder = Health.down();
            details.put("redis", "DOWN");
        }

        // 4. 检查今日Token用量
        String today = LocalDate.now().toString();
        String usage = redisTemplate.opsForValue().get("ai:tokens:" + today);
        details.put("todayTokenUsage", usage != null ? usage : "0");

        return builder.withDetails(details).build();
    }
}

40.9.2 自定义指标

java 复制代码
@Component
@RequiredArgsConstructor
public class AiMetrics {

    private final MeterRegistry meterRegistry;

    /**
     * 记录AI请求
     */
    public void recordRequest(String model, boolean cacheHit, boolean success) {
        meterRegistry.counter("ai.requests.total",
            "model", model,
            "cache_hit", String.valueOf(cacheHit),
            "status", success ? "success" : "error"
        ).increment();
    }

    /**
     * 记录Token消耗
     */
    public void recordTokens(String model, int inputTokens, int outputTokens) {
        meterRegistry.counter("ai.tokens.input", "model", model).increment(inputTokens);
        meterRegistry.counter("ai.tokens.output", "model", model).increment(outputTokens);
    }

    /**
     * 记录响应延迟
     */
    public void recordLatency(String model, long durationMs, boolean stream) {
        meterRegistry.timer("ai.response.duration",
            "model", model,
            "stream", String.valueOf(stream)
        ).record(Duration.ofMillis(durationMs));
    }

    /**
     * 记录RAG检索
     */
    public void recordRagRetrieval(int retrievedCount, long retrievalMs) {
        meterRegistry.gauge("ai.rag.retrieved.count", retrievedCount);
        meterRegistry.timer("ai.rag.retrieval.duration")
            .record(Duration.ofMillis(retrievalMs));
    }

    /**
     * 记录幻觉检测结果
     */
    public void recordHallucinationCheck(boolean passed) {
        meterRegistry.counter("ai.hallucination.check",
            "result", passed ? "pass" : "fail"
        ).increment();
    }

    /**
     * 更新预算使用率
     */
    public void updateBudgetRatio(double ratio) {
        meterRegistry.gauge("ai.budget.ratio", ratio);
    }
}

40.9.3 Grafana 监控面板

配置 application.yml 暴露Prometheus指标后,Grafana面板关键指标:

面板 指标 PromQL
QPS 每秒请求数 rate(ai_requests_total[1m])
延迟P99 99分位响应时间 histogram_quantile(0.99, rate(ai_response_duration_bucket[5m]))
Token消耗 每分钟Token rate(ai_tokens_input_total[1m]) + rate(ai_tokens_output_total[1m])
缓存命中率 命中率 rate(ai_requests_total{cache_hit="true"}[5m]) / rate(ai_requests_total[5m])
错误率 错误请求占比 rate(ai_requests_total{status="error"}[5m]) / rate(ai_requests_total[5m])
预算使用 预算消耗率 ai_budget_ratio
RAG检索数 平均检索数 avg(ai_rag_retrieved_count)
幻觉率 幻觉失败率 rate(ai_hallucination_check_total{result="fail"}[1h])

40.10 评估与迭代

40.10.1 离线评估

java 复制代码
@Service
public class EvaluationService {

    private final ChatClient chatClient;
    private final RagService ragService;

    /**
     * 离线评估 - 用测试集评估RAG质量
     */
    public EvaluationReport evaluateRag(List<TestCase> testCases) {
        List<TestResult> results = new ArrayList<>();

        for (TestCase testCase : testCases) {
            long startTime = System.currentTimeMillis();

            // 执行RAG查询
            RagResponse response = ragService.query(
                "eval-user", "eval-session", testCase.question()
            );

            long duration = System.currentTimeMillis() - startTime;

            // LLM-as-Judge 评估
            EvaluationResult evalResult = evaluateWithLLM(
                testCase.question(),
                response.answer(),
                testCase.expectedAnswer(),
                testCase.context()
            );

            results.add(TestResult.builder()
                .question(testCase.question())
                .answer(response.answer())
                .expectedAnswer(testCase.expectedAnswer())
                .accuracy(evalResult.accuracy())
                .faithfulness(evalResult.faithfulness())
                .completeness(evalResult.completeness())
                .relevance(evalResult.relevance())
                .languageQuality(evalResult.languageQuality())
                .overallScore(evalResult.overallScore())
                .durationMs(duration)
                .retrievedCount(response.retrievedCount())
                .hallucinationWarning(response.hallucinationWarning())
                .build());
        }

        // 生成汇总报告
        return generateReport(results);
    }

    /**
     * LLM-as-Judge
     */
    private EvaluationResult evaluateWithLLM(String question, String answer,
                                              String expected, String context) {
        String evalPrompt = """
            请评估以下AI回答的质量,从5个维度评分(1-5分):

            【问题】%s
            【AI回答】%s
            【参考答案】%s
            【检索上下文】%s

            评分维度:
            1. 准确性:回答是否正确,与参考答案是否一致
            2. 忠实度:回答是否基于上下文,有无幻觉
            3. 完整性:回答是否覆盖了问题的所有要点
            4. 相关性:回答是否与问题紧密相关
            5. 语言质量:表达是否清晰、流畅、有条理

            请输出JSON格式:
            {"accuracy":5, "faithfulness":5, "completeness":4,
             "relevance":5, "languageQuality":4, "overallScore":4.6,
             "reason": "评价理由"}
            """.formatted(question, answer, expected, context);

        String result = chatClient.prompt()
            .user(evalPrompt)
            .call()
            .content();

        return parseEvaluationResult(result);
    }

    private EvaluationReport generateReport(List<TestResult> results) {
        double avgAccuracy = results.stream()
            .mapToDouble(TestResult::accuracy).average().orElse(0);
        double avgFaithfulness = results.stream()
            .mapToDouble(TestResult::faithfulness).average().orElse(0);
        double avgCompleteness = results.stream()
            .mapToDouble(TestResult::completeness).average().orElse(0);
        double avgRelevance = results.stream()
            .mapToDouble(TestResult::relevance).average().orElse(0);
        double avgLanguage = results.stream()
            .mapToDouble(TestResult::languageQuality).average().orElse(0);
        double avgDuration = results.stream()
            .mapToDouble(TestResult::durationMs).average().orElse(0);
        long hallucinationCount = results.stream()
            .filter(TestResult::hallucinationWarning).count();

        return EvaluationReport.builder()
            .totalCases(results.size())
            .avgAccuracy(avgAccuracy)
            .avgFaithfulness(avgFaithfulness)
            .avgCompleteness(avgCompleteness)
            .avgRelevance(avgRelevance)
            .avgLanguageQuality(avgLanguage)
            .avgOverallScore((avgAccuracy + avgFaithfulness + avgCompleteness
                + avgRelevance + avgLanguage) / 5)
            .avgDurationMs(avgDuration)
            .hallucinationRate((double) hallucinationCount / results.size())
            .results(results)
            .timestamp(LocalDateTime.now())
            .build();
    }
}

40.10.2 Bad Case 管理

java 复制代码
@Service
public class BadCaseService {

    private final BadCaseRepository repository;
    private final EvaluationService evaluationService;

    /**
     * 用户反馈 - 差评自动加入Bad Case
     */
    @EventListener
    public void handleNegativeFeedback(UserFeedbackEvent event) {
        if (event.rating() <= 2) {  // 1-2星为差评
            BadCase badCase = BadCase.builder()
                .question(event.question())
                .answer(event.answer())
                .expectedAnswer("")       // 待人工标注
                .feedback(event.feedback())
                .rating(event.rating())
                .userId(event.userId())
                .timestamp(LocalDateTime.now())
                .status(BadCaseStatus.PENDING_REVIEW)
                .build();

            repository.save(badCase);
            log.info("差评自动加入Bad Case库: question={}",
                event.question().substring(0, Math.min(50, event.question().length())));
        }
    }

    /**
     * 批量审核Bad Case
     */
    @Scheduled(cron = "0 0 10 * * MON")  // 每周一上午10点
    public void batchReviewBadCases() {
        List<BadCase> pending = repository.findByStatus(BadCaseStatus.PENDING_REVIEW);
        log.info("本周待审核Bad Case: {}条", pending.size());

        for (BadCase badCase : pending) {
            // 自动生成参考答案
            String reference = generateReferenceAnswer(badCase.getQuestion());
            badCase.setExpectedAnswer(reference);
            badCase.setStatus(BadCaseStatus.AUTO_REVIEWED);
            repository.save(badCase);
        }
    }

    /**
     * 从Bad Case生成测试用例
     */
    public List<TestCase> convertToTestCases() {
        return repository.findByStatus(BadCaseStatus.VERIFIED).stream()
            .map(bc -> new TestCase(bc.getQuestion(), bc.getExpectedAnswer(),
                bc.getCategory()))
            .collect(Collectors.toList());
    }
}

40.11 部署架构

40.11.1 Docker Compose

yaml 复制代码
# docker-compose.yml
version: '3.8'
services:
  ai-app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - SPRING_PROFILES_ACTIVE=prod
      - DB_HOST=postgres
      - REDIS_HOST=redis
    depends_on:
      - postgres
      - redis
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  postgres:
    image: pgvector/pgvector:pg16
    environment:
      POSTGRES_DB: ai_app
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    command: redis-server --maxmemory 512mb --maxmemory-policy allkeys-lru
    restart: unless-stopped

volumes:
  pgdata:

40.11.2 Kubernetes 部署

yaml 复制代码
# k8s-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ai-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: ai-app
  template:
    spec:
      containers:
        - name: ai-app
          image: registry.example.com/ai-app:1.0.0
          ports:
            - containerPort: 8080
          env:
            - name: OPENAI_API_KEY
              valueFrom:
                secretKeyRef:
                  name: ai-secrets
                  key: openai-api-key
          resources:
            requests:
              memory: "1Gi"
              cpu: "500m"
            limits:
              memory: "2Gi"
              cpu: "1000m"
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 30
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 60
            periodSeconds: 30
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: ai-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: ai-app
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80

40.12 全书总结

40.12.1 7个阶段40章全景回顾

阶段 章节 核心知识 大白话
第1阶段 认知启蒙 第1-4章 AI简史、AI/ML/DL关系、能力边界、三条路线 先知道AI是啥,再谈怎么用
第2阶段 机器学习基础 第5-9章 监督/无监督学习、特征工程、模型评估、实操 让机器学会看数据找规律
第3阶段 深度学习 第10-14章 神经网络、CNN、RNN、Transformer、框架实操 神经网络到底怎么"学习"的
第4阶段 大模型(LLM) 第15-20章 LLM原理、模型选型、Prompt、微调、量化部署、评估 GPT们到底是怎么工作的
第5阶段 RAG检索增强生成 第21-27章 RAG原理、数据处理、向量化、向量库、检索策略、生成优化、框架 给AI装上外挂知识库
第6阶段 Agent智能体 第28-33章 Agent概念、Function Call、ReAct、工作流、记忆、实战 让AI学会用工具干活
第7阶段 AI工程化落地 第34-40章 架构设计、成本控制、性能优化、安全防护、可观测性、评估迭代、Spring AI实战 从Demo到生产,差的不止一点

40.12.2 核心知识图谱

复制代码
AI开发知识全景
│
├─ 认知层(第1阶段)
│  └─ 理解AI能干什么、不能干什么 → 决定用什么路线
│
├─ 基础层(第2-3阶段)
│  ├─ 机器学习:传统ML在表格数据上仍然不可替代
│  └─ 深度学习:CNN看图、RNN看序列、Transformer看一切
│     └─ Transformer → 所有现代大模型的架构基础
│
├─ 应用层(第4-6阶段)
│  ├─ LLM:Next Token Prediction → 千亿参数 → 智能涌现
│  │  ├─ 使用:Prompt工程(零成本优化,先做这个)
│  │  ├─ 定制:微调(LoRA/QLoRA,改能力不改知识)
│  │  ├─ 部署:量化(INT4)+ 推理引擎(vLLM)
│  │  └─ 评估:自有测试集 > 通用Benchmark
│  │
│  ├─ RAG:检索→增强→生成(给模型开卷考试)
│  │  ├─ 数据处理 → 切分策略决定检索质量上限
│  │  ├─ 向量化 → BGE选型,成本极低
│  │  ├─ 向量库 → PgVector够用,简单优先
│  │  ├─ 检索策略 → 改写+混合(RRF)+重排序三板斧
│  │  └─ 生成优化 → 上下文组装+引用标注+幻觉检测
│  │
│  └─ Agent:自主决策+工具使用
│     ├─ Function Call → 模型发令,系统执行
│     ├─ ReAct → Think-Act-Observe循环
│     ├─ 工作流 → 多Agent协作(LangGraph)
│     └─ 记忆 → 短期(滑动窗口)+长期(向量/结构化)
│
└─ 工程层(第7阶段)
   ├─ 架构设计 → AI网关+模型路由+三级缓存+五级降级
   ├─ 成本控制 → 缓存+路由+Prompt精简 → 降50%+
   ├─ 性能优化 → 流式输出+KV Cache+量化 → 体验关键
   ├─ 安全防护 → 四层防御+Function Call安全+多租户隔离
   ├─ 可观测性 → 指标+链路+日志+质量监控(第四支柱)
   ├─ 评估迭代 → 闭环:监控→分析→优化→评估→上线
   └─ Spring AI → Java团队的企业级AI开发框架

40.12.3 给开发者的建议

如果你是后端开发者想转AI开发:

  1. 第1-3阶段快速过 --- 理解概念,不死磕数学。重点理解Transformer
  2. 第4阶段重点投入 --- LLM是当前AI应用的核心。Prompt工程是基本功
  3. 第5阶段必须掌握 --- RAG是后端开发者最容易上手的AI应用场景
  4. 第6阶段按需学习 --- Agent是趋势但成熟度不够,别过度投入
  5. 第7阶段是你的主场 --- 后端工程能力是AI落地的核心竞争力

技术选型建议:

场景 推荐方案 理由
快速验证AI效果 API + Prompt + RAG 零成本起步,先验证再投入
企业知识库 PgVector + BGE + LangChain/Spring AI 简单够用,运维成熟
智能客服 Spring AI + Function Call + RAG Java团队首选,企业级生态完善
高并发AI服务 vLLM + Redis缓存 + 模型路由 量化+缓存+路由三板斧
复杂Agent系统 LangGraph + 多Agent协作 可控性强,适合生产
个人知识助手 Ollama + 本地模型 隐私+免费+够用

最重要的三句话:

  1. 先API再自部署 --- 90%的场景API就够了,别一开始就折腾模型部署
  2. 先Prompt再微调 --- Prompt没优化好别提微调,微调改能力不改知识
  3. 先简单再复杂 --- 能用单Agent解决别上多Agent,能用PgVector别上Milvus,能用规则解决别上AI
相关推荐
zzm6281 小时前
WSDM 2018论文精读:基于多关系学习与路径约束的商品替代互补关系挖掘
人工智能·学习
aneasystone本尊1 小时前
学习 Headroom 的 CCR 可逆压缩
人工智能
大模型任我行1 小时前
谷歌:扩散模型实现极速文本生成
人工智能·语言模型·自然语言处理·论文笔记
IT_陈寒1 小时前
Vite热更新失效?我的几个犯傻操作害我debug两小时
前端·人工智能·后端
月光船幽幽1 小时前
锁死后干预有效性的关键突破
人工智能·python·算法
深念Y1 小时前
CPA-Auto池方案总结
人工智能·ai·自动化·路由·代理·账号·轮询
FellAveal1 小时前
【Transformer入门】从函数到Transformer
人工智能·深度学习·transformer
阳光开朗男孩1 小时前
Pytorch的安装与配置
人工智能·pytorch·python
ZeekerLin1 小时前
本体论Ontology在企业AI项目落地思考
大数据·人工智能·企业ai落地·本体论