端侧大模型:DeepSeek-R1 WebGPU 推理全流程源码深度解析

前言

随着端侧 AI 技术的快速发展,在浏览器本地运行大模型已经从概念走向落地。本文将完整拆解一套基于 React + WebWorker + WebGPU + Transformers.js 的 DeepSeek-R1 推理项目,从后台推理线程到前端交互逻辑,逐行解析每一处代码的设计思路与底层原理,带你彻底搞懂浏览器本地大模型的运行机制。

项目核心能力:在浏览器纯前端运行 DeepSeek-R1-Distill-Qwen-1.5B 推理模型,所有计算都在用户本地完成,数据不经过任何服务器,加载完成后可离线使用。

一、整体架构:双线程协作模型

1.1 为什么采用双线程设计

大模型推理是重计算任务,如果放在主线程执行,会直接阻塞页面渲染和用户交互,导致页面卡死。因此行业通用方案是主线程 + Worker 后台线程的双线程架构:

  • 主线程(React) :负责 UI 渲染、用户交互、聊天消息状态管理,轻量运行保证页面流畅。
  • Worker 线程:负责模型下载、分词、推理计算等重负载任务,后台运行不阻塞 UI。

1.2 线程通信机制

两个线程通过浏览器原生的 postMessage 进行消息传递,约定统一的消息格式:{ type: 指令类型, data: 数据 },实现指令下发与状态回传。

1.3 核心技术栈

表格

技术 作用
@huggingface/transformers JS 端侧推理框架,兼容 ONNX 模型,支持 WebGPU 硬件加速
WebWorker 浏览器后台线程,隔离重计算任务,避免阻塞主线程
WebGPU 浏览器 GPU 并行计算接口,大幅提升模型推理速度
React 前端 UI 框架,管理状态与交互

二、Worker 推理线程:核心逻辑全拆解

Worker 线程是大模型运行的核心,所有模型加载、分词、生成计算都在这里完成。

2.1 依赖导入:四大核心 API

javascript 复制代码
import {
  AutoTokenizer,          // 分词器:文本与模型 token id 互转
  AutoModelForCausalLM,   // 因果语言模型:执行推理生成
  TextStreamer,           // 文本流式输出器:实现打字机效果
  InterruptableStoppingCriteria, // 可中断停止条件:支持中途停止生成
} from "@huggingface/transformers";
  1. AutoTokenizer(分词器) 负责自然语言与模型内部数字 id 的双向转换:输入文本时,将文字拆分为 token 并映射为数字;输出时,将数字 id 还原为可读文本。不同模型有不同的词表,from_pretrained 会自动加载对应模型的分词配置。
  2. AutoModelForCausalLM(自回归语言模型) 大模型推理的核心实例,负责接收输入 token,计算并生成下一个 token。支持指定 WebGPU 设备,调用浏览器 GPU 算力完成并行计算。
  3. TextStreamer(流式输出器) 流式输出工具,模型每生成一个 token 就触发对应回调,无需等待全部生成完毕,以此实现打字机效果。
  4. InterruptableStoppingCriteria(可中断停止条件) 模型每生成一个 token 都会检查停止条件。通过它可以实现用户中途打断生成的功能,无需等待全部输出结束。

2.2 TextGenerationPipeline:单例懒加载流水线

kotlin 复制代码
class TextGenerationPipeline {
  // HuggingFace 模型仓库 ID
  static model_id = "onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX";

  static async getInstance(progress_callback = null) {
    // 空值赋值:只在首次调用时加载分词器
    this.tokenizer ??= AutoTokenizer.from_pretrained(this.model_id, {
      progress_callback,
    });

    // 空值赋值:只在首次调用时加载模型
    this.model ??= AutoModelForCausalLM.from_pretrained(this.model_id, {
      dtype: "q4f16",
      device: "webgpu",
      progress_callback,
    });
    
    // 并行加载,同时返回分词器和模型实例
    return Promise.all([this.tokenizer, this.model]);
  }
}

