老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
欢迎各位同学互相交流
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)