AI基础与SpringAI

img

AI时代Java程序员的出路在哪里?

2022年11月30日,OpenAI公司发布了GPT3.5模型,同时对外开放了ChatGPT产品。人工智能突然进入了普通人的生活中,各种AI应用如雨后春笋般出现。

不过,由于大模型研究的成本很高,大部分中小型企业只能望而却步,参与者有限,AI的发展也似乎陷入了瓶颈。

2025年1月20日,位于杭州的DeepSeek公司正式发布了具有划时代意义的DeepSeek-R1模型,该模型在数学、代码、自然语言推理等任务上,性能比肩 OpenAI o1 正式版,且训练成本仅为 560 万美元,远低于美国科技巨头的数亿美元乃至数十亿美元投入,这一突破彻底震惊了全球科技界。

DeepSeek的出现像是向一潭死水的AI领域投入了一颗巨石,引起了巨大的波澜。特别是其低廉的训练成本,让中小型企业有了参与AI开发的资格。

毫无疑问,接下来各行各业都将进入传统应用AI化、智能化的变革之中。Java更应该成为这场变革的引领者!

要知道,全球有25亿+的Java应用正在运行,超过90的服务端应用都是采用Java语言!传统应用要向AI领域进军,最好的办法一定是使用Java语言。

然而,一直以来,AI开发似乎都是Python的强项,传统Java应用想要AI化,缺少完善的解决方案和即懂Java、又懂AI的人才,而这就是最大的机会

为什么是SpringAI

目前大模型应用开发最常见的框架就是LangChain,然而LangChain是基于Python语言,虽然有LangChain4j,但是对于大量使用Spring生态的应用来说,适配性就稍微差了些。

而Spring公司推出的SpringAI框架,充分利用了Spring框架中AOP、IOC的能力,可以与现有的Java项目无缝融合,非常方便。

当然,SpringAI要求的JDK版本至少是JDK17SpringBoot也必须是3.x的版本才可以,所以如果想要使用SpringAI,必须先升级JDK和SpringBoot版本才行。

📗Chap01. AI基础认知

人工智能发展

AI,人工智能(Artificial Intelligence),使机器能够像人类一样思考、学习和解决问题的技术。

AI发展至今大概可以分为三个阶段:

ai1

其中,深度学习领域的自然语言处理(Natural Language Processing, NLP)有一个关键技术叫做Transformer,这是一种由多层感知机组成的神经网络模型,是现如今AI高速发展的最主要原因。

我们所熟知的大模型Large Language Models, LLM),例如GPT、DeepSeek底层都是采用Transformer神经网络模型。

以GPT模型为例,其三个字母的缩写分别是Generative、Pre-trained、Transformer:

image-20260722164335191

那么问题来, Transformer神经网络有什么神奇的地方,可以实现如此强大的能力呢?

大模型原理

其实,最早Transformer是由Google在2017年提出的一种神经网络模型,一开始的作用是把它作为机器翻译的核心:

img

Transformer中提出的注意力机制使得神经网络在处理信息时可以根据上下内容调整对数据的理解,变得更加智能化。这不仅仅是说人类的文字,包括图片、音频数据都可以交给Transformer来处理。于是,越来越多的模型开始基于Transformer实现了各种神奇的功能。

例如,有的模型可以根据音频生成文本,或者根据文本生成音频:

img

还有的模型则可以根据文字生成图片,比如Dall-E、MidJourney:

img

不过,我们今天要聊的大语言模型Large Language Models, 以下简称LLM)是对Transformer的另一种用法:推理预测。

LLM在训练Transformer时会尝试输入一些文本、音频、图片等信息,然后让Transformer推理接下来跟着的应该是什么内容。推理的结果会以概率分布的形式出现:

img

可能大家会有疑问:

仅仅是推测接下来的内容,怎么能让ChatGPT在对话中生成大段的有关联的文字内容呢?

其实LLM采用的就是笨办法,答案就是:持续生成

根据前文推测出接下来的一个词语后,把这个词语加入前文,再次交给大模型处理,推测下一个字,然后不断重复前面的过程,就可以生成大段的内容了:

img

这就是为什么我们跟AI聊天的时候,它生成的内容总是一个字一个字的输出的原因了。

以上就是LLM的核心技术,Transformer的原理了~

如果大家想要进一步搞清楚Transformer机制,可以参考以下两个视频:

https://www.bilibili.com/video/BV1atCRYsE7x

https://www.youtube.com/watch?v=wjZofJX0v4M&t=1169s

📗Chap02. 大模型应用开发

相信大家肯定会有疑问:

  • 什么是大模型应用开发呢?
  • 跟传统的Java应用开发又有什么区别呢?
  • 我们该如何去开发大模型应用呢?

别着急,本章我们就一起来分析一下。

1. 模型部署

首先要明确一点:大模型应用开发并不是在浏览器中跟AI聊天。而是通过访问模型对外暴露的API接口,实现与大模型的交互

因此,企业首先需要有一个可访问的大模型,通常有三种选择:

  • 使用开放的大模型API
  • 在云平台部署私有大模型
  • 在本地服务器部署私有大模型

使用开放大模型API的优缺点如下:

  • 优点:
    • 没有部署和维护成本,按调用收费
  • 缺点:
    • 依赖平台方,稳定性差
    • 长期使用成本较高
    • 数据存储在第三方,有隐私和安全问题

