项目概述
构建一个智能客服系统,集成Spring AI作为应用框架,使用MCP协议连接外部工具,通过Dify管理AI工作流。
项目结构
css
intelligent-customer-service/
├── src/main/java/
│ ├── com/example/service/
│ │ ├── CustomerServiceApplication.java
│ │ ├── config/
│ │ │ ├── SpringAIConfig.java
│ │ │ ├── MCPConfig.java
│ │ │ └── DifyConfig.java
│ │ ├── controller/
│ │ │ └── ChatController.java
│ │ ├── service/
│ │ │ ├── ChatService.java
│ │ │ ├── MCPToolService.java
│ │ │ └── DifyWorkflowService.java
│ │ ├── model/
│ │ │ ├── ChatRequest.java
│ │ │ ├── ChatResponse.java
│ │ │ └── CustomerInfo.java
│ │ └── mcp/
│ │ ├── MCPClient.java
│ │ ├── MCPTool.java
│ │ └── tools/
│ │ ├── DatabaseTool.java
│ │ └── EmailTool.java
├── src/main/resources/
│ ├── application.yml
│ └── mcp-tools.json
└── pom.xml
1. Maven依赖配置
xml
<?xml version="1.0" encoding="UTF-8"?>
<pom.xml>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</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>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<spring-ai.version>0.8.0</spring-ai.version>
</properties>
<dependencies>
<!-- Spring Boot Starters -->
<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>
<!-- Spring AI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- HTTP Client for MCP and Dify -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
</pom.xml>
2. 配置文件
yaml
# application.yml
server:
port: 8080
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY:your-openai-api-key}
chat:
model: gpt-4
temperature: 0.7
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password: password
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create-drop
# MCP Configuration
mcp:
server:
url: ws://localhost:3001
timeout: 30000
tools:
database:
enabled: true
connection-url: jdbc:h2:mem:testdb
email:
enabled: true
smtp-host: smtp.gmail.com
smtp-port: 587
# Dify Configuration
dify:
api:
base-url: https://api.dify.ai/v1
key: ${DIFY_API_KEY:your-dify-api-key}
workflow:
customer-service: ${DIFY_WORKFLOW_ID:workflow-id}
logging:
level:
com.example.service: DEBUG
org.springframework.ai: DEBUG
3. 核心配置类
java
// SpringAIConfig.java
package com.example.service.config;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.openai.OpenAiChatClient;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringAIConfig {
@Value("${spring.ai.openai.api-key}")
private String apiKey;
@Bean
public OpenAiApi openAiApi() {
return new OpenAiApi(apiKey);
}
@Bean
public ChatClient chatClient(OpenAiApi openAiApi) {
return OpenAiChatClient.builder(openAiApi)
.withDefaultOptions(OpenAiChatOptions.builder()
.withModel("gpt-4")
.withTemperature(0.7f)
.withMaxTokens(1000)
.build())
.build();
}
}
// MCPConfig.java
package com.example.service.config;
import com.example.service.mcp.MCPClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
import org.springframework.web.reactive.socket.client.WebSocketClient;
@Configuration
public class MCPConfig {
@Value("${mcp.server.url}")
private String mcpServerUrl;
@Value("${mcp.server.timeout}")
private int timeout;
@Bean
public WebSocketClient webSocketClient() {
return new ReactorNettyWebSocketClient();
}
@Bean
public MCPClient mcpClient(WebSocketClient webSocketClient) {
return new MCPClient(webSocketClient, mcpServerUrl, timeout);
}
}
// DifyConfig.java
package com.example.service.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class DifyConfig {
@Value("${dify.api.base-url}")
private String baseUrl;
@Value("${dify.api.key}")
private String apiKey;
@Bean
public WebClient difyWebClient() {
return WebClient.builder()
.baseUrl(baseUrl)
.defaultHeader("Authorization", "Bearer " + apiKey)
.defaultHeader("Content-Type", "application/json")
.build();
}
}
4. 数据模型
java
// ChatRequest.java
package com.example.service.model;
import javax.validation.constraints.NotBlank;
public class ChatRequest {
@NotBlank
private String message;
private String sessionId;
private String userId;
private String context;
// Constructors
public ChatRequest() {}
public ChatRequest(String message, String sessionId, String userId) {
this.message = message;
this.sessionId = sessionId;
this.userId = userId;
}
// Getters and Setters
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getSessionId() { return sessionId; }
public void setSessionId(String sessionId) { this.sessionId = sessionId; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getContext() { return context; }
public void setContext(String context) { this.context = context; }
}
// ChatResponse.java
package com.example.service.model;
import java.time.LocalDateTime;
import java.util.List;
public class ChatResponse {
private String response;
private String sessionId;
private LocalDateTime timestamp;
private List<String> suggestedActions;
private boolean requiresHumanIntervention;
// Constructors
public ChatResponse() {
this.timestamp = LocalDateTime.now();
}
public ChatResponse(String response, String sessionId) {
this();
this.response = response;
this.sessionId = sessionId;
}
// Getters and Setters
public String getResponse() { return response; }
public void setResponse(String response) { this.response = response; }
public String getSessionId() { return sessionId; }
public void setSessionId(String sessionId) { this.sessionId = sessionId; }
public LocalDateTime getTimestamp() { return timestamp; }
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
public List<String> getSuggestedActions() { return suggestedActions; }
public void setSuggestedActions(List<String> suggestedActions) {
this.suggestedActions = suggestedActions;
}
public boolean isRequiresHumanIntervention() { return requiresHumanIntervention; }
public void setRequiresHumanIntervention(boolean requiresHumanIntervention) {
this.requiresHumanIntervention = requiresHumanIntervention;
}
}
5. MCP客户端实现
java
// MCPClient.java
package com.example.service.mcp;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.socket.WebSocketMessage;
import org.springframework.web.reactive.socket.WebSocketSession;
import org.springframework.web.reactive.socket.client.WebSocketClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Component
public class MCPClient {
private static final Logger logger = LoggerFactory.getLogger(MCPClient.class);
private final WebSocketClient client;
private final String serverUrl;
private final int timeout;
private final ObjectMapper objectMapper;
private final AtomicLong messageIdCounter;
private final Map<String, Mono<JsonNode>> pendingRequests;
private WebSocketSession session;
public MCPClient(WebSocketClient client, String serverUrl, int timeout) {
this.client = client;
this.serverUrl = serverUrl;
this.timeout = timeout;
this.objectMapper = new ObjectMapper();
this.messageIdCounter = new AtomicLong();
this.pendingRequests = new ConcurrentHashMap<>();
}
public Mono<Void> connect() {
return client.execute(URI.create(serverUrl), this::handleSession);
}
private Mono<Void> handleSession(WebSocketSession session) {
this.session = session;
Mono<Void> input = session.receive()
.map(WebSocketMessage::getPayloadAsText)
.doOnNext(this::handleMessage)
.then();
Mono<Void> output = session.send(Flux.never());
return Mono.zip(input, output).then();
}
private void handleMessage(String message) {
try {
JsonNode jsonMessage = objectMapper.readTree(message);
String id = jsonMessage.get("id").asText();
if (pendingRequests.containsKey(id)) {
pendingRequests.get(id).subscribe(result -> {
// Handle response
});
pendingRequests.remove(id);
}
} catch (Exception e) {
logger.error("Error handling MCP message", e);
}
}
public Mono<JsonNode> callTool(String toolName, Map<String, Object> parameters) {
String messageId = String.valueOf(messageIdCounter.incrementAndGet());
Map<String, Object> request = Map.of(
"jsonrpc", "2.0",
"id", messageId,
"method", "tools/call",
"params", Map.of(
"name", toolName,
"arguments", parameters
)
);
try {
String requestJson = objectMapper.writeValueAsString(request);
Mono<JsonNode> responseMono = Mono.<JsonNode>create(sink -> {
pendingRequests.put(messageId, Mono.just(sink));
}).timeout(Duration.ofMillis(timeout));
if (session != null) {
session.send(Mono.just(session.textMessage(requestJson)))
.subscribe();
}
return responseMono;
} catch (Exception e) {
logger.error("Error calling MCP tool: " + toolName, e);
return Mono.error(e);
}
}
}
6. MCP工具实现
java
// DatabaseTool.java
package com.example.service.mcp.tools;
import com.example.service.mcp.MCPClient;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.Map;
@Component
public class DatabaseTool {
@Autowired
private MCPClient mcpClient;
public Mono<String> queryCustomerInfo(String customerId) {
Map<String, Object> params = Map.of(
"query", "SELECT * FROM customers WHERE id = ?",
"parameters", new String[]{customerId}
);
return mcpClient.callTool("database_query", params)
.map(this::extractResult)
.onErrorReturn("Customer information not found");
}
public Mono<String> updateCustomerStatus(String customerId, String status) {
Map<String, Object> params = Map.of(
"query", "UPDATE customers SET status = ? WHERE id = ?",
"parameters", new String[]{status, customerId}
);
return mcpClient.callTool("database_update", params)
.map(result -> "Customer status updated successfully")
.onErrorReturn("Failed to update customer status");
}
private String extractResult(JsonNode response) {
if (response.has("result")) {
return response.get("result").toString();
}
return "No data found";
}
}
7. Dify工作流服务
java
// DifyWorkflowService.java
package com.example.service.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.Map;
@Service
public class DifyWorkflowService {
private static final Logger logger = LoggerFactory.getLogger(DifyWorkflowService.class);
@Autowired
private WebClient difyWebClient;
@Value("${dify.workflow.customer-service}")
private String workflowId;
private final ObjectMapper objectMapper = new ObjectMapper();
public Mono<String> executeWorkflow(String userMessage, Map<String, Object> context) {
Map<String, Object> requestBody = Map.of(
"inputs", Map.of(
"user_message", userMessage,
"context", context
),
"response_mode", "blocking",
"user", context.getOrDefault("userId", "anonymous")
);
return difyWebClient.post()
.uri("/workflows/run")
.bodyValue(requestBody)
.retrieve()
.bodyToMono(JsonNode.class)
.map(this::extractWorkflowResult)
.doOnNext(result -> logger.info("Dify workflow result: {}", result))
.onErrorResume(error -> {
logger.error("Error executing Dify workflow", error);
return Mono.just("I apologize, but I'm experiencing technical difficulties. Please try again later.");
});
}
private String extractWorkflowResult(JsonNode response) {
if (response.has("data") && response.get("data").has("outputs")) {
JsonNode outputs = response.get("data").get("outputs");
if (outputs.has("answer")) {
return outputs.get("answer").asText();
}
}
return "No response from workflow";
}
public Mono<Boolean> isWorkflowHealthy() {
return difyWebClient.get()
.uri("/workflows/{workflowId}/status", workflowId)
.retrieve()
.bodyToMono(JsonNode.class)
.map(response -> response.get("status").asText().equals("active"))
.onErrorReturn(false);
}
}
8. 主要服务层
java
// ChatService.java
package com.example.service.service;
import com.example.service.model.ChatRequest;
import com.example.service.model.ChatResponse;
import com.example.service.mcp.tools.DatabaseTool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class ChatService {
private static final Logger logger = LoggerFactory.getLogger(ChatService.class);
@Autowired
private ChatClient chatClient;
@Autowired
private DifyWorkflowService difyWorkflowService;
@Autowired
private DatabaseTool databaseTool;
private static final String SYSTEM_PROMPT = """
You are an intelligent customer service assistant. You have access to:
1. Customer database through MCP tools
2. Dify workflows for complex business logic
3. Email and notification systems
Your responsibilities:
- Provide accurate and helpful customer support
- Use appropriate tools when needed
- Escalate to human agents when necessary
- Maintain a professional and friendly tone
If you need to access customer information, use the database tools.
For complex workflows like order processing or refunds, use Dify workflows.
""";
public Mono<com.example.service.model.ChatResponse> processChat(ChatRequest request) {
logger.info("Processing chat request from user: {}", request.getUserId());
// First, try to determine if we need customer data
return determineRequiredTools(request.getMessage())
.flatMap(toolsNeeded -> {
if (toolsNeeded.contains("customer_data")) {
return enrichWithCustomerData(request);
} else {
return handleWithAI(request, Map.of());
}
})
.map(this::createChatResponse)
.doOnNext(response -> logger.info("Generated response for session: {}", response.getSessionId()));
}
private Mono<List<String>> determineRequiredTools(String message) {
// Simple keyword-based tool detection
// In production, this could be more sophisticated
if (message.toLowerCase().contains("order") ||
message.toLowerCase().contains("account") ||
message.toLowerCase().contains("profile")) {
return Mono.just(Arrays.asList("customer_data"));
}
return Mono.just(Arrays.asList());
}
private Mono<String> enrichWithCustomerData(ChatRequest request) {
if (request.getUserId() != null) {
return databaseTool.queryCustomerInfo(request.getUserId())
.flatMap(customerInfo -> {
Map<String, Object> context = Map.of(
"customer_info", customerInfo,
"user_id", request.getUserId()
);
return handleWithAI(request, context);
});
} else {
return handleWithAI(request, Map.of());
}
}
private Mono<String> handleWithAI(ChatRequest request, Map<String, Object> context) {
// First try Dify workflow for complex scenarios
if (isComplexScenario(request.getMessage())) {
return difyWorkflowService.executeWorkflow(request.getMessage(), context)
.onErrorResume(error -> {
logger.warn("Dify workflow failed, falling back to Spring AI", error);
return handleWithSpringAI(request, context);
});
} else {
return handleWithSpringAI(request, context);
}
}
private boolean isComplexScenario(String message) {
return message.toLowerCase().contains("refund") ||
message.toLowerCase().contains("return") ||
message.toLowerCase().contains("exchange") ||
message.toLowerCase().contains("cancel order");
}
private Mono<String> handleWithSpringAI(ChatRequest request, Map<String, Object> context) {
return Mono.fromCallable(() -> {
List<Message> messages = Arrays.asList(
new SystemMessage(SYSTEM_PROMPT + buildContextPrompt(context)),
new UserMessage(request.getMessage())
);
Prompt prompt = new Prompt(messages);
ChatResponse response = chatClient.call(prompt);
return response.getResult().getOutput().getContent();
});
}
private String buildContextPrompt(Map<String, Object> context) {
if (context.isEmpty()) {
return "";
}
StringBuilder contextPrompt = new StringBuilder("\n\nAdditional Context:\n");
context.forEach((key, value) -> {
contextPrompt.append("- ").append(key).append(": ").append(value).append("\n");
});
return contextPrompt.toString();
}
private com.example.service.model.ChatResponse createChatResponse(String aiResponse) {
com.example.service.model.ChatResponse response =
new com.example.service.model.ChatResponse();
response.setResponse(aiResponse);
// Analyze response for suggested actions
if (aiResponse.toLowerCase().contains("contact support") ||
aiResponse.toLowerCase().contains("human agent")) {
response.setRequiresHumanIntervention(true);
}
return response;
}
}
9. 控制器
java
// ChatController.java
package com.example.service.controller;
import com.example.service.model.ChatRequest;
import com.example.service.model.ChatResponse;
import com.example.service.service.ChatService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/chat")
@CrossOrigin(origins = "*")
public class ChatController {
@Autowired
private ChatService chatService;
@PostMapping("/message")
public Mono<ResponseEntity<ChatResponse>> sendMessage(@Valid @RequestBody ChatRequest request) {
return chatService.processChat(request)
.map(ResponseEntity::ok)
.onErrorReturn(ResponseEntity.internalServerError().build());
}
@GetMapping("/health")
public ResponseEntity<String> health() {
return ResponseEntity.ok("Chat service is running");
}
@PostMapping("/session/{sessionId}/end")
public ResponseEntity<String> endSession(@PathVariable String sessionId) {
// Implementation for ending chat session
return ResponseEntity.ok("Session ended: " + sessionId);
}
}
10. 主应用类
java
// CustomerServiceApplication.java
package com.example.service;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication
@EnableAspectJAutoProxy
public class CustomerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CustomerServiceApplication.class, args);
}
}
11. MCP工具配置
json
// mcp-tools.json
{
"tools": [
{
"name": "database_query",
"description": "Query customer database",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "SQL query to execute"
},
"parameters": {
"type": "array",
"description": "Query parameters"
}
},
"required": ["query"]
}
},
{
"name": "send_email",
"description": "Send email to customer",
"parameters": {
"type": "object",
"properties": {
"to": {
"type": "string",
"description": "Recipient email address"
},
"subject": {
"type": "string",
"description": "Email subject"
},
"body": {
"type": "string",
"description": "Email body"
}
},
"required": ["to", "subject", "body"]
}
}
]
}
12. 使用示例
bash
# 启动应用
mvn spring-boot:run
# 发送聊天请求
curl -X POST http://localhost:8080/api/chat/message \
-H "Content-Type: application/json" \
-d '{
"message": "I need help with my order #12345",
"sessionId": "session_123",
"userId": "user_456"
}'
总结
这个完整的案例展示了如何将Spring AI、MCP和Dify集成到一个智能客服系统中:
- Spring AI: 提供基础的AI对话能力
- MCP: 通过工具扩展AI的能力(数据库查询、邮件发送等)
- Dify: 处理复杂的业务流程和工作流