ChatGLM-6B Java开发实战:SpringBoot微服务集成指南

1. 为什么选择Java与ChatGLM-6B的组合

在企业级AI应用开发中,Java生态的稳定性、成熟度和庞大的开发者基础让它成为后端服务的首选语言。而ChatGLM-6B作为一款开源的双语对话模型,凭借其对中文场景的深度优化和相对轻量的部署需求,正逐渐成为国内开发者构建智能服务的重要选择。

但现实情况是,大多数ChatGLM-6B的教程都聚焦在Python环境下的直接调用,这让习惯于Java SpringBoot体系的工程师们面临一个实际问题:如何让这个强大的AI能力无缝融入现有的微服务架构?不是简单地写个HTTP客户端调用,而是要真正把它当作一个可管理、可监控、可扩展的服务组件。

我最近在一个电商客服系统升级项目中就遇到了这个问题。团队已经用SpringBoot构建了完整的订单、用户、商品服务,现在需要为客服坐席增加智能辅助功能——实时生成回复建议、自动总结对话要点、识别客户情绪。我们试过几种方案:用Python单独部署API服务再通过RestTemplate调用,结果发现网络延迟不稳定;也尝试过Jython嵌入,但性能和兼容性都不理想。最终我们找到了一条更稳妥的路径:将ChatGLM-6B作为独立推理服务运行,然后在SpringBoot中构建一套健壮的客户端封装,配合合理的缓存、降级和监控机制。

这条路并不复杂,但需要关注几个关键点:服务通信的可靠性、大模型响应的异步处理、资源使用的合理控制,以及最重要的——如何让业务代码完全感知不到底层是AI还是传统逻辑。这篇文章会带你一步步实现这个目标,从零开始搭建一个生产可用的集成方案。

2. 构建ChatGLM-6B推理服务

2.1 服务部署准备

ChatGLM-6B的官方仓库提供了开箱即用的API服务脚本,但直接使用需要做一些适配。首先明确我们的部署目标:一个稳定、可监控、支持基本认证的HTTP服务,而不是仅供本地测试的demo。

根据硬件条件选择合适的部署方式:

  • GPU服务器(推荐):至少8GB显存,如NVIDIA T4或RTX 3090。这是最理想的配置,能保证良好的响应速度。
  • CPU服务器:需要32GB以上内存,适合测试和低并发场景。虽然速度慢些,但成本更低,且避免了GPU驱动的兼容性问题。
  • 云服务实例:阿里云ECS的gn7i系列、腾讯云的GN10X系列都是不错的选择,预装了CUDA环境,省去大量配置时间。

无论哪种方式,都需要先准备好基础环境:

# 创建工作目录
mkdir -p /opt/chatglm-service
cd /opt/chatglm-service

# 克隆官方仓库(注意使用稳定版本)
git clone https://github.com/THUDM/ChatGLM-6B.git
cd ChatGLM-6B

# 安装依赖(推荐使用虚拟环境隔离)
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt

# 安装FastAPI和Uvicorn(API服务必需)
pip install fastapi uvicorn python-multipart

2.2 定制化API服务

官方的api.py脚本功能完整但过于简单,缺少生产环境必需的特性。我们需要创建一个增强版的服务入口文件production_api.py

# production_api.py
import os
import time
import logging
from fastapi import FastAPI, HTTPException, Request, BackgroundTasks
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
from transformers import AutoTokenizer, AutoModel
import torch
import gc

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('/var/log/chatglm-service/app.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

app = FastAPI(
    title="ChatGLM-6B Production API",
    description="企业级ChatGLM-6B推理服务,支持认证、限流和健康检查",
    version="1.0.0"
)

# 模型加载配置
MODEL_PATH = os.getenv("MODEL_PATH", "/opt/chatglm-model")
QUANTIZE_LEVEL = os.getenv("QUANTIZE_LEVEL", "int4")  # int4, int8, fp16
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"

# 全局模型和分词器实例
tokenizer = None
model = None

@app.on_event("startup")
async def startup_event():
    """服务启动时加载模型"""
    global tokenizer, model
    
    logger.info(f"开始加载ChatGLM-6B模型,路径: {MODEL_PATH}, 量化级别: {QUANTIZE_LEVEL}, 设备: {DEVICE}")
    
    try:
        # 加载分词器
        tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)
        
        # 根据量化级别加载模型
        if QUANTIZE_LEVEL == "fp16":
            model = AutoModel.from_pretrained(MODEL_PATH, trust_remote_code=True).half()
        elif QUANTIZE_LEVEL == "int8":
            model = AutoModel.from_pretrained(MODEL_PATH, trust_remote_code=True).quantize(8).half()
        else:  # int4
            model = AutoModel.from_pretrained(MODEL_PATH, trust_remote_code=True).quantize(4).half()
        
        # 移动到指定设备
        if DEVICE == "cuda":
            model = model.cuda()
        else:
            model = model.float()  # CPU上使用float精度
        
        model = model.eval()
        logger.info("模型加载成功")
        
    except Exception as e:
        logger.error(f"模型加载失败: {str(e)}")
        raise RuntimeError(f"模型加载失败: {str(e)}")