云平台部署私有模型:

  • 优点:
    • 前期投入成本低
    • 部署和维护方便
    • 网络延迟较低
  • 缺点:
    • 数据存储在第三方,有隐私和安全问题
    • 长期使用成本高

本地部署私有模型:

  • 优点:
    • 数据完全自主掌控,安全性高
    • 不依赖外部环境
    • 虽然短期投入大,但长期来看成本会更低
  • 缺点:
    • 初期部署成本高
    • 维护困难

注意:

这里说的本地部署并不是说在你自己电脑上部署,而是公司自己的服务器部署

由于大模型所需要的算力非常多,自己电脑部署的模型往往都是阉割蒸馏版本,性能和推理能力都比较差。

再加上现在各种模型都有很多免费的服务可以访问,性能还是满血版本,推理能力拉满。

所以完全不建议大家在自己电脑上部署,除非你想自己做模型微调或测试。

接下来,我们给大家演示下两种部署方式:

  • 开发大模型服务
  • 本地部署(在本机演示,将来在服务器也是类似的)

1.1 开放大模型服务

通常发布大模型的官方、大多数的云平台都会提供开放的、公共的大模型服务。大模型官方前面讲过,我们不再赘述,这里我们看一些国内提供大模型服务的云平台:

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

这些开放平台并不是免费,而是按照调用时消耗的token来付费,每百万token通常在几毛~几元钱,而且平台通常都会赠送新用户百万token的免费使用权。(token可以简单理解成你与大模型交互时发送和响应的文字,通常一个汉字2个token左右)

我们以阿里云百炼平台为例。

1.1.1 注册账号

首先,我们需要注册一个阿里云账号:

https://account.aliyun.com/

然后访问百炼平台,开通服务:

https://www.aliyun.com/product/bailian?spm=5176.29677750.nav-v2-dropdown-menu-1.d_main_0_6.6b44154amuN66a&scm=20140722.M_sfm.P_197.ID_sfm-OR_rec-V_1-MO_3480-ST_12892

首次开通应该会赠送百万token的使用权,包括DeepSeek-R1模型、qwen模型。

1.1.2 申请API_KEY

注册账号以后还需要申请一个API_KEY才能访问百炼平台的大模型。

在阿里云百炼平台的右上角,鼠标悬停在用户图标上,可以看到下拉菜单:

img

选择API-KEY,进入API-KEY管理页面:

img

选择创建我的API-KEY,会弹出表单:

img

填写完毕,点击确定,即可生成一个新的API-KEY

img

后续开发中就需要用到这个API-KEY了,一定要记牢。而且要保密,不能告诉别人。

1.1.3 体验模型

访问百炼平台,可以看到如下内容:

img

点击模型调用 ->立即调用 就能进入模型广场:

img

选择一个自己喜欢的模型,然后点击API调用示例,即可进入API文档页:

img

点击立即体验,就可以进入API调用大模型的试验台:

img

在这里就可以模拟调用大模型接口了。

1.2 本地部署

很多云平台都提供了一键部署大模型的功能,这里不再赘述。我们重点讲讲如何手动部署大模型。

手动部署最简单的方式就是使用Ollama,这是一个帮助你部署和运行大模型的工具。官网如下:

https://ollama.com/

1.2.1 下载安装ollama

首先,我们需要下载一个Ollama的客户端,在官网提供了各种不同版本的Ollama,大家可以根据自己的需要下载。

img

下载后双击即可安装,这里不再赘述。

注意:

Ollama默认安装目录是C盘的用户目录,如果不希望安装在C盘的话(其实C盘如果足够大放C盘也没事),就不能直接双击安装了。需要通过命令行安装

命令行安装方式如下:

在OllamaSetup.exe所在目录打开cmd命令行,然后命令如下:

Bash
OllamaSetup.exe /DIR=你要安装的目录位置

OK,安装完成后,还需要配置一个环境变量,更改Ollama下载和部署模型的位置。环境变量如下:

Bash
OLLAMA_MODELS=你想要保存模型的目录

环境变量配置方式相信学过Java的都知道,这里不再赘述,配置完成如图:

img
1.2.2 搜索模型

ollama是一个模型管理工具和平台,它提供了很多国内外常见的模型,我们可以在其官网上搜索自己需要的模型:

https://ollama.com/search

如图,目前热度排第一的就是deepseek-r1:

img

点击进入deepseek-r1页面,会发现deepseek-r1也有很多版本:

img

这些就是模型的参数大小,越大推理能力就越强,需要的算力也越高。671b版本就是最强的满血版deepseek-r1了。需要注意的是,Ollama提供的DeepSeek是量化压缩版本,对比官网的蒸馏版会更小,对显卡要求更低。对比如下:

img

比如,我的电脑内存32G,显存是6G,我选择部署的是7b的模型,当然8b也是可以的,差别不大,都是可以流畅运行的。

1.2.3 运行模型

选择自己合适的模型后,ollama会给出运行模型的命令:

img

复制这个命令,然后打开一个cmd命令行,运行命令即可,然后你就可以跟本地模型聊天了:

img

