当前位置: 首页 > news >正文

老java 程序学习ai 第一步-LLM开发,ollama +LLM+Langchain4 开发ai智能客服

本地利用Ollama部署 大模型 进行LLM开发 AIGC,postgre数据库做向量数据库以及SpringAi智能客服,

AiConfig LLM关键配置文件

package com.example.ai.config; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.model.ollama.OllamaEmbeddingModel; import dev.langchain4j.model.ollama.OllamaStreamingChatModel; import dev.langchain4j.model.openai.OpenAiEmbeddingModel; import dev.langchain4j.model.openai.OpenAiStreamingChatModel; import dev.langchain4j.model.output.Response; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore; import dev.langchain4j.store.memory.chat.ChatMemoryStore; import dev.langchain4j.store.memory.chat.InMemoryChatMemoryStore; import org.springframework.context.annotation.Bean; import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.model.ollama.OllamaChatModel; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.jdbc.core.JdbcTemplate; import javax.sql.DataSource; import java.time.Duration; import java.util.List; @Configuration public class AiConfig { @Value("${langchain4j.open-ai.chat-model.api-key}") private String openAiApiKey; @Value("${langchain4j.ollama.chat-model.base-url}") private String ollamaBaseUrl; @Value("${langchain4j.ollama.chat-model.base-embed-url}") private String ollamaEmbedBaseUrl; @Value("${langchain4j.ollama.chat-model.model-name}") private String ollamaModelName; @Value("${spring.datasource.url}") private String jdbcUrl; @Value("${spring.datasource.username}") private String sqlUser; @Value("${spring.datasource.password}") private String sqlPassword; @Value("${spring.datasource.database-name}") private String databaseName; @Bean public OllamaStreamingChatModel localModel() { return OllamaStreamingChatModel.builder() .baseUrl(ollamaBaseUrl) .modelName(ollamaModelName) .temperature(0.1) .build(); } @Bean @Primary public ChatModel chatLanguageModel() { if ("demo-key".equals(openAiApiKey) || "sk-your-key".equals(openAiApiKey)) { System.out.println(" A Running in DEMO MODE - Using Ollama local model"); return OllamaChatModel.builder(). baseUrl(ollamaBaseUrl) .numCtx(512) .modelName(ollamaModelName).timeout(Duration.ofSeconds(60)).build(); } System.out.println("√ Running with OpenAI API"); return OpenAiChatModel.builder(). apiKey(openAiApiKey). modelName("gpt-4o"). temperature(0.7) .timeout(Duration.ofSeconds(60)) .build(); } @Bean public EmbeddingModel embeddingModel() { return OllamaEmbeddingModel.builder() .baseUrl(ollamaBaseUrl) .modelName("nomic-embed-text:latest") // 推荐用轻量嵌入模型 .timeout(Duration.ofSeconds(120)) // 嵌入请求超时设为30秒 .build(); } @Bean public EmbeddingStore<TextSegment> embeddingStore() { return PgVectorEmbeddingStore.builder() .host("localhost") .port(5432) .database(databaseName) .user(sqlUser) .password(sqlPassword) .table("embedding_store") // 向量专用表(不要用业务表!) .dimension(768) // Qwen 2.5 向量维度 .createTable(true) // 第一次运行自动建表 .dropTableFirst(false) // 生产环境必须 false .build(); } @Bean public StreamingChatModel streamingChatModel() { return OllamaStreamingChatModel.builder() .baseUrl(ollamaBaseUrl) .modelName(ollamaModelName) .temperature(0.1) .build(); } @Bean @Primary public ChatMemoryStore chatMemoryStore() { System.out.println(" Using in-memory chat storage"); return new InMemoryChatMemoryStore(); } }

application.yml

spring: application: name: ai-customer-service datasource: url: jdbc:postgresql://localhost:5432/ai_service username: postgres password: driver-class-name: org.postgresql.Driver database-name: ai_service data: redis: host: localhost port: 6379 database: 1 langchain4j: open-ai: chat-model: api-key: ${OPENAI_API_KEY:sk-your-key} model-name: gpt-4o temperature: 0.7 timeout: PT60S ollama: chat-model: base-url: http://localhost:11434 base-embed-url: http://127.0.0.1:11434 model-name: qwen2.5:1.5b timeout: PT120S ai: knowledge-base: enabled: true chunk-size: 500 chunk-overlap: 50 server: port: 8080 logging: level: com.example.ai: DEBUG dev.langchain4j: INFO
ChatController ai对话
package com.example.ai.controller; import com.example.ai.service.ChatService; import com.example.ai.service.KnowledgeBaseService; import com.example.ai.service.RagChatService; import dev.langchain4j.model.chat.request.ChatRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.Map; import java.util.UUID; @Slf4j @RestController @RequestMapping("/api/chat") @RequiredArgsConstructor public class ChatController { private final ChatService chatService; private final RagChatService ragChatService; private final KnowledgeBaseService kbService; /** * 普通对话(同步) */ @PostMapping("/chat") public ResponseEntity<ChatService.ChatResponse> chat( @RequestBody ChatRequest request) { log.info("收到对话请求: {}", request.message()); String sessionId = request.sessionId() != null ? request.sessionId() : java.util.UUID.randomUUID().toString(); var response = chatService.chat(sessionId, request.userId(), request.message()); // var response=ragChatService.chat(request.message(),sessionId); return ResponseEntity.ok(response); } @DeleteMapping("/{sessionId}") public ResponseEntity<Void> clearSession(@PathVariable String sessionId) { chatService.clearSession(sessionId); return ResponseEntity.noContent().build(); } // @PostMapping("/knowledge/upload") // public ResponseEntity<String> uploadKnowledge(@RequestParam("file") MultipartFile file) throws IOException { // if (file.isEmpty()) { // return ResponseEntity.badRequest().body("文件不能为空"); // } // String contentType = file.getContentType(); // if (contentType == null || !isValidFileType(contentType)) { // return ResponseEntity.badRequest().body("不支持的文件类型:" + contentType); // } // String docId = kbService.ingestDocument(file); // return ResponseEntity.accepted().body("文档理中,ID:" + docId); // // } private boolean isValidFileType(String contentType) { return contentType.equals("application/pdf") ||contentType.equals("application/msword") ||contentType.equals("application/vnd.openxmlformats -officedocument.wordprocessingml.document") ||contentType.equals("text/plain") || contentType.equals("text/markdown") || contentType.equals("text/csv"); } public record ChatRequest(String sessionId, String userId, String message) { } public record DocStatusResponse( String id, String fileName, String status, java.time.LocalDateTime uploadedAt) { } }
KnowledgeBaseController 上传知识库
package com.example.ai.controller; import com.example.ai.service.KnowledgeBaseService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.List; import java.util.UUID; @Slf4j @RestController @RequestMapping("/api/knowledge") @RequiredArgsConstructor public class KnowledgeBaseController { private final KnowledgeBaseService knowledgeBaseService; @PostMapping("/upload") public ResponseEntity<UploadResponse> uploadDocument(@RequestParam("file") MultipartFile file) { log.info("收到文档上传请求: {}", file.getOriginalFilename()); String docId = knowledgeBaseService.ingestDocument(file); return ResponseEntity.ok(new UploadResponse(docId, "文档上传成功,正在处理中")); } public record UploadResponse(String docId, String message) {} }
ChatService 调用llM 具体实现
package com.example.ai.service; import com.example.ai.tools.MsgExtractUtil; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.memory.ChatMemory; import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.service.AiServices; import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; import dev.langchain4j.store.embedding.EmbeddingMatch; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.memory.chat.ChatMemoryStore; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Slf4j @Service @RequiredArgsConstructor public class ChatService { private final ChatModel chatModel; private final ChatMemoryStore memoryStore; @Autowired private EmbeddingStore<TextSegment> embeddingStore; // PGVector 向量库 private final StreamingChatModel streamingChatModel; @Autowired private EmbeddingModel embeddingModel; // 关键:创建一个静态自己,供 WebSocket 调用 public static ChatService instance; @PostConstruct public void init() { instance = this; // Spring 初始化后赋值 } private final Map<String, CustomerSupportAgent> agentCache = new ConcurrentHashMap<>(); public interface CustomerSupportAgent { @SystemMessage(""" 你是一位专业的共享充电宝智能客服助手,名为"小智"。请遵循以下规则: 1. 使用中文回答用户问题,语气友好专业 2. 如果用户询问的是产品技术问题,优先使用知识库内容回答 3. 如果知识库中没有相关信息,坦诚告知并建议转人工客服 4. 对于敏感操作(如退款、修改订单),必须要求用户确认身份信息 5. 每次回复控制在 300 字以内,重点突出 当前时间:{{current_time}} 用户等级:{{user_level}} """) String chat(@UserMessage String userMessage, @V("current_time") String currentTime, @V("user_level") String userLevel); } public ChatResponse chat(String sessionId, String userId, String message) { CustomerSupportAgent agent = agentCache.computeIfAbsent(sessionId, sid -> { ChatMemory chatMemory = MessageWindowChatMemory.builder() .id(sid) .maxMessages(20) .chatMemoryStore(memoryStore) .build(); return AiServices.builder(CustomerSupportAgent.class) .chatModel(chatModel) .chatMemory(chatMemory) .build(); }); String currentTime = java.time.LocalDateTime.now() .format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); String response = agent.chat(message, currentTime, "VIP"); log.info("Session[{}] User: {} | AI: {}", sessionId, message.substring(0, Math.min(50, message.length())), response.substring(0, Math.min(100, response.length()))); return new ChatResponse(response, sessionId, false); } public void clearSession(String sessionId) { agentCache.remove(sessionId); memoryStore.deleteMessages(sessionId); } public record ChatResponse(String content, String sessionId, boolean escalated) {} /** * 流式问答(不带知识库) */ }
KnowledgeBaseService 知识库相关实现
package com.example.ai.service; import com.example.ai.entity.KnowledgeDocument; import com.example.ai.repository.KnowledgeDocumentRepository; import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.DocumentParser; import dev.langchain4j.data.document.DocumentSplitter; import dev.langchain4j.data.document.Metadata; import dev.langchain4j.data.document.parser.apache.tika.ApacheTikaDocumentParser; import dev.langchain4j.data.document.splitter.DocumentSplitters; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.model.embedding.EmbeddingModel; import dev.langchain4j.store.embedding.EmbeddingStore; import dev.langchain4j.store.embedding.EmbeddingStoreIngestor; import com.example.ai.entity.KnowledgeDoc; import com.example.ai.repository.knowledgeDocRepository; import io.hypersistence.utils.common.StringUtils; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.List; import java.util.UUID; @Slf4j @Service @RequiredArgsConstructor public class KnowledgeBaseService { private final EmbeddingStore<TextSegment> embeddingStore; private final EmbeddingModel embeddingModel; private final knowledgeDocRepository docRepository; private final KnowledgeDocumentRepository documentRepository; // @SneakyThrows // public String ingestDocument(MultipartFile file) { // String docId = java.util.UUID.randomUUID().toString(); // // KnowledgeDoc doc = new KnowledgeDoc(); // doc.setFileName(file.getOriginalFilename()); // doc.setContentType(file.getContentType()); // doc.setStatus(KnowledgeDoc.EmbeddingStatus.PROCESSING); // docRepository.save(doc); // // new Thread(() -> processDocument(docId, file)).start(); // // return docId; // } // // @SneakyThrows // private void processDocument(String docId, MultipartFile file) { // try { // DocumentParser parser = new ApacheTikaDocumentParser(); // Document document = parser.parse(new ByteArrayInputStream(file.getBytes())); // document.metadata().add("doc_id", docId); // document.metadata().add("file_name", file.getOriginalFilename()); // // var splitter = DocumentSplitters.recursive( // 500, // 50 // // ); // // DocumentByParagraphSplitter splitter = new DocumentByParagraphSplitter(1000, 200); // // EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder() // .documentSplitter(splitter) // .embeddingModel(embeddingModel) // .embeddingStore(embeddingStore) // .build(); // // ingestor.ingest(document); // // updateStatus(docId, KnowledgeDocument.EmbeddingStatus.COMPLETED); // log.info("文档 [{}] 向量化完成", file.getOriginalFilename()); // // } catch (Exception e) { // log.error("文档向量化失败: {}", e.getMessage(), e); // updateStatus(docId, KnowledgeDocument.EmbeddingStatus.FAILED); // } // } @SneakyThrows public String ingestDocument(MultipartFile file) { String docId = UUID.randomUUID().toString(); byte[] fileBytes = file.getBytes(); // 提前读取,避免线程中失效 String fileName = file.getOriginalFilename(); String contentType = file.getContentType(); // 1. 保存主文档元数据 KnowledgeDocument doc = new KnowledgeDocument(); doc.setId(docId); doc.setFileName(fileName); doc.setContentType(contentType); doc.setStatus(KnowledgeDocument.EmbeddingStatus.PROCESSING); documentRepository.save(doc); // 2. 异步处理(传递字节数组,避免MultipartFile失效) new Thread(() -> processDocumentToDb(docId, fileBytes, fileName)).start(); return docId; } @SneakyThrows private void processDocumentToDb(String docId, byte[] fileBytes, String fileName) { try { // 1. 解析文档 DocumentParser parser = new ApacheTikaDocumentParser(); Document document = parser.parse(new ByteArrayInputStream(fileBytes)); // 调试:打印原始文本,确认解析结果是否完整 System.out.println("解析后的原始文本:" + document.text()); document.metadata().put("doc_id", docId); document.metadata().put("file_name", fileName); // 2. 分块(修正写法 + 过滤空段) List<TextSegment> segments = Arrays.stream(document.text().split("\\n\\s*\\n")) .map(String::trim) .filter(text -> text != null && !text.isBlank()) .filter(text -> text.replaceAll("\\s+", "").length() > 0) .map(text -> { // 在这里构建 Metadata Metadata metadata = new Metadata(); metadata.put("docId", docId); metadata.put("fileName", fileName); metadata.put("question", text); // 你还可以加其他字段,比如分块索引、创建时间等 return TextSegment.from(cleanText(text), metadata); }) .toList(); // 调试:打印分块数量和每段内容 System.out.println("分块总数:" + segments.size()); for (int i = 0; i < segments.size(); i++) { System.out.println("第" + i + "段内容:" + segments.get(i).text()); } // 3 向量化 + 双写两张表 for (TextSegment segment : segments) { if(segment==null|| StringUtils.isBlank(segment.text())){ continue; } // 1. 生成向量:调用 .content() 从 Response 中取出 Embedding Embedding embedding = embeddingModel.embed(segment).content(); // 2. 写入向量表(LangChain4j 检索用) embeddingStore.add(embedding, segment); float[] vectorArray = embedding.vector(); // 3.3 写入你的业务表 knowledge_docs KnowledgeDoc knowledgeDoc = new KnowledgeDoc(); knowledgeDoc.setDocId(docId); knowledgeDoc.setChunkId(UUID.randomUUID().toString()); // 分块唯一ID knowledgeDoc.setFileName(fileName); knowledgeDoc.setContent(segment.text()); knowledgeDoc.setEmbedding(vectorArray); // 对应PG的vector类型 knowledgeDoc.setMetadata(segment.metadata().toMap()); // 转为JSONB knowledgeDoc.setStatus(KnowledgeDoc.EmbeddingStatus.COMPLETED); docRepository.save(knowledgeDoc); } // 4. 更新主文档状态 updateStatus(docId, KnowledgeDocument.EmbeddingStatus.COMPLETED); log.info("文档 [{}] 向量化并写入业务表完成", fileName); } catch (Exception e) { log.error("文档向量化失败: {}", e.getMessage(), e); updateStatus(docId, KnowledgeDocument.EmbeddingStatus.FAILED); } } private String cleanText(String text) { return text.trim() .replaceAll("^问:", "") // 去掉知识库文本的前缀 .replaceAll("[??]", "") // 去掉问号 .trim(); } private void updateStatus(String docId, KnowledgeDocument.EmbeddingStatus status) { documentRepository.findById(docId).ifPresent(doc -> { doc.setStatus(status); documentRepository.save(doc); }); } }

以上只是学习过程中实现问答,文档存入向量数据库,以及读取向量库数据进行回答的简单实现,完成代码可以参考

https://gitee.com/bluethky/ai-service-1.4.git

欢迎各位同学互相交流

http://www.jsqmd.com/news/990762/

相关文章:

  • Zotero Style:重塑文献管理体验的可视化增强神器
  • 终极无损音乐库构建指南:用qobuz-dl轻松获取24位高解析度音频
  • 3分钟掌握:免费使用Cursor Pro功能的完整教程与终极指南
  • Figma中文界面本地化:为什么专业翻译比机器翻译更能提升设计效率?
  • GanttProject深度解析:如何用开源架构实现企业级项目管理
  • MC9S12XE XGATE硬件信号量:嵌入式多核并发编程实战指南
  • ArkTS 严格类型系统:我答错 2 道题后才真正搞懂的几条规则
  • 如何用700欧元预算将随机割草机升级为RTK GPS智能机器人?
  • 如何快速搭建个人付费墙绕过工具:13ft Ladder终极指南
  • 用FPGA驱动WS2812B灯带:手把手教你从Verilog状态机到动态图像显示
  • 别再只会写一种了!用Verilog的三种描述方式搞定三人表决器(附完整代码)
  • 2026年6月,国产PCB行业迎来新一轮技术升级与市场洗牌
  • 编写程序汇总智能跑步机运动数据,计算运动强度,卡路里消耗,评估运动达标率。
  • 南宁旧金首饰回收多少钱一克 内行避坑实操指南 - 余生黄金回收
  • 青岛旧金回收怎么算价 2026行情与防踩坑完整攻略 - 余生黄金回收
  • 别再硬啃公式了!用Simscape Multibody从SolidWorks到MATLAB,手把手复现一阶倒立摆LQR控制
  • 掌握多头自注意力机制(Multi-Head Self-Attention)——Transformer 强大表达能力的核心来源
  • 2026苏州地坪翻新公司推荐榜:聚焦专业服务与品质保障 - 品牌排行榜
  • 2026年6月国产PCB厂家综合实力排行榜评测
  • 如何在非Windows系统上完美编辑Visio文件?drawio-desktop为您提供专业解决方案
  • 用51单片机和Proteus仿真,手把手教你做一个自己的RLC测量仪(附完整代码)
  • 南充黄金回收行情报价 本地变现避坑完整实用攻略 - 余生黄金回收
  • Mobaxterm中文版终极指南:5步掌握免费远程管理工具
  • 【Kafka源码解读和使用指南】第34篇:Kafka消费者配置全解析——提升消费性能的20个关键参数
  • 2026年6月恒温恒湿箱厂家深度洞察:在“国产精造”时代,谁在定义行业新标准? - 品牌推荐
  • 信号处理实战:用Python验证Fourier变换的积分性质(附完整代码)
  • 数据的加密与解密(07:24)
  • 2026温州黄金回收全攻略 本地多家靠谱回收商家详解与避坑指南 - 润富黄金回收
  • AD7606双通道数据采集实战:基于STM32 HAL库的SPI轮询与DMA传输效率对比
  • 连云港黄金变现全攻略2026年6月行情与四大商家推荐 - 润富黄金回收