跳转至

SpringAI

SpringAI 深度进阶

image.png

[Advisor 机制、VectorStore 集成、Function Calling、多模型降级]

🧭 1. SpringAI 的 Advisor 机制是什么?如何用 Advisor 实现 RAG?

一句话:Advisor 是 Spring AI 中拦截和增强请求/响应的切面(AOP)机制,可以在调用 LLM 前后插入自定义逻辑。

它借鉴了 Spring AOP 的思想,但专门针对 AI 调用链设计。每个 Advisor 都可以在 Prompt 发送给模型之前做预处理,在模型返回之后做后处理,甚至完全替换请求。

image.png

Advisor 接口定义(简化):

public interface RequestResponseAdvisor {
    // 前置拦截:修改 Prompt
    AdvisedRequest adviseRequest(AdvisedRequest request, Map<String, Object> context);
    // 后置拦截:修改响应
    ChatResponse adviseResponse(ChatResponse response, Map<String, Object> context);
    // 优先级
    int getOrder();
}

如何用 Advisor 实现 RAG?

核心思路是编写一个 RetrievalAugmentationAdvisor,在请求发送前,从向量数据库中检索相关文档,插入到 Prompt 的上下文中。

用户问题 → Advisor 拦截 → 向量检索 → 拼接文档 → 增强后的 Prompt → LLM → 回答

代码示例:实现一个简单的 RAG Advisor

@Component
public class RagAdvisor implements RequestResponseAdvisor {

    private final VectorStore vectorStore;

    public RagAdvisor(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    @Override
    public AdvisedRequest adviseRequest(AdvisedRequest request, Map<String, Object> context) {
        // 1. 获取用户原始输入
        String userQuery = request.userText();
        // 2. 从向量库检索相关文档
        List<Document> docs = vectorStore.similaritySearch(SearchRequest.query(userQuery).withTopK(3));
        // 3. 拼接检索结果
        String contextText = docs.stream()
                .map(Document::getContent)
                .collect(Collectors.joining("\n\n"));
        // 4. 构建增强后的系统提示词
        String augmentedSystem = String.format(
                "根据以下参考资料回答用户问题。如果资料中没有答案,请如实告知。\n\n参考资料:\n%s", contextText
        );
        // 5. 修改请求,注入上下文
        return AdvisedRequest.from(request)
                .withSystemText(augmentedSystem + "\n\n" + request.systemText())
                .build();
    }

    @Override
    public ChatResponse adviseResponse(ChatResponse response, Map<String, Object> context) {
        // 这里可做后处理,如提取引用、记录日志
        return response;
    }

    @Override
    public int getOrder() { return 0; }
}

在 ChatClient 中使用:

@Bean
public ChatClient chatClient(ChatClient.Builder builder, RagAdvisor ragAdvisor) {
    return builder
            .defaultAdvisors(ragAdvisor)
            .build();
}

// 调用时
String answer = chatClient.prompt().user("公司去年的营收是多少?").call().content();

Advisor 链的执行模型:

Spring AI 支持多个 Advisor 串联,形成一个链条。比如你可以同时启用:

  • RagAdvisor:检索增强

  • LoggingAdvisor:记录请求响应日志

  • ContentGuardAdvisor:过滤输出中的敏感词

它们按 getOrder() 的值顺序执行,像一个 Servlet Filter 链,但专门服务于 AI 调用。

Advisor vs LangChain 的 Runnable:

两者目标相似——都是“在 LLM 调用前后插入逻辑”。但实现哲学不同:

  • Advisor 是 Spring 的 AOP 风格,面向切面,无侵入式配置。

  • Runnable 是管道式组合,你需要显式地用 | 连接各个步骤。

在 Spring 生态中,Advisor 更符合 Java 开发者的习惯,且天然集成 Spring 的 Bean 管理和配置体系。


🗄️ 2. SpringAI 如何集成向量数据库?VectorStore 的抽象设计是什么?

Spring AI 对向量数据库的集成遵循 “统一接口 + 适配器实现” 的经典 Spring 风格。

核心抽象:VectorStore 接口

public interface VectorStore {
    void add(List<Document> documents);
    Optional<Boolean> delete(List<String> idList);
    List<Document> similaritySearch(SearchRequest request);
    default List<Document> similaritySearch(String query) {
        return similaritySearch(SearchRequest.query(query));
    }
}

设计要点:

  • Document 是通用载体,包含文本内容、元数据 Map、以及可选的 Embedding 向量。

  • SearchRequest 封装查询参数:查询文本、TopK、相似度阈值、过滤表达式等。