@app.on_event("shutdown")
async def shutdown_event():
    """服务关闭时清理资源"""
    global model, tokenizer
    if model is not None:
        del model
    if tokenizer is not None:
        del tokenizer
    gc.collect()
    if torch.cuda.is_available():
        torch.cuda.empty_cache()
    logger.info("服务已关闭,资源已释放")

# 请求数据模型
class ChatRequest(BaseModel):
    prompt: str
    history: Optional[List[List[str]]] = None
    max_length: int = 2048
    top_p: float = 0.8
    temperature: float = 0.95
    repetition_penalty: float = 1.1

class ChatResponse(BaseModel):
    response: str
    history: List[List[str]]
    status: int = 200
    time: str
    model_info: Dict[str, Any]

@app.post("/chat", response_model=ChatResponse)
async def chat_endpoint(request: ChatRequest, background_tasks: BackgroundTasks):
    """主聊天接口"""
    start_time = time.time()
    
    try:
        # 输入验证
        if not request.prompt.strip():
            raise HTTPException(status_code=400, detail="prompt不能为空")
        
        # 调用模型生成
        if request.history is None:
            request.history = []
            
        response, history = model.chat(
            tokenizer,
            request.prompt,
            history=request.history,
            max_length=request.max_length,
            top_p=request.top_p,
            temperature=request.temperature,
            repetition_penalty=request.repetition_penalty
        )
        
        # 计算耗时
        elapsed = time.time() - start_time
        logger.info(f"请求处理完成,耗时: {elapsed:.2f}s, 输入长度: {len(request.prompt)}")
        
        return {
            "response": response,
            "history": history,
            "status": 200,
            "time": time.strftime("%Y-%m-%d %H:%M:%S"),
            "model_info": {
                "device": DEVICE,
                "quantize_level": QUANTIZE_LEVEL,
                "response_time_ms": int(elapsed * 1000)
            }
        }
        
    except torch.cuda.OutOfMemoryError:
        logger.error("GPU内存不足")
        raise HTTPException(status_code=503, detail="服务暂时不可用,请稍后重试")
    except Exception as e:
        logger.error(f"处理请求时发生错误: {str(e)}")
        raise HTTPException(status_code=500, detail=f"内部服务器错误: {str(e)}")

@app.get("/health")
async def health_check():
    """健康检查端点"""
    return {
        "status": "healthy",
        "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
        "device": DEVICE,
        "model_loaded": model is not None
    }

@app.get("/info")
async def model_info():
    """模型信息端点"""
    if model is None:
        raise HTTPException(status_code=503, detail="模型未加载")
    
    return {
        "model_name": "ChatGLM-6B",
        "quantize_level": QUANTIZE_LEVEL,
        "device": DEVICE,
        "torch_version": torch.__version__,
        "transformers_version": "4.27.1"
    }

2.3 启动与配置服务

创建一个简单的启动脚本start_service.sh

#!/bin/bash
# start_service.sh

# 设置环境变量
export MODEL_PATH="/opt/chatglm-model"
export QUANTIZE_LEVEL="int4"
export CUDA_VISIBLE_DEVICES=0

# 启动服务
nohup uvicorn production_api:app \
    --host 0.0.0.0 \
    --port 8000 \
    --workers 2 \
    --reload \
    --log-level info \
    > /var/log/chatglm-service/service.log 2>&1 &

echo "ChatGLM-6B服务已启动,PID: $!"
echo "日志文件: /var/log/chatglm-service/service.log"

为了确保服务的稳定性,建议使用systemd进行管理:

# /etc/systemd/system/chatglm-service.service
[Unit]
Description=ChatGLM-6B Inference Service
After=network.target

