跳到主要内容
基于 Spring AI 和 Claude 构建企业智能客服系统:从架构到实践 | 极客日志
Java AI java
基于 Spring AI 和 Claude 构建企业智能客服系统:从架构到实践 使用 Spring AI 框架结合 Claude 大语言模型构建企业级智能客服系统的完整方案。内容涵盖技术选型理由、分层架构设计、Maven 依赖配置及 application.yml 设置。核心实现包括智能客服服务类(RAG 检索增强生成)、知识库管理服务(文档解析、分块、向量化)以及 REST API 控制器。此外,还详细阐述了性能优化策略(缓存、异步、流式响应)、安全考虑、监控运维指标、Docker 容器化及 Kubernetes 部署配置,并提供了单元测试与集成测试示例。该方案利用 Spring 生态优势,实现了基于企业知识库的精准问答,具备高扩展性和稳定性。
基于 Spring AI 和 Claude 构建企业智能客服系统:从架构到实践
随着人工智能技术的快速发展,越来越多的企业开始构建内部智能客服系统来提升客户服务效率和质量。本文将详细介绍如何使用 Spring AI 框架结合 Claude 大语言模型,构建一个功能完善的企业级智能客服系统。
为什么选择 Spring AI + Claude 的技术组合?
Spring AI:企业级 AI 应用的理想选择
Spring AI 是 Spring 生态系统中专门为 AI 应用开发设计的框架,它具有以下核心优势:
1. 天然的 Spring 生态集成
与 Spring Boot、Spring Security 等框架无缝集成
遵循 Spring 的依赖注入和自动配置机制
统一的配置管理和监控体系
2. 简化的 AI 开发体验
提供统一的 API 抽象,屏蔽底层复杂性
开箱即用的 RAG(检索增强生成)支持
内置的向量数据库集成和文档处理能力
3. 企业级特性
完整的可观测性和监控支持
生产级的错误处理和重试机制
丰富的配置选项和扩展点
Claude:强大的对话 AI 能力
Claude 作为 Anthropic 开发的大语言模型,在企业应用场景中表现出色:
高质量的中文理解和生成能力
良好的上下文理解和多轮对话支持
可靠的安全性和合规性保障
灵活的 API 调用方式
系统架构设计
整体架构概览
我们的智能客服系统采用分层架构设计,主要包含以下组件:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 前端界面 │────│ Spring Boot │────│ Claude API │
│ (Web/Mobile) │ │ 应用服务 │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ 知识库系统 │
│ (Vector Store) │
└─────────────────┘
核心组件说明
1. 对话管理引擎
处理用户输入和多轮对话
管理会话上下文和历史记录
实现智能路由和意图识别
2. 知识检索系统
基于向量相似度的语义搜索
支持多种文档格式的知识导入
动态知识更新和版本控制
3. Claude 集成层
封装 Claude API 调用
实现 Prompt 工程和上下文构建
处理流式响应和错误重试
项目搭建与依赖配置
Maven 依赖配置
首先,让我们配置项目的基础依赖:
<?xml version="1.0" encoding="UTF-8" ?>
4.0.0
com.company
intelligent-customer-service
1.0.0
jar
org.springframework.boot
spring-boot-starter-parent
3.2.0
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-security
org.springframework.ai
spring-ai-anthropic-spring-boot-starter
1.0.0-M1
org.springframework.ai
spring-ai-pgvector-store-spring-boot-starter
1.0.0-M1
org.springframework.ai
spring-ai-pdf-document-reader
1.0.0-M1
org.postgresql
postgresql
org.apache.commons
commons-lang3
<project xmlns ="http://maven.apache.org/POM/4.0.0" >
<modelVersion >
</modelVersion >
<groupId >
</groupId >
<artifactId >
</artifactId >
<version >
</version >
<packaging >
</packaging >
<parent >
<groupId >
</groupId >
<artifactId >
</artifactId >
<version >
</version >
<relativePath />
</parent >
<dependencies >
<dependency >
<groupId >
</groupId >
<artifactId >
</artifactId >
</dependency >
<dependency >
<groupId >
</groupId >
<artifactId >
</artifactId >
</dependency >
<dependency >
<groupId >
</groupId >
<artifactId >
</artifactId >
</dependency >
<dependency >
<groupId >
</groupId >
<artifactId >
</artifactId >
<version >
</version >
</dependency >
<dependency >
<groupId >
</groupId >
<artifactId >
</artifactId >
<version >
</version >
</dependency >
<dependency >
<groupId >
</groupId >
<artifactId >
</artifactId >
<version >
</version >
</dependency >
<dependency >
<groupId >
</groupId >
<artifactId >
</artifactId >
</dependency >
<dependency >
<groupId >
</groupId >
<artifactId >
</artifactId >
</dependency >
</dependencies >
</project >
应用配置 在 application.yml 中配置相关参数:
spring:
application:
name: intelligent-customer-service
datasource:
url: jdbc:postgresql://localhost:5432/customer_service
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:password}
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
ai:
anthropic:
api-key: ${ANTHROPIC_API_KEY}
chat:
options:
model: claude-sonnet-4-20250514
temperature: 0.3
max-tokens: 2000
vectorstore:
pgvector:
index-type: HNSW
distance-type: COSINE_DISTANCE
dimensions: 1536
app:
knowledge-base:
max-file-size: 10MB
supported-formats: pdf,docx,txt,md
chat:
max-history-size: 20
session-timeout: 30m
logging:
level:
org.springframework.ai: DEBUG
com.company.customerservice: DEBUG
核心业务实现
1. 智能客服服务类 @Service
@Slf4j
public class IntelligentCustomerService {
private final AnthropicChatModel chatModel;
private final VectorStore vectorStore;
private final ChatMemory chatMemory;
private final ConversationService conversationService;
public IntelligentCustomerService (AnthropicChatModel chatModel,
VectorStore vectorStore,
ChatMemory chatMemory,
ConversationService conversationService) {
this .chatModel = chatModel;
this .vectorStore = vectorStore;
this .chatMemory = chatMemory;
this .conversationService = conversationService;
}
public ChatResponse handleUserQuery (ChatRequest request) {
try {
String userId = request.getUserId();
String question = request.getMessage();
log.info("处理用户 {} 的问题:{}" , userId, question);
List<Document> relevantDocs = retrieveRelevantKnowledge(question);
String systemPrompt = buildSystemPrompt(relevantDocs, request.getUserContext());
List<Message> conversationHistory = chatMemory.get(userId, 10 );
List<Message> messages = buildMessageList(systemPrompt, conversationHistory, question);
Prompt prompt = new Prompt (messages, buildChatOptions());
org.springframework.ai.chat.model.ChatResponse aiResponse = chatModel.call(prompt);
String answer = aiResponse.getResult().getOutput().getContent();
saveConversationHistory(userId, question, answer);
return ChatResponse.builder()
.message(answer)
.conversationId(request.getConversationId())
.timestamp(LocalDateTime.now())
.sources(extractSources(relevantDocs))
.build();
} catch (Exception e) {
log.error("处理用户问题时发生错误" , e);
return ChatResponse.builder()
.message("抱歉,我暂时无法回答您的问题,请稍后重试。" )
.error(true )
.build();
}
}
private List<Document> retrieveRelevantKnowledge (String question) {
SearchRequest searchRequest = SearchRequest.query(question).withTopK(5 ).withSimilarityThreshold(0.7 );
return vectorStore.similaritySearch(searchRequest);
}
private String buildSystemPrompt (List<Document> relevantDocs, UserContext userContext) {
StringBuilder contextBuilder = new StringBuilder ();
contextBuilder.append("你是一个专业的企业内部客服助手。请基于以下知识库信息回答用户问题:\n\n" );
for (int i = 0 ; i < relevantDocs.size(); i++) {
Document doc = relevantDocs.get(i);
contextBuilder.append(String.format("知识片段 %d:\n%s\n\n" , i + 1 , doc.getContent()));
}
if (userContext != null ) {
contextBuilder.append(String.format("用户信息:部门=%s,角色=%s\n\n" , userContext.getDepartment(), userContext.getRole()));
}
contextBuilder.append("回答要求:\n" );
contextBuilder.append("1. 基于提供的知识库信息回答,如果信息不足请说明\n" );
contextBuilder.append("2. 回答要准确、简洁、友好\n" );
contextBuilder.append("3. 如果涉及敏感信息,请提醒用户通过正式渠道处理\n" );
contextBuilder.append("4. 使用中文回答\n" );
return contextBuilder.toString();
}
private List<Message> buildMessageList (String systemPrompt, List<Message> history, String currentQuestion) {
List<Message> messages = new ArrayList <>();
messages.add(new SystemMessage (systemPrompt));
messages.addAll(history);
messages.add(new UserMessage (currentQuestion));
return messages;
}
private AnthropicChatOptions buildChatOptions () {
return AnthropicChatOptions.builder()
.withModel("claude-sonnet-4-20250514" )
.withTemperature(0.3 )
.withMaxTokens(2000 )
.build();
}
private void saveConversationHistory (String userId, String question, String answer) {
chatMemory.add(userId, new UserMessage (question));
chatMemory.add(userId, new AssistantMessage (answer));
conversationService.saveConversation(userId, question, answer);
}
private List<String> extractSources (List<Document> documents) {
return documents.stream()
.map(doc -> doc.getMetadata().get("source" ))
.filter(Objects::nonNull)
.map(Object::toString)
.distinct()
.collect(Collectors.toList());
}
}
2. 知识库管理服务 @Service
@Slf4j
public class KnowledgeBaseService {
private final VectorStore vectorStore;
private final KnowledgeDocumentRepository documentRepository;
private final TextSplitter textSplitter;
@Value("${app.knowledge-base.max-file-size:10MB}")
private String maxFileSize;
public KnowledgeBaseService (VectorStore vectorStore, KnowledgeDocumentRepository documentRepository) {
this .vectorStore = vectorStore;
this .documentRepository = documentRepository;
this .textSplitter = new TokenTextSplitter (500 , 50 );
}
@Transactional
public void addDocument (MultipartFile file, String category, String uploadedBy) {
try {
validateFile(file);
List<Document> documents = readDocument(file);
List<Document> chunks = splitDocuments(documents);
enrichDocuments(chunks, file.getOriginalFilename(), category, uploadedBy);
vectorStore.add(chunks);
saveDocumentRecord(file, category, uploadedBy, chunks.size());
log.info("成功添加文档到知识库:{}, 分块数:{}" , file.getOriginalFilename(), chunks.size());
} catch (Exception e) {
log.error("添加文档到知识库失败:{}" , file.getOriginalFilename(), e);
throw new KnowledgeBaseException ("文档处理失败:" + e.getMessage());
}
}
private List<Document> readDocument (MultipartFile file) throws IOException {
String filename = file.getOriginalFilename();
String extension = getFileExtension(filename);
DocumentReader reader = switch (extension.toLowerCase()) {
case "pdf" -> new PagePdfDocumentReader (file.getResource());
case "docx" -> new TikaDocumentReader (file.getResource());
case "txt" , "md" -> new TextDocumentReader (file.getResource());
default -> throw new UnsupportedOperationException ("不支持的文件格式:" + extension);
};
return reader.get();
}
private List<Document> splitDocuments (List<Document> documents) {
List<Document> allChunks = new ArrayList <>();
for (Document document : documents) {
List<Document> chunks = textSplitter.split(document);
allChunks.addAll(chunks);
}
return allChunks;
}
private void enrichDocuments (List<Document> chunks, String filename, String category, String uploadedBy) {
for (int i = 0 ; i < chunks.size(); i++) {
Document chunk = chunks.get(i);
Map<String, Object> metadata = chunk.getMetadata();
metadata.put("source" , filename);
metadata.put("category" , category);
metadata.put("uploadedBy" , uploadedBy);
metadata.put("chunkIndex" , i);
metadata.put("totalChunks" , chunks.size());
metadata.put("uploadTime" , LocalDateTime.now().toString());
}
}
public List<KnowledgeSearchResult> searchKnowledge (String query, int limit) {
SearchRequest searchRequest = SearchRequest.query(query).withTopK(limit).withSimilarityThreshold(0.6 );
List<Document> results = vectorStore.similaritySearch(searchRequest);
return results.stream().map(this ::convertToSearchResult).collect(Collectors.toList());
}
@Transactional
public void deleteDocument (Long documentId) {
KnowledgeDocument document = documentRepository.findById(documentId)
.orElseThrow(() -> new EntityNotFoundException ("文档不存在" ));
vectorStore.delete(List.of(document.getFilename()));
documentRepository.delete(document);
log.info("成功删除文档:{}" , document.getFilename());
}
public KnowledgeBaseStats getStatistics () {
long totalDocuments = documentRepository.count();
long totalChunks = vectorStore.similaritySearch(SearchRequest.query("" ).withTopK(Integer.MAX_VALUE)).size();
Map<String, Long> categoryStats = documentRepository.findCategoryStatistics();
return KnowledgeBaseStats.builder()
.totalDocuments(totalDocuments)
.totalChunks(totalChunks)
.categoryStatistics(categoryStats)
.lastUpdated(LocalDateTime.now())
.build();
}
private void validateFile (MultipartFile file) {
if (file.isEmpty()) {
throw new IllegalArgumentException ("文件不能为空" );
}
String filename = file.getOriginalFilename();
if (filename == null || filename.trim().isEmpty()) {
throw new IllegalArgumentException ("文件名不能为空" );
}
}
private String getFileExtension (String filename) {
if (filename == null || !filename.contains("." )) {
return "" ;
}
return filename.substring(filename.lastIndexOf("." ) + 1 );
}
private void saveDocumentRecord (MultipartFile file, String category, String uploadedBy, int chunkCount) {
KnowledgeDocument document = KnowledgeDocument.builder()
.filename(file.getOriginalFilename())
.fileSize(file.getSize())
.category(category)
.uploadedBy(uploadedBy)
.chunkCount(chunkCount)
.uploadTime(LocalDateTime.now())
.build();
documentRepository.save(document);
}
private KnowledgeSearchResult convertToSearchResult (Document document) {
return KnowledgeSearchResult.builder()
.content(document.getContent())
.source(document.getMetadata().get("source" ).toString())
.category(document.getMetadata().get("category" ).toString())
.relevanceScore(0.0 )
.build();
}
}
3. REST API 控制器 @RestController
@RequestMapping("/api/chat")
@Slf4j
@Validated
public class ChatController {
private final IntelligentCustomerService customerService;
private final KnowledgeBaseService knowledgeBaseService;
public ChatController (IntelligentCustomerService customerService, KnowledgeBaseService knowledgeBaseService) {
this .customerService = customerService;
this .knowledgeBaseService = knowledgeBaseService;
}
@PostMapping("/message")
public ResponseEntity<ApiResponse<ChatResponse>> sendMessage (@Valid @RequestBody ChatRequest request, HttpServletRequest httpRequest) {
try {
String userId = getUserIdFromRequest(httpRequest);
request.setUserId(userId);
ChatResponse response = customerService.handleUserQuery(request);
return ResponseEntity.ok(ApiResponse.success(response));
} catch (Exception e) {
log.error("处理聊天消息失败" , e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("服务暂时不可用,请稍后重试" ));
}
}
@GetMapping("/history/{conversationId}")
public ResponseEntity<ApiResponse<List<ConversationHistory>>> getConversationHistory (
@PathVariable String conversationId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "50") int size) {
try {
List<ConversationHistory> history = customerService.getConversationHistory(conversationId, page, size);
return ResponseEntity.ok(ApiResponse.success(history));
} catch (Exception e) {
log.error("获取对话历史失败" , e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("获取对话历史失败" ));
}
}
@DeleteMapping("/history/{conversationId}")
public ResponseEntity<ApiResponse<Void>> clearConversationHistory (@PathVariable String conversationId) {
try {
customerService.clearConversationHistory(conversationId);
return ResponseEntity.ok(ApiResponse.success(null ));
} catch (Exception e) {
log.error("清除对话历史失败" , e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("清除对话历史失败" ));
}
}
private String getUserIdFromRequest (HttpServletRequest request) {
return request.getHeader("X-User-ID" );
}
}
@RestController
@RequestMapping("/api/knowledge")
@Slf4j
public class KnowledgeController {
private final KnowledgeBaseService knowledgeBaseService;
public KnowledgeController (KnowledgeBaseService knowledgeBaseService) {
this .knowledgeBaseService = knowledgeBaseService;
}
@PostMapping("/upload")
public ResponseEntity<ApiResponse<String>> uploadDocument (
@RequestParam("file") MultipartFile file,
@RequestParam("category") String category,
HttpServletRequest request) {
try {
String uploadedBy = getUserIdFromRequest(request);
knowledgeBaseService.addDocument(file, category, uploadedBy);
return ResponseEntity.ok(ApiResponse.success("文档上传成功" ));
} catch (Exception e) {
log.error("上传文档失败" , e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("文档上传失败:" + e.getMessage()));
}
}
@GetMapping("/search")
public ResponseEntity<ApiResponse<List<KnowledgeSearchResult>>> searchKnowledge (
@RequestParam String query,
@RequestParam(defaultValue = "10") int limit) {
try {
List<KnowledgeSearchResult> results = knowledgeBaseService.searchKnowledge(query, limit);
return ResponseEntity.ok(ApiResponse.success(results));
} catch (Exception e) {
log.error("搜索知识库失败" , e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("搜索失败" ));
}
}
@GetMapping("/stats")
public ResponseEntity<ApiResponse<KnowledgeBaseStats>> getStatistics () {
try {
KnowledgeBaseStats stats = knowledgeBaseService.getStatistics();
return ResponseEntity.ok(ApiResponse.success(stats));
} catch (Exception e) {
log.error("获取统计信息失败" , e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("获取统计信息失败" ));
}
}
private String getUserIdFromRequest (HttpServletRequest request) {
return request.getHeader("X-User-ID" );
}
}
系统优化与最佳实践
1. 性能优化策略
对频繁查询的知识片段进行缓存
使用 Redis 缓存用户会话和对话历史
实现智能的缓存失效策略
文档上传和处理使用异步队列
长时间的 AI 推理任务异步执行
实现流式响应提升用户体验
合理配置数据库连接池
优化向量检索的参数设置
实现请求限流和熔断保护
2. 安全考虑
所有敏感配置使用环境变量管理
实现完整的用户认证和授权机制
对上传文档进行安全扫描
实现请求签名验证
添加频率限制和防爬虫机制
记录详细的审计日志
3. 监控和运维 @Component
public class ChatServiceMetrics {
private final MeterRegistry meterRegistry;
private final Counter chatRequestCounter;
private final Timer responseTimeTimer;
public ChatServiceMetrics (MeterRegistry meterRegistry) {
this .meterRegistry = meterRegistry;
this .chatRequestCounter = Counter.builder("chat.requests.total" )
.description("Total number of chat requests" )
.register(meterRegistry);
this .responseTimeTimer = Timer.builder("chat.response.time" )
.description("Chat response time" )
.register(meterRegistry);
}
public void recordChatRequest () {
chatRequestCounter.increment();
}
public Timer.Sample startTimer () {
return Timer.start(meterRegistry);
}
}
@Component
public class ChatServiceHealthIndicator implements HealthIndicator {
private final AnthropicChatModel chatModel;
private final VectorStore vectorStore;
@Override
public Health health () {
try {
checkClaudeConnection();
checkVectorStoreConnection();
return Health.up()
.withDetail("claude" , "Available" )
.withDetail("vectorStore" , "Available" )
.build();
} catch (Exception e) {
return Health.down().withDetail("error" , e.getMessage()).build();
}
}
private void checkClaudeConnection () {
chatModel.call(new Prompt ("Hello" ));
}
private void checkVectorStoreConnection () {
vectorStore.similaritySearch(SearchRequest.query("test" ).withTopK(1 ));
}
}
部署与运维
Docker 容器化 FROM openjdk:17-jdk-slim
COPY target/intelligent-customer-service-1.0.0.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]
Docker Compose 配置 version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- DB_USERNAME=postgres
- DB_PASSWORD=password
depends_on:
- postgres
- redis
postgres:
image: pgvector/pgvector:pg16
environment:
- POSTGRES_DB=customer_service
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
Kubernetes 部署配置 apiVersion: apps/v1
kind: Deployment
metadata:
name: intelligent-customer-service
labels:
app: customer-service
spec:
replicas: 3
selector:
matchLabels:
app: customer-service
template:
metadata:
labels:
app: customer-service
spec:
containers:
- name: customer-service
image: company/intelligent-customer-service:1.0.0
ports:
- containerPort: 8080
env:
- name: ANTHROPIC_API_KEY
valueFrom:
secretKeyRef:
name: api-secrets
key: anthropic-api-key
- name: DB_USERNAME
valueFrom:
configMapKeyRef:
name: app-config
key: db-username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secrets
key: password
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: customer-service-service
spec:
selector:
app: customer-service
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: ClusterIP
测试策略
单元测试示例 @ExtendWith(MockitoExtension.class)
class IntelligentCustomerServiceTest {
@Mock
private AnthropicChatModel chatModel;
@Mock
private VectorStore vectorStore;
@Mock
private ChatMemory chatMemory;
@Mock
private ConversationService conversationService;
@InjectMocks
private IntelligentCustomerService customerService;
@Test
void shouldHandleUserQuerySuccessfully () {
ChatRequest request = ChatRequest.builder()
.userId("user123" )
.message("如何申请年假?" )
.conversationId("conv456" )
.build();
List<Document> mockDocs = Arrays.asList(new Document ("年假申请需要提前 2 周提交申请表..." ));
when (vectorStore.similaritySearch(any(SearchRequest.class))).thenReturn(mockDocs);
when (chatMemory.get(eq("user123" ), eq(10 ))).thenReturn(Arrays.asList());
org.springframework.ai.chat.model.ChatResponse mockResponse = new org .springframework.ai.chat.model.ChatResponse(
Arrays.asList(new Generation (new AssistantMessage ("根据公司政策,年假申请需要..." ))));
when (chatModel.call(any(Prompt.class))).thenReturn(mockResponse);
ChatResponse response = customerService.handleUserQuery(request);
assertThat(response).isNotNull();
assertThat(response.getMessage()).contains("年假申请" );
assertThat(response.isError()).isFalse();
verify(vectorStore).similaritySearch(any(SearchRequest.class));
verify(chatModel).call(any(Prompt.class));
verify(chatMemory, times(2 )).add(eq("user123" ), any(Message.class));
}
@Test
void shouldHandleEmptyKnowledgeBase () {
ChatRequest request = ChatRequest.builder()
.userId("user123" )
.message("这是一个新问题" )
.build();
when (vectorStore.similaritySearch(any(SearchRequest.class))).thenReturn(Arrays.asList());
ChatResponse response = customerService.handleUserQuery(request);
assertThat(response).isNotNull();
}
}
集成测试 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class CustomerServiceIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer <>("pgvector/pgvector:pg16" )
.withDatabaseName("test_customer_service" )
.withUsername("test" )
.withPassword("test" );
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private KnowledgeBaseService knowledgeBaseService;
@MockBean
private AnthropicChatModel chatModel;
@Test
void shouldCompleteFullChatFlow () throws Exception {
MockMultipartFile testFile = new MockMultipartFile ("file" , "test-doc.txt" , "text/plain" ,
"这是一个测试文档,包含公司政策信息。" .getBytes());
knowledgeBaseService.addDocument(testFile, "policy" , "test-user" );
org.springframework.ai.chat.model.ChatResponse mockResponse = new org .springframework.ai.chat.model.ChatResponse(
Arrays.asList(new Generation (new AssistantMessage ("基于提供的文档,我可以回答您的问题..." ))));
when (chatModel.call(any(Prompt.class))).thenReturn(mockResponse);
ChatRequest chatRequest = ChatRequest.builder()
.message("请告诉我公司政策" )
.userId("test-user" )
.conversationId("test-conv" )
.build();
HttpHeaders headers = new HttpHeaders ();
headers.set("X-User-ID" , "test-user" );
HttpEntity<ChatRequest> request = new HttpEntity <>(chatRequest, headers);
ResponseEntity<ApiResponse> response = restTemplate.postForEntity("/api/chat/message" , request, ApiResponse.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().isSuccess()).isTrue();
}
}
性能调优与扩展
1. 缓存优化 @Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager () {
RedisCacheManager.Builder builder = RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory())
.cacheDefaults(cacheConfiguration());
return builder.build();
}
private RedisCacheConfiguration cacheConfiguration () {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30 ))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer ()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer ()));
}
}
@Service
public class CachedKnowledgeService {
@Cacheable(value = "knowledge-search", key = "#query + '-' + #limit")
public List<KnowledgeSearchResult> searchWithCache (String query, int limit) {
return knowledgeBaseService.searchKnowledge(query, limit);
}
@CacheEvict(value = "knowledge-search", allEntries = true)
public void clearSearchCache () {
}
}
2. 异步处理优化 @Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "documentProcessingExecutor")
public TaskExecutor documentProcessingExecutor () {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor ();
executor.setCorePoolSize(4 );
executor.setMaxPoolSize(8 );
executor.setQueueCapacity(100 );
executor.setThreadNamePrefix("doc-processing-" );
executor.setRejectedExecutionHandler(new ThreadPoolExecutor .CallerRunsPolicy());
executor.initialize();
return executor;
}
@Bean(name = "chatProcessingExecutor")
public TaskExecutor chatProcessingExecutor () {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor ();
executor.setCorePoolSize(8 );
executor.setMaxPoolSize(16 );
executor.setQueueCapacity(200 );
executor.setThreadNamePrefix("chat-processing-" );
executor.initialize();
return executor;
}
}
@Service
public class AsyncDocumentProcessor {
@Async("documentProcessingExecutor")
public CompletableFuture<Void> processDocumentAsync (MultipartFile file, String category, String uploadedBy) {
try {
knowledgeBaseService.addDocument(file, category, uploadedBy);
notificationService.sendProcessingComplete(uploadedBy, file.getOriginalFilename());
return CompletableFuture.completedFuture(null );
} catch (Exception e) {
log.error("异步文档处理失败" , e);
notificationService.sendProcessingError(uploadedBy, file.getOriginalFilename(), e.getMessage());
throw new CompletionException (e);
}
}
}
3. 流式响应实现 @RestController
public class StreamingChatController {
@GetMapping(value = "/api/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamChat (@RequestParam String message, @RequestParam String userId) {
return Flux.create(sink -> {
try {
ChatRequest request = ChatRequest.builder()
.message(message)
.userId(userId)
.build();
customerService.handleUserQueryStream(request).subscribe(
chunk -> sink.next(ServerSentEvent.<String>builder().data(chunk).build()),
error -> sink.error(error),
() -> sink.complete()
);
} catch (Exception e) {
sink.error(e);
}
});
}
}
总结与展望 基于 Spring AI 和 Claude 构建企业智能客服系统,我们获得了以下核心优势:
技术优势
开发效率大幅提升 :Spring AI 提供的统一抽象层大大简化了 AI 集成的复杂度
企业级稳定性 :完整的 Spring 生态支持确保了系统的可靠性和可维护性
灵活的扩展能力 :模块化设计支持快速添加新功能和集成其他 AI 服务
业务价值
智能化客服体验 :基于企业知识库的精准回答提升了服务质量
成本效益显著 :自动化处理减少了人工客服的工作量
数据安全可控 :内部部署确保了企业数据的安全性
未来发展方向 随着 AI 技术的快速发展,我们的智能客服系统还可以在以下方面进行增强:
集成图像理解能力,支持图文混合问答
添加语音交互功能,提供更自然的交互体验
实现意图识别和情感分析
支持主动推荐和个性化服务
集成工作流自动化能力
实现更智能的缓存策略
优化向量检索算法
支持大规模并发处理
通过本文的详细介绍,相信您已经掌握了使用 Spring AI 和 Claude 构建企业智能客服系统的核心技术和实践方法。这套方案不仅技术先进,而且具有良好的工程实践性,能够满足企业级应用的各种需求。
在实际项目中,建议根据具体的业务场景和技术栈情况,对架构和实现细节进行适当调整。同时,持续关注 Spring AI 和 Claude 的更新动态,及时采用新功能来进一步提升系统能力。
企业智能客服系统的建设是一个持续迭代的过程,通过不断优化和完善,必将为企业带来更大的价值和竞争优势。
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online