  • add 方法内部会自动调用 Embedding 模型将文档向量化,再存入实际后端。

  • 通过 @ConditionalOnClass@EnableConfigurationProperties 实现自动配置,只要引入对应的 Starter,Spring Boot 就自动装配。

支持的向量数据库(部分):

  • Chroma:轻量级,适合本地开发。

  • Pinecone:全托管,高性能。

  • Weaviate:支持混合搜索。

  • Milvus:开源,适合大规模生产。

  • PGVector:基于 PostgreSQL,DBA 友好。

  • Redis:支持向量索引的缓存。

  • MongoDB Atlas:文档 + 向量一体化。

集成示例:以 Chroma 为例

依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-chroma-store-spring-boot-starter</artifactId>
</dependency>

配置:

spring:
  ai:
    vectorstore:
      chroma:
        host: http://localhost:8000
        collection-name: my-knowledge-base
    embedding:
      openai:
        api-key: ${OPENAI_API_KEY}

使用:

@RestController
public class KnowledgeController {

    @Autowired
    private VectorStore vectorStore;

    // 入库
    @PostMapping("/ingest")
    public void ingest(@RequestBody String text) {
        Document doc = new Document(text, Map.of("source", "user_upload"));
        vectorStore.add(List.of(doc));
    }

    // 查询
    @GetMapping("/search")
    public List<String> search(@RequestParam String q) {
        return vectorStore.similaritySearch(
                SearchRequest.query(q).withTopK(5).withSimilarityThreshold(0.7)
        ).stream().map(Document::getContent).toList();
    }
}

高级特性:元数据过滤

在检索时可以按元数据条件过滤,比如只搜索某个来源的文档:

SearchRequest request = SearchRequest.query("营收")
        .withFilterExpression("source == 'financial_report'")
        .withTopK(3);
List<Document> docs = vectorStore.similaritySearch(request);

向量库选择指南:

查看内嵌表格

Spring AI 的 VectorStore 抽象让切换向量数据库几乎零代码改动——改个配置和依赖即可,这正是 Spring 生态最擅长的事情:“换实现,不换代码”。


🔧 3. SpringAI 的 Function Calling 如何实现?和 LangChain4J 的 @Tool 有什么区别?

Spring AI 的 Function Calling 实现:基于 @Description 注解和 FunctionCallback 接口。

核心流程:定义一个 Spring Bean,用 @Description 注解标注参数,框架自动将其转换为 OpenAI 兼容的 tools 参数,填入 API 请求中。模型返回调用指令后,Spring AI 自动执行函数,并将结果追加到对话上下文。

1. 定义函数 → 2. 自动注册为 FunctionCallback → 3. API调用时附带 tools 参数
→ 4. 模型返回 tool_calls → 5. 框架自动执行 → 6. 结果追加 → 7. 再次调用LLM

代码示例:定义一个天气查询函数

@Configuration
public class WeatherConfig {

    @Bean
    @Description("获取指定城市当前天气信息,包括温度、湿度和天气状况")
    public Function<WeatherRequest, WeatherResponse> weatherFunction() {
        return new WeatherService();
    }

    public static class WeatherService implements Function<WeatherRequest, WeatherResponse> {
        @Override
        public WeatherResponse apply(WeatherRequest request) {
            // 实际调用天气API
            return new WeatherResponse(request.city(), 25.5, "晴", 65);
        }
    }

    // 输入参数记录,用 @JsonProperty 和 @JsonPropertyDescription 注解
    public record WeatherRequest(
            @JsonProperty(required = true)
            @JsonPropertyDescription("城市名称,如 Beijing") String city
    ) {}

    // 输出
    public record WeatherResponse(String city, double temperature, String condition, int humidity) {}
}

使用(框架自动发现并调用):

@RestController
public class ChatController {

    @Autowired
    private ChatClient chatClient;

    @GetMapping("/ask")
    public String ask(@RequestParam String q) {
        return chatClient.prompt()
                .user(q)
                .call()
                .content();
    }
}

当用户问“北京天气怎么样?”,Spring AI 会自动:

  1. weatherFunction 包装为 FunctionCallback,生成 JSON Schema。

  2. 调用 OpenAI API 时带上这个工具定义。

  3. 模型返回 tool_calls: [{function: {name: "weatherFunction", arguments: {city: "Beijing"}}}]

  4. 框架自动调用 weatherFunction.apply(new WeatherRequest("Beijing"))

  5. 将结果 WeatherResponse(...) 转为文本追加到 messages 中。

