流行的 LLM 命令行工具加入向量化能力。简化 AI 应用原型开发流程。
LLM 现已提供处理嵌入的工具
LLM 是我用来处理语言模型的 Python 库和命令行工具。我刚刚发布了 LLM 0.9,其中新增了一系列功能,使 LLM 能够提供处理嵌入的工具。
这是一篇很长的文章,包含大量理论和背景知识。如果你已经了解什么是嵌入,下面是一份可以立即尝试的简要说明:
# 安装 LLM
pip install llm
# 如果已经通过 Homebrew/pipx 安装,可以这样升级:
llm install -U llm
# 安装 llm-sentence-transformers 插件
llm install llm-sentence-transformers
# 安装 all-MiniLM-L6-v2 嵌入模型
llm sentence-transformers register all-MiniLM-L6-v2
# 以递归方式为主目录中的每个 README.md 生成并存储嵌入
llm embed-multi readmes \
--model sentence-transformers/all-MiniLM-L6-v2 \
--files ~/ '**/README.md'
# 添加 --store 还可存储文本内容
# 在这些嵌入中对 "sqlite" 运行相似度搜索
llm similar readmes -c sqlite
其他读者请继续阅读,希望读完之后,上面的示例都能变得清晰易懂。
LLM 的新嵌入功能
嵌入集合
嵌入相似度搜索
为目录中的文件生成嵌入
使用 llm-cluster 进行聚类
在语言模型这个更广阔的领域中,嵌入是一个非常有趣的概念。
我在《嵌入:它们是什么,以及为什么重要》中详细解释了嵌入,并提供了视频和带有大量注释的幻灯片。
嵌入模型可以接收一段文本——一个单词、句子、段落,甚至整篇文档——并将其转换为一个称为嵌入向量的浮点数数组。
同一个模型生成的数组长度始终相同——OpenAI 嵌入模型会生成 1,536 个数字,all-MiniLM-L6-v2 则会生成 384 个——但数组本身晦涩难懂。你能用它做什么?
答案是,你可以对它们进行比较。我喜欢将嵌入向量理解为 1,536 维空间中的一个位置。两个向量之间的距离可以衡量它们在语义上有多么相似——至少按照生成这些向量的模型的判断是如此。
“One happy dog”和“A playful hound”最终会彼此接近,尽管它们没有任何相同的关键词。嵌入向量代表了语言模型对文本含义的理解。
你可以使用嵌入完成以下任务:
查找相关条目。我在自己的 TIL 网站上使用了这种方式来展示相关文章,具体做法可参见《使用 openai-to-sqlite 和嵌入存储并提供相关文档》。
构建语义搜索。如上所示,基于嵌入的搜索引擎可以找到与用户搜索词相关的内容,即使其中没有匹配任何关键词。
实现检索增强生成——其技巧是接收用户的问题,在你自己的语料库中找到相关文档,然后利用这些文档让 LLM 给出答案。这里有更多相关内容。
聚类:你可以找到彼此接近的条目簇,并识别文档语料库中的模式。
分类:计算一段文本的嵌入,然后将其与不同类别预先计算出的“平均”嵌入进行比较。
我开发 LLM 的目标,是围绕不断增加的语言模型集合提供一种由插件驱动的抽象。我希望尽可能简化这些模型的安装、使用和比较过程。
新版本增加了几个用于处理嵌入的命令行工具,还新增了一套 Python API,供你在自己的代码中处理嵌入。
它还支持通过插件安装其他嵌入模型。目前我已经发布了一个这样的插件:llm-sentence-transformers,它增加了对基于 sentence-transformers 库的新模型的支持。
上面的示例展示了如何使用 sentence-transformers。LLM 还支持通过 API 访问 OpenAI ada-002 模型。
假设你已经安装了 LLM,可以通过以下方式使用 ada-002 为文本生成嵌入:
# 设置你的 OpenAI API 密钥
llm keys set openai
# <在此处粘贴密钥>
# 为一些文本生成嵌入
llm embed -m ada-002 -c "Hello world"
这会在终端中输出一个很大的 JSON 浮点数列表。你可以添加 -f base64(或 -f hex)以其他格式获取结果,不过这些输出都无法立即派上用场。
将嵌入存储起来之后,它们会有趣得多。
LLM 已经使用 SQLite 存储提示词和响应,因此使用 SQLite 存储嵌入也顺理成章。
LLM 0.9 引入了嵌入集合的概念。每个集合都有一个名称——例如 readmes——并包含一组嵌入,其中每个嵌入都有自己的 ID 和嵌入向量。
同一个集合中的所有嵌入均由同一模型生成,以确保它们可以相互比较。
llm embed 命令可以将向量存储到数据库中,而不是将其返回到控制台。你需要向它传入一个现有(或待创建)集合的名称,以及用于存储该嵌入的 ID。
下面使用 ada-002 嵌入模型,将短语“Hello world”的嵌入存储到名为 phrases 的集合中,ID 为 hello:
llm embed phrases hello -m ada-002 -c "Hello world"
之后添加其他短语时,不再需要指定模型,因为集合会记住它:
llm embed phrases goodbye -c "Goodbye world"
llm embed-db collections 会显示集合列表:
phrases: ada-002
2 embeddings
readmes: sentence-transformers/all-MiniLM-L6-v2
16796 embeddings
数据存储在 SQLite 的嵌入表中,其 schema 如下:
CREATE TABLE [collections] (
[id] INTEGER PRIMARY KEY,
[name] TEXT,
[model] TEXT
);
CREATE TABLE "embeddings" (
[collection_id] INTEGER REFERENCES [collections]([id]),
[id] TEXT,
[embedding] BLOB,
[content] TEXT,
[content_hash] BLOB,
[metadata] TEXT,
[updated] INTEGER,
PRIMARY KEY ([collection_id], [id])
);
CREATE UNIQUE INDEX [idx_collections_name]
ON [collections] ([name]);
CREATE INDEX [idx_embeddings_content_hash]
ON [embeddings] ([content_hash]);
默认情况下,数据会存储在通过 llm embed-db path 查看位置的 SQLite 数据库中,但你可以向各种 LLM 命令传入 --database my-embeddings.db,以使用其他数据库。
每个嵌入向量都以二进制 BLOB 的形式存储在 embedding 列中,其中包含以 32 位浮点数形式打包在一起的那些数字。
content_hash 列包含内容的 MD5 哈希值。除非内容发生变化,否则这有助于避免重新计算嵌入;对于 ada-002 这类基于 API 的嵌入模型,重新计算可能会产生实际费用。
content 列通常为 null,但如果向 llm embed 命令传入 --store 选项,它也可以包含原始文本内容的副本。
如果传入 --metadata '{"json": "goes here"},metadata 可以包含一个带有元数据的 JSON 对象。
你不必使用 -c 传入内容,也可以使用 -i/--input 选项传入文件路径:
llm embed docs llm-setup -m ada-002 -i llm/docs/setup.md
或者像这样通过管道将内容传给标准输入:
cat llm/docs/setup.md | llm embed docs llm-setup -m ada-002 -i -
构建好集合之后,可以使用 llm similar 命令搜索相似的嵌入。
-c "term" 选项会使用该集合的嵌入模型为你传入的文本生成嵌入,并将其用作比较向量:
llm similar readmes -c sqlite
你也可以传入该集合中某个对象的 ID,以改用该对象的嵌入。例如,可以通过这种方式获取相关文档:
llm similar readmes sqlite-utils/README.md
目前,该命令的输出格式是以换行符分隔的 JSON。
llm embed 命令每次只能为一个字符串生成嵌入。llm embed-multi 的功能则强大得多:你可以向它提供 CSV 或 JSON 文件、SQLite 数据库,甚至让它读取目录中的文件,从而一次为多个条目生成嵌入。
许多嵌入模型针对批量操作进行了优化,因此一次为多个条目生成嵌入可以显著提升速度。
文档详细介绍了 embed-multi 命令。下面是几个可以用它完成的有趣操作。
首先,我要为自己的每一条 Apple Notes 笔记创建嵌入。
我的 apple-notes-to-sqlite 工具可以将 Apple Notes 导出到 SQLite 数据库中。我会先运行它:
apple-notes-to-sqlite notes.db
它在我的机器上运行了相当长的时间,最终生成了一个 828M 的 SQLite 数据库,其中包含 6,462 条记录!
接下来,我要使用 sentence-transformers/all-MiniLM-L6-v2 模型,为所有这些笔记的内容生成嵌入:
llm embed-multi notes \
-d notes.db \
--sql 'select id, title, body from notes' \
-m sentence-transformers/all-MiniLM-L6-v2
这花费了大约 15 分钟,并使数据库的大小增加了 13MB。
这里的 --sql 选项指定了一个 SQL 查询。第一列必须是 id,随后的列将被连接在一起形成要嵌入的内容。
在这种情况下,embeddings 被写回到内容来源的同一个 notes.db 数据库。
现在我可以对所有 Apple 笔记运行 embedding 相似性操作了!
llm similar notes -d notes.db -c 'ideas for blog posts'
让我们重新访问这篇文章开头的例子。在这种情况下,我使用 --files 选项来搜索磁盘上的文件,并对每个文件进行嵌入:
llm embed-multi readmes \
--model sentence-transformers/all-MiniLM-L6-v2 \
--files ~/ '**/README.md'
--files 选项接受两个参数:目录路径和匹配文件名的模式。在这种情况下,我在主目录中递归搜索任何名为 README.md 的文件。
运行这个命令会为我的所有 README.md 文件生成 embeddings,然后我可以这样搜索它们:
llm similar readmes -c sqlite
到目前为止,我只涉及了命令行工具。LLM 0.9 还引入了一个新的 Python API 来处理 embeddings。
这包含两个方面。如果你只想嵌入内容并自己处理生成的向量,你可以使用 llm.get_embedding_model():
import llm
# This takes model IDs and aliases defined by plugins:
model = llm.get_embedding_model("sentence-transformers/all-MiniLM-L6-v2")
vector = model.embed("This is text to embed")
vector 随后将是一个 Python 浮点数列表。
你可以这样将其序列化为 LLM 使用的相同二进制格式:
binary_vector = llm.encode(vector)
# And to deserialize:
vector = llm.decode(binary_vector)
Python API 的第二个方面是 llm.Collection 类,用于处理 embeddings 集合。这个示例代码引用自文档:
import sqlite_utils
import llm
# This collection will use an in-memory database that will be
# discarded when the Python process exits
collection = llm.Collection("entries", model_id="ada-002")
# Or you can persist the database to disk like this:
db = sqlite_utils.Database("my-embeddings.db")
collection = llm.Collection("entries", db, model_id="ada-002")
# You can pass a model directly using model= instead of model_id=
embedding_model = llm.get_embedding_model("ada-002")
collection = llm.Collection("entries", db, model=embedding_model)
# Store a string in the collection with an ID:
collection.embed("hound", "my happy hound")
# Or to store content and extra metadata:
collection.embed(
"hound",
"my happy hound",
metadata={"name": "Hound"},
store=True
)
# Or embed things in bulk:
collection.embed_multi(
[
("hound", "my happy hound"),
("cat", "my dissatisfied cat"),
],
# Add this to store the strings in the content column:
store=True,
)
与 LLM 中的其他所有功能一样,目标是 CLI 能做的任何事,Python API 也能做,反之亦然。
embeddings 的另一个有趣应用是你可以用它们来对内容进行聚类——识别文档语料库中的模式。
我开始用一个名为 llm-cluster 的新插件来探索这个领域。
你可以这样安装它:
llm install llm-cluster
让我们使用从 GitHub 提取的数据创建一个新集合。我将使用我的 paginate-json 工具从 GitHub API 导入所有 LLM 问题:
paginate-json 'https://api.github.com/repos/simonw/llm/issues?state=all&filter=all' \
| jq '[.[] | {id: .id, title: .title}]' \
| llm embed-multi llm-issues - \
--database issues.db \
--model sentence-transformers/all-MiniLM-L6-v2 \
--store
运行这个命令得到一个 issues.db SQLite 数据库,其中包含 218 个 embeddings,这些 embeddings 包含在一个名为 llm-issues 的集合中。
现在让我们尝试 llm-cluster 命令,从该集合请求十个聚类:
llm cluster llm-issues --database issues.db 10
这个命令的输出(截断后)看起来像这样:
[
{
"id": "0",
"items": [
{
"id": "1784149135",
"content": "Tests fail with pydantic 2"
},
{
"id": "1837084995",
"content": "Allow for use of Pydantic v1 as well as v2."
},
{
"id": "1857942721",
"content": "Get tests passing against Pydantic 1"
}
]
},
{
"id": "1",
"items": [
{
"id": "1724577618",
"content": "Better ways of storing and accessing API keys"
},
{
"id": "1772024726",
"content": "Support for `-o key value` options such as `temperature`"
},
{
"id": "1784111239",
"content": "`--key` should be used in place of the environment variable"
}
]
},
{
"id": "8",
"items": [
{
"id": "1835739724",
"content": "Bump the python-packages group with 1 update"
},
{
"id": "1848143453",
"content": "Python library support for adding aliases"
},
{
"id": "1857268563",
"content": "Bump the python-packages group with 1 update"
}
]
}
]
这些看起来都不错!但如果我们为每个聚类都有一个精炼的标题,不是很棒吗?
--summary 选项可以通过管道将每个聚类的成员传递给另一个 LLM 调用来生成有用的摘要,从而提供正是这个功能。
llm cluster llm-issues --database issues.db 10 --summary
这使用 gpt-3.5-turbo 为每个聚类生成摘要,使用这个默认提示:
Short, concise title for this cluster of related documents.
我获得的结果相当不错,包括:
Template Storage and Management Improvements
Package and Dependency Updates and Improvements
Adding Conversation Mechanism and Tools
我尝试用在自己笔记本电脑上运行的 Llama 2 模型做同样的事情,带有自定义提示:
llm cluster llm-issues --database issues.db 10 \
--summary --model mlc-chat-Llama-2-13b-chat-hf-q4f16_1 \
--prompt 'Concise title for this cluster of related documents, just return the title'
我没有完全得到我想要的!Llama 2 的提示方式证明难度要大得多,所以每个聚类都返回了看起来像这样的内容:
Sure! Here's a concise title for this cluster of related documents:
"Design Improvements for the Neat Prompt System"
This title captures the main theme of the documents, which is to improve the design of the Neat prompt system. It also highlights the focus on improving the system's functionality and usability
llm-cluster 仅花了几个小时就组装起来,我认为这是一个积极的指标,表明 LLM 库正在朝着正确的方向发展。
我最期待的两个未来特性是索引和分块。
llm similar 命令和 collection.similar() Python 方法目前使用的是有效上最慢的蛮力方法:计算输入向量与集合中每个其他 embedding 之间的余弦差,然后对结果进行排序。
这对包含几百个项目的集合来说效果不错,但对于 100,000 个或更多项目的集合来说会开始出现问题。
有许多潜在的方法可以加快这个过程:你可以运行像 FAISS 或 hnswlib 这样的向量索引,使用像 sqlite-vss 或 pgvector 这样的数据库扩展,或转向像 Pinecone 或 Milvus 这样的托管向量数据库。
有这么多潜在的解决方案,LLM 的明显答案是通过插件来解决这个问题。
我仍在思考细节,但核心思想是用户应该能够针对一个或多个集合定义索引,然后 LLM 将协调对该索引的更新。这些可能不会实时发生——某些索引的重建成本可能很高,所以分批应用更新有好处。
我在今年早些时候在 datasette-faiss 上试验过 FAISS。这可能会成为我第一次实现的基础。
embeddings 表有一个更新时间戳列来支持这个用例——所以索引器可以针对自上次索引运行以来已更改的项目运行。
关注 issue #216 了解这个特性的更新。
在构建基于 embeddings 的搜索引擎时,最大的挑战是决定如何最好地对文档进行"分块"。
用户会输入短语或问题。一个四个单词的问题的 embedding 可能不一定紧密映射到一篇千字文章的 embedding,即使这篇文章本身应该是对该查询的很好匹配。
为了最大化返回最相关内容的机会,我们需要对嵌入什么内容更加聪明。
我仍在尝试获得对这里有意义的策略的良好感受。我看到的一些包括:
将文档拆分为固定长度的较短段。
拆分成段,但与前后段之间有大约 10% 的重叠,以减少由于单词和句子被拆分的方式破坏其语义含义而引起的问题。
使用 NLP 技术按句子拆分。
根据文档标题等内容拆分成更高层级的章节。
此外,还有一些更令人兴奋的、由 LLM 驱动的方法:
让 LLM 生成文档摘要,并对该摘要进行嵌入。
询问 LLM:“以下文本回答了哪些问题?”,然后对生成的每个问题分别进行嵌入!
现在已经可以使用 LLM 尝试这些不同的技术:编写代码完成拆分,然后将结果传给 Collection.embed_multi() 或 llm embed-multi。
但是……如果 LLM 能够替你拆分文档,那就太酷了——而且拆分技术本身可以由插件定义,从而方便尝试新的方法。
现在应该已经很清楚,LLM 项目的潜在范围极其庞大。我正尝试使用插件,将规模巨大且快速增长的模型与技术生态系统整合起来,让人们能够尽可能轻松地使用它并在其基础上进行构建。
你可以通过很多方式提供帮助!
加入 #llm Discord 频道,讨论这个项目。
试用插件,并通过它们运行不同的模型。目前已经有 12 个插件,其中几个插件可以用来运行几十种、甚至数百种模型(尤其是 llm-mlc、llm-gpt4all 和 llm-llama-cpp)。我自己对这些插件的探索还只是浅尝辄止,而且目前只在 Apple Silicon 上进行测试。我非常希望进一步了解哪些模型表现良好、哪些模型表现不佳,以及哪些模型在不同硬件上的性能最好。
尝试为一个新模型构建插件。我的梦想是,每一个重要的大语言模型都能拥有一个 LLM 插件,让它可以轻松安装和使用。
使用 LLM 构建一些东西,并告诉我你构建了什么。没有什么比人们使用开源项目构建出酷炫产品的故事更能为项目注入动力了。
OpenAI 对 Hugging Face 的意外网络攻击,是已经成为现实的科幻故事——2026 年 7 月 22 日
与 Claude Code 团队的 Cat 和 Thariq 炉边对谈——2026 年 7 月 21 日
Kimi K3,以及我们仍然可以从鹈鹕基准测试中学到什么——2026 年 7 月 16 日
本文是 Simon Willison 撰写的《LLM 现在提供处理嵌入的工具》,发布于 2023 年 9 月 4 日。
“LLM 新版本”系列的一部分
llm、ttok 和 strip-tags——用于处理 ChatGPT 及其他 LLM 的 CLI 工具——2023 年 5 月 18 日晚上 9:04
我的 LLM CLI 工具现在通过插件支持自托管语言模型——2023 年 7 月 12 日下午 2:24
LLM 现在提供处理嵌入的工具——2023 年 9 月 4 日晚上 8:32
LLM 0.13:带注释的发布说明——2024 年 1 月 26 日晚上 11:08
现在,你可以在终端中使用 LLM,针对图像、音频和视频运行提示词——2024 年 10 月 29 日下午 3:09
LLM 0.22:带注释的发布说明——2025 年 2 月 17 日早上 6:19
使用 LLM schema 从非结构化内容中提取结构化数据——2025 年 2 月 28 日下午 5:07
下一篇:使用 llm-clip 构建图像搜索引擎,通过 llm chat 与模型聊天
上一篇:Datasette 1.0a4 和 1.0a5,以及每周笔记
每月赞助我 10 美元,即可收到一封经过精心筛选的邮件摘要,了解当月最重要的 LLM 进展。
付钱让我少给你发点内容!