最近我把工作流里的 AI 使用方式彻底换了:不再开一堆标签页在 ChatGPT、Claude、Gemini 以及几个国产模型之间来回切换,而是把它们全部收进 Chrome 侧边栏里面。这个想法的来源是一个开源项目,做法很直接:侧边栏里同时住着 6 个 AI,一键切换对话对象;需要对比的时候就并排摆在同一个窗口里,同一个问题发出去,哪个答得好一眼就能看出来。项目标题其实已经把核心功能概括完了——Chrome 侧边栏一键切换 6 个 AI,还能多 AI 并排对比,而且源码是开放的。
我前后用了一周多,从源码构建、配置到日常使用都折腾了一遍,今天把设计思路、接入原理和实操踩坑记录完整整理出来。无论你是想直接照搬这个工具来用的人,还是想摸清楚这类多模型聚合工具该怎么做的开发者,这篇文章应该都能提供一些有实际消耗价值的东西——不是那种照着 README 念一遍的说明,而是真正到“为什么这么设计”这一层的拆解。
1. 为什么需要侧边栏 AI 工作台:从单聊到对比的核心思路
1.1 多开标签页的痛点,侧边栏为什么更合适
说实话,大部分人最开始根本不会想到用侧边栏做 AI 工作台,先入为主的想法还是“开一个网页然后跟它对话”。但真把四个五个 AI 各开一个标签页用起来,你很快就会发现问题:标签页一多,切换的成本会指数级上升。你以为切过去只要随手点一下,实际上大脑需要先回忆“刚才那个对话在哪个标签页”,找到之后还要重新阅读上下文,然后再组织语言发问题。这个过程中,你原本想验证的思路可能已经丢了一半。
相比之下,侧边栏的核心好处是“低侵入”:它不会抢占你当前的主工作区域,你在写代码、看文档、处理表格的时候,AI 就在旁边随时待命。而且侧边栏在 Chrome 里可以固定在右侧,视觉上一直存在,不用来回切换浏览器窗口。这种人机交互形态其实更贴近“助理”的定位——助理应该坐在你旁边,而不是把你叫去另一个房间开会。
这个项目选侧边栏而不是新标签页,还有一个很实际的理由:Chrome 官方的 Side Panel API 从 114 版本开始稳定,扩展用 manifest 里声明 sidePanel 的默认路径,就能直接在浏览器右侧画出一个持久面板。开发成本比做完整页面低不少,因为不需要做独立的标签页路由,也不用处理窗口关闭后状态丢失的问题。面板挂在浏览器生命周期里,关闭浏览器再打开,会话状态还可以用 storage 恢复。
1.2 一键切换与多 AI 对比的产品定位
光把 AI 塞进侧边栏还不够,真正让这个项目区别于“网页版套壳”的,是“一键切换”和“并排对比”这两个交互设计。
先说一键切换。我见过不少聚合类工具,做法是一个列表里排了二三十个模型,但切换的时候要进设置页改配置、改完再回来生成,这个开销本身就违背了聚合的初衷。这个项目把切换动作压缩到一次点击:侧边栏顶部一排按钮,每个对应一个 AI,点谁就跟谁聊。关键是每个 AI 的会话上下文互相独立——这非常关键。我在 GPT 里问到一半的技术方案,切到 Claude 问另一个问题时,GPT 那边的上下文不会被清掉,切回来还能接着聊。这种体验本质上是在模拟“你同时有几个不同风格的同事,分别帮你跟进不同方向的问题”。
再说并排对比。这个功能我一开始觉得是个噱头,实际用下来才发现它解决的是“信任问题”。AI 模型在回答专业问题的时候经常自信满满地给出看似合理、实则错误的结论,单独用的时候很难识别。但把两个模型对同一个问题的回答并排放在一起,差异立刻暴露出来。一些模型会在推理步骤上偷工减料,另一些会过度否定已知结论,对照着看我往往能更快定位到真正正确的信息。而且并排并不是要求我把两个回答逐字读完,而是用扫视的方式快速找到分歧点,再针对分歧点深挖。
从产品定位上说,这个项目解决的是一个很朴素的需求:在 AI 工具已经如此丰富的今天,用户不想被某一个模型绑定,也不愿意为了比较几个模型去开一堆窗口。把选择、对比、切换的成本降到最低,让用户把注意力放在问题本身,这就是它存在的最大价值。
2. 六个 AI 的统一接入:适配层设计与配置机制
2.1 为什么大多走 OpenAI 兼容接口
我最初有个疑问:六个不同的 AI,来源各不相同,有的是云服务商,有的是开源模型,接入方式肯定五花八门,要做多少适配才能统一起来?翻完源码才发现,这个项目用了一个非常聪明的“偷懒”策略:绝大部分接入都走 OpenAI 兼容的 Chat Completions 协议。
所谓 OpenAI 兼容,本质上就是约定了一个标准的 HTTP 接口格式:POST 一个 JSON 到/v1/chat/completions,里面带上model、messages、temperature这些参数,然后从响应里解析choices[0].message.content就能拿到回答。这套协议现在几乎成了大模型行业的事实标准。DeepSeek、Moonshot(Kimi 背后的公司)、智谱、Ollama、LM Studio、还有各种开源的本地推理框架,全都提供了兼容端点。甚至很多不是直接用 OpenAI 系模型的厂商,也会为了生态兼容专门套一层兼容壳。
这种做法给项目带来的直接收益是:不需要为每个 AI 写一套单独的网络请求逻辑。只要在配置里指定 baseUrl、model、apiKey,扩展内部就可以用同一套请求函数去处理所有请求。代码维护成本低,新接入一个模型只需要加一条配置记录,不用改任何源码。
2.2 Provider 配置结构与密钥存储
实际配置层面,这个项目的思路是把所有接入信息放在一个数组里,每个元素定义一种 AI。结构大概是这样的:
{ "providers": [ { "id": "gpt", "name": "GPT", "type": "openai-compatible", "baseUrl": "https://your-endpoint/v1", "model": "gpt-4o", "apiKey": "sk-xxxx" }, { "id": "deepseek", "name": "DeepSeek", "type": "openai-compatible", "baseUrl": "https://api.deepseek.com/v1", "model": "deepseek-chat", "apiKey": "sk-yyyy" }, { "id": "local", "name": "本地模型", "type": "openai-compatible", "baseUrl": "http://127.0.0.1:11434/v1", "model": "qwen2.5:14b", "apiKey": "ollama" } ] }注意这里我把 OpenAI 官方地址改成了your-endpoint,是因为实际使用中很多人不会直连官方,而是通过聚合网关或者中转服务来统一管理密钥和额度。这个扩展并不关心你填的 baseUrl 背后是谁,只要它支持 OpenAI 兼容协议就行。
密钥存储这块值得单独强调。这个项目把 apiKey 放在chrome.storage.local里面,而不是sync。很多刚接触 Chrome 扩展的人会顺手用chrome.storage.sync,心想还能跨设备同步,方便。但 sync 存储有 8KB 的单条数据上限限制,配置多个 provider 之后很快就超了,而且密钥这种敏感信息同步到云端本来就不够安全。local 存储是分机分用户的,不会同步出去,相对更稳妥。如果你真的想跨设备同步,我的建议是用加密工具管理密钥文件,手动导入到每一台设备,而不是靠浏览器扩展的同步机制。
2.3 不是 OpenAI 兼容的接口怎么办
虽然 OpenAI 兼容协议覆盖面很广,但确实还有几家是原生接口,代表性的比如 Anthropic 的 Claude 和 Google 的 Gemini。这两个的请求格式跟 OpenAI 风格差异很大:Anthropic 用/v1/messages端点,消息结构里区分system和user,返回的格式也完全不同;Gemini 有自己的一套contents和parts结构,流式返回还带一长串 JSON 片段。
这个项目的处理方式是写一个适配层,把原生接口转换成内部统一的消息格式再渲染。简单说就是:每种 provider type 对应一个发送函数,内部先转换成目标 API 需要的请求体,收到响应后再统一转换回标准格式。这样做的好处是,前端界面完全不需要知道当前对话的是哪个模型,永远只处理一套数据结构,模型差异被封装在适配层里。以后如果 Facebook 的 Llama 或者别的什么模型出了更优的原生接口,只需要新增一个适配函数,UI 一行都不用改。
这种“统一抽象 + 独立适配”的思路,是这个项目整个接入层设计的核心,也是它的代码结构能保持简洁的最主要原因。很多多模型聚合工具体积臃肿,就是没搞好这一层边界。
3. 一键切换与并排对比的实现细节
3.1 会话上下文隔离与共享
一键切换这个功能看起来简单,背后有一个容易忽略的设计问题:切换之后,新的对话要不要带上之前那个 AI 的上下文?
这个项目的默认策略是“隔离”。每个 provider 有自己的独立消息历史,切换时互不干扰。比如你问 GPT“帮我写一个 Python 爬虫的异常处理”,然后切到 Claude 问“帮我看看这段 SQL 为什么慢”,这两个会话是完全平行的关系。过五分钟切回 GPT,之前聊的爬虫问题还在,从断点继续问完全没问题。
这个设计我是支持的。如果切换时强制共享上下文,会有两个副作用:一是上下文窗口很快被塞满,模型还没回答就先把资源吃掉了;二是不同模型的思考方式不一样,强行共享会让回答风格混乱。比如 GPT 倾向于结构化解释,Claude 倾向于先给结论再论证,如果你问 GPT 的问题突然夹带了一大段 Claude 风格的历史对话,GPT 很可能会被带偏。
但在并排对比模式下,情况有所不同。对比模式更合理的做法是:所有面板都以同一个初始问题出发,各自独立推演,互不污染。这个项目里也确实是这么实现的:你在底部输入框敲一个问题,回车后这个问题会被同时发送到所有打开的面板里,每个面板用自己的历史上下文去处理。如果你原本在某个面板里已经聊过一段,再发起对比时它会带着自己的历史回答新问题——这其实是好事,因为你能看到不同模型在“自己语境下”的发挥。
3.2 并排模式下请求调度与流式渲染
并排对比看着热闹,实现上最麻烦的是并发请求和流式渲染。六个面板同时发请求,如果全部用 await 串行等待,只要有一个模型响应慢,整个界面就会卡住,体验非常差。所以这里必须做两件事:并发调度和逐字渲染。
并发调度很简单,核心就是把每个面板的请求封装成独立的 Promise,不互相等待。每个面板内部再处理自己的流式数据。到目前为止这个项目写得比较漂亮,底层的流式解析是每个模型响应用一个fetch的ReadableStream来读取,拿到一段就渲染一段,用户能看到打字机效果。下面是一段简化后的流式处理逻辑:
async function streamChat(provider, messages, onDelta) { const resp = await fetch(`${provider.baseUrl}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${provider.apiKey}` }, body: JSON.stringify({ model: provider.model, messages, stream: true }) }); const reader = resp.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); // 每个 chunk 是形如 data: {...} 的 SSE 数据 const lines = chunk.split('\n'); for (const line of lines) { if (!line.startsWith('data:')) continue; const jsonStr = line.replace('data:', '').trim(); if (jsonStr === '[DONE]') return; const payload = JSON.parse(jsonStr); onDelta(payload.choices?.[0]?.delta?.content || ''); } } }这里有个细节容易被新手忽略:SSE 流式返回的数据并不保证每个 chunk 都是完整 JSON。网络层可能把一条数据拆成两半传过来,也可能把多条数据合在一段里。所以在解析前先按换行符拆分成多行,再用startsWith('data:')过滤,最后处理[DONE]结束标记。不这么处理的话,大概率会遇到 JSON.parse 报错,界面突然停住。
另外,并排模式还要处理“某个面板请求失败”的情况。这个项目采取的策略是独立渲染:某个面板出错只在那个面板里显示错误信息,其他面板继续正常流式输出,不会因为一个失败就整页白屏。这属于工程经验层面的细节,但放在真实使用里非常关键,毕竟六家服务不可能一直同时健康。
3.3 同步滚动与结果对齐
并排对比还有一个容易被忽视的交互痛点:不同模型回答的长度差异可能很大。GPT 回答写了八百字,Claude 可能只写了两百字,Gemini 也许写了一串列表。如果每个面板各滚各的,你很难把两个模型的对应用上,看对比就会很费劲。
这个项目做了一件事:同步滚动。当一个面板滚动时,其他面板跟着滚动到相同的比例位置。实现上不复杂,核心是监听 scroll 事件,拿到当前面板的滚动位置scrollTop和总高度scrollHeight,算出百分比,然后同步设置其他面板的scrollTop。为了避免循环触发,需要加一个锁变量,防止一个面板的滚动事件触发其他面板的回调修改状态。这个细节如果没处理好,页面滚动会出现抖动,体验非常糟糕。
不过我实际用下来发现一个局限:面对长度差异很大的回答时,同步滚动按“比例”同步并不能做到逐行对齐。如果想实现真正的“按内容锚点对齐”(比如某个段落标题在左右两侧都出现在同一视口内),那就需要引入语义分段,按段落拆分成组件,再按段落的相对位置做滚动映射。这个项目目前没有做,属于未来可以扩展的方向。但对大多数用户来说,比例滚动已经够用了,至少能保证两个面板的大致位置一致,不用来回找。
4. 实操过程:从源码构建、安装扩展到调通六个 AI
4.1 环境准备与构建
我在本地搭建这套环境大概花了不到半小时,主要是卡在一些依赖版本的小坑上。这个项目用 Node.js 做构建,前台界面用了 React 和 Tailwind,底层是 Manifest V3 的扩展结构。构建前最好确认本地 Node 版本在 18 以上,太老的话 Vite 那一套跑不起来。
流程很常规,先拉仓库,再装依赖,然后构建。命令大概是:
git clone https://github.com/your-project/sidepanel-ai.git cd sidepanel-ai pnpm install pnpm build如果网络环境不理想,装依赖这步可能会卡住,我建议在合适的网络条件下重试,或者用国内镜像的 npm registry。装完依赖后 build 会在dist目录生成完整的扩展包。这一步比较关键,因为你说不定改了什么源码,如果不重新 build,Chrome 加载的还是旧版本。
这里有一个常见坑:构建产物默认输出的文件名带 hash 后缀,比如index-abc123.js。Manifest 里的 content script 如果写死了旧文件名,加载后扩展会一直报错。目前大多数项目会用构建工具自动生成 manifest,比如 Vite 插件会把新的资源路径写进去。如果改了文件名还是看不到效果,记得删掉旧的dist目录重新构建一次,避免残留文件混淆。
4.2 在 chrome://extensions 加载与配置
构建完成后,打开 Chrome 地址栏输入chrome://extensions/,打开右上角的“开发者模式”,点击“加载已解压的扩展程序”,选择dist目录加载。加载成功之后,工具栏会出现扩展图标,点击图标就能调出侧边栏。如果没有看到侧边栏,检查一下扩展详情里的“固定”选项,有时候默认没固定到工具栏,图标藏在折叠菜单里。
进去之后的配置点是 6 个 Provider。界面上会有一个设置入口,打开后是一个 provider 列表,每个 provider 需要填名称、baseUrl、model、apiKey。这一步相当于对着项目管理界面填参数,而不是改源码。如果项目没有做可视化配置,那就得在扩展的 options 页面里粘贴 JSON 配置。两种方式本质上没有区别,都是写 baseUrl 和 key。
填的时候有两点建议。第一,baseUrl 不要带多余路径,填到/v1这一层就够了,扩展内部的 API 调用会自动拼接/chat/completions或者/messages。多填一个路径或者漏填都会导致请求 404。第二,模型名称必须跟你实际使用的模型 ID 完全一致,大小写、点号、横线都不能错。比如 me版地址是qwen2.5:14b,你写Qwen2.5:14b可能就会直接报错。
4.3 侧边栏偏好和快捷键设置
这个项目还支持把侧边栏固定在某个特定网站或者全局生效。这个配置是在扩展详情页的“Site access”里设置。我日常习惯是“全局生效”,这样在任何页面都能呼出 AI,不用每个网站都授权一遍。如果你公司内网有敏感页面,不想在这类页面弹出 AI 面板,也可以配置“点击时才读取当前页面”,或者直接把扩展访问权限限定到指定站点。
快捷键方面默认可能没绑,需要自己去 chrome://extensions/shortcuts 里设置。我建议绑定一个全局组合键,比如Ctrl+Shift+F(在 Mac 上是Command+Shift+F),这样在任何网页里都能秒开侧边栏。我自己实测这个快捷键唤起效率特别高,写代码卡住了顺手呼出提问,问完再关掉,整个流程不超过 10 秒。
还有一个小技巧:如果同时开了好几个 Chrome 窗口,侧边栏默认是在当前窗口显示。旧窗口的侧边栏状态互不干扰,关闭窗口也不用担心其他窗口的会话丢失,因为会话数据都存在本地 storage 里。不过要注意,如果清理浏览数据时勾选了“扩展程序本地数据”,会一并把会话和密钥清掉,这个要小心别误操作。
5. 常见问题与排查记录
5.1 高频故障:白屏、请求失败、密钥丢失
我在实际使用中遇到的最多问题基本是这几类。整理成一张速查表,方便大家对照排查。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 侧边栏打开后纯白屏 | 构建产物与 manifest 路径不匹配 | 重新 build,检查 dist 目录,清理 Chrome 里的旧版本再重新加载 |
| 请求返回 401/403 | apiKey 错误、baseUrl 配错、模型无权限 | 先用 curl 测试接口,确认 key 和模型权限没问题再填到配置里 |
| 请求返回 404 | baseUrl 多了路径,或 model ID 填错 | 把 baseUrl 统一整理到/v1层,仔细核对模型 ID |
| 某模型一直转圈无输出 | 网络不通,或该模型不支持流式 | 先关掉 stream 测试一次,确认非流式接口能通再排查流式解析 |
| 配置保存后重启扩展丢失 | 用了 sync storage 超出配额 | 改用 local storage,或减少配置字段 |
| 并排模式下个别面板直接报错 | 该 provider 请求限流 | 降低并发,触发后等待一段时间再试 |
这里面最容易被坑的是 CORS 问题。Chrome 扩展在有host_permissions的情况下,扩展内发出的请求一般不受网页跨域限制,不像写一个普通网页那样会被浏览器拦截。但如果扩展的权限配置不够,或者 provider 的接口做了额外的 origin 校验,请求也会失败。遇到这类问题,优先看开发者工具 Console 面板里的报错信息,是不是明确提示CORS policy或者Invalid value for origin。如果是,就去扩展详情页确认权限范围覆盖了目标域名,或者把请求走一遍你自己部署的代理网关。
5.2 多 AI 同时请求的限流与成本控制
并排对比功能好用,代价是真的有。六个模型同时发请求,对免费额度来说非常容易触发限流。我实测 DeepSeek 免费档并发超过 2 个请求就会报rate limit exceeded,OpenAI 那边按账号等级有不同的 RPM 限制。如果你把所有面板同时打开并发起问题,大概率会看到至少一两个面板报错。
我的建议是并排时不要开满 6 个,开 2 到 3 个最有代表性的模型就够用了。一个是擅长代码的,一个是综合能力稳的,一个是本地或者免费额度大的。这样既能完成对比,又不会把额度烧完。另外请求之间可以加一个小的延迟间隔,比如错开 500 毫秒再发第二个面板的请求,很多限流问题靠这个就能解决。
成本控制方面,并排模式本质上就是同一个 prompt 发给多个模型,每个模型都会计费。聊几句不重要内容没问题,但如果你拿并排模式去处理长篇代码文件或者整篇文档,token 消耗是成倍增长的。建议把它定位成“决策辅助工具”,在真正需要对比判断时才用,日常普通问题还是单模型对话就够了。
5.3 想要私有化部署一个的进阶改造思路
如果你不想直接用云服务的 API,这个项目的结构也很适合改成私有化部署版本。我见过有人把 provider 全配置成 Ollama 的本地模型,然后在局域网内跑一个代理网关,把 Ollama 的接口暴露成一个 OpenAI 兼容端点。扩展端零改动,只需要把 baseUrl 指到内网地址,apiKey 随便填一个占位符就行,因为 Ollama 默认不做鉴权。
还有一个改造方向是给这个项目加“本地知识库”。扩展目前的架构是纯前端,想接 RAG 需要额外搭一个向量数据库服务,扩展只负责把问题发过去,由服务端做检索增强再转发给大模型。好处是可以让所有模型的回答都基于你自己的资料,而不是通用知识。这个改造的工程量不算小,但做出来之后相当于拥有一个统一的“私人团队 AI”,每个 AI 都懂你的业务背景。
另外一个轻量一点的改造是增加“回答差异高亮”。两个模型并排展示,让扩展自动识别回答里关键句子的不同,把不一致的部分用高亮标注出来。实现思路是把回答按句子切分,再用 embedding 向量算相似度,低于阈值的句子标出来。这个功能对提升对比效率帮助很大,就是因为目前手动找差异还是太费眼。我在复用这个项目时给某个版本加过简单版,效果还不错,推荐大家也试试。
最后再提一个我实际使用中的体会
把这个侧边栏 AI 工作台用了两周之后,我最大的改变不是“用 AI 更频繁了”,而是“对 AI 的回答更警惕了”。并排对比让我看到了同一个问题在不同模型笔下的巨大差异,有的模型为了讨好用户会把不存在的细节说得像真事,有的模型则会反复强调不确定性。这种差异不是单独聊一个模型就能感知到的,只有对比着看,才会逼着你养成交叉验证的习惯。
我也越来越觉得,这类开源工具的意义不只是“方便”,更重要的是把选择权重新交还给用户。我可以随时换掉某个表现不好的模型,可以接入本地模型保护隐私,可以把各个模型的答案放在一起挑选真正有用的信息。整个 AI 生态正在往百花齐放的方向走,一个能自由切换、自由对比的侧边栏入口,正好站在了这个趋势上。如果你也受够了在标签页里来回倒腾,不妨找个时间把这个项目跑起来,你会回来的。