⬆︎
×

SpringAI大模型应用开发教程

Java大模型应用开发框架——Spring AI。注意教程时效性,多查阅官方文档获取最新用法,及时反馈更新!

Spring AI官方文档:docs.spring.io/spring-ai/reference
本文案例仓库:github.com/hyperplasma/hyproj-ai

1 模型部署

大模型原理简介详见A Survey of Large Language Models

AI

GPT

各种部署方式的优缺点对比:

开放大模型API 云平台部署私有模型 本地部署私有模型
优点 没有部署和维护成本,按调用收费
简单
前期投入成本低
部署和维护方便
网络延迟较低
数据完全自主掌控,安全性高
不依赖外部环境
虽然短期投入大,但长期来看成本会更低
缺点 依赖平台方,稳定性差
长期使用成本较高
数据存储在第三方,有隐私和安全问题
数据存储在第三方,有隐私和安全问题
长期使用成本高
初期部署成本高
维护困难

1.1 开放大模型服务

通常发布大模型的官方、大多数的云平台都会提供开放的、公共的大模型服务。国内常见的大模型服务云平台如下:

云平台 公司 地址
阿里百炼 阿里巴巴 bailian.console.aliyun.com
腾讯TI平台 腾讯 cloud.tencent.com/product/ti
千帆平台 百度 console.bce.baidu.com/qianfan/overview
SiliconCloud 硅基流动 siliconflow.cn/zh-cn/siliconcloud
火山方舟-火山引擎 字节跳动 www.volcengine.com/product/ark
GPT-GOD - gptgod.online
... ... ...
KINA Hyperplasma kina.hyperplasma.top

一般流程:

  1. 注册账号
  2. 申请API_KEY
  3. 体验模型

1.2 本地部署

使用Ollama(官网:ollama.com)部署和运行大模型(相当于Docker)。

一般流程:

  1. 下载、安装Ollama,配置环境变量
  2. 搜索模型
  3. 运行模型

常用命令:

ollama serve    # Start ollama
ollama create   # Create a model from a Modelfile
ollama show # Show information for a model
ollama run  # Run a model
ollama stop # Stop a running model
ollama pull # Pull a model from a registry
ollama push # Push a model to a registry
ollama list # List models
ollama ps   # List running models
ollama cp   # Copy a model
ollama rm   # Remove a model
ollama help # Help about any command

2 大模型接口规范

大模型开发通过访问模型对外暴露的API接口,实现与大模型的交互。目前大多数大模型都遵循OpenAI的接口规范,均为基于Http协议的接口,故请求路径、参数、返回值信息都类似,具体细微差别请查阅各自的官方API文档。以DeepSeek官方给出的文档为例(Python):

# Please install OpenAI SDK first: `pip3 install openai`

from openai import OpenAI

# 1.初始化OpenAI客户端,要指定两个参数:api_key、base_url
client = OpenAI(api_key="<DeepSeek API Key>", base_url="https://api.deepseek.com")

# 2.发送http请求到大模型,参数比较多
response = client.chat.completions.create(
    model="deepseek-chat", # 2.1.选择要访问的模型
    messages=[ # 2.2.发送给大模型的消息
        {"role": "system", "content": "You are a helpful assistant"},
        {"role": "user", "content": "Hello"},
    ],
    stream=False # 2.3.是否以流式返回结果
)

print(response.choices[0].message.content)

接口说明:

  • 请求方式:通常为POST,因为要传递JSON风格的参数
  • 请求路径:视具体平台而定
    • DeepSeek官方平台:https://api.deepseek.com
    • 阿里云百炼平台:https://dashscope.aliyuncs.com/compatible-mode/v1
    • 本地ollama部署的模型:http://localhost:11434
    • GPT-GOD:https://api.gptgod.online/(或https://api.gptgod.online/v1https://api.gptgod.online/v1/chat/completions
    • ……
  • 安全校验:开放平台都需要提供API_KEY来校验权限,本地ollama则不需要
  • 请求参数:
    • model:要访问的模型名称
    • messages:发送给大模型的消息(提示词/指令,Prompt),一个消息数组(以此实现会话记忆),包含两个属性:
      • role:消息对应的角色,有如下三种:
        1. system:优先于user指令之前的指令,即给大模型设定角色和任务背景的系统指令
        2. user:终端用户输入的指令
        3. assistant:由大模型生成的消息,可能是上一轮对话生成的结果
      • content:消息内容
    • streamtrue表示响应结果流式返回;false表示响应结果一次性返回,但需要等待
    • temperature:取值范围[0, 2),代表大模型生成结果的随机性,越小随机性越低。(DeepSeek-R1不支持)
    • ……

