最近在做一个客服系统的升级项目,客户那边反馈人工客服压力大,响应慢,成本还高。传统的基于关键词匹配的机器人,稍微复杂点的问题就答非所问,用户体验很差。于是我们团队决定引入AI能力,打造一个真正能“听懂人话”的智能客服。经过一番技术选型和折腾,最终用 Spring BootDeepSeek AI 把系统搭了起来,效果还不错。今天就把整个实践过程整理成笔记,分享给大家。

背景与痛点:为什么需要AI客服?

先说说我们遇到的实际情况。原来的客服系统主要靠人工和简单的规则机器人。

  • 人工客服:成本高,培训周期长,而且面对大量重复性问题(比如“怎么退货”、“快递到哪了”),人力浪费严重。高峰期排队时间长,用户满意度直线下降。
  • 规则机器人:基于关键词匹配,逻辑僵硬。比如用户问“我的包裹还没到,怎么回事?”,如果关键词库没有“包裹没到”的精确匹配,可能就触发不了正确的回答流程。更别提理解上下文了,用户多问两句,机器人就跟不上了。

而AI客服的核心优势在于自然语言理解(NLU)。它不需要精确的关键词,能理解用户语句的意图和上下文。比如用户先说“我想买手机”,接着问“有红色的吗?”,AI能结合上下文知道是在问“红色的手机”。这种体验的提升是质的飞跃。

技术选型:为什么是DeepSeek?

市面上提供NLP能力的方案很多,我们主要对比了几个方向:

  1. 自研模型:效果最好,可控性强,但需要专业的算法团队、大量的标注数据和昂贵的算力,对于我们中小型团队来说,门槛太高,周期太长。
  2. 国内大厂云服务:如百度UNIT、阿里云智能对话机器人。开箱即用,有可视化配置后台,初期上手快。但定制能力相对受限,费用模型复杂(调用量、QPS都可能计费),长期来看成本和控制力是个问题。
  3. 开源模型本地部署:比如ChatGLM、Qwen。数据隐私有保障,可深度定制。但对服务器资源(尤其是GPU)要求高,推理速度优化、模型维护都需要投入额外精力。
  4. DeepSeek等API服务:提供了强大的通用或专用模型通过API调用。平衡了效果、成本和控制力。

我们最终选择 DeepSeek 主要是基于以下几点考虑:

  • 效果与成本平衡:DeepSeek的模型在中文理解和生成上表现非常出色,性价比高。API调用按Token计费,对于我们这种对话量可预估的场景,成本清晰可控。
  • 开发友好:提供标准的RESTful API,文档清晰,集成到Spring Boot项目中非常简单,几行代码就能完成一次对话。
  • 灵活性强:虽然不像本地部署那样完全自主,但通过Prompt工程、上下文管理和后续可能的微调(fine-tuning),已经能满足我们绝大部分定制化需求,比如让客服语气更亲切、更了解我们的产品知识。

对于我们这个项目来说,核心目标是快速验证AI客服的效果并稳定上线,DeepSeek API方案是最佳路径。

技术选型对比图

核心实现:三步搭建智能客服骨架

1. Spring Boot项目初始化配置

首先,创建一个标准的Spring Boot项目。我们用的是Spring Boot 2.7.x 和 Java 11。

<!-- pom.xml 关键依赖 -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 用于配置管理 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <!-- HTTP客户端,用于调用DeepSeek API -->
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
        <version>4.5.13</version>
    </dependency>
    <!-- 用于JSON处理 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
    <!-- 后续缓存、异步等依赖按需引入 -->
</dependencies>

application.yml 中配置基础信息和DeepSeek的API密钥(切记不要硬编码在代码里)。

app:
  deepseek:
    api-key: ${DEEPSEEK_API_KEY:your-api-key-here} # 推荐从环境变量读取
    base-url: https://api.deepseek.com
    chat-completions-path: /v1/chat/completions
    model: deepseek-chat # 根据需求选择模型
    timeout: 10000 # 超时时间10秒

server:
  port: 8080

