RAG

什么是RAG

简单说,RAG(检索增强生成)是让 AI “先查资料再作答” 的技术,核心是解决大模型 “记不住新信息、容易说胡话” 的问题。

核心逻辑

  1. 先检索:AI 接到问题后,不直接凭 “记忆” 回答,而是先去指定的知识库(比如公司文档、最新新闻、专业资料)里找相关信息。
  2. 再生成:把找到的精准资料和自身知识结合,整理成自然语言回答,既保证准确性,又不脱离模型本身的语言能力。
    Alt
    Alt
    关键价值
  • 解决 “知识过期”:大模型的训练数据有截止时间,RAG 能实时调取新信息(比如 2025 年的行业数据、刚发布的政策)。
  • 降低 “幻觉率”:基于真实资料作答,减少 AI 编造不存在的事实、数据或逻辑。
  • 支持 “专属知识”:可以接入企业内部文档、个人笔记等私域数据,让 AI 只围绕指定内容回答(比如公司产品手册、行业专属规范)。
    Alt
    举个实际例子

你问 AI “2025 年某行业的最新政策要求”,但 AI 的训练数据只到 2023 年:

  • 没有 RAG:AI 可能会说 “没有相关信息”,或编造过时的政策。
  • 有 RAG:AI 会先去检索 2025 年该行业的官方政策文件、权威解读,再基于这些真实资料,整理出清晰的政策要点和合规建议。
    Alt

应用场景

Alt
以下是 RAG(检索增强生成)的 8 个典型应用场景,覆盖企业、生活、学习等核心领域:

  1. 企业内部知识库问答
  • 核心用法:接入公司内部文档(员工手册、产品手册、流程规范、历史项目资料),员工提问时,AI 实时检索相关文档给出精准答案。
  • 例子:新员工问 “报销流程和限额”,AI 直接调取最新报销规范,分步骤说明材料要求、审批节点;销售问 “某产品的技术参数”,快速检索产品手册给出对应信息。
  1. 智能客服(ToB/ToC)
  • 核心用法:关联产品 FAQ、售后手册、用户反馈记录,客户咨询时,AI 检索匹配问题的解决方案,避免重复回复或答非所问。
  • 例子:用户问 “家电保修范围”,AI 检索对应产品的保修政策,明确质保期限、免责条款;企业客户问 “API 接口调用限制”,调取技术文档给出具体参数和解决办法。
  1. 行业动态与政策解读
  • 核心用法:接入行业权威网站、政府政策平台、最新研究报告,实时检索最新信息,帮助用户快速掌握动态。
  • 例子:创业者问 “2025 年小微企业税收优惠政策”,AI 检索税务总局最新文件,整理优惠条件、申报流程;从业者问 “AI 行业最新监管要求”,汇总近期政策要点和合规建议。
  1. 学术科研与论文辅助
  • 核心用法:对接学术数据库(知网、万方、SCI 论文库)、行业研究成果,科研人员提问时,检索相关文献、数据和研究结论。
  • 例子:研究生问 “某算法的最新改进方向”,AI 检索近 3 年相关论文,总结主流改进思路和实验效果;医生问 “某疾病的最新治疗方案”,调取权威医学期刊的研究成果和临床指南。
  1. 个人私域知识管理
  • 核心用法:接入个人笔记(Notion、备忘录)、阅读过的文章、收藏的资料,打造专属 “私人知识库”,快速检索记忆模糊的信息。
  • 例子:你问 “之前收藏的 Excel 数据透视表教程”,AI 检索个人收藏文档,提取关键操作步骤;想回忆 “某本书的核心观点”,调取读书笔记给出提炼总结。
  1. 金融 /法律等专业领域咨询
  • 核心用法:接入行业法规、案例库、市场数据,为专业咨询提供精准依据,避免主观判断。
  • 例子:律师问 “某类合同纠纷的胜诉案例”,AI 检索相似司法案例,整理判决要点和法律依据;投资者问 “某股票的最新财务数据和行业对比”,调取财经平台数据给出客观分析。
  1. 产品说明书与使用指导
  • 核心用法:关联产品电子版说明书、常见故障排查手册,用户遇到使用问题时,实时检索解决方案。
  • 例子:用户问 “智能音箱怎么连接 WiFi”,AI 检索对应型号说明书,分步骤给出操作指引;程序员问 “某软件的函数用法”,调取开发文档给出语法示例和注意事项。
  1. 新闻资讯与热点汇总
  • 核心用法:接入主流新闻平台、权威媒体账号,实时检索特定主题的最新资讯,自动汇总关键信息。
  • 例子:你问 “近期某赛事的赛况和结果”,AI 检索最新报道,整理赛程、比分、核心亮点;关注 “某地区的天气预警”,调取气象部门实时信息,给出预警等级和应对建议。