3 大模型应用

大模型应用是基于大模型的推理、分析、生成能力,结合传统编程能力,开发出的各种应用。

3.1 传统应用与AI大模型

传统应用和AI大模型应用特点对比:

传统应用 AI大模型
核心特点 基于明确规则的逻辑设计,确定性执行,可预测结果 基于数据驱动的概率推理,擅长处理模糊性和不确定性
擅长领域 结构化计算(银行转账系统、Excel公式)
确定性任务(排序算法)
高性能低延迟场景(操作系统内核调度、数据库索引查询)
规则明确的流程控制(红绿灯信号切换系统)
自然语言处理(写作、翻译、客服机器人理解用户意图)
非结构化数据分析(医学影像识别、TTS)
创造性内容生成(图像生成、AI作曲)
复杂模式预测(股票市场趋势预测)
不擅长领域 非结构化数据处理(无法直接理解用户自然语言提问)
模糊推理与模式识别(判断一张图片是"猫"还是"狗")
动态适应性(用户需求频繁变化,例如电商促销规则每天调整)
精确计算
确定性逻辑验证(验证身份证号码是否符合规则)
低资源消耗场景(嵌入式设备)
因果推理("公鸡打鸣导致日出")
总结 适用于确定性、规则化、高性能,适合数学计算、流程控制等场景 适用于概率性、非结构化、泛化性,适合语言、图像、创造性任务

两者恰好互补,强强联合能够解决以前难以实现的一些问题:

  • 混合系统(Hybrid AI):用传统程序处理结构化逻辑(如支付校验),AI处理非结构化任务(如用户意图识别)。
    • 示例:智能客服中,AI理解用户问题,传统代码调用数据库返回结果。
  • 增强可解释性:结合规则引擎约束AI输出(如法律文档生成时强制符合条款格式)。
  • 低代码/无代码平台:通过AI自动生成部分代码,降低传统开发门槛。

综上所述,大模型应用就是整合传统程序和大模型的能力和优势来开发的一种应用。

大模型应用

3.2 大模型应用开发技术架构

基于大模型开发应用有多种方式,大模型应用开发的技术架构主要有四种:

大模型应用开发架构

从开发成本由低到高来看,四种方案排序为:Prompt < Function Calling < RAG < Fine-tuning

在选择技术时通常遵循"在达成目标效果的前提下,尽量降低开发成本"这一首要原则,然后可参考以下流程来思考:

技术选型

3.2.1 纯Prompt模式

不断雕琢提示词,使大模型能给出最理想的答案的过程称为提示词工程(Prompt Engineering):通过优化提示词,使大模型生成出尽可能理想的内容。

很多简单的AI应用,仅仅靠一段足够好的提示词即可实现,这就是纯Prompt模式

提示词工程可分为如下6点:

  1. 清晰明确的指令
  2. 使用分隔符标记输入
  3. 按步骤拆解复杂任务
  4. 提供输入输出示例
  5. 明确要求输出格式
  6. 给模型设定一个角色

特点:利用大模型推理能力完成应用的核心功能。

纯Prompt模式

应用场景:文本摘要分析、舆情分析、坐席检查、AI对话……

3.2.2 Function Calling

特点:将应用端业务能力与AI大模型推理能力相结合,简化复杂业务开发。

可分为以下步骤:

  1. 将传统应用中的部分功能封装成若干函数(Function)。
  2. 构建智能体(Agent):在提示词中描述用户的需求,并且描述清楚每个函数的作用,要求AI理解用户意图,判断何时需要调用(Call)哪个函数,并将任务拆解为多个步骤。
  3. 当AI执行到某一步需要调用某个函数时,返回要调用的函数名称、函数需要的参数信息。
  4. 传统应用接收到这些数据后调用本地函数,再将函数执行结果封装为提示词,再次发送给AI。
  5. 以此类推,逐步执行,直至达成最终结果。

