一、概述

Spring AI定位

Spring AI是方便快速开发AI上层应用的脚手架,可以在不了解大模型底层原理的条件下快速开发基于大模型的AI应用。

适合场景及人群

  1. 适合java技术栈开发人员,类似的还有langchain4j
  2. 更适合有一定开发经验的技术人员,无开发经验的更推荐字节的coze或dify。
  3. 适合有一定定制化需求的场景,通用场景可以使用coze,dify等低代码平台。

优势

  1. spring-ai与spring生态无缝兼容
  2. 自动化配置通过使用默认配置的方式大大减少了使用时所需填写的参数
  3. 开箱即用大大降低了ai应用开发的上手难度

劣势

起步较晚,对比python的langchain,功能插件少了很多

国内大模型支持不全,仅支持部分厂商的大模型 (deepseek、minimax、kimi的Moonshot、百度的千帆、智谱的ZhipuAi(阿里的通义建议直接使用spring-ai-alibaba))

常用功能

  1. 大模型连接及调用(流式及非流式)
  2. 提示词管理
  3. 结构化输出
  4. Advosor
  5. 多模态
  6. 工具调用
  7. 聊天记忆
  8. 向量数据库
  9. RAG(检索增强生成)
  10. MCP(模型上下文协议)

二、大模型连接及调用

1.Pom文件导包

引入spring-boot-starter-web和spring-ai-starter-model-zhipuai(根据所选大语言模型不同引入的包不同)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.5.0</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>spring-ai</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-ai</name>
	<description>Demo project for Spring AI</description>
 
	<properties>
		<java.version>24</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
 
		<dependency>
			<groupId>org.springframework.ai</groupId>
			<artifactId>spring-ai-starter-model-zhipuai</artifactId>
			<version>1.0.0</version>
		</dependency>
 
	</dependencies>
 
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
 
</project>

2.application.properties配置

spring.ai.zhipuai.api-key=<your key>
spring.ai.zhipuai.chat.options.model=glm-4-air

3.通过Model实现,返回大模型结果

package com.example.spring_ai.controller;
 
import org.springframework.ai.zhipuai.ZhiPuAiChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
 
import java.util.Map;
 
@RestController
public class SpringAiExample01
{
 
    private final ZhiPuAiChatModel chatModel;
 
    @Autowired
    public SpringAiExample01(ZhiPuAiChatModel chatModel)
    {
        this.chatModel = chatModel;
    }
 
    /**
     * 简单调用
     * @param message
     * @return
     */
    @GetMapping("/ai/generate")
    public Map generate(@RequestParam(value = "message", defaultValue = "请问你是谁") String message)
    {
        return Map.of("generation", this.chatModel.call(message));
    }
 
}

4.通过封装ChatClient实现,ChatClient会自动匹配可用的ChatModel,它封装了ChatModel,屏蔽了不同底层模型的差异,方便一键切换底层大模型

package com.example.spring_ai.controller;
 
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class SpringAiExample02ChatClient
{
    
    private final ChatClient chatClient;
    
    public SpringAiExample02ChatClient(ChatClient.Builder chatClientBuilder)
    {
        //ChatClient封装了ChatModel,屏蔽了不同底层模型的差异
        this.chatClient = chatClientBuilder.build();
    }
 
    @GetMapping("/ai")
    String generation(String userInput)
    {
        return this.chatClient.prompt()
                .user(userInput)
                .call()
                .content();
    }
}

五、流式输出

通过返回类型Flux实现流失输出

 @GetMapping("/ai/stream")
    Flux<String> generationStream(String userInput)
    {
        return this.chatClient.prompt()
                .user(userInput)
                .stream()
                .content();
    }

三、提示词管理

提示词工程

对于如何写好提示词,已总结出若干规则,可以参考Prompt Engineering Guide | Prompt Engineering Guide

常用的提示词规则

  • 设置系统角色与背景
  • 提供样例演示
  • 指明解决问题的步骤
  • 要求展示思考过程
  • 要求模型自我验证
  • 将任务分解为多个子任务
  • 检索增强生成RAG

其他优化查询的组件

  • CompressionQueryTransformer:查询压缩,将对话历史和后续查询压缩为捕获对话本质的独立查询
  • RewriteQueryTransformer:查询重写,重写用户查询,从而在查询向量数据库或搜索引擎等目标系统时获得更好结果
  • MultiQueryExpander:查询扩展,将查询扩展为多个语义多样化的变体,以捕获不同视角,有助于检索额外上下文信息并提高找到相关结果的概率

其他框架中的高级功能

  • 少样本模板:专门为附带样例的提示词设计,在样例较多时还可以自动筛选相关性较高的样例
  • 提示词库:聚合各种使用场景下的高质量提示词

