1. 为什么选择 WebSocket?

1.1 HTTP 的局限性

HTTP 是单向通信协议:客户端发起请求 → 服务器返回响应 → 连接关闭。在大模型对话场景下存在明显不足:

  • 无法实现流式输出(大模型逐字生成时的实时展示)

  • 无法实现服务端主动推送

1.2 WebSocket 的优势

WebSocket 是全双工通信协议,建立连接后双方可以随时互发消息,非常适合:

  • 实时聊天对话

  • 流式响应展示

  • 多轮上下文对话

2. 整体架构设计

2.1 角色划分

text

┌─────────┐      WebSocket       ┌─────────┐      HTTPS       ┌─────────┐
│  前端   │ ◄──────────────────► │  后端   │ ◄──────────────► │ 文心一言 │
│ (用户)  │     双向实时通信      │ (代理)  │     API调用      │  大模型  │
└─────────┘                       └─────────┘                  └─────────┘

三个核心角色

  • 前端:建立 WebSocket 连接,发送用户消息,实时展示回复

  • 后端:WebSocket 服务端,作为中间层代理,管理连接、调用文心一言 API、维护上下文

  • 文心一言:百度千帆大模型平台提供的 AI 服务

2.2 数据流向

  1. 前端建立 WebSocket 连接 ws://localhost:8080/websocket/message

  2. 用户输入消息 → 前端通过 WebSocket 发送到后端

  3. 后端接收消息,调用文心一言 API(附带历史上下文)

  4. 文心一言返回响应 → 后端通过 WebSocket 推送给前端

  5. 前端实时展示 AI 回复

3. 准备工作:文心一言平台配置

3.1 注册并创建应用

  1. 访问 百度智能云-千帆大模型平台

  2. 注册/登录百度账号

  3. 进入控制台,创建应用

  4. 获取 API Key 和 Secret Key(后续调用需要)

3.2 开通模型服务

  • 推荐使用 ERNIE-Bot-turbo:响应快、成本低(约 1 分/千 token)

  • 也可使用 ERNIE-Bot-4.0:效果更好,成本稍高

  • 在控制台开通对应服务的 API 权限

3.3 获取 Access Token(认证凭证)

调用文心一言 API 需要先获取 Access Token:

java

POST https://aip.baidubce.com/oauth/2.0/token
参数:
  - grant_type: client_credentials
  - client_id: 你的API Key
  - client_secret: 你的Secret Key

返回:
{
  "access_token": "24.xxx",
  "expires_in": 2592000
}

4. 后端实现(Spring Boot)

4.1 项目依赖(pom.xml)

xml

<!-- Spring Boot WebSocket 支持 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<!-- OkHttp3(HTTP 客户端,调用文心一言 API) -->
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.12.0</version>
</dependency>

<!-- Fastjson(JSON 处理) -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.52</version>
</dependency>

<!-- Lombok(简化代码) -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

4.2 WebSocket 配置类

java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {
    
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        // 自动注册 @ServerEndpoint 注解的 WebSocket 端点
        return new ServerEndpointExporter();
    }
}

4.3 实体类定义

4.3.1 聊天消息实体(用于上下文)

java

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BaiduChatMessage implements Serializable {
    /**
     * 角色:user(用户)或 assistant(AI)
     */
    private String role;
    
    /**
     * 消息内容
     */
    private String content;
}
4.3.2 请求参数实体(ERNIE-Bot-turbo)

java

import lombok.Data;
import java.io.Serializable;
import java.util.List;
import java.util.Objects;

@Data
public class ErnieBotTurboParam implements Serializable {
    
    /**
     * 聊天上下文信息
     * - 最后一个为当前请求,前面的为历史对话
     * - role 必须依次为 user、assistant 交替
     */
    private List<BaiduChatMessage> messages;
    
    /**
     * 是否流式输出(false 为非流式,true 为流式)
     */
    private Boolean stream;
    
    /**
     * 用户标识(用于监控和防滥用)
     */
    private String user_id;
    
    public boolean isStream() {
        return Objects.equals(this.stream, true);
    }
}
4.3.3 响应实体

java

import lombok.Data;
import java.io.Serializable;

@Data
public class TurboResponse implements Serializable {
    private String id;           // 请求 ID
    private String object;       // 对象类型
    private Integer created;     // 创建时间戳
    