[Service]
Type=simple
User=chatglm
WorkingDirectory=/opt/chatglm-service/ChatGLM-6B
Environment="MODEL_PATH=/opt/chatglm-model"
Environment="QUANTIZE_LEVEL=int4"
ExecStart=/usr/bin/uvicorn production_api:app --host 0.0.0.0 --port 8000 --workers 2 --log-level info
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=chatglm-service

[Install]
WantedBy=multi-user.target

启用并启动服务:

sudo systemctl daemon-reload
sudo systemctl enable chatglm-service
sudo systemctl start chatglm-service
sudo systemctl status chatglm-service

2.4 模型下载与优化

ChatGLM-6B模型文件较大(约13GB),直接从Hugging Face下载可能较慢。推荐使用ModelScope镜像:

# 创建模型目录
sudo mkdir -p /opt/chatglm-model
sudo chown chatglm:chatglm /opt/chatglm-model

# 使用ModelScope下载(更快)
pip install modelscope
python -c "
from modelscope.pipelines import pipeline
from modelscope.utils.constant import Tasks
from modelscope.models import Model
model = Model.from_pretrained('ZhipuAI/ChatGLM-6B', revision='v1.0.16')
model.save_pretrained('/opt/chatglm-model')
"

如果显存有限,可以使用量化版本:

# 下载INT4量化模型(约5.2GB)
git clone https://www.modelscope.cn/ZhipuAI/ChatGLM-6B-int4.git /opt/chatglm-model-int4
export MODEL_PATH="/opt/chatglm-model-int4"
export QUANTIZE_LEVEL="int4"

3. SpringBoot客户端封装设计

3.1 项目结构与依赖配置

在SpringBoot项目中,我们不希望业务代码直接与HTTP客户端打交道。最佳实践是创建一个专门的AI服务模块,提供清晰的业务接口。项目结构如下:

chatglm-springboot/
├── pom.xml
├── src/
│   ├── main/
│   │   ├── java/com/example/chatglm/
│   │   │   ├── ChatGlmAutoConfiguration.java     # 自动配置类
│   │   │   ├── ChatGlmClient.java               # 核心客户端
│   │   │   ├── ChatGlmProperties.java           # 配置属性
│   │   │   ├── dto/
│   │   │   │   ├── ChatRequest.java
│   │   │   │   ├── ChatResponse.java
│   │   │   │   └── ChatHistoryItem.java
│   │   │   └── exception/
│   │   │       ├── ChatGlmException.java
│   │   │       └── ChatGlmTimeoutException.java
│   │   └── resources/
│   │       └── application.yml

