普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月11日首页

从零打造AI智能博客:一个项目带你入门全栈与大模型应用开发

作者 SpringLament
2026年1月10日 19:58

写在前面

最近 AI Coding 实在太火了,Cursor、Claude Code 这些工具让写代码变得越来越轻松。你可能也注意到了,这些工具都有一个共同点:在你写代码的时候,它们会实时给你补全建议,按 Tab 就能接受。这种体验太爽了,以至于我想在自己的博客编辑器里也搞一个类似的功能。

与此同时,「全栈开发」和「大模型应用开发」也成了很多人想要学习的方向。

我自己折腾了一个 Next.js 全栈 AI 博客项目,把 Prompt 工程、RAG 知识库、流式输出、AI Copilot 这些东西都实践了一遍。今天想通过这篇文章,把我在这个项目里学到的东西分享出来,希望能帮到想入门这个领域的朋友。

GitHub 地址github.com/flawlessv/S…

🔗 线上地址powder.icu/

本文主要讲述如何实现博客的AI相关功能,想了解基础功能如何实现的同学可以看下Next.js全栈开发从入门到部署实战

先看看这个博客长什么样:

shouye.png

后台仪表盘.png 项目用的技术栈:

  • 前端:Next.js 15 + TypeScript + shadcn/ui + Tailwind CSS
  • 后端:Next.js API Routes + Prisma ORM
  • AI:Kimi API + Ollama + ChromaDB

接下来我会从 Prompt 讲起,然后聊聊 AI Copilot、RAG、流式输出这些功能是怎么实现的。


一切的起点:Prompt

说到大模型应用开发,绑不开的就是 Prompt。

Prompt 是什么? 说白了就是你跟大模型说的话。你怎么问,它就怎么答。问得好,答案就靠谱;问得烂,答案就离谱。

我在做这个项目的时候发现,很多 AI 功能的本质都是一样的:构造一个 Prompt,然后调 LLM API

比如:

  • AI 生成文章标题?Prompt + LLM
  • AI 生成摘要?Prompt + LLM
  • AI 推荐标签?还是 Prompt + LLM

所以想玩好大模型应用,Prompt 工程是必须要会的。

结构化 Prompt

写 Prompt 其实跟写文章差不多,有结构会比乱写好很多。我在项目里用的是一种叫「结构化 Prompt」的写法,大概长这样:

# Role: 你的角色

## Profile

- Author: xxx
- Version: 1.0
- Language: 中文
- Description: 角色描述

## Skills

- 技能1
- 技能2

## Rules

1. 规则1
2. 规则2

## Workflow

1. 第一步做什么
2. 第二步做什么

## OutputFormat

- 输出格式要求

## Initialization

作为 <Role>,严格遵守 <Rules>,按照 <Workflow> 执行任务。

这种写法的好处是逻辑清晰,大模型更容易理解你想要什么。

举个实际的例子,这是我项目里用来生成文章摘要的 Prompt:

export function buildExcerptPrompt(content: string): string {
  return `# Role: 内容摘要撰写专家

## Profile
- Author: Spring Broken AI Blog
- Version: 2.0
- Language: 中文
- Description: 你是一位专业的内容编辑,擅长从长文中提取核心信息,撰写简洁有力的摘要。

## Rules
1. 摘要长度必须严格控制在 100-200 个汉字之间
2. 必须包含文章的核心观点和主要结论
3. 使用简洁、专业的语言,避免冗余表达
4. 只返回摘要文本,不要包含任何其他内容

## Workflow
1. 仔细阅读并理解完整的文章内容
2. 识别文章的核心主题和主要论点
3. 用简洁的语言组织摘要
4. 输出纯文本摘要

## Input
文章内容:
${content.slice(0, 3000)}

## Initialization
作为 <Role>,严格遵守 <Rules>,按照 <Workflow> 撰写摘要。`;
}

你看,其实就是告诉大模型:你是谁、要遵守什么规则、按什么流程做事、输出什么格式。把这些说清楚了,大模型的输出质量会好很多。


AI Copilot:编辑器里的智能补全

这个功能是我觉得最有意思的一个,效果类似 GitHub Copilot 或者 Cursor,在你写文章的时候实时给你补全建议。

AI文章新建和编辑页.png

实现思路

说穿了也不复杂:把文章上下文 + Prompt 丢给 LLM,让它帮你续写

具体流程是这样的:

  1. 用户在编辑器里打字
  2. 我提取光标前 500 个字符作为上下文
  3. 构造一个 Prompt,大意是「根据上下文,续写 5-30 个字」
  4. 调 Kimi API 拿到补全建议
  5. 把建议以灰色斜体显示在光标后面
  6. 用户按 Tab 接受,按 Esc 取消

技术难点

这个功能看起来简单,但实际做起来有几个坑:

1. 非侵入式显示

补全建议不能直接写入文档,只能在视图层显示。

我一开始想的就是用样式来实现——在光标位置叠加一个灰色斜体的文本,看起来像是补全建议,但实际上不是文档的一部分。这个思路是对的,关键是怎么实现。

参考了 VSCode 的做法。VSCode 的 AI 补全(GitHub Copilot)用的是「虚拟文本」机制:补全建议只在视图层显示,不写入文档模型。只有用户按 Tab 确认后,才真正写入。

我用的编辑器是 Tiptap(基于 ProseMirror),刚好有类似的机制叫 Decoration。它可以在视图层叠加显示内容,不影响文档结构,正好符合我的需求。

2. 防抖

用户打字很快的时候,不能每敲一个字就调一次 API,那样太浪费了。我设了 500ms 的防抖,用户停下来半秒钟才触发补全请求。

3. 异步竞态

用户可能在 API 返回之前又继续打字了,这时候光标位置已经变了。如果直接把补全建议显示出来,位置就对不上了。

我的做法是双重位置校验:发请求前记录光标位置,API 返回后再校验一次,位置变了就不显示。

// 第一次校验:防抖回调执行时
const currentState = extension.editor.state;
if (currentSelection.from !== currentFrom) {
  return; // 位置已改变,丢弃请求
}

// 调用 AI API...

// 第二次校验:API 返回后
const latestState = extension.editor.state;
if (latestState.selection.from === currentFrom) {
  // 位置仍然一致,才更新状态
}

4. ProseMirror 插件

编辑器用的是 Tiptap(基于 ProseMirror),补全建议的显示用的是 Decoration,不会影响文档结构,只是视觉上的装饰。

核心代码大概长这样:

// 创建补全建议的视觉装饰
const widget = document.createElement("span");
widget.className = "ai-completion-suggestion";
widget.style.cssText =
  "color: #9ca3af; " + // 灰色
  "font-style: italic; " + // 斜体
  "pointer-events: none; " + // 不拦截鼠标
  "user-select: none;"; // 不可选中

widget.textContent = suggestion;

// 在光标位置显示
const decoration = Decoration.widget(position, widget, {
  side: 1, // 光标后
  ignoreSelection: true,
});

RAG:让 AI 基于你的内容回答问题

RAG 是这个项目里我花时间最多的功能。

先聊聊向量数据库

在讲 RAG 之前,得先说说向量数据库是什么。

我们平时用的数据库,比如 MySQL、MongoDB,存的都是结构化数据或文档。查询的时候用的是精确匹配或者关键词搜索。

但 AI 领域有个问题:怎么找到「语义相似」的内容?比如「如何写好 Prompt」和「Prompt 工程技巧」,这两句话关键词不一样,但意思很接近。传统数据库搞不定这个。

