本地利用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

欢迎各位同学互相交流

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