跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
JavaAIjava

知识库问答机器人:基于 SpringAI+RAG 的实现

综述由AI生成基于 Spring AI 和 RAG 技术构建知识库问答机器人的完整流程。内容涵盖 RAG 原理、项目结构搭建、自定义向量存储实现(TextBasedVectorStore)、文档分块与量化(使用 HanLP)、以及问答服务与控制器开发。通过集成智谱大模型,实现了文档上传、检索增强生成及流式响应功能,适合快速原型开发与学习。

竹影清风发布于 2026/4/6更新于 2026/5/2533 浏览
知识库问答机器人:基于 SpringAI+RAG 的实现

一、引言

随着大语言模型的快速发展,RAG(Retrieval-Augmented Generation)技术已成为构建知识库问答系统的核心技术之一。本文将带领大家从零开始,使用 Spring AI 框架构建一个支持文档上传的知识库问答机器人,帮助大家深入理解 RAG 技术的核心原理和实践应用。

1.1 什么是 RAG? RAG(检索增强生成)是一种结合了信息检索和文本生成的技术。它的基本工作流程是:

  • 用户提出问题
  • 系统从知识库中检索相关信息
  • 大语言模型基于检索到的信息生成答案

从系统设计角度触发,RAG 的核心作用可以被描述为:在 LLM 调用生成响应之前,由系统动态构造一个'最小且相关的知识上下文'。

请注意两个关键词:

  • 动态:每次问题都不同,检索的知识也不同(比如用户问 A 产品时找 A 的文档,问 B 产品时找 B 的文档)
  • 最小:只注入必要信息(比如用户问'A 产品的定价',就只塞定价相关的片段,而非整份产品手册)

RAG 可以有效的弥补上下文窗口的先天不足:不再需要把所有知识塞进窗口,而是只在需要时'临时调取'相关部分,既避免了窗口溢出,又减少了注意力竞争。

1.2 RAG 在交互链路中的位置 接下来我们以 RAG 的经典应用场景——企业知识库为例,来看一下 RAG 在这个流程中所处的位置。

在这个结构中,RAG 主要就是在用户提问与向 LLM 发起请求这个中间段,用于检索关联的文档构建上下文。

1.3 RAG 工作原理 我们以一张图来介绍 RAG 的工作原理,具体的 RAG 详细介绍,请参照官方文档。

二、核心实现

2.1 项目结构概览

D05-rag-qa-bot/
├── src/main/java/com/git/hui/springai/app/
│   ├── D05Application.java # 启动类
│   ├── mvc/
│   │   ├── QaApiController.java # API 控制器
│   │   └── QaController.java # 页面控制器
│   ├── qa/QaBoltService.java # 问答服务
│   └── vectorstore/
│       ├── DocumentChunker.java # 文档分块工具
│       ├── DocumentQuantizer.java # 文档量化器
│       └── TextBasedVectorStore.java # 文本向量存储
├── src/main/resources/
│   ├── application.yml # 配置文件
│   ├── prompts/qa-prompts.pt # 提示词模板
│   └── templates/chat.html # 前端页面
└── pom.xml # 依赖配置

2.2 项目初始化

2.2.1 Maven 依赖配置 首先,我们需要在 pom.xml 中配置必要的依赖: 其中关于向量数据库、tika 的文档解析属于核心依赖项。 hanlp 适用于无法直接使用 EmbeddingModel 的场景,在我们的示例中,会实现一个基础的文档向量化方案,其中会采用 HanLP 来做中文分词。 使用智谱的免费大模型来体验我们的 RAG 知识库问答(当然也可以基于 OpenAI-Starter 来切换其他的大模型,使用层面并没有改变,只需要替换依赖、api 配置即可)。