Function Calling

应用场景:旅行指南、数据提取、数据聚合分析、课程顾问……

3.2.3 RAG

检索增强生成(Retrieval-Augmented Generation,RAG)为把信息检索技术和大模型结合的方案。

大模型从知识角度存在很多限制:

  • 时效性差:大模型训练比较耗时,其训练数据都是旧数据,无法实时更新
  • 缺少专业领域知识:大模型训练数据都是采集的通用数据,缺少专业数据

RAG利用信息检索技术来拓展大模型的知识库,解决大模型的知识限制,总体上可分为两个模块:

  1. 检索模块(Retrieval):负责存储和检索拓展的知识库
    • 文本拆分:将文本按照某种规则拆分为很多片段
    • 文本嵌入(Embedding):根据文本片段内容,将文本片段归类存储
    • 文本检索:根据用户提问的问题,找出最相关的文本片段
  2. 生成模块(Generation):
    • 组合提示词:将检索到的片段与用户提问组织成提示词,形成更丰富的上下文信息
    • 生成结果:调用生成式模型,根据提示词生成更准确的回答

离线步骤:文档加载 → 文档切分 → 文档编码 → 写入知识库

在线步骤:获得用户问题 → 检索知识库中相关知识片段 → 将检索结果和用户问题填入Prompt模板 → 用最终获得的Prompt调用LLM → 由LLM生成回复

RAG

应用场景:个人知识库、AI客服助手……

3.2.4 Fine-tuning

Fine-tuning(模型微调)是在预训练大模型的基础上,通过企业自己的数据做进一步的训练,使大模型的回答更符合自己企业的业务需求。该过程通常需要在模型的参数上进行细微的修改,以达到最佳的性能表现。

在进行微调时,通常会保留模型的大部分结构和参数,只对其中的一小部分进行调整。这样做的好处是可以利用预训练模型已经学习到的知识,同时减少了训练时间和计算资源的消耗。微调的过程包括以下几个关键步骤:

  1. 选择合适的预训练模型:根据任务的需求,选择一个已经在大量数据上进行过预训练的模型。
  2. 准备特定领域的数据集:收集和准备与任务相关的数据集,这些数据将用于微调模型。
  3. 设置超参数:调整学习率、批次大小、训练轮次等超参数,以确保模型能够有效学习新任务的特征。
  4. 训练和优化:使用特定任务的数据对模型进行训练,通过前向传播、损失计算、反向传播和权重更新等步骤,不断优化模型的性能。

模型微调虽然更加灵活、强大,但也存在一些问题:

  • 需要大量的计算资源
  • 调参复杂性高
  • 过拟合风险

故Fine-tuning成本较高,难度较大,并不适合大多数企业。


4 Spring AI

目前Java平台常用的大模型框架有Spring AILangChain4j,注意Spring AI要求JDK版本至少为17(LangChain4j则为JDK 8),但LangChain4j暂不支持DeepSeek,故请按实际要求选用。

本文通过4个大模型应用案例介绍Spring AI用法。

本文案例仓库:github.com/hyperplasma/hyproj-ai

4.1 对话机器人

4.1.1 快速入门

初始化Spring AI项目的一般步骤:

  1. 引入和管理依赖和起步依赖(此处为Ollama平台,可换为其他平台,例如想使用OpenAI则将<artifactId>ollama改为openai):
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-bom</artifactId>
    <version>${spring-ai.version}</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
<!-- Ollama -->
<dependency>
        <groupId>org.springframework.ai</groupId>
        <!-- <artifactId>spring-ai-ollama-spring-boot-starter</artifactId> -->
        <artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
<!-- OpenAI -->
<dependency>
        <groupId>org.springframework.ai</groupId>
        <!-- <artifactId>spring-ai-openai-spring-boot-starter</artifactId> -->
        <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

可在新建项目时直接选定以下依赖:Web(Spring Web)、SQL(MySQL Driver)、AI(Ollama、OpenAI)。注意用此种方式引入的Lombok存在bug,请手动在pom.xml中引入。

  1. 配置模型,其中api-key需添加到环境变量中:点击工具栏启动按钮左侧下拉菜单中的Edit Configurations...,点击Modify options后选择Environment variables,即可添加OPENAI_API_KEY=xxx环境变量。