设计解析

  1. 单例模式 + 懒加载 使用类静态属性存储实例,配合空值赋值运算符 ??=,只有第一次调用时会执行下载和初始化,后续调用直接返回已有实例。

    为什么必须用单例?模型文件体积达到 GB 级别,加载耗时久、占用显存高,全局只需要一份实例,重复加载会造成严重的性能浪费和内存溢出。

  2. from_pretrained 核心参数

    • model_id:HuggingFace 上的模型仓库地址,Transformers.js 会自动从仓库下载分词器文件、模型权重、配置文件。
    • progress_callback:下载进度回调,每次进度更新都会触发,用于在主线程渲染加载进度条。
    • dtype: "q4f16":模型量化格式,4 位整数量化 + 16 位浮点混合精度,大幅减小模型体积和显存占用,是端侧推理的标准优化手段。
    • device: "webgpu":指定推理设备为 WebGPU,调用浏览器 GPU 算力,推理速度远高于 CPU。
  3. Promise.all 并行加载 分词器和模型文件并行下载,最大化利用带宽,缩短加载总时长。

2.3 全局状态变量

csharp 复制代码
// 可中断停止条件全局实例
const stopping_criteria = new InterruptableStoppingCriteria();
// KV 缓存:保存历史注意力计算结果
let past_key_values_cache = null;
  1. InterruptableStoppingCriteria 全局唯一的停止条件实例。模型每生成一个 token 都会检查它的状态,调用 interrupt() 方法可将状态置为中断,模型立即停止生成,实现用户中途打断。

  2. past_key_values_cache(KV 缓存) 自回归模型生成时,每一步都需要计算所有历史 token 的注意力权重。如果每轮对话都从头计算,会浪费大量算力。 KV 缓存会把历史 token 的 Key 和 Value 向量保存下来,下一轮生成时直接复用,无需重复计算,可大幅提升多轮对话的推理速度。

    注意:开启新对话时必须手动置为 null 清空,否则会残留旧的上下文,导致回答错乱。

2.4 generate 函数:推理生成全流程

这是整个项目最核心的函数,负责接收聊天消息、格式预处理、流式生成、结果回传。

2.4.1 输入预处理:聊天模板格式化

php 复制代码
const inputs = tokenizer.apply_chat_template(messages, {
  add_generation_prompt: true,
  return_dict: true,
});

大模型不能直接识别 [{role, content}] 格式的消息数组,必须按照训练时约定的对话格式拼接成文本。

  • apply_chat_template:自动按照模型对应的聊天模板(DeepSeek/Qwen 系列为 ChatML 格式),把消息数组拼接成规范文本,格式类似:

    sql 复制代码
    <|im_start|>user
    用户的问题<|im_end|>
    <|im_start|>assistant
  • add_generation_prompt: true:在文本末尾自动追加 <|im_start|>assistant\n,告诉模型 "现在轮到你输出回答了",是生成任务的必选配置。

  • return_dict: true:一步到位,直接返回包含 input_idsattention_mask 的张量对象,可以直接传给 model.generate 使用。如果设为 false,只返回拼接后的字符串,还需要手动调用分词器二次编码。

2.4.2 思考标签预处理:识别推理过程

DeepSeek-R1 是推理模型,输出内容会被 ... 标签包裹思考过程。我们需要提前获取这两个标签对应的 token id,用于流式过程中区分思考内容和正式回答。

php 复制代码
const [START_THINKING_TOKEN_ID, END_THINKING_TOKEN_ID] = tokenizer.encode(
  "",
  { add_special_tokens: false },
);
  • tokenizer.encode:把文本转换为 token id 数字数组。
  • add_special_tokens: false:不额外添加模型的开始、结束特殊标记,只编码传入的标签本身。
  • 解构赋值:数组第一个值对应 (思考开始标记),第二个值对应 (思考结束标记)。
  • 为什么提前编码?流式过程中拿到的是原始数字 id,直接比对数字比转成字符串再比对性能高得多。