  6. 再次调用 LLM 生成最终回答。

与 LangChain4J 的 @Tool 对比:

查看内嵌表格

核心区别深入分析:

  1. 类型安全与复用性 Spring AI 的函数就是一个标准的 java.util.function.Function,不依赖任何 AI 框架接口。这意味着你可以把这个 Bean 直接用于其他场景(如 REST 端点、消息队列消费),它是真正的“普通 Java 函数”。而 LangChain4J 的 @Tool 只能用在 AiServices 上下文中。

  2. 自动发现机制 Spring AI 通过 Spring 的 Bean 扫描机制,自动找到所有带有 @Description 注解的 Function Bean,注册为 FunctionCallback。这比 LangChain4J 的显式 @Tool 注解更符合 Spring 的“约定大于配置”哲学——不需要额外配置类,直接定义 Bean 即可。

  3. 参数元数据 Spring AI 使用 Jackson 的 @JsonProperty 和自定义的 @JsonPropertyDescription 来生成 JSON Schema 中的字段描述。这充分利用了 Java 生态中 JSON 序列化的成熟基础设施。LangChain4J 则需要单独维护 @P 注解,生态系统更封闭。

  4. 与 Spring 生态的融合 Spring AI 的 Function Calling 天然集成 Spring 的依赖注入、AOP、事务管理。你可以在函数里使用 @Transactional@Cacheable 等 Spring 注解,而 LangChain4J 做不到这一点。例如:

@Bean
@Description("查询订单详情")
public Function<OrderRequest, OrderResponse> orderFunction(OrderService orderService) {
    return request -> {
        // 这里可以使用 Spring 的事务、安全、缓存等
        return orderService.getOrder(request.orderId());
    };
}
  1. 工具的交互性(Tool Interaction) Spring AI 1.0 之后引入了 ToolCallbackToolCallbackProvider,使得工具注册更加灵活。你可以动态地从注册中心拉取工具列表,或者对不同用户暴露不同工具,这比 LangChain4J 的静态绑定更适应多租户或 SaaS 场景。

总结一句话: Spring AI 的 Function Calling 是 “用 Spring 的方式写 AI 应用” ——把 LLM 的函数调用能力无缝嫁接到 Java 的函数式编程和 Bean 管理体系中,让 AI 集成变得像加一个 @Service 一样自然。LangChain4J 的 @Tool 更像是 Python 生态中 @tool 装饰器的 Java 移植,虽然简洁,但丧失了 Spring 的深度整合优势。


4、场景题:SpringAI 项目中如何做多模型切换和降级?

难度:⭐⭐⭐(多模型管理、降级策略、高可用设计)

1️⃣ Common Answer

多模型切换就是配置多个 ChatModel,根据需要选择用哪个。降级的话就是如果主模型挂了,就切换到备用模型。可以用配置文件管理多个模型的 key,然后在代码里根据条件选择。也可以用 Sentinel 做熔断降级,主模型不行就切到备用模型。

2️⃣ Impressive Answer

多模型切换和降级需要从模型管理路由策略降级机制三个层面设计。

架构设计

graph TD
    A[请求] --> B[ChatService]
    B --> C{路由策略}
    C -->|正常| D[主模型 GPT-4]
    C -->|降级| E[备用模型 GPT-3.5]
    C -->|兜底| F[国产模型 Qwen-Max]
    D --> G[响应]
    E --> G
    F --> G

实现代码

@Configuration
public class MultiModelConfig {

    @Primary
    @Bean("primaryChatModel")
    public ChatModel primaryChatModel() {
        return OpenAiChatModel.builder()
            .apiKey(primaryApiKey)
            .modelName("gpt-4")
            .build();
    }

    @Bean("fallbackChatModel")
    public ChatModel fallbackChatModel() {
        return OpenAiChatModel.builder()
            .apiKey(primaryApiKey)
            .modelName("gpt-3.5-turbo")
            .build();
    }
}

@Service
public class ChatService {

    @Qualifier("primaryChatModel")
    private final ChatModel primaryModel;

    @Qualifier("fallbackChatModel")
    private final ChatModel fallbackModel;

    @Retry(name = "chatRetry", fallbackMethod = "fallbackChat")
    @CircuitBreaker(name = "chatCircuitBreaker")
    public String chat(String prompt) {
        return primaryModel.call(prompt);
    }

