news 2026/9/24 14:16:58

RunAnywhere Web SDK 最小示例应用:从零构建一个浏览器端本地 AI 流式生成应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RunAnywhere Web SDK 最小示例应用:从零构建一个浏览器端本地 AI 流式生成应用
  • AI
  • 模型推理服务
  • 推理引擎
  • 本地部署
  • 多模态

【免费下载链接】runanywhere-sdks

Production ready toolkit to run AI locally

项目地址:https://gitcode.com/gh_mirrors/ru/runanywhere-sdks
点击查看免费下载

本指南以 bindings/web/example 仓库中的最小示例应用为骨架,完整讲解 RunAnywhere Web SDK 在浏览器中的真实启动流程、模型注册与流式生成调用链,以及跨源隔离、WASM 构建等关键工程细节。读完本指南,你将掌握如何在纯 DOM + Vite 项目中正确初始化 SDK 双阶段启动、注册 llama.cpp 后端、让模型按需下载加载,并通过generateStream消费流式事件,同时理解该应用作为仓库内贡献者验证环境(contributor harness)与 Playwright 浏览器测试默认目标的设计原理。

一、这个最小示例是什么

bindings/web/example是整个仓库中"最小的、能证明 Web SDK 可用"的应用:一个输入框、一个 Generate 按钮、一段流式输出的回答。它不依赖任何前端框架,直接使用原生 DOM 操作,全部逻辑集中在 src/main.ts 与一个 index.html 中。

它在项目里扮演两个角色:

  • 仓库内贡献者验证环境(contributor harness):默认构建时通过 Vite 别名(alias)与tsconfig.json的 paths 映射,把@runanywhere/web@runanywhere/web-llamacpp@runanywhere/proto-ts直接指向仓库源码目录。这意味着修改 SDK 源码后无需发布即可在此应用里立即看到效果。
  • 浏览器测试默认目标bindings/web/tests/browser/*下的 Playwright 测试默认驱动该应用;RA_E2E_APP_DIR环境变量可以将同一批测试指向其他应用(如发布验收用的 release harness)。

从源码结构看,整个应用刻意保持"零框架、单屏":index.html里只有一个status段落、一个prompt文本域、一个generate按钮和一个output预格式化块。这是为了让任何人(人类或自动化测试)都能在 10 秒内看懂它做了什么。

二、运行与构建命令

bindings/web/example目录下,核心命令如下:

npm install npm run typecheck # 类型检查 npm run build # 生产构建,产物输出到 dist/ npm run preview # 预览 dist/,地址 http://localhost:3000 npm run dev # 开发服务器,地址 http://localhost:3000

这些脚本定义在 package.json 中:

  • dev/previewvite --host localhost --port 3000 --strictPort,端口固定 3000,端口被占用会直接报错而非换端口。
  • typechecktsc --noEmit,基于 tsconfig.json。
  • typecheck:installedtsc --noEmit -p tsconfig.installed-sdk.json,用于针对"已安装的 SDK 包"做类型检查(见下文 RAC_USE_INSTALLED_SDK)。

构建前置条件:四个 canonical WASM 对必须存在。npm run build依赖四个标准 Emscripten 运行时产物对(每个包含.js.wasm两个文件):

baseName所属包作用
racommonscoreSDK 核心 commons WASM
racommons-llamacppllamacppllama.cpp CPU 后端
racommons-llamacpp-webgpullamacppllama.cpp WebGPU 后端
racommons-onnx-sherpaonnxONNX / sherpa 语音后端

这四个对分别位于bindings/web/packages/*/wasm目录。构建前需先在bindings/web/下执行npm run build:wasm:all生成它们。Vite 插件(vite.config.ts 中的copyWasmPlugin)会在buildStart阶段检查这些文件是否存在且非空,缺失时会直接让构建失败并列出缺失文件名,而不是产出一个只在浏览器里才报错的残缺 bundle——这是刻意的 fail-fast 设计。

切换到已安装的 SDK(发布消费方验证模式)