2.4.3 状态与性能统计变量

csharp 复制代码
let state = "thinking";   // 输出状态:thinking 思考中 / answering 正式回答
let startTime;            // 第一个 token 生成的时间戳
let numTokens = 0;        // 已生成的 token 总数
let tps;                  // 每秒生成 token 数(Tokens Per Second),推理性能核心指标
  • state:标记当前输出版块,后续可根据状态控制是否展示思考内容。
  • tps:大模型推理的核心性能指标,数值越高代表生成速度越快。

2.4.4 Token 级回调:状态切换与性能统计

ini 复制代码
const token_callback_function = (tokens) => {
  // 只在第一个 token 到达时记录开始时间
  startTime ??= performance.now();

  // 计算 TPS 性能指标
  if (numTokens++ > 0) {
    tps = (numTokens / (performance.now() - startTime)) * 1000;
  }

  // 检测到思考结束标签,切换为正式回答状态
  if (tokens[0] == END_THINKING_TOKEN_ID) {
    state = "answering";
  }
};
  • 触发时机:模型每生成一个原始 token id 就调用一次,拿到的是未处理的数字数组。

  • 核心逻辑

    1. startTime ??= ...:空值赋值运算符,只在第一个 token 到达时记录起始时间,后续不再更新。
    2. TPS 计算:总 token 数除以耗时,乘以 1000 转换为秒级单位。
    3. 状态切换:检测到 `` 对应的 token id 时,将状态从 thinking 改为 answering
  • 注意:这个回调只负责修改状态和统计性能,不负责向前端输出文本。

2.4.5 文本回调:流式推送打字内容

lua 复制代码
const callback_function = (output) => {
  self.postMessage({
    status: "update",
    output,
    tps,
    numTokens,
    state,
  });
};
  • 触发时机:token 被解码成文本、过滤掉特殊标签之后调用。
  • 作用:把生成的文本片段、性能数据、当前状态一起发送给主线程,主线程更新消息内容,实现打字机效果。
  • 配合 skip_special_tokens: true 配置,这里收到的 output 已经自动去掉了 ``、<|im_end|> 这类特殊标签,是干净的自然语言文本。

2.4.6 TextStreamer 流式输出器初始化

arduino 复制代码
const streamer = new TextStreamer(tokenizer, {
  skip_prompt: true,
  skip_special_tokens: true,
  callback_function,
  token_callback_function,
});

TextStreamer 是 Transformers.js 提供的流式工具,把模型生成的 token 流转化为回调事件。

参数解析:

  • skip_prompt: true:跳过输入的 prompt 部分,只回调模型新生成的内容。如果设为 false,会把用户输入的问题也重新回调一遍,造成内容重复。

  • skip_special_tokens: true:在文本回调中自动过滤特殊 token(如思考标签、对话标记),只返回纯文本。

    关键注意:这个配置只影响文本回调,不影响 token 回调,token 回调依然能拿到完整的原始 id。

  • 两个回调函数:分别对应原始 token 阶段和解码后文本阶段,职责分层清晰。

2.4.7 启动生成与结果处理

php 复制代码
// 通知主线程:生成开始
self.postMessage({ status: "start" });

const { past_key_values, sequences } = await model.generate({
  ...inputs,
  // past_key_values: past_key_values_cache,
  do_sample: false,
  max_new_tokens: 2048,
  streamer,
  stopping_criteria,
  return_dict_in_generate: true,
});

// 保存本轮 KV 缓存,供下一轮对话复用
past_key_values_cache = past_key_values;

// 把完整 token 数组批量解码为文本
const decoded = tokenizer.batch_decode(sequences, {
  skip_special_tokens: true,
});

// 通知主线程:生成完成,返回完整结果
self.postMessage({
  status: "complete",
  output: decoded,
});