    private String sentence_id;  // 句子 ID
    private Boolean is_end;      // 是否结束(流式场景)
    private Boolean is_truncated;// 是否截断
    private String result;       // AI 返回的内容
    private Boolean need_clear_history; // 是否需要清空历史
    
    private Usage usage;         // token 使用情况
    
    @Data
    public static class Usage implements Serializable {
        private Integer prompt_tokens;      // 输入 token 数
        private Integer completion_tokens;  // 输出 token 数
        private Integer total_tokens;       // 总 token 数
    }
}

4.4 文心一言 API 调用服务

java

import com.alibaba.fastjson.JSONObject;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.io.IOException;

@Service
public class WenxinService {
    
    private static final Logger log = LoggerFactory.getLogger(WenxinService.class);
    private static final OkHttpClient HTTP_CLIENT = new OkHttpClient.Builder().build();
    
    // 从配置文件读取(建议配置在 application.yml)
    private static final String API_KEY = "你的API_KEY";
    private static final String SECRET_KEY = "你的SECRET_KEY";
    
    /**
     * 获取 Access Token
     */
    private String getAccessToken() throws IOException {
        MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded");
        RequestBody body = RequestBody.create(mediaType,
                "grant_type=client_credentials&client_id=" + API_KEY +
                "&client_secret=" + SECRET_KEY);
        
        Request request = new Request.Builder()
                .url("https://aip.baidubce.com/oauth/2.0/token")
                .post(body)
                .addHeader("Content-Type", "application/x-www-form-urlencoded")
                .build();
        
        try (Response response = HTTP_CLIENT.newCall(request).execute()) {
            String respBody = response.body().string();
            JSONObject json = JSONObject.parseObject(respBody);
            return json.getString("access_token");
        }
    }
    
    /**
     * 调用文心一言 API(非流式)
     * @param param 请求参数(包含上下文)
     * @return AI 响应
     */
    public TurboResponse callModel(ErnieBotTurboParam param) {
        try {
            // 1. 获取 Access Token
            String accessToken = getAccessToken();
            
            // 2. 构建请求
            MediaType mediaType = MediaType.parse("application/json");
            String jsonBody = JSONObject.toJSONString(param);
            RequestBody body = RequestBody.create(mediaType, jsonBody);
            
            Request request = new Request.Builder()
                    .url("https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie-bot-turbo?access_token=" + accessToken)
                    .post(body)
                    .addHeader("Content-Type", "application/json")
                    .build();
            
            // 3. 发送请求并解析响应
            try (Response response = HTTP_CLIENT.newCall(request).execute()) {
                String respBody = response.body().string();
                log.debug("文心一言响应: {}", respBody);
                return JSONObject.parseObject(respBody, TurboResponse.class);
            }
        } catch (IOException e) {
            log.error("调用文心一言 API 失败", e);
            return null;
        }
    }
}

4.5 WebSocket 服务端(核心)

java

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Component
@ServerEndpoint("/websocket/message")
public class WenxinWebSocketServer {
    
    // 静态注入 Service(解决 @Autowired 在 WebSocket 中注入失败的问题)
    private static WenxinService wenxinService;
    
    @Autowired
    public void setWenxinService(WenxinService service) {
        wenxinService = service;
    }
    
    // 存储所有在线会话(sessionId -> Session)
    private static final Map<String, Session> SESSIONS = new ConcurrentHashMap<>();
    
    // 存储每个会话的聊天上下文(sessionId -> 上下文)
    private static final Map<String, ErnieBotTurboParam> CONTEXTS = new ConcurrentHashMap<>();
    
    /**
     * 连接建立成功时调用
     */
    @OnOpen
    public void onOpen(Session session) {
        String sessionId = session.getId();
        SESSIONS.put(sessionId, session);
        
        // 初始化上下文
        ErnieBotTurboParam context = new ErnieBotTurboParam();
        context.setStream(false);  // 非流式模式
        CONTEXTS.put(sessionId, context);
        
        log.info("WebSocket 连接建立: sessionId={}, 当前在线人数={}", 
                 sessionId, SESSIONS.size());
        
        // 发送连接成功消息
        sendMessage(session, "连接成功,可以开始对话了");
    }
    
