Skip to content

本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com

写在前面

本文作者是一个赛博仓鼠,囤积文章如山,打开率却堪比银行金库。 如题,赛博仓鼠是如何努力对抗碎片化阅读的呢? 先来看看几张成品:

从 Obsidian 收集高亮片段:

Obsidian 示例 1

在 Chrome 浏览文章时发现类似的片段之后:

Chrome 示例 1

如果对某些结论感到兴趣,让 AI 结合此前收藏的若干线索进行解读:

Chrome 示例 2

上述呈现为我的场景定制,它能为我完成以下操作:

  1. 从 Obsidian 笔记软件收集知识片段

  2. 从 Chrome 浏览器收集知识片段

  3. 在 Chrome 展现历史关联并生成 AI 解读

第一二点是常见的笔记场景(Obsidian)和碎片化阅读场景(Chrome)。 第三点目标是「温故知新」:借助 AI 分析当前文本和历史关联片段,例如差异对比、字面逻辑和表达张力分析等。

本文的完整代码,我放在 daijinru/wenko,作为爱好会长期更新。欢迎指正。 作为小白,做了自认为最容易入门的技术选型,如图:

流程图

准备工作

开发 AI 应用的最佳工具是什么呢?当然是 AI 应用本身。首先我们准备几个强力工具:

  1. 集成开发环境 VSCode + Copilot

  2. 作为技术顾问的纳米 AI 桌面端

  3. 负责 Text Embedding 的本地模型 nomic-embed-text

  4. 负责 AI 解读的免费模型 qwen3-32b:free

  5. 用于存储向量和文本数据的 MongoDB Atlas

此外,本文不作完整步骤复现,而是着重于几个关键过程与其技术复现:

  1. 在有限的硬件条件下运行轻量化、成本低的技术方案

  2. 如何快速开发各类工具以满足多种知识采集场景

  3. 简单的讨论知识碎片化

开始动手

动手环节是:

  1. 运行环境

  2. 文本向量化、存储和近似搜索

  3. 开发知识采集插件

  4. 文本解读生成

如果我们有基础,折腾起来应该不费劲。在动手过程,会存在些许技术选型的问题:我倾向于将本项目看作一个 MVP(最小化可行产品),所以尽可能以快速完成目标为准。例如数据存储为何不选择 FAISS 或者 Elasticsearch 等更具技术性的方案——仅因为选用 MongoDB Atlas 后仅需数十行易于理解的代码,对小白较为友好。

运行环境

代码运行环境准备如下:

  1. Node.js 作为 Obsidian Plugin 和 Chrome Plugin 的运行环境

  2. Go 用于开发数据存取服务

  3. 运行 nomic-embed-text 的 Ollama

  4. 注册 MongoDB Atlas 服务,创建免费方案。

  5. 注册 OpenRouter,拿到 API Key

上述准备工作完成后获得的配置用于填入到(如果没有请新建)位于项目根目录的 config.json。 运行环境准备妥当后,我们来到第二步。

文本向量化、存储和近似搜索

文本向量化指将文本片段转化为高维向量(嵌入表示),在本文通过本地模型 nomic-embed-text 完成。 上述模型通过 ollama 快速部署:

$ ollama pull nomic-embed-text

由于 nomic-embed-text 模型仅用于生成文本嵌入向量,无需 ollama run 命令,可直接调用:

$ curl http://localhost:11434/api/embeddings -d '{ "model": "nomic-embed-text", "prompt": "Your text here" }'

使用 go 代码编写,例如:

type OllamaResponse struct { Embedding []float32`json:"embedding"`}// 省略其它逻辑// text 变量是我们提交的文本片段resp, err := http.Post(config.OllamaURL, "application/json", bytes.NewBufferString(fmt.Sprintf(`{"model":"nomic-embed-text","prompt":"%s"}`, text)))if err != nil {return"", err}defer resp.Body.Close()var response OllamaResponseif err := json.NewDecoder(resp.Body).Decode(&response); err != nil {return"", err}

这里需留意 nomic-embed-text 生成的是 768 维向量,关系后续近似搜索的配置。 接下来,我们着手存储工作。首先定义一个简单的存储结构体 EmbeddingDoc

type EmbeddingDoc struct { ID string `bson:"_id,omitempty"` Content string `bson:"content"` Article string `bson:"article"` Vector []float32 `bson:"vector"` CreatedAt time.Time `bson:"created_at"`}

在该结构体,Content 用于获取原文,Vector 用于建立索引,以便完成近似搜索。

建立索引也是关键的一个步骤,请按照 Atlas Search 提供的 Create Search Index 流程创建。

前面已完成在线数据库的配置,这里是存储过程代码:

// 省略连接数据库的操作collection := client.Database(config.DatabaseName).Collection(config.Collection)doc := EmbeddingDoc{ Content:   text, Vector:    response.Embedding, CreatedAt: time.Now(),}result, err := collection.InsertOne(context.TODO(), doc)

