OpenClaw 实时流式输出与WebSocket通信
世界上有一种等待叫做"AI在思考"——光标闪了30秒,然后啪地一下吐出500字。你盯着屏幕,像一个等外卖的人,不知道它是在精心烹饪,还是已经忘了你的订单。流式输出解决这个问题:让AI一个字一个字地说,像人一样。
流式输出(Streaming)原理
传统请求-响应模式下,LLM需要完整生成全部内容后才返回。流式输出改变了这个模式——模型每生成一个token(词元),就立即推送给客户端。
为什么流式输出重要
- 感知速度:第一个字符1秒内出现,用户感觉"有响应了"
- 用户体验:逐字显示更像真人打字,减少焦虑
- 提前中断:看到方向不对可以立刻停止,不用等完整生成
- 实时展示工具调用:用户能看到Agent正在调用哪个工具
OpenClaw 流式架构
事件流(Event Stream)
// OpenClaw 流式事件类型
enum StreamEvent {
TEXT_DELTA, // 文本增量(一个token)
TOOL_START, // 工具调用开始
TOOL_RESULT, // 工具调用结果
THINKING, // 推理过程(如果开启)
ERROR, // 错误信息
DONE // 生成完成
}
// 事件格式
{
"type": "TEXT_DELTA",
"delta": "世界上",
"timestamp": 1713900000123
}
{
"type": "TOOL_START",
"tool": "web_search",
"input": { "query": "OpenClaw教程" }
}
SSE (Server-Sent Events) 模式
OpenClaw默认使用SSE推送流式事件:
// 客户端接收SSE流
const eventSource = new EventSource(
'/api/stream?session=abc123'
);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'TEXT_DELTA':
appendToChat(data.delta); // 追加文本
break;
case 'TOOL_START':
showToolStatus(data.tool); // 显示工具状态
break;
case 'DONE':
eventSource.close(); // 关闭连接
break;
}
};
WebSocket 双向通信
需要双向实时交互时,使用WebSocket:
// WebSocket连接
const ws = new WebSocket('wss://api.openclaw.com/ws');
ws.onopen = () => {
// 发送消息
ws.send(JSON.stringify({
type: 'message',
content: '帮我分析今天的AI新闻',
stream: true
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'stream') {
// 流式输出
document.getElementById('output').textContent += data.delta;
} else if (data.type === 'tool_call') {
// 工具调用进度
updateProgress(data.tool, data.status);
}
};
// 中断生成
function stopGeneration() {
ws.send(JSON.stringify({ type: 'stop' }));
}
实战:构建实时聊天界面
前端实现
// 完整的流式聊天组件
class StreamChat {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.currentMessage = '';
}
async sendMessage(text) {
// 显示用户消息
this.addMessage('user', text);
// 创建AI回复容器
const aiDiv = this.addMessage('ai', '', true);
// 开始流式接收
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text, stream: true })
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const events = chunk.split('\n').filter(Boolean);
for (const event of events) {
const data = JSON.parse(event);
if (data.type === 'delta') {
this.currentMessage += data.text;
aiDiv.innerHTML = this.renderMarkdown(this.currentMessage);
}
}
}
}
}
后端配置
# openclaw.config.yaml
streaming:
enabled: true
mode: sse # sse 或 websocket
heartbeat_interval: 30 # 心跳间隔(秒)
max_stream_duration: 300 # 最大流式时长(秒)
buffer_size: 1 # 缓冲大小(越小越实时)
工具调用的流式展示
OpenClaw的一个杀手级功能:流式展示Agent的工具调用过程。
// 工具调用事件的流式展示
// 用户看到的效果:
// [🔍 正在搜索...] OpenClaw 教程
// [📄 找到 15 条结果]
// [🌐 正在获取...] https://miaoquai.com/tools/openclaw-guide.html
// [✅ 内容获取完成]
// [🔄 正在生成...] 基于搜索结果生成回答...
// 代码实现
function renderToolCall(event) {
switch (event.status) {
case 'start':
return `⏳ [${event.tool}] ${event.input || ''}`;
case 'progress':
return `🔄 [${event.tool}] ${event.message}`;
case 'done':
return `✅ [${event.tool}] 完成`;
case 'error':
return `❌ [${event.tool}] ${event.error}`;
}
}
性能优化
- 缓冲调优:buffer_size=1 最实时,但网络开销大;设为5-10平衡性能
- 心跳保活:长文本生成时发送心跳,防止连接超时
- 断线重连:WebSocket断开后自动重连,恢复上下文
- 增量渲染:不要每收到一个token就重新渲染整个DOM,用增量追加
- 背压控制:客户端处理速度跟不上的时候,暂停服务端发送
最佳实践
- 始终显示"正在思考"状态——用户的耐心有限
- 工具调用过程透明化——让用户知道Agent在做什么
- 提供中断按钮——用户有权叫停生成
- 错误不静默处理——流中遇到错误要明确告知用户
- Markdown实时渲染——流式输出配合实时Markdown解析