目前,相信所有的人和企业都有一个认知,在现在或者未来,我要在工作中使用 AI,我的企业也要在运行中使用 AI

但是,由于每个人对大模型的认知程度不一,以上的“ 我要用 AI!”其实就等于“我要进步”这种比较模糊的认知,真正到应用的时候,这种认知是不够的。

大模型技术之所以受到广泛关注,源于它们在处理和生成自然语言、图像、视频等复杂数据类型方面所展现的非凡能力。这些模型通过分析海量数据,能够挖掘出深层的模式和关联,从而在多样化的任务上实现突破性的性能提升。无论是作为智能助手、自动驾驶系统的决策支持,还是应用于医疗诊断和股市预测,大模型都在助力 AI 技术向更广泛的行业渗透,为数字化转型注入源源不断的动力。

大模型

什么是大模型

大模型,主要称之为大型语言模型(Large Language Models),是一种基于人工智能和机器学习技术构建的先进模型,旨在理解和生成自然语言文本。

这些模型通过分析和学习海量的文本数据,掌握语言的结构、语法、语义和上下文等复杂特性,从而能够执行各种语言相关的任务。它们能够学习并执行多种复杂的任务,包括但不限于文本生成、问答、文本摘要、翻译、情感分析等。更重要的是,大模型通常具有较强的泛化能力,这使得它们可以跨领域应用,不仅限于特定的单一任务。

AI 大型模型是大数据强大计算能力先进算法相结合的产物。这种模型通过预训练大规模两个阶段来提升其性能,意味着在经过大规模数据集的预训练之后,模型能够在不需要额外微调或只需少量数据微调的情况下,直接应用于多种不同的场景。

这些大型人工智能模型标志着人工智能向通用智能迈出的重要一步。它们的实际应用将人工智能的核心要素从传统的数据、算法、算力扩展到了场景、产品、算力

在经历了基于数据的互联网时代和基于算力的云计算时代之后,我们正步入一个以大型模型为基础的人工智能新时代。

核心特征

  • ‌参数量庞大‌:大模型的参数规模通常达到数十亿甚至数万亿级别。例如,GPT-3 拥有约1750亿个参数。
  • ‌大规模数据预训练‌:大模型在通用数据(如互联网文本、图像)上进行预训练,学习广泛知识,再针对特定任务微调。
  • ‌通用能力与泛化性‌:大模型不仅能完成单一任务,还能解决多种任务,表现出较强的通用性和泛化能力。
  • ‌零样本或少样本能力‌:经过充分预训练后,大模型仅通过简单提示即可完成新任务,无需额外训练。

发展历程

从最初的概念到如今的成熟应用,大模型技术已经走过了一段充满创新和突破的历史。它的起源可以追溯到机器学习和人工智能的早期阶段,但真正作为独立研究领域崭露头角,是在21世纪的第二个十年。

image-20251031113456802

在这一时期,大模型技术的发展历程是一段充满创新和突破的历史。每一个里程碑都标志着深度学习在图像识别领域的重大突破,也为大模型的发展注入了新的动力。

  • 2006年,深度学习技术开始受到关注。Geoffrey Hinton 及其团队的研究成果,展示了如何利用非监督学习方法来训练深度神经网络,这为后续的大模型技术奠定了基础。
  • 2012年,AlexNet 模型在 ImageNet 竞赛中的压倒性胜利,不仅标志着深度学习在图像识别领域的重大突破,也为大模型的发展注入了新的动力。
  • 随后的2013至2014年,Word2VecGloVe 等词嵌入模型的出现,推动了自然语言处理领域的发展,为大模型在语言理解方面的应用奠定了坚实的基础。
  • 2018年,OpenAI 的成立和谷歌 BERT 模型的发布,成为大模型发展史上的两个关键里程碑。BERT 模型在多项自然语言处理任务上的突破性性能,展示了大模型的强大能力,并促进了后续模型的快速发展。
  • 2019年,OpenAI 发布了 GPT-2,一个拥有15亿参数的模型,能够生成连贯且上下文相关的文本,证实了大模型在语言生成方面的巨大潜力。
  • 2020年,谷歌的 T5 模型通过将各种自然语言处理任务统一到一个框架下的能力,证明了大模型的灵活性和泛化能力。
  • 2021年,OpenAI 发布的 DALL-E 模型能够根据文本描述生成新颖且高质量的图像,标志着大模型在多模态领域的一次飞跃。
  • 2022年,ChatGPT 的发布引起了广泛关注,其自然语言交互能力在多个领域展现了巨大潜力。
  • 2023年,GPT-4 的发布进一步提升了大模型的多模态理解和生成能力。
  • 2024年,发布的主要大模型包括文心一言、豆包、Kimi、讯飞星火、智谱清言、通义千问和混元。
  • 2025年,DeepSeek-R1 正式发布,该模型在发布后迅速引起了广泛关注,并在人工智能领域取得了重要突破。

国内外企业发展状况

自2022年底 ChatGPT 问世以来,AI 大模型成为全球科技行业的热点。在我国,科技企业迅速跟进,有百模大战的趋势。国内外企业纷纷加入竞争,其中美国和中国发布的通用大模型总数占全球发布量的80%。

b19ed7d4-217a-416b-9555-4d0eda4b74f4

OpenAI 已经成为了这一领域的领头羊,推动了大型模型的发展。继推出多模态大型模型 GPT-4 之后,微软公司通过投资和合作 OpenAI,已经将其 Office 办公产品系列与 OpenAI 的技术进行了全面整合,并在3月底推出了 Copilot Office。5 月 24 日,微软进一步宣布 Windows 11 将整合 GPT-4 技术。谷歌在 5 月 10 日推出了新一代大型模型 PaLM 2,并已经将超过 25 个 AI 产品和功能与 PaLM 2 整合,包括原有的对话机器人 BardAI+ 办公助手 Duet AIAI+ 搜索引擎等。Meta 也推出了大型模型 LLaMA,参与到这场竞赛中来。亚马逊则与人工智能初创公司 Hugging Face 合作,开发了 ChatGPT 的竞争对手——BLOOM

在国内,产业界、投资界和研究机构的各方力量都在加速布局大型模型领域。首先,国内科技龙头企业纷纷发布自主研发的大型模型。百度推出了大模型文心一言,阿里巴巴发布了首个超大规模语言模型通义千问,智谱 AI 也率先进行了商业化的落地,推出了不同参数量级的商业化模型 ChatGLM 系列。深度求索在2025年1月重磅推出 DeepSeek-R1 大模型,被称之为大模型中的 PDD

其次,投资和创新界也在积极参与大型模型的竞争。美团联合创始人王慧文携带5000万美元资金进入 AI 大模型领域,搜狗前 CEO 王小川和前 COO 茹丽云共同创立了百川智能,澜舟科技发布了其语言生成模型——孟子 MChat 可控大模型,西湖心辰也推出了心辰 Chat 大模型。

最后,高校和科研机构也在积极布局大型模型。复旦大学推出了国内首个类 ChatGPT 大模型 MOSS ,清华大学知识工程实验室与其技术成果转化公司智谱 AI 发布了 ChatGLM,中科院自动化所推出了多模态大模型紫东太初,IDEA 研究院 CCNL 推出了开源通用大模型“姜子牙”。

2022年及以后,大模型技术持续发展,模型规模不断增大。研究者们开始更多地关注模型的效率、可解释性和伦理问题,预示着大模型技术正朝着更加成熟和负责任的方向发展。

发展趋势

在参数规模方面,人工智能大型模型已经经历了预训练模型、大规模预训练模型和超大规模预训练模型三个发展阶段。每年参数规模至少增长10倍,实现了从亿级到万亿级的飞跃。目前,千亿级参数规模的大型模型已经成为主流。

从技术架构来看,Transformer 架构已经成为大模型领域的主流算法架构基础,由此衍生出 GPTBERT 两条主要技术路线。BERT 最著名的应用项目是谷歌的 AlphaGo。随着 GPT-3.0 的发布,GPT 逐渐成为大模型的主流路线。目前,几乎所有参数规模超过千亿的大型语言模型都采用 GPT 模式,例如百度的文心一言,阿里发布的通义千问等,还有结合两者优势智谱华章发布的 GLM 大模型。

在模态支持方面,人工智能大型模型可以分为自然语言处理大模型、计算机视觉大模型、科学计算大模型等。这些模型支持的模态更加多样化,从支持文本、图片、图像、语音等单一模态下的单一任务,逐渐发展为支持多种模态下的多种任务。

在应用领域方面,大模型可以分为通用大模型和行业大模型两种。通用大模型具有强大的泛化能力,可以在不进行微调或只需少量微调的情况下完成多场景任务,相当于 AI 完成了“通识教育”。例如 ChatGPT 、智谱的 ChatGLM 都是通用大模型。行业大模型则是利用行业知识对大模型进行微调,让 AI 完成“专业教育”,以满足在能源、金融、制造、传媒等不同领域的需求。

当前,人工智能大型模型的发展正从以不同模态数据为基础,逐渐转向与知识、可解释性、学习理论等方面相结合,呈现出全面发力、多点开花的新格局。

什么是本地大模型

本地大模型(Local Large Model)是指不依赖于云端计算资源,而是部署和运行在本地设备上的大规模人工智能模型。‌这些模型通常具有较强的计算能力和存储需求,因此通常在高性能的本地硬件(如高性能计算机、专用服务器、甚至边缘设备)上运行。

与传统的云端大模型不同,本地大模型将数据处理和推理任务限制在本地,不依赖外部服务器或云平台。‌

优缺点

‌优点‌:

  • ‌数据安全和隐私保护‌:本地大模型不需要将数据上传到云端,减少了数据泄露的风险。
  • ‌减少依赖‌:不需要稳定的网络连接,适合网络环境较差的地区。
  • ‌降低延迟‌:数据处理和推理在本地进行,减少了网络延迟。

缺点‌:

  • ‌硬件要求高‌:本地大模型需要高性能的硬件支持,增加了部署成本。
  • ‌维护复杂‌:需要专业的技术支持来安装、配置和优化模型,增加了维护难度。
  • ‌资源占用大‌:本地大模型占用大量的计算资源和存储空间,可能对设备性能有较高要求。

本地部署意味着需要一次性投资于高性能的计算硬件。尽管这可能增加一些用户的经济成本,但它提供了长期的成本效益,尤其是对于那些有持续高强度使用需求的用户。个人用户通过本地部署能够获得更大的控制权和自定义能力,这可能对于研究人员或开发者特别有价值。然而,需要注意的是,本地部署也意味着用户必须具备一定的技术能力来配置和维护系统。

应用场景

  • ‌企业内部分析和决策支持‌:在企业内部部署本地大模型,用于数据分析、预测和市场趋势研究。
  • ‌教育和科研‌:在实验室或研究机构内部署本地大模型,用于复杂的模拟和计算任务。
  • ‌工业自动化‌:在工厂或生产线上部署本地大模型,用于实时监控和优化生产过程。

硬件需求

本地大模型的推理性能和速度直接受限于个人或组织的硬件配置,如处理器、内存和存储空间等。要保证速度,需要显卡和高速内存,一般而言,显卡是必须的。

具体内存来说:

  • 对于70亿参数的模型,推荐至少配备 16GB 的系统内存(RAM)。
  • 130亿参数的模型,建议使用 32GB 内存。
  • 对于700亿参数的模型,一般推荐使用 64GB 内存,尽管有报告称 32GB 内存也能运行,但可能会非常卡顿。

对于运算处理,更多的依赖 CPUGPU

  • CPU:中央处理单元是通用计算的核心,设计用于处理多任务、逻辑性强的事物处理和串行计算。
  • GPU:图形处理单元包含成百上千个较小、更专用的处理核心,这使得它在执行并行任务时更加高效,例如大规模数学计算和数据处理。量化后的模型显存需求:即使是经过量化的模型(如 ChatGLM2-6B INT4),也需要至少 5GB 以上的显存。

这种并行处理能力使 GPU 特别适合于机器学习和深度学习模型训练,因为这些任务通常涉及大量的矩阵运算和数据并行处理,而 CPU 在处理这类任务时会受到其串行计算限制的影响。以 MacOS 为例,虽然 Macm 系列芯片集成了 CPU GPU 的功能,但 GPU 的性能远不如独立显卡提供的性能。所以对 Mac 而言,只适合做一些中轻量级的机器学习任务。

Ollama 技术

