news 2026/5/13 13:44:09

分享一套锋哥原创的基于Spring AI 2.0的RAG企业内部知识库问答系统(AI大模型 SpringBoot4+Vue3+Ollama)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
分享一套锋哥原创的基于Spring AI 2.0的RAG企业内部知识库问答系统(AI大模型 SpringBoot4+Vue3+Ollama)

大家好,我是Java1234_小锋老师,分享一套锋哥原创的基于Spring AI 2.0的RAG企业内部知识库问答系统(AI大模型 SpringBoot4+Vue3+Ollama)。

项目介绍

随着人工智能技术的飞速发展,大语言模型(LLM)已经成为企业数字化转型的重要驱动力。然而,通用大模型由于训练语料的截止时间和领域局限性,在直接回答企业内部专业问题时常常出现"幻觉"现象,难以准确利用企业沉淀的内部知识。检索增强生成(Retrieval-Augmented Generation,RAG)技术通过将外部知识库与大语言模型相结合,能够有效缓解上述问题,提升回答的准确性、时效性和可解释性,因而成为企业落地大模型应用的主流方案。

本文围绕"基于Spring AI 2.0的RAG企业内部知识库问答系统"的设计与实现展开研究。系统采用前后端分离架构,后端基于Spring Boot 3.x与Spring AI 2.0框架,前端采用Vue 3 + TypeScript + Element Plus构建,数据层采用MySQL存储业务数据,PGVector存储文档向量,MinIO存储原始文件,Redis用于缓存登录态及热点数据。系统实现了用户管理、知识库管理、文档上传与向量化、智能问答(RAG检索+流式回答+引用展示)、对话历史管理、系统管理等核心功能。

系统设计上,本文使用Spring AI 2.0提供的ChatClient、EmbeddingModel、VectorStore等抽象,对文档解析、文本切分、向量化、相似度检索、Prompt组装、流式生成等RAG核心流程进行了完整实现,并结合企业实际需求设计了权限控制、引用溯源、模型可插拔等机制。经过功能测试与性能测试,系统运行稳定,问答准确率较通用大模型直接问答有显著提升,能够满足中小企业内部知识检索与问答的实际需求。

源码下载

链接:https://pan.baidu.com/s/1347t_Ys9deQ72vhVVK4HEg?pwd=1234
提取码:1234

系统展示

核心代码