<dependencies>
    <!-- 向量数据库 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-advisors-vector-store</artifactId>
    </dependency>
    <!-- 文档提取,使用 apache-tika 来实现 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-tika-document-reader</artifactId>
    </dependency>
    <!-- pdf 文档提取,实际也可以用上面的 tika 来实现 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-pdf-document-reader</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-rag</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 使用智谱大模型 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-zhipuai</artifactId>
    </dependency>
    <!-- 用于前端页面的支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <!-- 中文分词,用在文档向量化 -->
    <dependency>
        <groupId>com.hankcs</groupId>
        <artifactId>hanlp</artifactId>
        <version>portable-1.8.4</version>
    </dependency>
</dependencies>

这里我们引入了 Spring AI 的核心依赖,以及用于文档处理的 Tika 和 PDF 读取器,还特别加入了 HanLP 中文分词库来优化中文处理效果。

2.2.2 应用配置 在 application.yml 中配置 API 密钥和相关参数:

spring:
  ai:
    zhipuai:
      api-key: ${zhipuai-api-key}
      chat:
        options:
          model: GLM-4-Flash
          temperature: 0.1
  thymeleaf:
    cache: false
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 50MB
logging:
  level:
    org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor: debug
    org.springframework.ai.chat.client: DEBUG
server:
  port: 8080

2.3 自定义向量存储实现 通常 RAG 会使用一些成熟的向量数据库(如 Pinecone、weaviate、qdrant、milvus 或者 es、redis 等),但是考虑到安装、环境配置等成本,我们接下来会实现一个基础的自定义的文本向量库 TextBasedVectorStore,基于内存实现,无需额外的外部依赖,单纯的用来体验 RAG 并没有太大问题。

SpringAI 原生提供了一个基于内存的向量数据库 SimpleVectorStore,在它的实现中,向量数据写入,依赖向量模型,因此如果有额度使用大模型厂家提供的 EmbeddingModel 时,直接用它进行测试即可;

当然如果你现在并没有渠道使用向量模型的,那也没关系,接下来我们将参照 SpringAI 的 SimpleVectorStore 实现的一个自定义的向量库 TextBasedVectorStore,提供一套不依赖向量模型的解决方案,特别适合快速原型开发,核心实现如下(当然你也完全可以忽略它,它不是我们的重点)。

2.3.1 TextBasedVectorStore - 文本匹配向量存储 在下面的实现中,重点体现了两个方法:doAdd(将文档保存到向量数据库中:文档分片 -> 向量化 -> 存储)、doSimilaritySearch(基于相似度的搜索)。需要注意一点,文档的向量化与搜索时传入文本的向量化,需要采用同一套向量化方案。

public class TextBasedVectorStore extends AbstractObservationVectorStore {
    @Getter
    protected Map<String, SimpleVectorStoreContent> store = new ConcurrentHashMap<>();
    /**
     * 已经存储到向量库的 document,用于幂等
     */
    private Set<String> persistMd5 = new CopyOnWriteArraySet<>();

    /**
     * 添加文档到向量数据库
     *
     * @param documents
     */
    @Override
    public void doAdd(List<Document> documents) {
        if (CollectionUtils.isEmpty(documents)) {
            return;
        }
        // 创建一个新的可变列表副本
        List<Document> mutableDocuments = new ArrayList<>();
        for (Document document : documents) {
            // 过滤掉重复的文档,避免二次写入,浪费空间
            if (!persistMd5.contains((String) document.getMetadata().get("md5"))) {
                mutableDocuments.add(document);
            }
        }
        if (CollectionUtils.isEmpty(mutableDocuments)) {
            return;
        }
        // 文档分片
        List<Document> chunkers = DocumentChunker.DEFAULT_CHUNKER.chunkDocuments(mutableDocuments);
        // 存储本地向量库
        chunkers.forEach(document -> {
            float[] embedding = DocumentQuantizer.quantizeDocument(document);
            if (embedding.length == 0) {
                return;
            }
            SimpleVectorStoreContent storeContent = new SimpleVectorStoreContent(
                    document.getId(), document.getText(), document.getMetadata(), embedding
            );
            this.store.put(document.getId(), storeContent);
        });
        mutableDocuments.forEach(document -> persistMd5.add((String) document.getMetadata().get("md5")));
    }