提示词模板

一:链式调用

  1. 在创建ChatClient的时候,可以通过defaultUser和defaultSystem指定默认的系统消息和用户消息;
  2. 在使用ChatClient发送请求的时候,可以通过prompt、system、user、message等4个方法来操作提示词。其中prompt和message不能在调用后动态修改内容,而system和user可以像上述代码中的形式来动态替换占位符
package com.example.spring_ai.controller;
 
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
 
import java.util.Map;
 
@RestController
public class SpringAiExample04Prompt
{
 
    private final ChatClient chatClient;
 
    public SpringAiExample04Prompt(ChatClient.Builder chatClientBuilder)
    {
        this.chatClient = chatClientBuilder
                .defaultSystem("你是一个{industry}问题专家,请回答以下问题:")
                .build();
    }
 
    @GetMapping("/ai/prompt")
    String generation(String userInput)
    {
        return this.chatClient.prompt()
                .system("你是一个{industry}问题专家,擅长{goodAt},请回答以下问题:")
                .system(a -> a.params(Map.of("industry", "IT", "goodAt", "机器学习")))
                .user("问题是:{question}")
                .user(a -> a.param("question", userInput))
                .call()
                .content();
    }
 
}

二、普通调用

String userText = """
    Tell me about three famous pirates from the Golden Age of Piracy and why they did.
    Write at least a sentence for each pirate.
    """;
 
Message userMessage = new UserMessage(userText);
 
String systemText = """
  You are a helpful AI assistant that helps people find information.
  Your name is {name}
  You should reply to the user's request with your name and also in the style of a {voice}.
  """;
 
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemText);
Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", name, "voice", voice));
 
Prompt prompt = new Prompt(List.of(userMessage, systemMessage));
 
List<Generation> response = chatModel.call(prompt).getResults();

四、结构化输出

1.返回实体

将返回结果转化为实体类,只需要调用entity方法并传入对应的类即可

Java 16 中引入的 record 是一种用于简化定义不可变数据类 的新语言特性,它特别适用于那些主要用于存储数据的类。通过使用 record,你可以以一种简洁、声明式的方式定义类,而无需手动编写大量样板代码(如构造函数、getter、toString、equals 和 hashCode 等)。

    /**
     * 返回实体
     * http://localhost:8080/ai/entity?userInput=推荐一下6月最适合吃的水果
     * @param userInput
     * @return
     */
    @GetMapping("/ai/entity")
    Fruit entity(String userInput)
    {
        return this.chatClient.prompt()
                .user(userInput)
                .call()
                .entity(Fruit.class);
    }
 
    record Fruit(String name, String month){}

2.返回实体列表

若希望转换为List或Map,则需要用到ParameterizedTypeReference

    /**
     * 返回实体列表
     * http://localhost:8080/ai/entity?userInput=推荐一下各个月份适合吃的水果
     * @param userInput
     * @return
     */
    @GetMapping("/ai/entitylist")
    List<Fruit> entitylist(String userInput)
    {
        return this.chatClient.prompt()
                .user(userInput)
                .call()
                .entity(new ParameterizedTypeReference<List<Fruit>>() {});
    }

3.返回字符串列表

    /**
     * 列表返回
     * http://localhost:8080/ai/list?userInput=推荐一下各个月份适合吃水果
     * @param userInput
     * @return
     */
    @GetMapping("/ai/list")
    List<String> list(String userInput)
    {
        return this.chatClient.prompt()
                .user(userInput)
                .call()
                .entity(new ListOutputConverter(new DefaultConversionService()));
    }

4.自定义类型转换器

Spring AI通过StructuredOutputConverter接口和entity()方法实现了将模型输出转换为结构化实体类的功能,其核心流程如下:

1. 接口定义与继承关系

StructuredOutputConverter接口继承了Converter(定义类型转换方法)和FormatProvider(提供数据格式声明能力),并通过两个抽象类实现:

  • AbstractMessageOutputConverter:基于消息内容的转换逻辑。
  • AbstractConversionServiceOutputConverter:结合Spring的ConversionService进行类型转换。

2. entity()方法的作用

ChatClient中调用entity()时,框架会:

  • 将模型返回的原始字符串(如JSON)作为输入。
  • 使用StructuredOutputConverter实例(默认或自定义)将字符串转换为目标实体类(如Java Record或POJO)。

3. 默认转换器的实现

Spring AI内置了默认的JSON解析能力:

  • 通过addDefaultConverters()注册标量类型(如String、Number)和集合类型(如List、Map)的转换器。
  • 默认使用Jackson库解析JSON格式的模型输出,并映射到实体类字段。