设置环境变量RAC_USE_INSTALLED_SDK=1后,模块解析与 WASM 源目录都会切换到node_modules中已安装的@runanywhere/*包。这是发布消费方门禁(release consumer gate)使用的模式:安装发布候选 tarball 后,验证真实发布包而不是仓库源码。

RAC_USE_INSTALLED_SDK=1 npm run build RAC_USE_INSTALLED_SDK=1 npm run typecheck:installed

typecheck:installed会清空 tsconfig 中的本地源码 paths 映射(见 tsconfig.installed-sdk.json),确保类型检查针对的是真正安装进node_modules的包,而非源码路径。

三、应用到底做了什么:双阶段启动与流式生成

整个应用逻辑就在 src/main.ts。启动顺序至关重要——它精确镜像了 SDK 文档化的双阶段初始化流程:

await RunAnywhere.initialize({ environment: 'development' }); // 阶段一:加载 racommons.wasm await LlamaCPP.register({ acceleration: 'auto' }); // 阶段二:加载 racommons-llamacpp[-webgpu].wasm await RunAnywhere.completeServicesInitialization(); // 已废弃入口,但此处并非无效: // initialize() 已在后台启动 Phase 2, // 此调用负责 join/await 它 RunAnywhere.models.register({ id: 'smollm2-360m-q8_0', ... }); // 目录(catalog)由应用持有

之后是流式生成一次回答:

for await (const event of RunAnywhere.llm.generateStream(prompt, { model: MODEL_ID })) { // event.type: 'textDelta' | 'reasoningDelta' | 'completed' | 'failed' | 'cancelled' }

3.1 启动流程逐行拆解

  1. RunAnywhere.initialize({ environment: 'development' }):初始化 SDK 核心,加载racommons.wasmenvironment: 'development'会启用开发模式日志与遥测。
  2. LlamaCPP.register({ acceleration: 'auto' }):注册 llama.cpp 后端,加载racommons-llamacpp[-webgpu].wasmacceleration: 'auto'让 SDK 根据当前设备自动选择 CPU 或 WebGPU 路径。
  3. RunAnywhere.completeServicesInitialization():文档注释明确标注为 deprecated(已废弃)的转发入口。它保留在这里是为了兼容文档化的两阶段启动写法——实际上initialize()已经折叠了两个阶段,这个调用只是在后台 join/await 已启动的 Phase 2。
  4. RunAnywhere.models.register(...):把模型元数据注册进 SDK 的模型注册表。main.ts 中注册的是smollm2-360m-q8_0
RunAnywhere.models.register({ id: MODEL_ID, // 'smollm2-360m-q8_0' name: 'SmolLM2 360M Q8_0', category: ModelCategory.MODEL_CATEGORY_LANGUAGE, // 语言模型 framework: InferenceFramework.INFERENCE_FRAMEWORK_LLAMA_CPP, format: ModelFormat.MODEL_FORMAT_GGUF, // GGUF 格式 url: 'https://huggingface.co/HuggingFaceTB/SmolLM2-360M-Instruct-GGUF/resolve/main/smollm2-360m-instruct-q8_0.gguf', sizeBytes: 386_404_992, // 约 386 MB memoryRequiredBytes: 500_000_000, // 建议内存 500 MB contextLength: 2048, });

为什么应用必须在启动时调用一次models.register?因为浏览器版 SDK不内置模型目录(catalog)——generateStream只能解析注册表里已有的 id。模型下载与加载则由 SDK 负责:在生成请求中命名options.model即可,首次使用时 SDK 会自动下载并加载模型,应用自身从不编排下载/加载流程。

3.2 流式事件消费

generate()函数(main.ts 第 78 行起)调用generateStream并逐个处理事件:

  • textDelta:增量文本,累加后写入output元素,实现打字机效果;
  • reasoningDelta:推理过程的增量文本(如思维链);
  • completed:生成完成,从event.result读取最终文本、outputTokens(输出 token 数)与tokensPerSecond(生成速度),状态栏显示Done — N tokens at X.X tok/s.
  • failed:生成失败,读取event.error.message
  • cancelled:生成被取消。

generateStream的第二个参数还演示了maxOutputTokens: 256的用法,用于限制最大输出 token 数。

四、浏览器环境要求:跨源隔离与 WASM 产物拷贝

4.1 COOP/COEP 跨源隔离

SharedArrayBuffer——以及依赖它的 pthread CPU WASM 构建——要求页面处于跨源隔离(cross-origin isolation)状态。因此 vite.config.ts 对 dev 服务器和 preview 服务器都设置了响应头:

const isolationHeaders = { 'Cross-Origin-Opener-Policy': 'same-origin', 'Cross-Origin-Embedder-Policy': 'credentialless', } as const;

同时生产构建目标被固定为chrome86(Web SDK 文档化的最低浏览器版本),防止未来 Vite 主版本通过其动态的baseline-widely-available默认值静默抬高浏览器要求。

4.2 为什么 WASM 必须以原始文件名拷贝

构建过程会把四个 canonical Emscripten.js/.wasm对以原始文件名拷贝到dist/assets/copyWasmPluginwriteBundle钩子):

  • Emscripten glue 通过new URL("x.wasm", import.meta.url)解析自己的二进制文件;
  • 每个启用 pthread 的模块还会按精确文件名生成 worker——如果只有 Vite 加了 hash 的副本,worker 握手会失败,CPU 构建会永远卡在等待 pthread 池上。

这正是 vite 配置中assetsInclude: ['**/*.wasm']copyWasmPlugin存在的意义。构建日志会逐对打印拷贝结果,例如✓ Copied racommons-llamacpp-webgpu.wasm (XX.X MB)

4.3 开发体验细节