逐段解析:

  1. 发送 start 消息 :通知主线程生成开始,主线程可更新 isRunning 状态、创建空的 assistant 消息占位。

  2. model.generate 核心参数

    • ...inputs:传入预处理好的 input_idsattention_mask
    • past_key_values:传入上一轮的 KV 缓存,复用历史计算。代码中默认注释,开启后多轮对话速度会显著提升。
    • do_sample: false:贪心解码模式,每次都选择概率最高的 token,输出稳定可复现。设为 true 会开启随机采样,回答更多样但稳定性下降。
    • max_new_tokens: 2048:限制最大生成 token 数量,防止模型无限输出。
    • streamer:传入流式输出器,模型生成过程中自动触发回调。
    • stopping_criteria:传入可中断停止条件,支持中途停止。
    • return_dict_in_generate: true:以对象形式返回结果,包含 sequences(生成的 token 序列)和 past_key_values(KV 缓存)。设为 false 只返回 token 数组,拿不到 KV 缓存。
  3. 关键执行时序

    • await model.generate 会一直阻塞,直到模型生成结束(或被中断)才会继续向下执行。
    • 但阻塞期间,streamer 的两个回调会持续触发,打字机效果一直在运行。
    • 也就是说:流式推送是生成过程中持续进行的,而 complete 消息是全部结束后才发送的,两套机制并行工作。
  4. 结果后处理

    • 保存 KV 缓存到全局变量,供下一轮对话复用。
    • batch_decode:把完整的 token 数字数组批量解码为自然语言文本。
    • 发送 complete 消息:通知主线程生成彻底结束,附带完整的回答文本。

2.5 辅助函数:检测、加载、重置

2.5.1 check:检测 WebGPU 支持

javascript 复制代码
async function check() {
  try {
    const adapter = await navigator.gpu.requestAdapter();
    if (!adapter) {
      throw new Error("WebGPU is not supported (no adapter found)");
    }
  } catch (e) {
    self.postMessage({
      status: "error",
      data: e.toString(),
    });
  }
}

尝试请求 GPU 适配器,如果失败说明浏览器不支持 WebGPU,将错误信息返回给主线程展示。WebGPU 是较新的浏览器特性,必须提前做兼容性检测。

2.5.2 load:加载模型与预热

php 复制代码
async function load() {
  self.postMessage({
    status: "loading",
    data: "Loading model...",
  });

  // 加载模型,转发下载进度
  const [tokenizer, model] = await TextGenerationPipeline.getInstance((x) => {
    self.postMessage(x);
  });

  self.postMessage({
    status: "loading",
    data: "Compiling shaders and warming up model...",
  });

  // 模型预热:用简单输入跑一次,编译着色器
  const inputs = tokenizer("a");
  await model.generate({ ...inputs, max_new_tokens: 1 });

  self.postMessage({ status: "ready" });
}

核心逻辑:

  1. 发送 loading 状态,通知主线程显示加载界面。

  2. 调用 getInstance 加载模型和分词器,把进度回调事件转发给主线程,用于渲染进度条。

  3. 模型预热:用单个字符 "a" 执行一次极短的生成。

    为什么要预热?WebGPU 首次运行时需要编译着色器,耗时较长。如果用户首次提问时才编译,会造成明显的卡顿。提前预热后,用户实际使用时响应速度会快很多。

  4. 发送 ready 状态:模型加载与预热完成,可以正常使用。

2.6 消息监听:线程通信入口

typescript 复制代码
self.addEventListener("message", async (e) => {
  const { type, data } = e.data;

  switch (type) {
    case "check":
      check();
      break;
    case "load":
      load();
      break;
    case "generate":
      stopping_criteria.reset();
      generate(data);
      break;
    case "interrupt":
      stopping_criteria.interrupt();
      break;
    case "reset":
      past_key_values_cache = null;
      stopping_criteria.reset();
      break;
  }
});