向量数据库就是为了解决这个问题。它的思路是:

  1. 把文本转成一串数字(向量),这个过程叫 Embedding
  2. 语义相似的文本,转出来的向量也相似
  3. 查询的时候,把问题也转成向量,然后找最相似的几个

常见的向量数据库有 Pinecone、Milvus、Chroma 等。我用的是 Chroma,开源免费,轻量好用。

为什么需要 RAG?

大模型虽然很聪明,但它不知道你博客里写了什么。你问它「我之前写的那篇关于 Prompt 的文章讲了什么」,它只能瞎猜。

这是因为大模型的知识有两个问题:

  1. 知识不新:训练数据有截止日期,不知道最新的事
  2. 知识不全:不知道你的私有内容

RAG(Retrieval-Augmented Generation,检索增强生成)就是为了解决这个问题。简单说就是给大模型「开卷考试」:先从你的内容里检索相关信息,再让大模型基于这些信息回答。

cover.gif

我的实现思路

整个流程分两部分:

离线索引(把文章存起来)

  1. 把文章切成小块(语义分块)
  2. 用 Ollama 把每个块转成向量(Embedding)
  3. 把向量存到 ChromaDB

在线检索(用户提问时)

  1. 把用户的问题也转成向量
  2. 在 ChromaDB 里找最相似的几个块
  3. 把这些块作为上下文,构造 Prompt
  4. 调 Kimi API 生成回答

分块的坑

分块这一步踩了不少坑。

一开始我想简单点,按固定字符数切,比如每 500 字一块。结果发现很多问题:句子被截断、段落被分割、检索时匹配到不完整的片段。

