Spring AI Alibaba + Ollama 的 RAG 智能问答系统:完整实现指南


Spring AI Alibaba + Ollama+Embedding向量化项目完整指南:

https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/161612772

基于上述基础,实现RAG智能本地知识库问答。

参考SpringAI的实现:

Spring AI + Ollama 深度实战:从 RAG 问答到 Graph Agent 全流程指南:

https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/161199169

一、什么是 RAG?

1.1 概念定义

RAG(Retrieval-Augmented Generation,检索增强生成)是一种将信息检索大语言模型(LLM)文本生成相结合的技术范式。

传统 LLM 存在两个核心痛点:

  • 知识截止:模型训练数据有截止日期,无法回答最新信息
  • 幻觉问题:模型可能生成看似合理但实际错误的内容

RAG 的核心思想是:先检索,再生成。当用户提问时,先从外部知识库中检索出与问题相关的文档片段,将这些片段作为上下文注入到 Prompt 中,再交给 LLM 生成最终回答。这样既让模型拥有最新知识,又大幅减少了幻觉。

1.2 RAG vs 微调(Fine-tuning)

维度 RAG 微调
知识更新 实时更新,改文档即可 需要重新训练
成本 低,无需训练 高,需要 GPU 资源
可解释性 高,可追溯引用来源 低,知识融入模型权重
适用场景 企业知识库、文档问答 风格调整、特定任务优化

二、RAG 核心流程概览

RAG 的完整流程分为两个大阶段:离线索引阶段(Indexing)在线问答阶段(Querying)

在线问答阶段

离线索引阶段

原始文档

文档分块

文本向量化

存入向量数据库

用户提问

问题向量化

向量相似度检索

构建增强 Prompt

LLM 生成回答


三、离线索引阶段:知识库构建

3.1 文档加载(Document Loading)

文档是知识库的原始素材。在本项目中,支持以下输入方式:

  • 文本直接输入:用户在前端直接粘贴文本
  • 文件上传:支持 .txt.md 文件上传
  • 预置示例数据:系统内置的 AI 知识条目

关键代码在 RagController 中提供了三个入口:

POST /api/rag/document          → 添加单条文本
POST /api/rag/document/upload   → 上传文件
POST /api/rag/init-sample-data  → 加载示例数据

3.2 文档分块(Chunking)—— RAG 中最重要的预处理步骤

为什么要分块?

LLM 有上下文窗口长度限制,且过长的文本会降低检索的精确度。将文档切分为合适大小的片段(Chunk),既能保证语义完整性,又能提高检索匹配率。

本项目的三种分块策略

策略一:按段落分块(Paragraph Chunking)

以双换行符 \n\n+ 为分隔符,每个自然段作为独立块。

// DocumentChunkService.chunkByParagraph()
String[] paragraphs = text.split("\n\n+");

优点:语义边界自然,不会切断句子
缺点:如果文档段落过长,单块可能超出模型上下文


策略二:按固定大小分块(Size-based Chunking)

设定固定字符数(如 500 字符),并支持 Overlap(重叠),避免语义断裂。

// DocumentChunkService.chunkBySize()
int start = 0;
while (start < text.length()) {
    int end = Math.min(start + chunkSize, text.length());
    // 尝试在句子边界分割(句号、问号、感叹号)
    chunks.add(text.substring(start, end).trim());
    start = end - overlap;  // 重叠区域,保证上下文连续
}

Overlap(重叠):相邻两个块共享一部分文本,防止切分点正好在关键信息中间。例如设置 overlap=50,则相邻块共享 50 个字符的上下文。

句子边界检测:项目还实现了智能断句,在切分点优先选择句号 、问号 、感叹号 等标点符号作为分界点。


策略三:智能分块(Smart Chunking)—— 推荐策略

结合段落分割和大小控制:先按段落拆分,再将小段落合并成不超过 maxChunkSize 的块。

// DocumentChunkService.smartChunk()
for (String paragraph : paragraphs) {
    if (currentChunk.length() + paragraph.length() > maxChunkSize 
        && currentChunk.length() > 0) {
        chunks.add(currentChunk.toString().trim());
        currentChunk.setLength(0);
    }
    currentChunk.append(paragraph).append("\n\n");
}

最佳实践:建议每块 300-800 字,过小丢失语义上下文,过大降低检索精度。


3.3 文本向量化(Embedding)

什么是 Embedding?

Embedding(文本嵌入) 是将文本转换为一个高维浮点数组(向量)的过程。语义相近的文本,其向量在空间中的距离也更近。

"员工年假政策" → [0.12, -0.34, 0.56, ..., 0.78]  (如 768 维)
"带薪年假规定" → [0.11, -0.33, 0.55, ..., 0.77]  (语义相近,向量也相近)
"量子力学原理" → [-0.89, 0.45, -0.12, ..., 0.03]  (语义无关,向量相距远)
本项目使用的 Embedding 模型

application.yml 中配置:

spring.ai.ollama.embedding.options.model: bge-m3

bge-m3 是一个多语言 embedding 模型,对中文有良好支持。此前使用 nomic-embed-text(英文为主的模型),导致中文文档的相似度偏低。

模型下载:

ollama pull bge-m3

余弦相似度(Cosine Similarity)

