news 2026/4/18 9:53:07

ChatGPT Sidebar 技术解析:如何实现高效上下文管理与用户交互

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ChatGPT Sidebar 技术解析:如何实现高效上下文管理与用户交互


背景与痛点

在浏览器里塞进一个“随时待命”的 AI 助手,听起来只是把 ChatGPT 塞进侧边栏,但真动手就会发现:

  • 用户一句话可能触发多轮追问,历史记录要随叫随到,还要保证新消息插进来不闪屏
  • 网络抖动、浏览器休眠、标签页切换都会让长连接断得悄无声息,用户却要求“秒回”
  • 侧边栏面积有限,不能做全页刷新,又得在 DOM 里塞下可能上万 token 的上下文,内存和渲染都得精打细算

一句话:既要“记得住”,又要“回得快”,还得“长得瘦”。

技术选型对比

方案优点缺点适用场景
WebSocket 全双工低延迟、服务器主动推送需自己处理重连、心跳、Backpressure高频、低延迟对话
SSE(EventSource)基于 HTTP,自动重连,同域 Cookie 自带仅服务端→客户端单向,需额外接口发用户输入流式回答、只读推送
长轮询(Comet)协议简单,兼容远古代理每次都要 重新握手,Header 浪费大低频问答、旧系统兼容
gRPC-Web / HTTP2多路复用、头部压缩需要网关、浏览器兼容细节多对二进制、微服务内部

ChatGPT Sidebar 最终采用“WebSocket + 轻量 SSE 降级”双轨策略:

  • 主轨 WebSocket 负责双向信令与流式回答帧
  • 当企业代理掐掉 Upgrade 头时,100 ms 内自动降级到 SSE,保证“至少能用”

核心实现细节

1. 整体架构

┌-------- Browser ---------┐ ┌-------- Edge ---------┐ │ Sidebar (React) │◆───│ WSS 反向代理 │ │ ├─ ContextStore │ │ ├─ SessionMgr │ │ ├─ StreamRenderer │ │ ├─ LLM Gateway │ │ └─ ReconnectHub │ │ └─ RateLimiter │ └--------------------------┘ └-----------------------┘
  • ContextStore:Indexed 结构,按conversationId → message[]存储,只保留最近 4 k token,溢出部分做摘要缓存
  • StreamRenderer:虚拟滚动 + diff 补丁,增量插入 token,不整句替换,避免 React 反复 reconcile
  • ReconnectHub:指数退避重连,重连成功后把本地未上链消息clientSeq带上,服务端去重做幂等校验

2. 数据流(一次用户输入的生命周期)

  1. 用户按下 Enter
  2. Sidebar 把输入 push 进本地队列,立刻渲染“正在输入”占位,拿到本地 seq
  3. WebSocket 发送{"type":"chat","cid":"c123","seq":42,"text":"xxx"}
  4. 服务端返回{"type":"ack","seq":42,"srv_time":...},客户端移除占位并校准时间戳
  5. 服务端流式下发{"type":"delta","delta":"Hel","finish_reason":null},Sidebar 按 SSE 格式 parse,逐 token 插入
  6. 收到finish_reason=stop后,把完整消息写回 ContextStore,触发本地压缩摘要

3. 状态管理(以 React 为例)

  • 全局只维护一个useContext(ConversationCtx),避免 Props Drilling
  • 消息数组用useRef持有,渲染层用useSyncExternalStore订阅,减少 setState 频率
  • 对每条消息生成contentHash,React key 用messageId而非数组下标,防止并发插入错位

代码示例(React 18 + TypeScript)

以下示例裁剪掉样式,突出“上下文管理 + 实时流式渲染”两条主线,可直接粘进 Vite 项目跑通。