Worker 线程的消息中枢,接收主线程发来的指令,执行对应操作:

  • check:检测 WebGPU 兼容性
  • load:加载并预热模型
  • generate:开始生成回答,先重置停止条件
  • interrupt:中断当前生成
  • reset:重置对话,清空 KV 缓存和停止条件

三、主线程 React:UI 与交互逻辑

主线程负责页面渲染、用户交互、消息状态管理,通过 postMessage 和 Worker 通信。

3.1 常量与初始化

ini 复制代码
// 检测浏览器是否支持 WebGPU
const IS_WEBGPU_AVAILABLE = !!navigator.gpu;
// 滚动吸附阈值
const STICKY_SCROLL_THRESHOLD = 120;
// 示例问题列表
const EXAMPLES = [
  "Solve the equation x^2 - 3x + 2 = 0",
  "Lily is three times older than her son. In 15 years, she will be twice as old as him. How old is she now?",
  "Write python code to compute the nth fibonacci number.",
];
  • IS_WEBGPU_AVAILABLE:检测浏览器 WebGPU 支持情况,不支持则展示提示页面。
  • EXAMPLES:预设示例问题,方便用户快速体验。

3.2 状态与 Ref 定义

scss 复制代码
const worker = useRef(null);           // Worker 实例引用
const chatContainerRef = useRef(null); // 聊天容器 DOM 引用
const textareaRef = useRef(null);      // 输入框 DOM 引用

// 模型加载相关状态
const [status, setStatus] = useState(null);
const [error, setError] = useState(null);
const [loadingMessage, setLoadingMessage] = useState("");
const [progressItems, setProgressItems] = useState([]);
const [isRunning, setIsRunning] = useState(false);

// 聊天消息与输入
const [messages, setMessages] = useState([]);
const [input, setInput] = useState("");
  • Worker 实例用 useRef 存储:Worker 不需要参与渲染,用 ref 保存可以避免重复创建。
  • status:模型整体状态,取值为 null / loading / ready
  • progressItems:下载进度列表,每个模型文件对应一个进度项。
  • isRunning:标记是否正在生成回答,用于切换发送 / 停止按钮。
  • messages:聊天消息数组,核心状态,驱动 UI 渲染和推理触发。

3.3 Worker 初始化与消息监听

javascript 复制代码
useEffect(() => {
  if (!worker.current) {
    // 创建 Worker,支持 ES 模块语法
    worker.current = new Worker(new URL("./worker.js", import.meta.url), {
      type: "module",
    });
    // 页面加载后先检测 WebGPU 兼容性
    worker.current.postMessage({ type: "check" });
  }

  // 接收 Worker 消息
  const onMessageReceived = (e) => {
    switch (e.data.status) {
      case "loading":
        setStatus("loading");
        setLoadingMessage(e.data.data);
        break;
      case "initiate":
        // 新增一个下载进度项
        setProgressItems((prev) => [...prev, e.data]);
        break;
      case "progress":
        // 更新对应文件的下载进度
        setProgressItems((prev) =>
          prev.map((item) => {
            if (item.file === e.data.file) {
              return { ...item, ...e.data };
            }
            return item;
          }),
        );
        break;
      case "done":
        // 文件下载完成,移除进度项
        setProgressItems((prev) =>
          prev.filter((item) => item.file !== e.data.file),
        );
        break;
      case "ready":
        setStatus("ready");
        break;
      case "start":
        // 生成开始:标记运行状态,新增空 assistant 消息占位
        setIsRunning(true);
        setMessages(prev => [...prev, { role: "assistant", content: "" }]);
        break;
      case "update":
        // 流式更新:修改最后一条 assistant 消息的内容
        setMessages(prev => {
          const newMsgs = [...prev];
          newMsgs[newMsgs.length - 1].content += e.data.output;
          return newMsgs;
        });
        break;
      case "complete":
        setIsRunning(false);
        break;
      case "error":
        setError(e.data.data);
        break;
    }
  }

  const onErrorReceived = (e) => {
    console.error("Worker error:", e);
  } 

  worker.current.addEventListener("message", onMessageReceived);
  worker.current.addEventListener("error", onErrorReceived);

  // 组件卸载时清理事件监听
  return () => {
    worker.current.removeEventListener("message", onMessageReceived);
    worker.current.removeEventListener("error", onErrorReceived);
  };
}, [])