衡量两个向量之间相似程度的的数学指标,取值范围 [-1, 1]

[
\cos(\theta) = \frac{\vec{A} \cdot \vec{B}}{|\vec{A}| \times |\vec{B}|}
]

  • 值为 1:完全相同
  • 值为 0:不相关
  • 值越接近 1,语义越相似

本项目在 EmbeddingService 中实现了该算法:

public static double cosineSimilarity(float[] vec1, float[] vec2) {
    double dotProduct = 0.0, norm1 = 0.0, norm2 = 0.0;
    for (int i = 0; i < vec1.length; i++) {
        dotProduct += vec1[i] * vec2[i];
        norm1 += vec1[i] * vec1[i];
        norm2 += vec2[i] * vec2[i];
    }
    return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
}

3.4 向量存储(Vector Store)

向量化后的文档片段需要存入向量数据库。本项目使用 Spring AI 内置的 SimpleVectorStore(内存向量存储),适合开发测试:

// RagService 构造函数
this.vectorStore = SimpleVectorStore.builder(embeddingModel).build();

添加文档到向量存储时,会自动调用 Embedding 模型进行向量化:

// 构建 Document 对象
Document document = new Document.Builder()
    .text(text)                          // 文本内容
    .id(UUID.randomUUID().toString())    // 唯一ID
    .metadata(metadata)                  // 元数据(来源、分类等)
    .build();
vectorStore.add(List.of(document));      // 存入时自动向量化

元数据(Metadata):附加在文档上的结构化信息,如来源文件名、分类、分块序号等。检索后可根据元数据进行过滤和追溯。

生产环境建议:使用 Milvus、Chroma、Pinecone、Weaviate 等专业向量数据库替代内存存储。


注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

四、在线问答阶段:RAG 检索与生成

4.1 完整问答流程

当用户通过前端页面发送问题时,后端经历以下步骤:

用户提问

问题向量化

向量相似度检索

筛选 Top-K 文档

构建增强 Prompt

调用 LLM 生成回答

返回答案给用户

4.2 向量相似度检索(Similarity Search)

用户的问题首先被向量化,然后在向量数据库中搜索最相似的文档片段。

// RagService.searchSimilarDocuments()
vectorStore.similaritySearch(
    SearchRequest.builder()
        .query(question)              // 用户问题
        .topK(topK)                   // 返回前 K 个最相似文档
        .similarityThreshold(threshold) // 相似度阈值过滤
        .build()
);
两个关键参数
参数 含义 建议值
Top-K 返回最相似的 K 个文档数量 5-20
相似度阈值(Threshold) 低于此分数的文档会被过滤 0.3-0.7
  • Top-K 过大:引入噪音文档,影响回答质量
  • Top-K 过小:可能遗漏重要信息
  • 阈值过高:过于严格,可能找不到任何文档
  • 阈值过低:引入不相关内容

4.3 构建增强 Prompt

检索到的相关文档会被组装成 Prompt 的参考资料部分,连同用户问题一起发送给 LLM:

// RagService.buildRagPrompt()
String prompt = String.format("""
    你是一个智能问答助手。请基于以下提供的参考资料来回答用户的问题。
    
    要求:
    1. 如果资料中有相关信息,请结合资料内容给出准确、完整的回答
    2. 如果资料中没有相关信息,请明确说明"知识库中没有相关信息"
    3. 回答要简洁明了,条理清晰
    4. 可以适当引用资料中的关键信息
    
    【参考资料】
    %s
    
    【用户问题】
    %s
    
    【你的回答】
    """, context, question);

这个 Prompt 模板的设计要点:

  • 角色定义:“你是一个智能问答助手” — 引导模型行为
  • 行为约束:明确要求基于资料回答,不确定时坦诚告知
  • 参考资料注入:将检索到的文档内容填入上下文
  • 问题明确:用户原始问题放在最后

4.4 LLM 生成回答

最终将构建好的 Prompt 发送给本地部署的 Ollama 大模型(qwen2.5:7b-instruct):

// RagService.answerWithRAG()
String answer = chatClient.prompt(prompt)
    .call()
    .content();

temperature 参数application.yml 中设为 0.3):控制生成文本的随机性。RAG 场景通常设较低值(0.1-0.3),以获得更准确、确定性的回答。


五、项目技术架构

5.1 技术栈总览

组件 技术选型 作用
后端框架 Spring Boot 3.2.5 Web 服务框架
AI 框架 Spring AI 1.0.0-M6 统一 AI 能力抽象
大语言模型 Ollama + qwen2.5:7b-instruct 本地部署的生成式模型
Embedding 模型 Ollama + bge-m3 文本向量化
向量数据库 SimpleVectorStore(内存) 向量存储与检索
前端 原生 HTML + JavaScript 交互式测试页面

5.2 核心类职责

类名 职责
RagService RAG 核心服务:文档管理、向量检索、Prompt 构建、LLM 调用
DocumentChunkService 文档分块:支持段落分块、固定大小分块、智能分块三种策略
EmbeddingService 向量化服务:文本转向量、余弦相似度计算、批量相似度矩阵
RagController REST API 控制器:暴露文档上传、RAG 问答、文档搜索等接口
EmbeddingController Embedding 独立接口:提供向量化和相似度分析的测试 API

5.3 API 接口一览

