😋 我让 DeepSeek-R1 在浏览器本地跑了起来,后端同事说"你认真的?"

18 阅读7分钟

写在前面:今天学了一个让我"惊掉下巴"的知识——端侧模型。以前我们用 AI,都得调远程 API,数据发出去,等半天,还得担心隐私泄露。老师说,现在小参数模型已经可以在浏览器里跑了!对,你没看错——浏览器里跑 LLM。我们亲手做了一个 React + TypeScript + TailwindCSS 的项目,在浏览器端用 WebGPU 加载 DeepSeek-R1 的 1.5B 蒸馏模型。当看到模型在本地加载完毕、聊天框可以打字的那一刻,我整个人都兴奋了——以后再也不用把数据发给第三方了!


一、端侧模型:AI 也要"本地化"

1.1 远程 API 调用的痛点

老师说:

"有别于 OpenAI/DeepSeek API 调用——LLM 在远程,和调用客户端不在一起。贵、不安全。Context 会随着请求发送到服务器。"

远程调用的三大问题:

问题说明比喻
百万 Token 几块钱,频繁调用钱包疼每次打车去吃饭,不如楼下小吃店
不安全你的 Prompt 和数据会发到别人服务器上你把日记本寄给陌生人帮你读
依赖网络没网就不能用断网就成了砖头

1.2 端侧模型的好处

老师说:

"Ollama 本地开源模型部署,在客户端——端侧模型。手机端、汽车端、Agent 端。开源小参数模型就能完成这些任务。浏览器端,随时下载,随时使用。WebGPU。"

端侧模型 = 模型在用户本地运行,不联网,不出门。

我们的项目用的就是 DeepSeek-R1-Distill-Qwen-1.5B——一个 15 亿参数的蒸馏版推理模型,专门优化到可以在浏览器里用 WebGPU 加速运行。


二、React + TypeScript + TailwindCSS:AI 时代的"基建"

2.1 为什么选这套技术栈?

老师说:

"React + TS —— AI 时代的大型项目首选前端技术。React 比 Vue 难入门,但大型项目用得多,AI 训练代码 React 的偏多。"

TailwindCSS 更是成了 Vibe Coding 的标配:

老师说:

"几乎不再需要写 CSS——原子类 CSS。TailwindCSS 已经成为 Vibe UI 的基本构成。写类名就行,自然语义编程。"

以前写 CSS:

.container {
    display: flex;
    flex-direction: column;
    align-items: center;
    height: 100vh;
    background-color: white;
}

用 TailwindCSS:

<div className="flex flex-col items-center h-screen bg-white">

这就是"自然语义编程"——在 JSX 里用英文单词描述样式,不用再写选择器和 CSS Rules。

2.2 React 组件:函数就是组件

"搭积木的方式搭建页面。React 封装一个组件——函数就是组件。函数返回 HTML 就是组件。"

Progress 组件——一个进度条:

function formatBytes(size) {
    const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
    return (
        +(size / Math.pow(1024, i)).toFixed(2) * 1 +
        ["B", "kB", "MB", "GB", "TB"][i]
    );
}

const Progress = ({ text, percentage, total }) => {
    percentage ??= 0;  // ES12 空值合并运算符
    return (
        <div className="w-full bg-gray-100 text-left rounded-lg overflow-hidden mb-0.5">
            <div
                style={{ width: `${percentage}%` }}
                className="bg-blue-400 whitespace-nowrap px-1"
            >
                {text}
                {percentage.toFixed(2)}%
                {isNaN(total) ? "" : `of ${formatBytes(total)}`}
            </div>
        </div>
    );
};

export default Progress;

几个关键点:

概念说明代码
Props父组件传过来的属性,子组件只展示不修改{ text, percentage, total }
空值合并ES12 语法,没传参时默认 0percentage ??= 0
JSX在 JS 里写 HTML<div className="...">{text}</div>
className因为 class 是 JS 关键字,改用 classNameclassName="w-full bg-gray-100"

老师说:

"封装者多考虑,使用者用得爽。percentage ??= 0——当 percentage 为空时赋值为 0。初始化的时候,没有下载进度这个概念。如果传了呢?传入的值不赋值。"


三、React 的两种数据:State 和 Props

3.1 State:组件自己的状态

App.tsx 中的状态管理:

const [input, setInput] = useState('');        // 输入框内容
const [status, setStatus] = useState(null);    // 模型状态 null | loading | ready
const [error, setError] = useState(null);       // 错误信息
const [loadingMessage, setLoadingMessage] = useState('开始加载...');
const [progressItems, setProgressItems] = useState([]);  // 下载进度列表

老师特别强调了一种设计思路:

"数据状态驱动界面状态。数据有不同的状态,界面不同的状态——川剧变脸。"

status 的值界面的表现
null显示"Load Model"按钮
"loading"显示下载进度条,按钮禁用
"ready"显示聊天输入框

这种"状态驱动"的思维,让 UI 逻辑变得非常清晰。

3.2 Props:父组件传给子组件

// 父组件 App 中循环渲染 Progress
progressItems.map(({ text, percentage, total }, i) => (
    <Progress
        key={i}
        text={text}
        percentage={percentage}
        total={total}
    />
))

老师说:

"Props 从父组件传递到子组件的属性,不能在子组件里修改。子组件主要负责展示——父组件给我什么 Props,我显示成什么样子。报告父组件,才可以修改。"

State = 组件自己的"私房钱"。 Props = 父母给的"零花钱",只能花不能改。


四、React 合成事件和原生事件的区别

4.1 DOM 事件的发展史

event.html

<button id="btn" onclick="console.log('点击了按钮')">按钮</button>

<script>
    document.getElementById('btn')
        .addEventListener('click', function() {
            console.log('点击了按钮')
        })
</script>

老师说:

"onclick 是最原始的 DOM0 级事件监听。addEventListener 是 DOM2 级事件监听——同一个 DOM 元素可以多次监听同一事件。"

4.2 React 的合成事件

"React 代码洁癖——能不发明新概念就不发明。@ 事件绑定是 Vue 的做法。React 直接用已有的概念,onClick 作为高手没有学习成本。"

Vue:

<button @click="handleClick">点我</button>

React:

<button onClick={() => { setStatus("loading"); }}>点我</button>

老师说:

"React 里的事件并不是原生事件,是合成事件(SyntheticEvent)。它封装了浏览器的原生事件,解决了跨浏览器兼容性问题。"

onClick 而不是 @click,用 className 而不是 class——React 的原则就是"尽量不发明新概念"。


五、检测 WebGPU + 状态驱动的 UI

const IS_WEBGPU_AVALABLE = !!navigator.gpu;

" 取反——navigator.gpu 不支持的时候是 undefined!! 再取反,一定可以转成 true | false。双重否定等于肯定。"

然后整个 App 的返回根据这个值分为两种:

return IS_WEBGPU_AVALABLE ? (
    // 支持 WebGPU —— 显示完整的 LLM 界面
    <div className="flex flex-col h-screen mx-auto items-center ...">
        {/* 标题、按钮、进度条、聊天框 */}
    </div>
) : (
    // 不支持 —— 提示用户
    <div>您的浏览器还不支持 WebGPU</div>
);

这个三元表达式就是"条件渲染"——根据不同的条件,渲染不同的 UI。


六、总结:端侧 AI 是未来

概念说明
端侧模型在客户端本地运行 LLM,不依赖远程 API
WebGPU浏览器端 GPU 加速 API,支持本地模型推理
React 组件函数返回 JSX,组件化搭建页面
JSXJavaScript + XML,在 JS 里写 HTML
State组件自身的状态,useState 管理
Props父组件传给子组件的属性,只读
合成事件React 封装的跨浏览器事件系统
TailwindCSS原子类 CSS,自然语义写样式
空值合并 ??=ES12 语法,变量为空时才赋值

端侧模型的优势:不用花钱调 API、数据不出本地、断网也能用。React + TypeScript + TailwindCSS 是 AI 时代前端的标准基建,这套技术栈让你能快速构建出 AI 应用的界面。


写在最后

今天最大的收获,是亲眼看着一个 LLM 在浏览器里跑了起来。以前觉得"在本地跑 AI"是算法工程师的事,现在发现前端也能做——用 WebGPU + Transformers.js + ONNX Runtime,前端工程师也能让 AI 在用户浏览器里"原地运行"。这种感觉太酷了。

下次面试官问你:"端侧模型和 API 调用有什么区别?React 的合成事件是什么?"

你可以淡定地说:

"端侧模型是在客户端本地运行 LLM,比如在浏览器里通过 WebGPU 加载 DeepSeek-R1 的蒸馏模型。相比远程 API 调用,它的优势是免费、数据安全(不出本地)、离线可用。React 的合成事件(SyntheticEvent)是对浏览器原生事件的跨浏览器封装,解决了不同浏览器事件模型的兼容性问题。React 用 onClick 等驼峰命名绑定事件,而不是 Vue 的 @click——基本原则是尽量不发明新概念。在项目中我们还用了 TailwindCSS 的原子类来写样式,不需要手写 CSS,直接在 className 里用自然语义描述样式。"

然后看着面试官满意的表情,心里默念:这波,又稳了。


本文所有代码示例均来自课堂学习资料,真实可运行。