vite.config.ts还做了两件事来保证开发体验:

  • optimizeDeps.exclude: ['@runanywhere/web', '@runanywhere/web-llamacpp']:排除这两个包进入 Vite 依赖预构建,避免重复打包出两个 SDK 单例;
  • 所有别名必须指向同一份源码模块——如果某个别名解析到了dist/,就会创建第二个 SDK 单例(duplicate SDK singleton),这是配置注释中特别强调的坑。

五、浏览器测试就绪契约(Readiness Contract)

bindings/web/tests/browser/*下的 Playwright 测试不依赖 DOM 布局来判定应用状态,而是读取 src/readiness.ts 发布的两个全局变量。这保证了即使应用被重写,测试门禁依然有效:

全局变量承载内容
window.__RUNANYWHERE_AI_READY__启动进度快照(statebackendstepreasonshellReady
window.__RUNANYWHERE_SDK__导入的 SDK 单例,供公共 API 面探测

此外,根<html>元素会把后端状态镜像为data-runanywhere-ai-backend属性,Playwright 无需轮询脚本状态即可等待该属性。

ReadinessSnapshot的类型定义(readiness.ts第 28 行起)包含:

  • ready:应用是否可接受提示词;
  • state'booting' | 'initializing-sdk' | 'interactive' | 'error'
  • backend'pending' | 'registered' | 'unavailable'
  • step:细化到'booting' | 'initializing-sdk' | 'registering-llamacpp' | 'registering-catalog' | 'interactive' | 'error'
  • shellReady:单屏 shell 是否可用(smoke 测试读取它);
  • reason:当前状态的人类可读说明;错误时附加error字段。

publishReadiness()每次合并快照补丁并同步更新 DOM 属性;publishSDK()把 SDK 单例挂到window.__RUNANYWHERE_SDK__。main.ts 中的boot()在不同阶段调用publishReadiness({...}),把启动进度逐步上报——从initializing-sdk("Loading the commons WASM.")到registering-llamacpp,再到registering-catalog,最终ready: true进入interactive状态。

运行浏览器测试

bindings/web/执行:

npm run test:browser:smoke # playwright test tests/browser/backend-readiness.spec.ts tests/browser/hybrid-stt.spec.ts npm run test:browser # 完整默认套件 npm run test:browser:release # RA_RUN_FULL_E2E=1 playwright test tests/browser/release-app.e2e.spec.ts

根据 playwright.config.ts 第 59 行,默认appDir = process.env.RA_E2E_APP_DIR ?? resolve(__dirname, 'example')——即默认驱动本示例应用,发布验收测试可把RA_E2E_APP_DIR指向外部 checkout 的 release harness。

六、验证:什么才算"真的跑通了"

构建和类型检查只是冒烟检查。真正的验证是一次完整的浏览器启动:模型下载 → 模型加载 → 流式回答。文档记录的最后一次验证是针对npm run preview完成的:

  • COOP/COEP 响应头已生效(跨源隔离开启);
  • 四个 canonical WASM 对都以 JavaScript /application/wasmMIME 类型正常提供;
  • smollm2-360m-q8_0在 WebGPU llama.cpp 路径上完成下载、加载并成功生成。

这也给出了一个可复现的验收清单:启动npm run preview,打开http://localhost:3000,输入提示词点击 Generate,观察状态栏从 "Generating with smollm2-360m-q8_0 (first run downloads the model)…" 推进到 "Done — N tokens at X.X tok/s."——如果模型首次运行会自动下载,说明按需下载/加载链路是通的。

七、总结

bindings/web/example以不到百行代码演示了 RunAnywhere Web SDK 在浏览器端最核心的完整链路:

  1. 双阶段启动initialize()LlamaCPP.register()completeServicesInitialization()
  2. 应用持有目录:通过models.register注册模型元数据,模型下载/加载交给 SDK 按需完成;
  3. 流式生成generateStream的五类事件(textDelta/reasoningDelta/completed/failed/cancelled)驱动 UI 更新;
  4. 工程基建:COOP/COEP 跨源隔离、WASM 原始文件名拷贝、构建失败前置检查、就绪契约全局变量。

对于想要在自己项目中集成 RunAnywhere Web SDK 的开发者,这个示例是最好的起点:复制 index.html 与 src/main.ts 的骨架,替换模型 id 与 UI 元素,即可快速跑通首个浏览器端本地 AI 应用。

  • AI
  • 模型推理服务
  • 推理引擎
  • 本地部署
  • 多模态

【免费下载链接】runanywhere-sdks

Production ready toolkit to run AI locally

项目地址:https://gitcode.com/gh_mirrors/ru/runanywhere-sdks
点击查看免费下载

相关推荐

上一篇:OpenSign开源电子签名平台:10分钟快速部署与专业配置指南
下一篇:终极指南:如何用tokenizers CTC解码器解决语音识别中的重复令牌问题

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 14:12:48

Flutter OHOS 适配排障:内存泄漏与GPU问题定位实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 14:10:08

LuatOS如何重构Cat.1开发:从AT指令到事件驱动的效率革命

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 14:09:15

高精度TEC温控系统实战:H桥驱动与PID算法详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华