    // 降级方法:主模型失败时自动调用
    private String fallbackChat(String prompt, Exception error) {
        log.warn("主模型调用失败,降级到备用模型: {}", error.getMessage());
        return fallbackModel.call(prompt);
    }
}

Resilience4j 配置

resilience4j:
  circuitbreaker:
    instances:
      chatCircuitBreaker:
        failure-rate-threshold: 50        # 失败率超过 50% 触发熔断
        wait-duration-in-open-state: 30s  # 熔断后等待 30 秒
        sliding-window-size: 10           # 滑动窗口大小
  retry:
    instances:
      chatRetry:
        max-attempts: 3
        wait-duration: 1s

降级策略设计

  1. 熔断降级:连续失败达到阈值后熔断,自动切换备用模型

  2. 超时降级:设置合理的超时时间(如 30 秒),超时后切换

  3. 成本路由:根据请求复杂度动态选择模型,简单问题用便宜模型

  4. 监控告警:实时监控各模型的可用性、延迟、成本,及时发现异常

3️⃣ Key Differences

查看内嵌表格

🧩 5. Spring AI 的核心抽象是什么?如何与不同模型集成?

Spring AI 的设计思想和 Spring 一贯的理念一致:定义统一接口,通过适配器屏蔽底层差异。它的核心抽象围绕着 AI 应用最常见的几个能力展开:

image.png

① ChatModel —— 统一对话入口

ChatModel 是最常用的接口,它代表一个能理解 Prompt 并返回 ChatResponse 的语言模型。不管底层是 OpenAI、Azure、Ollama、还是本地 HuggingFace 模型,调用方式完全一致。

public interface ChatModel {
    ChatResponse call(Prompt prompt);
    // 默认流式方法
    default Flux<ChatResponse> stream(Prompt prompt) {
        throw new UnsupportedOperationException();
    }
}

集成不同模型的步骤:引入对应的 Starter,在 application.yml 里配好认证信息,框架就会自动创建 ChatModel Bean。

  • OpenAI:spring-ai-openai-spring-boot-starter

  • Azure OpenAI:spring-ai-azure-openai-spring-boot-starter

  • Ollama(本地):spring-ai-ollama-spring-boot-starter

  • HuggingFace:spring-ai-huggingface-spring-boot-starter

配置示例:

# 主模型
spring.ai.openai.api-key: ${OPENAI_KEY}
spring.ai.openai.chat.options.model: gpt-4o
# 本地模型
spring.ai.ollama.chat.model: llama3

使用时只需注入 ChatModel

@Autowired
private ChatModel chatModel;

public String ask(String question) {
    return chatModel.call(new Prompt(question)).getResult().getOutput().getContent();
}

多模型并存:当需要同时使用多个模型时,可以用 @Qualifier 或自定义 Bean 名来区分,或者实现一个路由 ChatModel,内部持有多个模型并根据策略选择。

② EmbeddingModel —— 把文本转为向量

接口简洁,输入一段文本或 Document,返回浮点数组:

public interface EmbeddingModel {
    EmbeddingResponse embedForResponse(List<String> texts);
    default float[] embed(String text) { ... }
}

同样,换底层模型只需改配置,代码不动。支持 OpenAI、Azure、Ollama、HuggingFace 等。

③ VectorStore —— 统一向量数据库访问

VectorStore 接口抽象了向量存储的增删查:

public interface VectorStore {
    void add(List<Document> docs);
    List<Document> similaritySearch(SearchRequest request);
    default List<Document> similaritySearch(String query) { ... }
}

Spring AI 提供了 Chroma、Pinecone、Milvus、PGVector、Redis 等十几种实现,切换时只需换 Starter 和配置,代码零修改。

④ Advisor —— AOP 风格的拦截器

Advisor 可以在请求发送前和响应返回后插入自定义逻辑,是实现 RAG、日志、内容过滤等功能的利器。它让 AI 调用链和 Spring 的 AOP 体系无缝融合。

核心设计思想:Spring AI 不发明新轮子,而是让 AI 能力变成 Spring 容器中普通的 Bean,用 DI、AOP、自动配置这些开发者熟悉的方式来使用。这种“Spring 方式”的 AI 集成,正是它相比 LangChain 等框架的独特优势。


⚙️ 6. 在 Spring AI 中如何实现 Function Calling?如何处理异步调用?

Function Calling 是让 LLM 能“办事”的关键。Spring AI 的实现非常简洁:把 Java 函数声明为 Bean,框架自动包装成 OpenAI 兼容的工具定义,并在模型返回调用指令后自动执行该函数。

2.1 实现 Function Calling

分三步:

第一步:定义函数 Bean

@Configuration
public class ToolsConfig {