POST /api/rag/document             → 添加单条文本
POST /api/rag/document/upload      → 上传文件
POST /api/rag/document/chunked     → 智能分块后添加
POST /api/rag/documents/batch      → 批量添加
POST /api/rag/init-sample-data     → 初始化示例数据
POST /api/rag/ask                  → RAG 问答(核心接口)
POST /api/rag/search               → 向量检索(不含 LLM 生成)
GET  /api/rag/stats                → 知识库统计

GET  /ai/embed                     → 单文本向量化
POST /ai/embed/similarity          → 两文本相似度
POST /ai/embed/batch-similarity    → 批量相似度矩阵
POST /ai/embed/batch               → 批量向量化

六、关键概念速查表

概念 英文 说明
RAG Retrieval-Augmented Generation 检索增强生成,先检索再生成
Embedding Text Embedding 文本向量化,将文本映射到高维向量空间
向量数据库 Vector Store / Vector Database 存储和检索向量的数据库
分块 Chunking 将长文档切分为适合检索的小片段
Top-K Top-K Retrieval 返回相似度最高的 K 个文档
相似度阈值 Similarity Threshold 过滤低相关度文档的分数线
余弦相似度 Cosine Similarity 衡量两个向量间语义相似程度的数学指标
元数据 Metadata 附加在文档上的结构化信息
Prompt Prompt Engineering 提示工程,构造给 LLM 的输入文本
温度 Temperature 控制 LLM 输出随机性的参数
Overlap Chunk Overlap 分块重叠,保证相邻块的上下文连续性

七、RAG 系统调优要点

  1. 分块策略选择:根据文档类型选择合适的分块策略。结构化文档适合段落分块,长文适合智能分块。每块建议 300-800 字。

  2. Embedding 模型选择:中文场景选择 bge-m3 等多语言模型,英文场景可用 nomic-embed-text。模型质量直接决定检索精度。

  3. Top-K 和阈值调优:根据实际问答效果动态调整。知识库内容多时,适当增大 Top-K;阈值过高会导致"找不到",过低会引入噪音。

  4. Prompt 工程:明确告知模型"只基于参考资料回答",避免模型凭自身知识"编造"答案。

  5. 字符编码:读取上传文件时必须显式指定 UTF-8 编码(StandardCharsets.UTF_8),否则中文内容会乱码,导致分块、向量化、LLM 生成全部失败。


八、关键代码实现

1、pom文件

    <properties>
        <java.version>17</java.version>
        <spring-ai.version>1.0.0-M6</spring-ai.version>
        <spring-ai-alibaba.version>1.0.0-M6.1</spring-ai-alibaba.version>
    </properties>

    <dependencies>
        <!-- Spring Boot Web Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring AI Ollama Starter(提供 ChatClient.Builder 和 Ollama 支持) -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
            <version>${spring-ai.version}</version>
        </dependency>

        <!-- Spring AI Alibaba(提供 @Tool 注解支持,排除 DashScope 自动配置) -->
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-core</artifactId>
            <version>${spring-ai-alibaba.version}</version>
        </dependency>
    </dependencies>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

2、yml

server:
  port: 885

logging:
  level:
    com.badao: debug
    org.springframework.ai: debug

spring:
  okhttp:                     # 注意前缀是 spring.okhttp
    connect-timeout: 30s
    read-timeout: 180s
    write-timeout: 60s
  restclient:
    connect-timeout: 30s    # 建立连接超时
    read-timeout: 180s      # 等待响应的超时(应大于 chat.timeout)
  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        options:
          model: qwen2.5:7b-instruct
          temperature: 0.3
          timeout: 120s   # 单次请求等待模型输出的最大时间
      embedding:
        options:
          model: bge-m3
          timeout: 120s   # 单次请求等待模型输出的最大时间
  
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB

3、ChatClientConfig

package com.badao.ai.config;

import com.badao.ai.function.ToolService;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ChatClientConfig {

    @Bean
    public ChatClient chatClientWithTools(ChatClient.Builder builder, ToolService toolService) {
        return builder.build();
    }
}

4、ollamaApi 连接Ollama超时配置

package com.badao.ai.config;

import okhttp3.OkHttpClient;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
import org.springframework.web.reactive.function.client.WebClient;

import java.time.Duration;

@Configuration
public class OllamaTimeoutConfig {

    @Value("${spring.ai.ollama.base-url}")
    private String baseUrl;

    @Bean
    public OllamaApi ollamaApi() {
        // 1. 自定义 OkHttpClient 超时
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .connectTimeout(Duration.ofSeconds(30))
                .readTimeout(Duration.ofMinutes(3))   // 3 分钟,足够大模型生成
                .writeTimeout(Duration.ofSeconds(60))
                .build();

        // 2. 创建 OkHttp3ClientHttpRequestFactory(虽然已过时,但可用)
        OkHttp3ClientHttpRequestFactory factory =
                new OkHttp3ClientHttpRequestFactory(okHttpClient);

        // 3. 构建 RestClient.Builder,注入自定义 factory
        RestClient.Builder restClientBuilder = RestClient.builder()
                .baseUrl(baseUrl)
                .requestFactory(factory);

        // 4. 提供 WebClient.Builder(必须,传默认空 builder 即可)
        WebClient.Builder webClientBuilder = WebClient.builder();

        // 5. 调用正确构造函数:OllamaApi(String, RestClient.Builder, WebClient.Builder)
        return new OllamaApi(baseUrl, restClientBuilder, webClientBuilder);
    }
}