package com.java1234.service.impl; import com.fasterxml.jackson.databind.ObjectMapper; import com.java1234.dto.ChatAskRequest; import com.java1234.dto.ChatAskResult; import com.java1234.entity.ChatMessage; import com.java1234.entity.ChatSession; import com.java1234.exception.BusinessException; import com.java1234.mapper.ChatMessageMapper; import com.java1234.mapper.ChatSessionMapper; import com.java1234.service.ChatService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.document.Document; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.filter.Filter; import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; /** * {@link com.java1234.service.ChatService} 实现。 */ @Service @RequiredArgsConstructor @Slf4j public class ChatServiceImpl implements ChatService { private static final int RAG_TOP_K = 10; /** * 系统提示:要求仅依据上下文、Markdown 输出。 */ public static final String SYSTEM_PROMPT = """ 你是「Java1234 RAG 企业知识库」的智能助手。请严格根据检索到的上下文回答问题。 若上下文不足以回答,请明确说明「知识库中未找到相关信息」,不要编造。 回答请使用清晰的 Markdown(可适当使用标题、列表)。结尾可简要列出依据的文档标题。 """; private final ChatClient chatClient; private final VectorStore vectorStore; private final ChatSessionMapper chatSessionMapper; private final ChatMessageMapper chatMessageMapper; private final ObjectMapper objectMapper; /** * {@inheritDoc} */ @Override @Transactional(rollbackFor = Exception.class) public ChatAskResult ask(Long userId, ChatAskRequest req) throws Exception { Long sessionId = req.getSessionId(); if (sessionId == null) { ChatSession s = new ChatSession(); s.setUserId(userId); String t = req.getQuestion().trim(); s.setTitle(t.length() > 30 ? t.substring(0, 30) + "…" : t); chatSessionMapper.insert(s); sessionId = s.getId(); } else { ChatSession exist = chatSessionMapper.selectById(sessionId); if (exist == null || !exist.getUserId().equals(userId)) { throw new BusinessException("会话不存在或无权限"); } } long t0 = System.nanoTime(); List<Document> cited = retrieveForCategories(req.getQuestion(), req.getCategoryIds()); long retrievalMs = (System.nanoTime() - t0) / 1_000_000L; log.info("RAG 向量检索完成 sessionId={} 命中块数={} 耗时={}ms", sessionId, cited.size(), retrievalMs); String userTurn = buildRagUserMessage(req.getQuestion(), cited); t0 = System.nanoTime(); String answer = chatClient.prompt().system(SYSTEM_PROMPT).user(userTurn).call().content(); long llmMs = (System.nanoTime() - t0) / 1_000_000L; log.info("LLM 生成完成 sessionId={} 耗时={}ms(SimpleLoggerAdvisor 将打出请求/响应摘要)", sessionId, llmMs); List<Map<String, Object>> refs = toRefs(cited); String refsJson = objectMapper.writeValueAsString(refs); ChatMessage um = new ChatMessage(); um.setSessionId(sessionId); um.setRole("USER"); um.setContent(req.getQuestion()); um.setRefs(null); chatMessageMapper.insert(um); ChatMessage am = new ChatMessage(); am.setSessionId(sessionId); am.setRole("ASSISTANT"); am.setContent(answer); am.setRefs(refsJson); chatMessageMapper.insert(am); chatSessionMapper.touchUpdateTime(sessionId); ChatAskResult res = new ChatAskResult(); res.setSessionId(sessionId); res.setAnswer(answer); res.setReferences(refs); return res; } /** * 有分类时先带 {@code categoryId} 过滤检索;无命中或异常时与无分类相同,做全库无条件检索兜底。 */ private List<Document> retrieveForCategories(String question, List<Long> categoryIds) { if (categoryIds == null || categoryIds.isEmpty()) { return vectorSimilaritySearch(question, null); } Set<String> keys = categoryIds.stream() .filter(Objects::nonNull) .map(String::valueOf) .collect(Collectors.toCollection(LinkedHashSet::new)); if (keys.isEmpty()) { return vectorSimilaritySearch(question, null); } try { Filter.Expression expr = buildCategoryIdFilter(keys); List<Document> filtered = vectorSimilaritySearch(question, expr); if (filtered != null && !filtered.isEmpty()) { return filtered; } log.info("限定 categoryId {} 向量检索无命中,降级为全库无条件检索", keys); return vectorSimilaritySearch(question, null); } catch (Exception ex) { log.warn("categoryId 过滤向量检索失败,降级为全库无条件检索:{}", ex.toString()); return vectorSimilaritySearch(question, null); } } /** * SimpleVectorStore 内存向量检索:无多余参数;{@code filter} 为 null 表示全库。 */ private List<Document> vectorSimilaritySearch(String question, Filter.Expression filter) { SearchRequest.Builder b = SearchRequest.builder() .query(question) .topK(RAG_TOP_K) .similarityThreshold(0.0); if (filter != null) { b.filterExpression(filter); } List<Document> docs = vectorStore.similaritySearch(b.build()); return docs != null ? docs : Collections.emptyList(); } private static Filter.Expression buildCategoryIdFilter(Set<String> categoryIdsAsString) { FilterExpressionBuilder fb = new FilterExpressionBuilder(); if (categoryIdsAsString.size() == 1) { return fb.eq("categoryId", categoryIdsAsString.iterator().next()).build(); } List<Object> values = new ArrayList<>(categoryIdsAsString); return fb.in("categoryId", values).build(); } /** * 将检索结果拼成单条 user 消息,等价于一次 RAG 上下文注入(避免 Advisor 内二次检索)。 */ private static String buildRagUserMessage(String question, List<Document> cited) { if (cited == null || cited.isEmpty()) { return """ (知识库检索未命中足够相关的片段,请直接依据系统说明作答。) 用户问题: """ + question; } StringBuilder sb = new StringBuilder(); sb.append("以下是检索到的知识片段,请严格据此回答;片段相互冲突时优先采纳与问题最直接相关的表述。\n\n"); int i = 1; for (Document d : cited) { Map<String, Object> meta = d.getMetadata(); String title = meta != null && meta.get("title") != null ? String.valueOf(meta.get("title")) : "(无标题)"; sb.append("### 片段 ").append(i++).append(" · ").append(title).append("\n"); String text = d.getText(); if (text != null) { sb.append(text.strip()).append("\n\n"); } } sb.append("---\n用户问题:\n").append(question.strip()); return sb.toString(); } /** * {@inheritDoc} */ @Override public List<ChatSession> listSessions(Long userId) { return chatSessionMapper.listByUserId(userId); } /** * {@inheritDoc} */ @Override public List<ChatMessage> listMessages(Long userId, Long sessionId) { ChatSession s = chatSessionMapper.selectById(sessionId); if (s == null || !s.getUserId().equals(userId)) { throw new BusinessException("会话不存在或无权限"); } return chatMessageMapper.listBySessionId(sessionId); } /** * {@inheritDoc} */ @Override @Transactional(rollbackFor = Exception.class) public void deleteSession(Long userId, Long sessionId) { ChatSession s = chatSessionMapper.selectById(sessionId); if (s == null || !s.getUserId().equals(userId)) { throw new BusinessException("会话不存在或无权限"); } chatMessageMapper.deleteBySessionId(sessionId); chatSessionMapper.deleteById(sessionId); } private static List<Map<String, Object>> toRefs(List<Document> docs) { List<Map<String, Object>> refs = new ArrayList<>(); for (Document d : docs) { Map<String, Object> m = new LinkedHashMap<>(); Map<String, Object> meta = d.getMetadata(); m.put("title", meta != null ? meta.get("title") : null); m.put("docId", meta != null ? meta.get("docId") : null); m.put("categoryId", meta != null ? meta.get("categoryId") : null); String tx = d.getText(); if (tx != null && tx.length() > 240) { tx = tx.substring(0, 240) + "…"; } m.put("snippet", tx); refs.add(m); } return refs; } }
<script setup> import { ref, onMounted } from 'vue' import { ElMessageBox } from 'element-plus' import { Plus } from '@element-plus/icons-vue' import { listCategories, saveCategory, deleteCategory } from '../../api/category' import { formatDateTime } from '../../utils/date' const loading = ref(false) const list = ref([]) const dlg = ref(false) const form = ref({ id: null, name: '', description: '', icon: 'Document', sortOrder: 0 }) async function load() { loading.value = true try { const res = await listCategories() list.value = res.data } finally { loading.value = false } } function openCreate() { form.value = { id: null, name: '', description: '', icon: 'Folder', sortOrder: 0 } dlg.value = true } function openEdit(row) { form.value = { ...row, sortOrder: row.sortOrder ?? 0 } dlg.value = true } async function save() { await saveCategory(form.value) dlg.value = false load() } async function del(row) { await ElMessageBox.confirm(`删除分类「${row.name}」?`, '提示') await deleteCategory(row.id) load() } onMounted(load) </script> <template> <div> <div class="page-title">知识分类</div> <el-card shadow="hover" class="box"> <div class="toolbar"> <el-button type="primary" :icon="Plus" @click="openCreate">新增分类</el-button> </div> <el-table :data="list" v-loading="loading" stripe> <el-table-column prop="id" label="ID" width="70" /> <el-table-column prop="name" label="名称" /> <el-table-column prop="description" label="描述" show-overflow-tooltip /> <el-table-column prop="sortOrder" label="排序" width="90" /> <el-table-column label="创建时间" width="170"> <template #default="{ row }">{{ formatDateTime(row.createTime) }}</template> </el-table-column> <el-table-column label="操作" width="160" fixed="right"> <template #default="{ row }"> <el-button link type="primary" @click="openEdit(row)">编辑</el-button> <el-button link type="danger" @click="del(row)">删除</el-button> </template> </el-table-column> </el-table> </el-card> <el-dialog v-model="dlg" title="分类" width="460px"> <el-form :model="form" label-width="80px"> <el-form-item label="名称"><el-input v-model="form.name" /></el-form-item> <el-form-item label="描述"><el-input v-model="form.description" type="textarea" rows="3" /></el-form-item> <el-form-item label="图标"><el-input v-model="form.icon" placeholder="Element 图标名" /></el-form-item> <el-form-item label="排序"><el-input-number v-model="form.sortOrder" /></el-form-item> </el-form> <template #footer> <el-button @click="dlg = false">取消</el-button> <el-button type="primary" @click="save">保存</el-button> </template> </el-dialog> </div> </template> <style scoped> .toolbar { margin-bottom: 12px; } .box { border-radius: 16px; } </style>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/13 13:41:09