步骤解析

Alt

  1. 文件上传

这是 RAG 文件处理的起始步骤,核心是接收、校验用户上传的文件,同时完成基础的预处理

  • 文件接收:基于 Spring Boot 的MultipartFile组件实现文件上传接口,支持的文件类型一般包括 TXT、PDF、DOCX、MD 等常见的文本类文档,也可以扩展支持 PPTX、XLSX(需要提取其中的文本内容)
  • 文件校验:
    • 校验文件大小,避免过大文件占用资源
    • 校验文件格式,拒绝非允许的文件类型
    • 校验文件的完整性,避免损坏的文件
  • 预处理:将文件的元信息(文件名、文件大小、上传时间、文件唯一标识)存储到关系型数据库(比如 MySQL)中,方便后续和向量数据做关联
  1. 文档分割
    Alt
    这一步是为了解决大文本无法直接进行向量化的问题(大模型的上下文窗口有限),同时提升后续检索的精准度
  • 核心逻辑:将完整的文档,按照一定的规则切割为多个小的文本片段(Chunk)
  • 常用分割策略:
    • 按固定长度分割:比如每 500 个字符为一个 Chunk,同时设置一定的重叠长度(比如 50 个字符),避免切割到完整的语义单元
    • 按语义分割:借助 Ollama 的本地大模型,或者 Spring AI 的语义分割工具,按照句子、段落的语义完成分割,这种方式可以避免切断完整的语义
  • 处理细节:为每个分割后的 Chunk 生成唯一 ID,同时记录这个 Chunk 所属的源文件 ID、Chunk 在源文件中的位置信息,方便后续溯源
  1. 向量化

将分割后的文本片段,转换为计算机可以理解的向量数据
Alt

  • 工具选择:可以选择 Ollama 部署的本地嵌入模型(比如nomic-embed-text),或者 Spring AI 集成的嵌入模型
  • 处理流程:
    1. 读取分割后的文本 Chunk
    2. 将文本传入嵌入模型,模型会将文本转换为固定维度的向量(比如 768 维、1536 维)
    3. 对生成的向量做标准化处理,保证向量的数值范围统一
  • 注意事项:如果是中文文本,需要确保嵌入模型支持中文语义的理解,避免向量无法准确表达文本语义
  1. 向量库存储

将生成的向量数据存储到向量数据库中,用于后续的相似性检索
Alt

  • 工具选择:你用到的 Elasticsearch 8.0 + 版本已经支持向量存储和向量检索,可以直接使用
  • 存储流程:
    1. 在 Elasticsearch 中创建专门的索引,设置向量字段的类型为dense_vector,指定向量的维度(和嵌入模型生成的向量维度保持一致)
    2. 将每个 Chunk 的向量、Chunk 的唯一 ID、源文件 ID 等信息,写入到 Elasticsearch 的索引中
  • 优化操作:可以为 Elasticsearch 的向量索引设置合适的分片和副本数,提升检索的性能
  1. 文档对应关系存储

建立源文件、文本 Chunk、向量数据之间的关联关系,保证检索结果可以溯源到源文件
Alt

  • 存储内容:
    • 在关系型数据库中,维护源文件 ID、Chunk ID、向量 ID 的对应关系
    • 同时存储 Chunk 的元信息:比如 Chunk 的文本内容、Chunk 在源文件中的位置、Chunk 的长度等
  • 作用:当后续检索到相关的向量时,可以通过这个对应关系,找到对应的 Chunk 文本,以及这个 Chunk 所属的源文件,最终可以将源文件的完整内容返回给用户

向量化

在 RAG 技术(以及整个大模型应用领域)中,向量化(Vectorization) 本质是将非结构化的文本信息转换为计算机可理解、可计算的数值向量的过程,可以把它理解为给每一段文本生成一串 “数字身份证”,这串数字能精准表达文本的语义、情感、逻辑等核心特征。

一、为什么需要向量化?

计算机天生不理解 “文字”,只懂 “数字”。比如:

  • 你看到 “猫” 和 “小猫”,能立刻判断它们语义高度相似;
  • 但计算机直接处理文字时,只能看到两个不同的字符串,无法感知这种相似性。
    Alt
    而向量化就是解决这个问题:把 “猫” 转换成 [0.12, 0.35, -0.21, …](一串固定长度的数字),把 “小猫” 转换成 [0.11, 0.34, -0.22, …]—— 这两个向量的数值高度接近,计算机就能通过计算向量间的距离(比如余弦相似度),判断出 “猫” 和 “小猫” 语义相似。

