SpringBoot+本地部署大模型实现知识库功能

SpringBoot+本地部署大模型实现RAG知识库功能

  • 1、Linux系统部署本地大模型
    • 1.1 安装ollama
    • 1.2 启动ollama
    • 1.3 下载deepseek模型
  • 2、Springboot代码调用本地模型实现基础问答功能
  • 3、集成向量数据库
  • 4、知识库数据喂取
  • 5、最终实现RAG知识库功能

1、Linux系统部署本地大模型

1.1 安装ollama

# wget https://ollama.com/download/ollama-linux-amd64.tgz
# tar -C /usr/local -zxvf ollama-linux-amd64.tgz

1.2 启动ollama

# ollama serve  
// 这里注意如果要允许其他客户端远程调用本模型的话需要执行以下启动命令
OLLAMA_HOST=0.0.0.0 OLLAMA_ORIGINS=* ollama serve
或者
OLLAMA_DEBUG=1 OLLAMA_HOST=0.0.0.0 OLLAMA_ORIGINS=* ollama serve > ollama.log 2>&1

这里我启动的时候遇到了报错(主要问题是服务器上的libstdc++ 版本低)

ollama: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.25‘ not found (required by ollama)

解决办法如下:(链接放这里)

https://blog.csdn.net/u011250186/article/details/147144845
这里需要注意的是这里的第三步骤配置并编译的时间会比较长

1.3 下载deepseek模型

在这里插入图片描述

ollama run deepseek-r1:1.5b

可以直接通过服务器去先模型发起提问:
在这里插入图片描述

2、Springboot代码调用本地模型实现基础问答功能

这里前端我主要做的是知识库基本问答功能的一个界面可以调用后台接口然后后台接口根据http请求去调用本地模型的问题大接口生成回复。

    @GetMapping(value = "/getArtificialIntelligence")public ResponseEntity<String> getFaultsByTaskId(@RequestParam(name = "message") String message) throws PromptException {return ResponseEntity.ok(aiService.getArtificialIntelligence(message));}
