本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
写在前面
本文作者是一个赛博仓鼠,囤积文章如山,打开率却堪比银行金库。 如题,赛博仓鼠是如何努力对抗碎片化阅读的呢? 先来看看几张成品:
从 Obsidian 收集高亮片段:
Obsidian 示例 1
在 Chrome 浏览文章时发现类似的片段之后:
Chrome 示例 1
如果对某些结论感到兴趣,让 AI 结合此前收藏的若干线索进行解读:
Chrome 示例 2
上述呈现为我的场景定制,它能为我完成以下操作:
从 Obsidian 笔记软件收集知识片段
从 Chrome 浏览器收集知识片段
在 Chrome 展现历史关联并生成 AI 解读
第一二点是常见的笔记场景(Obsidian)和碎片化阅读场景(Chrome)。 第三点目标是「温故知新」:借助 AI 分析当前文本和历史关联片段,例如差异对比、字面逻辑和表达张力分析等。
本文的完整代码,我放在 daijinru/wenko,作为爱好会长期更新。欢迎指正。 作为小白,做了自认为最容易入门的技术选型,如图:
流程图
准备工作
开发 AI 应用的最佳工具是什么呢?当然是 AI 应用本身。首先我们准备几个强力工具:
集成开发环境 VSCode + Copilot
作为技术顾问的纳米 AI 桌面端
负责 Text Embedding 的本地模型 nomic-embed-text
负责 AI 解读的免费模型 qwen3-32b:free
用于存储向量和文本数据的 MongoDB Atlas
此外,本文不作完整步骤复现,而是着重于几个关键过程与其技术复现:
在有限的硬件条件下运行轻量化、成本低的技术方案
如何快速开发各类工具以满足多种知识采集场景
简单的讨论知识碎片化
开始动手
动手环节是:
运行环境
文本向量化、存储和近似搜索
开发知识采集插件
文本解读生成
如果我们有基础,折腾起来应该不费劲。在动手过程,会存在些许技术选型的问题:我倾向于将本项目看作一个 MVP(最小化可行产品),所以尽可能以快速完成目标为准。例如数据存储为何不选择 FAISS 或者 Elasticsearch 等更具技术性的方案——仅因为选用 MongoDB Atlas 后仅需数十行易于理解的代码,对小白较为友好。
运行环境
代码运行环境准备如下:
Node.js 作为 Obsidian Plugin 和 Chrome Plugin 的运行环境
Go 用于开发数据存取服务
运行
nomic-embed-text的 Ollama注册 MongoDB Atlas 服务,创建免费方案。
注册
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 应用,我们了解到:
引入更多数据构建层次化索引,例如原始文本 - 智能摘要 - 自定义属性,甚至包含 Obdiain 的知识图谱;
开发面向跨端、多模态的收集器,以期收集更多更实时的知识片段;
开发更多生成结果的呈现交互,例如 Anki 卡片、LaTeX 片段和思维导图等;
按照业务领域定制专业的提示词模板,以期带来更加精准、实际的价值;
更多可展开的...
但这里只是抛个砖头——我想讨论技术以外的话题: 在完成上述工作以后,赛博仓鼠就能安枕无忧了吗?不是的。 我们完成的工作仅仅是第一步:从知识输入(整合文本碎片)到处理(近似搜索)。AI 解读看起来更像是一个「知识缝合怪」。仓鼠如何消化这些坚果,将其内化为知识能量,是值得更加深入讨论、也是更有想象力的过程。以终为始,举个例子:我如何借助费曼学习法将知识内化?围绕它可以尝试借由当下流行的 AI 写作工具(或者更好的产品形态),再者通过 MCP 将坚果碎片输送到更多的场景。
-END -
如果您关注前端 + AI 相关领域可以扫码进群交流
添加小编微信进群😊
关于奇舞团
奇舞团是 360 集团最大的大前端团队,非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。