    @Bean
    @Description("获取指定城市的当前天气,包括温度、湿度和天气状况")
    public Function<WeatherRequest, WeatherResponse> weatherFunction() {
        return new WeatherService();
    }

    // 函数内部可以用 Spring 管理的依赖
    public static class WeatherService implements Function<WeatherRequest, WeatherResponse> {
        @Override
        public WeatherResponse apply(WeatherRequest request) {
            // 实际调用天气 API
            return new WeatherResponse(request.city(), 25.6, "晴", 60);
        }
    }

    // 使用 @JsonProperty 和 @JsonPropertyDescription 生成精确的 JSON Schema
    public record WeatherRequest(
        @JsonProperty(required = true)
        @JsonPropertyDescription("城市名称,如 Beijing") String city
    ) {}

    public record WeatherResponse(
        String city, double temperature, String condition, int humidity
    ) {}
}

第二步:框架自动发现并注册 Spring AI 会扫描所有 @Description 注解的 Function Bean,自动生成 FunctionCallback,在调用 LLM 时将其放入 tools 参数。

第三步:通过 ChatClient 调用,自动完成工具调用闭环

@RestController
public class ChatController {

    @Autowired
    private ChatClient chatClient;

    @GetMapping("/ask")
    public String ask(@RequestParam String q) {
        return chatClient.prompt()
                .user(q)
                .call()
                .content();
    }
}

当用户问“北京天气怎么样?”,Spring AI 自动执行以下流程:

用户输入 → [框架] 构造 API 请求 (带 tools 定义) → LLM 返回 tool_calls
→ [框架] 解析 tool_calls,反射调用 weatherFunction → 拿到结果
→ [框架] 将结果追加到消息历史 → 再次调用 LLM → 生成最终回答

整个过程业务代码只需要定义函数 Bean,其他的全由框架托管。

2.2 处理异步调用

LLM 调用本身是网络 IO 密集型操作,生产环境必须异步化。Spring AI 提供了以下几个层面的异步支持:

Flux<ChatResponse> stream(Prompt prompt) 这是最常用的异步流式方法,底层基于 Reactor。调用后立即返回 Flux,你可以用它来逐 token 推送给前端。

@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam String q) {
    return chatModel.stream(new Prompt(q))
            .map(resp -> resp.getResult().getOutput().getContent());
}

② 结合 @Async 注解 把 AI 调用放在 Spring 管理的线程池中执行,避免阻塞主线程(如 Tomcat 的 worker 线程)。

@Service
public class AiAsyncService {

    @Async("aiTaskExecutor")
    public CompletableFuture<String> askAsync(String question) {
        String answer = chatModel.call(new Prompt(question)).getResult().getOutput().getContent();
        return CompletableFuture.completedFuture(answer);
    }
}

@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean("aiTaskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("ai-async-");
        executor.initialize();
        return executor;
    }
}

③ 处理 Function Calling 中的长耗时工具 如果一个工具函数本身是异步的(如需要等待外部服务回调),可以让函数返回 CompletableFuture,或在函数内部使用 DeferredResult 等方式。Spring AI 的 FunctionCallback 支持返回 Future 类型,框架会处理好等待和结果提取。

④ 并行工具调用 当 LLM 返回多个独立的 tool_calls 时,Spring AI 会默认用线程池并行执行这些工具,汇总结果后再统一返回给 LLM。这大幅减少了多工具调用场景的延迟。

// 框架内部逻辑伪代码
List<ToolCall> toolCalls = response.getToolCalls();
List<CompletableFuture<ToolResult>> futures = toolCalls.stream()
    .map(tc -> CompletableFuture.supplyAsync(() -> execute(tc), executor))
    .toList();
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

总的来说,Spring AI 的 Function Calling 是“声明式”的——你声明函数,框架处理调用、序列化、错误重试、并行执行。这种设计让业务逻辑和 AI 调用完全解耦,业务函数甚至可以脱离 AI 框架独立测试和部署。


📚 7. Spring AI 的 EmbeddingClient 与 VectorStore 如何配合实现 RAG?

RAG(检索增强生成)的流程可以抽象为一条数据流水线:文档 → 切分 → 向量化 → 存入向量库 → 用户提问 → 向量化 → 检索相关文档 → 注入 Prompt → LLM 生成答案。

image.png

Spring AI 为每一步都提供了对应组件,让整个链路能在 Spring 容器内无缝串联。

第一步:文档读取与切分

// 读取 PDF、TXT 等文件
DocumentReader reader = new TextReader("classpath:knowledge.txt");
List<Document> rawDocs = reader.read();