@Value("${ollama.url}")private String OLLAMA_API_URL;@Value("${ollama.model}")private String OLLAMA_MODEL;@Overridepublic String getArtificialIntelligence(String message) throws PromptException {try {// 1. 构建请求体OllamaRequest request = new OllamaRequest(OLLAMA_MODEL, message,true);// 2. 发送请求HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON);HttpEntity<OllamaRequest> entity = new HttpEntity<>(request, headers);ResponseEntity<OllamaResponse> response = restTemplate.exchange(OLLAMA_API_URL,HttpMethod.POST,entity,OllamaResponse.class);System.out.println(response);System.out.println(response.getBody().getResponse());// 3. 解析响应// generate接口if (response.getStatusCode().is2xxSuccessful() && response.hasBody()) {return response.getBody().getResponse();}throw new PromptException("API响应异常:" + response.getStatusCode());} catch (HttpStatusCodeException e) {// 精准处理HTTP状态码switch (e.getStatusCode().value()) {case 400:throw new PromptException("请求参数错误:" + e.getResponseBodyAsString());case 404:throw new PromptException("模型未找到,请检查配置");case 500:throw new PromptException("模型服务内部错误");default:throw new PromptException("API请求失败:" + e.getStatusCode());}} catch (Exception e) {throw new PromptException("系统异常:" + e.getMessage());}}

3、集成向量数据库

这里我选择使用的是PostgreSQL的vector向量拓展
官网地址:

https://pgxn.org/dist/vector/0.7.4/README.html#Windows

首先必须确保已安装Visual Studio 中的 C++ 支持,然后运行:

& "D:\SoftWare\visual Studio\VC\Auxiliary\Build\vcvars64.bat"cd D:\SoftWare\pgvector\pgvector-master
set "PGROOT=D:\SoftWare\PgSQL"
nmake /F Makefile.win
nmake /F Makefile.win install# 设置 Visual Studio 环境变量
& "D:\SoftWare\Visual Studio\VC\Auxiliary\Build\vcvars64.bat"# 进入源码目录
cd D:\SoftWare\pgvector\pgvector-master# 执行编译
& "D:\SoftWare\Visual Studio\VC\Tools\MSVC\14.43.34808\bin\Hostx64\x64\nmake.exe" /F Makefile.win PGROOT="D:\SoftWare\PgSQL" PG_CONFIG="D:\SoftWare\PgSQL\bin\pg_config.exe"

然后就可以根据pgsql的拓展给的提示去建数据库和表了
在这里插入图片描述

4、知识库数据喂取

我是通过前端界面实现一个文件上传功能然后将文件解析成向量然后存入向量数据库。
在这里插入图片描述
这里使用到了另外一个deepseek模型 nomic-embed-text 需要通过此模型将文件中的数据转成向量然后存储到数据库中。
在这里插入图片描述
后端代码如下:

    @PostMapping("/upload")public ResponseEntity<String> uploadFile(@RequestParam("multipartFiles") MultipartFile file) {try {documentService.processUploadedFile(file);return ResponseEntity.ok("文件已成功解析并存入数据库");} catch (Exception e) {return ResponseEntity.status(500).body("文件处理失败:" + e.getMessage());}}
    private final Tika tika = new Tika();@Resourceprivate  EmbeddingService embeddingService;@Resourceprivate  DocumentChunkRepository documentChunkRepository;/*** 处理上传的文件* @param file*/@Overridepublic void processUploadedFile(MultipartFile file) throws IOException, TikaException {String fileName = file.getOriginalFilename();if (fileName == null || fileName.isEmpty()) throw new IllegalArgumentException("文件名为空");// 使用Tika解析文件为文本String textContent = tika.parseToString(file.getInputStream());// 对文本进行分块 - 改进分块策略
//        List<String> chunks = splitTextIntoChunks(textContent, 512);List<String> chunks = splitTextIntoChunks(textContent, 512, 100);// 批量保存DocumentChunk对象List<DocumentChunk> documentChunks = new ArrayList<>();for (String chunk : chunks) {try {float[] embedding = embeddingService.getEmbedding(chunk);DocumentChunk chunkEntity = new DocumentChunk();chunkEntity.setFilename(fileName);chunkEntity.setContent(chunk);chunkEntity.setEmbedding(embedding);documentChunks.add(chunkEntity);} catch (Exception e) {logger.error("处理文本块时出错: {}", e.getMessage(), e);}}// 批量保存到数据库if (!documentChunks.isEmpty()) {documentChunkRepository.saveAll(documentChunks);}}/*** 将文本分割成适当大小的块,同时尝试保留句子或段落的完整性。** @param text 文本内容* @param maxChunkSize 每个块的最大字符数* @return 分割后的文本块列表*/private List<String> splitTextIntoChunks(String text, int maxChunkSize, int overlap) {List<String> chunks = new ArrayList<>();StringBuilder currentChunk = new StringBuilder(maxChunkSize);String[] sentences = text.split("。|?|!|\\n"); // 按句号/换行切分句子for (String sentence : sentences) {if (sentence.trim().isEmpty()) continue;if (currentChunk.length() + sentence.length() > maxChunkSize) {chunks.add(currentChunk.toString());// 添加 overlap 部分if (overlap > 0 && !chunks.isEmpty()) {String lastPart = getLastNChars(chunks.get(chunks.size() - 1), overlap);currentChunk = new StringBuilder(lastPart).append(sentence);} else {currentChunk = new StringBuilder(sentence);}} else {currentChunk.append(sentence).append(" ");}}if (currentChunk.length() > 0) {chunks.add(currentChunk.toString());}return chunks;
}// 辅助函数:取字符串末尾 n 字符
private String getLastNChars(String str, int n) {return str.length() > n ? str.substring(str.length() - n) : str;
}
@Service
public class EmbeddingServiceImpl implements EmbeddingService {private final RestTemplate restTemplate = new RestTemplate();private final ObjectMapper objectMapper = new ObjectMapper();@Overridepublic float[] getEmbedding(String text) {String url = "http://192.168.2.45:11434/api/embeddings";ObjectMapper objectMapper = new ObjectMapper();try {Map<String, Object> requestBody = new HashMap<>();requestBody.put("model", "nomic-embed-text");requestBody.put("prompt", text);String requestBodyJson = objectMapper.writeValueAsString(requestBody);// 发送POST请求并接收响应ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, requestBodyJson, String.class);if (responseEntity.getStatusCode() != HttpStatus.OK) {throw new RuntimeException("HTTP 错误状态码: " + responseEntity.getStatusCodeValue());}Map<String, Object> map = objectMapper.readValue(responseEntity.getBody(), Map.class);Object embeddingObj = map.get("embedding");if (embeddingObj instanceof float[]) {return (float[]) embeddingObj;} else if (embeddingObj instanceof List<?>) {@SuppressWarnings("unchecked")List<Double> list = (List<Double>) embeddingObj;float[] arr = new float[list.size()];for (int i = 0; i < arr.length; i++) {arr[i] = list.get(i).floatValue();}return arr;} else {throw new RuntimeException("Unexpected type for embedding: " + (embeddingObj != null ? embeddingObj.getClass().getName() : "null"));}} catch (Exception e) {throw new RuntimeException("Failed to get embedding", e);}}

5、最终实现RAG知识库功能

最后就是当用户发起提问的时候首先得去数据库中检索是否有相似的内容片段 如果有的话需要将向量匹配到的内容的原文拿到之后再给模型发起提问的时候带上这些上下文的 Prompt然后让模型根据这部分内容去增强生成 这既是检索增强生成 RAG。下面是代码示例如下:

这里调用的模型的chat接口返回的数据格式是流式回复所以我这里月使用Flux格式返给前端 让前端生成回复的时候不需要等待太久用户体验更好一点。

    /*** 流式回复* @param message* @return*/@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Flux<StreamResponse> streamResponse(@RequestParam String message) {return aiService.getStreamResponse(message).timeout(Duration.ofMinutes(10)).doFinally((SignalType signal) -> {if (signal == SignalType.CANCEL) {logger.info("客户端中断了流式连接");}});}
 public Flux<StreamResponse> getStreamResponse(String message) {return Flux.defer(() -> {try {// 1. 检索相关上下文List<DocumentChunk> chunks = retrievalService.retrieveRelevantChunks(message, 3);StringBuilder contextBuilder = new StringBuilder();for (DocumentChunk chunk : chunks) {contextBuilder.append(chunk.getContent()).append("\n\n");}// 2. 构建带上下文的 PromptString prompt = String.format("请基于以下上下文回答问题:\n\n%s\n\n问题:%s", contextBuilder.toString(), message);// 3. 构建请求体Map<String, Object> requestBody = new HashMap<>();requestBody.put("model", OLLAMA_MODEL);requestBody.put("messages", Collections.singletonList(new Message("user", prompt)));requestBody.put("stream", true);requestBody.put("options", new Options());// 4. 发送流式请求return webClient.post().uri(OLLAMA_API_URL).contentType(MediaType.APPLICATION_JSON).bodyValue(requestBody).retrieve().bodyToFlux(String.class).map(this::parseChunk).doOnSubscribe(sub -> log.debug("建立连接成功")).doOnNext(response -> log.trace("收到分块数据:{}", response)).doOnError(e -> log.error("流式处理异常:", e)).onErrorResume(e -> {log.error("流式请求失败", e);return Flux.error(new PromptException("AI服务暂时不可用"));});} catch (Exception e) {return Flux.error(new PromptException("文档检索失败: " + e.getMessage()));}})
}// 处理每部分分块数据private StreamResponse parseChunk(String chunk) {try {JsonNode node = new ObjectMapper().readTree(chunk);StreamResponse response = new StreamResponse();response.setResponse( node.path("message").path("content").asText());response.setDone(node.path("done").asBoolean());return response;} catch (Exception e) {return new StreamResponse("解析错误", true);}}
@Service
public class RetrievalServiceImpl implements RetrievalService {@Resourceprivate EmbeddingService embeddingService;@Resourceprivate DocumentChunkRepository documentChunkRepository;/*** 检索最相关的文档片段* @param query 检索内容* @param topK 返回最相似的数量* @return*/@Overridepublic List<DocumentChunk> retrieveRelevantChunks(String query, int topK) {// 1. 获取嵌入向量float[] queryVector = embeddingService.getEmbedding(query);// 2. 将 float[] 转换为 PostgreSQL 可识别的 vector 字符串格式 "[v1,v2,v3]"StringBuilder sb = new StringBuilder("[");for (int i = 0; i < queryVector.length; i++) {sb.append(queryVector[i]);if (i < queryVector.length - 1) {sb.append(",");}}sb.append("]");String vectorAsString = sb.toString();// 3. 使用字符串形式传参,避免 Hibernate 自动转成 byteareturn documentChunkRepository.findSimilarChunks(vectorAsString, topK);}
}

这个demo测试已经写了很久了 有很多细节在文章的时候有点想不起来了 后期还会继续优化补充!!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/87370.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/87370.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

嵌入式原理与应用篇---ARM

ARM 架构的 STM32 系列微控制器广泛应用于嵌入式系统开发&#xff0c;理解其汇编语言指令对于优化性能、访问硬件底层非常重要。下面详细解释常见的 ARM 汇编指令及其使用实例。 数据处理指令 1. MOV&#xff08;移动&#xff09; 功能&#xff1a;将立即数或寄存器值复制到…

【RHCSA-Linux考试题目笔记(自用)】servera的题目

一、开始 1、启动rhcsa环境 2、点击题目&#xff0c;看题 3、通过控制器来启动所有虚拟机 控制器 打开后点start&#xff0c;然后ok 之后进入一个有classroom、servera、serverb&#xff08;考试不一定叫这些名&#xff0c;但大差不差&#xff09;什么之类的界面&#xff0c;…

SpringBoot项目使用arthas-tunnel-server

参考官网Arthas Spring Boot Starter | arthas Spring Boot系列之使用Arthas Tunnel Server 进行远程调试实践-腾讯云开发者社区-腾讯云 springBoot项目, 增加maven依赖 <dependency><groupId>com.taobao.arthas</groupId><artifactId>arthas-sprin…

Modbus TCP 进阶:基于以太网的远程设备控制(二)

基于 Modbus TCP 的远程设备控制实战 &#xff08;一&#xff09;硬件与网络搭建实操 1. 设备选型与连接 在工业现场&#xff0c;根据远程控制需求进行设备选型至关重要 。对于传感器&#xff0c;若要监测温度&#xff0c;可选择高精度的热电偶传感器&#xff0c;如 K 型热电…

分库分表之实战-sharding-JDBC

大家好&#xff0c;我是工藤学编程 &#x1f989;一个正在努力学习的小博主&#xff0c;期待你的关注实战代码系列最新文章&#x1f609;C实现图书管理系统&#xff08;Qt C GUI界面版&#xff09;SpringBoot实战系列&#x1f437;【SpringBoot实战系列】Sharding-Jdbc实现分库…

httpcore-nio引起的线程、fd泄露问题

依赖来源&#xff1a;httpasyncclient-4.1.4.jar 现象 程序报错too many open files 线程数飙升、句柄数飙升 thread dump显示大量 "I/O dispatcher 7215" #9102 prio5 os_prio0 tid0x00002b7ba036a800 nid0x6f24 runnable [0x00002b7d98d41000]java.lang.Thread.…

多线程生产者消费者模型实战案例

多线程生产者消费者模型实战案例 前言业务场景术前准备无锁无事务有事务 synchronized事务在锁外事务在锁内 数据库行锁什么是数据库行锁有事务没有事务 乐观锁ReentrantLock分布式锁 前言 曾经一直有一个疑惑&#xff0c;就是关于多线程生产者消费者模型的学习过程中&#xf…

青少年编程与数学 02-022 专业应用软件简介 03 三维建模及动画软件:Autodesk Maya

青少年编程与数学 02-022 专业应用软件简介 03 三维建模及动画软件&#xff1a;Autodesk Maya 一、什么是三维建模二、什么是计算机动画三、三维建模及动画设计软件的发展历程&#xff08;一&#xff09;早期探索阶段&#xff08;20世纪60年代 - 80年代&#xff09;&#xff08…

获得 OCM 大师证书学习历练

当我站在山城重庆的洪崖洞前&#xff0c;看着璀璨的夜景倒映在嘉陵江上&#xff0c;手中紧握着 OCM 大师证书&#xff0c;那一刻&#xff0c;备考时的艰辛与考试时的紧张都化作了满满的成就感。这段在重庆获得 OCM 大师证书的经历&#xff0c;就像一场充满挑战与惊喜的冒险&…

srs-gb28181 与 SRS 5.0 对 GB28181 国标支持

srs-gb28181 是基于 SRS 4.0/5.0 的国标&#xff08;GB28181&#xff09;扩展分支&#xff0c;而 SRS 5.0 官方版本也逐步增强了对 GB28181 的支持。以下是两者的主要区别&#xff1a; 1. 功能支持对比 功能srs-gb28181&#xff08;扩展分支&#xff09;SRS 5.0&#xff08;官…

算法第18天|继续二叉树:修剪二叉搜索树、将有序数组转化为二叉搜索树、把二叉搜索树转换为累加树

今日总结&#xff1a; 1、修剪二叉搜索树&#xff08;重点思考如何修剪&#xff09; &#xff08;1&#xff09;递归的返回值是什么&#xff1f;&#xff08;与插入、删除一样&#xff09; &#xff08;2&#xff09;递归的单层逻辑一定要缕清&#xff08;3中情况讨论&#xff…

C# 多线程(三)线程池

目录 1.通过TPL使用线程池 2.不使用TPL进入线程池的办法 异步委托 3.线程池优化技术 最小线程数的工作原理 每当启动一个新线程时&#xff0c;系统都需要花费数百微秒来分配资源&#xff0c;例如创建独立的局部变量栈空间。默认情况下&#xff0c;每个线程还会占用约1…

学习笔记(29):训练集与测试集划分详解:train_test_split 函数深度解析

学习笔记(29):训练集与测试集划分详解&#xff1a;train_test_split 函数深度解析 一、为什么需要划分训练集和测试集&#xff1f; 在机器学习中&#xff0c;模型需要经历两个核心阶段&#xff1a; 训练阶段&#xff1a;用训练集数据学习特征与目标值的映射关系&#xff08;…

【全网唯一】自动化编辑器 Windows版纯本地离线文字识别插件

目的 自动化编辑器超轻量级RPA工具&#xff0c;零代码制作RPA自动化任务&#xff0c;解放双手&#xff0c;释放双眼&#xff0c;轻松玩游戏&#xff0c;刷任务。本篇文章主要讲解下自动化编辑器的TomatoOCR纯本地离线文字识别Windows版插件如何使用和集成。 准备工作 1、下载自…

GitHub 2FA绑定

GitHub 2FA绑定 作为全球最大的代码托管平台&#xff0c;GitHub对账号安全的重视程度不断提升——自2023年3月起&#xff0c;GitHub已要求所有在GitHub.com上贡献代码的用户必须启用双因素身份验证&#xff08;2FA&#xff09;。如果你是符合条件的用户&#xff0c;会收到一封…

pytest fixture基础大全详解

一、介绍 作用 fixture主要有两个作用&#xff1a; 复用测试数据和环境&#xff0c;可以减少重复的代码&#xff1b;可以在测试用例运行前和运行后设置和清理资源&#xff0c;避免对测试结果产生影响&#xff0c;同时也可以提高测试用例的运行效率。 优势 pytest框架的fix…

Unity知识点-Renderer常用材质变量

本篇总结了Unity中renderer的3种常用的材质相关的变量&#xff1a;renderer.material,renderer.sharedMaterial,renderer.MaterialPropertyBlock。以及三者对SRPBatcher的影响。 一.介绍及对比 1.概念介绍 1.material 定义&#xff1a;material 是Render组件&#xff08;如…

【算法】​​如何判断时间复杂度?

文章目录 1. 什么是时间复杂度&#xff1f;为什么需要时间复杂度&#xff1f; 2. 常见时间复杂度对比3. 如何分析时间复杂度&#xff1f;&#xff08;Java版&#xff09;&#x1f539; 步骤1&#xff1a;找出基本操作&#x1f539; 步骤2&#xff1a;分析循环结构&#xff08;1…

MySQL使用C语言连接

文章目录 版本查看以及编译mysql接口介绍初始化链接数据库下发mysql命令mysql_query获取执行结果mysql_store_result获取结果行数mysql_num_rows获取结果列数mysql_num_fields获取列名mysql_fetch_fields获取结果内容mysql_fetch_row关闭mysql链接mysql_closeC语言操作mysql查看…

坚持每日Codeforces三题挑战:Day 7 - 题目详解(2025-06-11,难度:1200,1300,1500)

每天坚持写三道题第七天&#xff1a; Problem - A - Codeforces 1200 Problem - B - Codeforces 1300 Problem - A - Codeforces 1500 目录 题目一: 题目大意: 解题思路: 代码(C): 题目二: 题目大意: 解题思路: 代码(C): 题目三: 题目大意: 解题思路: 代码(C): …