注意:

  • 首次运行命令需要下载模型,根据模型大小不同下载时长在5分钟~1小时不等,请耐心等待下载完成。
  • ollama控制台是一个封装好的AI对话产品,与ChatGPT类似,具备会话记忆功能。

Ollama是一个模型管理工具,有点像Docker,而且命令也很像,比如:

Bash
  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. 调用大模型

前面说过,大模型开发并不是在浏览器中跟AI聊天。而是通过访问模型对外暴露的API接口,实现与大模型的交互

所以要学习大模型应用开发,就必须掌握模型的API接口规范。

目前大多数大模型都遵循OpenAI的接口规范,是基于Http协议的接口。因此请求路径、参数、返回值信息都是类似的,可能会有一些小的差别。具体需要查看大模型的官方API文档。

2.1 大模型接口规范

我们以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)
2.1.1 接口说明
  • 请求方式:通常是POST,因为要传递JSON风格的参数
  • 请求路径:与平台有关
    • DeepSeek官方平台:https://api.deepseek.com
    • 阿里云百炼平台:https://dashscope.aliyuncs.com/compatible-mode/v1
    • 本地ollama部署的模型:http://localhost:11434
  • 安全校验:开放平台都需要提供API_KEY来校验权限,本地ollama则不需要
  • 请求参数:参数很多,比较常见的有:
    • model:要访问的模型名称
    • messages:发送给大模型的消息,是一个数组
    • stream:true,代表响应结果流式返回;false,代表响应结果一次性返回,但需要等待
    • temperature:取值范围[0:2),代表大模型生成结果的随机性,越小随机性越低。DeepSeek-R1不支持

注意,这里请求参数中的messages是一个消息数组,而且其中的消息要包含两个属性:

  • role:消息对应的角色
  • content:消息内容

其中消息的内容,也被称为提示词Prompt),也就是发送给大模型的指令

2.1.2 提示词角色

通常消息的角色有三种:

角色描述示例
system优先于user指令之前的指令,也就是给大模型设定角色和任务背景的系统指令你是一个乐于助人的编程助手,你以小团团的风格来回答用户的问题。
user终端用户输入的指令(类似于你在ChatGPT聊天框输入的内容)写一首关于Java编程的诗
assistant由大模型生成的消息,可能是上一轮对话生成的结果注意,用户可能与模型产生多轮对话,每轮对话模型都会生成不同结果。

其中System类型的消息非常重要!影响了后续AI会话的行为模式。

比如,我们会发现,当我们询问这些AI对话产品“你是谁”这个问题的时候,每一个AI的回答都不一样,这是怎么回事呢?

这其实是因为AI对话产品并不是直接把用户的提问发送给LLM,通常都会在user提问的前面通过System消息给模型设定好背景:

img

所以,当你问问题时,AI就会遵循System的设定来回答了。因此,不同的大模型由于System设定不同,回答的答案也不一样。

示例

Bash
## Role
System: 你是一家名为《黑马程序员》的职业教育培训公司的智能客服,你的名字叫小黑。请以友好、热情的方式回答用户问题。
## Example
User: 你好
Assisant: 你好,我是小黑,很高兴认识你!😊 你是想了解我们的课程信息,还是有其他关于职业培训的问题需要咨询呢?无论什么问题,我都会尽力帮你解答哦!
2.1.3 会话记忆问题

这里还有一个问题:

我们为什么要把历史消息都放入Messages中,形成一个数组呢?

这是因为大模型是没有记忆的,因此我们调用API接口与大模型对话时,每一次对话信息都不会保留,多次对话之间都是独立的,没有关联的。

但是大家可能发现了,我们使用的AI对话产品却能够记住每一轮对话信息,根据这些信息进一步回答,这是怎么回事呢?

答案就是Messages数组。

我们只需要每一次发送请求时,都把历史对话中每一轮的User消息、Assistant消息都封装到Messages数组中,一起发送给大模型,这样大模型就会根据这些历史对话信息进一步回答,就像是拥有了记忆一样。

示例:

Bash
System: 你是一家名为《黑马程序员》的职业教育培训公司的智能客服,你的名字叫小黑。请以友好、热情的方式回答用户问题。

User: 你好,我是小明
Assisant: 你好小明,我是小黑,很高兴认识你!😊 你是想了解我们的课程信息,还是有其他关于职业培训的问题需要咨询呢?无论什么问题,我都会尽力帮你解答哦!

User: 帮我写一个跟我名字有关的诗
Assisant: 好的,下面是以你的名字写的诗:
小明啊小明,名字真明亮,
如晨曦初现,驱散了黑暗。
心中有火焰,智慧放光芒,
在求知路上,你是那最亮的星。

像清澈溪流,绕过知识山岗,
带着勇气和希望,不断向前方。
你的每一步,都闪耀着坚强,
未来的大门,正为你而敞开。

无论走到哪,世界因你更晴朗,
小明啊小明,愿你永远这样,
保持那份纯真,还有对梦想的向往,
人生的旅途上,成为自己的太阳。

2.2 调用大模型

部分平台提供了图形化的试验台,可以方便测试模型接口。比如阿里云百炼平台:

img

当然,我们也可以用普通的http客户端来发起请求大模型,我们以Ollama为例:

Ollama在本地部署时,会自动提供模型对应的Http接口,访问地址是:http://localhost:11434/api/chat