spring:
    application:
    name: hyproj-ai
    ai:
#       ollama:
#           base-url: http://localhost:11434
#           chat:
#               model: deepseek-r1:7b
        openai:
            api-key: ${OPENAI_API_KEY}
            base-url: https://api.gptgod.online/
            chat:
                options:
                    model: gpt-4o-mini
                    temperature: 0.7

Spring AI会自动补全base-url/v1/completions部分,故无需在配置文件中手动编写。

  1. 配置客户端:自动装配ChatClient
/* CommonConfiguration.java */
@Configuration
public class CommonConfiguration {
    @Bean   // 根据所引依赖自动装配模型
    public ChatClient chatClient(OpenAiChatModel model) {
        return ChatClient.builder(model)
                    .defaultSystem("你是Hyperplasma的热心可爱的AI助手,名为KINA,请你以友好、热情的语气回答用户的问题。")
                    .build();
    }
}

要实现的相关接口如下:

请求方式 请求路径 返回值
会话 GET /ai/chat?prompt=用户信息 Flux<String>

定义Controller来接收前端消息,调用模型:

/* ChatController.java */
// 发送消息
@RequestMapping("/chat")
public String chat(String prompt) {
    return chatClient.prompt()
            .user(prompt)
            .call()
            .content();
}
/* ChatController.java */
// 使用stream()方法流式调用,返回结果为Flux(响应式流式技术)
// 需指定编码
@RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
public Flux<String> chat(String prompt) {
    return chatClient.prompt()
            .user(prompt)
            .stream()
            .content();
}

当与前端对接时,可能会发生跨域错误(端口不同),可在MVC配置类中进行相关的配置:

/* MvcConfiguration.java */
@Configuration
public class MvcConfiguration implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*") // 允许所有来源
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的HTTP方法
                .allowedHeaders("*"); // 允许所有请求头
    }
}

4.1.2 会话日志

Spring AI利用AOP原理提供了AI会话时的拦截、增强等功能,即Advisor。该接口有3个实现类,分别为SimpleLoggerAdvisorMessageChatMemoryAdvisorQuestionAnswerAdvisor

Advisor

直接在配置类中装配模型时配置默认Advisors即可。.defaultAdvisors(new SimpleLoggerAdvisor())可实现会话日志的输出,此外还需在配置文件中添加指定包日志级别的配置:

logging:
    level:
        # 给指定包添加日志输出
        # org.springframework.ai.chat.client.advisor: debug
        org.springframework.ai: debug
        top.hyperplasma.hyprojai: debug

此后进行调用时控制台即会显示debug日志。

4.1.3 会话记忆

Spring AI提供了标准的会话记忆接口ChatMemory,部分源码如下,可通过编写其实现类来实现各种数据库(MySQL、MongoDB等)的持久化操作:

public interface ChatMemory {
    void add(String conversationId, List<Message> messages);

    List<Message> get(String conversationId);

    void clear(String conversationId);
}

改进后的会话接口如下(已改为使用POST请求发送请求体,详见本项目仓库):

请求方式 请求路径 返回值
带记忆的会话 GET /ai/chat?chatId=2&prompt=用户信息 Flux<String>

以下为实现会话记忆的一般流程:

  1. 定义会话存储方式:Spring AI会自动配置一个ChatMemory Bean,可以在应用程序中直接使用。默认情况下,它使用内存存储库来存储消息(InMemoryChatMemoryRepository),并使用一个MessagewindowChatMemory实现来管理对话历史记录。但为了实现对话历史存储,请按如下方式编写装配函数(注意与旧版区别,时刻关注官方文档变更):
/* CommonConfiguration.java */
@Bean
public ChatMemory chatMemory(ChatMemoryRepository repository) {
    return MessageWindowChatMemory.builder()
        .chatMemoryRepository(repository)
        .maxMessages(10)
        .build();
}
  1. 配置会话记忆Advisor(注意与旧版的区别):