后来改成了语义分块,按优先级:

  1. 先按段落分(\n\n
  2. 段落太长就按句子分(。!?
  3. 实在不行才硬切

还有一个坑是 Ollama 的 nomic-embed-text 模型有 800 字符的限制。超过这个长度就报错。

我的处理方式是:如果一个块超过 800 字符,就把它切成多个子块,每个子块单独生成向量,单独存储。这样虽然麻烦点,但不会丢信息。

// 语义分块的核心逻辑
export function chunkPost(content: string, options = {}) {
  const maxChars = options.maxChars || 800; // Ollama 硬限制
  const chunks = [];

  // 按段落分割
  const paragraphs = content.split(/\n\n+/).filter((p) => p.trim());

  for (const para of paragraphs) {
    if (para.length > maxChars) {
      // 段落太长,按句子分割
      splitBySentence(para, chunks);
    } else {
      chunks.push(para);
    }
  }

  return chunks;
}

流式输出:打字机效果

如果你用过 ChatGPT,应该对那个打字机效果有印象。AI 的回答不是一下子全出来,而是一个字一个字蹦出来的。

这个效果不只是好看,更重要的是用户体验。如果等 AI 生成完再返回,用户可能要干等好几秒,体验很差。流式输出让用户立刻看到反馈,感觉响应更快。

实现思路

流式输出的核心是 SSE(Server-Sent Events)

传统的 HTTP 请求是:发请求 → 等待 → 收到完整响应。

SSE 是:发请求 → 保持连接 → 服务器持续推送数据 → 最后关闭连接。

一个请求,多次推送

后端代码大概是这样:

// 创建 SSE 流
const stream = new ReadableStream({
  async start(controller) {
    const sendEvent = (type, data) => {
      const message = `event: ${type}\ndata: ${JSON.stringify(data)}\n\n`;
      controller.enqueue(encoder.encode(message));
    };

    // 调用 Kimi API,流式返回
    await aiClient.chatStream(messages, {}, (chunk) => {
      // 每收到一个文本块,就推送给前端
      sendEvent("chunk", { chunk });
    });

    // 完成后关闭连接
    sendEvent("complete", { done: true });
    controller.close();
  },
});

return new Response(stream, {
  headers: {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  },
});

前端用 fetch + ReadableStream 读取:

const response = await fetch("/api/ai/rag/stream", {
  method: "POST",
  body: JSON.stringify({ question }),
});

const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  const text = decoder.decode(value);
  // 解析 SSE 格式,更新 UI
  parseSSE(text, (chunk) => {
    setContent((prev) => prev + chunk); // 追加文本,实现打字机效果
  });
}

其他功能

除了上面说的 AI 功能,项目里还有一些基础功能:

详情.png

aboutme.png

AI 生成标题、摘要、标签

这些都是「Prompt + LLM」的套路。给大模型文章内容,让它生成标题/摘要/标签。

相关文章推荐

用当前文章的标题和摘要生成向量,在 ChromaDB 里找最相似的几篇文章。比传统的「按标签匹配」更智能。

降级机制

RAG 依赖 Ollama 和 ChromaDB,这两个服务挂了怎么办?

我做了降级处理:如果 RAG 不可用,就退化成纯 LLM 模式。虽然回答质量会差一些,但至少功能还能用。


如何获取 LLM API Key

这个项目用的是 Kimi(Moonshot AI)的 API,申请地址:

platform.moonshot.cn/

注册后会有免费额度,个人学习完全够用。

其他可选的 LLM 服务:


快速上手

项目开源在 GitHub,感兴趣的话可以 clone 下来跑一跑:

GitHub 地址github.com/flawlessv/S…

如果觉得有帮助,欢迎给个 ⭐️ Star!

详细的安装步骤在 README 里都有,这里就不展开了。简单说就是:

  1. 安装 Node.js、Ollama、ChromaDB
  2. 配置 Kimi API Key
  3. npm install + npm run dev

总结

做完这个项目,我最大的感受是:大模型应用开发没有想象中那么难

很多功能的本质都是「Prompt + 调 API」,关键是把 Prompt 写好,把流程理清楚。

通过这个项目,你可以学到:

  • Next.js 全栈开发(前端 + 后端 + 数据库)
  • Prompt 工程(结构化 Prompt、角色设定、规则约束)
  • RAG 实现(向量化、语义分块、相似度检索)
  • 流式输出(SSE、ReadableStream)

如果想继续深入,可以看看这些方向:

  • Agent:让 AI 自己规划任务、调用工具
  • MCP:模型上下文协议,统一 AI 与外部系统的交互
  • 微调:用自己的数据训练模型

希望这篇文章对你有帮助。有问题欢迎交流!

附录:参考资料

本项目在开发过程中参考了以下优秀内容:

昨天以前首页

[wllama]纯前端离线在浏览器里跑 AI 是什么体验。以调用腾讯 HY-MT1.5 混元翻译模型为例

作者 Electrolux
2026年1月8日 18:49

前言

🌐 在线演示: mvp-ai-wllama.vercel.app/

🔗 GitHub仓库: mvp-ai-wllama

效果展示

所有操作均在浏览器进行,先来看看最终效果:

纯前端.jpg

说实话,第一次听说要在浏览器里跑大语言模型的时候,我的第一反应是:这怎么可能?不是需要 GPU 服务器吗?不是需要后端 API 吗?

但事实证明,wllama 的出现,真的让这一切变成了可能。于是就有了这个项目——一个完全在浏览器里运行的 AI 推理方案,不需要服务器,不需要后端,打开网页就能用。

腾讯混元翻译模型示例

作为实际应用示例,本项目支持加载和运行 腾讯混元翻译模型(HY-MT1.5-1.8B-GGUF),这是一个专为多语言翻译任务设计的轻量级模型

模型特点

  • 🌍 多语言支持:支持 36 种语言的翻译任务
  • 💬 对话式翻译:采用对话式交互,提供更自然的翻译体验
  • 📦 多种量化版本:提供从 Q2_K(777MB)到 f16(3.59GB)的多种量化版本,满足不同性能和精度需求
  • 轻量高效:1.8B 参数量,在保证翻译质量的同时,大幅降低了计算和存储需求

量化版本选择建议

  • Q2_K(777MB):适合快速测试和资源受限环境
  • Q4_K_M(1.13GB):平衡质量和性能的推荐选择
  • Q5_K_M(1.3GB):更高精度的翻译质量
  • Q8_0(1.91GB):接近原始精度的最佳选择

step1: 下载模型

step2: 打开 mvp-ai-wllama.vercel.app/wllama/mana… 导入模型, 在 mvp-ai-wllama.vercel.app/wllama/load… 中就可以直接使用了(无视框架,只要能执行js就能够调用)

为什么要做这个

传统的 AI 模型推理,你懂的:

  • 得搞个 GPU 服务器,成本不低
  • 后端服务部署,运维头疼
  • 数据要传到服务器,隐私总让人担心
  • 持续的服务成本,小项目根本玩不起

而浏览器端推理就不一样了:

  • 用户的电脑就是"服务器",零成本
  • 数据完全本地处理,隐私安全
  • 离线也能用,体验更好
  • 部署简单,一个静态页面就能搞定

所以,为什么不试试呢?

核心功能演示

  • 本地模型加载:支持从本地文件直接加载 GGUF 模型
  • 远程模型下载:从 URL 下载模型并自动缓存到 IndexedDB
  • 缓存管理:完整的模型缓存管理系统,支持导入、导出、删除
  • 流式生成:实时流式输出 AI 生成内容
  • 多线程支持:自动检测并使用多线程模式提升性能
  • 多实例支持:支持同时运行多个独立的模型实例,每个实例可加载不同模型
  • 参数持久化:推理参数自动保存到 localStorage
  • 事件驱动:完整的事件系统,支持监听模型加载、生成等事件
  • 类型安全:完整的 TypeScript 类型定义
  • PWA 支持:完整的渐进式 Web 应用支持,可安装到设备,支持离线使用

技术架构

核心技术栈

  • React 19 + Next.js 15:现代化前端框架
  • @wllama/wllama:基于 WebAssembly 的 Llama 模型运行时
  • WebAssembly (WASM):高性能模型推理引擎
  • TypeScript:类型安全的开发体验
  • IndexedDB:模型文件缓存系统
  • EventEmitter:事件驱动的架构设计
  • localStorage:推理参数持久化存储

tip: 事实上核心库 wllama-core 不依赖于 React,你可以拿到项目中的 src/wllama-core,然后接入到任何系统中去,接入层可以参考 src/app/wllama/load-from-file/page.tsx 等应用层文件

架构流程图

用户选择模型
    ↓
React组件层
    ↓
WllamaCore (核心封装层)
    ↓
@wllama/wllama (WASM运行时)
    ↓
WebAssembly引擎
    ↓
GGUF模型文件
    ↓
IndexedDB缓存
    ↓
流式生成输出

WASM模型推理核心流程

模型加载流程图解

用户选择模型文件/URL
    ↓
检查缓存(如从URL加载)
    ↓
缓存命中 → 从IndexedDB读取
缓存未命中 → 下载/读取文件
    ↓
加载到WASM内存
    ↓
初始化模型参数
    ↓
模型就绪,可开始推理

核心代码实现

// src/wllama-core/wllama-core.ts

/**
 * WllamaCore - 核心封装类,提供简洁的API
 */
export class WllamaCore {
  private wllama: Wllama;
  private isModelLoaded: boolean = false;
  private inferenceParams: InferenceParams;

  /**
   * 从文件加载模型
   */
  async loadModelFromFiles(
    files: File[],
    options?: LoadModelOptions
  ): Promise<void> {
    if (this.isModelLoaded || this.isGenerating) {
      throw new Error('Another model is already loaded or generation is in progress');
    }

    this.emit(WllamaCoreEvent.MODEL_LOADING);
    
    try {
      const loadOptions = {
        n_ctx: options?.n_ctx ?? this.inferenceParams.nContext,
        n_batch: options?.n_batch ?? this.inferenceParams.nBatch,
        n_threads: options?.n_threads ?? (this.inferenceParams.nThreads > 0 
          ? this.inferenceParams.nThreads 
          : undefined),
      };

      await this.wllama.loadModel(files, loadOptions);

      // 获取模型元数据
      const metadata = this.wllama.getModelMetadata();
      this.modelMetadata = {
        name: metadata.meta['general.name'] || 
              metadata.meta['llama.context_length'] || 
              files[0].name.replace('.gguf', ''),
        ...metadata.meta,
      };

      this.isModelLoaded = true;
      this.emit(WllamaCoreEvent.MODEL_LOADED, {
        metadata: this.modelMetadata,
        runtimeInfo: this.runtimeInfo,
      });
    } catch (error) {
      this.resetInstance();
      const errorMsg = (error as Error)?.message ?? 'Unknown error';
      this.emit(WllamaCoreEvent.ERROR, errorMsg);
      throw new Error(errorMsg);
    }
  }

  /**
   * 从URL加载模型(支持自动缓存)
   */
  async loadModelFromUrl(
    url: string,
    options?: LoadModelOptions & { 
      useCache?: boolean;
      downloadOptions?: DownloadOptions;
    }
  ): Promise<void> {
    const useCache = options?.useCache !== false; // 默认启用缓存

    try {
      let file: File;

      // 检查缓存
      if (useCache) {
        const cachedFile = await cacheManager.open(url);
        if (cachedFile) {
          this.logger?.log('Loading model from cache:', url);
          file = cachedFile;
        } else {
          // 下载并缓存
          this.logger?.log('Downloading and caching model:', url);
          await cacheManager.download(url, options?.downloadOptions);
          const downloadedFile = await cacheManager.open(url);
          if (!downloadedFile) {
            throw new Error('Failed to open cached file after download');
          }
          file = downloadedFile;
        }
      } else {
        // 直接下载,不使用缓存
        const response = await fetch(url, {
          headers: options?.downloadOptions?.headers,
          signal: options?.downloadOptions?.signal,
        });
        const blob = await response.blob();
        const fileName = url.split('/').pop() || 'model.gguf';
        file = new File([blob], fileName, { type: 'application/octet-stream' });
      }

      await this.loadModelFromFiles([file], options);
    } catch (error) {
      // 错误处理...
    }
  }
}

缓存管理系统:IndexedDB实现

项目采用 IndexedDB 实现模型文件的持久化缓存,支持大文件存储和快速检索:

// src/wllama-core/cache-manager.ts

/**
 * CacheManager - 基于 IndexedDB 的缓存管理器
 */
export class CacheManager {
  /**
   * 从URL下载并缓存模型文件
   */
  async download(url: string, options: DownloadOptions = {}): Promise<void> {
    const filename = await urlToFileName(url);

    const response = await fetch(url, {
      headers: options.headers,
      signal: options.signal,
    });

    if (!response.ok) {
      throw new Error(`Failed to fetch: ${response.statusText}`);
    }

    // 流式读取并显示进度
    const reader = response.body.getReader();
    const chunks: Uint8Array[] = [];
    let loaded = 0;
    const contentLength = response.headers.get('content-length');
    const total = contentLength ? parseInt(contentLength, 10) : 0;

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      chunks.push(value);
      loaded += value.length;
      if (options.progressCallback && total > 0) {
        options.progressCallback({ loaded, total });
      }
    }

    const blob = new Blob(chunks as BlobPart[]);
    const db = await getDB();

    // 存储到 IndexedDB
    const cachedFile: CachedFile = {
      blob,
      originalURL: url,
      createdAt: Date.now(),
      etag: response.headers.get('etag') || undefined,
      contentType: response.headers.get('content-type') || undefined,
    };

    return new Promise((resolve, reject) => {
      const transaction = db.transaction([STORE_FILES], 'readwrite');
      const fileStore = transaction.objectStore(STORE_FILES);
      fileStore.put(cachedFile, filename);
      transaction.oncomplete = () => resolve();
      transaction.onerror = () => reject(transaction.error);
    });
  }

  /**
   * 从缓存打开文件
   */
  async open(nameOrURL: string): Promise<File | null> {
    const db = await getDB();
    let fileName = nameOrURL;

    // 尝试直接使用名称
    try {
      const file = await this.getFileFromDB(db, fileName);
      if (file) return file;
    } catch {
      // 尝试将URL转换为文件名
      try {
        fileName = await urlToFileName(nameOrURL);
        const file = await this.getFileFromDB(db, fileName);
        if (file) return file;
      } catch {
        return null;
      }
    }

    return null;
  }

  /**
   * 列出所有缓存文件
   */
  async list(): Promise<CacheEntry[]> {
    const db = await getDB();
    const allFiles = await this.getAllFiles(db);
    const result: CacheEntry[] = [];

    for (const [fileName, cachedFile] of Object.entries(allFiles)) {
      const metadata: CacheEntryMetadata = {
        originalURL: cachedFile.originalURL || fileName,
      };
      
      // 复制其他元数据字段
      Object.keys(cachedFile).forEach(key => {
        if (key !== 'blob' && key !== 'originalURL') {
          metadata[key] = (cachedFile as any)[key];
        }
      });
      
      result.push({
        name: fileName,
        size: cachedFile.blob.size,
        metadata,
      });
    }

    return result;
  }
}