// 按语义或固定长度切分
TextSplitter splitter = new TokenTextSplitter(800, 100, 5, 1000, true);
List<Document> chunks = splitter.apply(rawDocs);

第二步:向量化并存入向量库

@Autowired
private EmbeddingModel embeddingModel;

@Autowired
private VectorStore vectorStore;

public void ingest() {
    // 批量向量化
    List<String> texts = chunks.stream().map(Document::getContent).toList();
    List<float[]> embeddings = embeddingModel.embed(texts);

    // 将向量赋给 Document
    for (int i = 0; i < chunks.size(); i++) {
        chunks.get(i).setEmbedding(embeddings.get(i));
    }

    // 存入向量库
    vectorStore.add(chunks);
}

在实际应用中,Spring AI 的 VectorStore.add() 内部会帮你调 EmbeddingModel,你可以直接存原始文本,框架负责向量化。所以上面的代码可以简化为:

vectorStore.add(chunks);  // 框架会自动向量化

第三步:构建 RAG 问答链

最直接的方式是用 Advisor 实现一个检索增强切面,注入到 ChatClient 中:

@Component
public class RagAdvisor implements RequestResponseAdvisor {

    private final VectorStore vectorStore;

    public RagAdvisor(VectorStore vectorStore) {
        this.vectorStore = vectorStore;
    }

    @Override
    public AdvisedRequest adviseRequest(AdvisedRequest request, Map<String, Object> context) {
        // 1. 从用户输入中提取查询文本
        String userQuery = request.userText();

        // 2. 检索相关文档
        List<Document> docs = vectorStore.similaritySearch(
                SearchRequest.query(userQuery).withTopK(4));

        // 3. 拼接为上下文字符串
        String contextText = docs.stream()
                .map(Document::getContent)
                .collect(Collectors.joining("\n\n---\n\n"));

        // 4. 增强 System Prompt
        String augmentedSystem = String.format(
                "基于以下参考资料回答用户问题。如果资料中没有答案,请明确告知。\n\n参考资料:\n%s",
                contextText
        );

        // 5. 修改请求
        return AdvisedRequest.from(request)
                .withSystemText(augmentedSystem + "\n\n" + request.systemText())
                .build();
    }

    @Override
    public ChatResponse adviseResponse(ChatResponse response, Map<String, Object> context) {
        return response;
    }

    @Override
    public int getOrder() { return 0; }
}

配置 ChatClient 使用该 Advisor:

@Bean
public ChatClient chatClient(ChatClient.Builder builder, RagAdvisor ragAdvisor) {
    return builder.defaultAdvisors(ragAdvisor).build();
}

第四步:调用

@Autowired
private ChatClient chatClient;

public String ask(String question) {
    return chatClient.prompt().user(question).call().content();
}

这样,所有通过 ChatClient 发出的请求都会自动携带检索到的知识,业务代码一行都不需要改。

进阶优化手段:

  • 混合检索:除了向量相似度,还可以结合 BM25 关键词检索。SearchRequest 支持设置 filterExpression,可以在向量库层面实现元数据过滤。

  • 检索结果为空处理:在 RagAdvisor 中判断 docs 是否为空,如果为空,可以把 System Prompt 改为“知识库中暂无相关信息,请基于你的常识谨慎回答”。

  • 长文档压缩:检索到的文档如果太长,可以先过一个 DocumentCompressor(如 LLM 摘要),再注入 Prompt,避免撑爆上下文窗口。

  • 多路召回与重排序:用 MultiQueryRetriever 对用户 query 做多角度改写,分别检索后合并,再用一个重排序模型取最相关的 Top-K 片段。

整体效果: Spring AI 通过 EmbeddingClientVectorStore 这两个抽象,把 RAG 的核心——语义搜索和存储——变成了一种可自由切换的底层能力。而 Advisor 则让你在不侵入业务代码的前提下,将检索能力“织入”每一次 AI 调用。这种设计让 RAG 从“需要写一堆胶水代码”变成了“引入一个切面就生效”的即插即用模式。

🧩 8. Spring AI 的自动配置原理是什么?如何添加自定义模型?

Spring AI 的自动配置完全遵循 Spring Boot 的 AutoConfiguration 机制,核心思路是“条件装配”——类路径下存在对应的依赖时,框架就自动创建并注册 ChatModel、EmbeddingModel 等 Bean。

image.png