    /**
     * 搜索向量数据库,根据相似度返回相关文档
     *
     * @param request
     * @return
     */
    @Override
    public List<Document> doSimilaritySearch(SearchRequest request) {
        Predicate<SimpleVectorStoreContent> documentFilterPredicate = this.doFilterPredicate(request);
        final float[] userQueryEmbedding = this.getUserQueryEmbedding(request.getQuery());
        return this.store.values().stream()
                .filter(documentFilterPredicate)
                .map((content) -> content.toDocument(DocumentQuantizer.calculateCosineSimilarity(userQueryEmbedding, content.getEmbedding())))
                .filter((document) -> document.getScore() >= request.getSimilarityThreshold())
                .sorted(Comparator.comparing(Document::getScore).reversed())
                .limit((long) request.getTopK())
                .toList();
    }

    private float[] getUserQueryEmbedding(String query) {
        return DocumentQuantizer.quantizeQuery(query);
    }
}

2.3.2 DocumentChunker - 文档分块器 合理地将长文档分块是 RAG 系统的关键环节,合理的分块大小,可以有效的增加检索效率、提高准确率、减少上下文长度。

在真实的 RAG 应用中,这一块具体的方案挺多的,比如固定尺寸(下面的方案)、地柜拆分、语义拆分、结构化拆分(如结构化的 markdown 文档就很适合)、延迟拆分、自适应拆分、层级拆分、LLM 驱动拆分、智能体拆分等。

public class DocumentChunker {
    private final int maxChunkSize;
    private final int overlapSize;

    public DocumentChunker() {
        this(500, 50); // 默认值:最大块大小 500 个字符,重叠 50 个字符
    }

    public List<Document> chunkDocument(Document document) {
        String content = document.getText();
        if (content == null || content.trim().isEmpty()) {
            return List.of(document);
        }
        List<String> chunks = splitText(content);
        List<Document> chunkedDocuments = new ArrayList<>();
        for (int i = 0; i < chunks.size(); i++) {
            String chunk = chunks.get(i);
            String chunkId = document.getId() + "_chunk_" + i;
            Document chunkDoc = new Document(chunkId, chunk, new HashMap<>(document.getMetadata()));
            chunkDoc.getMetadata().put("chunk_index", i);
            chunkDoc.getMetadata().put("total_chunks", chunks.size());
            chunkDoc.getMetadata().put("original_document_id", document.getId());
            chunkedDocuments.add(chunkDoc);
        }
        return chunkedDocuments;
    }

    private List<String> splitText(String text) {
        List<String> chunks = new ArrayList<>();
        // 按多种分隔符分割,优先在语义边界处分割
        String[] sentences = text.split("(?<=。)|(?<=!)|(?<=!)|(?<=?)|(?<=\\?)|(?<=\\n\\n)");
        StringBuilder currentChunk = new StringBuilder();
        for (String sentence : sentences) {
            if (sentence.trim().isEmpty()) {
                continue; // 跳过空句子
            }
            if (currentChunk.length() + sentence.length() <= maxChunkSize) {
                // 如果当前块加上新句子不超过最大大小,就添加到当前块
                if (currentChunk.length() > 0) {
                    currentChunk.append(sentence);
                } else {
                    currentChunk.append(sentence);
                }
            } else {
                // 如果当前块为空,但是单个句子太长,需要强制分割
                if (currentChunk.length() == 0) {
                    List<String> subChunks = forceSplit(sentence, maxChunkSize);
                    for (int i = 0; i < subChunks.size(); i++) {
                        String subChunk = subChunks.get(i);
                        if (i < subChunks.size() - 1) {
                            chunks.add(subChunk);
                        } else {
                            currentChunk.append(subChunk);
                        }
                    }
                } else {
                    chunks.add(currentChunk.toString());
                    currentChunk = new StringBuilder();
                    // 添加重叠部分,如果句子长度大于重叠大小,则只取末尾部分
                    if (sentence.length() > overlapSize) {
                        String overlap = sentence.substring(Math.max(0, sentence.length() - overlapSize));
                        currentChunk.append(overlap);
                        currentChunk.append(sentence);
                    } else {
                        currentChunk.append(sentence);
                    }
                }
            }
        }
        if (currentChunk.length() > 0) {
            chunks.add(currentChunk.toString());
        }
        return chunks;
    }
}