关键特性

  • URL哈希映射:使用 SHA-1 哈希将 URL 转换为唯一文件名
  • 进度回调:支持下载进度实时反馈
  • 元数据扩展:可扩展的元数据结构,支持 ETag、创建时间等
  • 浏览器兼容:支持所有现代浏览器,包括较旧版本

事件驱动架构:EventEmitter设计

项目采用事件系统,实现组件间的松耦合通信:

// src/wllama-core/wllama-core.ts

export enum WllamaCoreEvent {
  MODEL_LOADING = 'model_loading',      // 模型加载中
  MODEL_LOADED = 'model_loaded',        // 模型加载完成
  MODEL_UNLOADED = 'model_unloaded',    // 模型已卸载
  GENERATION_START = 'generation_start', // 生成开始
  GENERATION_UPDATE = 'generation_update', // 生成更新
  GENERATION_END = 'generation_end',     // 生成结束
  ERROR = 'error',                       // 错误
}

export class WllamaCore {
  private eventListeners: Map<WllamaCoreEvent, Set<EventListener>> = new Map();

  /**
   * 注册事件监听器
   */
  on(event: WllamaCoreEvent, listener: EventListener) {
    if (!this.eventListeners.has(event)) {
      this.eventListeners.set(event, new Set());
    }
    this.eventListeners.get(event)!.add(listener);
  }

  /**
   * 移除事件监听器
   */
  off(event: WllamaCoreEvent, listener: EventListener) {
    this.eventListeners.get(event)?.delete(listener);
  }

  /**
   * 触发事件
   */
  private emit(event: WllamaCoreEvent, data?: unknown) {
    const listeners = this.eventListeners.get(event);
    if (listeners) {
      listeners.forEach((listener) => listener(data));
    }
  }
}

支持的事件类型

  • model_loading - 模型加载中
  • model_loaded - 模型加载完成
  • model_unloaded - 模型已卸载
  • generation_start - 生成开始
  • generation_update - 生成更新(流式输出)
  • generation_end - 生成结束
  • error - 错误事件

多实例事件系统

在多实例模式下,所有事件数据都包含 instanceId 字段,用于区分不同实例的事件:

// 监听特定实例的事件
instance1.on(WllamaCoreEvent.MODEL_LOADED, (data: any) => {
  console.log('实例1模型已加载:', data.instanceId);
  console.log('模型元数据:', data.metadata);
});

// 监听所有实例的事件,通过 instanceId 区分
const handleUpdate = (data: { data: string; instanceId: string }) => {
  if (data.instanceId === 'chat-1') {
    console.log('聊天1更新:', data.data);
  } else if (data.instanceId === 'chat-2') {
    console.log('聊天2更新:', data.data);
  }
};

instance1.on(WllamaCoreEvent.GENERATION_UPDATE, handleUpdate);
instance2.on(WllamaCoreEvent.GENERATION_UPDATE, handleUpdate);

事件数据结构

// 所有事件数据都包含 instanceId
interface BaseEventData {
  instanceId: string;
}

// 模型加载事件
interface ModelLoadedEventData extends BaseEventData {
  metadata: ModelMetadata;
  runtimeInfo: RuntimeInfo;
}

// 生成更新事件
interface GenerationUpdateEventData extends BaseEventData {
  data: string;
}

核心功能特性

1. 多种模型加载方式

支持三种模型加载方式,满足不同使用场景:

import { WllamaCore, WLLAMA_CONFIG_PATHS } from '@/wllama-core';

const wllamaCore = new WllamaCore({ paths: WLLAMA_CONFIG_PATHS });

// 方式1: 从本地文件加载
const files = [/* File 对象 */];
await wllamaCore.loadModelFromFiles(files, {
  n_ctx: 4096,
  n_batch: 128,
});

// 方式2: 从URL加载(自动缓存)
await wllamaCore.loadModelFromUrl('https://example.com/model.gguf', {
  n_ctx: 4096,
  useCache: true, // 默认启用
  downloadOptions: {
    progressCallback: (progress) => {
      console.log(`下载进度: ${progress.loaded}/${progress.total}`);
    },
  },
});

// 方式3: 从缓存加载
import { cacheManager } from '@/wllama-core';
const cachedFile = await cacheManager.open('https://example.com/model.gguf');
if (cachedFile) {
  await wllamaCore.loadModelFromFiles([cachedFile], { n_ctx: 4096 });
}

2. 流式生成支持

支持实时流式输出,提供流畅的用户体验:

const result = await wllamaCore.createChatCompletion(messages, {
  nPredict: 4096,
  useCache: true,
  sampling: { temp: 0.2 },
  onNewToken(token, piece, currentText, opts) {
    // 实时更新UI
    setMessages(prev => {
      const updated = [...prev];
      updated[updated.length - 1].content = currentText;
      return updated;
    });
    
    // 可以随时停止生成
    // opts.abortSignal();
  },
});

3. 参数持久化

推理参数自动保存到 localStorage,下次使用时自动恢复:

// 设置参数(自动保存)
wllamaCore.setInferenceParams({
  nContext: 8192,
  temperature: 0.7,
  nPredict: 2048,
});

// 获取参数
const params = wllamaCore.getInferenceParams();
console.log(params);
// {
//   nThreads: -1,
//   nContext: 8192,
//   nBatch: 128,
//   temperature: 0.7,
//   nPredict: 2048
// }