4. 转换过程的关键点

  • 最大努力转换 :框架尽力将模型输出结构化,但AI模型可能无法严格遵循格式要求(如返回非JSON文本),此时转换会失败。
  • 自定义转换器 :可通过实现StructuredOutputConverter接口自定义转换逻辑(如处理特殊格式或校验数据),并结合andThen()方法链式调用其他转换器。

5. 典型使用场景

  • 直接返回实体类 :模型输出需符合目标类的字段结构(如JSON键与属性名匹配)。
  • 流式处理 :结合stream()方法逐步解析流式响应(如分块返回的JSON)
// 自定义转换器(简化版)
class MyConverter implements StructuredOutputConverter {
    @Override
    public Object convert(String input, Class<?> targetType) {
        // 实现从字符串到targetType的转换逻辑
        return parseJson(input, targetType);
    }
}

// 使用自定义转换器
chatClient.entity(new MyConverter());

结构化输出的作用原理

结构化输出其实主要包含2部分功能:

FormatProvider:告诉大模型想要的输出格式,这部分内容将追加到提示词中

Converter:从大模型获取输出后,将文本转换为期望的结构,比如类或者列表

结构化输出的相关接口及类

StructuredOutputConverter:结构化输出转换器的接口,其继承了FormatProvider接口和Converter接口,具体实现类有BeanOutputConverter、ListOutputConverter等。

BeanOutputConverter:实体转换器,样例中返回实体和返回实体列表两个例子的底层实现类,具体作用原理是要求大语言模型输出json形式的文本,然后将json转换为bean

spring-ai ChatClient的几种输出格式

spring-ai的链式调用中,有如下几种方法返回不同结果类型:

content:大模型返回的字符串,不包含其他额外信息

chatResponse:返回ChatResponse类型,包含大模型返回的文本和一些元数据

chatClientResponse:返回ChatClientResponse类型,包含ChatResponse和上下文信息

entity:大模型返回的文本转换而来的bean

responseEntity:包含大模型返回的文本转换而来的bean,和ChatResponse

大语言模型或其他框架相关功能

部分大模型支持返回类型的入参,可以指定返回类型,例如json

langchain支持json、xml、yaml等格式的输出解析

五、Advosor

初步介绍

Advosor类似于Spring中AOP的思想,利用其面向切面编程能力,提供的功能模块,可以允许用户在请求前和请求后,在不影响原有代码的情况下,插入自己的通用逻辑。

    /**
     * 配合配置文件进行日志输出(打印请求和响应)
     * @param userInput
     * @return
     */
    @GetMapping("/ai/log")
    String generation(String userInput)
    {
        return this.chatClient.prompt()
                .advisors(new SimpleLoggerAdvisor())
                .user(userInput)
                .call()
                .content();
    }

SimpleLoggerAdvisor是spring-ai内置的一个日志工具,支持打印输入和输出(Debug级别),使用时需添加配置:logging.level.org.springframework.ai.chat.client.advisor=DEBUG

如果想创建自己的Advisor,实现CallAdvisor和StreamAdvisor接口即可,也可以直接实现BaseAdvisor接口,添加Advisor也可以放在创建ChatClient时,使用defaultAdvisor方法添加。

    this.chatClient = chatClientBuilder.defaultAdvisors(
                new SimpleLoggerAdvisor()
        ).build();

执行顺序

dvisor的执行顺序是一个需要关注的点,defaultAdvisor和advisor方法都支持添加多个Advisor,若添加多个Advisor,则最早添加的Advisor在请求时最早执行,在返回响应时最后执行,如下图

Advisor的作用原理

我们以上面的SimpleLoggerAdvisor为例,看一下其中关键的代码

    public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
        this.logRequest(chatClientRequest);
        ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest);
        this.logResponse(chatClientResponse);
        return chatClientResponse;
    }

在这段代码中,传入了请求对象和AdvisorChain,打印了请求对象和响应对象,中间调用了nextCall,我们看下nextCall的代码,在nextCall中,弹出了下一个Advisor,调用了下一个Advisor的advisorCall,并将自身作为参数传入。

    public ChatClientResponse nextCall(ChatClientRequest chatClientRequest) {
        Assert.notNull(chatClientRequest, "the chatClientRequest cannot be null");
        if (this.callAdvisors.isEmpty()) {
            throw new IllegalStateException("No CallAdvisors available to execute");
        } else {
            CallAdvisor advisor = (CallAdvisor)this.callAdvisors.pop();
            AdvisorObservationContext observationContext = AdvisorObservationContext.builder().advisorName(advisor.getName()).chatClientRequest(chatClientRequest).order(advisor.getOrder()).build();
            return (ChatClientResponse)AdvisorObservationDocumentation.AI_ADVISOR.observation((ObservationConvention)null, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry).observe(() -> advisor.adviseCall(chatClientRequest, this));
        }
    }