2.3.3 DocumentQuantizer - 文档量化器 使用 HanLP 进行中文分词,实现了一个简单的文档向量化工具类(同样的你也完全可以忽略它的具体实现,因为它的效果显然比使用 EmbedingModel 要差很多很多,但用于学习体验 RAG 也基本够用)。

public class DocumentQuantizer {
    private static final Segment SEGMENT = HanLP.newSegment();

    public static float[] quantizeText(String text) {
        if (text == null || text.trim().isEmpty()) {
            return new float[0];
        }
        String[] words = preprocessText(text);
        Map<String, Integer> wordFreq = countWordFrequency(words);
        // 生成固定长度的向量表示(这里使用前 128 个高频词)
        return generateFixedLengthVector(wordFreq, 128);
    }

    /**
     * 将文本转换为数值向量表示(简化版)
     * 使用 TF-IDF 的基本思想,但简化为词频统计
     *
     * @param text 输入文本
     * @return 数值向量
     */
    private static String[] preprocessText(String text) {
        List<Term> termList = SEGMENT.seg(text);
        return termList.stream()
                .filter(term -> !isStopWord(term.word)) // 过滤停用词
                .filter(term -> !term.nature.toString().startsWith("w")) // 过滤标点符号
                .map(term -> term.word.toLowerCase()) // 转换为小写
                .toArray(String[]::new);
    }

    /**
     * 生成固定长度的向量表示
     *
     * @param wordFreq 词频映射
     * @param length 向量长度
     * @return 固定长度的向量
     */
    private static float[] generateFixedLengthVector(Map<String, Integer> wordFreq, int length) {
        float[] vector = new float[length];
        // 获取频率最高的词汇
        List<Map.Entry<String, Integer>> sortedEntries = wordFreq.entrySet().stream()
                .sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
                .limit(length)
                .collect(Collectors.toList());
        // 将词频填入向量
        for (int i = 0; i < Math.min(sortedEntries.size(), length); i++) {
            vector[i] = sortedEntries.get(i).getValue();
        }
        return vector;
    }

    public static double calculateCosineSimilarity(float[] vectorA, float[] vectorB) {
        if (vectorA == null || vectorB == null || vectorA.length == 0 || vectorB.length == 0) {
            return 0.0;
        }
        int minLength = Math.min(vectorA.length, vectorB.length);
        float[] adjustedA = Arrays.copyOf(vectorA, minLength);
        float[] adjustedB = Arrays.copyOf(vectorB, minLength);
        double dotProduct = 0.0;
        double normA = 0.0;
        double normB = 0.0;
        for (int i = 0; i < minLength; i++) {
            dotProduct += adjustedA[i] * adjustedB[i];
            normA += Math.pow(adjustedA[i], 2);
            normB += Math.pow(adjustedB[i], 2);
        }
        normA = Math.sqrt(normA);
        normB = Math.sqrt(normB);
        if (normA == 0 || normB == 0) {
            return 0.0;
        }
        return dotProduct / (normA * normB);
    }
}

2.3.4 注册向量库 接下来就是注册使用这个向量库,在配置类 or 启动类中,添加下面这个声明即可。

@Bean
public VectorStore vectorStore() {
    return TextBasedVectorStore.builder().build();
}

2.4 SpringAI 向量存储 上面 2.3 适用于无法直接使用大模型厂家的向量模型的场景,如果可以直接使用,那么上面的全部可以直接忽略掉,直接使用下面的方式进行声明向量库即可。

@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
    return SimpleVectorStore.builder(embeddingModel).build();
}