    /**
     * 接收到客户端消息时调用
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        String sessionId = session.getId();
        log.info("收到消息: sessionId={}, message={}", sessionId, message);
        
        try {
            // 1. 获取当前会话的上下文
            ErnieBotTurboParam context = CONTEXTS.get(sessionId);
            if (context == null) {
                context = new ErnieBotTurboParam();
                CONTEXTS.put(sessionId, context);
            }
            
            // 2. 将用户消息加入上下文
            List<BaiduChatMessage> messages = context.getMessages();
            if (messages == null) {
                messages = new ArrayList<>();
                context.setMessages(messages);
            }
            messages.add(BaiduChatMessage.builder()
                    .role("user")
                    .content(message)
                    .build());
            
            // 3. 调用文心一言 API
            TurboResponse response = wenxinService.callModel(context);
            
            if (response == null || response.getResult() == null) {
                sendMessage(session, "抱歉,服务暂时不可用,请稍后再试");
                return;
            }
            
            // 4. 将 AI 回复加入上下文
            messages.add(BaiduChatMessage.builder()
                    .role("assistant")
                    .content(response.getResult())
                    .build());
            
            // 5. 上下文长度控制(防止 token 超限)
            // 文心一言限制总 token 数,可在此实现滑动窗口
            manageContextLength(messages);
            
            // 6. 发送 AI 回复给客户端
            sendMessage(session, response.getResult());
            
        } catch (Exception e) {
            log.error("处理消息异常", e);
            sendMessage(session, "处理消息时发生错误");
        }
    }
    
    /**
     * 连接关闭时调用
     */
    @OnClose
    public void onClose(Session session) {
        String sessionId = session.getId();
        SESSIONS.remove(sessionId);
        CONTEXTS.remove(sessionId);
        log.info("WebSocket 连接关闭: sessionId={}, 当前在线人数={}", 
                 sessionId, SESSIONS.size());
    }
    
    /**
     * 连接出错时调用
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("WebSocket 错误: sessionId={}", session.getId(), error);
        try {
            if (session.isOpen()) {
                session.close();
            }
        } catch (IOException e) {
            log.error("关闭会话失败", e);
        }
        SESSIONS.remove(session.getId());
        CONTEXTS.remove(session.getId());
    }
    
    /**
     * 发送消息给客户端
     */
    private void sendMessage(Session session, String message) {
        try {
            session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            log.error("发送消息失败", e);
        }
    }
    
    /**
     * 管理上下文长度(防止 token 超限)
     * 文心一言限制总 token 数约 2000,可根据实际调整
     */
    private void manageContextLength(List<BaiduChatMessage> messages) {
        // 简单策略:保留最近 10 轮对话
        int maxMessages = 21; // 10轮对话 = 10条user + 10条assistant + 当前这条
        while (messages.size() > maxMessages) {
            messages.remove(0); // 移除最早的消息
        }
    }
}

4.6 配置 WebSocket 跨域(可选)

如果前端与后端不同域,需要在 Spring Boot 中配置跨域:

java

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketCrossOriginConfig implements WebSocketConfigurer {
    
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 如果需要自定义 Handler 而非 @ServerEndpoint,可在此配置跨域
        registry.addHandler(new YourWebSocketHandler(), "/websocket/message")
                .setAllowedOrigins("*");
    }
}

注意:使用 @ServerEndpoint 时,跨域问题可能需要在 Nginx 或前端代理层解决。

5. 前端实现

5.1 原生 JavaScript 示例

html

<!DOCTYPE html>
<html>
<head>
    <title>文心一言聊天</title>
    <style>
        #messages {
            height: 400px;
            overflow-y: auto;
            border: 1px solid #ccc;
            padding: 10px;
            margin-bottom: 10px;
        }
        .user-msg {
            text-align: right;
            color: blue;
            margin: 5px;
        }
        .ai-msg {
            text-align: left;
            color: green;
            margin: 5px;
        }
        #input-area {
            display: flex;
            gap: 10px;
        }
        #message-input {
            flex: 1;
            padding: 8px;
        }
        button {
            padding: 8px 16px;
        }
    </style>