Ollama 是一个强大的本地大语言模型运行框架,它让用户能够在本地设备上轻松运行和管理各种大语言模型。它支持多种编程语言(如 PythonJava 等)和框架(如 TensorFlowPyTorch 等)。

  • 能直接运行大模型,与大模型进行对话。
  • ollama 命令具有管理大模型的能力。
  • 利用 cpu 运行大模型。
  • 本地大模型安全可靠。
  • 终端直接开始聊天。
  • 社区提供了支持 web api 方式访问 WebUI
image-20250317142438100

Ollama 的主要特点包括: 简单易用,Ollama 提供了简洁的 API 和命令行工具,使得模型部署变得简单快捷。

对于开发的同学来说,我们知道目前最常用的 dockerdocker 是一个容器环境,我们所有的应用就是打包的 docker 镜像,然后运行在 docker 环境里面的。

所以结合 ollama 的解释,就是 ollama 就相当于一个 docker 环境,也就是 AI 大模型的管理平台,大部分开源的大模型,我们可以直接在 ollama 上进行安装部署。这样子的理解是不是比较清楚了。

Ollama 安装

  1. 下载 Ollama

    首先我们到 Ollama 官网去下载安装包,此处我们下载的是 Windows 版本的安装包,如下图所示:

    image-20250317143144735
  2. 安装 Ollama

    安装 Ollama 的时候,有两种安装方式,分别:

    • 命令行方式

      之所以推荐使用命令行方式安装,是因为通过鼠标安装的话,默认会安装到 C 盘。通常情况下,我们不希望安装到 C 盘中,此时我们就可以通过使用命令的方式将 Ollama 安装到其他盘的某个目录下。

      以管理员身份运行 CMD ,并定位到 OllamaSetup.exe 所在的目录(假设 OllamaSetup.exeE:\MySoftware\Ollama 目录下并且也想安装到该目录),然后执行如下命令:

      OllamaSetup.exe /DIR=E:\MySoftware\Ollama

      然后 Ollama 就会进入安装界面

      image-20250317144651056

      点击 Install 后,可以看到 Ollama 的安装路径就变成了我们指定的目录了,这样大模型数据包也会默认下载在指定目录中。

      image-20250317144726388

      等待片刻后,Ollama 就会安装成功!

    • 鼠标双击方式

      我们直接双击安装包,然后点击 Install 按钮等待安装完成即可。

      默认情况下,Ollama 会安装在 C 盘的 C:\Users\%username%\.ollama\ 目录下,大小大概有 4GB

    安装成功后,打开 CMD 窗口,输入:

    ollama -v

    出现如下的效果,就是安装成功了。

    image-20250317151256671

    推荐使用命令方式来安装,好处是可以修改安装的目录位置,鼠标双击方式是不能修改安装的目录位置的(默认安装在 C 盘)。

  3. 修改大模型存储位置

    这步不是必须的。默认情况下,下载下来的大模型会存放在你的安装目录下。但是,有些大模型还是比较大的,存放在 C 盘下还是比较浪费空间。那么如何将大模型存放到另一个指定位置。

    • 手动创建存放目录

      首先先在你想要存储的盘下创建好存储目录,比如我想存到 E 盘下面,我这里就创建了该目录:E:\MySoftware\Ollama

      image-20250317150201916
    • 增加环境变量

      然后鼠标右键我的电脑–>属性–>高级系统设置–>环境变量,新建一个环境变量:

      OLLAMA_MODELS :E:\MySoftware\Ollama\models
      image-20250317150309707
    • 复制转移大模型存储目录

      创建完环境变量后,把 Ollama 停止,然后将安装目录下的 models 复制到新建的存储目录下。复制完成后要删除原有目录下的 models文件夹。

      image-20250317151021517
    • 重启 Ollama

      重启 Ollama,打开 CMD 输入 Ollama list,查看大模型资源包是否能正常显示,显示正常则迁移完成,也可以直接和大模型进行提问,能回复说明也正常。

Ollama 指令

Ollama 的指令还是比较少的,学习起来也比较简单,在命令窗口下输入:

ollama help

会看到 Ollama 的相关指令。注意: Ollama 需要启动。

image-20250317151901926

具体命令解释:

  • 启动 Ollama 服务器

    用于启动 ollama 服务器。

    ollama serve
  • 创建模型

    使用 Modelfile 文件来创建一个新模型。你需要提供一个包含模型信息的 Modelfile 文件。

    ollama create /path/to/Modelfile
  • 显示模型信息

    ollama show 模型名称
    image-20250317153250730
  • 下载模型

    从模型注册表中拉取一个模型,供本地使用。

    ollama pull 模型名
  • 启动模型

    运行一个已安装的模型,执行某些任务。

    ollama run 模型名
  • 删除模型

    删除一个已安装的模型。

    ollama rm 模型名
  • 获取模型列表

    列出本地所有可用的模型。

    ollama list
  • 获取正在运行模型列表

    显示当前正在运行的模型列表。

    ollama ps
  • 注册模型

    将本地模型推送到模型注册表中,以便他人或其他系统使用。

    ollama push 模型名
  • 停止模型

    停止正在运行的模型

    ollama stop 模型名
  • 复制模型

    复制一个模型到另一个位置或给定名称的地方。

    ollama cp 模型名 新模型名
  • 查看帮助

    获取 ollama 命令或任何具体命令的帮助信息。

    ollama help # 获取 ollama 主命令的帮助信息
    ollama run --help # 获取 run 命令的帮助信息
  • 获取版本信息

    显示当前 ollama 工具的版本信息。

    ollama --version 
    ollama -v

部署本地大模型

下来,我们演示下,如果通过 Ollama 部署当下火爆的 Deepseek 大模型到本地上。

获取大模型命令

进入 Ollama 官网,找到 Models ,进去后就能看到:

image-20250317155724922

点击 deepseek-r1 ,进入模型具体页面:

image-20250317155829518

选择适合自己电脑的模型配置,右侧会生成拉取命令。

ollama run deepseek-r1:1.5b

拉取大模型

进入电脑的 CMD 窗口,执行刚才获取到的命令,我这里运行的是 7b 模型。

image-20250317160607075

开始慢慢等待吧,拼网速的时候到了!

友情提示:不止13分钟哦,下到最后会非常慢的。

运行大模型

下载好了后,我们就可以开始和大模型聊天了。

image-20250317163114987

结束聊天输入:

/bye

就会退出聊天模式。

DeepSeek 大模型

2025年的过年期间,相信大家已经被国产的 DeepSeek 这款人工智能大模型刷屏,随着人工智能的迅猛发展,DeepSeek成为了一款炙手可热的大语言模型(LLM)。但许多同学对它的认识还停留在聊天机器人阶段,或误以为它能无所不知

那么,DeepSeek 真的如此神奇吗?同学们该如何正确认识和使用它?如果想从事 LLM 研究,又该如何入门?

DeepSeek 是什么

DeepSeek 是一款由杭州深度求索人工智能基础技术研究有限公司开发的人工智能模型。它的英文名 DeepSeek 可以读作深度探索,寓意着通过深度学习技术探索未知的领域。

这款基于 Transformer 架构的人工智能助手,具备自然语言理解与生成、多轮对话管理、跨领域知识整合及代码处理等核心能力,依托大规模预训练和持续学习机制,支持中英文等语言交互,能够辅助信息检索、学习研究、创意生成等场景。

DeepSeek 的第一个开源模型于 2023 年 11 月发布,而最近火出圈的 DeepSeek-v3DeepSeek-R1DeepSeek 最新的两个大模型。

DeepSeek 在一年的时间里持续发布新产品,如下图所示:

图片

DeepSeek 的核心技术

  • Transformer 结构:深度学习领域的革命性模型,支持长文本理解和生成。

  • 混合专家稀疏模型(MoE): 采取 “术业有专攻”理念,每次让若干个合适专家协作发挥各自能力,完成特定任务,具有更快的推理速度。

  • 自回归语言建模:基于之前的文本预测下一个词,从而实现连贯的对话。

  • 大规模训练:使用互联网公开数据训练,涵盖广泛的知识领域。

  • 基于人类反馈的强化学习(Reinforcement Learning from Human Feedback, RLHF):结合人类反馈,让模型回答更符合人类期望。

尽管 DeepSeek 模型仍然基于 Transformer 架构,并非颠覆性基础理论创新,但在语言模型的设计及其训练方式上,DeepSeek 有许多创新之处。

DeepSeek 模型分类

DeepSeek 是近年来备受关注的 AI 研究团队,推出了一系列先进的深度学习模型,涵盖了大语言模型(LLM)、代码生成模型、多模态模型等多个领域。下来我们将介绍 DeepSeek 旗下的不同类别的模型,帮助你更好地理解它们的特点和应用场景。

大语言模型

DeepSeek LLMDeepSeek 推出的通用大语言模型,主要用于文本生成、文本理解、对话交互等任务。这些模型采用 Transformer 架构,并经过大规模的预训练和指令微调,以提供更自然、智能的文本处理能力。

主要特性:

  • 支持多种任务:问答、文本补全、翻译等。
  • 经过 RLHF(人类反馈强化学习)优化,提高回答质量。
  • 适用于各种 NLP 任务,如聊天机器人、智能客服、文本摘要等。

训练方式:它是在包含 2 万亿个英文和中文标记的庞大数据集上从头开始训练的。

代码生成模型

DeepSeek Coder 是针对编程任务优化的代码生成和理解模型,可用于代码补全、代码解释、自动修复等。

主要特性:

  • 支持多种编程语言,如 PythonJavaC++JavaScript 等。
  • 能够基于自然语言描述生成代码,提升开发效率。
  • 代码补全和重构能力强,可用于 IDE 插件或自动化开发工具。

训练方式:基于 DeepSeek LLM 模型继续运行得到的。

多模态模型

DeepSeek-VL(Vision-Language)DeepSeek 推出的多模态 AI 模型,能够处理文本、图像等不同模态的数据,实现跨模态的理解与生成。

主要特性:

  • 能够根据文本生成图像,支持 AI 绘画任务。
  • 具备图像理解能力,可以进行图片标注、OCR 识别等。
  • 适用于 AIGC(人工智能生成内容)、数字创意等领域。

模型类别:DeepSeek-VLDeepSeek-VL2

数学推理模型

DeepSeek Math 主要针对数学推理任务优化,适用于数学问题求解、公式推导、数学建模等任务。

主要特性:

  • 适用于解方程、数学证明、数值计算等任务。
  • 结合符号推理和深度学习,提高数学问题的解答能力。
  • 可用于数学教育、科学研究等领域。

训练方式:基于代码领域模型 DeepSeek-Coder-v1.5 进行初始化,而不是从通用语言模型开始。

混合专家模型

DeepSeek MoEMixture of Experts)采用专家混合架构,在计算效率和模型能力之间取得平衡,适用于大规模推理任务。

主要特性:

  • 采用 MoE 机制,提高计算效率。

  • 适用于超大规模 NLP 任务。

  • 结合多个子模型,根据任务动态分配计算资源。

不同版本, DeepSeek-V2DeepSeek-V2-LiteDeepSeek-V3 ….

对话模型

DeepSeek Chat 是专门针对对话任务优化的聊天 AI,旨在提供更自然、更符合人类沟通习惯的交互体验。

主要特性:

  • 经过 RLHF 训练,提高对话的连贯性和可控性。
  • 适用于 AI 助手、智能客服、社交聊天等应用场景。
  • 支持多轮对话记忆,提升用户体验。

训练方式:基于 DeepSeek-V2 模型。

DeepSeek-R1 模型

DeepSeek-R1,是深度求索研发的推理模型。DeepSeek-R1 采用强化学习进行后训练,旨在提升推理能力,尤其擅长数学、代码和自然语言推理等复杂任务。

2024年11月20日,DeepSeek-R1-Lite 预览版正式上线。2025年1月20日,深度求索正式发布 DeepSeek-R1 模型,并同步开源模型权重。2025年1月31日,DeepSeek R1 671b 已作为英伟达NIM微服务预览版发布。

DeepSeek-R1 拥有卓越的性能,在数学、代码和推理任务上可与 OpenAI o1 媲美。其采用的大规模强化学习技术,仅需少量标注数据即可显著提升模型性能。该模型完全开源,采用 MIT 许可协议,并开源了多个小型模型,进一步降低了 AI 应用门槛,赋能开源社区发展。