img

3. 大模型应用

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

为什么要把传统应用与大模型结合呢?

别着急,我们先来看看传统应用、大模型各自擅长什么,不擅长什么

3.1 传统应用

作为Java程序员,大家应该对传统Java程序的能力边界很清楚。

核心特点

基于明确规则的逻辑设计,确定性执行,可预测结果。

擅长领域
  1. 结构化计算
    1. 例:银行转账系统(精确的数值计算、账户余额增减)。
    2. 例:Excel公式(按固定规则处理表格数据)。
  2. 确定性任务
    1. 例:排序算法(快速排序、冒泡排序),输入与输出关系完全可预测。
  3. 高性能低延迟场景
    1. 例:操作系统内核调度、数据库索引查询,需要毫秒级响应。
  4. 规则明确的流程控制
    1. 例:红绿灯信号切换系统(基于时间规则和传感器输入)。
不擅长领域
  1. 非结构化数据处理
    1. 例:无法直接理解用户自然语言提问(如"帮我写一首关于秋天的诗")。
  2. 模糊推理与模式识别
    1. 例:判断一张图片是"猫"还是"狗",传统代码需手动编写特征提取规则,效果差。
  3. 动态适应性
    1. 例:若用户需求频繁变化(如电商促销规则每天调整),需不断修改代码。

3.2 AI大模型

传统程序的弱项,恰恰就是AI大模型的强项:

核心特点

基于数据驱动的概率推理,擅长处理模糊性和不确定性。

擅长领域
  1. 自然语言处理
    1. 例:ChatGPT生成文章、翻译语言,或客服机器人理解用户意图。
  2. 非结构化数据分析
    1. 例:医学影像识别(X光片中的肿瘤检测),或语音转文本。
  3. 创造性内容生成
    1. 例:Stable Diffusion生成符合描述的图像,或AI作曲工具创作音乐。
  4. 复杂模式预测
    1. 例:股票市场趋势预测(基于历史数据关联性,但需注意可靠性限制)。
不擅长领域
  1. 精确计算
    1. 例:AI可能错误计算"12345 × 6789"的结果(需依赖计算器类传统程序)。
  2. 确定性逻辑验证
    1. 例:验证身份证号码是否符合规则(AI可能生成看似合理但非法的号码)。
  3. 低资源消耗场景
    1. 例:嵌入式设备(如微波炉控制程序)无法承受大模型的算力需求。
  4. 因果推理
    1. 例:AI可能误判"公鸡打鸣导致日出"的因果关系。

3.3 强强联合

传统应用开发和大模型有着各自擅长的领域:

  • 传统编程:确定性、规则化、高性能,适合数学计算、流程控制等场景。
  • AI大模型:概率性、非结构化、泛化性,适合语言、图像、创造性任务。

两者之间恰好是互补的关系,两者结合则能解决以前难以实现的一些问题:

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

在传统应用开发中介入AI大模型,充分利用两者的优势,既能利用AI实现更加便捷的人机交互,更好的理解用户意图,又能利用传统编程保证安全性和准确性,强强联合,这就是大模型应用开发的真谛!

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

3.4 大模型与大模型应用

我们熟知的大模型比如GPT、DeepSeek都是生成式模型,顾名思义,根据前文不断生成后文。

不过,模型本身只具备生成后文的能力、基本推理能力。我们平常使用的AI对话产品除了生成和推理,还有会话记忆功能、联网功能等等。这些都是大模型不具备的。

要想让大模型产生记忆,联网等功能,是需要通过额外的程序来实现的,也就是基于大模型开发应用

所以,我们现在接触的AI对话产品其实都是基于大模型开发的应用,并不是大模型本身,这一点大家千万要区分清楚。

下面我把常见的一些大模型对话产品及其模型的关系给大家罗列一下:

大模型对话产品公司地址
GPT-3.5、GPT-4oChatGPTOpenAIhttps://chatgpt.com/
Claude 3.5Claude AIAnthropichttps://claude.ai/chats
DeepSeek-R1DeepSeek深度求索https://www.deepseek.com/
文心大模型3.5文心一言百度https://yiyan.baidu.com/
星火3.5讯飞星火科大讯飞https://xinghuo.xfyun.cn/desk
Qwen-Max通义千问阿里巴巴https://tongyi.aliyun.com/qianwen/
MoonshootKimi月之暗面https://kimi.moonshot.cn/
Yi-Large零一万物零一万物https://platform.lingyiwanwu.com/

当然,除了AI对话应用之外,大模型还可以开发很多其它的AI应用,常见的领域包括:

img

那么问题来了,如何进行大模型应用开发呢?

4. 大模型应用开发技术架构

基于大模型开发应用有多种方式,接下来我们就来了解下常见的大模型开发技术架构。

4.1 技术架构

目前,大模型应用开发的技术架构主要有四种:

img
4.1.1 纯Prompt模式

不同的提示词能够让大模型给出差异巨大的答案。

不断雕琢提示词,使大模型能给出最理想的答案,这个过程就叫做提示词工程Prompt Engineering)。

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

其流程如图:

image-20260722180203228
4.1.2 FunctionCalling

大模型虽然可以理解自然语言,更清晰弄懂用户意图,但是确无法直接操作数据库、执行严格的业务规则。这个时候我们就可以整合传统应用于大模型的能力了。