5、RagService

package com.badao.ai.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;

import java.io.File;
import java.util.*;

@Service
public class RagService {

    private static final Logger log = LoggerFactory.getLogger(RagService.class);

    private final EmbeddingModel embeddingModel;
    private final ChatClient chatClient;
    private final VectorStore vectorStore;

    public RagService(EmbeddingModel embeddingModel, ChatClient.Builder chatClientBuilder) {
        this.embeddingModel = embeddingModel;
        this.chatClient = chatClientBuilder.build();
        // 使用内存向量存储(生产环境建议使用 Milvus、Chroma 等)
        this.vectorStore = SimpleVectorStore.builder(embeddingModel).build();
    }

    /**
     * 添加文档到知识库
     */
    public void addDocument(String text, String id, Map<String, Object> metadata) {
        Document document = new Document.Builder()
                .text(text)
                .id(id != null ? id : UUID.randomUUID().toString())
                .metadata(metadata != null ? metadata : new HashMap<>())
                .build();
        
        vectorStore.add(List.of(document));
    }

    /**
     * 批量添加文档
     */
    public void addDocuments(List<String> texts, List<Map<String, Object>> metadatas) {
        List<Document> documents = new ArrayList<>();
        for (int i = 0; i < texts.size(); i++) {
            Map<String, Object> metadata = i < metadatas.size() ? metadatas.get(i) : new HashMap<>();
            Document document = new Document.Builder()
                    .text(texts.get(i))
                    .id(UUID.randomUUID().toString())
                    .metadata(metadata)
                    .build();
            documents.add(document);
        }
        vectorStore.add(documents);
    }

     /**
     * 从文本文件添加文档(简单分块)
     */
    public void addDocumentFromFile(File file, String category) {
        try {
            String content = new String(java.nio.file.Files.readAllBytes(file.toPath()), java.nio.charset.StandardCharsets.UTF_8);
            log.info("开始处理文件: {}, 内容长度: {} 字符", file.getName(), content.length());

            // 统一换行符,兼容 Windows (\r\n) 和 Unix (\n)
            content = content.replace("\r\n", "\n").replace("\r", "\n");
            
            // 简单按段落分割
            String[] chunks = content.split("\n\n+");
            log.info("文件分块完成,共 {} 个段落", chunks.length);
            
            List<Map<String, Object>> metadatas = new ArrayList<>();
            for (int i = 0; i < chunks.length; i++) {
                String chunk = chunks[i].trim();
                if (chunk.isEmpty()) {
                    continue;
                }
                
                Map<String, Object> metadata = new HashMap<>();
                metadata.put("source", file.getName());
                metadata.put("category", category);
                metadata.put("chunkIndex", i);
                metadatas.add(metadata);
                
                if (chunk.length() > 50) {
                    log.debug("段落 {}: 长度 {} 字符, 内容预览: {}...", i, chunk.length(), chunk.substring(0, 50));
                } else {
                    log.debug("段落 {}: 长度 {} 字符, 内容: {}", i, chunk.length(), chunk);
                }
            }
            
            // 过滤空段落
            List<String> validChunks = Arrays.asList(chunks).stream()
                    .map(String::trim)
                    .filter(s -> !s.isEmpty())
                    .toList();
            
            log.info("准备添加 {} 个有效段落", validChunks.size());
            addDocuments(validChunks, metadatas);
            log.info("文件处理完成: {}", file.getName());
        } catch (Exception e) {
            log.error("读取文件失败: {}", e.getMessage(), e);
            throw new RuntimeException("读取文件失败: " + e.getMessage(), e);
        }
    }

    /**
     * 删除文档
     */
    public void deleteDocument(String id) {
        vectorStore.delete(List.of(id));
    }

    /**
     * 清空所有文档
     */
    public void clearAllDocuments() {
        // SimpleVectorStore 不支持直接清空,需要重新创建
        throw new UnsupportedOperationException("请使用重启应用的方式清空");
    }

    /**
     * 获取所有文档列表
     */
    public List<Map<String, Object>> getAllDocuments() {
        // SimpleVectorStore 不直接支持查询所有文档
        // 这里返回一个空列表,实际项目中应该记录已添加的文档ID
        return new ArrayList<>();
    }