核心依赖配置(pom.xml):

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- Spring Boot Actuator (监控) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    
    <!-- HTTP客户端 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    
    <!-- JSON处理 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
    
    <!-- 连接池优化 -->
    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-resolver-dns</artifactId>
    </dependency>
    
    <!-- 熔断与限流 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
    </dependency>
    
    <!-- Lombok (简化代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

3.2 配置属性与自动装配

创建配置类ChatGlmProperties.java

package com.example.chatglm;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Positive;
import java.time.Duration;

@Data
@ConfigurationProperties(prefix = "chatglm.client")
@Validated
public class ChatGlmProperties {
    /**
     * ChatGLM服务基础URL,如 http://localhost:8000
     */
    @NotBlank
    private String baseUrl;
    
    /**
     * 连接超时时间
     */
    @Positive
    private Duration connectTimeout = Duration.ofSeconds(5);
    
    /**
     * 读取超时时间
     */
    @Positive
    private Duration readTimeout = Duration.ofSeconds(30);
    
    /**
     * 写入超时时间
     */
    @Positive
    private Duration writeTimeout = Duration.ofSeconds(30);
    
    /**
     * 最大连接数
     */
    @Positive
    private int maxConnections = 100;
    
    /**
     * 每个路由的最大连接数
     */
    @Positive
    private int maxConnectionsPerRoute = 20;
    
    /**
     * 连接空闲时间
     */
    @Positive
    private Duration connectionIdleTime = Duration.ofMinutes(5);
    
    /**
     * 是否启用熔断
     */
    private boolean circuitBreakerEnabled = true;
    
    /**
     * 熔断失败阈值(百分比)
     */
    private int failureRateThreshold = 50;
    
    /**
     * 熔断等待时间
     */
    private Duration waitDurationInOpenState = Duration.ofSeconds(60);
    
    /**
     * 滑动窗口大小(秒)
     */
    private Duration slidingWindowSize = Duration.ofSeconds(60);
    
    /**
     * 滑动窗口内最小请求数
     */
    private int minimumNumberOfCalls = 10;
}

自动配置类ChatGlmAutoConfiguration.java

package com.example.chatglm;

import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import io.github.resilience4j.timelimiter.TimeLimiterRegistry;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;

import java.time.Duration;

@Configuration
@EnableConfigurationProperties(ChatGlmProperties.class)
public class ChatGlmAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public WebClient chatGlmWebClient(ChatGlmProperties properties) {
        // 配置HTTP客户端
        HttpClient httpClient = HttpClient.create()
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 
                        Math.toIntExact(properties.getConnectTimeout().toMillis()))
                .responseTimeout(properties.getReadTimeout())
                .doOnConnected(conn -> conn
                        .addHandlerLast(new ReadTimeoutHandler(
                                properties.getReadTimeout().toMillis(), TimeUnit.MILLISECONDS))
                        .addHandlerLast(new WriteTimeoutHandler(
                                properties.getWriteTimeout().toMillis(), TimeUnit.MILLISECONDS)));

        return WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }

    @Bean
    @ConditionalOnMissingBean
    public CircuitBreakerRegistry circuitBreakerRegistry(ChatGlmProperties properties) {
        if (!properties.isCircuitBreakerEnabled()) {
            return CircuitBreakerRegistry.ofDefaults();
        }

        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
                .failureRateThreshold(properties.getFailureRateThreshold())
                .waitDurationInOpenState(properties.getWaitDurationInOpenState())
                .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.TIME_BASED)
                .slidingWindowSize(Math.toIntExact(properties.getSlidingWindowSize().getSeconds()))
                .minimumNumberOfCalls(properties.getMinimumNumberOfCalls())
                .build();

        return CircuitBreakerRegistry.of(config);
    }

    @Bean
    @ConditionalOnMissingBean
    public TimeLimiterRegistry timeLimiterRegistry(ChatGlmProperties properties) {
        TimeLimiterConfig config = TimeLimiterConfig.custom()
                .timeoutDuration(properties.getReadTimeout())
                .cancelRunningFuture(true)
                .build();

        return TimeLimiterRegistry.of(config);
    }

    @Bean
    @ConditionalOnMissingBean
    public ChatGlmClient chatGlmClient(
            @Qualifier("chatGlmWebClient") WebClient webClient,
            ChatGlmProperties properties,
            CircuitBreakerRegistry circuitBreakerRegistry,
            TimeLimiterRegistry timeLimiterRegistry) {
        return new ChatGlmClient(webClient, properties, circuitBreakerRegistry, timeLimiterRegistry);
    }
}

3.3 核心客户端实现

ChatGlmClient.java是整个集成的核心,它封装了所有与ChatGLM服务交互的细节:

package com.example.chatglm;

import com.example.chatglm.dto.*;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.timelimiter.TimeLimiter;
import io.github.resilience4j.timelimiter.TimeLimiterRegistry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;

import java.time.Duration;
import java.util.List;
import java.util.Optional;

@Slf4j
@Service
@RequiredArgsConstructor
public class ChatGlmClient {

    private final WebClient webClient;
    private final ChatGlmProperties properties;
    private final CircuitBreakerRegistry circuitBreakerRegistry;
    private final TimeLimiterRegistry timeLimiterRegistry;

    /**
     * 发送聊天请求
     */
    public Mono<ChatResponse> chat(String prompt) {
        return chat(prompt, null);
    }

    /**
     * 发送带历史记录的聊天请求
     */
    public Mono<ChatResponse> chat(String prompt, List<ChatHistoryItem> history) {
        ChatRequest request = ChatRequest.builder()
                .prompt(prompt)
                .history(history)
                .build();

        return executeRequest(request);
    }