2. DeepSeek API集成与封装

这是最核心的一步。我们封装了一个服务类 DeepSeekService 来处理与AI的通信。

首先,定义请求和响应的DTO(数据传输对象),严格遵循DeepSeek API文档的格式。

// ChatRequest.java
@Data
@Builder
public class ChatRequest {
    private String model;
    private List<Message> messages;
    private Double temperature; // 控制回复随机性,客服场景建议较低,如0.7
    private Integer maxTokens; // 限制回复长度

    @Data
    @Builder
    public static class Message {
        private String role; // "system", "user", "assistant"
        private String content;
    }
}

// ChatResponse.java
@Data
public class ChatResponse {
    private String id;
    private String object;
    private Long created;
    private String model;
    private List<Choice> choices;
    private Usage usage;

    @Data
    public static class Choice {
        private Message message;
        private Integer index;
        private String finishReason;
    }

    @Data
    public static class Message {
        private String role;
        private String content;
    }

    @Data
    public static class Usage {
        private Integer promptTokens;
        private Integer completionTokens;
        private Integer totalTokens;
    }
}

然后,实现服务类。这里使用 RestTemplateWebClient(响应式)进行HTTP调用。我们先用 RestTemplate 演示。

@Service
@Slf4j
public class DeepSeekService {

    @Value("${app.deepseek.api-key}")
    private String apiKey;

    @Value("${app.deepseek.base-url}")
    private String baseUrl;

    @Value("${app.deepseek.chat-completions-path}")
    private String chatPath;

    @Value("${app.deepseek.model}")
    private String model;

    private final RestTemplate restTemplate;

    public DeepSeekService(RestTemplateBuilder builder) {
        this.restTemplate = builder
                .setConnectTimeout(Duration.ofSeconds(5))
                .setReadTimeout(Duration.ofSeconds(10))
                .build();
    }

    public String chat(String userMessage, String sessionId) {
        // 1. 构建请求消息列表。可以从缓存或DB中取出该sessionId的历史对话作为上下文
        List<ChatRequest.Message> messages = buildMessagesWithContext(userMessage, sessionId);

        ChatRequest request = ChatRequest.builder()
                .model(model)
                .messages(messages)
                .temperature(0.7)
                .maxTokens(500)
                .build();

        // 2. 设置请求头(鉴权)
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setBearerAuth(apiKey); // 使用Bearer Token认证

        HttpEntity<ChatRequest> entity = new HttpEntity<>(request, headers);

        // 3. 发送请求
        String url = baseUrl + chatPath;
        try {
            ResponseEntity<ChatResponse> response = restTemplate.exchange(
                    url,
                    HttpMethod.POST,
                    entity,
                    ChatResponse.class
            );

            if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
                String aiReply = response.getBody().getChoices().get(0).getMessage().getContent();
                // 4. 保存本次对话到上下文(用于下一次对话)
                saveContext(sessionId, userMessage, aiReply);
                return aiReply;
            } else {
                log.error("DeepSeek API调用失败,状态码: {}", response.getStatusCode());
                return "抱歉,AI助手暂时无法响应,请稍后再试。";
            }
        } catch (Exception e) {
            log.error("调用DeepSeek API异常", e);
            return "网络或服务异常,请重试。";
        }
    }

    // 以下为上下文管理相关方法(简化版)
    private List<ChatRequest.Message> buildMessagesWithContext(String userMessage, String sessionId) {
        List<ChatRequest.Message> messages = new ArrayList<>();
        // 添加系统指令,塑造AI角色
        messages.add(ChatRequest.Message.builder()
                .role("system")
                .content("你是一个专业、友好、耐心的电商客服助手。请用简洁明了的中文回答用户关于订单、物流、退换货、产品咨询等问题。如果无法确定答案,请引导用户联系人工客服。")
                .build());

        // 从缓存获取该session的历史对话(例如最近5轮)
        List<ChatRequest.Message> history = getHistoryFromCache(sessionId);
        if (history != null) {
            messages.addAll(history);
        }

        // 加入用户当前问题
        messages.add(ChatRequest.Message.builder()
                .role("user")
                .content(userMessage)
                .build());
        return messages;
    }

    private void saveContext(String sessionId, String userMessage, String aiReply) {
        // 将本轮对话存入缓存,并可能限制总长度(如只保留最近10轮)
        // 实现略,可使用Redis的List结构存储
    }
}