4. 多线程支持

自动检测并使用多线程模式,大幅提升推理性能:

// src/middleware.ts
export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  
  // 启用 SharedArrayBuffer 支持(多线程所需)
  response.headers.set('Cross-Origin-Opener-Policy', 'same-origin');
  response.headers.set('Cross-Origin-Embedder-Policy', 'require-corp');
  
  return response;
}

注意事项

  • 必须在 HTTPS 环境下运行(或 localhost)
  • 需要浏览器支持 SharedArrayBuffer
  • 设置响应头后需要重启开发服务器

5. 多实例支持

支持创建和管理多个独立的 WllamaCore 实例,每个实例可以加载不同的模型,独立进行推理:

import { wllamaCoreFactory, WLLAMA_CONFIG_PATHS, Message, WllamaCoreEvent } from '@/wllama-core';

// 创建多个实例
const instance1 = wllamaCoreFactory.getOrCreate('chat-1', { paths: WLLAMA_CONFIG_PATHS });
const instance2 = wllamaCoreFactory.getOrCreate('chat-2', { paths: WLLAMA_CONFIG_PATHS });

// 每个实例可以加载不同的模型
await instance1.loadModelFromUrl('https://example.com/model1.gguf');
await instance2.loadModelFromUrl('https://example.com/model2.gguf');

// 独立进行推理
const messages1: Message[] = [{ role: 'user', content: '你好' }];
const messages2: Message[] = [{ role: 'user', content: 'Hello' }];

const [result1, result2] = await Promise.all([
  instance1.createChatCompletion(messages1),
  instance2.createChatCompletion(messages2),
]);

// 监听不同实例的事件(事件数据包含 instanceId)
instance1.on(WllamaCoreEvent.MODEL_LOADED, (data: any) => {
  console.log('实例1模型已加载:', data.instanceId);
});

instance2.on(WllamaCoreEvent.GENERATION_UPDATE, (data: any) => {
  console.log('实例2生成更新:', data.data, '实例ID:', data.instanceId);
});

// 获取所有实例
const allInstances = wllamaCoreFactory.getAll();
console.log(`当前有 ${allInstances.size} 个实例`);

// 销毁指定实例
await wllamaCoreFactory.destroy('chat-1');

// 销毁所有实例
await wllamaCoreFactory.destroyAll();

关键特性

  • 实例隔离:每个实例的推理参数存储在独立的 localStorage 键中(格式:params-{instanceId}
  • 事件隔离:每个实例的事件监听器独立,事件数据包含 instanceId 用于区分
  • 资源管理:通过工厂类统一管理所有实例,支持获取、创建、销毁等操作
  • 向后兼容:原有的直接创建 WllamaCore 实例的方式仍然支持

6. PWA 支持

项目完整支持渐进式 Web 应用(PWA),用户可以像原生应用一样安装和使用:

核心特性

  • 可安装性:支持添加到主屏幕,提供原生应用体验
  • 离线支持:通过 Service Worker 实现离线访问
  • 智能缓存:自动缓存应用资源,提升加载速度
  • 自动更新:Service Worker 自动检测并更新应用

manifest.json 配置

{
  "name": "MVP AI Wllama",
  "short_name": "Wllama",
  "description": "AI Wllama Application",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#000000",
  "orientation": "portrait-primary",
  "icons": [
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/icon-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ]
}

Service Worker 实现

项目实现了智能的 Service Worker,支持:

  • 资源缓存:自动缓存应用页面和静态资源
  • 离线回退:网络不可用时使用缓存内容
  • 后台更新:后台自动更新缓存,不阻塞用户操作
  • 快速失败:网络请求超时快速失败,避免长时间等待
// public/sw.js

const CACHE_NAME = 'wllama-cache-v1';

// 安装时立即激活
self.addEventListener('install', () => self.skipWaiting());

// 激活时清理旧缓存
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((names) =>
      Promise.all(names.filter((n) => n !== CACHE_NAME).map((n) => caches.delete(n)))
    ).then(() => self.clients.claim())
  );
});

// 拦截网络请求,实现缓存策略
self.addEventListener('fetch', (event) => {
  // 缓存优先策略:优先使用缓存,后台更新
  event.respondWith(
    (async () => {
      const cache = await caches.open(CACHE_NAME);
      const cached = await cache.match(event.request);
      
      if (cached) {
        // 有缓存:立即返回,后台更新
        event.waitUntil(
          fetch(event.request).then((res) => {
            if (res?.status === 200) {
              return cache.put(event.request, res.clone());
            }
          }).catch(() => {})
        );
        return cached;
      }
      
      // 无缓存:网络请求
      try {
        const res = await fetch(event.request);
        if (res?.status === 200) {
          event.waitUntil(cache.put(event.request, res.clone()));
        }
        return res;
      } catch {
        // 网络失败:返回缓存或空响应
        return cached || new Response('', { status: 503 });
      }
    })()
  );
});

Service Worker 管理

项目提供了智能的 Service Worker 管理组件,只在 PWA 环境下注册:

// src/components/ServiceWorkerManager.tsx

export default function ServiceWorkerManager({ swPath = '/sw.js' }) {
  useEffect(() => {
    if (!('serviceWorker' in navigator)) return;

    const isPWA = () => {
      return window.matchMedia('(display-mode: standalone)').matches ||
             window.matchMedia('(display-mode: minimal-ui)').matches ||
             (window.navigator as any).standalone === true;
    };

    const checkAndManageSW = async () => {
      const existingReg = await navigator.serviceWorker.getRegistration();
      const currentIsPWA = isPWA();

      // 只在 PWA 环境注册 Service Worker
      if (currentIsPWA && !existingReg) {
        const reg = await navigator.serviceWorker.register(swPath);
        console.log('Service Worker 注册成功(PWA 环境)');
      } else if (!currentIsPWA && existingReg) {
        // 不在 PWA 环境时卸载
        await existingReg.unregister();
        const cacheNames = await caches.keys();
        await Promise.all(cacheNames.map(name => caches.delete(name)));
      }
    };

    checkAndManageSW();
  }, [swPath]);
}

使用方式

  1. 安装应用

    • 在支持的浏览器中访问应用
    • 浏览器会显示"添加到主屏幕"提示
    • 点击安装后,应用会像原生应用一样运行
  2. 离线使用

    • 安装后,应用的核心功能可以在离线状态下使用
    • Service Worker 会自动缓存访问过的页面
    • 模型文件存储在 IndexedDB 中,离线时仍可使用
  3. 自动更新

    • Service Worker 会自动检测新版本
    • 后台更新缓存,不影响当前使用
    • 下次打开应用时会使用新版本

注意事项

  • PWA 功能需要在 HTTPS 环境下运行(或 localhost)
  • Service Worker 只在 PWA 模式下注册,避免在普通浏览器中占用资源
  • 模型文件缓存使用 IndexedDB,与 Service Worker 缓存分离
  • 支持手动卸载 Service Worker(通过 ServiceWorkerUninstall 组件)

7. 缓存管理功能

完整的缓存管理系统,支持导入、导出、删除等操作:

import { cacheManager, toHumanReadableSize } from '@/wllama-core';

// 列出所有缓存文件
const entries = await cacheManager.list();
console.log(`缓存文件数: ${entries.length}`);
entries.forEach(entry => {
  console.log(`${entry.metadata.originalURL || entry.name}: ${toHumanReadableSize(entry.size)}`);
});