    /**
     * 执行请求的通用方法
     */
    private Mono<ChatResponse> executeRequest(ChatRequest request) {
        String url = properties.getBaseUrl() + "/chat";

        // 获取熔断器和超时器
        CircuitBreaker circuitBreaker = circuitBreakerRegistry
                .circuitBreaker("chatglm", () -> {
                    log.warn("ChatGLM服务熔断开启");
                    return Mono.error(new ChatGlmException("ChatGLM服务暂时不可用"));
                });

        TimeLimiter timeLimiter = timeLimiterRegistry.timeLimiter("chatglm");

        return webClient.post()
                .uri(url)
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(request)
                .retrieve()
                .bodyToMono(ChatResponse.class)
                .transformDeferred(it -> Mono
                        .transform(it, circuitBreaker)
                        .transformDeferred(timeLimiter::decorateMono)
                        .onErrorResume(throwable -> {
                            log.error("ChatGLM请求失败", throwable);
                            return Mono.error(new ChatGlmException("ChatGLM请求失败", throwable));
                        }))
                .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
                        .filter(throwable -> throwable instanceof ChatGlmTimeoutException));
    }

    /**
     * 健康检查
     */
    public Mono<Boolean> healthCheck() {
        String url = properties.getBaseUrl() + "/health";
        
        return webClient.get()
                .uri(url)
                .retrieve()
                .bodyToMono(String.class)
                .map(response -> response.contains("healthy"))
                .onErrorResume(throwable -> {
                    log.warn("健康检查失败", throwable);
                    return Mono.just(false);
                });
    }

    /**
     * 获取模型信息
     */
    public Mono<ModelInfo> getModelInfo() {
        String url = properties.getBaseUrl() + "/info";
        
        return webClient.get()
                .uri(url)
                .retrieve()
                .bodyToMono(ModelInfo.class)
                .onErrorResume(throwable -> {
                    log.warn("获取模型信息失败", throwable);
                    return Mono.empty();
                });
    }
}

DTO类定义(简化版):

// ChatRequest.java
package com.example.chatglm.dto;

import lombok.Builder;
import lombok.Data;

import java.util.List;

@Data
@Builder
public class ChatRequest {
    private String prompt;
    private List<List<String>> history;
    private Integer max_length;
    private Double top_p;
    private Double temperature;
    private Double repetition_penalty;
}

// ChatResponse.java
package com.example.chatglm.dto;

import lombok.Builder;
import lombok.Data;

import java.util.List;
import java.util.Map;

@Data
@Builder
public class ChatResponse {
    private String response;
    private List<List<String>> history;
    private Integer status;
    private String time;
    private Map<String, Object> model_info;
}

// ChatHistoryItem.java
package com.example.chatglm.dto;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class ChatHistoryItem {
    private String user;
    private String bot;
}

// ModelInfo.java
package com.example.chatglm.dto;

import lombok.Data;

import java.util.Map;

@Data
public class ModelInfo {
    private String model_name;
    private String quantize_level;
    private String device;
    private String torch_version;
    private String transformers_version;
}

3.4 异常处理与重试策略

创建专门的异常类来区分不同类型的错误:

package com.example.chatglm.exception;

public class ChatGlmException extends RuntimeException {
    public ChatGlmException(String message) {
        super(message);
    }
    
    public ChatGlmException(String message, Throwable cause) {
        super(message, cause);
    }
}

package com.example.chatglm.exception;

public class ChatGlmTimeoutException extends ChatGlmException {
    public ChatGlmTimeoutException(String message) {
        super(message);
    }
    
    public ChatGlmTimeoutException(String message, Throwable cause) {
        super(message, cause);
    }
}

ChatGlmClient中,我们已经集成了Resilience4j的熔断和超时机制。但还需要处理一些特定场景:

  • 网络超时:当请求超过设定时间仍未返回时,抛出ChatGlmTimeoutException
  • 服务不可用:当ChatGLM服务完全宕机时,熔断器会自动打开,返回友好的错误信息
  • 模型响应异常:当ChatGLM服务返回非200状态码时,转换为适当的异常

为了进一步提升用户体验,可以在业务层添加缓存策略。例如,对于常见的FAQ问题,可以使用Caffeine缓存:

// 在配置类中添加缓存配置
@Bean
public Cache<String, String> chatGlmCache() {
    return Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(Duration.ofMinutes(10))
            .recordStats()
            .build();
}

// 在业务服务中使用
@Service
public class ChatService {
    
    @Autowired
    private ChatGlmClient chatGlmClient;
    
    @Autowired
    private Cache<String, String> chatGlmCache;
    