3. 对话状态管理设计

智能客服不是一问一答就结束,需要维持会话上下文。我们设计了一个简单的对话状态管理器

核心思路是:为每个用户会话(sessionId)维护一个对话历史列表。每次用户提问,都将历史对话作为上下文传给AI。同时,为了防止上下文过长导致API费用增加和模型性能下降,我们需要限制历史记录的长度(比如只保留最近10轮对话)。

对话状态管理序列图

类图简化描述

  • ConversationSession: 核心类,包含 sessionIdList<DialogueTurn>
  • DialogueTurn: 记录一轮对话,包含 userMessage, aiResponse, timestamp
  • ConversationManager: 管理所有会话,提供 getSession(sessionId), addTurn(sessionId, turn), trimHistory(sessionId, maxTurns) 等方法。

在实际项目中,ConversationManager 可以基于内存(如Caffeine Cache)或分布式缓存(如Redis)实现,后者更适合集群部署。

@Component
public class ConversationManager {
    // 使用Caffeine缓存,设置过期时间(如30分钟无活动则清除会话)
    private final Cache<String, ConversationSession> sessionCache = Caffeine.newBuilder()
            .expireAfterAccess(30, TimeUnit.MINUTES)
            .maximumSize(10000)
            .build();

    public ConversationSession getOrCreateSession(String sessionId) {
        return sessionCache.get(sessionId, id -> new ConversationSession(id));
    }

    public void addDialogueTurn(String sessionId, String userMsg, String aiResp) {
        ConversationSession session = getOrCreateSession(sessionId);
        session.addTurn(new DialogueTurn(userMsg, aiResp));
        // 修剪历史,只保留最近N轮
        session.trimHistory(10);
    }

    public List<ChatRequest.Message> getRecentHistoryForPrompt(String sessionId, int maxTurns) {
        ConversationSession session = sessionCache.getIfPresent(sessionId);
        if (session == null) {
            return Collections.emptyList();
        }
        return session.getRecentTurns(maxTurns).stream()
                .flatMap(turn -> Stream.of(
                        ChatRequest.Message.builder().role("user").content(turn.getUserMessage()).build(),
                        ChatRequest.Message.builder().role("assistant").content(turn.getAiResponse()).build()
                ))
                .collect(Collectors.toList());
    }
}

性能优化:让客服系统又快又稳

直接调用外部API,在高并发下很容易成为瓶颈。我们做了以下几层优化。

1. 异步处理高并发请求

使用Spring的 @AsyncCompletableFuture 将耗时的AI调用与HTTP请求线程解耦,避免阻塞。

@Service
public class AsyncChatService {
    @Async("taskExecutor") // 需要配置线程池
    public CompletableFuture<String> chatAsync(String message, String sessionId) {
        String reply = deepSeekService.chat(message, sessionId);
        return CompletableFuture.completedFuture(reply);
    }
}

// 在Controller中
@PostMapping("/chat")
public CompletableFuture<ResponseEntity<ApiResponse>> chat(@RequestBody ChatRequestDto request) {
    return asyncChatService.chatAsync(request.getMessage(), request.getSessionId())
            .thenApply(reply -> ResponseEntity.ok(ApiResponse.success(reply)))
            .exceptionally(ex -> ResponseEntity.status(500).body(ApiResponse.error("处理失败")));
}

Benchmark数据:在单机4核8G环境下,同步调用QPS约50(受限于API延迟,约200ms/次)。改为异步后,虽然API延迟不变,但Web容器的线程得以快速释放,系统吞吐量提升至约300 QPS(受限于线程池大小和网络IO)。

2. 缓存常用问答对