那么整体流程就大概清楚了,spring-ai中AdvisorChain保存了所有的Advisor,AdvisorChain每次弹出一个Advisor并调用advisorCall,调用时将请求参数及自身传入;在Advisor中,advisorCall先执行自身前置逻辑,然后调用AdvisorChain的nextCall执行下一个Advisor,最后返回时执行自身后置逻辑。

在这个过程中比较特殊的是ChatModelCallAdvisor,它是在我们调用ChatClient的call方法时创建并加入到advisor中的(最后加入),它并不会继续调用AdvisorChain的nextCall,而是直接调用chatModel的call方法向大模型发送请求。同时,在发送请求前,会根据配置的结构化输出类,将输出格式的要求附加到提示词的末尾。代码如下:

    public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
        Assert.notNull(chatClientRequest, "the chatClientRequest cannot be null");
        ChatClientRequest formattedChatClientRequest = augmentWithFormatInstructions(chatClientRequest);
        ChatResponse chatResponse = this.chatModel.call(formattedChatClientRequest.prompt());
        return ChatClientResponse.builder().chatResponse(chatResponse).context(Map.copyOf(formattedChatClientRequest.context())).build();
    }

Advisor相关接口及实现

Advisor:Advisor的顶层接口

CallAdvisor:传统调用方式(一次请求一次响应)对应的Advisor接口

StreamAdvisor:流式调用方式(一次请求多次响应)对应的Advisor接口

BaseAdvisor:继承了CallAdvisor和StreamAdvisor的接口,提供了一些默认实现,用户仅需实现before和after方法。

ChatModelCallAdvisor:CallAdvisor的实现类,在调用ChatClient的call方法时创建,之后加入到DefaultAroundAdvisorChain中。负责向提示词追加结构化输出的指令,并调用ChatModel的call方法。

AdvisorChain:Advisor容器(链)的顶级接口

CallAdvisorChain:传统调用方式对应的Advisor链接口

StreamAdvisorChain:流式调用方式对应的Advisor链接口

BaseAdvisorChain:同时继承CallAdvisorChain和StreamAdvisorChain的接口

DefaultAroundAdvisorChain:默认AdvisorChain类,实现了BaseAdvisorChain,之前看到的nextCall就在这个类中。

Spring AI内置的Advisor

ChatModelCallAdvisor:大语言模型

SimpleLoggerAdvisor:日志

QuestionAnswerAdvisor:通过向量存储实现问答功能

SafeGuardAdvisor:阻止模型生成不恰当的内容

还有一系列聊天记忆的Advisor,如MessageChatMemoryAdvisor、PromptChatMemoryAdvisor、VectorStoreChatMemoryAdvisor等。

六、多模态

多模态:模型同时理解和处理文本、图像、音频及其他数据格式等多源信息的能力。

例子:用户输入一个网图地址,要求大模型描述在图中看到了什么。

目前media只支持UserMessage,同时要求大模型具备处理多模态数据的能力。media方法第一个参数为多模态类型,如png图片,第二个参数可以传递一个URL,也可以传递一个Resource类型的本地文件。

    /**
     * 多模态
     * @param userInput 网图地址
     * @return
     */
    @GetMapping("/ai/multi")
    String generation(String userInput)
    {
        return this.chatClient.prompt()
                .advisors(new SimpleLoggerAdvisor())
                .user(u -> {
                    try {
                        u.text("说明你在这张图中看到了什么")
                                .media(MimeTypeUtils.IMAGE_PNG, URI.create(userInput).toURL());
                    } catch (MalformedURLException e) {
                        throw new RuntimeException(e);
                    }
                })
                .call()
                .content();
    }

七、工具调用

工具调用:能够将代码工程中的一些接口开放给大模型使用

    /**
     * 调用工具
     * 可以使用toolContext传递参数
     * @param userInput
     * @return
     */
    @GetMapping("/ai/tool")
    String generation(String userInput)
    {
        return this.chatClient.prompt()
                .user(userInput)
                .tools(new DateTimeTools())
                .call()
                .content();
    }
 
class DateTimeTools
{
 
    @Tool(description = "获取当前日期时间")
    String getCurrentDateTime()
    {
        return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
    }
 
}

工具调用共分两步:

  1. 声明:在想要开放给大模型使用的方法上添加@Tool
  2. 注册:调用请求时通过tools方法注册工具类

注解说明

Tool注解:用来标注作为工具的方法,有下列属性:

name:名称,唯一识别该方法,不可重复

description:描述,方便大模型理解工具的作用

returnDirect:工具调用后,是直接返回,还是将调用结果送往大模型,默认送往大模型