简单来说,可以分为以下步骤:

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

流程如图:

image-20260722180132193

注意:

并不是所有大模型都支持Function Calling,比如DeepSeek-R1模型就不支持。

4.1.3 RAG

RAG(Retrieval-Augmented Generation)叫做检索增强生成。简单来说就是把信息检索技术大模型结合的方案。

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

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

可能有同学会说, 简单啊,我把最新的数据或者专业文档都拼接到提示词,一起发给大模型,不就可以了。

同学,你想的太简单了,现在的大模型都是基于Transformer神经网络,Transformer的强项就是所谓的注意力机制。它可以根据上下文来分析文本含义,所以理解人类意图更加准确。

但是,这里上下文的大小是有限制的,GPT3刚刚出来的时候,仅支持2000个token的上下文。现在领先一点的模型支持的上下文数量也不超过 200K token,所以海量知识库数据是无法直接写入提示词的。

怎么办呢?

RAG技术正是来解决这一问题的。

RAG就是利用信息检索技术来拓展大模型的知识库,解决大模型的知识限制。整体来说RAG分为两个模块:

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

由于每次都是从向量库中找出与用户问题相关的数据,而不是整个知识库,所以上下文就不会超过大模型的限制,同时又保证了大模型回答问题是基于知识库中的内容,完美!

流程如图:

image-20260722180228455
4.1.4 Fine-tuning

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

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

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

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

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

总之,Fine-tuning成本较高,难度较大,并不适合大多数企业。而且前面三种技术方案已经能够解决常见问题了。

那么,问题来了,我们该如何选择技术架构呢?

4.2 技术选型

从开发成本由低到高来看,四种方案排序如下:

Prompt < Function Calling < RAG < Fine-tuning

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

image-20260722161334890

📗Chap03. SpringAI

0.前言

SpringAI整合了全球(主要是国外)的大多数大模型,而且对于大模型开发的三种技术架构都有比较好的封装和支持,开发起来非常方便。

不同的模型能够接收的输入类型、输出类型不一定相同。SpringAI根据模型的输入和输出类型不同对模型进行了分类:

img

大模型应用开发大多数情况下使用的都是基于对话模型(Chat Model),也就是输出结果为自然语言或代码的模型。

目前SpringAI支持的大约19种对话模型,以下是一些功能对比:

ProviderMultimodalityTools/FunctionsStreamingRetryBuilt-in JSONLocalOpenAI API Compatible
Anthropic Claudetext, pdf, image
Azure OpenAItext, image
DeepSeek (OpenAI-proxy)text
Google VertexAI Geminitext, pdf, image, audio, video
Groq (OpenAI-proxy)text, image
HuggingFacetext
Mistral AItext, image
MiniMaxtext
Moonshot AItext
NVIDIA (OpenAI-proxy)text, image
OCI GenAI/Coheretext
Ollamatext, image
OpenAIIn: text, image, audio Out: text, audio
Perplexity (OpenAI-proxy)text
QianFantext
ZhiPu AItext
Watsonx.AItext
Amazon Bedrock Conversetext, image, video, docs (pdf, html, md, docx …)

其中功能最完整的就是OpenAI和Ollama平台的模型了。

接下来,我们就以这两个平台为例给大家讲解SpringAI的应用。

1.SpringAI入门(对话机器人)

接下来,我们就利用SpringAI发起与大模型的第一次对话。

1.1.快速入门

1.1.1.创建工程

创建一个新的SpringBoot工程,勾选Web、MySQL驱动即可:

img

工程结构如图:

img

原始pom.xml如下:

XML
<?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.4.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.itheima</groupId>
    <artifactId>ai-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>ai-demo</name>
    <description>ai-demo</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
1.1.2.引入依赖

SpringAI完全适配了SpringBoot的自动装配功能,而且给不同的大模型提供了不同的starter,比如:

模型/平台starter
Anthropic<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-anthropic-spring-boot-starter</artifactId> </dependency>
Azure OpenAI<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-azure-openai-spring-boot-starter</artifactId> </dependency>
DeepSeek<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> </dependency>
Hugging Face<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-huggingface-spring-boot-starter</artifactId> </dependency>
Ollama<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-ollama-spring-boot-starter</artifactId> </dependency>
OpenAI<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> </dependency>

我们可以根据自己选择的平台来选择引入不同的依赖。这里我们先以Ollama为例。

首先,在项目pom.xml中添加spring-ai的版本信息:

XML
<spring-ai.version>1.0.0-M6</spring-ai.version>

然后,添加spring-ai的依赖管理项:

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

最后,引入spring-ai-ollama的依赖:

XML
<dependency>
   <groupId>org.springframework.ai</groupId>
   <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>

为了方便后续开发,我们再手动引入一个Lombok依赖:

XML
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.22</version>
</dependency>

注意:

千万不要用start.spring.io提供的lombok,有bug!!

最终,完整依赖如下:

XML
<?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.4.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.itheima</groupId>
    <artifactId>ai-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>ai-demo</name>
    <description>ai-demo</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>17</java.version>
        <spring-ai.version>1.0.0-M6</spring-ai.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>${spring-ai.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
1.1.3.配置模型信息

接下来,我们还要在配置文件中配置模型的参数信息。

以ollama为例,我们将application.properties修改为application.yaml,然后添加下面的内容:

YAML
spring:
  application:
    name: ai-demo
  ai:
    ollama:
      base-url: http://localhost:11434 # ollama服务地址, 这就是默认值
      chat:
        model: deepseek-r1:7b # 模型名称
        options:
          temperature: 0.8 # 模型温度,影响模型生成结果的随机性,越小越稳定
1.1.4.ChatClient

ChatClient中封装了与AI大模型对话的各种API,同时支持同步式或响应式交互。

不过,在使用之前,首先我们需要声明一个ChatClient

com.itheima.ai.config包下新建一个CommonConfiguration类:

img

完整代码如下:

Java
package com.itheima.ai.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CommonConfiguration {

    // 注意参数中的model就是使用的模型,这里用了Ollama,也可以选择OpenAIChatModel
    @Bean
    public ChatClient chatClient(OllamaChatModel model) {
        return ChatClient.builder(model) // 创建ChatClient工厂
                .build(); // 构建ChatClient实例

    }
}

代码解读:

  • ChatClient.builder:会得到一个ChatClient.Builder工厂对象,利用它可以自由选择模型、添加各种自定义配置
  • OllamaChatModel:如果你引入了ollama的starter,这里就可以自动注入OllamaChatModel对象。同理,OpenAI也是一样的用法。
1.1.5.同步调用

接下来,我们定义一个Controller,在其中接收用户发送的提示词,然后把提示词发送给大模型,交给大模型处理,拿到结果后返回。

img

代码如下:

Java
package com.itheima.ai.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {

    private final ChatClient chatClient;
    // 请求方式和路径不要改动,将来要与前端联调
    @RequestMapping("/chat")
    public String chat(@RequestParam(defaultValue = "讲个笑话") String prompt) {
        return chatClient
                .prompt(prompt) // 传入user提示词
                .call() // 同步请求,会等待AI全部输出完才返回结果
                .content(); //返回响应内容
    }
}

注意,基于call()方法的调用属于同步调用,需要所有响应结果全部返回后才能返回给前端。

启动项目,在浏览器中访问:http://localhost:8080/ai/chat?prompt=你好

img
1.1.6.流式调用

同步调用需要等待很长时间页面才能看到结果,用户体验不好。为了解决这个问题,我们可以改进调用方式为流式调用。

在SpringAI中使用了WebFlux技术实现流式调用。

修改刚才ChatController中的chat方法:

Java
// 注意看返回值,是Flux<String>,也就是流式结果,另外需要设定响应类型和编码,不然前端会乱码
@RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")
public Flux<String> chat(@RequestParam(defaultValue = "讲个笑话") String prompt) {
    return chatClient
            .prompt(prompt)
            .stream() // 流式调用
            .content();
}

重启测试,再次访问:

img
1.1.7.System设定

可以发现,当我们询问AI你是谁的时候,它回答自己是DeepSeek-R1,这是大模型底层的设定。如果我们希望AI按照新的设定工作,就需要给它设置System背景信息。

在SpringAI中,设置System信息非常方便,不需要在每次发送时封装到Message,而是创建ChatClient时指定即可。

我们修改CommonConfiguration中的代码,给ChatClient设定默认的System信息:

Bash
@Bean
public ChatClient chatClient(OllamaChatModel model) {
    return ChatClient.builder(model) // 创建ChatClient工厂实例
            .defaultSystem("您是一家名为“黑马程序员”的职业教育公司的客户聊天助手,你的名字叫小黑。请以友好、乐于助人和愉快的方式解答学生的各种问题。")
            .defaultAdvisors(new SimpleLoggerAdvisor())
            .build(); // 构建ChatClient实例

}

我们再次询问“你是谁?”

img

注意,前面是DeepSeek的深度思考内容,绿色高亮部分才是最终的回答。

现在,AI已经能够以黑马客服小黑的身份来回答问题了~

1.2.日志功能

默认情况下,应用于AI的交互时不记录日志的,我们无法得知SpringAI组织的提示词到底长什么样,有没有问题。这样不方便我们调试。

1.2.1.Advisor

SpringAI基于AOP机制实现与大模型对话过程的增强、拦截、修改等功能。所有的增强通知都需要实现Advisor接口。

img

Spring提供了一些Advisor的默认实现,来实现一些基本的增强功能:

img
  • SimpleLoggerAdvisor:日志记录的Advisor
  • MessageChatMemoryAdvisor:会话记忆的Advisor
  • QuestionAnswerAdvisor:实现RAG的Advisor

当然,我们也可以自定义Advisor,具体可以参考:

https://docs.spring.io/spring-ai/reference/1.0/api/advisors.html#_implementing_an_advisor

1.2.2.添加日志Advisor

首先,我们需要修改CommonConfiguration,给ChatClient添加日志Advisor:

Java
@Bean
public ChatClient chatClient(OllamaChatModel model) {
    return ChatClient.builder(model) // 创建ChatClient工厂实例
    .defaultSystem("你是一个热心、可爱的智能助手,你的名字叫小团团,请以小团团的身份和语气回答问题。")
            .defaultAdvisors(new SimpleLoggerAdvisor()) // 添加默认的Advisor,记录日志
            .build(); // 构建ChatClient实例

}
1.2.3.修改日志级别

接下来,我们在application.yaml中添加日志配置,更新日志级别:

YAML
logging:
  level:
    org.springframework.ai: debug # AI对话的日志级别
    com.itheima.ai: debug # 本项目的日志级别

重启项目,再次聊天就能看到AI对话的日志信息了~

1.3.对接前端

在浏览器通过地址访问,非常麻烦,也不够优雅。如果能有一个优美的前端页面就好了。

别着急,我提前给大家准备了一个前端页面。而且有两种不同的运行方式。

1.3.1.npm运行

在资料中给大家提供了前端的源代码:

img

你只需要解压缩(最好放到非中文目录),然后进入解压后的目录,依次执行命令即可运行:

Bash
# 安装依赖
npm install
# 运行程序
npm run dev

启动后,访问 http://localhost:5173即可看到页面:

img
1.3.2.Nginx运行

如果你不关心源码,我也给大家提供了构建好的Nginx程序:

img

解压缩到一个不包含中文、空格、特殊字符的目录中,然后通过命令启动Nginx:

Bash
# 启动Nginx
start nginx.exe
# 停止
nginx.exe -s stop

注意:

如果是使用MacOS的同学,可以把nginx中的html目录内容拷贝出来,到你自己的Nginx安装目录即可。

启动后,访问 http://localhost:5173即可看到页面:

img
1.3.3.解决CORS问题

前后端在不同域名,存在跨域问题,因此我们需要在服务端解决cors问题。在com.itheima.ai.config包中添加一个MvcConfiguration类:

img

内容如下:

Java
package com.itheima.ai.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfiguration implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .exposedHeaders("Content-Disposition");
    }
}