在 RAG 中,向量化的核心价值是:让后续的 “相似性检索” 成为可能(比如用户提问 “如何训练小猫”,能快速从向量库中找到 “猫的饲养方法” 相关的文本片段)。

二、向量化的核心逻辑

以你用到的 Ollama(嵌入模型)+ Spring AI 为例,向量化的过程可以拆解为 3 步:
Alt

  1. 输入:分割后的文本片段(Chunk)

比如从 PDF 中分割出的一句话:“Spring Boot 是基于 Spring 框架的快速开发脚手架”。

  1. 处理:嵌入模型(Embedding Model)的计算

Ollama 可以部署专门的嵌入模型(比如 nomic-embed-text、bge-large),这类模型的核心作用就是 “语义转数字”:

  • 模型会先对文本做分词(比如把上面的句子拆成 “Spring Boot”“Spring 框架”“快速开发” 等语义单元);
  • 再通过预训练的语义规则,给每个语义单元分配数值权重,最终拼接成固定维度的向量(比如 768 维、1024 维 —— 维度越高,语义表达越精细,但存储 / 计算成本也越高)。
  1. 输出:固定长度的数值向量

比如最终生成的向量可能是:

[0.087, -0.123, 0.456, 0.098, …, -0.321](共 768 个数字,每个数字的取值范围通常在 [-1, 1] 之间)。

三、向量化的关键特征
Alt

  1. 固定维度:同一模型生成的向量长度是固定的(比如 nomic-embed-text 生成 768 维向量),不管输入文本是 10 个字还是 50 个字,输出向量的长度都一样 —— 这是为了后续能统一计算相似度。
  2. 语义等价性:语义相似的文本,向量数值高度相似;语义无关的文本,向量数值差异很大。比如:
    • “Java 开发框架” 和 “Spring 框架” → 向量距离近;
    • “Java 开发框架” 和 “咖啡的冲泡方法” → 向量距离远。
  3. 不可逆(近似):从文本能生成向量,但从向量无法 100% 还原出原文本(向量只保留核心语义,不保留字面细节)。

向量库

在 RAG(检索增强生成)和大模型应用体系中,向量库(Vector Database) 是专门用于存储、管理、检索「文本 / 数据的向量表示」的核心组件 ,可以把它理解为 “语义级别的数据库”,普通数据库(如 MySQL)按 “关键词 / 主键” 检索,而向量库按 “语义相似性” 检索,是实现 RAG 精准检索的核心基础设施。
Alt

向量库的核心作用

存储海量向量 + 快速找到和 “目标向量” 语义最相似的向量,并关联回原始文本 / 数据,为 RAG 提供 “精准的本地知识库素材”。

拆解为 3 个具体作用:

向量存储:安全、结构化管理向量数据

  • 通过 Ollama 生成的文档片段(Chunk)向量(比如 768 维浮点数数组),需要一个专门的地方存储 ,向量库会将向量与「Chunk ID、源文件 ID、Chunk 文本、上传时间」等元信息绑定存储,保证数据完整性。
  • 对比:如果直接存在 MySQL 中,只能用 BLOB/TEXT 存向量数组,无法高效计算相似度;而 Elasticsearch 这类向量库会对向量做结构化存储(dense_vector 字段),适配向量的数值特性。

Alt
相似性检索:核心价值,实现 “语义匹配”

这是向量库最核心的作用 : 当用户提问生成 “问题向量” 后,向量库能在毫秒级内从数万 / 数百万个文档向量中,找到「语义最相似的 Top-K 个向量」:

  • 底层原理:通过优化的相似度算法(如余弦相似度、欧式距离)+ 向量索引(如 HNSW、IVF),避免 “全量遍历计算”(否则百万级向量检索要几秒 / 几分钟,无法落地);
    Alt
    向量管理:支撑知识库的动态维护

实际落地中,你的本地知识库会不断更新(新增文件、删除过期文件、修改文档内容),向量库能支持:

  • 新增:上传新文件后,分割→向量化→插入向量库;
  • 删除:删除源文件时,批量删除关联的向量;
  • 更新:修改文档后,重新分割向量化,替换旧向量;
  • 过滤:检索时可结合元信息过滤(比如 “只检索 2025 年上传的 PDF 文档的向量”)。