2025年1月27日,DeepSeek 应用登顶苹果中国地区和美国地区应用商店免费 APP 下载排行榜,在美区下载榜上超越了 ChatGPT 。截至2025年1月27日,在世界大模型排名 Arena 上,DeepSeek-R1 基准测试升至全类别大模型第三,在风格控制类模型分类中与 OpenAI o1 并列第一。其竞技场得分达到1357分,略超 OpenAI o1 的1352分。

2025年1月31日,英伟达官宣,DeepSeek-R1 模型已作为 NVIDIA NIM 微服务预览版,在英伟达面向开发者的网站上发布。2月18日消息,百度搜索已全量上线 DeepSeek-R1 满血版模型。2月,快影正式接入 DeepSeek-R1 满血版。 3月17日消息,快手可灵 AI 已全面接入 DeepSeek-R1

DeepSeek-R1 模型性能要求

以下是 DeepSeek-R1 各版本配置要求的汇总表格,综合多个来源整理:

版本 CPU要求 内存要求 存储要求 显卡要求(显存) 适用场景
DeepSeek-R1-1.5B 4核(如i5/Ryzen 5)‌ 8GB‌ 12-256GB 非必需/可选4GB‌ 教育演示、简单文本生成‌
DeepSeek-R1-7B 8核(如i9/Ryzen 7)‌ 16GB‌ 32-256GB 8GB+(RTX 3060/3070)‌ 内容创作、客户服务‌
DeepSeek-R1-8B 8核+ 16GB+‌ 256GB+‌ 8GB+‌ 代码生成、逻辑推理‌
DeepSeek-R1-14B 12核+(如Ryzen 9)‌ 32GB‌ 80-256GB‌ 16GB+(RTX 4090)‌ 企业级应用、长文本生成‌
DeepSeek-R1-32B 16核+(EPYC)‌ 64GB‌ 250-320GB 24GB+(A100)‌ 专业领域知识问答、多模态任务‌
DeepSeek-R1-70B 32核(服务器级)‌ 128GB‌ 70GB+‌ 多卡并行(A100)‌ 科研机构/大型企业‌
DeepSeek-R1-671B 64核(集群)‌ 512GB‌ 300GB+‌ 多节点分布式‌ 超大规模生成任务‌

DeepSeek 的主要创新与贡献

  1. Multi-latent 注意力—— 通常情况下,LLM 是基于多头注意力机制(MHA)的 Transformer 架构。DeepSeek 团队开发了一种 MHA 机制的变体——多头潜在注意力机制,显著降低了计算和存储成本,把显存占用降到了其他大模型的5%-13%,极大提升了模型运行效率。

  2. GRPO 与可验证奖励——自从 OpenAI o1 发布以来,AI 社区一直在尝试复现其效果。由于 OpenAI 对其工作原理保持高度封闭,社区不得不探索各种不同的方法以实现类似 o1 的结果。有许多研究者尝试不同方法达到 o1的性能但这些方法最终被证明不如最初预期的那么有前景。另一方面,DeepSeek 展示了一个非常简单的强化学习(RL)流程实际上可以实现类似 o1 的结果。更重要的是,他们开发了自己版本的 PPORL 算法,称为 GRPO,这种算法更高效且性能更优。

  3. DualPipe—— DeepSeek 使用 FP8 混合精度加速训练并减少 GPU 内存使用,使用 DualPipe 算法(即将前向和后向计算与通信阶段重叠以最大限度地减少计算资源闲置)提升训练效率,并进行了极致的内存优化。他们开发了一套完善的数据处理流程,着重于最小化数据冗余,同时保留数据的多样性。

  4. “纯”强化学习——DeepSeek 是全球首个通过纯强化学习技术,成功复现了 o1 的能力。在此之前,几乎没有任何团队能够成功地将强化学习应用到大规模语言模型的训练中。更值得注意的是,DeepSeek-R1 并未局限于规则驱动的数学模型或算法,而是成功地将强化学习带来的强推理能力泛化到其他领域,使得用户在实际使用过程中能够感受到其在写作等任务中的卓越表现。DeepSeek-R1 训练过程示意图如下图所示:

    图片

  5. 小模型蒸馏——DeepSeek 的火爆把模型蒸馏技术再一次推到技术讨论前沿。简单来说“模型蒸馏”就是让小模型模仿大模型回答问题的结果,来提升自身能力。比如,在对一篇文章分类时,大模型认为该文章以85%、10%和5%的概率分别属于正面、负面和中性等不同情感类别。小模型就从大模型输出的结果中认真思考,不断调整参数,以期望继承大模型能力,从而输出类似结果。

  6. 开源!开源!开源!——与美国的 OpenAIChatGPT 不同。DeepSeek 将自己的研究与创新无私公开到人工智能社区,DeepSeek 的开源让全球的研究者能够快速建立起相关能力。如果说 ChatGPT 让全球看到了大模型的重要性,那么 DeepSeek 的开源则让全球研究者有机会亲身参与到强大推理能力的开发中。现在,每个人都可以利用这些进步来受益并改进自己的 AI 模型训练。

如何正确认识 DeepSeek

  • DeepSeek 并非全知全能

    DeepSeek 并非全能,它只是基于概率预测回答内容。因此,它的回答可能是错误的,甚至会编造信息(即幻觉现象)。

  • DeepSeek 不是知识库

    DeepSeek 信息更新可能滞后,不能直接访问某些受限数据(如论文数据库、专利系统等)。

  • DeepSeek 也有偏见

    由于训练数据来源于互联网,它可能会带有一定的社会偏见,因此在使用时要保持批判性思维,不盲目相信其结论。

正确心态:

DeepSeek 作为辅助工具,而非权威答案的唯一来源。

AI 的输出保持怀疑与验证,特别是涉及学术、科研或重要决策时。

DeepSeek 擅长总结与启发,但最终的学习成果还要依赖自己的思考与实践。

DeepSeek 不是解题神器,数学证明仍需要人工验证。

DeepSeek 不是编程外挂,生成的代码仍需要严格测试。

DeepSeek 是一款强大的 AI 助手,但并非完美,需要理性使用,使用者的认知范围决定 DeepSeek 的能力上限。正确使用 DeepSeek,提升学习效率,提高编程、写作、科研能力。

AI 发展过程中,美国 OpenAIGoogleMeta 等人工智能巨头也付出了许多努力或承担了更多责任,在为 DeepSeek 成功喝彩同时,我们也要为推动人工智能发展的所有努力而喝彩,每个人的努力均不可忽视。

记住:AI 不会取代人类,但会用 AI 的人类会取代不用 AI 的人类!

Spring AI

Spring AISpring 框架在人工智能领域的延伸,它旨在帮助开发者更高效地构建和部署 AI 应用。作为 Spring 生态系统的一部分,Spring AI 继承了 Spring 框架的诸多优点,如轻量级、松耦合、易于测试等,同时为 AI 应用开发提供了专门的支持和优化。

在经历了八个里程碑式的版本之后(M1~M8),SpringAl 1.0 正式版本终于在2025年5月20日正式发布了,这是另一个新高度的里程碑式的版本,标志着 Spring 生态系统正式全面拥抱人工智能技术,并且意味着 SpringAI 将会给企业带来稳定 API 支持。

image-20251107172220274

介绍

尽管 Python 最近成为了人工智能编程语言的首选,但是 Java 在人工智能领域的地位同样不可撼动,得益于强大的 Spring 框架。随着人工智能技术的快速发展,我们正处于一个创新不断涌现的时代。从智能语音助手到复杂的自然语言处理系统,人工智能已经成为了现代生活和工作中不可或缺的一部分。在这样的背景下,Spring AI 项目迎来了发展的机遇。尽管该项目汲取了 Python 项目如 LangChainLlamaIndex 的灵感,但 Spring AI 并不是简单的移植。该项目的初衷在于推进生成式人工智能应用程序的发展,使其不再局限于 Python 开发者。

Spring AI 的核心理念是提供高度抽象化的组件,作为开发 AI 应用程序的基础。这些抽象化组件具备多种实现,使得开发者能够以最少的代码改动便捷地交换和优化功能模块。

具体而言,Spring AI 提供了支持多种主流模型提供商的功能,包括 OpenAIMicrosoftAmazonGoogleHugging Face。支持的模型类型涵盖了从聊天机器人到文本生成、图像处理、语音识别等多个领域。而其跨模型提供商的可移植 API 设计,不仅支持同步和流式接口,还提供了针对特定模型功能的灵活选项。

此外,Spring AI 还支持将 AI 模型输出映射为 POJO,以及与主流向量数据库提供商无缝集成的能力。其功能不仅局限于模型本身,还包括了数据工程中的 ETL 框架和各种便利的函数调用,使得开发 AI 应用程序变得更加高效和可靠。

应用场景

自然语言处理

Spring AI 提供了丰富的自然语言处理工具,开发人员可以利用这些工具来处理文本数据、执行情感分析、实现语音识别等功能。例如,你可以使用 Spring AI 构建一个智能客服系统,自动回答用户的问题,提高客户服务效率。

图像生成

在图像生成与处理方面,Spring AI 同样发挥着重要作用。以设计和广告行业为例,设计师可以利用 Spring AI 集成图像生成模型,如 OpenAIDALL - E 模型或 StableDiffusion 模型。当设计师需要为一个新产品设计宣传海报时,只需输入相关的文字描述,如 “一张充满科技感的智能手机宣传海报,手机位于画面中心,周围环绕着蓝色的光线和抽象的科技元素”,模型就能快速生成相应的图像。这不仅大大节省了设计时间,还能为设计师提供更多的创意灵感,帮助他们快速将创意转化为视觉图像。

数据分析与预测

在金融行业,利用 Spring AI 可以快速构建风险评估模型。通过集成机器学习模型,如逻辑回归、决策树等,对用户的信用数据、交易数据等进行分析,预测用户的信用风险,为金融机构的贷款审批、信用卡发放等业务提供决策支持。

在电商领域,Spring AI 可以帮助企业进行销售预测。通过分析历史销售数据、用户行为数据、市场趋势数据等,使用时间序列分析模型、神经网络等,预测未来的销售情况,帮助企业合理安排库存、制定营销策略。

知识图谱和智能问答

在智能搜索场景中,利用 Spring AI 构建知识图谱,将各种领域的知识进行结构化表示。当用户输入查询关键词时,系统不仅能够进行传统的文本匹配搜索,还能通过知识图谱理解关键词的语义和相关知识,提供更精准、全面的搜索结果。在智能客服场景中,知识图谱可以帮助客服机器人更好地理解用户问题,结合图谱中的知识和业务逻辑,给出更准确、智能的回答。

例如,在一个电子产品售后客服场景中,当用户询问某款手机的电池续航问题时,客服机器人可以通过知识图谱快速定位到该手机型号的电池参数、续航优化建议等相关知识,为用户提供详细的解决方案。

入门案例

基于 Spring AIOllamaDeepSeek-R1 构建本地 API 服务。开发代码前请务必启动 Ollama

SpringBoot 的版本建议为 3.5.3

引入依赖

创建一个 SpringBoot 项目,引入相关依赖



<parent>

<groupid>org.springframework.boot</groupid>

<artifactid>spring-boot-starter-parent</artifactid>

<version>3.4.5</version>
        <relativepath></relativepath>
    </parent>

<dependencymanagement>

<dependencies>

<dependency>

<groupid>org.springframework.ai</groupid>

<artifactid>spring-ai-bom</artifactid>

<version>1.0.0</version>

<scope>import</scope>

<type>pom</type>
            </dependency>

<dependency>

<groupid>org.springframework.boot</groupid>

<artifactid>spring-boot-dependencies</artifactid>

<version>3.4.5</version>

<scope>import</scope>

<type>pom</type>
            </dependency>
        </dependencies>
    </dependencymanagement>
        

注意:1.0.0-GA 版本的 ollama 依赖更换的名称,如果使用过去版本的需注意名称更换。

配置文件

创建配置文件 application.yml ,配置本地大模型相关信息。

spring:
  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        model: deepseek-r1:7b  # 下载的本地大模型 这里使用的是7b版本
        options:
          temperature: 0.8 # 模型温度

模型温度

温度参数(Temperature)是控制模型输出随机性的一个数值,其取值范围为0到2。以下是温度参数的核心作用:

  • 低温(接近0):模型输出更确定、保守,适合需要精准回答的任务。
  • 中温(接近1):模型输出在确定性和创造性之间取得平衡,适合通用任务。
  • 高温(接近2):模型输出更随机、多样化,适合需要高创意或灵活性的任务。