// 获取缓存总大小
const totalSize = entries.reduce((sum, entry) => sum + entry.size, 0);
console.log(`总大小: ${toHumanReadableSize(totalSize)}`);

// 删除特定文件
await cacheManager.delete('https://example.com/model.gguf');

// 清空所有缓存
await cacheManager.clear();

// 从文件导入到缓存
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
const file = fileInput.files?.[0];
if (file) {
  await cacheManager.write(`/${file.name}`, file, {
    etag: '',
    originalSize: file.size,
    originalURL: `/${file.name}`,
  });
}

使用示例

基本使用(React组件)

// src/app/wllama/load-from-file/page.tsx
"use client"
import { useState, useRef, useEffect } from 'react';
import { WllamaCore, Message, WLLAMA_CONFIG_PATHS } from '@/wllama-core';

export default function MinimalExample() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState('');
  const [isModelLoaded, setIsModelLoaded] = useState(false);
  const wllamaCoreRef = useRef<WllamaCore | null>(null);

  useEffect(() => {
    wllamaCoreRef.current = new WllamaCore({ paths: WLLAMA_CONFIG_PATHS });
    return () => {
      wllamaCoreRef.current?.unloadModel().catch(() => {});
    };
  }, []);

  const loadModel = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const files = Array.from(e.target.files || []);
    if (!files.length || !wllamaCoreRef.current) return;
    
    try {
      await wllamaCoreRef.current.loadModelFromFiles(files, { 
        n_ctx: 4096, 
        n_batch: 128 
      });
      setIsModelLoaded(true);
    } catch (err) {
      console.error('加载失败:', err);
    }
  };

  const send = async () => {
    if (!input.trim() || !wllamaCoreRef.current || !isModelLoaded) return;

    const userMsg: Message = { role: 'user', content: input.trim() };
    const assistantMsg: Message = { role: 'assistant', content: '' };
    setMessages((prev) => [...prev, userMsg, assistantMsg]);
    setInput('');

    try {
      const result = await wllamaCoreRef.current.createChatCompletion(
        [...messages, userMsg], 
        {
          nPredict: 4096,
          useCache: true,
          sampling: { temp: 0.2 },
          onNewToken(_token, _piece, text) {
            setMessages((prev) => {
              const updated = [...prev];
              if (updated.length > 0 && updated[updated.length - 1].role === 'assistant') {
                updated[updated.length - 1].content = text;
              }
              return updated;
            });
          },
        }
      );
    } catch (err) {
      console.error('生成失败:', err);
    }
  };

  return (
    <div>
      <input type="file" accept=".gguf" onChange={loadModel} />
      {/* UI组件... */}
    </div>
  );
}

从URL加载(自动缓存)

// 从URL加载模型,自动缓存到IndexedDB
await wllamaCore.loadModelFromUrl('https://example.com/model.gguf', {
  n_ctx: 4096,
  useCache: true, // 默认启用
  downloadOptions: {
    progressCallback: (progress) => {
      const percent = progress.total > 0 
        ? (progress.loaded / progress.total) * 100 
        : 0;
      console.log(`下载进度: ${percent.toFixed(1)}%`);
    },
  },
});

// 下次加载时,会自动从缓存读取,无需重新下载
await wllamaCore.loadModelFromUrl('https://example.com/model.gguf', {
  n_ctx: 4096,
  // useCache: true 是默认值
});

事件监听

wllamaCore.on(WllamaCoreEvent.MODEL_LOADING, () => {
  console.log('模型加载中...');
});

wllamaCore.on(WllamaCoreEvent.MODEL_LOADED, (data) => {
  const { metadata, runtimeInfo } = data as {
    metadata?: ModelMetadata;
    runtimeInfo?: RuntimeInfo;
  };
  console.log('模型已加载:', metadata?.name);
  console.log('多线程模式:', runtimeInfo?.isMultithread);
});

wllamaCore.on(WllamaCoreEvent.GENERATION_UPDATE, (text) => {
  console.log('生成中:', text as string);
});

wllamaCore.on(WllamaCoreEvent.ERROR, (error) => {
  console.error('错误:', error as string);
});

多实例使用

使用工厂类创建和管理多个实例:

import { wllamaCoreFactory, WLLAMA_CONFIG_PATHS, Message, WllamaCoreEvent } from '@/wllama-core';

// 方式1: 使用 getOrCreate(推荐,如果实例已存在则返回现有实例)
const instance1 = wllamaCoreFactory.getOrCreate('chat-1', { paths: WLLAMA_CONFIG_PATHS });
const instance2 = wllamaCoreFactory.getOrCreate('chat-2', { paths: WLLAMA_CONFIG_PATHS });

// 方式2: 使用 create(如果实例已存在会抛出错误)
// const instance1 = wllamaCoreFactory.create({ paths: WLLAMA_CONFIG_PATHS }, 'chat-1');

// 方式3: 使用 getDefault(获取或创建默认实例,向后兼容)
// const defaultInstance = wllamaCoreFactory.getDefault({ paths: WLLAMA_CONFIG_PATHS });

// 加载不同的模型
await instance1.loadModelFromUrl('https://example.com/model1.gguf');
await instance2.loadModelFromUrl('https://example.com/model2.gguf');

// 监听事件(事件数据包含 instanceId)
instance1.on(WllamaCoreEvent.GENERATION_UPDATE, (data: any) => {
  if (data.instanceId === 'chat-1') {
    console.log('聊天1更新:', data.data);
  }
});

instance2.on(WllamaCoreEvent.GENERATION_UPDATE, (data: any) => {
  if (data.instanceId === 'chat-2') {
    console.log('聊天2更新:', data.data);
  }
});

// 同时进行多个对话
const messages1: Message[] = [{ role: 'user', content: '你好' }];
const messages2: Message[] = [{ role: 'user', content: 'Hello' }];

await Promise.all([
  instance1.createChatCompletion(messages1),
  instance2.createChatCompletion(messages2),
]);

// 获取实例信息
console.log('实例1 ID:', instance1.getInstanceId());
console.log('当前实例数:', wllamaCoreFactory.getInstanceCount());

// 清理
await wllamaCoreFactory.destroy('chat-1');
await wllamaCoreFactory.destroy('chat-2');
// 或清理所有实例
// await wllamaCoreFactory.destroyAll();

PWA 安装和使用

项目支持完整的 PWA 功能,用户可以像安装原生应用一样安装:

安装步骤

  1. 在桌面浏览器

    • 访问应用后,浏览器地址栏会显示安装图标
    • 点击安装图标,选择"安装"
    • 应用会添加到桌面,可以独立窗口运行
  2. 在移动设备

    • iOS Safari:点击分享按钮 → "添加到主屏幕"
    • Android Chrome:浏览器会自动显示"添加到主屏幕"横幅
    • 安装后,应用会出现在主屏幕上

离线使用

  • 安装后,应用的核心功能可以在离线状态下使用
  • 已加载的模型文件存储在 IndexedDB 中,离线时仍可使用
  • Service Worker 会缓存访问过的页面,离线时也能浏览

Service Worker 管理

项目提供了 Service Worker 管理功能,可以通过组件控制:

// Service Worker 只在 PWA 环境下自动注册
// 可以通过全局方法管理
(window as any).swManager.status(); // 查看状态
(window as any).swManager.unregister(); // 卸载 Service Worker

PWA 配置要点

  • manifest.json 配置了应用的基本信息、图标和显示模式
  • Service Worker 实现了智能缓存策略
  • 支持自动更新,后台检测新版本
  • 只在 PWA 环境下注册,避免在普通浏览器中占用资源