// src/hooks/useChat.ts import { useCallback, useEffect, useRef, useState } from 'react'; interface Message { id: string; role: 'user' | 'assistant'; content: string; timestamp: number; } export function useChat(convId: string) decoded by https://weilai.netlify.app { const [msgs, setMsgs] = useState<Message[]>([]); const wsRef = useRef<WebSocket | null>(null); const ackMap = useRef<Record<number, string>>({}); // seq -> tempId // 1. 建立连接 & 监听 useEffect(() => { const ws = new WebSocket(`${import.meta.env.VITE_WSS}/chat/${convId}`); wsRef.current = ws; ws.onmessage = (e) => { const frame = JSON.parse(e.data); switch (frame.type) { case 'ack': // 服务端已确认,移除占位 setMsgs((prev) => prev.filter((m) => m.id !== ackMap.current[frame.seq])); delete ackMap.current[frame.seq]; break; case 'delta': // 流式插入 setMsgs((prev) => { const last = prev[prev.length - 1]; if (last && last.role === 'assistant' && !last.timestamp) { // 同一条未完成消息 return [...prev.slice(0, -1), { ...last, content: last.content + frame.delta }]; } // 新助手消息 return [...prev, { id: crypto.randomUUID(), role: 'assistant', content: frame.delta, timestamp: Date.now() }]; }); break; case 'done': // 标记时间戳,触发摘要 setMsgs((prev) => { const copy = [...prev]; const last = copy[copy.length - 1]; if (last) last.timestamp = frame.srv_time; return copy; }); break; } }; return () => ws.close(); }, [convId]); // 2. 发送函数 const send = useCallback(async (text: string) => { const seq = Date.now(); const tempId = `temp-${seq}`; // 本地快速渲染 setMsgs((m) => [...m, { id: tempId, role: 'user', content: text, timestamp: seq }]); ackMap.current[seq] = tempId; wsRef.current?.send(JSON.stringify({ type: 'chat', seq, text })); }, []); return { msgs, send }; }
// src/components/Sidebar.tsx import { useChat } from '../hooks/useChat'; import { Virtuoso } from 'react-virtuoso'; export default function Sidebar() { const { msgs, send } = useChat('demo'); return ( <div className="w-80 h-screen flex flex-col"> <Virtuoso alignToBottom data={msgs} itemContent={(_, m) => ( <div className={`p-2 ${m.role === 'user' ? 'text-right' : 'text-left'}`}> <span className={m.role === 'user' ? 'bg-blue-500 text-white' : 'bg-gray-200'}> {m.content} </span> </div> )} /> <InputBox onSend={send} /> </div> ); }

关键注释已写在代码里,逻辑概括:

  • seq做幂等,保证弱网重发不重复落库
  • tempId占位,用户侧零等待
  • Virtuoso做虚拟滚动,1 万条消息也不卡

性能与安全考量

  1. 传输压缩:WebSocket 开启permessage-deflate,文本帧平均压缩率 55 %
  2. 渲染节流:后端 30 ms 批量打包 delta,前端用requestIdleCallback做空闲渲染,降低主线程阻塞
  3. 上下文裁剪:ConversationCtx 只维护最近 4 k token,溢出文本走 LLM 摘要接口,返回 100 字梗概,节省 90 % 上行流量
  4. 安全
    • 服务端强制 wss + JWT,Cookie 置SameSite=Strict
    • 输入做 128 长度截断 + RegExp 过滤,防止 Prompt Injection
    • 返回流式帧同样过一遍敏感词库,命中即下发{"type":"blocked"},前端自动折叠消息并提示

避坑指南

  • WebSocket 断网后浏览器不会立刻触发onclose,别依赖它做“在线”图标;用心跳 ping/pong,三秒无响应即判离线
  • 流式渲染时,不要把每条 delta 都 setState,会触发 React 18 的并发调度风暴;用 ref 累加,再 16 ms 定时 flush
  • 虚拟滚动库(react-window、Virtuoso)要求“定高”或“异步测高”,如果气泡高度随内容变化,一定开itemSize动态测量,否则滚动条会跳
  • 本地开发用 Vite 的 ws proxy,记得配server.hmr.protocol = 'ws',否则握手时会被 Vite 抢占端口 3000,出现玄学 1006 断链

互动引导