通俗理解:

  • 温度=0:模型会选择最可能的回答,输出非常确定。
  • 温度=1:模型会在多种可能的回答中随机选择,输出较为平衡。
  • 温度=2:模型会尝试更多非主流的选择,输出更具创意和多样性。

配置类

创建配置类,注册 ChatClient 组件。

@Configuration
public class AiClientConfig {

    @Bean
    public ChatClient ollamaClient(OllamaChatModel ollamaChatModel) {
        return ChatClient.builder(ollamaChatModel).build();
    }
}

控制类

创建 controller 下的类,用于访问信息。

@RestController
@RequestMapping(&quot;/ai&quot;)
public class AiController {
    @Resource
    private ChatClient ollamaClient;

    // 普通返回 
    @GetMapping(&quot;/call&quot;)
    public String call(@RequestParam(&quot;message&quot;) String message) {
        return ollamaClient.prompt().user(message).call().content();
    }

    // 流式返回
    @GetMapping(value = &quot;/stream&quot;, produces = &quot;text/html;charset=utf-8&quot;)
    public Flux&lt;String&gt; stream(@RequestParam(&quot;message&quot;) String message) {
        return ollamaClient.prompt().user(message).stream().content();
    }
}

效果展示

  1. 普通返回效果

    image-20250918141957536
  2. 流式返回效果

    2025-09-18-142345

注意:建议使用浏览器测试,流式效果比较明显。

两个常用类

Spring AI 框架中,ChatModelChatClient 都可以实现大模型的文本生成功能,例如聊天机器人,但二者是两种不同层级的 API 封装,分别针对不同的开发场景和需求设计。

功能定位

  • ChatModel 类 直接与具体的大语言模型进行交互,提供基础的 call()stream() 方法,用于同步或流式调用模型,具体使用如下。

    String result1 = chatModel.call(message);
    Flux<String> result2 = chatModel.stream(message);

    它的特点是:使用简单、灵活性高。但需要开发者手动处理提示词组装、响应解析、参数配置等细节,适合处理简单的大模型交互场景。

  • ChatClient

    基于 ChatModel 构建,功能强大、开发效率高,通过流式 API 隐藏底层复杂性,提供链式调用的便捷接口,具体使用如下。

    String result1 = chatClient.prompt().user(message).call().content();
    Flux<String> result2 = chatClient.prompt().user(message).stream().content();

核心能力对比

维度 ChatModel ChatClient
交互方式 直接调用模型,需手动处理请求/响应 链式调用,自动封装提示词和解析响应
功能扩展 强,内置 Advisor 机制(如对话历史管理、RAG)
结构化输出 需手动解析响应文本 支持自动映射为 Java 对象(如 entity(Recipe.class))
适用场景 实现简单功能和场景 快速开发复杂功能的场景,如企业级智能客服、连接外部工具等

两者并非互斥,实际项目中可混合使用,例如用 ChatModel 处理常规请求,而通过注入 ChatClient 实现复杂场景。

ChatClient 详解

Spring AI 1.0 的核心是 ChatClient 接口,这是一个可移植且易于使用的 API,是与 AI 模型交互的主要接口。它支持调用 20 多种 AI 模型,从 AnthropicZhiPu AI,并支持多模态输入和输出(当底层模型支持时)以及结构化响应(通常以 JSON 格式,便于应用程序处理输出)。

实例产生

默认情况下,ChatClient 类并没有注入到 Spring 容器中。例如:当我们在某个类中引入 ChatClient 后,启动服务器会出现:

image-20250918153948317

所有,这里就需要我们手动产生 ChatClient 类的实例。这里可以使用 SpringBoot 的自动装配方式,也可以使用编程式方式。

  • 自动装配方式

    @Configuration
    public class AiClientConfig {
      @Bean
      public ChatClient chatClient(ChatClient.Builder chatClientBuilder) {
          return chatClientBuilder.build();
      }
    }
  • 编程式方式

    也可以通过编程式方式产生 ChatClient 对象。

    @Configuration
    public class AiClientConfig {
    
      @Bean(name = "ollamaChatClient")
      public ChatClient ollamaClient(OllamaChatModel ollamaChatModel) {
          return ChatClient.builder(ollamaChatModel).build();
      }
    }

ChatClient 的响应

ChatClient API 提供了多种方法来格式化来自 AI 模型的响应。

  1. 返回 ChatResponse

    来自 AI 模型的响应是一个由 ChatResponse 类型定义的丰富结构。它包括有关响应如何生成的元数据,并且还可以包含多个响应,每个响应都有自己的元数据。元数据包括用于创建响应的令牌数量(每个令牌约等于 3/4 个单词)。此信息很重要,因为托管的 AI 模型根据每个请求使用的令牌数量收费。

    下面通过调用 call() 方法后的 chatResponse() 方法展示了一个返回包含元数据的 ChatResponse 对象的示例。

    ChatResponse chatResponse = ollamaChatClient.prompt()
           .user("介绍一下宝鸡文理学院")
           .call()
           .chatResponse();
    image-20250918163826753
  2. 返回文本

    直接返回响应的文本信息,其实就是返回 ChatResponsetextContent 属性。

    String content = ollamaChatClient.prompt()
           .user("介绍一下宝鸡文理学院")
           .call()
           .content();
  3. 流式响应

    返回流式的响应信息,可以是文本信息,也可以是 ChatResponse 对象。

    Flux<String> result1 = ollamaChatClient.prompt()
           .user("介绍一下宝鸡文理学院")
           .stream()
           .content();
    
    Flux<ChatResponse> result2 = ollamaChatClient.prompt()
           .user("介绍一下宝鸡文理学院")
           .stream()
           .chatResponse();

背景设定

可以发现,当我们询问 AI 你是谁的时候,它回答⾃⼰是 DeepSeek-R1,这是⼤模型底层的设定。如果我们希望 AI 按照新的设定⼯作,就需要给它设置 System 背景信息。 在 SpringAI 中,设置 System 信息⾮常⽅便,不需要在每次发送时封装到 Message,⽽是创建 ChatClient 时指定即可。

@Bean(name = &quot;ollamaChatClient&quot;)
public ChatClient chatClient() {
    return ChatClient.builder(ollamaChatModel)
            .defaultSystem(&quot;你是张三,来自于宝鸡文理学院。请以大学生的幽默欢快的语气说话&quot;)
            .build();
}

运行后的效果是:

image-20250922113331659

多轮会话

大模型的对话记忆这一概念,根植于人工智能与自然语言处理领域,特别是针对具有深度学习能力的大型语言模型而言,它指的是模型在与用户进行交互式对话过程中,能够追踪、理解并利用先前对话上下文的能力。

此机制使得大模型不仅能够响应即时的输入请求,还能基于之前的交流内容能够在对话中记住先前的对话内容,并根据这些信息进行后续的响应。这种记忆机制使得模型能够在对话中持续跟踪和理解用户的意图和上下文,从而实现更自然和连贯的对话。

一个问题

我们有如下代码:

@Test
public void e11() {
    String message = &quot;a是9,b是7,它们之和是?&quot;;
    String content = ollamaChatClient.prompt().user(message).call().content();
    log.info(&quot;content:{}&quot;, content);
}

输出结果:

image-20250920192625890

如果我们继续提问:

@Test
public void e12() {
    String message = &quot;在乘以6是多少呢?&quot;;
    String content = ollamaChatClient.prompt().user(message).call().content();
    log.info(&quot;content:{}&quot;, content);
}

结果是:

image-20250920192717073

会发现,我们这里并没有记录着上一次的结果。

简单案例

我们本次演示最简单的上下文管理实现方式 – 手动拼接对话历史,后续交互将历史对话拼接后作为上下文。

@Test
public void e2() {
    String message = &quot;a是9,b是7,它们只和是?&quot;;
    message += &quot;在乘以6是多少呢?&quot;;
    String content = ollamaChatClient.prompt().user(message).call().content();
    log.info(&quot;content:{}&quot;, content);
}

可以发现结果为:

content:&lt;think&gt;
首先,给定两个数a=9和b=7。

计算它们的和:9 + 7 = 16。

然后将和乘以6:16 × 6 = 96。
&lt;/think&gt;

解答:

我们有以下数据:
\[
a = 9,\quad b = 7
\]

首先,计算 \(a\) 和 \(b\) 的和:
\[
a + b = 9 + 7 = 16
\]

接下来,将这个和乘以6:
\[
(a + b) \times 6 = 16 \times 6 = 96
\]

所以,最终的答案是:
\[
\boxed{96}
\]

基于 memory 的对话记忆

SpringAI 基于 ChatMemory 实现对话的记忆,也就是上下文内容的存储、发送请求时候附带。

ChatMemory 表示聊天对话记忆的存储,它提供添加消息到对话、从对话中检索消息以及清除对话历史的方法。

public interface ChatMemory {
    default void add(String conversationId, Message message) {
        this.add(conversationId, List.of(message));
    }
        // 添加消息到记忆
    void add(String conversationId, List&lt;Message&gt; messages);
        // 获取对话历史
    List&lt;Message&gt; get(String conversationId, int lastN);
        // 清除对话记忆
    void clear(String conversationId);
}

Spring AI 提供了 ChatMemory 的实例用于存储聊天上下文,默认使用内存的方式进行实现,将对话直接存储在内存中。

什么是会话 ID

SpringAI 中,conversationId 是用于标识和隔离不同对话会话的核心参数,通过该 ID 实现对话上下文的持久化与连续性管理。它有如下的一些核心作用:

  • 会话隔离:为每个对话分配唯一 ID,确保不同会话的数据独立存储和读取。 ‌
  • 上下文持久化:通过 ChatMemoryRepository 接口,将过滤后的消息(如用户输入、AI 回复)按 conversationId 存储,支持本地内存或 JDBC 数据库等存储方式。

三种角色

image-20251107111905458

在人工智能交互中,系统(System)、用户(User)和助手(Assistant)是三个关键角色。它们通过不同的提示词(Prompt)来引导模型的行为和输出,从而实现高效、精准的交互。

聊天模型中不同角色(role)的主要区别:

  1. System role (系统角色)

    • 用于设定 AI 助手的基础行为准则、个性特征和能力范围。

    • 类似于给 AI 助手的”说明书”或”操作手册”。

    • 通常在对话开始前设置,对整个对话过程都有效,一般设置一次。

    • 系统角色的内容通常不会直接与用户互动,但会对对话的走向产生间接影响,比如指定助理的语气、知识范围、以及对用户的帮助方式等。

    • 用户一般看不到 system prompt 的内容。

    • 例如:”你是一个专业的 Java 编程助手,需要用通俗易懂的方式回答问题”。

  2. User role (用户角色)

    • 代表与 AI 对话的人类用户,通常是向模型提问、提出需求或请求信息的用户。

    • 负责提出问题、要求或任务。

    • 引导对话的方向。

    • 用户的发言构成了模型接收到的输入,模型根据用户的发言生成相应的回复。

    • 例如:”请解释一下 Java 中的装饰器是什么?”。

  3. Assistant role (助手角色)

    • AI 助手的回应角色,这个角色代表模型本身,即用于为用户提供信息、回答问题或完成用户请求的助手。

    • 根据 system prompt 的指示来回应用户。

    • 保持一致的语气和个性。

    • 提供符合要求的答案或帮助。

    • Assistant 的目的是理解用户的问题并生成合适的回复,同时考虑到上下文和交互目标。

    • 例如:”让我用简单的方式来解释装饰器…”。