很多用户问题高度重复,比如“运费多少?”“退货流程”。每次调用AI既浪费钱又慢。我们可以引入本地缓存(如Caffeine)或分布式缓存(Redis),对高频、标准的问题进行缓存。

@Service
public class CachedChatService {
    private final DeepSeekService deepSeekService;
    // 缓存:Key为问题内容的MD5,Value为答案
    private final Cache<String, String> qaCache = Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(1, TimeUnit.HOURS) // 答案可能更新,设置过期时间
            .build();

    public String chatWithCache(String userMessage, String sessionId) {
        String cacheKey = DigestUtils.md5DigestAsHex(userMessage.getBytes());
        String cachedAnswer = qaCache.getIfPresent(cacheKey);
        if (cachedAnswer != null) {
            log.info("缓存命中:{}", userMessage);
            return cachedAnswer;
        }
        // 缓存未命中,调用AI
        String aiAnswer = deepSeekService.chat(userMessage, sessionId);
        // 判断是否为通用、稳定的答案,如果是则缓存
        if (isCacheableAnswer(userMessage, aiAnswer)) {
            qaCache.put(cacheKey, aiAnswer);
        }
        return aiAnswer;
    }

    private boolean isCacheableAnswer(String question, String answer) {
        // 简单的启发式规则:答案长度适中,不包含“可能”、“或许”等不确定性词汇,且问题为通用问题
        // 更复杂的可以用一个分类模型来判断
        return answer.length() > 10 && answer.length() < 200
                && !answer.contains("可能") && !answer.contains("或许")
                && isGeneralQuestion(question);
    }
}

3. 限流策略实现

防止恶意刷接口或突发流量打垮系统或产生过高API费用。我们使用Guava的 RateLimiter 或 Spring Cloud Gateway/ Sentinel进行限流。

这里展示一个简单的基于IP的令牌桶限流(可在拦截器中实现):

@Component
public class RateLimitInterceptor implements HandlerInterceptor {
    // 每个IP每秒钟限制5次请求
    private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
    private static final double PERMITS_PER_SECOND = 5.0;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String clientIp = getClientIp(request);
        RateLimiter limiter = limiters.computeIfAbsent(clientIp, ip -> RateLimiter.create(PERMITS_PER_SECOND));

        if (limiter.tryAcquire()) {
            return true;
        } else {
            response.setStatus(429); // Too Many Requests
            response.getWriter().write("请求过于频繁,请稍后再试");
            return false;
        }
    }

    private String getClientIp(HttpServletRequest request) {
        // 从请求头中获取真实IP(考虑代理情况)
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

避坑指南:那些我们踩过的“坑”

1. 上下文丢失的预防措施

问题:用户在一个长对话中,突然AI“失忆”了,不记得之前说过什么。 原因:sessionId 生成或传递错误;缓存过期或被意外清除;上下文拼接逻辑有bug。

解决措施

  • 确保sessionId稳定:对于Web应用,可以使用前端生成的UUID(并存储到localStorage)或结合用户登录ID。每次请求必须携带。
  • 上下文长度管理:AI模型有Token限制(如4096)。我们的 ConversationManager 中的 trimHistory 方法至关重要。通常采用两种策略:
    1. 固定轮数:只保留最近N轮对话(如10轮)。
    2. 智能摘要:当对话轮数过多时,调用AI对之前的对话历史生成一个简短摘要,然后用“摘要+最近几轮对话”作为新的上下文。这更高级,但成本也更高。
  • 持久化备份:对于重要的客服对话(尤其是可能转为人工的),将会话历史持久化到数据库,即使缓存丢失也能恢复。

2. 敏感词过滤实现

AI可能生成不受控的内容。必须在返回给用户前进行过滤。

@Component
public class ContentFilter {
    private Set<String> sensitiveWords = new HashSet<>(Arrays.asList("敏感词1", "敏感词2")); // 可从DB加载

    public String filter(String text) {
        if (text == null) return "";
        String filteredText = text;
        for (String word : sensitiveWords) {
            filteredText = filteredText.replaceAll(word, "***");
        }
        // 更复杂的可以用DFA算法提高效率
        return filteredText;
    }

    // 在DeepSeekService返回答案后调用
    public String getSafeReply(String rawReply) {
        return filter(rawReply);
    }
}

3. 模型fine-tuning建议

虽然DeepSeek的通用模型很强,但要让客服更“懂”你的业务,可以考虑微调。

  • 何时需要微调:当通用模型在特定领域(如你的产品型号、内部流程术语)上表现不佳时;当你希望客服回复具有固定格式或风格时。
  • 准备数据:收集历史的优秀客服对话记录(Q-A对),至少需要几百条。数据质量很重要,回答需准确、专业、友好。
  • 微调步骤:按照DeepSeek官方文档,将数据整理成特定的JSONL格式,上传并进行微调作业。完成后会得到一个专属的模型ID。
  • 成本与评估:微调有一次性成本,并且调用微调后的模型可能更贵。上线前需进行充分的测试,评估效果提升是否值得付出成本。

生产部署:容器化与监控

开发完了,怎么上线?

1. 容器化方案

使用Docker + Docker Compose(单机)或 Kubernetes(集群)。

# Dockerfile
FROM openjdk:11-jre-slim
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
# docker-compose.yml
version: '3.8'
services:
  smart-customer-service:
    build: .
    container_name: ai-customer-service
    ports:
      - "8080:8080"
    environment:
      - DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY}
      - SPRING_REDIS_HOST=redis
      - SPRING_PROFILES_ACTIVE=prod
    depends_on:
      - redis
    networks:
      - app-network

  redis:
    image: redis:alpine
    container_name: cache-redis
    ports:
      - "6379:6379"
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