这是主线程的消息中枢,页面加载时执行一次,创建 Worker 并监听消息。

核心解析

  1. Worker 创建

    • new URL("./worker.js", import.meta.url):ES 模块方式引入 Worker 文件。
    • type: "module":允许 Worker 内部使用 import 语法导入依赖,是现代前端的标准写法。
    • 创建后立即发送 check 指令,检测 WebGPU 兼容性。
  2. 消息事件分支

    • loading:更新加载状态和提示文字。
    • initiate / progress / done:管理下载进度列表,实现多文件并行下载的进度展示。
    • ready:模型就绪,更新状态。
    • start:生成开始,设置 isRunning=true并新增一条空的 assistant 消息占位。这是流式打字机的关键:先占好位置,后续不断拼接内容。
    • update:流式更新,修改数组最后一条 assistant 消息的 content,拼接新收到的文本,实现打字机效果。注意是修改已有项,不是新增项。
    • complete:生成结束,设置 isRunning=false
    • error:捕获错误并展示。
  3. 清理函数 组件卸载时移除事件监听,防止内存泄漏。

3.4 触发推理:messages 副作用

javascript 复制代码
useEffect(() => {
  // 没有用户消息时不触发推理
  if (messages.filter((x) => x.role === "user").length === 0) {
    return;
  }
  // 最后一条是 assistant 消息时不触发(防止流式更新循环触发)
  if (messages.at(-1).role === "assistant") {
    return;
  }
  // 向 Worker 发送生成指令
  worker.current.postMessage({ type: "generate", data: messages });
}, [messages])

这是最容易踩坑的核心逻辑,两个判断的设计非常关键:

  1. 触发时机messages 数组每次变化都会执行这个副作用。

  2. 第一个判断:页面初始状态没有用户消息,不触发推理。

  3. 第二个判断(核心防循环逻辑)

    • 流式更新时,每收到一个 token,我们都会修改最后一条 assistant 消息的 content,导致 messages 状态更新,useEffect 重新执行。
    • 如果不加这个判断,每次更新 content 都会重新发送 generate 指令,造成无限循环生成。
    • 当最后一条是 assistant 时,说明要么正在流式生成,要么生成结束,都不需要重新发起推理。
  4. 只有当用户发送新消息、最后一条是 user 时,才真正向 Worker 发送 generate 指令。

3.5 交互函数

scss 复制代码
function onEnter(message) {
  setMessages((prev) => [...prev, { role: "user", content: message }]);
  setInput("");
}

function onInterrupt() {
  worker.current.postMessage({ type: "interrupt" });
}
  • onEnter:用户发送消息,把 user 消息加入数组,清空输入框。messages 更新后会自动触发上面的 useEffect,发起推理。
  • onInterrupt:点击停止按钮,向 Worker 发送中断指令。

3.6 输入框交互

ini 复制代码
<textarea
  ref={textareaRef}
  value={input}
  disabled={status !== "ready"}
  onKeyDown={(e) => {
    if (
      input.length > 0 &&
      !isRunning &&
      e.key === "Enter" &&
      !e.shiftKey
    ) {
      e.preventDefault();
      onEnter(input);
    }
  }}
  onInput={(e) => setInput(e.target.value)}
/>
  • 模型未就绪时输入框禁用。
  • 回车发送:非 Shift + 回车、有内容、未在生成时,按回车发送消息。
  • Shift + 回车保留换行输入,符合聊天产品的通用交互习惯。

3.7 UI 结构

页面分为三种状态:

  1. 初始欢迎页:展示模型介绍、加载按钮。
  2. 加载页:展示下载进度条和加载提示。
  3. 就绪页:聊天展示区域 + 底部输入框。 不支持 WebGPU 时直接显示错误提示页面。