如果我们顺利的完成上述几个步骤,并积累了一定量的文本片段,接下来可以跑一遍近似搜索。 不要忘了先前提到的建立索引。

create-vector-index

这里是近似搜索的简单实现:

// 省略数据库连接操作// 向量近似搜索,提前在 Atlas Search 创建索引 numCandidates 768 默认 cosinepipeline := mongo.Pipeline{ {{  Key: "$vectorSearch",  Value: bson.D{   {Key: "queryVector", Value: queryVector},   {Key: "path", Value: "vector"},   {Key: "numCandidates", Value: 768},   {Key: "limit", Value: 5},   {Key: "index", Value: "vector_index"},  }, }},}// 省略解码查找结果数据的过程

完成上面的内容,我们得到一个可以完成文本向量化、存储和近似搜索的服务。可以通过命令行简单的测试:

$ curl -X POST http://localhost:8080/generate -H "Content-Type: application/json" -d '{"text": "Hello World"}'$ curl -X POST http://localhost:8080/generate -H "Content-Type: application/json" -d '{"text": "Hi World"}'$ curl -X POST http://localhost:8080/search -H "Content-Type: application/json" -d '{"text": "world"}'

开发知识采集插件

接下来的内容比上一章节要更加扣题,但是开发内容相对常规:主要是文本的收集、近似搜索结果和解读开发交互界面。按照个人的习惯,我选择为 Obsidian 和 Chrome 两个软件开发插件。 首先为 Obsidian 开发一个用于收集 == 高亮文本 == 的插件。

Obsidian 插件

Obsidian 是我最常用的笔记软件,是因为它有丰富且开放的插件生态(还有更多的优点,这里不展开)。 为它开发插件,首先要 Fork 一份 obsidian-sample-plugin 到本地。 这里也可直接使用本文提到的代码仓库路径。在该插件的基础上开发,我们仅需将该路径(即 inbox/obsidian/wenku) 作为本地目录打开。这里推荐阅读插件开发指南。

该插件的功能比较简单,仅仅用于收集当前页面的高亮文本,代码示例:

this.registerEvent(   this.app.workspace.on("file-menu",  (menu, file) => { menu.addItem((item)  => {   item   .setTitle("🍉 对高亮 Embedding") // 注册一个按钮到页面菜单  .setIcon("highlighter")  .onClick(async () => {    const highlights = this.collectHighlights()    if (highlights.length === 0) {   new Notice("没有找到高亮内容");   return;    }    new Notice(`已收集高亮内容: ${highlights.join(", ")}`);

拿到高亮文本数组,请求此前开发的服务接口 /generate 转换向量并存储:

private async generateVector(text: string): Promise<string> {try {   const response = await request({  url: 'http://localhost:8080/generate',  method: 'POST',  headers: { 'Content-Type': 'application/json' },  body: JSON.stringify({  text })   });   const result: GenerateResult = JSON.parse(response);    if (!result.id)  thrownewError("无效的ID响应");   console.info(`生成的ID: ${result.id}`);

这里需要考虑:并非所有高亮文本皆需转换为向量。因此在文本向量嵌入前需要作重复 / 相似度计算,但在本文的实现相对小白(原型验证优先):先做近似搜索,将原文和返回结果中作一番简单的比较后再决定是否将其存入。

Chrome 插件

由于经常通过 Chrome 阅读网络文章。我设想的场景是:当阅读到某个段落,会想到此前深入理解过的某个概念,或者仅仅是想把这段文字和另外一篇文章的观点结合起来。如果有一个工具能够自动创建这种连接,应该会很有帮助。所以本文的结论是开发一个 Chrome 插件(扩展),负责收集用户选定的文本,调用语义(近似)搜索服务得到关联的文档集合。 初始化 Chrome 插件应用,同样可选取本项目(路径在 inbox/chrome/chrome-wenko)或使用工具:

$ npm create chrome-ext@latest chrome-wenko -- --template react-ts

开发 Chrome 插件可能会陷入多种交互界面的选择困难。我选择的是 Sidepanel 侧边栏组件。它的优点是非侵入式,既不干扰原文阅读,同时有独立呈现空间。但是由于其架构特性,Sidepanel 需要通过 chrome.runtime 提供的 onMessage 和 sendMessage 和原文页面作交互,譬如: 先往页面菜单注册一个「高亮」按钮,用于发送选中的文本:

chrome.contextMenus.create({   id: "highlightAndOpenPanel",  title: "高亮选中文本并打开Wenko",  // 菜单显示名称   contexts: ["selection"]  // 仅在用户选中文本时显示 })

当按下右键菜单的「高亮」按钮,需要发送事件:

// 先打开 Sidepanel 界面hrome.sidePanel.open({ windowId: tab.windowId })// 等一会儿将选中文本发送到 sidepanelchrome.runtime.sendMessage({   action: "updateSidePanel",  text: selectedText})

在 Sidepanel 组件,监听 updateSidePanel 事件,将接收内容呈现:

const [selectedText, setSelectedText] = useState('')chrome.runtime.onMessage.addListener((request)  => {  if (request.action  === "updateSidePanel") { const text = request.text setSelectedText(text)// 省略内容

当 Sidepanel 组件拿到选中文本,执行与 Obsidian 插件相似的逻辑:文本转换向量并作近似搜索。所以不再添加代码,有兴趣可前往阅读:inbox/chrome/chrome-wenko/src/sidepanel/SidePanel.tsx

文本解读生成

文本解读生成同样放在 Sidepanel 组件。单独拎出是因为其可探索的点比较有趣。该功能可以理解为轻量级的 RAG,通过组合目标文本与关联历史文本,构建提示词,交由 AI 解读。

相关实现分几个步骤,由于上一步我们已经拿到关联文本,这里就从手动组织提示词模板开始:

const getPrompts = () => {  return `【智能解析任务】基于用户选中的文本片段「${selectedText}」,结合下列${Math.min(matchResults.length,5)} 条上下文线索,进行多维度语义解析。注意:1. 若上下文存在矛盾信息,需标注差异点并解释成因 2. 涉及专业术语时,须构建领域知识图谱关联 3. 区分文本的字面逻辑与潜在表达张力  【上下文线索】${matchResults.slice(0,5).map((item,index)  =>   `线索${index+1}: ${item.content}`).join('\n\n')}`;}

由于本文的 AI 解读是设计为一个类似 Chat 的流行交互,所以选择使用 @microsoft/fetch-event-source 库处理 SSE 文本流(服务器发送事件)。

await fetchEventSource('http://localhost:8080/chat', {  method: 'POST',  headers: { "Content-Type": "application/json"  },  body: JSON.stringify({ model: "qwen/qwen3-32b:free", messages: [   {  role: "user",  content: `${getPrompts()}`   } ]  }),

服务端返回的数据同样遵循 Server-sent Events 规范。

// 设置响应头w.Header().Set("Content-Type", "text/event-stream")w.Header().Set("Cache-Control", "no-cache")w.Header().Set("Connection", "keep-alive")// 省略代码...// 请求 openrouter 提供的 qwen/qwen3-32b:freereq, _ := http.NewRequest("POST", "https://openrouter.ai/api/v1/chat/completions", // 省略代码...fmt.Fprintf(w, "id: %d\n", eventId)fmt.Fprintf(w, "event: %s\n", eventType)fmt.Fprintf(w, "data: %s\n\n", dataBytes)flusher.Flush()

在交互界面,对 SSE 数据流作如下处理:

onmessage(line) { try {   const parsed = JSON.parse(line.data)   if (parsed?.type === 'statusText') {  setLoadingText(parsed?.content)   }   if (parsed?.content && parsed?.type === 'text') {  setInterpretation(prev => prev + parsed.content)  setLoadingText('')   }

到这里可以说是一切就绪:一个简单的 RAG 应用。但是后续才是有意思的地方。 对比一个标准的 RAG 应用,我们了解到:

步骤
我们的实现
标准实现
检索
向量近似搜索返回若干历史文本
向量库 / 全文检索返回相关文档片段
增强
手动设计提示词模板
自动构建上下文提示
生成
由通用 AI 模型生成角度
专用微调或者约束生成
可从技术角度角度展开讲的点很多,譬如:


  1. 引入更多数据构建层次化索引,例如原始文本 - 智能摘要 - 自定义属性,甚至包含 Obdiain 的知识图谱;

  2. 开发面向跨端、多模态的收集器,以期收集更多更实时的知识片段;

  3. 开发更多生成结果的呈现交互,例如 Anki 卡片、LaTeX 片段和思维导图等;

  4. 按照业务领域定制专业的提示词模板,以期带来更加精准、实际的价值;

  5. 更多可展开的...

但这里只是抛个砖头——我想讨论技术以外的话题: 在完成上述工作以后,赛博仓鼠就能安枕无忧了吗?不是的。 我们完成的工作仅仅是第一步:从知识输入(整合文本碎片)到处理(近似搜索)。AI 解读看起来更像是一个「知识缝合怪」。仓鼠如何消化这些坚果,将其内化为知识能量,是值得更加深入讨论、也是更有想象力的过程。以终为始,举个例子:我如何借助费曼学习法将知识内化?围绕它可以尝试借由当下流行的 AI 写作工具(或者更好的产品形态),再者通过 MCP 将坚果碎片输送到更多的场景。

-END -

如果您关注前端 + AI 相关领域可以扫码进群交流

添加小编微信进群😊

关于奇舞团

奇舞团是 360 集团最大的大前端团队,非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。