resultConverter:结果转换器

ToolParam注解:搭配Tool注解使用,用来对方法参数进行说明,有下列属性:

required:该参数是否必填,默认是

description:描述,方便大模型理解该参数的作用

工具调用原理

如下图所示,共分为6步:

1.发送带有工具定义的请求到大模型

2.大模型返回是否需要调用工具,若需要,则同时返回当符合预定义模式的输入参数的响应。

3.应用程序负责根据工具名称识别对应工具,并使用提供的输入参数执行该工具。

4.工具调用的结果由应用程序进行处理。

5.应用程序将工具调用结果返回至模型。

6. 模型最终利用工具调用结果作为附加上下文生成响应

执行链路

了解了大概步骤,我们来跟踪解析一下Spring AI框架的内部代码,主要分为2个视角:

注册时:即调用ChatClient的tools方法时,框架会将对象交给MethodToolCallbackProvider,由其对该对象进行扫描,识别出被Tool注解标记的方法,并将其转换为ToolCallback列表。

调用时:即调用ChatClient的call方法时(实际为获取结果时),会调用对应底层ChatModel的call方法,此时可以从请求前和请求后来分析代码:

请求前:在ChatModel的createRequest方法中,获取每个ToolCallback中的ToolDefinition(工具定义),并加入到请求参数中。

请求后:ToolExecutionEligibilityPredicate.isToolExecutionRequired方法根据大模型的响应确定是否需要工具调用;若需要,ToolCallingManager.executeToolCalls则根据大模型响应中的ToolCall(包括工具名和调用参数)找到对应的ToolCallback对象,调用其call方法;调用完后,判断是否直接返回,若不直接返回,则将工具响应追加到提示词中并发送给大模型。

相关接口及实现

ToolDefinition:工具定义,包含名称、描述、入参格式等。

ToolMetadata:工具元数据,包含是否直接返回等信息。

ToolExecutionEligibilityPredicate:判断是否需要调用工具(AssistantMessage中的toolCalls不为空)

ps:FunctionCallback已过时,推荐使用ToolCallback

八、聊天记忆

大模型本身是没有记忆的,虽然我们在使用ChatGPT或者Deepseek等应用时,大模型仿佛存在记忆,但那是应用的封装,单纯调用api时,大模型并没有对聊天历史的记忆,所以要使大模型能够记忆聊天历史,需要我们调用时进行额外处理。

package com.example.spring_ai.controller;
 
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class SpringAi08ChatMemory
{
 
    private final ChatClient chatClient;
 
    public SpringAi08ChatMemory(ChatClient.Builder chatClientBuilder)
    {
        ChatMemory chatMemory = MessageWindowChatMemory.builder().build();
 
        this.chatClient = chatClientBuilder.defaultAdvisors(
                new SimpleLoggerAdvisor(),
                MessageChatMemoryAdvisor.builder(chatMemory).build()
        ).build();
    }
 
    /**
     * 聊天记忆
     * @param userInput
     * @param userId 用户id
     * @return
     */
    @GetMapping("/ai/memory")
    String memory(String userInput, String userId)
    {
        return this.chatClient.prompt()
                .user(userInput)
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, userId))
                .call()
                .content();
    }
 
 
}

在这个例子中,使用聊天记忆分为三步:

  1. 创建ChatMemory
  2. 在创建ChatClient的时候,配置聊天记忆的Advisor
  3. 在发送请求时,加入一个能唯一标识当前会话的id,这里方便起见用的用户ID

聊天记忆的原理

在系统中,为每一个对话都存储了一份历史聊天记忆

请求前:

取出历史聊天记忆,并将最新请求追加在末尾,一起发送给大模型

请求后:

将大模型的响应加入到历史聊天记忆的末尾

聊天记忆相关接口及类

BaseChatMemoryAdvisor

|--ChatMemory

|--ChatMemoryRepository

像前面原理中提到的,聊天记忆相关接口分为三层:

BaseChatMemoryAdvisor:负责在请求前和请求后追加对话内容,内部包含一个ChatMemory

ChatMemory:该层负责实现聊天记忆的存储策略,例如最大支持n条,或者最大支持n个token等等。内部包含一个ChatMemoryRepository

ChatMemoryRepository:该层负责消息的实际存储,例如保存在内存中,或者数据库中等。

Spring AI内置的聊天记忆实现

这里同样分层来看:

ChatMemoryRepository:存储方式默认实现为InMemoryChatMemoryRepository,同时支持cassandra、neo4j和关系型数据库jdbc类的数据库存储。

ChatMemory:目前只有MessageWindowChatMemory,其维护固定容量的消息(默认 20 条)