/* CommonConfiguration.java */
@Bean
public ChatClient chatClient(OpenAiChatModel model, ChatMemory chatMemory) {
    return ChatClient.builder(model)
        .defaultSystem("你是Hyperplasma的热心可爱的AI助手,名为KINA,请你以友好、热情的语气回答用户的问题。")
        .defaultAdvisors(
            new SimpleLoggerAdvisor(),
            MessageChatMemoryAdvisor.builder(chatMemory).build()
        )
        .build();
}
  1. 环绕增强,添加会话ID(前端传入chatId)到AdvisorContext上下文中
@RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
public Flux<String> chat(String prompt, String chatId) {
    return chatClient.prompt()
        .user(prompt)
        .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId))
        .stream()
        .content();
}

4.1.4 会话历史

要实现的相关接口如下:

查询会话记录列表 查询会话记录详情
请求方式 GET GET
请求路径 /ai/history/{type} /ai/history/{type}/{chatId}
请求参数 type:业务类型 type:业务类型
chatId:会话ID
返回值 ["1241", "1246", "1248"] [{role:"user",content:""}]
  1. 新建ChatHistoryRepository接口及其实现类InMemoryChatHistoryRepository,如下所示:
/* ChatHistoryRepository.java */
public interface ChatHistoryRepository {
    /**
     * 保存会话记录
     * @param type 业务类型,如chat、service、pdf
     * @param chatId 会话ID
     */
    void save(String type, String chatId);

    /**
     * 获取指定类型的会话ID列表
     * @param type 业务类型,如chat、service、pdf
     * @return 会话ID列表
     */
    List<String> getChatIds(String type);
}
/* InMemoryChatHistoryRepository.java */
@Component
public class InMemoryChatHistoryRepository implements ChatHistoryRepository {

    private final Map<String, List<String>> chatHistory = new HashMap<>();

    @Override
    public void save(String type, String chatId) {
        // if (!chatHistory.containsKey(type)) {
        //     chatHistory.put(type, new ArrayList<>());
        // }
        // List<String> chatIds = chatHistory.get(type);
        List<String> chatIds = chatHistory.computeIfAbsent(type, k -> new ArrayList<>());
        if (!chatIds.contains(chatId)) {
            chatIds.add(chatId);
        }
    }

    @Override
    public List<String> getChatIds(String type) {
        // List<String> chatIds = chatHistory.get(type);
        // return chatIds == null ? new ArrayList<>() : chatIds;
        return chatHistory.getOrDefault(type, new ArrayList<>());
    }
}
  1. 修改、新建Controller
/* ChatController.java */
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {

    private final ChatClient chatClient;

    private final ChatHistoryRepository chatHistoryRepository;

    @RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
    public Flux<String> chat(String prompt, String chatId) {
        // 保存会话ID
        chatHistoryRepository.save(ServiceType.CHAT, chatId);
        // 请求模型
        return chatClient.prompt()
                .user(prompt)
                .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId))
                .stream()
                .content();
    }
}
/* ChatHistoryController.java */
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/history")
public class ChatHistoryController {

    private final ChatHistoryRepository chatHistoryRepository;

    private final ChatMemory chatMemory;

    @GetMapping("/{type}")
    public List<String> getChatIds(@PathVariable("type") String type) {
        return chatHistoryRepository.getChatIds(type);
    }

    @GetMapping("/{type}/{chatId}")
    public List<MessageVO> getChatHistory(@PathVariable("type") String type, @PathVariable("chatId") String chatId) {
        List<Message> messages = chatMemory.get(chatId);
        // System.out.println("获取到的消息:" + messages);
        return messages.stream().map(MessageVO::new).collect(Collectors.toList());
    }
}

改进:在用户点击新建对话时后端生成、保存会话ID,并发送给前端,接口为/ai/chat/connect。最终的ChatController如下所示:

/* ChatController.java */
// 新建任意类型会话时,生成会话ID并返回
@GetMapping("/connect")
public String connect(String type) {
    // 生成会话ID
    String chatId = "chat-" + System.currentTimeMillis();
    // 保存会话ID
    chatHistoryRepository.save(type, chatId);
    return chatId;
}

@RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
public Flux<String> chat(String prompt, String chatId) {
    // 保存会话ID(Depicted)
    // chatHistoryRepository.save(ServiceType.CHAT, chatId);
    // 请求模型
    return chatClient.prompt()
        .user(prompt)
        .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId))
        .stream()
        .content();
}