向量库解决哪些问题

在这里插入图片描述
举个例子:

  • 用 MySQL 查 “如何用 Spring Boot 做向量检索”,只能找到包含 “Spring Boot”+“向量检索” 关键词的文本;
  • 用 Elasticsearch 向量库查,能找到 “Spring Boot 整合 ES 实现相似性查询” 这类语义相似但关键词不完全匹配的文本 —— 这正是 RAG 需要的 “精准检索”。
向量库的落地价值
  1. 保证回答的 “本地性”:所有向量都存在本地 Elasticsearch 中,检索过程不依赖外部服务,数据隐私可控;
  2. 提升回答的 “精准度”:大模型不再凭空回答,而是基于向量库检索到的 “语义最匹配” 的本地素材作答,避免 “胡说八道”;
  3. 支撑高并发 / 大数据量:如果你的知识库有上千份文档、数百万个 Chunk,向量库的索引优化能保证用户提问后 100~500ms 内返回检索结果,满足实际使用的响应要求;
  4. 溯源便捷:向量库存储了向量与源文件 / Chunk 的关联关系,检索结果能直接关联到原始文档,方便用户核对答案来源。
常见向量库

向量库的核心价值在于高维向量的高效存储与快速相似性检索,不同产品在性能、功能、部署方式上各有侧重,以下是业界常用的几款向量库:

  1. FAISS(Facebook AI Similarity Search)
  • 核心定位:Facebook 开源的轻量级向量检索库,专注于单机高性能向量检索。
  • 关键特性:支持稠密向量的 L2、内积等相似度计算,提供多种索引类型(Flat、IVF、HNSW 等),可通过量化(Scalar Quantization、Product Quantization)降低存储成本。
  • 适用场景:小规模数据场景、离线向量检索任务,需结合其他工具实现分布式部署。
  1. Pinecone
  • 核心定位:云端托管式向量数据库,主打 “零运维” 的向量检索服务。
  • 关键特性:完全托管,支持自动扩缩容,提供 REST API 接口,兼容多种向量生成模型,内置数据备份与高可用机制。
  • 适用场景:快速上线的业务系统、不愿投入运维资源的中小团队,按使用量付费。
  1. Weaviate
  • 核心定位:开源的分布式向量数据库,支持混合检索(向量检索 + 结构化数据过滤)。
  • 关键特性:基于 GraphQL 查询接口,支持动态模式定义,内置文本向量化功能(集成 Hugging Face 模型),支持容器化部署。
  • 适用场景:需要结合结构化数据与非结构化数据检索的场景,如智能文档管理系统。
  1. Qdrant
  • 核心定位:轻量级开源向量数据库,主打 “简单易用” 与 “低资源占用”。
  • 关键特性:支持稠密向量与稀疏向量检索,提供 REST API 和 gRPC 接口,支持动态索引更新,部署简单(单二进制文件或容器)。
  • 适用场景:小规模部署、边缘计算场景、快速原型验证。
  1. Milvus
  • 核心定位:开源分布式向量数据库,专为大规模高维向量检索设计,兼顾性能、可靠性与扩展性。
  • 关键特性:支持 PB 级向量存储、毫秒级检索响应,兼容多索引类型与相似度度量方式,提供完善的分布式架构与运维工具。
  • 适用场景:大规模生产环境、高并发检索需求、多模态数据处理系统。

6.ElasticSearch

  • 核心定位:开源分布式全文检索与分析引擎,支持向量存储 / 检索能力,兼顾文本检索与语义相似性检索,适配多场景数据处理需求。
  • 关键特性:原生支持全文关键词检索 + 向量稠密向量(dense_vector)存储,内置余弦相似度 / 欧氏距离等度量方式,具备成熟的分布式分片 / 副本机制、动态扩缩容能力,可结合元数据过滤实现精准的混合检索(关键词 + 语义)。
  • 适用场景:中小规模向量检索场景、文本 + 向量混合检索需求、已有 ES 生态的企业级生产环境、需轻量化部署的本地知识库系统。

示例代码

代码仓库:https://gitee.com/xiangweilll/spring-ai-rag-redis-elasticsearch.git

项目结构
Alt