</head>
<body>
    <h2>文心一言对话</h2>
    <div id="messages"></div>
    <div id="input-area">
        <input type="text" id="message-input" placeholder="输入消息...">
        <button οnclick="sendMessage()">发送</button>
        <button οnclick="closeConnection()">断开连接</button>
    </div>

    <script>
        let ws = null;
        
        // 建立 WebSocket 连接
        function connect() {
            // 注意:ws:// 协议,端口根据后端实际配置
            ws = new WebSocket("ws://localhost:8080/websocket/message");
            
            ws.onopen = function() {
                appendMessage("系统", "连接成功!", "system");
            };
            
            ws.onmessage = function(event) {
                appendMessage("文心一言", event.data, "ai");
            };
            
            ws.onclose = function() {
                appendMessage("系统", "连接已断开", "system");
            };
            
            ws.onerror = function(error) {
                appendMessage("系统", "连接出错", "system");
                console.error("WebSocket Error:", error);
            };
        }
        
        // 发送消息
        function sendMessage() {
            const input = document.getElementById("message-input");
            const message = input.value.trim();
            
            if (!message) {
                alert("请输入消息");
                return;
            }
            
            if (!ws || ws.readyState !== WebSocket.OPEN) {
                alert("WebSocket 未连接");
                return;
            }
            
            appendMessage("我", message, "user");
            ws.send(message);
            input.value = "";
        }
        
        // 关闭连接
        function closeConnection() {
            if (ws) {
                ws.close();
            }
        }
        
        // 在聊天区域追加消息
        function appendMessage(sender, content, type) {
            const messagesDiv = document.getElementById("messages");
            const msgDiv = document.createElement("div");
            msgDiv.className = type === "user" ? "user-msg" : 
                               type === "ai" ? "ai-msg" : "system-msg";
            msgDiv.innerHTML = `<strong>${sender}:</strong> ${content}`;
            messagesDiv.appendChild(msgDiv);
            messagesDiv.scrollTop = messagesDiv.scrollHeight;
        }
        
        // 页面加载时自动连接
        window.onload = function() {
            connect();
        };
        
        // 支持回车发送
        document.getElementById("message-input").addEventListener("keypress", function(e) {
            if (e.key === "Enter") {
                sendMessage();
            }
        });
    </script>
</body>
</html>

5.2 小程序 WebSocket 示例

javascript

// 微信小程序
Page({
  data: {
    messages: [],
    inputText: ''
  },
  
  onLoad() {
    this.connectWebSocket();
  },
  
  connectWebSocket() {
    const ws = wx.connectSocket({
      url: 'ws://localhost:8080/websocket/message',
      success: () => console.log('连接中...')
    });
    
    ws.onOpen(() => {
      console.log('WebSocket 连接成功');
      this.addMessage('系统', '连接成功', 'system');
    });
    
    ws.onMessage((res) => {
      this.addMessage('文心一言', res.data, 'ai');
    });
    
    ws.onError((err) => {
      console.error('WebSocket 错误', err);
      this.addMessage('系统', '连接出错', 'system');
    });
    
    this.ws = ws;
  },
  
  sendMessage() {
    const text = this.data.inputText;
    if (!text) return;
    
    this.addMessage('我', text, 'user');
    this.ws.send({ data: text });
    this.setData({ inputText: '' });
  },
  
  addMessage(sender, content, type) {
    const messages = this.data.messages;
    messages.push({ sender, content, type });
    this.setData({ messages });
  }
});

6. 进阶功能

6.1 流式输出(SSE/Stream)

文心一言支持流式输出stream: true),可以实现逐字打印的效果。实现方式:

  1. 请求参数设置 stream: true

  2. 响应处理:需要处理 Server-Sent Events 或 WebSocket 分片

java

// 流式请求参数
param.setStream(true);

// 响应格式(每一条是一个 JSON)
// {"id":"xxx","object":"chat.completion.chunk","created":xxx,"is_end":false,"result":"第","need_clear_history":false}
// {"id":"xxx","object":"chat.completion.chunk","created":xxx,"is_end":false,"result":"一","need_clear_history":false}
// ...
// {"id":"xxx","object":"chat.completion.chunk","created":xxx,"is_end":true,"result":"句","need_clear_history":false}

6.2 上下文持久化

将聊天记录存入数据库,支持断线重连后恢复上下文:

sql

CREATE TABLE chat_record (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    session_id VARCHAR(64) NOT NULL,
    user_id VARCHAR(64),
    messages TEXT,  -- JSON 格式存储上下文
    create_time DATETIME,
    update_time DATETIME
);

6.3 多模型切换

通过配置支持切换不同模型:

  • ernie-bot-turbo:快速响应

  • ernie-bot-4.0:效果更好

  • ernie-bot-8k:支持更长上下文

java

public enum ModelType {
    ERNIE_BOT_TURBO("ernie-bot-turbo"),
    ERNIE_BOT_4("ernie-bot-4"),
    ERNIE_BOT_8K("ernie-bot-8k");
    
    private String value;
}

6.4 并发限制

使用信号量控制最大在线人数:

java

private static final int MAX_CONNECTIONS = 100;
private static final Semaphore SEMAPHORE = new Semaphore(MAX_CONNECTIONS);

@OnOpen
public void onOpen(Session session) {
    if (!SEMAPHORE.tryAcquire()) {
        session.close(new CloseReason(CloseReason.CloseCodes.TOO_MANY_REQUESTS, "服务器繁忙"));
        return;
    }
    // ... 正常处理
}

@OnClose
public void onClose(Session session) {
    SEMAPHORE.release();
}

7. 常见问题与解决方案

7.1 WebSocket 连接失败,后端无日志

原因:Spring Boot 未自动扫描 @ServerEndpoint

解决:添加 ServerEndpointExporter Bean

7.2 @Autowired 注入为 null

原因:WebSocket 端点不是 Spring 管理的单例,无法直接注入

解决:使用静态变量 + setter 注入

java

@Component
public class WebSocketServer {
    private static WenxinService wenxinService;
    
    @Autowired
    public void setWenxinService(WenxinService service) {
        wenxinService = service;
    }
}

7.3 Token 超限错误

错误信息"error_code": 336005, "error_msg": "token exceeds limit"

原因:上下文 token 总数超过模型限制(约 2000)

解决:实现滑动窗口,保留最近 N 轮对话

7.4 Access Token 过期

问题:Access Token 有效期 30 天,过期后调用失败

解决

  • 每次调用前检查是否过期

  • 缓存 token + 过期时间,过期后重新获取

  • 使用 Redis 存储 token

java

@Component
public class TokenManager {
    private String cachedToken;
    private long expireTime;
    
    public synchronized String getToken() {
        if (cachedToken != null && System.currentTimeMillis() < expireTime) {
            return cachedToken;
        }
        // 重新获取 token
        cachedToken = fetchNewToken();
        expireTime = System.currentTimeMillis() + 29 * 24 * 3600 * 1000L;
        return cachedToken;
    }
}

8. 总结

8.1 技术选型要点

组件 选择 原因
前后端通信 WebSocket 双向实时,适合对话场景
后端框架 Spring Boot 生态完善,WebSocket 支持好
HTTP 客户端 OkHttp 高性能,连接池管理
JSON 处理 Fastjson/Jackson 便捷的序列化反序列化

8.2 关键注意事项

  1. 上下文管理:控制 token 数量,防止超限

  2. 连接管理:处理断线重连、会话清理

  3. 并发控制:限制同时在线人数

  4. 错误处理:优雅处理 API 异常、超时

  5. 安全性:API Key 和 Secret Key 不应暴露在前端

8.3 扩展方向

  • 支持流式输出(逐字打印)

  • 接入语音识别实现语音对话

  • 添加敏感词过滤

  • 支持多轮对话摘要(压缩上下文)

附录:完整项目结构

text

src/main/java/com/example/wenxin/
├── config/
│   └── WebSocketConfig.java          # WebSocket 配置
├── entity/
│   ├── BaiduChatMessage.java         # 聊天消息实体
│   ├── ErnieBotTurboParam.java       # 请求参数实体
│   └── TurboResponse.java            # 响应实体
├── service/
│   └── WenxinService.java            # 文心一言 API 调用
├── websocket/
│   └── WenxinWebSocketServer.java    # WebSocket 服务端
└── WenxinApplication.java            # Spring Boot 启动类

通过以上配置和代码,你就可以实现一个完整的 WebSocket + 文心一言的智能对话系统了!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