コマンドラインでLLMが使える「llm」を試す2: Embedding
以下で試した「llm」だが、
Embeddingも使えて、SQLiteに保存ができるので、どうやらCLIでRAGができてしまう様子。
ということで試してみる。RAGが目的なので、ドキュメントの記載順とは違う点にご留意いただければ。
前回同様、Dockerでお試し。
docker run --rm -it python:3.12-slim /bin/bash
以降はDockerコンテナ内で。
パッケージインストール
pip install llm
llm --version
llm, version 0.17.1
OpenAIのAPIキーをセット
llm keys set openai
APIキーを入力
Enter key:
OpenAIで使用可能なモデルを確認
llm embed-models
ada-002 (aliases: ada)
3-small
3-large
3-small-512
3-large-256
3-large-1024
デフォルトのモデルを確認
llm embed-models default
<No default embedding model set>
初期状態では設定されていないみたい。
text-embedding-3-smallをデフォルトにセット
llm embed-models default 3-small
再度デフォルトを確認
llm embed-models default
3-small
ではEmbeddingsを生成してみる。-c
(contents
)で文字列を渡す
llm embed -c 'こんにちは!'
[0.040503513, -0.013761906, -0.029625986, (snip) , 0.009661444, 0.025641633, 0.0067220675]
モデルを明示的に指定する場合は-m
llm embed -c 'こんにちは!' -m 3-large
[0.0035559454, 0.00020729085, -0.014258971, (snip) , -0.0055388995, 0.0020199036, -0.0008766699]
生成したEmbeddingsをSQLiteに保存することができる。
まずSQLiteのデータベースがどこに作成されるかを確認
llm collections path
/root/.config/io.datasette.llm/embeddings.db
上記はDockerコンテナの場合だが、通常はユーザの$HOME/.config/io.datasette.llm/embeddings.db
になると思う。ただし、この時点ではDBファイル自体はまだ作成されていない。
ls -lt /root/.config/io.datasette.llm/embeddings.db
ls: cannot access '/root/.config/io.datasette.llm/embeddings.db': No such file or directory
ではデータベースに登録する。データベースは
- コレクション: 同じモデルを使って生成したEmbeddingsのグループ。
- ID: 個々のEmbeddingsを識別するための一意なキーとなるID。
で管理することになる。コレクションが1つのベクトル空間になるのだろうと思う。
コレクションはIDはllm embed コレクション名 キー名 -c "文字列" --store
で指定する。以下の場合だと、コレクション名: quotes
、キー名: keiba-1
となる。
llm embed quotes keiba-1 -c "皐月賞は最も速い馬が勝つ" --store
--store
をつけない場合、EmbeddingsとIDだけが保存される。--store
をつけると指定した文字列がcontent
として併せて保存される。一般的には、Embeddingsに紐づいている文字列なしで使うことは少ないかなと思うので、--store
は必須と考えて良さそう。
コレクションの一覧を確認
llm collections list
quotes: 3-small
1 embedding
Embeddingsが一つ登録されているのがわかる。
データベースファイルも確認。
ls -lt /root/.config/io.datasette.llm/embeddings.db
-rw-r--r-- 1 root root 40960 Nov 6 11:41 /root/.config/io.datasette.llm/embeddings.db
データベースファイルが作成されているのがわかる。
では、続けて複数登録してみる。
llm embed quotes keiba-2 -c "ダービーは最も運のいい馬が勝つ" --store
llm embed quotes keiba-3 -c "菊花賞は最も強い馬が勝つ" --store
ではベクトル検索してみる。検索はllm similar
を使う。
llm similar quotes -c '最も強い馬が勝つのは?'
{"id": "keiba-3", "score": 0.7369883088595545, "content": "\u83ca\u82b1\u8cde\u306f\u6700\u3082\u5f37\u3044\u99ac\u304c\u52dd\u3064", "metadata": null}
{"id": "keiba-2", "score": 0.6486974513369872, "content": "\u30c0\u30fc\u30d3\u30fc\u306f\u6700\u3082\u904b\u306e\u3044\u3044\u99ac\u304c\u52dd\u3064", "metadata": null}
{"id": "keiba-1", "score": 0.5854581385413556, "content": "\u7690\u6708\u8cde\u306f\u6700\u3082\u901f\u3044\u99ac\u304c\u52dd\u3064", "metadata": null}
Unicodeエンコードされているので、jqでデコードする。Dockerコンテナなのでjqがない。インストール。
apt update && apt install -y jq
llm similar quotes -c '最も強い馬が勝つのは?' | jq -c .
{"id":"keiba-3","score":0.736949844261556,"content":"菊花賞は最も強い馬が勝つ","metadata":null}
{"id":"keiba-2","score":0.6487469154391556,"content":"ダービーは最も運のいい馬が勝つ","metadata":null}
{"id":"keiba-1","score":0.585456258576198,"content":"皐月賞は最も速い馬が勝つ","metadata":null}
想定通り。
ではRAGをやってみる。
question="最も強い馬が勝つのはなんというレースですか?"
llm similar quotes -c $question | jq -c . | llm $question
最も強い馬が勝つと言われるレースは「菊花賞」です。
シェルスクリプトとかにしておくといいかもね
以下は余談。
SQLiteの中身をのぞいてみた。
SQLiteのデータベースの中身を少しのぞいてみる。sqlite3をインストール。
apt install -y sqlite3
データベースに接続
sqlite3 /root/.config/io.datasette.llm/embeddings.db
SQLite version 3.40.1 2022-12-28 14:03:47
Enter ".help" for usage hints.
sqlite>
SELECT name FROM sqlite_master WHERE type='table';
_sqlite_migrations
collections
embeddings
.schema collections
CREATE TABLE [collections] (
[id] INTEGER PRIMARY KEY,
[name] TEXT,
[model] TEXT
);
CREATE UNIQUE INDEX [idx_collections_name]
ON [collections] ([name]);
SELECT * FROM collections;
1|quotes|3-small
.schema embeddings
CREATE TABLE IF NOT EXISTS "embeddings" (
[collection_id] INTEGER REFERENCES [collections]([id]),
[id] TEXT,
[embedding] BLOB,
[content] TEXT,
[content_blob] BLOB,
[content_hash] BLOB,
[metadata] TEXT,
[updated] INTEGER,
PRIMARY KEY ([collection_id], [id])
);
表示できるものだけ。
sqlite> SELECT collection_id, id, content, metadata, updated FROM embeddings;
1|keiba-1|皐月賞は最も速い馬が勝つ||1730893298
1|keiba-2|ダービーは最も運のいい馬が勝つ||1730893646
1|keiba-3|菊花賞は最も強い馬が勝つ||1730893650
sqlite-vssとか使ってるのかなと思いきや、そうではなさそう。
以下には注意
これは現在、大きなコレクションに対してうまくスケールしない、遅いブルートフォースアプローチを使用しています。プラグインによって提供されるベクトルインデックスによって、よりスケーラブルなアプローチを追加する計画については issue 216 を参照してください。
まとめ
Embeddingsも扱えてSQLiteがビルトインされているので、CLIでRAGもお手軽にできた。ファイルが多い場合は検索速度がネックになるかもしれないが、上に記載している通り、ベクトルDBもプラグイン化する計画はある様子なので、そのうちできるようになるかも?(ただIssueの最終更新は去年なのですぐにはできなさそう・・・)
その他ドキュメントには
- 画像のEmbeddings(
llm-clip
を使用) - sentence-transformersを使用したEmbeddings(
llm-sentence-transformers
を使用) - メタデータを追加
- コンテンツをファイルに記載して、まとめてバッチでEmbedding生成・登録
- ディレクトリ内のファイルを、まとめてバッチでEmbedding生成・登録
- データベースファイルを明示的に指定
などが紹介されているので参考に。
GitHubレポジトリをクローンして、レポジトリ内のファイルをまとめてベクトル化してDBに登録、そのままDBファイルもレポジトリにコミットしておけば、誰でも手元でRAGできる、みたいなユースケースは面白そう。