BaseChatMemoryAdvisor:目前有MessageChatMemoryAdvisor、PromptChatMemoryAdvisor、VectorStoreChatMemoryAdvisor三种,前两者的区别在于PromptChatMemoryAdvisor是将历史对话以文本的形式追加到SystemMessage的末尾,而MessageChatMemoryAdvisor是将历史对话以消息列表的形式追加到提示词的Messages中。VectorStoreChatMemoryAdvisor是将历史对话存储到向量数据库中,每次请求前搜索语义相近的k条历史记录(默认20),并以文本的形式追加到SystemMessage末尾。

其他框架的相关功能

聊天记忆摘要:为了压缩对话,减少聊天历史占用token数量,将历史聊天记录压缩为聊天摘要

聊天记忆摘要窗口:为了压缩对话同时又不错过最近的对话历史,保留最近n条对话,同时将n条以外的对话历史压缩为聊天摘要

此外还有知识图谱聊天记忆、实体聊天记忆、根据最大token(而不是对话条数)维护的记忆等。

九、向量数据库

向量数据库是AI系统中常用的工具,主要用来存储文档片段及进行语义相似度查找,与传统数据库不同,它执行的是相似度查找而不是精确匹配。

首先在application.properties中,根据所用Embedding模型,添加一个嵌入式模型型号的配置

spring.ai.zhipuai.embedding.options.model=embedding-3

然后需要声明一个VectorStore的bean,需要传入的EmbeddingModel在自动化配置包中已经声明过了

@Configuration
public class Config {
 
    @Bean
    public VectorStore vectorStore(EmbeddingModel embeddingModel) {
        return SimpleVectorStore.builder(embeddingModel).build();
    }
}

之后就可以装配使用了,样例中使用add方法添加了一个Document,然后使用similaritySearch进行语义相近搜索,并返回结果文本

    @Autowired
    VectorStore vectorStore;
 
    void save(){
        //目前仅提供JsonReader和TextReader
        Document document = new Document("kitty is a cat");
        vectorStore.add(List.of(document));
    }
 
    @GetMapping("/ai/vector")
    String generation(String userInput)
    {
        save();
        return this.vectorStore.similaritySearch(userInput).get(0).getText();
    }

在实际生产中,一般分为两步使用:

数据预处理:将信息资料(比如一些规章制度或其他领域的文件,包括docx、pdf等格式)加载到VectorStore中

处理用户查询:搜索与用户问题语义相近的片段,之后一起传递给大模型,由大模型综合搜索结果进行回答

作用原理

VectorStore中内置一个嵌入式大模型,数据预处理阶段加载到向量数据库中的每个文档片段,会由EmbeddingModel加工为一个浮点数数组(即多维向量)

在处理用户查询时,EmbeddingModel同样先将用户查询的文本加工为一个多维向量,然后向量数据库将其与库内每个多维向量进行匹配,得到相似度数值,相似度越接近0越不相关,越接近1越相关。

相关接口及实现

VectorStore接口:向量数据库接口,主要方法即add和similaritySearch,分别对应数据预处理和相似性查找

AbstractObservationVectorStore:VectorStore的抽象实现类,内部包含EmbeddingModel和BatchingStrategy,分别用来将Document向量化和Document分组,同时负责可观测性Observation相关的处理

SimpleVectorStore:一个简单的向量数据库实现,不建议用在生产,适合用来进行教学,内部实现了一个EmbeddingMath,用来计算两个向量的相似度

Document:相似性查找的结果对象,文档或文档片段,内部包含id、文档文本、得分等属性

SearchRequest:相似性查找请求对象,内部包含请求文本query,结果最大条数topK,最低相似度阈值similarityThreshold,筛选表达式filterExpression等

BatchingStrategy接口:文档分批策略接口,提供batch方法,将一系列文本按单次最大可处理token数划分批次,适合在进行文档向量化时使用,防止批量将文档片段向量化时超出嵌入式模型的最大token数

TokenCountBatchingStrategy:按token个数对Document进行分组

其他框架的相关功能

Spring AI的VectorStore实际已经涉及了RAG(检索增强生成)的各个方面,例如文档切分、向量化、存储、相似性查找等功能。在这些环节,有许多可以提高最终效果的优化方案,例如langchain的切分策略,除了按token个数进行切分,还可以按字符切分,比如按【换行符,句号,分号,逗号】的顺序对文档切分,这样能更好的保持语义完整性。而Spring AI由于起步较晚,此类扩展相对还比较少。

十、RAG检索增强生成

RAG其实就是通过外挂知识库,来解决大模型无法获取最新知识或某专业领域知识的问题,也就是通过检索外部数据源,来增强大模型生成的回答

作用原理

具体来说,RAG分为三步:

1)数据预处理:

准备外部数据源,可以使用向量数据库,也可以使用传统数据库甚至搜索引擎作为外部数据源。由于向量数据库相似性查找的特性,一般更多使用向量数据库作为外部数据源。

加载:首先将docx、pdf等格式的规章制度、专业领域文件等文档加载到系统中

切分:为了方便查找和减少token消耗,一般将各文档按一定大小进行切分

向量化:将切分后的文档片段使用嵌入式大模型转换为向量

保存:然后将向量与文档片段一起存入向量数据库

2)检索

向量化:将用户的输入转换为向量

相似性查找:在向量数据库中进行相似性查找,得到与用户输入相关的文档片段。

3)生成:

将用户输入与检索到的文档片段一起发送给大模型,大模型会根据其自身知识和文档片段,生成最终的回答。

RAG相关包的引入

<dependency>
	<groupId>org.springframework.ai</groupId>
	<artifactId>spring-ai-rag</artifactId>
</dependency>

数据预处理

TextReader textReader = new TextReader("src/main/resources/data/rag.txt");
TokenTextSplitter tokenTextSplitter = TokenTextSplitter.builder().build();
vectorStore.write(tokenTextSplitter.split(textReader.read()));

reader负责加载,splitter负责切分,vectorStore中的EmbeddingModel负责向量化,最终存入vectorStore中

问答RAG

Spring AI中有一个简单的问答系统的实现,QuestionAnsweiAdvisor,就是一个封装好的简单易用的RAG

    @Autowired
    VectorStore vectorStore;
 
    @GetMapping("/ai/rag")
    String generation(String userInput)
    {
        return this.chatClient.prompt()
                .user(userInput)
                .advisors(new QuestionAnswerAdvisor(vectorStore))
                .call()
                .content();
    }

只需提前准备好VectorStore,并在VectorStore中保存好相关领域的知识,将其传入QuestionAnswerAdvisor,即可通过QuestionAnswerAdvisor进行相关知识的问答

QuestionAnswerAdvisor会在调用大模型前,在VectorStore中查找相关文档,并将其融入提示词中,最终一起发送给大模型

数据预处理相关接口及实现类

检索增强

QuestionAnswerAdvisor能够使我们快速上手RAG,即开即用,但是其缺点也源于此,由于其对RAG内部各环节进行了封装,导致其扩展性并不好,不方便针对各个环节进行优化,SpringAI提供了另一个Advisor,RetrievalAugmentationAdvisor,可以对检索和生成阶段的环节进行优化。

Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
                .queryTransformers(...)
                .queryExpander(...)
                .documentRetriever(...)
                .documentJoiner(...)
                .documentPostProcessors(...)
                .queryAugmenter(...)
                .build();
 
String answer = chatClient.prompt()
        .advisors(retrievalAugmentationAdvisor)
        .user(question)
        .call()
        .content();
RetrievalAugmentationAdvisor相关接口及实现类

在调用大模型前,RetrievalAugmentationAdvisor会按照上述接口顺序依次调用相关功能,对相应环节进行优化

其中,使用QueryTransformer和QueryExpander时会调用大模型进行转换或扩展查询,此时建议将大模型温度设为0,减少大模型的自由发挥

优化方案

针对不同的具体环节,有许多不同的优化方案,来进一步提高回答的质量

文档切分环节:尽量保持语义的完整性

1)递归按字符切分,切分字符为【换行符,句号,问号,叹号,分号,逗号等】,首先按第一个字符进行切分,若仍然有文档片段大于最大长度时,继续用第二个字符切分,直到所有文档片段长度都满足要求。

2)允许切分时文档片段有部分重叠

3)切分后的文档最前面添加文章标题或段落标题

向量化环节:建议对多种嵌入式大模型进行测试,需要关注的指标有是否支持中文、向量维度、最大token数、词汇表大小等。

文档入库环节:

1)2层检索:对所有文档总结摘要信息,每次检索时先检索摘要,找到相关文档,然后再检索该文档切分的文档片段

检索环节:

1)检索到相似文档片段时,不仅返回命中的结果片段,同时返回其相邻上下文片段

2)结果压缩,去除结果片段中与用户查询不相关的内容

3)自适应检索策略:根据问题类型不同(事实性、分析性、观点性、上下文性)采用不同的检索策略

4)根据用户查询来决定是否需要进行检索

5)融合检索:采用相似度检索和其他检索方式(如全文检索)相融合的方式进行检索

6)重排序:将检索到的文档片段按相关性大小排序,优先返回最相关的文档片段

大模型调用环节:

1)查询转换:将用户冗长、含歧义、包含无关信息、需要结合聊天历史理解的查询进行重写

