随着人工智能技术的快速发展,越来越多的企业开始构建内部智能客服系统来提升客户服务效率和质量。本文将详细介绍如何使用Spring AI框架结合Claude大语言模型,构建一个功能完善的企业级智能客服系统。
Spring AI是Spring生态系统中专门为AI应用开发设计的框架,它具有以下核心优势:
1. 天然的Spring生态集成
2. 简化的AI开发体验
3. 企业级特性
Claude作为Anthropic开发的大语言模型,在企业应用场景中表现出色:
我们的智能客服系统采用分层架构设计,主要包含以下组件:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 前端界面 │────│ Spring Boot │────│ Claude API │
│ (Web/Mobile) │ │ 应用服务 │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
┌─────────────────┐
│ 知识库系统 │
│ (Vector Store) │
└─────────────────┘1. 对话管理引擎
2. 知识检索系统
3. Claude集成层
首先,让我们配置项目的基础依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.company</groupId>
<artifactId>intelligent-customer-service</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<dependencies>
<!-- Spring Boot核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring AI相关依赖 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-anthropic-spring-boot-starter</artifactId>
<version>1.0.0-M1</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
<version>1.0.0-M1</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
<version>1.0.0-M1</version>
</dependency>
<!-- 数据库相关 -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<!-- 其他工具库 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</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配置
jpa:
hibernate:
ddl-auto: update
show-sql: false
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
# Spring AI配置
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@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);
// 1. 从知识库检索相关信息
List<Document> relevantDocs = retrieveRelevantKnowledge(question);
// 2. 构建系统提示词
String systemPrompt = buildSystemPrompt(relevantDocs, request.getUserContext());
// 3. 获取对话历史
List<Message> conversationHistory = chatMemory.get(userId, 10);
// 4. 构建完整的消息列表
List<Message> messages = buildMessageList(systemPrompt, conversationHistory, question);
// 5. 调用Claude获取回答
Prompt prompt = new Prompt(messages, buildChatOptions());
org.springframework.ai.chat.model.ChatResponse aiResponse = chatModel.call(prompt);
// 6. 处理和保存结果
String answer = aiResponse.getResult().getOutput().getContent();
saveConversationHistory(userId, question, answer);
// 7. 构建返回结果
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;
}
/**
* 构建Chat选项
*/
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());
}
}@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 {
// 1. 验证文件
validateFile(file);
// 2. 读取文档内容
List<Document> documents = readDocument(file);
// 3. 文档分块
List<Document> chunks = splitDocuments(documents);
// 4. 添加元数据
enrichDocuments(chunks, file.getOriginalFilename(), category, uploadedBy);
// 5. 向量化并存储
vectorStore.add(chunks);
// 6. 保存文档记录
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();
}
}@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) {
// 从JWT token或session中获取用户ID
// 这里简化处理,实际项目中需要根据认证方案实现
return request.getHeader("X-User-ID");
}
}
/**
* 知识库管理API
*/
@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");
}
}缓存机制
异步处理
资源优化
数据安全
API安全
应用监控
@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 {
// 检查Claude API连接
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));
}
}FROM openjdk:17-jdk-slim
COPY target/intelligent-customer-service-1.0.0.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]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: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() {
// Given
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);
// When
ChatResponse response = customerService.handleUserQuery(request);
// Then
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() {
// Given
ChatRequest request = ChatRequest.builder()
.userId("user123")
.message("这是一个新问题")
.build();
when(vectorStore.similaritySearch(any(SearchRequest.class)))
.thenReturn(Arrays.asList());
// When & Then
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 {
// 1. 准备测试数据 - 添加知识文档
MockMultipartFile testFile = new MockMultipartFile(
"file",
"test-doc.txt",
"text/plain",
"这是一个测试文档,包含公司政策信息。".getBytes()
);
knowledgeBaseService.addDocument(testFile, "policy", "test-user");
// 2. 模拟Claude响应
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);
// 3. 发送聊天请求
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);
// 4. 验证响应
ResponseEntity<ApiResponse> response = restTemplate.postForEntity(
"/api/chat/message",
request,
ApiResponse.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().isSuccess()).isTrue();
}
}@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() {
// 当知识库更新时清除缓存
}
}@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);
}
}
}@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构建企业智能客服系统,我们获得了以下核心优势:
随着AI技术的快速发展,我们的智能客服系统还可以在以下方面进行增强:
多模态支持
智能化升级
性能优化
通过本文的详细介绍,相信您已经掌握了使用Spring AI和Claude构建企业智能客服系统的核心技术和实践方法。这套方案不仅技术先进,而且具有良好的工程实践性,能够满足企业级应用的各种需求。
在实际项目中,建议根据具体的业务场景和技术栈情况,对架构和实现细节进行适当调整。同时,持续关注Spring AI和Claude的更新动态,及时采用新功能来进一步提升系统能力。
企业智能客服系统的建设是一个持续迭代的过程,通过不断优化和完善,必将为企业带来更大的价值和竞争优势。