以 OpenAI 为例,OpenAiAutoConfiguration 会做这几件事:

  • 检查类路径是否有 OpenAiApi.class(避免使用者没引包却启动了配置)。

  • spring.ai.openai 前缀的配置绑定到 OpenAiConnectionProperties

  • 创建 OpenAiApi(底层 HTTP 客户端),再创建 OpenAiChatModel

  • OpenAiChatModel 暴露为 ChatModel 接口的 Bean。

添加自定义模型有两种方式:

方式一:直接实现 ChatModel 接口(适合完全自定义的模型)

@Component
public class MyCustomChatModel implements ChatModel {

    @Override
    public ChatResponse call(Prompt prompt) {
        // 实现自定义推理逻辑,调用自研模型或第三方 API
        String generatedText = customModelInference(prompt.getContents());
        return new ChatResponse(List.of(
                new Generation(new AssistantMessage(generatedText))));
    }

    @Override
    public Flux<ChatResponse> stream(Prompt prompt) {
        // 如需流式,可实现此方法
        return ChatModel.super.stream(prompt);
    }
}

之后就可以在应用中直接注入 ChatModel 使用,Spring 会把它和其他自动配置的模型一样对待。

方式二:复用现有基础设施,只换底层客户端 如果你的模型兼容 OpenAI API 格式,只需继承 OpenAiApi 并修改 Base URL 和鉴权头,然后手动构建 OpenAiChatModel

@Bean
public ChatModel myCustomOpenAiCompatibleModel() {
    OpenAiApi api = new OpenAiApi("https://my-model-service/v1", "custom-api-key");
    return new OpenAiChatModel(api);
}

如果需要多个模型共存,可以用 @Qualifier 区分,或构造一个 RoutingChatModel 统一路由。

核心点:Spring AI 的自动配置就像一个可插拔的模型超市,引入不同的 Starter 就能获得不同的模型能力;而自定义模型只需交出符合 ChatModel 协议的 Bean,就能无缝融入整个生态。


🛡️ 9. 在 Spring AI 中如何保障生产环境的安全性,例如 API Key 管理和请求限流?

生产环境的安全围绕两个核心:凭证保护和流量控制。

① API Key 管理:绝不硬编码,与环境解耦

API Key 不能写在 application.yml 里提交到 Git。推荐分层管理:

  • 本地开发:使用环境变量或 .env 文件(不纳入版本控制)。
export OPENAI_API_KEY=sk-...

生产环境:注入 Kubernetes Secrets 或云平台的密钥管理服务(如 AWS Secrets Manager、Azure Key Vault)。

spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}   # 从环境变量读取
  • 进一步加固:结合 Spring Cloud Config 或 Vault,对配置文件进行加密存储,运行时解密。

Spring AI 的 *Properties 类默认支持从环境变量、系统属性、配置文件等多种来源读取,所以你只需在配置文件中写 ${变量名} 即可。

② 请求限流:保护 API 额度与系统稳定性

调用大模型有成本和频率限制,必须防止恶意或失控调用。可以在多个层面实现限流:

方式一:基于 Resilience4j 的注解限流

@Bean
public RateLimiter rateLimiter() {
    RateLimiterConfig config = RateLimiterConfig.custom()
            .limitRefreshPeriod(Duration.ofSeconds(1))
            .limitForPeriod(5)           // 每秒最多5次调用
            .timeoutDuration(Duration.ofMillis(500))
            .build();
    return RateLimiterRegistry.of(config).rateLimiter("ai-calls");
}

@Service
public class GuardedAiService {
    @Autowired
    private ChatModel chatModel;
    @Autowired
    private RateLimiter rateLimiter;

    public String ask(String question) {
        // 获取令牌,若失败会抛异常
        RateLimiter.waitForPermission(rateLimiter);
        return chatModel.call(new Prompt(question)).getResult().getOutput().getContent();
    }
}

超过阈值时,waitForPermission 会阻塞或直接抛出 RequestNotPermitted,你可以返回友好提示“系统繁忙,请稍后重试”。

方式二:通过 Advisor 实现透明限流 定义一个 RateLimitAdvisor,在 adviseRequest 方法中检查令牌,若不通过则拒绝调用。

@Component
public class RateLimitAdvisor implements RequestResponseAdvisor {
    private final RateLimiter rateLimiter;

    @Override
    public AdvisedRequest adviseRequest(AdvisedRequest request, Map<String, Object> context) {
        if (!rateLimiter.acquirePermission()) {
            throw new RateLimitExceededException("AI 调用太频繁,请稍后再试");
        }
        return request;
    }
    // ...
}