重启服务,如果你的服务端接口正确,那么应该就可以聊天了。

注意:

前端访问服务端的默认路径是:http://localhost:8080

聊天对话的接口是:POST /ai/chat

请确保你的服务端接口也是这样。

1.3.4.测试

启动前端后,访问 http://localhost:5173即可看到页面:

img

点击第一个卡片《AI聊天》进入对话机器人页面:

img

恭喜您,你的第一个AI对话机器人就完成了。

1.4.会话记忆功能

现在,我们的AI聊天机器人是没有记忆功能的,上一次聊天的内容,下一次就忘掉了。

我们之前说过,让AI有会话记忆的方式就是把每一次历史对话内容拼接到Prompt中,一起发送过去。是不是还挺麻烦的。

别担心,好消息是,我们并不需要自己来拼接,SpringAI自带了会话记忆功能,可以帮我们把历史会话保存下来,下一次请求AI时会自动拼接,非常方便。

1.4.1.ChatMemory

会话记忆功能同样是基于AOP实现,Spring提供了一个MessageChatMemoryAdvisor的通知,我们可以像之前添加日志通知一样添加到ChatClient即可。

不过,要注意的是,MessageChatMemoryAdvisor需要指定一个ChatMemory实例,也就是会话历史保存的方式。

ChatMemory接口声明如下:

Java
public interface ChatMemory {

    // TODO: consider a non-blocking interface for streaming usages

    default void add(String conversationId, Message message) {
       this.add(conversationId, List.of(message));
    }

    // 添加会话信息到指定conversationId的会话历史中
    void add(String conversationId, List<Message> messages);

    // 根据conversationId查询历史会话
    List<Message> get(String conversationId, int lastN);

    // 清除指定conversationId的会话历史
    void clear(String conversationId);

}

可以看到,所有的会话记忆都是与conversationId有关联的,也就是会话Id,将来不同会话id的记忆自然是分开管理的。

目前,在SpringAI中有两个ChatMemory的实现:

  • InMemoryChatMemory:会话历史保存在内存中
  • CassandraChatMemory:会话保存在Cassandra数据库中(需要引入额外依赖,并且绑定了向量数据库,不够灵活)

我们暂时选择用InMemoryChatMemory来实现。

1.4.2.添加会话记忆Advisor

CommonConfiguration中注册ChatMemory对象:

Java
@Bean
public ChatMemory chatMemory() {
    return new InMemoryChatMemory();
}

然后添加MessageChatMemoryAdvisorChatClient

Java
@Bean
public ChatClient chatClient(OllamaChatModel model, ChatMemory chatMemory) {
    return ChatClient.builder(model) // 创建ChatClient工厂实例
            .defaultSystem("您是一家名为“黑马程序员”的职业教育公司的客户聊天助手,你的名字叫小黑。请以友好、乐于助人和愉快的方式解答学生的各种问题。")
            .defaultAdvisors(new SimpleLoggerAdvisor()) // 添加默认的Advisor,记录日志
            .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
            .build(); // 构建ChatClient实例
}

OK,现在聊天会话已经有记忆功能了,不过现在的会话记忆还是不完善的,我们还有继续补充

1.5.会话历史

会话历史与会话记忆是两个不同的事情:

会话记忆:是指让大模型记住每一轮对话的内容,不至于前一句刚问完,下一句就忘了。

会话历史:是指要记录总共有多少不同的对话

以DeepSeek为例,页面上的会话历史:

img

在ChatMemory中,会记录一个会话中的所有消息,记录方式是以conversationId为key,以List<Message>为value,根据这些历史消息,大模型就能继续回答问题,这就是所谓的会话记忆。

