WebLLM 实战:在浏览器中使用 OpenAI 兼容 API 运行 Qwen3 并精细控制思考模式(enable_thinking / 软开关 / 多轮历史) WebLLM 实战在浏览器中使用 OpenAI 兼容 API 运行 Qwen3 并精细控制思考模式enable_thinking / 软开关 / 多轮历史【免费下载链接】web-llmHigh-performance In-browser LLM Inference Engine项目地址: https://gitcode.com/GitHub_Trending/we/web-llm导读本文以 WebLLM 仓库中的 Qwen3 示例examples/qwen3 为骨架系统讲解如何在浏览器端通过mlc-ai/web-llm加载 Qwen3 系列模型并借助 OpenAI 兼容的 Chat Completion 接口调用 Qwen3 的思考能力。读完本文你将掌握Qwen3 在 WebLLM 中的四种最佳实践调用姿势——默认开启思考、通过extra_body.enable_thinking硬关闭思考、通过/think与/no_think软开关控制思考、以及多轮对话前对历史消息中的think块做预处理——并理解这些行为在 WebLLM 源码中的底层实现src/engine.ts、src/llm_chat.ts、src/conversation.ts。一、示例概览项目结构与运行方式仓库中的 examples/qwen3 是一个完整可运行的 Parcel 工程目录结构如下examples/qwen3/ ├── README.md ├── package.json └── src/ ├── qwen3_example.html # 页面入口负责展示初始化进度与生成结果 └── qwen3_example.ts # 核心演示代码覆盖四种思考模式调用package.json 中声明了启动与构建脚本scripts: { start: parcel src/qwen3_example.html --port 8883, build: parcel build src/qwen3_example.html --dist-dir lib }, dependencies: { mlc-ai/web-llm: ^0.2.84 }运行示例只需两步README.mdnpm install npm start启动后浏览器访问 Parcel 输出地址默认端口 8883即可看到页面。页面结构非常简单qwen3_example.html一个init-label标签显示引擎初始化进度一个generate-label标签流式显示生成结果运行结果请以浏览器控制台输出为准。提示如果你想同时修改 WebLLM 核心包进行本地联调可将package.json中的mlc-ai/web-llm依赖改为file:../..并按 从源码构建指南 在仓库根目录本地构建 WebLLM。该方式仅推荐给确实需要 hack WebLLM 核心包的开发者。二、初始化引擎并加载 Qwen3 模型示例通过CreateMLCEngine创建引擎实例qwen3_example.tsconst initProgressCallback (report: webllm.InitProgressReport) { setLabel(init-label, report.text); }; const selectedModel Qwen3-4B-q4f16_1-MLC; const engine: webllm.MLCEngineInterface await webllm.CreateMLCEngine( selectedModel, { initProgressCallback: initProgressCallback }, );selectedModel使用 WebLLM 的model_id格式示例选用Qwen3-4B-q4f16_1-MLC。initProgressCallback会把模型下载与编译进度实时渲染到页面的init-label上。从 src/config.ts 的预置模型列表可以看到WebLLM 内置了完整的高通量 Qwen3 模型族Qwen3-0.6B、Qwen3-1.7B、Qwen3-4B、Qwen3-8B每个尺寸均提供q4f16_1、q4f32_1等量化变体4B 额外提供q0f16/q0f32等且示例采用的都是cs1kcontext window 1k 滑动窗口的 WebGPU wasm 变体。你也可以把selectedModel换成其中任意一个已注册的model_id。三、思考模式的核心开关extra_body.enable_thinkingQwen3 默认会在生成最终回答前输出一段think.../think思考内容。WebLLM 在 OpenAI 兼容协议之外扩展了extra_body.enable_thinking字段src/openai_api_protocols/chat_completion.ts置为falseWebLLM 会在响应中预置一个空的think\n\n/think\n\n块从而阻止模型生成任何思考 token置为true或未设置undefined不干预模型按默认行为输出思考内容。官方注释同时说明目前该开关仅面向 Qwen3 模型设计虽然代码层面未做强制校验。该字段会经 src/engine.ts 的GenerationConfig组装透传到生成管线。底层实现空思考块是如何注入的当enable_thinking false时核心管线 src/llm_chat.ts 会在每轮生成前执行conversation.appendMessage(msgRole, inp, inp_role_str); if (genConfig?.enable_thinking false) { // TODO(Charlie): In future we should make emptyThinkingBlockStr configurable. const emptyThinkingBlockStr think\n\n/think\n\n; const encoded this.tokenizer.encode(emptyThinkingBlockStr); this.outputIds.push(...encoded); conversation.appendEmptyThinkingReplyHeader( Role.assistant, emptyThinkingBlockStr, ); } else { conversation.appendReplyHeader(Role.assistant); }其原理是直接把这个空思考块当作已生成的 token 序列推入输出缓冲区相当于替模型补写了完整的think.../think结构模型便不会再生成思考内容。同时 src/conversation.ts 的appendEmptyThinkingReplyHeader会把对话最后一条消息标记为空思考回复头并在 finishReply 完成一轮生成后复位该标记保证后续轮次逻辑正确。四、四种最佳实践调用方式完整代码以下四种模式全部来自 qwen3_example.ts 的实际调用均使用流式输出并开启include_usage最后一个 chunk 携带 usage 统计。示例中的流式消费辅助函数qwen3_example.tsasync function streamResponse( engine: webllm.MLCEngineInterface, request: webllm.ChatCompletionRequestStreaming, ): Promisevoid { console.log(Requesting chat completion with request:, request); const asyncChunkGenerator await engine.chat.completions.create(request); let message ; for await (const chunk of asyncChunkGenerator) { message chunk.choices[0]?.delta?.content || ; setLabel(generate-label, message); if (chunk.usage) { console.log(chunk.usage); // only last chunk has usage } // engine.interruptGenerate(); // works with interrupt as well } console.log(Final message:\n, await engine.getMessage()); // the concatenated message }要点通过engine.chat.completions.create()拿到的是AsyncIterable生成器chunk.usage只在最后一个 chunk 上出现生成过程中随时可以调用engine.interruptGenerate()打断结束后可用engine.getMessage()获取拼接完成的完整消息。模式一默认行为开启思考不指定enable_thinking时模型默认思考qwen3_example.tslet request: webllm.ChatCompletionRequest { stream: true, stream_options: { include_usage: true }, messages: [ { role: user, content: How many rs are there in the word strawberry?, }, ], // Specifying enable_thinking is optional, as it defaults to think. // extra_body: { // enable_thinking: true, // } }; await streamResponse(engine, request);输出会先出现think.../think思考块再给出最终答案。显式传enable_thinking: true与此行为一致。模式二硬关闭思考enable_thinking: falserequest { stream: true, stream_options: { include_usage: true }, messages: [ { role: user, content: How many rs are there in the word strawberry? }, ], extra_body: { enable_thinking: false, }, }; await streamResponse(engine, request);此模式下模型不会输出任何思考内容直接给出答案适合对延迟敏感、只需最终结果的场景。模式三软开关/think与/no_think软开关在用户输入文本中追加指令 tokenqwen3_example.tsrequest { stream: true, stream_options: { include_usage: true }, messages: [ { role: user, content: How many rs are there in the word strawberry? /no_think, // content: How many rs are there in the word strawberry? /think, }, ], }; await streamResponse(engine, request);软开关与enable_thinking的交互规则代码注释原文语义当enable_thinkingTrue或未设置时无论用户输入/think还是/no_think模型始终输出一个think.../think包裹的块但若实际禁用思考该块内容可能为空当enable_thinkingFalse时软开关完全失效——无论输入/think还是/no_think模型都不会生成思考内容也不会出现think.../think块。也就是说enable_thinking是能不能思考的总闸/think、/no_think是总闸打开时对单次请求的细粒度指示。模式四多轮对话中的历史消息预处理Qwen3 的最佳实践建议多轮对话时把历史 assistant 回复中的思考内容解析出来不再回传给模型qwen3_example.tsconst history: webllm.ChatCompletionMessageParam[] [ { role: user, content: How many rs are there in the word strawberry? /think }, { role: assistant, content: thinkDummy thinking content here.../think\n\nThe answer is 3., }, ]; // Preprocess history to remove thinking content const preprocessedHistory history.map((msg) { if (msg.role assistant) { // Remove think.../think block from assistant messages that is at the start // and may contain two \n\n line breaks. const thinkRegex /think.*?\/think\n?\n?/s; // Match think.../think with optional \n\n const contentWithoutThink msg.content!.replace(thinkRegex, ).trim(); return { ...msg, content: contentWithoutThink }; } return msg; // User messages remain unchanged }); console.log(Preprocessed history:, preprocessedHistory); const newMessage: webllm.ChatCompletionMessageParam { role: user, content: What about blueberries?, }; request { stream: true, stream_options: { include_usage: true }, messages: [...preprocessedHistory, newMessage], }; await streamResponse(engine, request);实现要点正则/ think.*?\/think\n?\n?/s使用s标志让.跨行匹配精确删除位于消息开头的think.../think块及其后的两个换行仅处理assistant消息用户消息原样保留清洗后的历史与新增用户消息拼接为完整messages数组发起新请求。这样做的价值在于思考内容往往携带内部推理过程长期累积会占用上下文窗口尤其在cs1k滑动窗口模型上空间更紧张清洗后既省 token 又避免模型被上一轮的碎碎念干扰。五、从源码看思考模式的完整数据流将四种模式串起来思考控制从请求到生成的完整链路为请求层extra_body.enable_thinking定义于 src/openai_api_protocols/chat_completion.ts随ChatCompletionRequest传入引擎层engine.chat.completions.create()在 src/engine.ts 将request.extra_body?.enable_thinking映射进GenerationConfig管线层LLMChatPipeline.generate()在 src/llm_chat.ts 依据该标志注入空思考块 token或走常规appendReplyHeader对话层src/conversation.ts 维护空思考回复头标记并在finishReply后复位保障多轮状态机正确性。对应地仓库测试 tests/llm_chat_pipeline.test.ts 与 tests/llm_chat_abi.test.ts 覆盖了引擎初始化和生成管线相关行为可作为理解底层实现的补充参考。六、注意事项与最佳实践小结模型版本选择示例固定使用Qwen3-4B-q4f16_1-MLC如需更换确保model_id已注册在 src/config.ts 的预置model_list中或通过engineConfig.appConfig自定义注册且需在CreateMLCEngine或engine.reload()阶段预先加载软开关的边界牢记enable_thinking: false时/think、/no_think无效这一硬约束避免在硬关闭场景下误以为软开关仍生效多轮清洗是推荐做法只要历史里存在 assistant 思考块就应先用正则清洗再回传finishReply的标记复位逻辑保证了即便某轮被中断后续轮次也不会残留空思考头状态流式细节include_usage使最后一个 chunk 携带 token 用量适合做计费与成本统计打断生成使用engine.interruptGenerate()与思考模式无冲突本地联调核心包如需修改 WebLLM 核心实现把依赖改为file:../..并按 从源码构建指南 构建仅在需要 hack 核心包时推荐。至此你已掌握在 WebLLM 浏览器环境中对 Qwen3 思考模式的全部控制手段从默认思考、硬关闭、软开关到多轮历史清洗并能定位到引擎、管线、对话三层源码中的每一处关键实现。【免费下载链接】web-llmHigh-performance In-browser LLM Inference Engine项目地址: https://gitcode.com/GitHub_Trending/we/web-llm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考