GKD订阅管理实战手册:一站式解决Android自动化规则配置难题

GKD订阅管理实战手册&#xff1a;一站式解决Android自动化规则配置难题 【免费下载链接】GKD_THS_List GKD第三方订阅收录名单 项目地址: https://gitcode.com/gh_mirrors/gk/GKD_THS_List GKD订阅管理是Android自动化工具GKD的第三方订阅收录平台&#xff0c;为GKD用户…

作者头像 李华
网站建设 2026/5/13 13:41:07

Nexus Mods App:告别模组管理噩梦的终极解决方案

Nexus Mods App&#xff1a;告别模组管理噩梦的终极解决方案 【免费下载链接】NexusMods.App Home of the development of the Nexus Mods App 项目地址: https://gitcode.com/gh_mirrors/ne/NexusMods.App 你是否曾因游戏模组冲突而彻夜难眠&#xff1f;是否在手动安装…

作者头像 李华
网站建设 2026/5/13 13:41:06

智能填充革命:Fillinger如何用算法解放设计师的创造力

智能填充革命&#xff1a;Fillinger如何用算法解放设计师的创造力 【免费下载链接】illustrator-scripts Adobe Illustrator scripts 项目地址: https://gitcode.com/gh_mirrors/il/illustrator-scripts 在Adobe Illustrator的日常工作中&#xff0c;你是否曾为重复性的…

作者头像 李华
网站建设 2026/5/13 13:37:52

植物大战僵尸PC版终极修改器:5分钟掌握PvZ Toolkit完整使用指南

植物大战僵尸PC版终极修改器&#xff1a;5分钟掌握PvZ Toolkit完整使用指南 【免费下载链接】pvztoolkit 植物大战僵尸 PC 版综合修改器 项目地址: https://gitcode.com/gh_mirrors/pv/pvztoolkit 你是否曾经在玩植物大战僵尸时感到阳光不够用&#xff1f;是否想过自定义…

作者头像 李华
网站建设 2026/5/13 13:35:04

基于MyBot框架构建本地化可扩展聊天机器人:从架构设计到部署实践

1. 项目概述&#xff1a;一个高度可定制的个人聊天机器人框架 最近在折腾个人助理和自动化工具&#xff0c;发现市面上的方案要么太重&#xff0c;要么太封闭&#xff0c;要么就是云端服务&#xff0c;数据隐私总让人不放心。直到我遇到了 Aununo/MyBot 这个项目&#xff0c;…

作者头像 李华