    /**
     * RAG 核心:基于知识库回答问题
     */
    public String answerWithRAG(String question, int topK, double similarityThreshold) {
        log.info("开始 RAG 问答 - 问题: {}, Top-K: {}, 阈值: {}", question, topK, similarityThreshold);
        
        // 1. 检索相关文档
        List<Document> relevantDocs = searchSimilarDocuments(question, topK, similarityThreshold);
        
        if (relevantDocs.isEmpty()) {
            log.warn("未找到相关文档 - 问题: '{}', 阈值: {}", question, similarityThreshold);
            log.warn("提示: 1.确认知识库中是否有文档  2.尝试降低相似度阈值  3.检查文档内容是否相关");
            return "抱歉,知识库中没有找到与您的问题相关的信息。您可以先点击加载示例数据或添加自己的文档到知识库。";
        }

        log.info("找到 {} 个相关文档", relevantDocs.size());
        for (int i = 0; i < relevantDocs.size(); i++) {
            Document doc = relevantDocs.get(i);
            double score = doc.getScore() * 100;
            String preview = doc.getText().length() > 50 ? doc.getText().substring(0, 50) + "..." : doc.getText();
            log.debug("文档 {}: 相关度 {}%, 内容预览: {}", i + 1, String.format("%.2f", score), preview);
        }

        // 2. 构建上下文
        StringBuilder context = new StringBuilder();
        for (int i = 0; i < relevantDocs.size(); i++) {
            Document doc = relevantDocs.get(i);
            context.append("【参考资料 ").append(i + 1).append("】\n");
            context.append(doc.getText()).append("\n\n");
            
            // 添加元数据信息(如果有)
            if (doc.getMetadata() != null && !doc.getMetadata().isEmpty()) {
                context.append("来源: ").append(doc.getMetadata().getOrDefault("source", "未知")).append("\n");
                context.append("相关度: ").append(String.format("%.2f%%", doc.getScore() * 100)).append("\n\n");
            }
        }

        // 3. 构建 Prompt
        String prompt = buildRagPrompt(question, context.toString());
        log.debug("Prompt 长度: {} 字符", prompt.length());

        try {
            // 4. 调用大模型生成答案
            log.info("正在调用 LLM 生成答案...");
            String answer = chatClient.prompt(prompt)
                    .call()
                    .content();
            log.info("答案生成成功,长度: {} 字符", answer.length());
            return answer;
        } catch (Exception e) {
            log.error("调用 LLM 失败: {}", e.getMessage(), e);
            return "抱歉,生成答案时出现错误:" + e.getMessage() + "\n\n请检查:\n1. Ollama 服务是否正常运行\n2. 模型 qwen2.5 是否已下载\n3. 网络连接是否正常";
        }
    }

    /**
     * 搜索相似文档
     */
    public List<Document> searchSimilarDocuments(String query, int topK, double threshold) {
        return vectorStore.similaritySearch(
                org.springframework.ai.vectorstore.SearchRequest.builder()
                        .query(query)
                        .topK(topK)
                        .similarityThreshold(threshold)
                        .build()
        );
    }

    /**
     * 构建 RAG Prompt
     */
    private String buildRagPrompt(String question, String context) {
        return String.format("""
                你是一个智能问答助手。请基于以下提供的参考资料来回答用户的问题。
                
                要求:
                1. 如果资料中有相关信息,请结合资料内容给出准确、完整的回答
                2. 如果资料中没有相关信息,请明确说明"知识库中没有相关信息"
                3. 回答要简洁明了,条理清晰
                4. 可以适当引用资料中的关键信息
                
                【参考资料】
                %s
                
                【用户问题】
                %s
                
                【你的回答】
                """, context, question);
    }

    /**
     * 获取知识库统计信息
     */
    public Map<String, Object> getKnowledgeBaseStats() {
        Map<String, Object> stats = new HashMap<>();
        stats.put("message", "SimpleVectorStore 暂不支持统计接口");
        stats.put("note", "生产环境建议使用 Milvus、Chroma 等专业向量数据库");
        return stats;
    }
}

6、RagController

package com.badao.ai.controller;

import com.badao.ai.service.DocumentChunkService;
import com.badao.ai.service.RagService;
import org.springframework.ai.document.Document;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.util.*;

@RestController
@RequestMapping("/api/rag")
public class RagController {

    private final RagService ragService;
    private final DocumentChunkService chunkService;

    public RagController(RagService ragService, DocumentChunkService chunkService) {
        this.ragService = ragService;
        this.chunkService = chunkService;
    }

    /**
     * 添加单个文档
     */
    @PostMapping("/document")
    public Map<String, Object> addDocument(@RequestBody Map<String, Object> request) {
        String text = (String) request.get("text");
        String id = (String) request.get("id");
        @SuppressWarnings("unchecked")
        Map<String, Object> metadata = (Map<String, Object>) request.get("metadata");

        if (text == null || text.isEmpty()) {
            return Map.of("success", false, "message", "文本内容不能为空");
        }

        ragService.addDocument(text, id, metadata);

        return Map.of(
                "success", true,
                "message", "文档添加成功",
                "id", id != null ? id : "自动生成"
        );
    }

    /**
     * 批量添加文档
     */
    @PostMapping("/documents/batch")
    public Map<String, Object> addDocuments(@RequestBody Map<String, Object> request) {
        @SuppressWarnings("unchecked")
        List<String> texts = (List<String>) request.get("texts");
        
        @SuppressWarnings("unchecked")
        List<Map<String, Object>> metadatas = (List<Map<String, Object>>) request.get("metadatas");

        if (texts == null || texts.isEmpty()) {
            return Map.of("success", false, "message", "文档列表不能为空");
        }

        ragService.addDocuments(texts, metadatas != null ? metadatas : new ArrayList<>());

        return Map.of(
                "success", true,
                "message", "成功添加 " + texts.size() + " 个文档",
                "count", texts.size()
        );
    }