4.2 智能客服:Tool Calling

需求:为Hyperplasma实现一个24小时在线的AI智能客服KINA-24,可以为学员咨询Hyperplasma的课程资源,帮用户预约线下课程试听。

流程:了解、分析用户兴趣学历信息 → 查询满足用户条件的教程 → 给用户推荐教程引导用户试看教程 → 查询线下教程阅览所信息 → 引导学生留下联系方式 → 新增预约单。

其中紫色操作适合交给大模型,其余操作应定义为一系列函数(queryLesson()querySection()saveOrder())由传统程序完成。

项目中为了方便,使用SQLite数据库存储课程等数据,可改用其他JDBC支持的数据库,只需修改application.yamlspring.datasource 部分,无需变动源码。

Spring AI提供了强大的Tool Calling来简化函数调用的操作(内部通过反射获取工具的信息):

tool calling

实现Tool Calling的一般步骤如下:

  1. 选用支持Tool Calling的模型,例如通义千问base-urlhttps://dashscope.aliyuncs.com/compatible-mode)的qwen-max-latest模型。然后编写System提示词
  2. 定义Tool:使用@Tool@ToolParam注解指定description
/* CourseQuery.java */
@Data
public class CourseQuery {
    @ToolParam(required = false, description = "课程类型:编程、设计、自媒体、其他")
    private String type;
    @ToolParam(required = false, description = "学历要求:0-无,1-初中,2-高中,3-大专,4-本科,5-硕士,6-博士")
    private Integer edu;
    @ToolParam(required = false, description = "排序方式")
    private List<Sort> sorts;

    @Data
    public static class Sort {
        @ToolParam(required = false, description = "排序字段:price或duration")
        private String field;
        @ToolParam(required = false, description = "是否是升序:true-升序,false-降序")
        private boolean asc;
    }
}
/* CourseTools.java */
@RequiredArgsConstructor
@Component
public class CourseTools {

    private final ICourseService courseService;
    private final ISchoolService schoolService;
    private final ICourseReservationService courseReservationService;

    @Tool(description = "根据条件查询课程")
    public List<Course> queryCourse(@ToolParam(description = "查询条件", required = false) CourseQuery query) {
        if (query == null) {
            // return List.of();
            return courseService.list();
        }
        QueryChainWrapper<Course> wrapper = courseService.query()
                .eq(query.getType() != null, "type", query.getType())
                .le(query.getEdu() != null, "edu", query.getEdu());
        if (query.getSorts() != null && !query.getSorts().isEmpty()) {
            for (CourseQuery.Sort sort : query.getSorts()) {
                wrapper.orderBy(true, sort.getAsc(), sort.getField());
            }
        }
        return wrapper.list();
    }

    @Tool(description = "查询所有线下教程阅览所")
    public List<School> querySchool() {
        return schoolService.list();
    }

    @Tool(description = "生成预约单,返回预约单号")
    public Integer createCourseReservation(
            @ToolParam(description = "预约课程") String course,
            @ToolParam(description = "预约的线下教程阅览所") String school,
            @ToolParam(description = "学生姓名") String studentName,
            @ToolParam(description = "联系电话") String contactInfo,
            @ToolParam(description = "备注", required = false) String remark) {
        CourseReservation reservation = new CourseReservation();
        reservation.setCourse(course);
        reservation.setSchool(school);
        reservation.setStudentName(studentName);
        reservation.setContactInfo(contactInfo);
        reservation.setRemark(remark);
        courseReservationService.save(reservation);
        return reservation.getId();
    }
}
  1. 配置Tool:设置.defaultTools()
/* CommonConfiguration.java */
@Bean
public ChatClient serviceChatClient(OpenAiChatModel model, ChatMemory serviceChatMemory, CourseTools courseTools) {
    return ChatClient.builder(model)
        .defaultSystem(SystemConstants.TERESA_GAME_SYSTEM_PROMPT)
        .defaultAdvisors(
            new SimpleLoggerAdvisor(),
            MessageChatMemoryAdvisor.builder(serviceChatMemory).build()
        )
        .defaultTools(courseTools)
        .build();
}
  1. 编写CustomerServiceController