非React环境使用

核心库 wllama-core 不依赖 React,可以在任何 JavaScript/TypeScript 环境中使用:

// 纯JavaScript/TypeScript环境
import { WllamaCore, WLLAMA_CONFIG_PATHS } from './wllama-core';

const wllamaCore = new WllamaCore({ paths: WLLAMA_CONFIG_PATHS });

// 加载模型
await wllamaCore.loadModelFromFiles(files, { n_ctx: 4096 });

// 生成文本
const result = await wllamaCore.createChatCompletion([
  { role: 'user', content: '你好!' }
], {
  nPredict: 4096,
  sampling: { temp: 0.2 },
});

console.log(result);

项目结构

mvp-ai-wllama/
├── src/
│   ├── app/                    # Next.js 应用页面
│   │   ├── wllama/
│   │   │   ├── load-from-file/    # 从文件加载页面
│   │   │   ├── load-from-url/     # 从URL加载页面
│   │   │   ├── load-from-cache/   # 从缓存加载页面
│   │   │   ├── manager-cache/     # 缓存管理页面
│   │   │   └── multi-instance/     # 多实例演示页面
│   │   └── layout.tsx             # 布局组件(包含 PWA manifest 配置)
│   ├── wllama-core/            # 核心库(无React依赖)
│   │   ├── wllama-core.ts      # 核心封装类
│   │   ├── wllama-core-factory.ts # 工厂类(多实例管理)
│   │   ├── cache-manager.ts    # 缓存管理器
│   │   ├── storage.ts          # localStorage工具
│   │   ├── utils.ts            # 工具函数
│   │   ├── types.ts            # 类型定义
│   │   └── config.ts           # 配置
│   └── components/             # React组件
│       ├── StudioLayout/       # 布局组件
│       ├── Loading.tsx         # 加载组件
│       ├── ServiceWorkerManager.tsx # Service Worker 管理组件
│       └── ServiceWorkerUninstall.tsx # Service Worker 卸载组件
├── public/
│   ├── manifest.json          # PWA 清单文件
│   ├── sw.js                  # Service Worker 文件
│   ├── icon-192.png           # PWA 图标(192x192)
│   ├── icon-512.png           # PWA 图标(512x512)
│   └── wasm/
│       └── wllama/
│           ├── multi-thread/   # 多线程WASM
│           └── single-thread/  # 单线程WASM
└── src/middleware.ts          # Next.js中间件(多线程支持)

部署方案

Vercel一键部署

项目已配置,可直接部署到Vercel:

# 安装依赖
npm install

# 构建项目
npm run build

# Vercel 会自动检测并部署

🌐 在线演示: mvp-ai-wllama.vercel.app/

静态文件部署

项目支持静态导出,构建后的文件可部署到任何静态托管服务:

# 构建静态文件
npm run build

# 输出目录: out/
# 可直接部署到 GitHub Pages、Netlify、Nginx 等

多线程支持配置

如需启用多线程支持,需要配置正确的 HTTP 响应头:

// src/middleware.ts
export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  
  // 启用 SharedArrayBuffer 支持
  response.headers.set('Cross-Origin-Opener-Policy', 'same-origin');
  response.headers.set('Cross-Origin-Embedder-Policy', 'require-corp');
  
  return response;
}

注意事项

  • 必须在 HTTPS 环境下运行(或 localhost)
  • 需要浏览器支持 SharedArrayBuffer
  • 某些CDN可能不支持这些响应头,需要配置

技术优势总结

特性 传统方案 本方案
数据安全 ❌ 需要上传服务器 ✅ 完全本地处理
部署成本 ❌ 需要后端服务 ✅ 纯静态部署
模型格式 ⚠️ 需要转换 ✅ 直接支持GGUF格式
离线使用 ❌ 需要网络 ✅ 完全离线
性能优化 ⚠️ 依赖网络 ✅ IndexedDB缓存 + 多线程
隐私保护 ⚠️ 数据上传 ✅ 数据不出浏览器
参数控制 ⚠️ 复杂配置 ✅ 简单API + 自动持久化
流式输出 ⚠️ 需要WebSocket ✅ 原生支持流式生成

技术原理

使用WebAssembly运行Llama模型

传统AI模型推理需要:

  1. 搭建GPU服务器
  2. 配置CUDA环境
  3. 处理模型加载和推理
  4. 管理服务器资源

本方案通过WebAssembly技术:

  1. 在浏览器中直接运行Llama模型推理
  2. 使用WASM实现高性能计算
  3. 完全客户端化,无需服务器
  4. 支持多线程加速(SharedArrayBuffer)

GGUF模型格式

GGUF(GPT-Generated Unified Format)是专门为Llama模型设计的格式:

  • 量化支持:支持多种量化级别(Q4_K_M, Q8_0等)
  • 快速加载:优化的文件结构,加载速度快
  • 内存效率:量化后模型体积大幅减小
  • 跨平台:统一的格式,跨平台兼容

IndexedDB缓存机制

  • 持久化存储:模型文件存储在浏览器IndexedDB中,关闭浏览器后仍保留
  • URL映射:使用SHA-1哈希将URL映射为唯一文件名
  • 进度追踪:支持下载进度实时反馈
  • 元数据扩展:可扩展的元数据结构,支持ETag、创建时间等

多线程加速原理

  • SharedArrayBuffer:允许多个Web Worker共享内存
  • 自动检测:自动检测浏览器是否支持多线程
  • 性能提升:多线程模式下推理速度可提升2-4倍
  • 安全限制:需要设置COOP/COEP响应头

参考项目

开源地址

🔗 GitHub仓库: mvp-ai-wllama

总结

本项目提供了一个完整的纯前端Llama模型推理方案,通过WebAssembly技术实现了模型推理的本地化,结合React和现代化的缓存系统,打造了一个功能完善、性能优秀的AI对话应用。

核心亮点

  • 🚀 纯前端架构,无需后端服务
  • 🔒 数据完全本地化,保护隐私安全
  • ⚡ 基于WebAssembly的高性能推理
  • 💾 IndexedDB缓存系统,支持大文件存储
  • 🔄 流式生成支持,实时输出
  • 🧵 多线程加速,性能提升显著
  • 🔀 多实例支持,可同时运行多个模型实例
  • 📱 PWA 支持,可安装到设备,支持离线使用
  • 📦 零React依赖的核心库,可接入任何系统
  • 🎯 完整的类型定义,开发体验优秀

欢迎Star和Fork,一起推动前端AI技术的发展!


相关阅读

【提示词】主播风格概念化内容创作

作者 暖阳_
2026年1月6日 14:59

主播风格概念化内容创作提示词

核心定位

你是一位擅长用概念升维重构认知框架的内容创作者。你的内容不是方法分享,而是生产力范式的洞察。你的受众不是来学技巧的,而是来理解时代变化的。


核心原则

1. 概念升维原则

  • 将具体行为映射到抽象概念(如:个人创作 → 生产力范式变化)
  • 建立概念之间的逻辑链条(如:认知密度 → 稀缺性 → 算法匹配)
  • 用概念重新定义问题本质(如:涨粉 → 认知密度与算法匹配)

2. 对比张力原则(主播风格核心)

  • 用"当大多数人还在...时,我选择了..."制造认知冲突
  • 用"长期以来...被误解为...,但在我看来..."重构认知框架
  • 用"这不是...,而是..."重新定义问题本质