2.5 问答服务实现 接下来我们进入核心的基于 RAG 的 QA 问答机器人的实现。

2.5.1 QaBoltService - 核心问答服务

Pre. 问答服务流程 我们先从时序的角度来看一下这个问答服务的核心交互流程。

在这个时序过程中,为了简化大家的理解,我们将文档的向量化存储与问答进行了拆分。

第一步:文档向量化 这一部分包含 RAG 应用数据准备阶段的完整过程:数据提取、文本分割、向量化。

第二步:问答 应用层响应用户提问、从向量数据库检索相似度高的文档信息、注入提示词、访问大模型,获取答案。

Impl. 核心实现 接下来我们看一下具体的实现(上面的步骤分割得很清楚,但是实际使用时,用户可以在问答中上传附件,这个附件也会作为我们知识库的一部分,因此具体的实现中,你会发现这两部耦合在一起了,请不要惊讶)。

step1: 初始化 ChatClient 在开始之前,我们首先参照 SpringAI 的官方教程,通过 Advisor 来初始化支持 RAG 的 ChatClient。 官方文档:https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html

@Service
public class QaBoltService {
    private final ChatClient chatClient;
    private final ChatMemory chatMemory;
    private final VectorStore vectorStore;

    @Value("classpath:/prompts/qa-prompts.pt")
    private Resource boltPrompts;

    public QaBoltService(ChatClient.Builder builder, VectorStore vectorStore, ChatMemory chatMemory) {
        this.vectorStore = vectorStore;
        this.chatMemory = chatMemory;
        this.chatClient = builder.defaultAdvisors(
                new SimpleLoggerAdvisor(ModelOptionsUtils::toJsonStringPrettyPrinter, ModelOptionsUtils::toJsonStringPrettyPrinter, 0), // 用于支持多轮对话
                MessageChatMemoryAdvisor.builder(chatMemory).build(),
                // 用于支持 RAG
                RetrievalAugmentationAdvisor.builder()
                        .queryTransformers(
                                // 使用大型语言模型重写用户查询,以便在查询目标系统时提供更好的结果。
                                RewriteQueryTransformer.builder().chatClientBuilder(builder.build().mutate()).build()
                        )
                        .queryAugmenter(
                                // ContextualQueryAugmenter 使用来自所提供文档内容的上下文数据来增强用户查询。
                                // 默认不支持上下文为空的场景,出现之后大模型会不返回用户查询;这里调整为支持为空
                                ContextualQueryAugmenter.builder().allowEmptyContext(true).build()
                        )
                        .documentRetriever(VectorStoreDocumentRetriever.builder()
                                .similarityThreshold(0.50)
                                .vectorStore(vectorStore)
                                .build())
                        .build()
        ).build();
    }
}

接下来就是响应问答的实现,这里分两步。

step2: 文档处理 处理用户上传的附件,即上面时序图中的第一步,解析文档、切分、向量化、保存到向量库; 下面的实现中主要体现的是基于 SpringAI 封装的 tika 与 pdf 文档解析 starter,来提取上传的文档,生成供向量数据库使用的 List; 而具体的文档切分、向量化等则是在上面的 TextBasedVectorStore 实现。 注:为了一个文档,重复进行数据处理,我们在元数据中维护了文档的 md5,这样当添加到向量库中时,就可以基于这个 md5 来进行去重了。