/* CustomerServiceController.java */
@RequestMapping(value = "/service", produces = "text/html;charset=utf-8")
public Flux<String> chat(@RequestBody UserPromptDTO userPromptDTO) {
    String prompt = userPromptDTO.getPrompt();
    String chatId = userPromptDTO.getChatId();
    // Depicted - 保存会话ID
    // chatHistoryRepository.save(ServiceType.SERVICE, chatId);
    // 请求模型
    return serviceChatClient.prompt()
        .user(prompt)
        .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId))
        .stream()
        .content();
}

曾经SpringAI的OpenAI客户端与阿里云百炼存在兼容性问题,无法直接使用Flux流式输出数据,只能暂时改为String返回,或者自定义ChatModel的实现。目前问题已经解决。

4.3 ChatPDF知识库:RAG

需求:模仿chatpdf.com,实现个人知识库功能

功能列表:

  • 文件上传并导入向量库
  • 文件下载

由于训练大模型非常耗时,并且训练语料本身比较滞后,所以大模型存在知识限制问题:

  • 知识数据比较落后,往往是几个月之前的。
  • 不包含太过专业领域或者企业私有的数据。

RAG可以解决这些问题:外挂向量数据库

4.3.1 向量模型

通过计算两个向量之同的距离,可以判断向量相似度:欧氏距离越小,相似度越高;余弦距离越大,相似度越高。

向量相似度

向量模型:将文档向量化,保证内容越相似的文本,在向量空间中距离越近。

具体使用步骤如下:

  1. 配置向量模型:
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      base-url: https://api.gptgod.online/
      chat:
        options:
          model: gpt-4o-mini
          temperature: 0.7
      embedding:
        options:
          model: text-embedding-3-large # 向量模型
          dimensions: 1024  # 向量维度
  1. 使用EmbeddingModel。以下为测试类,亦需手动配置API_KEY环境变量(见前述),点击绿色三角按钮选择Modify Run Configuration...即可。
@SpringBootTest
class HyprojAiApplicationTests {

    @Autowired
    private OpenAiEmbeddingModel embeddingModel;

    @Test
    public void testEmbedding() {
        // 1.测试数据
        // 1.1. 用来查询的文本,国际冲突
        String query = "global conflicts";

        // 1.2. 用来做比较的文本
        String[] texts = new String[]{
                "哈马斯称加沙下阶段停火谈判仍在进行 以方尚未做出承诺",
                "土耳其、芬兰、瑞典与北约代表将继续就瑞典“入约”问题进行谈判",
                "日本航空基地水井中检测出有机氟化物超标",
                "国家游泳中心(水立方):恢复游泳、嬉水乐园等水上项目运营",
                "我国首次在空间站开展舱外辐射生物学暴露实验",
        };

        // 2. 向量化
        // 2.1. 先将查询文本向量化
        float[] queryVector = embeddingModel.embed(query);

        // 2.2. 再将比较文本向量化,放到一个数组
        List<float[]> textVectors = embeddingModel.embed(Arrays.asList(texts));

        // 3. 比较欧氏距离
        // 3.1. 把查询文本自己与自己比较,肯定是相似度最高的
        System.out.println(VectorDistanceUtils.euclideanDistance(queryVector, queryVector));
        // 3.2. 把查询文本与其它文本比较
        for (float[] textVector : textVectors) {
            System.out.println(VectorDistanceUtils.euclideanDistance(queryVector, textVector));
        }
        System.out.println("------------------");

        // 4. 比较余弦距离
        // 4.1. 把查询文本自己与自己比较,肯定是相似度最高的
        System.out.println(VectorDistanceUtils.cosineDistance(queryVector, queryVector));
        // 4.2. 把查询文本与其它文本比较
        for (float[] textVector : textVectors) {
            System.out.println(VectorDistanceUtils.cosineDistance(queryVector, textVector));
        }
    }
}

4.3.2 向量数据库

向量数据库有两个主要作用:

  • 存储向量数据。
  • 基于相似度检索数据。