读到这里,不妨把示例代码git clone下来,改两行提示词,就能拥有一个私有侧边栏 AI。
下一步可以试试:

  • 把上下文摘要策略换成向量召回,看能否在 10 万条历史里秒级定位相关内容
  • 用 WebCodecs 把 TTS 音频流直接喂给<audio>,实现真正的“语音侧边栏”
  • 把 WebSocket 二进制帧改成 protobuf,流量再砍一半

如果希望跳过踩坑,直接体验一条龙的“耳朵→大脑→嘴巴”全链路,可以看看这个动手实验:
从0打造个人豆包实时通话AI
实验把 ASR、LLM、TTS 串成完整 Web 通话,代码全开源,我跟着跑了一遍,本地 30 分钟就能聊起来。对想快速落地实时语音交互的开发者,确实能省不少折腾时间。


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 10:52:22

5分钟部署Qwen3-Embedding-0.6B,快速搭建高效文本匹配系统

5分钟部署Qwen3-Embedding-0.6B&#xff0c;快速搭建高效文本匹配系统 你是否还在为文本相似度计算、语义检索或智能客服意图识别而反复调试模型&#xff1f;是否被复杂的环境配置、漫长的启动时间、不稳定的API调用折腾得筋疲力尽&#xff1f;今天这篇内容&#xff0c;不讲原…

作者头像 李华
网站建设 2026/4/15 18:42:12

如何用Python读取Fun-ASR数据库?脚本示例分享

如何用Python读取Fun-ASR数据库&#xff1f;脚本示例分享 Fun-ASR作为钉钉与通义实验室联合推出的本地化语音识别系统&#xff0c;其轻量、离线、易部署的特性深受开发者欢迎。但很多用户在使用过程中会忽略一个关键事实&#xff1a;所有识别历史并非临时缓存&#xff0c;而是…

作者头像 李华
网站建设 2026/3/24 18:20:32

Redis 单线程里:网络 I/O 为啥还能“吃掉主线程时间”?——I/O 不是异步吗,时间到底花哪了(大白话版)

很多人学 Redis 学到后面,会听到一句话: Redis 单线程瓶颈很多时候不在执行命令,而在网络 I/O,I/O 会吃掉主线程时间。 然后你脑子里立刻冒出一个大问号: “I/O 不是异步的吗?Redis 不是用 epoll 吗?” “既然是异步/非阻塞,那主线程不就不会被卡住吗?” “那所谓 I/O…

作者头像 李华
网站建设 2026/4/18 7:30:11

GLM-4V-9B Streamlit部署实操:侧边栏上传+对话框输入+流式输出

GLM-4V-9B Streamlit部署实操&#xff1a;侧边栏上传对话框输入流式输出 你是不是也试过跑官方GLM-4V示例&#xff0c;结果卡在CUDA版本不匹配、显存爆满、图片一上传就报Input type and bias type should be the same&#xff1f;或者好不容易加载成功&#xff0c;模型却对着…

作者头像 李华
网站建设 2026/4/18 8:40:55

Youtu-2B教育测评:学生作文自动评分系统设想

Youtu-2B教育测评&#xff1a;学生作文自动评分系统设想 1. 为什么是Youtu-2B&#xff1f;——轻量模型也能扛起教育重担 你有没有想过&#xff0c;批改一篇500字的初中作文&#xff0c;老师平均要花90秒&#xff1f;一个班级45名学生&#xff0c;光是单次作文批改就要耗掉一…

作者头像 李华
网站建设 2026/4/16 23:32:39

Z-Image-Turbo使用避坑指南,少走弯路高效上手

Z-Image-Turbo使用避坑指南&#xff0c;少走弯路高效上手 1. 为什么需要这份避坑指南&#xff1f; Z-Image-Turbo 是阿里通义实验室推出的轻量级文生图模型&#xff0c;主打“快”与“准”——官方宣称支持单步生成&#xff0c;实测在主流显卡上平均响应时间低于20秒。但很多…

作者头像 李华