    /**
     * 上传文本文件并添加到知识库
     */
    @PostMapping("/document/upload")
    public Map<String, Object> uploadDocument(@RequestParam("file") MultipartFile file,
                                              @RequestParam(value = "category", defaultValue = "general") String category) {
        try {
            if (file.isEmpty()) {
                return Map.of("success", false, "message", "文件不能为空");
            }

            // 保存临时文件
            File tempFile = File.createTempFile("upload_", "_" + file.getOriginalFilename());
            file.transferTo(tempFile);

            // 添加到知识库
            ragService.addDocumentFromFile(tempFile, category);

            // 删除临时文件
            tempFile.delete();

            return Map.of(
                    "success", true,
                    "message", "文件上传并处理成功",
                    "filename", file.getOriginalFilename()
            );
        } catch (Exception e) {
            return Map.of("success", false, "message", "处理失败: " + e.getMessage());
        }
    }

    /**
     * 智能分块后添加文档
     */
    @PostMapping("/document/chunked")
    public Map<String, Object> addChunkedDocument(@RequestBody Map<String, Object> request) {
        String text = (String) request.get("text");
        String strategy = (String) request.getOrDefault("strategy", "smart");
        Integer chunkSize = (Integer) request.getOrDefault("chunkSize", 500);
        String category = (String) request.getOrDefault("category", "general");

        if (text == null || text.isEmpty()) {
            return Map.of("success", false, "message", "文本内容不能为空");
        }

        List<String> chunks;
        switch (strategy) {
            case "paragraph":
                chunks = chunkService.chunkByParagraph(text);
                break;
            case "size":
                Integer overlap = (Integer) request.getOrDefault("overlap", 50);
                chunks = chunkService.chunkBySize(text, chunkSize, overlap);
                break;
            default:
                chunks = chunkService.smartChunk(text, chunkSize);
        }

        // 为每个分块添加元数据
        List<Map<String, Object>> metadatas = new ArrayList<>();
        for (int i = 0; i < chunks.size(); i++) {
            Map<String, Object> metadata = new HashMap<>();
            metadata.put("category", category);
            metadata.put("chunkIndex", i);
            metadata.put("totalChunks", chunks.size());
            metadata.put("strategy", strategy);
            metadatas.add(metadata);
        }

        ragService.addDocuments(chunks, metadatas);

        return Map.of(
                "success", true,
                "message", "文档分块添加成功",
                "chunks", chunks.size(),
                "strategy", strategy
        );
    }

    /**
     * RAG 问答(核心接口)
     */
    @PostMapping("/ask")
    public Map<String, Object> askQuestion(@RequestBody Map<String, Object> request) {
        String question = (String) request.get("question");
        Integer topK = (Integer) request.getOrDefault("topK", 5);
        Double threshold = ((Number) request.getOrDefault("threshold", 0.7)).doubleValue();

        if (question == null || question.isEmpty()) {
            return Map.of("success", false, "message", "问题不能为空");
        }

        long startTime = System.currentTimeMillis();
        String answer = ragService.answerWithRAG(question, topK, threshold);
        long endTime = System.currentTimeMillis();

        return Map.of(
                "success", true,
                "question", question,
                "answer", answer,
                "topK", topK,
                "threshold", threshold,
                "responseTime", (endTime - startTime) + "ms"
        );
    }

    /**
     * 搜索相似文档(不包含 LLM 生成)
     */
    @PostMapping("/search")
    public Map<String, Object> searchDocuments(@RequestBody Map<String, Object> request) {
        String query = (String) request.get("query");
        Integer topK = (Integer) request.getOrDefault("topK", 5);
        Double threshold = ((Number) request.getOrDefault("threshold", 0.7)).doubleValue();

        if (query == null || query.isEmpty()) {
            return Map.of("success", false, "message", "查询内容不能为空");
        }

        List<Document> docs = ragService.searchSimilarDocuments(query, topK, threshold);

        List<Map<String, Object>> results = new ArrayList<>();
        for (Document doc : docs) {
            Map<String, Object> result = new HashMap<>();
            result.put("text", doc.getText());
            result.put("score", doc.getScore());
            result.put("metadata", doc.getMetadata());
            results.add(result);
        }

        return Map.of(
                "success", true,
                "query", query,
                "results", results,
                "count", results.size()
        );
    }

    /**
     * 获取知识库统计
     */
    @GetMapping("/stats")
    public Map<String, Object> getStats() {
        return ragService.getKnowledgeBaseStats();
    }