四、核心设计与知识点总结

4.1 流式打字机的完整链路

  1. 用户发送消息 → messages 新增 user 项 → useEffect 触发 → 向 Worker 发送 generate 指令。
  2. Worker 收到指令 → 发送 start 给主线程 → 主线程新增空的 assistant 消息占位。
  3. 模型生成 token → streamer 的 token 回调更新状态和 TPS → 文本回调发送 update 消息。
  4. 主线程收到 update → 修改最后一条 assistant 的 content → 页面文字增长,呈现打字机效果。
  5. 模型生成完毕 → Worker 发送 complete → 主线程设置 isRunning=false,流程结束。

4.2 为什么先占位再更新,不是生成完再添加?

  • 用户体验:打字机效果比等待几秒后一次性弹出答案的体验好得多,是当前 AI 产品的标准交互。
  • 实现机制:流式输出是逐 token 到达的,必须有一个载体来不断拼接内容。提前创建占位消息,后续只修改 content,是最简洁、性能最优的实现方式。

4.3 两个回调的分工设计

  • token_callback_function:拿原始数字,做状态判断、性能统计,不处理文本展示。
  • callback_function:拿干净文本,做输出推送,不处理底层逻辑。
  • 分层设计让底层识别和上层展示解耦,代码逻辑更清晰,也更容易扩展。

4.4 端侧推理的经典优化手段

  1. WebGPU 加速:利用 GPU 并行计算大幅提升推理速度。
  2. 4bit 量化:减小模型体积和显存占用,让浏览器端运行成为可能。
  3. KV 缓存:复用历史计算,显著提升多轮对话速度。
  4. 模型预热:提前编译着色器,避免首次使用卡顿。
  5. WebWorker:推理放后台线程,完全不阻塞 UI 交互。

五、可扩展优化方向

  1. 开启 KV 缓存 :打开代码中注释的 past_key_values,大幅提升多轮对话速度。
  2. 思考过程展示 :根据 state 状态,把思考内容用折叠面板单独展示。
  3. 对话重置功能:新增清空对话按钮,调用 reset 指令清空 KV 缓存和历史消息。
  4. Markdown 渲染:引入 marked 库,把 AI 回答渲染成格式化的富文本。
  5. 错误重试:加载失败时提供重试按钮,提升容错性。

写在最后

这套代码是非常经典的浏览器端大模型推理实现,涵盖了从模型加载、流式输出生成、多线程通信到前端交互的完整链路。理解这套代码,就能掌握端侧 LLM 应用的核心开发范式,在此基础上可以快速扩展出更多定制化功能

相关推荐
请你吃div1 小时前
个人 Nuxt4 博客 SEO 优化实战教程
前端·nuxt.js·seo
网安蟹佬霸1 小时前
Android安全攻防实战:从APK逆向到Frida动态Hook全流程详解(附脚本)
android·前端·安全·web安全·逆向·csrf·网安
计算机魔术师1 小时前
芯片不跌反涨?两家公司的赌局正在重塑全球算力格局
前端
IT_陈寒1 小时前
Python多进程池的坑:子进程竟然不会退出
前端·人工智能·后端
snow@li1 小时前
前端:全景深度分析/前端岗位起源,以及“美国没有前端岗位”的真相
前端
恋猫de小郭1 小时前
Gradle 9.7.0 将提速 Android 构建,Sync 提升接近一倍
android·前端·flutter
lilian2331 小时前
Harmony os 技术实战|拼豆制图48:把个人页字符串路由改成可穷尽的类型协议
前端·华为·harmonyos
咏方舟【长江支流】2 小时前
【前端2】单据编辑 -订单单据主子表,万能模板行,自定义添加行
前端·前端框架·状态模式·咏方舟-长江支流
qq_452396232 小时前
第六篇:《CI/CD 流水线:从前端构建到自动化部署》
前端·ci/cd·自动化