在 Obsidian 笔记中利用大型语言模型

2023 年 9 月 21 日

今天我在 Hacker News 上看到一篇关于另一个与 ChatGPT 集成的 Obsidian 插件的文章。市面上有很多这样的工具,我很喜欢看到它们在 Obsidian 中的不同使用方法。建立联系,让你的笔记更深入。一些评论者建议它做的是你需要自己完成的工作,但我认为它以全新的、不可思议的方式赋予你力量。

与你的笔记对话

你可能想做的第一件事,也是最明显的一件事,就是能够与你的笔记对话。向它提出问题以获得更深入的见解。如果你可以将模型指向你的笔记并完成它,那就很方便了。但是大多数模型不能一次接受所有内容。

当你提出问题时,并非所有笔记都相关。因此,你需要找到相关的部分,然后将其交给模型。Obsidian 有一个搜索功能,但它只搜索精确的词语和短语,而我们需要搜索概念。这就是嵌入发挥作用的地方。我们必须创建一个索引。事实证明,这很容易做到。

让我们构建索引器

当你创建 Obsidian 插件时,你可以让它在插件加载时执行某些操作,然后在你触发命令或打开笔记时,或在 Obsidian 中执行其他活动时执行其他操作。因此,我们需要一些东西在插件启动时了解你的笔记,并且它应该保存其进度,这样它就不必重新生成索引。让我们看一下索引我们其中一个笔记的代码示例。我将在这个示例中使用 Llama Index,但 LangChain 是另一个不错的选择。

import { VectorStoreIndex, serviceContextFromDefaults, storageContextFromDefaults, MarkdownReader } from "llamaindex";

const service_context = serviceContextFromDefaults({ chunkSize: 256 })
const storage_context = await storageContextFromDefaults({ persistDir: "./storage" });

const mdpath = process.argv[2];
const mdreader = new MarkdownReader();
const thedoc = await mdreader.loadData(mdpath)

首先,我们需要初始化一个内存数据存储。这是 Llama Index 附带的内存存储,但 Chroma DB 是另一个受欢迎的选择。第二行表示我们将持久化我们索引的所有内容。接下来,我获取文件的路径并初始化一个读取器。然后我读取文件。Llama Index 了解 Markdown,因此它以适当的方式读取并对其进行索引。它还了解 PDF、文本文件、Notion 文档等等。它不仅仅是存储单词,还了解单词的含义以及它们如何与文本中的其他单词相关联。

await VectorStoreIndex.fromDocuments(thedoc, { storageContext: storage_context, serviceContext: service_context });

现在,这部分代码使用的是 OpenAI 提供的服务,但它独立于 ChatGPT,是不同的模型,不同的产品,LangChain 中有一些替代方案可以在本地完成此操作,但速度会慢一些。Ollama 也有一个嵌入功能。你也可以在云中使用这些服务,这些服务是在一个超级快速的自我托管实例上,然后在索引完成后关闭该实例。

现在让我们搜索我们的笔记

现在我们有了此文件的索引。Obsidian 可以为我们提供所有文件的列表,因此我们可以反复运行它。我们正在持久化,因此这只是一次性操作。现在,我们如何提出问题?我们想要一些代码,这些代码可以找到笔记中的相关部分,将其交给模型,并使用这些信息来给出答案。

const storage_context = await storageContextFromDefaults({ persistDir: "./storage" });
const index = await VectorStoreIndex.init({ storageContext: storage_context });
const ret = index.asRetriever();
ret.similarityTopK = 5
const prompt = process.argv[2];
const response = await ret.retrieve(prompt);
const systemPrompt = `Use the following text to help come up with an answer to the prompt: ${response.map(r => r.node.toJSON().text).join(" - ")} `

因此,在这个代码示例中,我们使用已经处理过的内容来初始化索引。Retriever.retrieve 行将获取提示,并找到所有相关笔记片段,并将文本返回给我们。我们在这里说要使用前 5 个匹配项。因此,我将从我们的笔记中获得 5 段文本。有了这些原始信息,我们可以生成一个系统提示,以帮助我们的模型了解在提出问题时该怎么做。

const ollama = new Ollama();
ollama.setModel("llama2");
ollama.setSystemPrompt(systemPrompt);
const genout = await ollama.generate(prompt);

现在我们开始使用模型。我使用的是几天前创建的库,该库已发布在 npm 上。我可以将模型设置为使用 **llama2**,该模型已经使用命令 ollama pull llama2 下载到我的机器上。你可以尝试不同的模型,找到最适合你的模型。

为了快速得到答案,你可能希望坚持使用小型模型。但你还希望模型的输入上下文大小足够大,可以接受所有文本片段。我最多有 5 个片段,每个片段有 256 个令牌。我将模型设置为使用包含文本片段的系统提示。只需提出问题,它将在几秒钟内给出答案。

太棒了。现在,我们的 Obsidian 插件将适当地显示该答案。

我们还能做什么?

你还可以考虑对文本进行摘要,或找到与你的文本最匹配的最佳关键字,并将它们添加到前置内容中,这样你就可以在笔记之间建立更好的联系。我已经尝试过制作 10 个好的问题和答案,发送到 Anki。你可能想要尝试不同的模型和提示来完成这些不同的任务。更改提示甚至模型权重以更适合任务非常容易。

我希望这篇文章能给你一些想法,帮助你构建下一个伟大的 Obsidian 或任何其他笔记工具的插件。使用最新的本地 AI 工具,例如你在 ollama.com 上找到的工具,这种强大的功能非常容易实现,我希望你能向我展示你的成果。