核心依赖

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.elvis</groupId>
  <artifactId>spring-ai-rag-redis-elasticsearch</artifactId>
  <version>1.0</version>
  <packaging>jar</packaging>

  <name>spring-ai-rag-redis-elasticsearch</name>
  <url>http://maven.apache.org</url>

  <properties>
    <java.version>17</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring-boot.version>3.3.4</spring-boot.version>
    <spring-ai.version>1.0.0-M6</spring-ai.version>
    <mybatis-plus.version>3.5.9</mybatis-plus.version>
    <mysql.version>8.0.33</mysql.version>
    <elasticsearch.version>8.14.0</elasticsearch.version>
    <poi.version>5.2.4</poi.version>
    <apache-lucene.version>8.11.4</apache-lucene.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>

    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-core</artifactId>
      <version>${spring-ai.version}</version>
    </dependency>

    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
      <version>${spring-ai.version}</version>
    </dependency>

    <dependency>
      <groupId>com.alibaba.fastjson2</groupId>
      <artifactId>fastjson2</artifactId>
      <version>2.0.55</version>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
      <version>${mybatis-plus.version}</version>
    </dependency>

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>${mysql.version}</version>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-elasticsearch-store</artifactId>
      <version>${spring-ai.version}</version>
    </dependency>
    <dependency>
      <groupId>co.elastic.clients</groupId>
      <artifactId>elasticsearch-java</artifactId>
      <version>${elasticsearch.version}</version>
    </dependency>
    <dependency>
      <groupId>com.janeluo</groupId>
      <artifactId>ikanalyzer</artifactId>
      <version>2012_u6</version>
      <exclusions>
        <exclusion>
          <groupId>org.apache.lucene</groupId>
          <artifactId>lucene-core</artifactId>
        </exclusion>
        <exclusion>
          <groupId>org.apache.lucene</groupId>
          <artifactId>lucene-analyzers-common</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.apache.lucene</groupId>
      <artifactId>lucene-core</artifactId>
      <version>${apache-lucene.version}</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.lucene</groupId>
      <artifactId>lucene-analyzers-common</artifactId>
      <version>${apache-lucene.version}</version>
      <scope>runtime</scope>
    </dependency>
    <!-- 文本处理 -->
    <dependency>
      <groupId>org.apache.tika</groupId>
      <artifactId>tika-core</artifactId>
      <version>2.9.1</version>
    </dependency>
    <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi</artifactId>
      <version>${poi.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi-ooxml</artifactId>
      <version>${poi.version}</version>
    </dependency>

    <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi-scratchpad</artifactId>
      <version>${poi.version}</version>
    </dependency>
    <!-- 字符编码处理 -->
    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>32.1.3-jre</version>
    </dependency>

    <!-- PDFBox for PDF -->
    <dependency>
      <groupId>org.apache.pdfbox</groupId>
      <artifactId>pdfbox</artifactId>
      <version>2.0.29</version>
    </dependency>

    <dependency>
      <groupId>com.googlecode.juniversalchardet</groupId>
      <artifactId>juniversalchardet</artifactId>
      <version>1.0.3</version>
    </dependency>

  </dependencies>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>${spring-boot.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-client</artifactId>
        <version>${elasticsearch.version}</version>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <source>17</source>
          <target>17</target>
          <encoding>UTF-8</encoding>
          <compilerArgs>
            <arg>-parameters</arg>  <!--在编译代码是保留参数的名字-->
          </compilerArgs>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>${spring-boot.version}</version>
        <configuration>
          <mainClass>com.elvis.SpringAiRagApplication</mainClass>
          <skip>true</skip>
        </configuration>
        <executions>
          <execution>
            <id>repackage</id>
            <goals>
              <goal>repackage</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>
主配置文件
spring:
  elasticsearch:
    uris: http://localhost:9200  # 明确指定 HTTP 协议
    username: elastic
    password: root123
    connection-timeout: 5000
    socket-timeout: 30000
  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        options:
          model: qwen2.5:1.5b
          temperature: 0.2
      embedding-model-name: bge-m3:latest
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mall_116?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: root1234
# 知识库配置
knowledge:
  vector-index: knowledge-base    # 向量索引名(和主索引保持一致)
  vector-dims: 1024               # 向量维度(根据模型调整,qwen2.5:1.5b 通常是 1024/768)
  chunk-overlap: 80
  chunk-min-length: 50
  chunk-max-length: 600
  top-k: 10
  file-upload-dir: D:/knowledge-uploads/
logging:
  level:
    org.springframework.data.elasticsearch: debug  # 增加 ES 日志,便于排查
    org.springframework.ai: debug                  # 增加 AI 模块日志

AI配置

import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.OllamaEmbeddingModel;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.ai.ollama.management.ModelManagementOptions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @Author XiangWei
* @Date 2025/12/15 14:55
* @Description:
*/
@Configuration
public class AiConfig {
   @Value("${spring.ai.ollama.embedding-model-name:bge-m3:latest}")
   private String ollamaEmbeddingModelName;

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


   // 嵌入模型配置
   @Bean
   public ObservationRegistry observationRegistry() {
       return ObservationRegistry.NOOP;
   }

   @Bean
   public ModelManagementOptions modelManagementOptions() {
       return ModelManagementOptions.defaults();
   }

   @Bean
   public OllamaOptions embeddingOllamaOptions() {
       return OllamaOptions.builder()
               .model(ollamaEmbeddingModelName)
               .build();
   }
   @Bean
   public OllamaEmbeddingModel ollamaEmbeddingModel(
           OllamaApi ollamaApi,
           OllamaOptions embeddingOllamaOptions,
           ObservationRegistry observationRegistry,
           ModelManagementOptions modelManagementOptions
   ) {
       return new OllamaEmbeddingModel(
               ollamaApi,
               embeddingOllamaOptions,
               observationRegistry,
               modelManagementOptions
       );
   }
}

es配置

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
import org.springframework.ai.ollama.OllamaEmbeddingModel;
import org.springframework.ai.vectorstore.elasticsearch.ElasticsearchVectorStore;
import org.springframework.ai.vectorstore.elasticsearch.ElasticsearchVectorStoreOptions;
import org.springframework.ai.vectorstore.elasticsearch.SimilarityFunction;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
import java.time.Duration;

/**
 * @Author XiangWei
 * @Date 2025/12/15 15:05
 * @Description: Elasticsearch 配置类
 */
@Configuration
public class ElasticSearchConfig extends ElasticsearchConfiguration {

    @Value("${spring.elasticsearch.uris}")
    private String esUris;

    @Value("${spring.elasticsearch.username}")
    private String esUsername;

    @Value("${spring.elasticsearch.password}")
    private String esPassword;

    // 向量索引名
    @Value("${knowledge.vector-index:knowledge-base}")
    private String vectorIndex;

    // 向量维度
    @Value("${knowledge.vector-dims:1024}")
    private int vectorDims;

    @Override
    public ClientConfiguration clientConfiguration() {
        String hostAndPort = esUris.replace("https://", "").replace("http://", "");

        SSLContext sslContext = createSSLContext();

        return ClientConfiguration.builder()
                .connectedTo(hostAndPort)
                .usingSsl(sslContext)
                .withBasicAuth(esUsername, esPassword)
                .withConnectTimeout(Duration.ofMillis(10000))
                .withSocketTimeout(Duration.ofMillis(30000))
                .build();
    }

    /**
     * 创建 Elasticsearch RestClient(用于向量存储)
     */
    @Bean
    public RestClient elasticsearchRestClient() {
        String hostAndPort = esUris.replace("https://", "").replace("http://", "");
        String[] hostPort = hostAndPort.split(":");
        String host = hostPort[0];
        int port = hostPort.length > 1 ? Integer.parseInt(hostPort[1]) : 9200;

        SSLContext sslContext = createSSLContext();

        // 创建凭证提供者
        CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(
                AuthScope.ANY,
                new UsernamePasswordCredentials(esUsername, esPassword)
        );

        return RestClient.builder(new HttpHost(host, port, esUris.startsWith("https") ? "https" : "http"))
                .setRequestConfigCallback(requestConfigBuilder ->
                        requestConfigBuilder
                                .setConnectTimeout(10000)
                                .setSocketTimeout(30000))
                .setHttpClientConfigCallback(httpClientBuilder ->
                        httpClientBuilder
                                .setSSLContext(sslContext)
                                .setDefaultCredentialsProvider(credentialsProvider))
                .build();
    }

    /**
     * 创建 SSL 上下文
     */
    private SSLContext createSSLContext() {
        try {
            SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
            sslContext.init(null, new TrustManager[]{new X509TrustManager() {
                @Override
                public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
                @Override
                public void checkClientTrusted(X509Certificate[] certs, String authType) {}
                @Override
                public void checkServerTrusted(X509Certificate[] certs, String authType) {}
            }}, new java.security.SecureRandom());
            return sslContext;
        } catch (Exception e) {
            throw new RuntimeException("初始化 SSL 上下文失败", e);
        }
    }

    /**
     * 创建 Elasticsearch 操作模板
     */
    @Bean
    public ElasticsearchOperations elasticsearchOperations(ElasticsearchClient elasticsearchClient) {
        return new ElasticsearchTemplate(elasticsearchClient);
    }

    /**
     * 配置 Spring AI Elasticsearch 向量存储
     */
    @Bean
    public ElasticsearchVectorStore elasticsearchVectorStore(
            RestClient restClient,
            OllamaEmbeddingModel ollamaEmbeddingModel) {

        // 创建配置选项
        ElasticsearchVectorStoreOptions options = new ElasticsearchVectorStoreOptions();
        options.setIndexName(vectorIndex);
        options.setDimensions(vectorDims);
        options.setSimilarity(SimilarityFunction.cosine);

        // 使用 Builder 模式创建向量存储
        return ElasticsearchVectorStore.builder(restClient, ollamaEmbeddingModel)
                .options(options)
                .initializeSchema(true) // 自动创建索引和映射
                .build();
    }
}

线程池配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@EnableAsync // 必须开启异步注解支持
public class AsyncThreadPoolConfig  {

    /**
     * 对话记录落库专用线程池(指定bean名称,供@Async引用)
     *
     */
    @Bean(name = "chatTaskExecutor")
    public Executor chatTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数(根据服务器配置调整,比如4核8G设为8)
        // 获取到当前服务器CPU核心数
        int cpus = Runtime.getRuntime().availableProcessors();
        executor.setCorePoolSize(cpus);
        // 最大线程数(核心线程忙不过来时,最多扩容到这个数)
        executor.setMaxPoolSize(cpus * 2);
        // 队列容量(核心线程满了,任务先入队列,避免直接创建新线程)
        executor.setQueueCapacity(1000);
        // 线程空闲时间(超过60秒空闲的非核心线程会被销毁)
        executor.setKeepAliveSeconds(60);
        // 线程命名前缀(便于日志排查问题)
        executor.setThreadNamePrefix("chat-async-");
        // 拒绝策略(队列满+最大线程数满时,让提交任务的线程执行,避免任务丢失)
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 初始化线程池(必须调用,否则线程池不生效)
        executor.initialize();
        return executor;
    }
}

SpringMvc请求配置

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Spring MVC 异步请求配置(解决默认SimpleAsyncTaskExecutor告警)
 */
@Configuration
@RequiredArgsConstructor
public class WebMvcAsyncConfig implements WebMvcConfigurer {

    // 注入自定义的线程池(指定bean名称)
    private final ThreadPoolTaskExecutor chatTaskExecutor;

    /**
     * 配置MVC异步请求的线程池
     */
    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        // 1. 指定MVC异步请求使用的线程池
        configurer.setTaskExecutor(chatTaskExecutor);
        // 2. 可选:设置异步请求超时时间(默认30秒,根据业务调整)
        configurer.setDefaultTimeout(60 * 1000); // 60秒
    }
}