将其注册到 ChatClient,所有调用自动受保护。

③ 内容安全:输入/输出过滤

利用 Advisor 还可以对用户输入做敏感词过滤,对模型输出做合规检查,这是安全的另一道防线。

一句话总结:API Key 用环境变量/密钥管理服务注入,决不明文写死;请求限流用 Resilience4j 或 Spring Cloud Gateway 形成保护壳;安全 Advisor 则像是给每次 AI 调用穿上的防弹衣。


⚙️ 10. 如何自定义 Spring AI 的 ChatClient,实现多轮对话和上下文管理?

ChatClient 是 Spring AI 提供的流式构建器,通过它可以轻松装配多轮对话记忆、Advisor、默认系统提示等。

实现多轮对话的关键:将对话历史注入上下文窗口。

Spring AI 默认是无状态的,要实现“记住之前说过的话”,需要把历史消息手动追加到 Prompt 中。我们可以把这个逻辑封装在一个 Advisor 里,实现透明化的上下文管理。

自定义 ConversationMemoryAdvisor:

@Component
public class ConversationMemoryAdvisor implements RequestResponseAdvisor {

    // 使用线程安全的 Map 存储不同会话的历史,实际应用可改用 Redis
    private final Map<String, List<Message>> conversationStore = new ConcurrentHashMap<>();

    @Override
    public AdvisedRequest adviseRequest(AdvisedRequest request, Map<String, Object> context) {
        // 1. 获取会话 ID,可从请求中携带,也可用用户标识
        String conversationId = request.getAdviseParam("conversationId", String.class, "default");

        // 2. 获取该会话的历史消息
        List<Message> history = conversationStore.computeIfAbsent(conversationId, k -> new ArrayList<>());

        // 3. 构建带历史的消息列表:系统提示 + 历史消息 + 当前用户消息
        List<Message> fullMessages = new ArrayList<>();
        fullMessages.add(new SystemMessage(request.systemText()));
        fullMessages.addAll(history);
        fullMessages.add(new UserMessage(request.userText()));

        // 4. 替换原始的 Prompt,改由包含历史的消息列表
        Prompt newPrompt = new Prompt(fullMessages, request.options());
        return AdvisedRequest.from(request).withPrompt(newPrompt).build();
    }

    @Override
    public ChatResponse adviseResponse(ChatResponse response, Map<String, Object> context) {
        // 5. 从请求上下文中取出当前用户消息
        Message userMessage = new UserMessage(response.getMetadata().get("originalUserText", String.class));
        // 6. 从响应中取出模型的回复
        Message assistantMessage = response.getResult().getOutput();

        // 7. 更新历史:将当前轮的一对消息追加进会话历史
        String conversationId = context.get("conversationId", String.class, "default");
        List<Message> history = conversationStore.get(conversationId);
        if (history != null) {
            history.add(userMessage);
            history.add(assistantMessage);

            // 控制历史长度,避免 token 超限,保留最近 20 条消息
            if (history.size() > 20) {
                history.subList(0, history.size() - 20).clear();
            }
        }
        return response;
    }

    @Override
    public int getOrder() { return 10; }
}

装配到 ChatClient 并使用:

@Bean
public ChatClient chatClient(ChatClient.Builder builder, ConversationMemoryAdvisor memoryAdvisor) {
    return builder
            .defaultSystem("你是一个智能助手,请用简洁的中文回答。")
            .defaultAdvisors(memoryAdvisor)
            .build();
}

// 业务调用
@Autowired
private ChatClient chatClient;

public String chat(String userInput, String conversationId) {
    return chatClient.prompt()
            .user(userInput)
            .adviseParam("conversationId", conversationId)  // 传入会话 ID
            .call()
            .content();
}

效果:同一个 conversationId 下的所有调用,都会自动携带之前所有的对话历史,模型能记住上下文。

进阶设计:

  • 历史持久化:将 Map 替换为 Redis,实现跨 JVM、跨重启的持久会话。

  • 摘要压缩:当历史过长时,不直接删除旧消息,而是用 LLM 生成一份摘要,保留关键信息,极大节省 token。

  • 多用户隔离:conversationId 可以与登录用户绑定,避免不同用户串话。

收束: 自定义 ChatClient 的精髓就在于通过 Advisor 把“上下文管理”这个横切关注点独立出来。业务代码只关心发什么消息,完全不感知记忆机制的存在。这种干净分离让你可以单独测试记忆策略、随时切换存储后端,甚至给不同用户配置不同的记忆长度——所有复杂性都藏在了 Advisor 内部。