具体实现

  1. 书写 ChatMemoryConfig 配置类

    @Configuration
    public class ChatMemoryConfig {
    
       @Bean("inMemoryChatMemory")
       public ChatMemory chatMemory() {
           return MessageWindowChatMemory.builder()
                   .chatMemoryRepository(new InMemoryChatMemoryRepository()) // 对话记忆默认使用内存方式
                   .maxMessages(3) // 保存最近的三条历史记录
                   .build();
       }
    }

    注意:

    • 其实内存方式就是默认的方式,如果不自定义 ChatMemory 对象,默认的 ChatMemory 对象已经实现了内存记忆功能。
    • 保存的历史记录至少三条,因为会保存多个角色信息。
  2. 修改 OllamaConfig 配置类

    @Configuration
    public class OllamaConfig {
       @Resource
       private ChatModel ollamaChatModel;
    
       @Bean(name = "ollamaChatClient")
       public ChatClient chatClient() {
           return ChatClient.builder(ollamaChatModel)
                   .build();
       }
    
       // 具有记忆功能的 chatClient 
       @Bean("ollamaChatMemoryClient")
       public ChatClient ollamaChatMemoryClient(ChatMemory inMemoryChatMemory) {
           return ChatClient.builder(ollamaChatModel)
                        // 这里使用自定义的 inMemoryChatMemory 对象, 如果引入 chatMemory 对象就不需要第一步了
                   .defaultAdvisors(MessageChatMemoryAdvisor.builder(inMemoryChatMemory).build())
                   .build();
       }
    }

    修改 ChatClient 配置类,添加内存记忆功能。

    MessageChatMemoryAdvisorSpringAI 中用于处理会话历史记录的核心类。主要功能是将用户提出的问题和模型的回答添加到历史记录中,从而形成一个上下文记忆的增强机制。通过这种方式,系统能够更好地理解用户的需求,提供更加连贯和相关的响应。这对于提升聊天机器人的交互质量和用户体验至关重要。

  3. 控制层类

    @GetMapping(value = "/a", produces = "text/html;charset=utf-8")
    public Flux<String> a(@RequestParam("content") String content) {
       return ollamaChatClient.prompt()
               .user(content)
               .stream().content();
    }

    没有什么特别的地方。

  4. 演示效果

    image-20250920204734087

我们发现再次提问后,会在上一次的结果上进行运算,说明大模型具备了内存记忆功能。但是如果区分不同用户的提问?那么我们就要使用另一种写法,根据 conversationId 来实现这个功能。

具体代码实现如下:

@GetMapping(value = &quot;/history_memory_2&quot;, produces = &quot;text/html;charset=utf-8&quot;)
public String historyMemory(@RequestParam(&quot;question&quot;) String question, 
                            @RequestParam(&quot;id&quot;) String conversationId) {
    // 1. 初始化系统消息(仅首次)
    Message systemMessage = new SystemMessage(&quot;你是一名数学达人,擅长解决数学方面的各项问题&quot;);
    if (chatMemory.get(conversationId).isEmpty()) {
        chatMemory.add(conversationId, systemMessage);
    }
    // 2. 手动获取历史消息
    List&lt;Message&gt; historyMessages = chatMemory.get(conversationId);
    // 3. 获取用户消息
    Message userMessage = new UserMessage(question);
    // 4. 将新消息存放进来
    historyMessages.add(userMessage);
    chatMemory.add(conversationId, userMessage);
    // 5. 生成prompt对象
    Prompt prompt = new Prompt(historyMessages);
    // 6. 返回内容
    String result = ollamaChatClient.prompt(prompt).call().content();
    chatMemory.add(conversationId, new AssistantMessage(result));
    return result;
}

注意:这里如果自定义存储,就不要使用 ollamaChatMemoryClient 这个带内存实现的 client 了。并且手动方式返回不要使用流式。

基于数据库的对话记忆

SpringAI 提供了多种持久化的方式,这里讲解关系型数据库 MySQL 的操作。

  1. 添加依赖

    <dependency>
       <groupId>org.springframework.ai</groupId>
       <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
    </dependency>
  2. 创建存放对话消息的表记录

    resource 下创建 schema-mysql.sql 文件,书写如下创建表的语句。

    CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY
    (
       conversation_id VARCHAR(36) NOT NULL,
       content         TEXT        NOT NULL,
       type            VARCHAR(10) NOT NULL,
       <code>timestamp     TIMESTAMP   NOT NULL,
       CONSTRAINT TYPE_CHECK CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL'))
    );
    image-20250925155349044

    注意:不需要我们自行去创建该表,会在项目启动时创建。

  3. application.yml 配置

    spring:
     ai:
       chat:
         memory:
           repository:
             jdbc:
               schema: classpath:schema-mysql.sql
               # always:总是进行初始化(第一次启动)  never:从不进行初始化(非第一次启动)
               initialize-schema: always
     datasource:
       url: jdbc:mysql://localhost:3306/test
       username: root
       password: a123456
       driver-class-name: com.mysql.cj.jdbc.Driver
  4. ChatMemoryConfig 配置类

    @Slf4j
    @Configuration
    public class OllamaChatMemoryConfig {
        // 内存存储的memory
       @Bean("chatMemory")
       public ChatMemory chatMemory() {
           return MessageWindowChatMemory.builder()
                   .build();
       }
    
        // JDBC存储的memory
       @Resource
       private JdbcChatMemoryRepository jdbcChatMemoryRepository; // 自动装配
        // jdbc存储的memory
       @Bean("jdbcChatMemory")
       public ChatMemory jdbcChatMemory() {
           return MessageWindowChatMemory.builder()
                   .chatMemoryRepository(jdbcChatMemoryRepository)
                   .build();
       }
    }

    注意:SpringAIJDBC 存储提供了自动装配,可以直接在应用程序中使用。

  5. ChatClientConfig 配置类

    @Configuration
    public class OllamaChatClientConfig {
       @Resource
       private OllamaChatModel ollamaChatModel;
       @Resource
       private ChatMemory jdbcChatMemory;
    
       @Bean("ollamaChatClient")
       public ChatClient chatClient() {
           return ChatClient.builder(ollamaChatModel)
                   .defaultAdvisors(MessageChatMemoryAdvisor.builder(jdbcChatMemory).build())
                   .build();
       }
    }
  6. 控制类

    @GetMapping(value = "/history", produces = "text/html;charset=utf-8")
    public Flux<String> history(@RequestParam("question") String question, @RequestParam("conversationId") String conversationId) {
       return ollamaChatClient
               .prompt()
                    // 第一个参数是key,第二个参数是value
               .advisors(advisor -> advisor.param("chat_memory_conversation_id",conversationId))
               .user(question)
               .stream().content();
    }

    注意:

    • 第一个参数的 key 名称是固定的,不能更改。
    • 也可以使用上边基于内存的自定义 Message 写法实现。
  7. 测试效果

    第一次问问题:

    image-20250925160142530

    第二次问问题:

    image-20250925160209725

    第三次问问题:

    image-20250925160300995

    查看数据库表的效果:

    image-20250925160340219

我们会发现,第一次和第二次提问,能够连贯的进行记忆,第三次的问题,就没办法进行回答了。这是因为我们的 ID 不一样所导致的。

ChatMemoryRepository 接口

ChatMemoryRepository 接口是对话记忆存储的抽象。支持多种存储方式,例如:内存方式、JDBC 方式以及 Redis 方式等,每种实现方式都有特定的实现类。比如内存方式就是通过 InMemoryChatMemoryRepository 类实现的。

下来我们看看 ChatMemoryRepository 接口的源码:

public interface ChatMemoryRepository {
    // 获取所有活跃会话ID列表
    List&lt;String&gt; findConversationIds();
        // 根据会话ID获取消息列表
    List&lt;Message&gt; findByConversationId(String conversationId);
        // 根据会话ID全量替换存储,清除旧消息,录入新消息
    void saveAll(String conversationId, List&lt;Message&gt; messages);
        // 根据会话ID清除消息列表
    void deleteByConversationId(String conversationId);
}

Advisors 机制

SpringAI AdvisorsSpringAI 框架中用于拦截和增强 AI 交互的核心组件,其设计灵感类似于 WebFilter,通过链式调用实现对请求和响应的处理。

什么是 Advisor

SpringAI Advisors 是连接 AI 模型与业务逻辑的核心中间件,其设计理念与 SpringAOP 深度契合。她提供了一种灵活而强大的方法来拦截、修改和增强 Spring 应用程序中的 AI 驱动的交互。通过利用 Advisors API,开发人员可以创建更复杂、可重用和可维护的 AI 组件。例如:我们可能会创建聊天记录、排除敏感词或为每个请求添加额外的上下文。

其大致流程如下:

img

简单解释下大致流程:

  1. 我们将提示词发送给 AI 大模型。
  2. 大模型附加的 Advisor ,会依次执行链中每个 Advisorbefore 方法。
  3. 执行完所有的 before 方法后,交给大模型处理。
  4. 大模型处理后的响应,会依次执行链中每个 Advisorafter 方法。
  5. 执行完所有的 after 方法后,我们获得了最终的大模型响应内容,执行结束。

核心功能

  1. ‌请求和响应拦截‌

    通过 AroundAdvisor 接口动态修改聊天请求和响应,支持日志记录、内容转换等场景。处理流程遵循责任链模式,请求按顺序通过所有 Advisor,响应则逆序返回。

  2. 上下文共享‌

    通过 AdvisorContextAdvisor 链中传递数据,实现跨拦截器的状态共享。

  3. 多模型兼容性‌

    封装通用 AI 模式、例如如记忆管理、日志记录等,确保代码可复用且兼容不同大模型。

Advisor 的基本结构

我们可以来看一看官方提供的 Advisor 类图。

image-20251110094859903

我们可以看到核心接口 Advisor 有两个子接口,分别是:CallAdvisorStreamAdvisor。下来我们详细讲解体系结构中的一些接口和类。

  • Advisor 接口

    Advisor 接口是 advisor 体系中的顶层接口,它继承了 Ordered,用于方便制定 Advisor 链的执行顺序。

    public interface Advisor extends Ordered {
    
      int DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER = -2147482648;
    
      String getName();
    }

    该接口的主要作用就是用来声明当前的类是 Advisor 增强类,并可以声明自己的 advisor 名称。如果有多个 advisor 类,这些类按照其 getOrder() 值来排序。首先执行较低的值。自动添加的最后一个 advisor 将请求发送到大模型中。

    public interface Ordered {
      int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
      int LOWEST_PRECEDENCE = Integer.MAX_VALUE;
    
      int getOrder();
    }

    注意:如果多个 advisorgetOrder() 返回的值相同,则不能保证执行顺序。

  • CallAdvisorStreamAdvisor 子接口

    CallAdvisor 接口继承于 Advisor 接口,提供了一个环绕通知 adviseCall() 方法,可以在目标方法执行之前执行之后都会执行逻辑,完全控制目标方法的执行流程。

    public interface CallAdvisor extends Advisor {
      ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, 
                                    CallAdvisorChain callAdvisorChain);
    }

    StreamAdvisor 接口同样继承于 Advisor 接口,同样提供了一个环绕通知 adviseStream() 方法,同样在目标方法执行前后执行逻辑,控制执行流程。

    public interface StreamAdvisor extends Advisor {
      Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, 
                                            StreamAdvisorChain streamAdvisorChain);
    }

    CallAdvisor 接口与 StreamAdvisor 接口不同的是:CallAdvisor 接口是同步的请求和响应,而 StreamAdvisor 接口是流式请求和流失响应,通过返回 Flux<> 来增强流中的数据操作。

    • ChatClientRequest 提供了一种在请求发送之前修改它的方式,Advisor 可以拦截 ChatClientRequest 来对请求提示语进行增强,如添加上下文、重写查询、更改参数、丰富元数据等操作。
    • ChatClientResponse 表示 AI 客户端响应的数据。在 AI 服务返回结果之后,ChatClientResponse 会封装这个结果,并提供了一系列增强方式,可以在返回给应用程序之前进行如过滤、转换以及附加信息等。
    • advisorContext 实际上是个 MapChatClientRequestChatClientResponse 都携带了 advisorContext,也就是通知上下文。来允许 Advisor 访问某个阶段建立的共享状态。

    同步响应和流式响应流程:

    Advisors Streaming vs Non-Streaming Flow
  • CallAdvisorChain 接口和 StreamAdvisorChain 接口

    CallAdvisorChainSpringAI 框架中用于链式调用多个 Advisor 的核心组件,实现请求的拦截与增强功能。

    public interface CallAdvisorChain extends AdvisorChain {
      ChatClientResponse nextCall(ChatClientRequest chatClientRequest);
    
      List<CallAdvisor> getCallAdvisors();
    }

    StreamAdvisorChainSpringAI 框架中用于处理流式请求的链式拦截组件,其核心功能是通过链式调用多个 StreamAdvisor 处理流式交互。

    public interface StreamAdvisorChain extends AdvisorChain {
      Flux<ChatClientResponse> nextStream(ChatClientRequest chatClientRequest);
    
      List<StreamAdvisor> getStreamAdvisors();
    }
  • BaseAdvisor 接口

    下来我们来聊聊 BaseAdvisor 接口,这个接口同时继承了 CallAdvisorStreamAdvisor 接口,同时提供了同步和流式的链式默认实现。在默认实现的环绕通知方法中,前置和后置方法调用 beforeafter 方法用于使用者自定义。因此我们在需要时仅仅定义我们的 beforeafter 即可。

    public interface BaseAdvisor extends CallAdvisor, StreamAdvisor {
    
      Scheduler DEFAULT_SCHEDULER = Schedulers.boundedElastic();
    
      /**
       * 非流式场景处理的默认实现
       */
      @Override
      default ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, 
                                            CallAdvisorChain callAdvisorChain) {
         Assert.notNull(chatClientRequest, "chatClientRequest cannot be null");
         Assert.notNull(callAdvisorChain, "callAdvisorChain cannot be null");
    
         // 1. 通过before方法,对chatClientRequest进行处理,并返回处理后的ChatClientRequest
         ChatClientRequest processedChatClientRequest = this.before(chatClientRequest, callAdvisorChain);
    
         // 2. 调用chain的nextCall方法,传入处理后的chatClientRequest,并返回处理后的ChatClientResponse
         ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(processedChatClientRequest);
    
         // 3. 通过after方法,对chatClientResponse进行处理,并返回处理后的ChatClientResponse
         return this.after(chatClientResponse, callAdvisorChain);
      }
    
      /**
       * 流式场景处理的默认实现
       */
      @Override
        default Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, 
                                                    StreamAdvisorChain streamAdvisorChain) {
          Mono var10000 = Mono.just(chatClientRequest).publishOn(this.getScheduler())
                                        .map((request) -> this.before(request, streamAdvisorChain));
          Objects.requireNonNull(streamAdvisorChain);
          Flux<ChatClientResponse> chatClientResponseFlux 
                = var10000.flatMapMany(streamAdvisorChain::nextStream);
          return chatClientResponseFlux.map((response) -> {
              if (AdvisorUtils.onFinishReason().test(response)) {
                  response = this.after(response, streamAdvisorChain);
              }
              return response;
          }).onErrorResume((error) -> Flux.error(new IllegalStateException("Stream processing failed", error)));
      }
    
      default String getName() {
          return this.getClass().getSimpleName();
      }
    
      ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain);
    
      ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain);
    
      default Scheduler getScheduler() {
          return DEFAULT_SCHEDULER;
      }
    }