文件上传(添加知识库)

建表sql
create table file_info(
    id bigint primary key,
    file_name varchar(256),
    file_type varchar(32),
    file_path varchar(256),
    create_time datetime
);
create table file_document(
    id bigint primary key,
    file_info_id bigint,
    document_id varchar(64)
);
实体类
package com.elvis.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@TableName("file_info")
public class FileInfo {
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;
    private String fileName;
    private String fileType;
    private String filePath;
    private LocalDateTime createTime;
}
package com.elvis.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("file_document_re")
public class FileDocumentRe {
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;
    private Long fileInfoId;
    private String documentId;
}
Mapper层
package com.elvis.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.elvis.entity.FileInfo;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface FileInfoMapper extends BaseMapper<FileInfo> {
}
package com.elvis.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.elvis.entity.FileDocumentRe;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface FileDocumentReMapper extends BaseMapper<FileDocumentRe> {
}
service层
package com.elvis.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.elvis.entity.FileInfo;
import org.springframework.ai.document.Document;

import java.util.List;

public interface FileInfoService extends IService<FileInfo> {
    public void saveFileInfo(String fileName,String filePath, List<Document> documents);
}
package com.elvis.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.elvis.entity.FileDocumentRe;

public interface FileDocumentReService extends IService<FileDocumentRe> {
}