    /**
     * 测试预置知识库
     */
    @PostMapping("/init-sample-data")
    public Map<String, Object> initSampleData() {
        // 添加一些示例文档
        List<String> texts = new ArrayList<>();
        List<Map<String, Object>> metadatas = new ArrayList<>();

        // 人工智能相关知识
        texts.add("人工智能(Artificial Intelligence,简称 AI)是计算机科学的一个分支,它企图了解智能的实质,并生产出一种新的能以人类智能相似的方式做出反应的智能机器。");
        metadatas.add(Map.of("category", "AI基础", "topic", "定义"));

        texts.add("机器学习是人工智能的核心技术之一,它通过算法使计算机能够从数据中学习,而不需要明确的编程指令。机器学习分为监督学习、无监督学习和强化学习三大类。");
        metadatas.add(Map.of("category", "AI基础", "topic", "机器学习"));

        texts.add("深度学习是机器学习的一个子领域,它使用多层神经网络来模拟人脑的工作方式。深度学习在图像识别、自然语言处理等领域取得了突破性进展。");
        metadatas.add(Map.of("category", "AI基础", "topic", "深度学习"));

        texts.add("Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。");
        metadatas.add(Map.of("category", "Java开发", "topic", "Spring Boot"));

        texts.add("微服务架构是一种将单一应用程序开发为一组小型服务的方法,每个服务运行在自己的进程中,服务间采用轻量级通信机制(通常是 HTTP RESTful API)。");
        metadatas.add(Map.of("category", "Java开发", "topic", "微服务"));

        texts.add("Python 是一种广泛使用的解释型、高级和通用的编程语言。Python 支持多种编程范型,包括结构化、面向对象、函数式、反射式等。");
        metadatas.add(Map.of("category", "编程语言", "topic", "Python"));

        ragService.addDocuments(texts, metadatas);

        return Map.of(
                "success", true,
                "message", "已初始化示例知识库",
                "count", texts.size()
        );
    }
}

7、测试html

package com.badao.ai.controller;

import com.badao.ai.service.DocumentChunkService;
import com.badao.ai.service.RagService;
import org.springframework.ai.document.Document;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.util.*;

@RestController
@RequestMapping("/api/rag")
public class RagController {

    private final RagService ragService;
    private final DocumentChunkService chunkService;

    public RagController(RagService ragService, DocumentChunkService chunkService) {
        this.ragService = ragService;
        this.chunkService = chunkService;
    }

    /**
     * 添加单个文档
     */
    @PostMapping("/document")
    public Map<String, Object> addDocument(@RequestBody Map<String, Object> request) {
        String text = (String) request.get("text");
        String id = (String) request.get("id");
        @SuppressWarnings("unchecked")
        Map<String, Object> metadata = (Map<String, Object>) request.get("metadata");

        if (text == null || text.isEmpty()) {
            return Map.of("success", false, "message", "文本内容不能为空");
        }

        ragService.addDocument(text, id, metadata);

        return Map.of(
                "success", true,
                "message", "文档添加成功",
                "id", id != null ? id : "自动生成"
        );
    }

    /**
     * 批量添加文档
     */
    @PostMapping("/documents/batch")
    public Map<String, Object> addDocuments(@RequestBody Map<String, Object> request) {
        @SuppressWarnings("unchecked")
        List<String> texts = (List<String>) request.get("texts");
        
        @SuppressWarnings("unchecked")
        List<Map<String, Object>> metadatas = (List<Map<String, Object>>) request.get("metadatas");

        if (texts == null || texts.isEmpty()) {
            return Map.of("success", false, "message", "文档列表不能为空");
        }

        ragService.addDocuments(texts, metadatas != null ? metadatas : new ArrayList<>());

        return Map.of(
                "success", true,
                "message", "成功添加 " + texts.size() + " 个文档",
                "count", texts.size()
        );
    }

    /**
     * 上传文本文件并添加到知识库
     */
    @PostMapping("/document/upload")
    public Map<String, Object> uploadDocument(@RequestParam("file") MultipartFile file,
                                              @RequestParam(value = "category", defaultValue = "general") String category) {
        try {
            if (file.isEmpty()) {
                return Map.of("success", false, "message", "文件不能为空");
            }

            // 保存临时文件
            File tempFile = File.createTempFile("upload_", "_" + file.getOriginalFilename());
            file.transferTo(tempFile);

            // 添加到知识库
            ragService.addDocumentFromFile(tempFile, category);

            // 删除临时文件
            tempFile.delete();

            return Map.of(
                    "success", true,
                    "message", "文件上传并处理成功",
                    "filename", file.getOriginalFilename()
            );
        } catch (Exception e) {
            return Map.of("success", false, "message", "处理失败: " + e.getMessage());
        }
    }

    /**
     * 智能分块后添加文档
     */
    @PostMapping("/document/chunked")
    public Map<String, Object> addChunkedDocument(@RequestBody Map<String, Object> request) {
        String text = (String) request.get("text");
        String strategy = (String) request.getOrDefault("strategy", "smart");
        Integer chunkSize = (Integer) request.getOrDefault("chunkSize", 500);
        String category = (String) request.getOrDefault("category", "general");

        if (text == null || text.isEmpty()) {
            return Map.of("success", false, "message", "文本内容不能为空");
        }

        List<String> chunks;
        switch (strategy) {
            case "paragraph":
                chunks = chunkService.chunkByParagraph(text);
                break;
            case "size":
                Integer overlap = (Integer) request.getOrDefault("overlap", 50);
                chunks = chunkService.chunkBySize(text, chunkSize, overlap);
                break;
            default:
                chunks = chunkService.smartChunk(text, chunkSize);
        }

        // 为每个分块添加元数据
        List<Map<String, Object>> metadatas = new ArrayList<>();
        for (int i = 0; i < chunks.size(); i++) {
            Map<String, Object> metadata = new HashMap<>();
            metadata.put("category", category);
            metadata.put("chunkIndex", i);
            metadata.put("totalChunks", chunks.size());
            metadata.put("strategy", strategy);
            metadatas.add(metadata);
        }

        ragService.addDocuments(chunks, metadatas);

        return Map.of(
                "success", true,
                "message", "文档分块添加成功",
                "chunks", chunks.size(),
                "strategy", strategy
        );
    }