Advisor 的运行流程

下来,我们看看官方提供的流程运行图:

Advisors API Flow
  1. The Spring AI framework creates an ChatClientRequest from user’s Prompt along with an empty advisor context object.
  2. Each advisor in the chain processes the request, potentially modifying it. Alternatively, it can choose to block the request by not making the call to invoke the next entity. In the latter case, the advisor is responsible for filling out the response.
  3. The final advisor, provided by the framework, sends the request to the Chat Model.
  4. The Chat Model’s response is then passed back through the advisor chain and converted into ChatClientResponse. Later includes the shared advisor context instance.
  5. Each advisor can process or modify the response.
  6. The final ChatClientResponse is returned to the client by extracting the ChatCompletion.

翻译过来就是:

  1. SpringAI 框架根据用户的 Prompt 创建一个 ChatClientRequest 对象,同时生成一个空的 AdvisorContext 对象。

  2. 链中的每个 Advisor 都会对该请求进行处理,并可能对其进行修改。或者,Advisor 也可以通过不调用下一个实体来阻止该请求。

    后一种情况下,Advisor 需要负责填充响应内容。

  3. 框架提供的最终的 advisor 会将请求发送至 Chat Model

  4. Chat Model 的响应随后会通过 advisor chain 返回并被转换为 ChatClientResponse。该响应包含共享的 AdvisorContext 实例。

  5. 每个 Advisor 都可以对响应进行处理或修改。

  6. 通过提取 ChatCompletion,最终的 ChatClientResponse 会被返回给客户端。

内置的 Advisor 类型

SpringAI 提供了多种开箱即用的 Advisor ,应对一些常见的场景。

image-20251107173613284
  1. MessageChatMemoryAdvisor

    管理多轮会话上下文,使用 MessageChatMemoryAdvisor ,我们可以通过 messages 属性提供聊天客户端调用的聊天历史记录。我们可以将所有消息保存在 ChatMemory 实现中,并控制历史记录的大小。

    我们多轮会话中不管是内存方式还是 JDBC 方式使用的就是这种效果:

    image-20251107173845002

    注意:并不是所有大模型都支持 MessageChatMemoryAdvisor 方式。

  2. PromptChatMemoryAdvisor

    PromptChatMemoryAdvisorMessageChatMemoryAdvisor 都能实现类似效果。不同的是 PromptChatMemoryAdvisor 会将对话历史封装到系统提示词(System Prompt)中,兼容不支持多轮上下文的大模型。

    这里我们直接把 MessageChatMemoryAdvisor 替换成 PromptChatMemoryAdvisor 效果也是一样的。

    @Bean("ollamaChatClient")
    public ChatClient chatClient() {
       return ChatClient
               .builder(ollamaChatModel)
               //.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))  // 内存方式
               //.defaultAdvisors(new MessageChatMemoryAdvisor(jdbcChatMemory)) // JDBC方式
               .defaultAdvisors(new PromptChatMemoryAdvisor(jdbcChatMemory)) // 替换成PromptChatMemoryAdvisor
               .build();
    }

    根据官方的解释,PromptChatMemoryAdvisor 在响应速度上会更快,不过有时候出现顺序凌乱的可能性也更高些。

  3. SimpleLoggerAdvisor

    SimpleLoggerAdvisorSpringAI 框架中用于日志记录的内置组件,主要用于拦截并记录聊天请求与响应的详细信息,支持调试和监控应用程序运行状态。 ‌

    核心功能

    • 日志记录‌:默认记录 Debug 级别的日志,包括用户输入文本、模型响应等内容,但默认不显示。 ‌
    • 可配置性‌:支持通过配置文件调整日志级别(如改为 Info 级别),或下载源码自定义逻辑。

    项目搭配

    • 添加 SimpleLoggerAdvisor

      @GetMapping(value = "/logger", produces = "text/html;charset=utf-8")
      public Flux<String> logger(@RequestParam("question") String question) {
       // 1. 获取client对象,这里为了方便期间并没有获取我们在config中得到的client对象
       ChatClient client = ChatClient
          .builder(ollamaChatModel)
          .defaultAdvisors(new SimpleLoggerAdvisor())
          .build();
       // 2. 返回数据,没有引入历史会话内容
       return client.prompt().user(question).stream().content();
      }
    • 修改配置文件

      logging:
      level:
       org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor: DEBUG
    • 运行测试

      image-20250929152819546
  4. SafeGuardAdvisor

    Advisor 基于关键词或正则表达式过滤敏感内容,拦截非法请求。

    // SafeGuardAdvisor
    @GetMapping(value = "/safe", produces = "text/html;charset=utf-8")
    public Flux<String> safe(@RequestParam("question") String question) {
       // 1. 定义advisor,设置"张三", "中国", "陕西"为敏感内容
       SafeGuardAdvisor safeGuardAdvisor = new SafeGuardAdvisor(List.of("张三", "中国", "陕西"));
       // 2. 获取client对象,这里为了方便期间并没有获取我们在config中得到的client对象
       ChatClient client = ChatClient.builder(ollamaChatModel).defaultAdvisors(safeGuardAdvisor).build();
       // 3. 返回数据,没有引入历史会话内容
       return client.prompt().user(question).stream().content();
    }

    首先输入一个带有敏感内容的问题:

image-20250929150348837

根据显示结果,问题被屏蔽了。这时候,我们在输入一个正常的提问:

image-20250929150458160

这时候我们可以看到第二次提问就会正常显示了。

总的来说 SpringAI Advisor 提供了一种强大而优雅的方式来扩展和定义与 AI 大模型之间的交互行为,使得开发者能够以声明式和非侵入式的方式添加各种增强功能,从而构建更健壮、灵活和可维护的 AI 应用。

自定义 Advisor 类型

刚才我们介绍了什么是 Advisor 、运行流程以及内置的一些 Advisor 类型。下来,我们来尝试着开发我们自定义的 Advisor 类型。

要自定义一个 Advisor,其实很简单。只需遵循以下步骤:

  1. 创建一个类,实现 CallAroundAdvisorStreamAroundAdvisor 接口。
  2. 实现接口的 aroundCall()|aroundStream()getName() 以及 getOrder() 方法。
  3. 调用大模型时,将自定义的 advisor 类添加进去即可。

案例—-计算大模型调用耗时

这里我们实现一个在大模型响应前后,来计算它的耗时时间。在前置增强中统计我们的开始时间,在后置方法中计算耗时。

  1. 定义一个类,实现 StreamAroundAdvisor 接口并实现方法

    @Slf4j
    public class CalculateTimeStreamAdvisor implements StreamAdvisor {
       @Override
       public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) {
           // 1. 前置增强
           log.info("进入CalculateTimeStreamAdvisor拦截器的前置增强,开始计算耗时时间");
           chatClientRequest.context().put("START_TIME", System.currentTimeMillis());
           // 2. 调用后续的advisor链
           Flux<ChatClientResponse> chatClientResponseFlux 
                        = streamAdvisorChain.nextStream(chatClientRequest);
           // 3. 后置增强
           long endTime = System.currentTimeMillis();
           long startTime = Long.parseLong(chatClientRequest.context().get("START_TIME").toString());
           log.info("总共耗时:{}毫秒", endTime - startTime);
           return chatClientResponseFlux;
       }
    
       // 指定当前Advisor的名称
       public String getName() {
           return "CalculateTimeAdvisor";
       }
    
       // 设置优先级 数字越大优先级越低
       public int getOrder() {
           return 100000;
       }
    }

    建议实现 CallAroundAdvisor,同步方法效果比较明显。

  2. 注册自定义的 Advisor

    我们可以通过全局方式或者局部方式注册 advisor 类。

    1. 全局方式

      @Bean
      public ChatClient chatClient() {
        return ChatClient.builder(ollamaChatModel)
                .defaultAdvisors(new CalculateTimeStreamAdvisor())
                .build();
      }

      全局方式注册后,所有使用这个 Chatclient 的请求都自动触发 advisor 类。

    2. 局部方式

      @GetMapping(value = "/time", produces = "text/html;charset=utf-8")
      public Flux<String> time(@RequestParam("question") String question) {
        return client.prompt()
          .user(question)
          .advisors(new CalculateTimeAdvisor())
          .stream().content();
      }

      仅针对于当前请求,其他请求不会触发。

  3. 查看演示效果

    image-20250929162949443

    这里使用的是 stream 方式,触发速度较快。如果使用同步方式,耗时会比较明显。

一些主要的参数

// 获取到UserMessage
UserMessage userMessage = chatClientRequest.prompt().getUserMessage();
// 获取到用户的问题(question)
String question = userMessage.getText();
// 获取到上下文,param参数也可以通过这种方式获取
Map&lt;String, Object&gt; context = chatClientRequest.context();

全局方式传递参数

调用时要传递参数可以通过如下方式:

public String call(@RequestParam("question") String question) {
 return ollamaChatClient.prompt()
         .user(question)
         .advisors(e -> e.param("key", "value"))
         .call().content();
}

注意:仅支持全局方式注册。

案例—-基于 redisAdvisor 实现多轮会话