    public Mono<String> getAnswer(String question) {
        // 先查缓存
        String cached = chatGlmCache.getIfPresent(question);
        if (cached != null) {
            return Mono.just(cached);
        }
        
        // 缓存未命中,调用AI服务
        return chatGlmClient.chat(question)
                .map(ChatResponse::getResponse)
                .doOnSuccess(answer -> {
                    // 将结果放入缓存
                    chatGlmCache.put(question, answer);
                })
                .onErrorResume(throwable -> {
                    // 如果AI服务失败,返回默认回答
                    log.warn("AI服务调用失败,返回默认回答", throwable);
                    return Mono.just("抱歉,我现在无法回答这个问题,请稍后再试。");
                });
    }
}

4. 实战应用:电商客服智能辅助系统

4.1 场景分析与需求拆解

让我们通过一个真实的电商客服场景来展示如何将ChatGLM-6B集成到业务系统中。假设我们正在为一家大型电商平台开发客服坐席辅助系统,核心需求包括:

  • 实时回复建议:当客户发送消息时,自动生成3-5个专业、得体的回复选项
  • 对话摘要:在对话结束后,自动生成一段简洁的摘要,便于后续跟进
  • 情绪识别:识别客户的情绪倾向(愤怒、焦虑、满意等),帮助坐席调整沟通策略
  • 知识库检索:根据客户问题,从内部知识库中检索相关信息

这些需求看似复杂,但通过合理的Prompt工程和API调用组合,完全可以由ChatGLM-6B实现。

4.2 回复建议功能实现

创建ReplySuggestionService.java

package com.example.chatglm.service;

import com.example.chatglm.ChatGlmClient;
import com.example.chatglm.dto.*;
import com.example.chatglm.exception.ChatGlmException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class ReplySuggestionService {

    private final ChatGlmClient chatGlmClient;

    /**
     * 生成多个回复建议
     */
    public Flux<String> generateReplySuggestions(String customerMessage, String productInfo) {
        // 构建提示词模板
        String prompt = buildReplyPrompt(customerMessage, productInfo);
        
        return chatGlmClient.chat(prompt)
                .flatMapMany(response -> {
                    String rawResponse = response.getResponse();
                    // 解析ChatGLM返回的多个建议(格式:1. xxx\n2. yyy\n3. zzz)
                    return Flux.fromIterable(parseSuggestions(rawResponse));
                })
                .onErrorResume(throwable -> {
                    log.error("生成回复建议失败", throwable);
                    return Flux.just("您好,感谢您的咨询,请稍等,我马上为您查询相关信息。");
                });
    }

    /**
     * 构建回复建议的Prompt
     */
    private String buildReplyPrompt(String customerMessage, String productInfo) {
        return String.format(
                "你是一名专业的电商客服人员,正在处理客户咨询。请根据以下信息,生成3个专业、礼貌、简洁的回复建议。\n\n" +
                "【客户消息】\n%s\n\n" +
                "【商品信息】\n%s\n\n" +
                "要求:\n" +
                "1. 每个建议不超过30个字\n" +
                "2. 使用中文,语气友好专业\n" +
                "3. 不要包含任何解释性文字,只输出建议本身\n" +
                "4. 按照数字编号格式输出,如:1. xxx\n2. yyy\n3. zzz\n\n" +
                "请开始生成:",
                customerMessage,
                productInfo.isEmpty() ? "无相关信息" : productInfo
        );
    }

    /**
     * 解析ChatGLM返回的多个建议
     */
    private List<String> parseSuggestions(String rawResponse) {
        List<String> suggestions = new ArrayList<>();
        
        // 按行分割
        String[] lines = rawResponse.split("\\n");
        for (String line : lines) {
            line = line.trim();
            if (line.isEmpty()) continue;
            
            // 匹配数字编号格式:1. xxx 或 1) xxx
            if (line.matches("^\\d+[\\.\\)]\\s+.+")) {
                // 提取编号后的文本
                String suggestion = line.replaceFirst("^\\d+[\\.\\)]\\s*", "").trim();
                if (!suggestion.isEmpty()) {
                    suggestions.add(suggestion);
                }
            }
        }
        
        // 如果解析失败,返回默认建议
        if (suggestions.isEmpty()) {
            suggestions.add("您好,感谢您的咨询!");
            suggestions.add("请问有什么我可以帮您的吗?");
            suggestions.add("稍等,我为您查询一下相关信息。");
        }
        
        return suggestions;
    }
}

4.3 对话摘要与情绪识别

创建ConversationSummaryService.java

package com.example.chatglm.service;