实现类

import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;

/**
 * @Author XiangWei
 * @Date 2025/12/16 10:34
 * @Description:
 */
@Slf4j
@Data
@Service
@RequiredArgsConstructor
public class FileInfoServiceImpl extends ServiceImpl<FileInfoMapper, FileInfo> implements FileInfoService {

    private final FileDocumentReService fileDocumentReService;

    @Async("chatTaskExecutor")
    @Override
    public void saveFileInfo(String fileName, String filePath, List<Document> documents) {
        log.info("文件 {} 开始保存文件信息到数据库,线程id:{}", fileName, Thread.currentThread().getId());
        // 1.创建文件信息实体
        FileInfo fileInfo = new FileInfo();
        fileInfo.setFileName(fileName);
        fileInfo.setFilePath(filePath);
        fileInfo.setFileType(fileName.substring(fileName.lastIndexOf(".") + 1));
        fileInfo.setCreateTime(LocalDateTime.now());

        // 2.保存
        save(fileInfo);

        // 3.保存文件文档实体
        fileDocumentReService.saveBatch(documents.stream().map(document -> {
            FileDocumentRe fileDocument = new FileDocumentRe();
            fileDocument.setFileInfoId(fileInfo.getId());
            fileDocument.setDocumentId(document.getId());
            return fileDocument;
        }).toList());
    }
}
package com.elvis.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.elvis.entity.FileDocumentRe;
import com.elvis.mapper.FileDocumentReMapper;
import com.elvis.service.FileDocumentReService;
import org.springframework.stereotype.Service;