2. 关键监控指标配置

系统上线后,必须关注以下指标:

  • 应用性能:使用Spring Boot Actuator + Prometheus + Grafana。
    • http.server.requests.duration:API响应时间,重点关注P95和P99。
    • system.cpu.usagejvm.memory.used:资源使用率。
    • custom.deepseek.api.latency:自定义指标,记录调用DeepSeek API的耗时。
  • 业务指标
    • 问答成功率:AI回复被用户认为有效的比例(可通过后续的“是否解决”按钮埋点计算)。
    • 转人工率:用户请求转接人工客服的比率,过高可能意味着AI解决能力不足。
    • 平均对话轮数:衡量AI维持对话的能力。
  • 成本监控
    • 每日/每月Token消耗:通过DeepSeek API返回的 usage 字段统计,密切监控,防止意外费用。
    • 缓存命中率:评估缓存效果,命中率越高,成本节省越多。

总结与延伸思考

经过这一轮开发,我们的智能客服系统已经能处理70%以上的常见咨询,人工客服压力大大减轻,用户等待时间从平均几分钟降到秒级。整个技术栈(Spring Boot + DeepSeek)的选择让我们在效果、开发效率和成本之间取得了很好的平衡。

当然,这只是一个起点。智能客服还有很多可以深挖的方向:

  1. 多模态扩展:现在的客服是纯文本的。如果用户上传一张商品损坏的图片,AI能否识别并给出处理建议?未来可以考虑集成视觉模型,实现“图文并茂”的客服。
  2. 情感分析与主动关怀:能否通过分析用户语句的情感(焦急、愤怒、满意),让AI调整回复语气,甚至在识别到用户非常不满时主动提示转人工?这需要更精细的NLU能力。
  3. 与业务系统深度集成:现在的AI更多是“问答”,未来能否让它“办事”?比如用户说“帮我把订单123456退货”,AI在确认后,能否通过调用内部订单系统的API,真正创建一条退货工单?这需要将AI作为“大脑”,与后端的各个业务系统(OMS, CRM等)通过API打通,实现真正的自动化流程。

希望这篇笔记能给你带来一些启发。AI应用开发并不神秘,关键是想清楚场景,选对工具,然后一步步去实现和优化。如果你也在做类似的项目,欢迎一起交流探讨。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