    /**
     * RAG 问答(核心接口)
     */
    @PostMapping("/ask")
    public Map<String, Object> askQuestion(@RequestBody Map<String, Object> request) {
        String question = (String) request.get("question");
        Integer topK = (Integer) request.getOrDefault("topK", 5);
        Double threshold = ((Number) request.getOrDefault("threshold", 0.7)).doubleValue();

        if (question == null || question.isEmpty()) {
            return Map.of("success", false, "message", "问题不能为空");
        }

        long startTime = System.currentTimeMillis();
        String answer = ragService.answerWithRAG(question, topK, threshold);
        long endTime = System.currentTimeMillis();

        return Map.of(
                "success", true,
                "question", question,
                "answer", answer,
                "topK", topK,
                "threshold", threshold,
                "responseTime", (endTime - startTime) + "ms"
        );
    }

    /**
     * 搜索相似文档(不包含 LLM 生成)
     */
    @PostMapping("/search")
    public Map<String, Object> searchDocuments(@RequestBody Map<String, Object> request) {
        String query = (String) request.get("query");
        Integer topK = (Integer) request.getOrDefault("topK", 5);
        Double threshold = ((Number) request.getOrDefault("threshold", 0.7)).doubleValue();

        if (query == null || query.isEmpty()) {
            return Map.of("success", false, "message", "查询内容不能为空");
        }

        List<Document> docs = ragService.searchSimilarDocuments(query, topK, threshold);

        List<Map<String, Object>> results = new ArrayList<>();
        for (Document doc : docs) {
            Map<String, Object> result = new HashMap<>();
            result.put("text", doc.getText());
            result.put("score", doc.getScore());
            result.put("metadata", doc.getMetadata());
            results.add(result);
        }

        return Map.of(
                "success", true,
                "query", query,
                "results", results,
                "count", results.size()
        );
    }

    /**
     * 获取知识库统计
     */
    @GetMapping("/stats")
    public Map<String, Object> getStats() {
        return ragService.getKnowledgeBaseStats();
    }

    /**
     * 测试预置知识库
     */
    @PostMapping("/init-sample-data")
    public Map<String, Object> initSampleData() {
        // 添加一些示例文档
        List<String> texts = new ArrayList<>();
        List<Map<String, Object>> metadatas = new ArrayList<>();

        // 人工智能相关知识
        texts.add("人工智能(Artificial Intelligence,简称 AI)是计算机科学的一个分支,它企图了解智能的实质,并生产出一种新的能以人类智能相似的方式做出反应的智能机器。");
        metadatas.add(Map.of("category", "AI基础", "topic", "定义"));

        texts.add("机器学习是人工智能的核心技术之一,它通过算法使计算机能够从数据中学习,而不需要明确的编程指令。机器学习分为监督学习、无监督学习和强化学习三大类。");
        metadatas.add(Map.of("category", "AI基础", "topic", "机器学习"));

        texts.add("深度学习是机器学习的一个子领域,它使用多层神经网络来模拟人脑的工作方式。深度学习在图像识别、自然语言处理等领域取得了突破性进展。");
        metadatas.add(Map.of("category", "AI基础", "topic", "深度学习"));

        texts.add("Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。");
        metadatas.add(Map.of("category", "Java开发", "topic", "Spring Boot"));

        texts.add("微服务架构是一种将单一应用程序开发为一组小型服务的方法,每个服务运行在自己的进程中,服务间采用轻量级通信机制(通常是 HTTP RESTful API)。");
        metadatas.add(Map.of("category", "Java开发", "topic", "微服务"));

        texts.add("Python 是一种广泛使用的解释型、高级和通用的编程语言。Python 支持多种编程范型,包括结构化、面向对象、函数式、反射式等。");
        metadatas.add(Map.of("category", "编程语言", "topic", "Python"));

        ragService.addDocuments(texts, metadatas);

        return Map.of(
                "success", true,
                "message", "已初始化示例知识库",
                "count", texts.size()
        );
    }
}

8、测试知识库文本文件

Badao科技内部知识库

## 公司政策
- 员工年假:入职满1年享受10天带薪年假,满3年增加到15天。
- 远程办公:每周三、周五为固定远程办公日。
- 报销流程:所有报销需在“易报销”系统提交,金额超过500元需部门经理审批。

## 内部项目
- 项目代号“天枢”:基于Spring AI与Ollama构建的企业智能助手,负责人为张三。
- 项目代号“瑶光”:下一代实时数据湖方案,采用Apache Iceberg + Flink,负责人为李四。
- 内部编码规范:所有Java项目必须使用Lombok,禁止使用var关键字,SQL必须使用参数化查询。

## 技术选型
- 消息中间件:统一使用RocketMQ,禁止引入RabbitMQ。
- 向量数据库:生产环境采用PgVector,开发环境可使用SimpleVectorStore。
- 前端框架:统一使用React 18 + Ant Design 5.x。

## 团队联系人
- 基础架构组:赵六(zhaoliu@badao.com)
- AI组:王五(wangwu@badao.com)
- 数据组:钱七(qianqi@badao.com)

九、测试效果

测试默认文本:

上传上述知识文档并测试:
请添加图片描述

请添加图片描述

更多推荐