Spring AI支持很多向量数据库,并且都进行了封装(实现了统一的接口VectorStore,操作完全相同,详见文档),可以用统一的API去访问:

  • Azure Vector Search - The Azure vector store.
  • Apache Cassandra - The Apache Cassandra vector store.
  • Chroma Vector Store - The Chroma vector store.
  • Elasticsearch Vector Store - The Elasticsearch vector store.
  • GemFire Vector Store - The GemFire vector store.
  • MariaDB Vector Store - The MariaDB vector store.
  • Milvus Vector Store - The Milvus vector store.
  • MongoDB Atlas Vector Store - The MongoDB Atlas vector store.
  • Neo4j Vector Store - The Neo4j vector store.
  • OpenSearch Vector Store - The OpenSearch vector store.
  • Oracle Vector Store - The Oracle Database vector store.
  • PgVector Store - The PostgreSQL/PGVector vector store.
  • Pinecone Vector Store - PineCone vector store.
  • Qdrant Vector Store - Qdrant vector store.
  • Redis Vector Store - The Redis vector store.
  • SAP Hana Vector Store - The SAP HANA vector store.
  • Typesense Vector Store - The Typesense vector store.
  • Weaviate Vector Store - The Weaviate vector store.
  • SimpleVectorStore - A simple implementation of persistent vector storage, good for educational purposes.

具体步骤如下(以Redis向量数据库为例):

  1. 引入依赖:
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vector-store-redis</artifactId>
</dependency>
  1. 配置向量数据库
spring:
    ai:
        vectorstore:
            redis:
                index: spring_ai_index
                initialize-schema: true
                prefix: "doc:"
        data:
            redis:
                host: 192.168.150.101

SimpleVectorStore为基于内存实现的简单向量库,接下来以其为例继续演示(由于新版本依赖会默认自动注入一个VectorStore,易与手动编写的Bean相冲突,此处修改了方法名):

@Bean
public VectorStore simpleVectorStore(OpenAiEmbeddingModel embeddingModel) {
    return SimpleVectorStore.builder(embeddingModel).build();
}

之后即可读写数据,所有向量数据库使用同一套API:

public interface VectorStore extends DocumentWriter {

    void add(List<Document> documents);

    void delete(List<String> idList);

    void delete(Filter.Expression filterExpression);

    List<Document> similaritySearch(String query);

}

4.3.3 读取、拆分PDF文档

由于知识库太大,所以要将知识库拆分成文档片段,然后再做向量化。

对于PDF文档读取和拆分,Spring AI提供了两种默认的拆分原则:

  • PagePdfDocumentReader :按页拆分,推荐使用。
  • ParagraphPdfDocumentReader:按PDF的目录拆分,不推荐,因为很多PDF不规范,没有章节标签。

此处选择使用PagePdfDocumentReader,步骤如下:

  1. 引入依赖:
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
  1. 读取、拆分文档。以下为单元测试示例:
@Autowired
private VectorStore simpleVectorStore;

@Test
public void testVectorStore() {
    Resource resource = new FileSystemResource("SE_Akira37_example.pdf");

    // 1. 创建PDF读取器
    PagePdfDocumentReader reader = new PagePdfDocumentReader(
        resource,   // 文件源
        PdfDocumentReaderConfig.builder()
            .withPageExtractedTextFormatter(ExtractedTextFormatter.defaults())
            .withPagesPerDocument(1)    // 每1页PDF作为一个Document
            .build()
    );

    // 2. 读取PDF文档,拆分为Document
    List<Document> documents = reader.read();

    // 3. 写入向量库
    simpleVectorStore.add(documents);

    // 4. 搜索
    SearchRequest request = SearchRequest.builder()
        .query("软件的维护的目标是什么?")
        .topK(1)
        .similarityThreshold(0.5f)
        .filterExpression("file_name == 'SE_Akira37_example.pdf'")
        .build();
    List<Document> docs = simpleVectorStore.similaritySearch(request);
    if (docs == null) {
        System.out.println("未搜索到相关内容");
        return;
    }
    for (Document doc : docs) {
        System.out.println("id: " + doc.getId());
        System.out.println("score: " + doc.getScore());
        System.out.println("text: " + doc.getText());
    }
}

4.3.4 实现功能

大致步骤如下:

  1. 定义用于记录chatId与PDF文件映射关系的FileRepository(见仓库)
  2. 实现上传、下载功能

发表评论