而会话历史,就是每一个会话的conversationId,将来根据conversationId再去查询List<Message>

比如上图中,有3个不同的会话历史,就会有3个conversationId,管理会话历史,就是记住这些conversationId,当需要的时候查询出conversationId的列表。

注意,在接下来业务中,我们以chatId来代指conversationId.

1.5.1.管理会话id(会话历史)

由于会话记忆是以conversationId来管理的,也就是**会话id(以后简称为chatId)。**将来要查询会话历史,其实就是查询历史中有哪些chatId.

因此,为了实现查询会话历史记录,我们必须记录所有的chatId,我们需要定义一个管理会话历史的标准接口。

我们定义一个com.itheima.ai.repository包,然后新建一个ChatHistoryRepository接口:

Java
package com.itheima.ai.repository;

import java.util.List;

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
package com.itheima.ai.repository;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.*;

@Component
@RequiredArgsConstructor
public class InMemoryChatHistoryRepository implements ChatHistoryRepository {

    private Map<String, List<String>> chatHistory;

    @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)) {
            return;
        }
        chatIds.add(chatId);
    }

    @Override
    public List<String> getChatIds(String type) {
        /*List<String> chatIds = chatHistory.get(type);
        return chatIds == null ? List.of() : chatIds;*/
        return chatHistory.getOrDefault(type, List.of());
    }
}

注意:

目前我们业务比较简单,没有用户概念,但是将来会有不同业务,因此简单采用内存保存type与chatId关系。

将来大家也可以根据业务需要把会话id持久化保存到Redis、MongoDB、MySQL等数据库。

如果业务中有user的概念,还需要记录userId、chatId、time等关联关系

1.5.2.保存会话id

接下来,修改ChatController中的chat方法,做到3点:

  • 添加一个请求参数:chatId,每次前端请求AI时都需要传递chatId
  • 每次处理请求时,将chatId存储到ChatRepository
  • 每次发请求到AI大模型时,都传递自定义的chatId
Java
import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY;

@CrossOrigin("*")
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {

    private final ChatClient chatClient;

    private final ChatMemory chatMemory;

    private final ChatHistoryRepository chatHistoryRepository;

    @RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")
    public Flux<String> chat(@RequestParam(defaultValue = "讲个笑话") String prompt, String chatId) {
    
        chatHistoryRepository.addChatId(chatId);
        return chatClient
                .prompt(prompt)
                .advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
                .stream()
                .content();
    }
}

注意,这里传递chatId给Advisor的方式是通过AdvisorContext,也就是以key-value形式存入上下文:

Bash
chatClient.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))

其中的*CHAT_MEMORY_CONVERSATION_ID_KEY是AbstractChatMemoryAdvisor中定义的常量key,将来MessageChatMemoryAdvisor执行的过程中就可以拿到这个chatId了。*

1.5.3.查询会话历史

接着,我们定义一个新的Controller,专门实现会话历史的查询。包含两个接口:

  • 根据业务类型查询会话历史列表(我们将来有3个不同业务,需要分别记录历史。大家的业务可能是按userId记录,根据UserId查询)
  • 根据chatId查询指定会话的历史消息

其中,查询会话历史消息,也就是Message集合。但是由于Message并不符合页面的需要,我们需要自己定义一个VO.

定义一个com.itheima.entity.vo包,在其中定义一个MessageVO类:

java
package com.itheima.ai.entity.vo;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.ai.chat.messages.Message;

@NoArgsConstructor
@Data
public class MessageVO {
    private String role;
    private String content;

    public MessageVO(Message message) {
        this.role = switch (message.getMessageType()) {
            case USER -> "user";
            case ASSISTANT -> "assistant";
            case SYSTEM -> "system";
            default -> "";
        };
        this.content = message.getText();
    }
}

然后在com.itheima.ai.controller包下新建一个ChatHistoryController

typescript
package com.itheima.ai.controller;

import com.itheima.ai.entity.vo.MessageVO;
import com.itheima.ai.repository.ChatHistoryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/history")
public class ChatHistoryController {

    private final ChatHistoryRepository chatHistoryRepository;

    private final ChatMemory chatMemory;

    /**
     * 查询会话历史列表
     * @param type 业务类型,如:chat,service,pdf
     * @return chatId列表
     */
    @GetMapping("/{type}")
    public List<String> getChatIds(@PathVariable("type") String type) {
        return chatHistoryRepository.getChatIds(type);
    }

    /**
     * 根据业务类型、chatId查询会话历史
     * @param type 业务类型,如:chat,service,pdf
     * @param chatId 会话id
     * @return 指定会话的历史消息
     */
    @GetMapping("/{type}/{chatId}")
    public List<MessageVO> getChatHistory(@PathVariable("type") String type, @PathVariable("chatId") String chatId) {
        List<Message> messages = chatMemory.get(chatId, Integer.MAX_VALUE);
        if(messages == null) {
            return List.of();
        }
        return messages.stream().map(MessageVO::new).toList();
    }
}

OK,重启服务,现在AI聊天机器人就具备会话记忆和会话历史功能了!

2.其它功能


To Be Continued.
📗PromptEngineer和ToolCalling
面试笔录 - 2026项目面试技巧与面经