DeepSeek本地模型Web UI实战:从推理服务到浏览器侧边栏全配置指南
简介该资源面向需要在本地运行和管理AI模型的开发者与技术爱好者提供一套开箱即用的Web UI界面将模型调用、参数调整、状态监测与历史记录集中到统一面板让不具备深厚底层知识的用户也能通过浏览器与本地模型交互满足数据上传、结果展示和性能监控等常见需求。包内共416个文件以tsx/ts源码和json配置为主辅以png图标、html页面、js脚本、样式文件以及模型识别相关资源压缩后仅约4.09MB结构紧凑便于快速部署与二次开发。目前已有1923人学习使用值得作为本地AI模型前端界面的参考实现。通过源码可以了解界面组件划分、API调用逻辑、跨平台兼容性处理及配置管理方式也可以按需扩展功能或替换底层模型适合用作学习Web前端与AI应用结合的动手样例。1. DeepSeek 本地模型终于等来一套顺手的 Web UI先说清楚这是什么本地 AI 模型最难用的往往不是模型本身而是没有一个让人愿意每天打开的界面。DeepSeek Page Assist 这套方案解决的就是把本地跑的 DeepSeek 模型装进一个 Web UI 界面——侧边栏常驻、能读当前网页、支持多轮对话而不是每次都在黑乎乎的命令行里敲 curl。它适合两类人一类是已经在本地跑模型、但受够了大模型厂商的网页交互另一类是刚入门、想用浏览器侧边栏把“网页正文 提问”直接丢给本地模型又不想碰一整套复杂前后端的新手。整个链路不依赖公网模型服务数据留在本机这也是很多人愿意折腾它的根本原因。2. 先把模型服务跑通兼容接口选型与端口、模型 ID 配置2.1 为什么要先分清“推理服务”和“UI 界面”很多人第一次接触这套方案时会有一个误区以为 Web UI 界面本身要去加载模型权重。实际上本地模型生态里最常见的做法是拆成两层。底层是推理服务它负责读模型文件、分配显存、计算 token对外暴露一个 HTTP 接口上层是 Web UI它只负责渲染聊天框、收集网页内容、把用户问题组装成请求发过去。UI 界面不加载模型也不直接读模型文件所有 AI 计算都发生在底层推理服务里。这样做的好处是解耦。你可以先单独验证模型服务是否正常再让 UI 去对接换模型权重时也不用改界面代码只需要改 model ID 或者模型文件路径。坏处是第一次配置时要理解两个进程之间的关系但一旦把链路理清后面所有问题都好排查。2.2 把本地模型服务跑起来端口、上下文长度、量化模型我这里不绑定某个具体推理引擎因为这套 Web UI 界面只认“OpenAI 兼容接口”这个约定。常见做法是启动一个带兼容接口的推理服务让它监听 127.0.0.1 的某个端口例如 11434。先看资源包里准备好的启动脚本核心命令大致是这样# 启动本地推理服务模型权重放在 models/ 目录 # 假设模型文件是 deepseek-r1-7b-q4_k_m.gguf按需替换 ./bin/inference-server \ --model models/deepseek-r1-7b-q4_k_m.gguf \ --host 127.0.0.1 \ --port 11434 \ --ctx-size 8192 \ --threads 8 \ --gpu-layers 999这里的参数含义要弄清楚--model是模型文件路径建议选量化版模型比如 q4_k_m显存占用会比原版低一大截--host 127.0.0.1表示只允许本机访问不要轻易改成 0.0.0.0否则局域网里其他设备也能调用会有被刷的风险--port 11434是服务端口后续 Web UI 界面配置的地址要和这里一致--ctx-size 8192指上下文窗口大小显存不够就降到 4096--gpu-layers 999是让尽可能多的模型层跑在 GPU 上如果显存不足改成 0 就是纯 CPU 推理。启动后如果日志里出现 “server is listening on 127.0.0.1:11434” 类似的字样说明模型服务已经起来了。此时 UI 界面还没登场先别急着打开浏览器页面需要用 curl 验证一次。2.3 用 curl 验证接口先确定模型服务没毛病Web UI 界面后续发出的请求本质上也是 HTTP POST所以这一步能用 curl 通后面界面的问题就不会出在模型服务这一层。在终端里执行curl -s http://127.0.0.1:11434/v1/chat/completions \ -H Content-Type: application/json \ -d { model: deepseek-r1:7b, messages: [{role: user, content: 用一句话介绍你自己}], stream: false }返回内容应该是一个 JSON里面会有choices[0].message.content字段。如果你装了 jq可以只提取这部分curl -s http://127.0.0.1:11434/v1/chat/completions \ -H Content-Type: application/json \ -d {...} | jq -r .choices[0].message.content如果返回 404先确认服务是否支持 OpenAI 兼容接口有些推理服务需要在启动参数里显式打开兼容开关如果返回 connection refused说明服务没起来去看日志是模型路径不对还是端口被占。这步看起来是玄学但绝大多数“页面转圈”的问题最后都回到这一层模型服务根本没在跑界面再漂亮也白搭。2.4 浏览器侧边栏访问模型时的 CORS 边界curl 能通过不代表浏览器页面里也能直接请求模型接口。浏览器扩展的侧边栏页面、普通网页脚本向 127.0.0.1:11434 发请求时会触发跨域限制。很多本地推理服务默认不开放 CORS于是请求被浏览器拦截表现就是 console 里有 CORS 报错。一个常见做法是在推理服务启动参数里加--cors或类似开关来放开跨域。但我更推荐的做法是不要直接放开而是加一个本地代理层由代理层去请求模型服务浏览器只和代理层同源交互。这样既绕开 CORS又能统一做页面正文拼装、超时控制和上下文截断后面会专门写这一点。3. 把 Page Assist 界面接上最小代码跑通第一轮对话3.1 为什么一定要加一个本地代理层先看整条链路浏览器侧边栏页面发请求给代理层代理层组装消息、附带页面上下文再转发给本地推理服务推理服务返回结果后原路回传。这个代理层可以很简单甚至不用框架但它承担三个职责处理跨域、做 system prompt 拼装、控制模型请求超时。我见过有人跳过代理层直接在前端用 fetch 请求模型接口结果要么被 CORS 卡住要么把模型地址写死在代码里每次换模型都要改前端。更麻烦的是浏览器页面里如果把整页正文塞进请求体前端代码会越来越乱没法做 token 截断和后端缓存。所以哪怕只是自己用也值得把代理层单独写成一个服务。3.2 一个能跑的最短代理程序资源包里的后端是一个 Python 文件依赖 Flask 和 requests。核心实现并不复杂完整代码如下# app.py # 运行前提本地推理服务已经监听 127.0.0.1:11434 import os import requests from flask import Flask, request, jsonify app Flask(__name__) MODEL_URL os.getenv(MODEL_URL, http://127.0.0.1:11434/v1/chat/completions) MODEL_NAME os.getenv(MODEL_NAME, deepseek-r1:7b) MAX_PAGE_CHARS int(os.getenv(MAX_PAGE_CHARS, 6000)) app.route(/api/chat, methods[POST]) def chat(): body request.json or {} user_text (body.get(message) or ).strip() if not user_text: return jsonify({error: empty message}), 400 page_context (body.get(page_context) or )[:MAX_PAGE_CHARS] messages [] if page_context: messages.append({ role: system, content: ( 下面是当前网页的正文内容。请基于它回答用户问题 不要复述整篇页面不要编造页面中不存在的事实。\n\n---\n page_context \n--- ) }) for h in body.get(history, []): messages.append({role: h[role], content: h[content]}) messages.append({role: user, content: user_text}) payload { model: MODEL_NAME, messages: messages, temperature: float(body.get(temperature, 0.3)), max_tokens: int(body.get(max_tokens, 2048)), stream: False, } try: resp requests.post(MODEL_URL, jsonpayload, timeout180) resp.raise_for_status() return jsonify(resp.json()) except requests.exceptions.Timeout: return jsonify({error: model timeout}), 504 except requests.exceptions.ConnectionError as exc: return jsonify({error: fcannot connect to model: {exc}}), 502 if __name__ __main__: app.run(host127.0.0.1, port8080, debugFalse)这段代码的逻辑很直白前端 POST 一个 JSON 到/api/chat里面包含message、page_context、history三个字段后端先把页面上下文放到 system 消息里再将历史对话按原顺序加入最后把用户问题放到最后一条。这样做能保证模型始终看到“先页面、再历史、后新问题”的顺序避免页面正文淹没在最前面导致上下文错位。参数设置有讲究MAX_PAGE_CHARS默认 6000对应中文网页正文大约 3000 token太长会挤占上下文窗口temperature默认 0.3因为页面问答场景更看重准确性太高的温度容易让模型自由发挥timeout180给足推理时间本地模型跑长页面的速度本来就慢30 秒超时很容易误杀。启动方式也很简单# 在资源包目录下先装依赖 pip install flask requests # 启动代理层监听 8080 python app.py如果模型服务端口不是 11434或者模型 ID 不叫deepseek-r1:7b可以用环境变量覆盖不必改代码MODEL_URLhttp://127.0.0.1:11434/v1/chat/completions \ MODEL_NAMEdeepseek-r1:7b \ python app.py3.3 前端侧边栏把聊天框和“读当前页”按钮做出来代理层就绪后需要有一个浏览器侧边栏页面来承载 Web UI 界面。最简单的页面包含三块一个只读的页面内容预览区、一个用户输入框、一个输出区外加一个“读取当前页”按钮。关键代码是用 chrome.scripting API 获取当前标签页的正文然后填充到预览区再在发送时把这段文本一起 POST 给代理层// 从当前标签页提取正文文本 async function extractPageContent() { const tabs await chrome.tabs.query({ active: true, currentWindow: true }); const [{ result }] await chrome.scripting.executeScript({ target: { tabId: tabs[0].id }, func: () { const clone document.body.cloneNode(true); clone.querySelectorAll(script, style, nav, footer, iframe) .forEach(n n.remove()); return (clone.innerText || ).replace(/\n{3,}/g, \n\n).trim(); } }); return result; } // 发送消息 async function sendMessage() { const message document.querySelector(#userInput).value; const pageContext document.querySelector(#pageContext).value; const history []; // 真实场景会维护多轮历史 const resp await fetch(http://127.0.0.1:8080/api/chat, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ message, page_context: pageContext, history }) }); const data await resp.json(); document.querySelector(#output).textContent data.choices?.[0]?.message?.content ?? data.error; }这段前端代码用的是浏览器扩展的标准写法需要申请scripting、activeTab权限。如果你不想做成浏览器扩展也可以做成普通 Web 页面手动把网页正文粘贴到预览区但那样就失去了“读当前页”的便利性。建议优先做成扩展侧边栏即使切换标签页也能常驻。3.4 从“能对话”到“能干活”的配置检查表第一轮对话跑通后建议按下面的表格核对一遍参数避免后面踩坑配置项推荐值说明MODEL_URLhttp://127.0.0.1:11434/v1/chat/completions必须对应推理服务实际监听地址MODEL_NAMEdeepseek-r1:7b要与模型服务返回的模型 ID 一致MAX_PAGE_CHARS6000超出部分直接截断避免上下文爆掉temperature0.3页面问答建议偏低闲聊可以调回 0.7max_tokens2048回答长度上限按你的业务场景调整timeout180大模型生成慢超时太短会误报host127.0.0.1代理层不要暴露到局域网4. 页面上下文是怎么进上下文的从 click 到 prompt 的完整链路4.1 获取网页正文innerText 和 Readability 哪个更合适侧边栏上那个“读取当前页”按钮背后不只是 copy 一段文字它决定了模型能否准确回答。常见的提取方式有两种。第一种是直接用innerText它返回的是浏览器渲染后的可见文本包含页面的导航、侧边栏、广告区等杂物但实现简单适合结构复杂的 SPA 页面。第二种是类似 Readability 的正文提取算法它会找到文章主体内容去掉大部分导航噪音但对动态渲染的页面不够稳定。我的处理方式是先克隆 DOM再手动移除 script、style、nav、footer、iframe 等噪音节点最后取 innerText。这样做既保留了渲染后的文本又减少无用信息进入 prompt。注意一定要克隆节点直接操作 body 会破坏页面原本的 DOM导致用户看到的页面发生变化。提取代码里加了一个细节把连续三个以上换行压缩成两个。原因是有些页面源码里大段空白会被 innerText 保留压缩后能减少 token 浪费也不会影响模型理解段落结构。4.2 System prompt 的拼装页面正文放哪里、用户问题放哪里页面文本拿到以后下一步是把它放进请求里。很多人直接把它和用户问题拼在一起作为 user message样子是“用户问题 页面内容”这会让模型分不清哪部分是问题、哪部分是资料。正确做法是把页面上下文塞进 system prompt并且用醒目的分隔符框起来。回到第三章那段 Python 代码它构造出的 system 消息结构是system_content ( 下面是当前网页的正文内容。请基于它回答用户问题 不要复述整篇页面不要编造页面中不存在的事实。\n\n---\n page_context \n--- )这样模型收到的指令顺序是system 说明规则 - 页面正文 - 历史对话 - 用户最新问题。每次请求都携带完整上下文这是本地模型 API 的基本约束它没有跨请求记忆所以 page_context 必须重复发送而代理层要负责控制发送长度。还有一个容易忽略的点如果页面内容为空就不要构造 system prompt直接发送用户 message。否则系统提示里只有一句空话反而可能影响模型输出格式。4.3 长页面与多轮对话截断策略和 token 估算长网页是不可避免的一篇几千字的文章很容易突破上下文窗口。固定截断是最保守的方案也是资源包里默认的做法取前 6000 字丢掉后面。问题是如果关键结论在文章末尾模型就看不到。第二种是用滑动窗口截取正文的前后两部分。比如取前 1500 字和后 1500 字中间用一行省略号拼接。这样模型能看到开头和结尾对很多“先背景后结论”的文章来说有奇效。代价是回话题跳变剧烈时中间细节丢失。第三种是先让模型做一轮摘要再把摘要作为页面上下文。这种方案最稳但要多一次模型调用响应时间几乎翻倍。我的建议是日常问答用固定截断遇到论文或操作手册这类需要结论的内容临时调成前后窗口截断摘要方案只留给对准确率要求较高的场景。截断策略实现成本上下文保真度适用场景固定截断低中大部分网页、新闻、聊天记录前后窗口低中高长文、结论在文末的内容先摘要再问答高高论文、合同、技术手册截断时要留意中文字符和 token 不是简单的 1:1 关系。在 DeepSeek 这类模型中几千个汉字对应几千 token所以 MAX_PAGE_CHARS 不宜超过上下文窗口的一半否则留给对话历史和回答生成的空间就不够了。5. 避坑本地模型 Web UI 最常见的五个翻车点5.1 模型服务偶尔拒绝连接先分清是 CORS 还是服务挂了现象是界面能打开但一发送消息就报Failed to fetch或cannot connect to model控制台里能看到 CORS 错误。原因有两个层次如果 curl 能连通但浏览器报错那是跨域拦截如果 curl 也连不上那就是模型服务压根没在跑或者端口被占用。很多人一看到 CORS 就去改模型服务配置结果把端口暴露到了局域网风险没解决问题还在。解决方法是先 curl 一次模型服务接口确认底层正常再让前端请求代理层而不是直连模型服务。代理层属于同源请求天然绕开 CORS不需要在模型服务上放开跨域。5.2 明明把整页都传了模型却说“没有看到网页内容”现象是你点击“读取当前页”之后问题问的也是页面里的细节但模型回答得像一个完全没读过页面的陌生人。原因是页面正文被截断了。常见的截断点有两个一是前端提取时就取了前 8000 字二是后端 MAX_PAGE_CHARS 又截了一次。如果关键信息恰好在被丢掉的后半部分模型自然看不到。解决方法是先看代理层日志确认请求体里 page_context 的字符数再对照页面实际长度。如果确实被截断用第四章的前后窗口截断策略或者主动把页面按段落切成多块让模型先做局部摘要。5.3 多轮对话越到后面越慢重复发送大量上下文现象是前两轮响应很快到第三四轮开始明显变慢显存占用也持续升高。原因是很多实现会把对话历史里的每一条消息都重新发送一遍同时页面正文又没有被抽出来单独缓存导致每轮请求的 token 数越来越大。模型服务要重复处理同样的文本慢是必然的。解决方法是前端只维护一段有上限的历史例如保留最近 10 轮消息页面正文则固定放在 system 消息里不重复塞进每一条历史。如果整页正文超过 6000 字可以做一个简单缓存同一标签页在短时间内重复提问时后端只接收 page_key不重复接收正文。5.4 模型答非所问还特别啰嗦现象是回答里反复出现“根据您的要求”“综上所述”这类套话或者在一个问题上绕来绕去。原因典型的两个temperature 设置太高模型自由发挥DeepSeek 这类模型本身又有明显的“复读”倾向system prompt 里没有给出输出约束。解决方法是把 temperature 降到 0.2 到 0.3并且在 system prompt 里加一句“直接给结论不要复述问题不要分点列条”。注意不要把这个问题当成模型坏了它只是没有收到足够强的约束信号。5.5 UI 界面报 500 但模型服务正常现象是代理层返回 500 错误但用 curl 请求模型服务完全正常。原因通常是代理层传给模型服务的请求参数出了问题模型 ID 不存在、max_tokens 超出模型上限、history 里出现了空的 role。本地模型的错误信息往往不会通过 HTTP 状态码返回而是挂在 JSON error 字段里Flask 默认会把它包装成 500。解决方法是打开代理层的 debug 输出先看 requests.post 返回的原始响应的 text 内容而不是直接 raise_for_status。也可以把debugTrue临时开启看清完整调用栈问题基本都在 payload 参数上。6. 进阶把整条链路做成可重复验证的脚本6.1 用一个 30 行的脚本做端到端冒烟测试UI 界面能手动跑通只是第一步真正让这套方案长期好用的是自动化验证。我习惯在资源包里放一个最小冒烟脚本每次改完配置先跑一遍而不是打开浏览器手动点。# smoke_test.py # 用法先启动模型服务再启动 python app.py最后执行本脚本 import requests import time BASE_URL http://127.0.0.1:8080/api/chat PAGE_TEXT ( 某软件公司发布了一款新的本地模型管理工具 它支持在浏览器侧边栏中与本地模型对话。 ) payload { message: 这款工具的亮点是什么, page_context: PAGE_TEXT, history: [], temperature: 0.3, max_tokens: 512, } start time.time() resp requests.post(BASE_URL, jsonpayload, timeout180) cost time.time() - start print(HTTP 状态码:, resp.status_code) data resp.json() if resp.status_code ! 200: print(错误信息:, data.get(error)) raise SystemExit(1) answer data[choices][0][message][content] print(回答:, answer[:120]) print(耗时:, round(cost, 2), 秒) # 冒烟验证回答里必须提到工具的核心关键字之一 assert 侧边栏 in answer or 本地模型 in answer, 回答没有命中页面关键信息 print(冒烟测试通过)这个脚本虽然短但覆盖了最关键的三件事链路是否通、响应时间是否可接受、模型是否真正读到了页面内容。断言条件故意写得宽松因为模型回答没有固定句式只要命中页面里的核心关键词就说明 prompt 拼装没跑偏。6.2 把测试和数据准备沉淀到资源包里我建议把这份脚本和几个不同长度的样本网页放进资源包每次测试时用同一个样本跑一遍看到的响应差异就只来自模型参数变化而不是页面内容变化。我还会把耗时记录加进去因为本地模型在换模型文件之后响应速度变化能提前暴露显存不足或上下文窗口过大的问题。顺带分享一个教训我以前图省事总是改完配置直接在浏览器里点按钮验证结果有两次页面正文被截断模型答非所问我在接口日志里翻了半天才想起来去看请求体长度。从那以后我每次改代理层或模型参数都强制跑一遍冒烟测试页面数据用固定样本断言只检查关键点半小时的排查被压缩成了几十秒。这套方法希望能帮到你。本文还有配套的精品资源点击获取