import com.example.chatglm.ChatGlmClient;
import com.example.chatglm.dto.ChatRequest;
import com.example.chatglm.dto.ChatResponse;
import com.example.chatglm.exception.ChatGlmException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.Map;

@Slf4j
@Service
@RequiredArgsConstructor
public class ConversationSummaryService {

    private final ChatGlmClient chatGlmClient;

    /**
     * 生成对话摘要
     */
    public Mono<String> generateSummary(List<String> messages) {
        String conversationText = String.join("\n", messages);
        String prompt = buildSummaryPrompt(conversationText);
        
        return chatGlmClient.chat(prompt)
                .map(ChatResponse::getResponse)
                .onErrorResume(throwable -> {
                    log.error("生成对话摘要失败", throwable);
                    return Mono.just("对话摘要生成失败");
                });
    }

    /**
     * 识别客户情绪
     */
    public Mono<String> detectEmotion(String customerMessage) {
        String prompt = buildEmotionPrompt(customerMessage);
        
        return chatGlmClient.chat(prompt)
                .map(ChatResponse::getResponse)
                .onErrorResume(throwable -> {
                    log.error("情绪识别失败", throwable);
                    return Mono.just("未知");
                });
    }

    private String buildSummaryPrompt(String conversationText) {
        return String.format(
                "请对以下客服对话内容进行简洁摘要,要求:\n" +
                "1. 总结客户的主要问题和诉求\n" +
                "2. 总结客服的解决方案或承诺\n" +
                "3. 字数控制在100字以内\n" +
                "4. 使用客观、专业的语言\n\n" +
                "【对话内容】\n%s\n\n" +
                "摘要:",
                conversationText
        );
    }

    private String buildEmotionPrompt(String customerMessage) {
        return String.format(
                "请分析以下客户消息的情绪倾向,从以下选项中选择最符合的一个:\n" +
                "愤怒、焦虑、失望、满意、感激、困惑、中性\n\n" +
                "要求:\n" +
                "1. 只输出一个词,不要任何解释\n" +
                "2. 如果无法确定,输出'中性'\n\n" +
                "【客户消息】\n%s\n\n" +
                "情绪:",
                customerMessage
        );
    }
}

4.4 完整的客服辅助控制器

创建REST控制器暴露这些功能:

package com.example.chatglm.controller;

import com.example.chatglm.dto.*;
import com.example.chatglm.exception.ChatGlmException;
import com.example.chatglm.service.ConversationSummaryService;
import com.example.chatglm.service.ReplySuggestionService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/v1/chatglm")
@Slf4j
@RequiredArgsConstructor
public class ChatGlmController {

    private final ReplySuggestionService replySuggestionService;
    private final ConversationSummaryService conversationSummaryService;

    /**
     * 获取回复建议
     */
    @PostMapping("/suggestions")
    public ResponseEntity<?> getReplySuggestions(
            @RequestBody ReplySuggestionRequest request) {
        
        try {
            return ResponseEntity.ok(replySuggestionService
                    .generateReplySuggestions(request.getCustomerMessage(), request.getProductInfo())
                    .collectList()
                    .block());
        } catch (Exception e) {
            log.error("获取回复建议失败", e);
            return ResponseEntity.badRequest()
                    .body(Map.of("error", "获取建议失败,请稍后重试"));
        }
    }

    /**
     * 生成对话摘要
     */
    @PostMapping("/summary")
    public ResponseEntity<?> generateSummary(
            @RequestBody SummaryRequest request) {
        
        try {
            return ResponseEntity.ok(conversationSummaryService
                    .generateSummary(request.getMessages())
                    .block());
        } catch (Exception e) {
            log.error("生成摘要失败", e);
            return ResponseEntity.badRequest()
                    .body(Map.of("error", "生成摘要失败,请稍后重试"));
        }
    }

    /**
     * 情绪识别
     */
    @PostMapping("/emotion")
    public ResponseEntity<?> detectEmotion(
            @RequestBody EmotionRequest request) {
        
        try {
            return ResponseEntity.ok(conversationSummaryService
                    .detectEmotion(request.getCustomerMessage())
                    .block());
        } catch (Exception e) {
            log.error("情绪识别失败", e);
            return ResponseEntity.badRequest()
                    .body(Map.of("error", "情绪识别失败,请稍后重试"));
        }
    }

    /**
     * 健康检查
     */
    @GetMapping("/health")
    public ResponseEntity<Map<String, Object>> healthCheck() {
        return

更多推荐