@Service
public class FileDocumentReServiceImpl extends ServiceImpl<FileDocumentReMapper, FileDocumentRe> implements FileDocumentReService {
}
controller层
package com.woniuxy.knowledge.controller;

import com.woniuxy.knowledge.service.VectorService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

/**
 * @Author XiangWei
 * @Date 2025/12/15 14:18
 * @Description:
 */
@RestController
@RequestMapping("/chat")
@RequiredArgsConstructor
public class ChatController {
    private final VectorService vectorService;

    // 上传文件
    @PostMapping("/upload")
    public ResponseEntity uploadFile(MultipartFile file) throws IOException {
        // 调用向量服务上传文件
        return vectorService.uploadFile(file);
    }
}

知识库问答

基本流程
Alt

参数类
package com.elvis.params;

import lombok.Data;

@Data
public class ChatRequest {
    private String message;
}
service层
package com.elvis.service;

import reactor.core.publisher.Flux;

public interface KnowledgeService {
    Flux<String> ask(String message);
}

实现类

import com.woniuxy.knowledge.service.KnowledgeService;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.elasticsearch.ElasticsearchVectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.ai.document.Document;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @Author XiangWei
 * @Date 2025/12/15 19:03
 * @Description:
 */
@Service
@RequiredArgsConstructor
public class KnowledgeServiceImpl implements KnowledgeService {
    private final ElasticsearchVectorStore elasticsearchVectorStore;
    private final ChatClient chatClient;

    // 提示词模板(核心:告诉AI基于检索到的上下文回答问题)
    private static final String PROMPT_TEMPLATE = """
            1.请基于以下提供的上下文信息回答用户的问题,只使用上下文里的内容,不要编造信息。
            2.如果上下文里没有相关信息,请回答"暂无相关信息"。
            3.只包含上下文中的信息,不要包含其它信息。
            
            上下文:
            {context}
            
            用户问题:
            {question}
            
            回答:
            """;

    @Value("${knowledge.top-k:3}")
    private int topK;

    @Override
    public Flux<String> ask(String message) {
        // 步骤1:从ES向量库检索最相似的文档(自动将提问转为向量并做相似度匹配)
        SearchRequest searchRequest = SearchRequest.builder()
                .topK(topK)
                .query(message).build();
        List<Document> documents = elasticsearchVectorStore.similaritySearch(searchRequest);

        // 步骤2:拼接检索到的文档作为上下文
        String context = documents.stream()
                .map(Document::getText) // 获取文档内容
                .collect(Collectors.joining("\n\n")); // 多个文档用换行分隔

        // 步骤3:构建Prompt(将上下文和问题传入模板)
        PromptTemplate promptTemplate = new PromptTemplate(PROMPT_TEMPLATE);
        Map<String, Object> params = new HashMap<>();
        params.put("context", context);
        params.put("question", message);
        Prompt prompt = promptTemplate.create(params);

        // 步骤4:调用 AI
        return chatClient.prompt()
                .user(prompt.getContents()) // 用户输入
                .stream() // 流式响应
                .content() // 只提取内容(过滤元数据)
                .filter(content -> content != null && !content.trim().isEmpty());
    }
}
controller层

controller中添加代码

private final KnowledgeService knowledgeService;

// 知识库问答 流式输出
@PostMapping(value = "/ask", produces = "text/event-stream")
public Flux<String> ask(@RequestBody ChatRequest request) {
    return knowledgeService.ask(request.getMessage());
}
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