2)查询扩展:将单个复杂查询扩展为多个简单查询

十一、MCP模型上下文协议

MCP全称是模型上下文协议,通俗点理解,如果说tool是支持调用系统内部的工具,那么mcp就是支持调用系统外部的工具,比如高德地图、百度搜索等。

为了方便进行外部调用,就需要一个类似http协议的规范,来约定调用方的报文格式和被调用方如何解析报文,这就是mcp的由来。今天我们主要看如何使用MCP Client,即如何调用外部应用。

MCP Client

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
</dependency>

客户端需引入上述2个包之一,webflux包是基于SSE的WebFlux传输,另一个则是STDIO和基于SSE的HTTP。生产环境建议使用webflux。然后需要在application文件中添加如下配置(SSE)

spring:
  ai:
    mcp:
      client:
        sse:
          connections:
            server1:
              url: http://localhost:8080
            server2:
              url: http://otherserver:8081
              sse-endpoint: /custom-sse

其中server1、server2是自定义的服务名称,可自行修改,然后就可以在代码中使用了

    @Autowired
    ToolCallbackProvider toolCallbackProvider;
 
    @GetMapping("/ai/mcp-client")
    String generation(String userInput)
    {
        return this.chatClient.prompt()
                .user(userInput)
                .toolCallbacks(toolCallbackProvider)
                .call()
                .content();
    }

Spring AI会自动将配置文件中的url组装成ToolCallbackProvider,将其传递给大模型即可。

内部原理及相关接口和实现类

  1. 以基于SSE的Http为例,在spring-ai中,自动化配置会进行下列操作:
  2. 用户添加在application中的spring.ai.mcp.client.sse配置会首先被加载进McpSseClientProperties中
  3. 然后每个connection(样例中的server1、server2)会创建一个HttpClientSseClientTransport来负责通信
  4. 然后每个HttpClientSseClientTransport与其命名一起,被封装为NamedClientMcpTransport
  5. 然后NamedClientMcpTransport会与spring.ai.mcp.client下的其他通用配置,根据Type不同(Sync、Async)组成McpSyncClient或McpAsyncClient,创建完成后会向服务器发送请求initialize,初始化服务器相关参数(服务器能力介绍、是否支持tool,resource,prompt等)
  6. 最后所有McpSyncClient组成SyncMcpToolCallbackProvider
  7. SyncMcpToolCallbackProvider负责将McpSyncClient转换为SyncMcpToolCallback,期间会向服务器发送tools/list请求,获取工具详细信息(描述、请求格式等)之后就是在tool篇提到的,将工具定义发送给大模型等
  8. 之后就是在tool篇提到的,将工具定义发送给大模型等

MCP server

与使用Mcp Client一样,分为三步,引入依赖,添加配置,修改代码

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>

根据自身需要引入上述三个依赖之一,分别对应STDIO、WebFlux、Http模式,以http为例,在application文件中添加如下配置

# Using spring-ai-starter-mcp-server-webmvc
spring:
  ai:
    mcp:
      server:
        name: webmvc-mcp-server
        version: 1.0.0
        type: SYNC
        instructions: "This server provides weather information tools and resources"
        sse-message-endpoint: /mcp/messages
        capabilities: 
          tool: true       #是否开启工具
          resource: true   #是否开启资源
          prompt: true     #是否开启提示词
          completion: true #是否开启补全

instructions用来介绍当前服务器提供的功能然后将想要暴露的服务上添加Tool注解

@Service
public class WeatherService {
 
    @Tool(description = "Get weather information by city name")
    public String getWeather(String cityName) {
        // Implementation
    }
}

之后将服务注册为ToolCallbackProvider

@SpringBootApplication
public class McpServerApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(McpServerApplication.class, args);
    }
 
	@Bean
	public ToolCallbackProvider weatherTools(WeatherService weatherService) {
		return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
	}
}

然后就可以通过MCP Client进行调用了

内部原理

  1. 以http为例,Spring AI首先会将spring.ai.mcp.server相关配置注入McpServerProperties
  2. 然后根据McpServerProperties创建WebMvcSseServerTransportProvider,负责创建并管理会话
  3. 之后根据WebMvcSseServerTransportProvider、注册的ToolCallbackProvider等组件,创建McpSyncServer
  4. McpSyncServer内部包含了服务器信息(如服务器介绍、支持的能力等)和具体的能力(如工具列表),在构造函数中,声明了一系列的请求及其处理器(例如工具相关请求tool/list和tool/call)
  5. 每当WebMvcSseServerTransportProvider收到会话请求,就会创建一个McpServerSession,并将McpSyncServer中声明的请求及处理器交给McpServerSession
  6. McpServerSession根据请求路径选择相应的处理器进行处理

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