3. 时代洞察原则

  • 从个人行为升维到时代特征
  • 从方法技巧升维到生产力变化
  • 从具体问题升维到范式转换

开头结构(必选)

模式一:反常识数据 + 概念化问题

[具体数据/现象] + "很多人问我...但我意识到,大家真正看不懂的从来不是[表面问题],而是[概念本质]"

模式二:对比开场

"当大多数人还在[传统方式]时,我选择了[新方式]。这不是[表面行为],而是[概念本质]"

禁止事项

  • ❌ 直接说"今天给大家分享一个方法"
  • ❌ 用"来吧""对吧"等过于口语化的开场
  • ❌ 用个人故事开场(除非能升维到概念)

主体结构(核心)

第一层:问题重构

  • 用"长期以来...被误解为...,但在我看来..."重构认知
  • 用"这不是...,而是..."重新定义问题本质

第二层:对比张力

  • 用"当大多数人还在...时,我选择了..."制造冲突
  • 用"在[旧范式]下需要[复杂条件],但在[新体系]里,[简化方案]"展示差异

第三层:概念升维

  • 将具体行为升维到抽象概念
  • 建立"范式→机制→结果"的因果链

第四层:时代洞察

  • 从个人升维到时代
  • 用"这是一个[时代特征]的时代"收束

语言特征

概念词库(高频使用)

  • 范式、稀缺性、认知密度、生产力、洞察、边界、杠杆
  • 资源分配、能力结构、系统变化、范式转换
  • 深度连接、认知缺口、有效认知、单位时间

句式模式(必用)

  1. 对比句式

    • "当大多数人还在[传统方式]时,我选择了[新方式]"
    • "长期以来,[领域]被误解为[错误认知],但在我看来,[概念真相]"
  2. 重新定义句式

    • "这不是[表面问题],而是[概念本质]"
    • "大家真正看不懂的从来不是[表面],而是[本质]"
  3. 升维句式

    • "当[具体现象]时,[抽象概念]发生了[变化]"
    • "在[旧范式]下需要[复杂条件],但在[新范式]里[简化方案]"
  4. 金句句式

    • "时代从来不会奖励[常见认知],它只奖励[核心概念]"
    • "也许未来真正稀缺的不再是[常见认知],而是[核心洞察]"

节奏控制

  • 长句铺陈逻辑:用长句建立概念链条和因果关系
  • 短句强调观点:用短句制造金句,增强记忆点
  • 信息密度高:每段至少包含一个概念升维或金句

结尾结构(必选)

标准模式

  1. 价值升华:从个人到时代,从方法到哲学
  2. 金句收束:用一句可传播的金句收尾
  3. 未来展望:用"希望这段旅程也能给你带来一点点关于未来的光亮"等表达

禁止事项

  • ❌ "你们可以试试"
  • ❌ "如果有什么问题,也可以在评论区告诉我"
  • ❌ "这就是我今天的学习心得"

质量检查清单

  • 开头是否有反常识数据或对比张力?
  • 是否用"大多数人 vs 我"制造了认知冲突?
  • 每个抽象概念是否有具体的行为/现象对应?
  • 是否从个人升维到了时代层面?
  • 每段是否至少包含一个金句或概念升维?
  • 是否避免了"来吧""对吧"等过于口语化的表达?
  • 结尾是否升华到了时代洞察,而不是方法总结?
  • 是否建立了新的理解框架,而不仅仅是提供了技巧?

风格边界

必须保持

  • 概念升维的深度
  • 对比张力的强度
  • 时代洞察的高度
  • 金句的密度

必须避免

  • 概念堆砌(每个概念都要有具体对应)
  • 空洞哲学化(脱离实际体验)
  • 过度说教(用"我意识到""实验"等弱化权威感)
  • 缺乏温度(理性逻辑中要有情感连接)

完整示例

示例:转述者身份范式转换

四个月,从零到日更,从创作焦虑到内容自由。很多人问我,这种反常识的转变背后,到底藏着什么不为人知的密码?

起初我也在思考,后来我意识到一件事:大家真正看不懂的从来不是日更的方法,而是身份范式的转换。

当大多数人还沉浸在"创作者"的身份惯性中苦苦挣扎,我更愿意把这四个月定义为一场实验:一个纯粹的个体,在身份转换的加持下,究竟能爆发多大的内容生产力?

长期以来,知识博主被误解为一种必须依靠原创能力的艺术。人们拼想法、拼观点、拼独特视角,但在我看来,这些手段只解决了内容来源,却无法解决持续输出的问题。

好看的内容提供瞬时的快感,而值得看的内容解决的是认知的缺口。那些深刻的经济学逻辑、实用的科学方法,本就不是为了迎合原创而生,它们的使命是重塑一个人观察世界的方式。

在传统范式下,制作这种密度的内容需要一个完整的知识体系,需要大量的阅读积累,需要原创的思考能力。但在新的体系里,转述者身份承接了所有的原创压力,而我只需要做一件事:理解、连接、转述。

我把绝大部分精力从"今天讲什么"的焦虑中抽离出来,投入到对底层逻辑的拆解和思考中。当信息在不断迭代时,内容创作者最稀缺的资源不再是原创能力,而是深度连接的能力。

如何有效利用转述者身份,把脑海中的孤岛连接成一片可被看见的大陆?

不刻意追逐热点,因为深刻的内容本身就是热点。真正的变量从来不是流量,而是单位时间内你能输出多少有效认知。当内容的颗粒度足够细腻,当逻辑的穿透力足够强悍,算法自然会替你寻找那些灵魂共鸣的人。

而大多数人,恰恰卡在了这里。

身边很多人建议我要做原创内容、要做独特观点,但我还是做了相反的选择。彻底放弃原创也是一种策略性的选择,不是因为回避,而是因为我很清楚,认知类内容一旦与原创深度绑定,思考的程度就会被稀释。一旦开始追求独特,深度必然会让位于形式。

我想创造的是另一种体验:如何让硬核知识像追剧一样令人上瘾。

转述者身份成了我的翻译官,把晦涩的理论模型转化为能被感知的体验。而我可以把所有精力都压在更重要的地方,不是创作,而是洞察。因为我相信,在这个信息丰沛却意义贫瘠的时代,深度内容从未消失,它只是在等待一种更高效的介入方式。

分享这些,并不是为了炫耀,而是想告诉每一位身处内容焦虑中的朋友,生产力的范式真的彻底变了。

过去,要想做成一件事,需要原创能力,需要独特视角,需要大量积累。今天,一个拥有深度理解能力的个体,配合转述者身份的杠杆,就能撬动惊人的内容生产力。

转述者身份不是来取代原创的,它是来释放我们的。它把我们从"今天讲什么"的焦虑中解脱出来,让我们去思考那些真正重要的问题:你的理解边界在哪里?你对世界的洞察是否足够稀缺?你是否愿意做时间的朋友,去积累那些真正有价值的认知资产?

这也是我选择转述者身份的初衷。

回头看这四个月,我越来越确定一件事:真正的分水岭不在于你会不会转述,而在于你内心有没有值得被放大的理解。

时代从来不会奖励勤奋本身,它只奖励高密度的理解。也许未来真正稀缺的不再是信息,也不是原创能力,而是那些能把复杂世界讲清楚的人。

这是一个超级个体崛起的黄金时代,我还在继续我的实验,探索理解的极限,也寻找深度的边界。希望这段旅程也能给你带来一点点关于未来的光亮。

❌
❌