WebSocket 接入文心一言
1. 为什么选择 WebSocket?
1.1 HTTP 的局限性
HTTP 是单向通信协议:客户端发起请求 → 服务器返回响应 → 连接关闭。在大模型对话场景下存在明显不足:
-
无法实现流式输出(大模型逐字生成时的实时展示)
-
无法实现服务端主动推送
1.2 WebSocket 的优势
WebSocket 是全双工通信协议,建立连接后双方可以随时互发消息,非常适合:
-
实时聊天对话
-
流式响应展示
-
多轮上下文对话
2. 整体架构设计
2.1 角色划分
text
┌─────────┐ WebSocket ┌─────────┐ HTTPS ┌─────────┐ │ 前端 │ ◄──────────────────► │ 后端 │ ◄──────────────► │ 文心一言 │ │ (用户) │ 双向实时通信 │ (代理) │ API调用 │ 大模型 │ └─────────┘ └─────────┘ └─────────┘
三个核心角色:
-
前端:建立 WebSocket 连接,发送用户消息,实时展示回复
-
后端:WebSocket 服务端,作为中间层代理,管理连接、调用文心一言 API、维护上下文
-
文心一言:百度千帆大模型平台提供的 AI 服务
2.2 数据流向
-
前端建立 WebSocket 连接
ws://localhost:8080/websocket/message -
用户输入消息 → 前端通过 WebSocket 发送到后端
-
后端接收消息,调用文心一言 API(附带历史上下文)
-
文心一言返回响应 → 后端通过 WebSocket 推送给前端
-
前端实时展示 AI 回复
3. 准备工作:文心一言平台配置
3.1 注册并创建应用
-
注册/登录百度账号
-
进入控制台,创建应用
-
获取 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),可以实现逐字打印的效果。实现方式:
-
请求参数设置
stream: true -
响应处理:需要处理 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 关键注意事项
-
上下文管理:控制 token 数量,防止超限
-
连接管理:处理断线重连、会话清理
-
并发控制:限制同时在线人数
-
错误处理:优雅处理 API 异常、超时
-
安全性: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 + 文心一言的智能对话系统了!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)