private ProceedInfo processFiles(String chatId, Collection<MultipartFile> files) {
    StringBuilder context = new StringBuilder("\n\n");
    List<Media> mediaList = new ArrayList<>();
    files.forEach(file -> {
        try {
            var data = new ByteArrayResource(file.getBytes());
            var md5 = calculateHash(chatId, file.getBytes());
            MimeType mime = MimeType.valueOf(file.getContentType());
            if (mime.equalsTypeAndSubtype(MediaType.APPLICATION_PDF)) {
                PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(data, PdfDocumentReaderConfig.builder()
                        .withPageTopMargin(0)
                        .withPageExtractedTextFormatter(ExtractedTextFormatter.builder().withNumberOfTopTextLinesToDelete(0).build())
                        .withPagesPerDocument(1)
                        .build());
                List<Document> documents = pdfReader.read();
                documents.forEach(document -> {
                    document.getMetadata().put("md5", md5);
                    if (document.getMetadata().containsKey("file_name") && document.getMetadata().get("file_name") == null) {
                        document.getMetadata().put("file_name", file.getName());
                    }
                });
                vectorStore.add(documents);
                var content = String.join("\n", documents.stream().map(Document::getText).toList());
                context.append(String.format(ATTACHMENT_TEMPLATE, file.getName(), content));
            } else if ("text".equalsIgnoreCase(mime.getType())) {
                List<Document> documents = new TikaDocumentReader(data).read();
                documents.forEach(document -> document.getMetadata().put("md5", md5));
                vectorStore.add(documents);
                var content = String.join("\n", documents.stream().map(Document::getText).toList());
                context.append(String.format(ATTACHMENT_TEMPLATE, file.getName(), content));
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    });
    return new ProceedInfo(context.toString(), mediaList);
}

step3: 问答实现 然后就是具体的问答实现,这里主要是借助 QuestionAnswerAdvisor 来封装 RAG 相关的信息。 说明:在下面的实现中,使用了自定义的提示词模板,当然也可以直接使用 SpringAI 默认的方案。

public Flux<String> ask(String chatId, String question, Collection<MultipartFile> files) {
    processFiles(chatId, files);
    // 自定义的提示词模板,替换默认的检索参考资料的提示词模板
    // 其中 <query> 对应的是用户的提问 question
    // <question_answer_context> 对应的是增强检索的 document,即检索到的参考资料
    PromptTemplate customPromptTemplate = PromptTemplate.builder()
            .renderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build())
            .template("""
                <query>
                 Context information is below.
                 ---------------------
                 <question_answer_context>
                 ---------------------
                 Given the context information and no prior knowledge, answer the query. Follow these rules:
                 1. If the answer is not in the context, just say that you don't know.
                 2. Avoid statements like "Based on the context..." or "The provided information...".
                """).build();
    var qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
            .searchRequest(SearchRequest.builder().similarityThreshold(0.5d).topK(3).build())
            .promptTemplate(customPromptTemplate)
            .build();
    var requestSpec = chatClient.prompt().system(boltPrompts).user(question)
            .advisors(qaAdvisor)
            .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId));
    return requestSpec.stream().content().map(s -> s.replaceAll("\n", "<br/>"));
}

到这里,一个基于 RAG 的问答机器人的核心逻辑,已经全部完成,接下来我们进入体验阶段。

2.5.2 控制器实现

// QaApiController - API 控制器
@RestController
@RequestMapping("/api")
public class QaApiController {
    @Autowired
    private QaBoltService qaBolt;

    @GetMapping(path = "/chat/{chatId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> qaGet(@PathVariable("chatId") String chatId, @RequestParam("question") String question) {
        return qaBolt.ask(chatId, question, Collections.emptyList());
    }

    @PostMapping(path = "/chat/{chatId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> qaPost(@PathVariable("chatId") String chatId, @RequestParam("question") String question,
                               @RequestParam(value = "files", required = false) Collection<MultipartFile> files) {
        if (files == null) {
            files = Collections.emptyList();
        }
        return qaBolt.ask(chatId, question, files);
    }
}

三、体验与小结

3.1 启动类

@SpringBootApplication
public class D05Application {
    @Bean
    public VectorStore vectorStore() {
        return TextBasedVectorStore.builder().build();
    }

    public static void main(String[] args) {
        SpringApplication.run(D05Application.class, args);
        System.out.println("启动成功,前端测试访问地址:http://localhost:8080/chat");
    }
}

3.2 问答提示词 在 resources/prompts/qa-prompts.pt 中维护我们的 qa 机器人的系统提示词。

## 角色设定
你是一个智能问答助手,专门负责根据用户提供的文档内容进行准确的回答和信息提取。