在前边的案例中,我们讲解了多轮会话,分别基于内存和 JDBC 方式,而官方的 ChatMemory 并没有提供基于 Redis 的实现方式。 下来我们使用自定义的 advisor 来实现自己的多轮会话。

  1. 引入依赖

    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
       <groupId>com.alibaba.fastjson2</groupId>
       <artifactId>fastjson2</artifactId>
       <version>2.0.59</version>
    </dependency>

    注意:fastjson 必须引入高版本,否则转换时会出现符号错误。

  2. 自定义可序列化的 Message 包装类

    因为 Message 接口下的常用实现类均没有序列化操作,我们需要自定义一个类去进行序列化和反序列化操作。

    @NoArgsConstructor
    @Data
    public class SerializableMessage implements Serializable {
       private static final long serialVersionUID = 1L;
       private MessageType messageType; // 消息类型
       private String content; // 消息内容
       private Map<String, Object> properties; // 消息元数据
    
       public SerializableMessage(Message message) {
           this.messageType = message.getMessageType();
           this.content = message.getText();
           this.properties = message.getMetadata();
       }
    
       public Message getMessage() {
           if (this.messageType.equals(MessageType.USER)) {
               return new UserMessage(this.content);
           } else if (this.messageType.equals(MessageType.ASSISTANT)) {
               return new AssistantMessage(this.content, properties);
           }
           return null;
       }
    }

    MessageType 是用于区分不同类型消息的枚举类,主要包含以下四种类型:

    1. 用户消息(UserMessage

      用于用户输入的文本或语音消息,可携带媒体内容(如图片、视频)。 ‌

    2. 系统消息(SystemMessage

      由系统生成的消息,通常用于流程控制或状态反馈。 ‌

    3. 助手消息(AssistantMessage

      模型生成的回复内容,属于中间处理结果。 ‌

    4. 工具消息(ToolMessage

      特定工具或模块产生的内部消息,如语音识别结果或图像处理反馈。

    注意:这里我们仅使用了用户消息和助手消息。

  3. 自定义 Memory

    定义一个类 RedisMessageWindowChatMemory,实现 ChatMemory 接口,重点书写添加和获取的方法。

    @Component("redisChatMemory")
    public class RedisMessageWindowChatMemory implements ChatMemory {
       private static final int DEFAULT_MAX_MESSAGES = 20;
       private static final String prefix_key = "spring_ai_redis_chat_memory:";
       @Resource
       private StringRedisTemplate stringRedisTemplate;
    
       @Override
       public void add(String conversationId, List<Message> messages) {
           List<SerializableMessage> serializableMessages = new ArrayList<>();
           for (Message message : messages) {
               SerializableMessage serializableMessage = new SerializableMessage(message);
               serializableMessages.add(serializableMessage);
           }
           String jsonString = JSONArray.toJSONString(serializableMessages);
           stringRedisTemplate.opsForValue().set(prefix_key + conversationId, jsonString);
       }
    
       @Override
       public List<Message> get(String conversationId) {
           ValueOperations<String, String> forValue = stringRedisTemplate.opsForValue();
           String s = forValue.get(prefix_key + conversationId);
           List<SerializableMessage> serializableMessages 
             = JSONArray.parseArray(s, SerializableMessage.class);
           if (serializableMessages != null) {
               List<Message> messages = new ArrayList<>();
               for (SerializableMessage serializableMessage : serializableMessages) {
                   Message message = serializableMessage.getMessage();
                   messages.add(message);
               }
               return messages;
           }
           return List.of();
       }
    
       @Override
       public void clear(String conversationId) {
       }
    }
  4. 自定义 advisor

    通过自定义的 advisor 类实现在消息发送之前和响应之后对消息进行拦截操作。

    public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, 
                                                StreamAdvisorChain streamAdvisorChain) {
       Mono var10000 = 
         Mono.just(chatClientRequest).publishOn(BaseAdvisor.DEFAULT_SCHEDULER)
         .map((request) -> this.before(request));
       return var10000.flatMapMany((Function<ChatClientRequest, Publisher<? extends ChatClientResponse>>) streamAdvisorChain::nextStream)
         .transform((Function<Flux<ChatClientResponse>, Flux<ChatClientResponse>>) flux -> 
                    (new ChatClientMessageAggregator())
                    .aggregateChatClientResponse(flux, response -> observeAfter(response)));
    }

    这个方法就是改写了 MessageChatMemoryAdvisoradviseStream 方法内容,重点看的是 observeAfterbefore 方法。

    消息发送之前,我们需要将历史记录以及新消息合并到一起产生新请求并发送到大模型中,具体如下:

    private ChatClientRequest before(ChatClientRequest request) {
       // 1. 获取conversationId
       id = request.context().get("id").toString();
       // 2. 获取当前conversationId的会话记忆
       List<Message> messageList = this.redisChatMemory.get(id);
       // 3. 合并消息列表
       String question = request.prompt().getUserMessage().getText();
       UserMessage message = new UserMessage(question);
       if (messageList.isEmpty()) {
           messageList = List.of(message);
       } else {
           messageList.add(message);
       }
       // 4. 创建一个新的请求
       ChatClientRequest processedChatClientRequest 
         = request.mutate().prompt(request.prompt().mutate().messages(messageList).build()).build();
       // 5. 将新的消息添加到对话记忆中
       this.redisChatMemory.add(id, messageList);
       // 6. 提交本次请求
       return processedChatClientRequest;
    }

    大模型进行响应后,我们需要将返回的消息同步添加到 redis 中,具体如下:

    private ChatClientResponse observeAfter(ChatClientResponse chatClientResponse) {
       List<AssistantMessage> assistantMessages = new ArrayList();
       // 获取到刚才的助手消息
       if (chatClientResponse.chatResponse() != null) {
           assistantMessages = chatClientResponse.chatResponse().getResults().stream().map((g) -> g.getOutput()).toList();
       }
       List<Message> messages = this.redisChatMemory.get(id);
       messages.addAll(assistantMessages);
       this.redisChatMemory.add(id, messages);
       return chatClientResponse;
    }

    注意:不管是从 redis 中获取消息,还是存放到 redis 中,都需要转换成我们自己的可序列化类来完成。

  5. 配置类

    在配置类中引入自定义的 advisor 类。

    @Bean("redisChatClient")
    public ChatClient redisChatClient() {
       return ChatClient.builder(ollamaChatModel)
               .defaultAdvisors(new RedisStreamAdvisor(redisChatMemory))
               .build();
    }
  6. 配置文件

    这里就书写 redis 的配置信息即可。

  7. 查看效果

    1. 第一次问以及大模型的回答

      image-20251010173156074
    2. 第二次问和大模型的回答

      image-20251010173511732
    3. 我们来看看 redis 中存放的数据

      [
      {
        "content": "7加上5等于多少?",
        "message": {
          "media": [],
          "messageType": "USER",
          "metadata": {
            "messageType": "USER"
          },
          "text": "7加上5等于多少?"
        },
        "messageType": "USER",
        "properties": {
          "messageType": "USER"
        }
      },
      {
        "content": "<think>\n首先,我需要计算数字7和5的总和。\n\n为了做到这一点,我可以使用加法的基本原理。\n\n将这两个数相加,得到结果为12。\n</think>\n\n**解答:**\n\n我们需要计算 \\(7 + 5\\) 的值。\n\n步骤如下:\n\n\\[\n7 + 5 = 12\n\\]\n\n所以,答案是 \\(\\boxed{12}\\)。",
        "message": {
          "media": [],
          "messageType": "ASSISTANT",
          "metadata": {
            "messageType": "ASSISTANT"
          },
          "text": "<think>\n首先,我需要计算数字7和5的总和。\n\n为了做到这一点,我可以使用加法的基本原理。\n\n将这两个数相加,得到结果为12。\n</think>\n\n**解答:**\n\n我们需要计算 \\(7 + 5\\) 的值。\n\n步骤如下:\n\n\\[\n7 + 5 = 12\n\\]\n\n所以,答案是 \\(\\boxed{12}\\)。",
          "toolCalls": []
        },
        "messageType": "ASSISTANT",
        "properties": {
          "messageType": "ASSISTANT"
        }
      },
      {
        "content": "再乘以6等于多少?",
        "message": {
          "media": [],
          "messageType": "USER",
          "metadata": {
            "messageType": "USER"
          },
          "text": "再乘以6等于多少?"
        },
        "messageType": "USER",
        "properties": {
          "messageType": "USER"
        }
      },
      {
        "content": "<think>\n好的,我现在要解决的问题是在之前的计算基础上再次乘以6是多少。让我先回顾一下之前的步骤。\n\n上一次,我计算了7加上5的结果,得到了12。现在,问题变成了“在得到的12的基础上,再乘以6,结果是多少?”听起来这是一个相对简单的问题,但我还是得仔细思考一下,确保自己理解正确,并且能够准确地进行计算。\n\n首先,我要明确这个问题的具体要求:我已经有了一个数,也就是12,我需要将它乘以6。这意味着我要把12看作一个整体,然后把它分成6个相等的部分,或者说是把12加上自己5次(因为6=1+5)。这样可以确保我的计算是正确的。\n\n接下来,我需要确认“再乘以6”这个表述是否正确理解了题目的意思。也就是说,是不是要将原来的7加上5得到的12,然后再乘以6?或者是否有其他的意思?\n\n假设题目就是让我先计算7加5等于12,然后把这12乘以6,那么我的任务就是准确地进行这一运算。\n\n好的,现在我来具体计算一下:12乘以6是多少。我可以使用多种方法来验证这个结果是否正确:\n\n1. **直接计算法**:\n   我可以直接将12和6相乘,得到的结果就是72。这是因为10乘以6等于60,再加上2乘以6等于12,总共是72。\n\n2. **分解计算法**:\n   我可以将12拆分成10和2,分别与6相乘,然后将结果相加。\n   \n   - 10乘以6等于60\n   - 2乘以6等于12\n   - 将这两个结果相加:60 + 12 = 72\n\n3. **重复相加法**:\n   我可以将12加上自己5次,这样总共加了6次(包括原来的数)。\n   \n   - 12 + 12 = 24 (两次)\n   - 24 + 12 = 36 (三次)\n   - 36 + 12 = 48 (四次)\n   - 48 + 12 = 60 (五次)\n   - 60 + 12 = 72 (六次)\n\n无论是使用哪种方法,结果都是72。这让我确信我的计算是正确的。\n\n不过,为了确保万无一失,我可以再换一种方法来验证,比如使用乘法表或者计算器进行交叉检查。虽然在这个问题中可能没有必要如此复杂,但我还是希望通过多种方法的验证,确保自己的答案是准确无误的。\n\n另外,我也要考虑到是否有其他可能的解释方式。例如,“再乘以6”是否指的是将7和5分别乘以6?如果是这样的话,那么计算过程会有所不同:\n\n- 7乘以6等于42\n- 5乘以6等于30\n\n然后把这两个结果相加:42 + 30 = 72\n\n这与之前直接将12乘以6得到的结果是一致的。因此,无论我采用哪种理解方式,最终的结果都是72。\n\n综上所述,我认为这个问题的答案应该是72。\n</think>\n\n再乘以6等于\\(\\boxed{72}\\)",
        "message": {
          "media": [],
          "messageType": "ASSISTANT",
          "metadata": {
            "messageType": "ASSISTANT"
          },
          "text": "<think>\n好的,我现在要解决的问题是在之前的计算基础上再次乘以6是多少。让我先回顾一下之前的步骤。\n\n上一次,我计算了7加上5的结果,得到了12。现在,问题变成了“在得到的12的基础上,再乘以6,结果是多少?”听起来这是一个相对简单的问题,但我还是得仔细思考一下,确保自己理解正确,并且能够准确地进行计算。\n\n首先,我要明确这个问题的具体要求:我已经有了一个数,也就是12,我需要将它乘以6。这意味着我要把12看作一个整体,然后把它分成6个相等的部分,或者说是把12加上自己5次(因为6=1+5)。这样可以确保我的计算是正确的。\n\n接下来,我需要确认“再乘以6”这个表述是否正确理解了题目的意思。也就是说,是不是要将原来的7加上5得到的12,然后再乘以6?或者是否有其他的意思?\n\n假设题目就是让我先计算7加5等于12,然后把这12乘以6,那么我的任务就是准确地进行这一运算。\n\n好的,现在我来具体计算一下:12乘以6是多少。我可以使用多种方法来验证这个结果是否正确:\n\n1. **直接计算法**:\n   我可以直接将12和6相乘,得到的结果就是72。这是因为10乘以6等于60,再加上2乘以6等于12,总共是72。\n\n2. **分解计算法**:\n   我可以将12拆分成10和2,分别与6相乘,然后将结果相加。\n   \n   - 10乘以6等于60\n   - 2乘以6等于12\n   - 将这两个结果相加:60 + 12 = 72\n\n3. **重复相加法**:\n   我可以将12加上自己5次,这样总共加了6次(包括原来的数)。\n   \n   - 12 + 12 = 24 (两次)\n   - 24 + 12 = 36 (三次)\n   - 36 + 12 = 48 (四次)\n   - 48 + 12 = 60 (五次)\n   - 60 + 12 = 72 (六次)\n\n无论是使用哪种方法,结果都是72。这让我确信我的计算是正确的。\n\n不过,为了确保万无一失,我可以再换一种方法来验证,比如使用乘法表或者计算器进行交叉检查。虽然在这个问题中可能没有必要如此复杂,但我还是希望通过多种方法的验证,确保自己的答案是准确无误的。\n\n另外,我也要考虑到是否有其他可能的解释方式。例如,“再乘以6”是否指的是将7和5分别乘以6?如果是这样的话,那么计算过程会有所不同:\n\n- 7乘以6等于42\n- 5乘以6等于30\n\n然后把这两个结果相加:42 + 30 = 72\n\n这与之前直接将12乘以6得到的结果是一致的。因此,无论我采用哪种理解方式,最终的结果都是72。\n\n综上所述,我认为这个问题的答案应该是72。\n</think>\n\n再乘以6等于\\(\\boxed{72}\\)",
          "toolCalls": []
        },
        "messageType": "ASSISTANT",
        "properties": {
          "messageType": "ASSISTANT"
        }
      }
      ]

我们可以看到通过 redis + advisor 我们实现了多轮会话的记忆功能。

Structured Output

AI 大模型的输出传统上以 java.lang.String 的形式到达,即使您要求以 JSON 格式回复也是如此。它可能是正确的 JSON,但不是 JSON 数据结构。它只是一个字符串。此外,在提示中请求 for JSON 并不是 100% 准确的。

这种复杂性导致了一个专业领域的出现,该领域涉及创建提示以产生预期的输出,然后将生成的简单字符串转换为可用于应用程序集成的数据结构。

img

结构化输出将模型的自由文本响应转换为特定数据结构(如 JSON),它允许开发者将 AI 大模型生成的自由文本响应自动转换为预定义的 Java 对象结构。这种能力极大地简化了 AI 集成工作,特别是在需要处理复杂、结构化数据的场景中。

结构化输出会尽力将模型输出转换为结构化格式,但无法保证 LLM 必然返回请求的结构。模型可能无法理解提示词或无法按要求生成结构化输出,因此建议实现验证机制以确保输出符合预期。

此外,结构化输出不用于 LLM 工具调用(Tool Calling),因为该功能默认已提供结构化输出。

我们知道在我们与 AI 对话过程中,大模型基本上是以文本字符串的形式响应,但是在我们实际开发过程中,使用的都是具有特定数据类型的实体类,如果 AI 模型的结果是字符串,我们还要想办法进行数据转换。

比如我有一个查询数据库某表数据列表的操作,并且把这个接口注册成 ChatClient 的工具函数,一般情况下,AI 去调用这个接口的时候会将函数返回结果作为回答的一部分进行重排响应给用户,但是这个结果是没有任何结构的字符串,我前端拿到这个字符串后并不能把它变成表格,那我就想能不能让 AI 就响应给我具有一定格式的数据类型呢,比如我我给 AI 一个实体类,让 AI 根据该实体类的格式进行响应。

答案是当然可以,SpringAI 提供了一个结构化输出转换器,帮助 LLM 的输出转换为结构化格式,开发人员可以快速将 AI 模型的结果转换为可以传递给其他应用程序函数和方法的数据类型。

结构化输出 API

StructuredOutputConverter 接口允许获取结构化输出,例如将输出映射到 Java 类或从文本型 AI 模型输出中提取值数组。接口定义如下:

public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {
}

该接口继承了 SpringConverter<String, T> 接口和 FormatProvider 接口:

public interface FormatProvider {
    String getFormat();
}

下图展示了使用结构化输出 API 时的数据流:

image-20251014114152469

FormatProvider 为人工智能模型提供特定的格式化指南,使其能够生成文本输出,这些输出可以使用 Converter 转换为指定的目标类型 T。以下是一个此类格式化指令的示例:

您的回复应采用JSON格式。
  JSON的数据结构应与此Java类匹配:java.util.HashMap
  请勿包含任何解释,仅需按照此格式无偏差地提供一个符合RFC8259标准的JSON响应。

格式指令通常通过 PromptTemplate 附加到用户输入末尾,例如:

StructuredOutputConverter outputConverter = ...;
String userInputTemplate = &quot;&quot;&quot;
    ... 用户文本输入 ....
    {format}
    &quot;&quot;&quot;; // 用户输入中包含&quot;format&quot;占位符
Prompt prompt = new Prompt(
   new PromptTemplate(
           this.userInputTemplate,
          Map.of(..., &quot;format&quot;, outputConverter.getFormat()) // 用转换器的格式替换&quot;format&quot;占位符
   ).createMessage());

Converter<String, T> 负责将模型的文本输出转换为指定类型T的实例。

可用转换器

目前,SpringAI 提供了 AbstractConversionServiceOutputConverterAbstractMessageOutputConverterBeanOutputConverterMapOutputConverterListOutputConverter 实现:

image-20251014143703900
  • AbstractConversionServiceOutputConverter

    提供预配置的 GenericConversionService,用于将 LLM 输出转换为所需格式。未提供默认的 FormatProvider 实现。

  • AbstractMessageOutputConverter

    提供预配置的 MessageConverter,用于将 LLM 输出转换为所需格式。未提供默认的 FormatProvider 实现。

  • BeanOutputConverter

    配置了指定 Java 类或 ParameterizedTypeReference,其 FormatProvider 实现会引导 AI 模型生成符合基于该 Java 类推导出的 JSON SchemaJSON 响应。随后使用 ObjectMapperJSON 反序列化为目标 Java 类的实例。

  • MapOutputConverter

    扩展 AbstractMessageOutputConverter,其 FormatProvider 实现引导 AI 模型生成符合 RFC8259 标准的 JSON 响应。内置转换器通过提供的 MessageConverterJSON 负载转换为 java.util.Map<String, Object> 实例。

  • ListOutputConverter

    扩展 AbstractConversionServiceOutputConverter,其 FormatProvider 实现专为逗号分隔的列表输出设计。转换器使用提供的 ConversionService 将模型文本输出转换为 java.util.List

使用转换器

The following sections provide guides how to use the available converters to generate structured outputs。

特别声明Ollama 本地 Deepseek 模型没有通过结构化输出测试。这里我们使用的是 Zhipu 大模型进行测试。

这里要使用 Zhipu 大模型,需要如下几个步骤:

  1. 引入智谱的依赖

    <dependency>
     <groupId>org.springframework.ai</groupId>
     <artifactId>spring-ai-starter-model-zhipuai</artifactId>
    </dependency>
  2. 注册智谱的账号并设置 key

    官方网址:https://open.bigmodel.cn/

    image-20251014175900406
  3. application.yml 中设置 key

    spring:
    ai:
     zhipuai:
       api-key: ${ZHIPU_API_KEY} #你自己的key
       chat:
         options:
           model: glm-4.6
  4. OllamaChatModel 换成 ZhiPuAiChatModel 即可,其他一致。

下来是具体相关转换器的使用。需要注意的是这里不能使用流模式,使用同步文本模式。

  1. Bean 输出转换器

    以下示例显示了如何使用 BeanOutputConverter 为演员生成电影作品集。

    代表演员电影作品的目标记录:

    record ActorsFilms(String actor, List<String> movies) {}

    通过 chatClient 进行调用:

    @Test
    public void obj() {
       String question = "为{actor}生成5部电影的电影目录";
       ActorForms actorForms = zhipuChatClient.prompt()
               .user(t -> t.text(question).param("actor", "李小龙"))
               .call()
               .entity(ActorForms.class);
       log.info("name:{},movies:{}", e.name(), Arrays.toString(e.movies()));
    }

    使用 ParameterizedTypeReference 构造器指定更复杂的类结构。例如,表示多个演员及其影视作品的列表:

    @Test
    public void objs() {
       String question = "为{actor1}和{actor2}各自生成5部电影的电影目录";
       List<ActorForms> actorForms = zhipuChatClient.prompt()
               .user(t -> t.text(question).param("actor1", "李小龙").param("actor2", "刘德华"))
               .call()
               .entity(new ParameterizedTypeReference<List<ActorForms>>() {
               });
       actorForms.forEach(e -> {
           log.info("name:{},movies:{}", e.name(), Arrays.toString(e.movies()));
       });
    }
  2. Map 输出转换器

    以下片段演示如何使用 MapOutputConverter 将模型输出转换为包含数字列表的映射:

    @Test
    public void map() {
       String question = "为{actor}生成5部电影的电影目录";
       Map<String, Object> map = zhipuChatClient.prompt()
               .user(t -> t.text(question).param("actor", "李小龙"))
               .call()
               .entity(new ParameterizedTypeReference<>() {
               });
       log.info("map:{}", map);
    }

    这里我们会发现,其实使用的还是 ParameterizedTypeReference 来构建 map 类型的结果,但是出现的数据属性就交由大模型来决定了。

    image-20251015095526570
  3. List 输出转换器

    以下代码片段显示了如何使用 ListOutputConverter 将模型输出转换为冰淇淋口味列表:

    @Test
    public void list() {
       List<String> flavors = zhipuChatClient.prompt()
               .user(u -> u.text("列出五种 {subject}").param("subject", "冰淇淋口味"))
               .call()
               .entity(new ListOutputConverter());
       log.info("list:{}", flavors);
    }

通过对输出的结果进行格式化能够更有效的统一格式、方便前端或其他调用者进行调用。

图片识别技术

在人工智能技术深度渗透各行业的2025年,图像识别技术已从实验室走向规模化应用,成为智能制造、智慧医疗、自动驾驶等领域的核心基础设施。Spring AI 作为 Spring 框架家族的新成员,凭借其模块化设计、多模型支持及与 Spring 生态的无缝集成能力,正在重塑 Java 开发者在图像识别领域的开发范式。

  1. 大模型选择

    大模型选择是,要选择具备视觉处理的大模型

    image-20260407093346753
  2. 前端部分

    // 表单数据
    let data = new FormData();
    data.append('file', this.file);
    // 定义发送格式
    let type = {
              headers: {
                  'Content-Type': 'multipart/form-data'
              }
    }
    axios.post('siliconflow/image', data, type)
          .then((response) => {
              this.content = response.data
            });
  3. 后端处理

    @PostMapping(value = "/image", produces = "text/html;charset=utf-8")
    public Flux<String> image(@RequestParam("file") MultipartFile file) {
      return openAiChatClient.prompt()
           .user(u -> u.text("请描述这张图片的内容")
                     .media(MimeTypeUtils.IMAGE_JPEG, file.getResource())
           )
           .stream().content();
    }

    最新版本中使用 media 处理图像捕捉技术,请求方式必须是 POST

图片生成技术

Spring AI 中,图片生成(Text to Image)是一个核心的 AI 能力,它允许你通过一段文字描述(Prompt)来让 AI 生成对应的图片。

  1. 配置图像生成大模型

    不同的大模型能做的事情是不一样的,有些能够对话,有的能够生图,有的能够生成视频合成语音,我们要使用图片生成大模型来实现效果:

    spring:
    ai:
      openai:
        base-url: https://api.siliconflow.cn
        api-key: ${siliconflow_key} # 你要写你的key
        image:
          options:
            model: "Qwen/Qwen-Image" # 处理图⽚的⼤模型
            height: 1024 # 图⽚⾼度
            width: 1024 # 图⽚宽度
            quality: hd # 图⽚质量 部分模型不适⽤
            n: 1 # 图⽚⽣成数量
  2. 核心接口

    Spring AI 为图像生成提供了一个统一的抽象接口 ImageModel,这让你可以无缝切换不同的底层模型,而无需修改业务代码。

  3. 工作流程

    @GetMapping("/image_display")
    public ResponseEntity<ByteArrayResource> displayImage(@RequestParam String question) {
     // 构建图⽚选项
     var options = ImageOptionsBuilder.builder().height(800).width(600).build();
     // 调⽤⼤模型⽣成图像
     ImageResponse response = zhiPuAiImageModel.call(new ImagePrompt(prompt, options));
    String imageUrl = response.getResult().getOutput().getUrl();
     // 下载刚⽣成的图像
     URL url = URI.create(imageUrl).toURL();
    byte[] imageData = url.openStream().readAllBytes();
    ByteArrayResource resource = new ByteArrayResource(imageData);
     // 将这个图像返回给前端并显示
     // 设置响应头,让浏览器直接渲染图⽚
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.IMAGE_PNG);
    return ResponseEntity.ok().headers(headers).body(resource);
    }
  4. 关键参数配置

    你可以通过 ImageOptions 精细控制生成效果:

    参数 说明 常见值
    model 指定使用的模型 wanx2.1-t2i-turbo, dall-e-3
    size 图片分辨率 1024x1024, 512x512
    quality 生成质量 standard, hd
    style 画风 写实, 卡通, 水墨, 3d
    n 生成数量 1, 2, 4

By admin

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注