## 核心任务
- 仔细阅读并理解用户上传的文档内容
- 基于文档中的信息回答用户的问题
- 提供准确、相关且基于文档的答案
- 当问题超出文档范围时,明确告知用户该信息未在文档中提及

## 工作流程
1. 首先分析用户上传的文档,提取关键信息
2. 理解用户提出的问题
3. 在文档中查找与问题相关的信息
4. 整合相关信息并形成结构化答案
5. 如无法从文档中找到相关信息,则说明情况

## 回答规范
- 严格基于文档内容作答,不得编造信息
- 引用文档中的具体信息时,请保持原文准确性
- 如果问题涉及多个知识点,在答案中清晰分点说明
- 对于不确定的内容,应诚实表达不确定性,而非猜测
- 保持回答简洁明了,同时确保信息完整

## 注意事项
- 不得脱离文档内容进行回答
- 遇到模糊或不明确的问题时,可以请求用户提供更详细的信息
- 如果文档中没有相关内容,必须明确告知用户
- 保持专业、礼貌的沟通态度

3.3 运行与测试

  • 启动应用:运行 D05Application 主类
  • 访问页面:打开 http://localhost:8080/chat
  • 上传文档:选择 PDF、Word 或文本文件
  • 提问测试:在输入框中输入关于文档的问题

当然在启动时,可以在启动参数中指定大模型的 ApiKey,也可以直接修改 applicatino.yml,直接维护上 apiKey 也可以哦。

3.4 核心技术要点小结

  1. RAG 工作流程
    • 检索阶段:当用户提问时,系统首先将问题转换为向量,然后在文档向量库中查找相似的文档片段
    • 生成阶段:将检索到的相关文档内容与用户问题一起输入大语言模型,生成最终答案
  2. 文档处理优化
    • 中文分词:使用 HanLP 进行精确的中文分词,提高语义理解准确性
    • 文档分块:将长文档合理分块,保持语义完整性的同时便于检索
    • 去重机制:通过 MD5 哈希避免重复上传相同的文档
  3. 性能优化
    • 相似度计算:使用余弦相似度算法计算文本相似度
    • 缓存机制:对已处理的文档进行缓存,避免重复处理
    • 流式响应:使用 SSE 实现答案的流式返回,提升用户体验

目录

  1. 一、引言
  2. 二、核心实现
  3. 三、体验与小结
  4. 角色设定
  5. 核心任务
  6. 工作流程
  7. 回答规范
  8. 注意事项
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • C++调用PaddleOCR进行图像推理
  • Linux 环境下配置 SSH 免密登录与互信详解
  • Docker 镜像基础与操作详解
  • 非原生应用技术全解析:Web、混合与跨平台
  • Python 实现 MCP 客户端调用高德地图天气查询示例
  • 从零开始搭建 Trae 的 Java 开发环境
  • 机器人系统十年演进:从专用机械到通用具身智能体
  • GTC 2026 深度解析:Blackwell 架构升级与 AI Agent 落地实战
  • GitHub 镜像加速:Gitee 与 Coding 开源项目克隆指南
  • 分布式环境下高可靠性分布式锁实现
  • OpenClaw 小白入门:定位、部署与核心场景
  • 自然语言处理在医疗领域的应用与实战
  • FPGA PCIe XDMA 无法 Link Up 使用 LTSSM 定位问题
  • 纪念钞预约实战指南:网络优化与流程准备
  • Ubuntu 20.04 下 C++ 与 LibTorch 深度学习部署实战
  • 石头扫地机器人接入 HomeAssistant 的本地集成方案
  • ChatGPT 实用技巧:文本与数据的结构化方法解析
  • OpenClaw 保姆级超详细教程:小白也能轻松上手的 AI 智能体
  • 拆解 DDSM400 钕强磁外转子 65mm 伺服轮毂电机
  • M2FP 多人人体解析:WebUI 交互与 5 款互补开源模型

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online

  • Mermaid 预览与可视化编辑

    基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online