最近、RAG(Retrieval-Augmented Generation)やLLMを実際に使った開発をやってみようと思い、Pythonで簡単な法律RAGを作ってみました。
題材にしたのは、日本の法律です。
「他人のお金を盗んだらどうなる?」
「他人のコンピュータに勝手にログインしたら?」
といった質問に対して、関連する法律の条文を検索し、その条文をLLMに渡して回答させる仕組みです。
今回の目的は法律相談システムを作ることではなく、RAGの仕組みを実際に一通り実装して、検索・評価・LLMによる回答生成まで試してみることです。
ソースコードや環境構築などの詳細はGitHubにまとめています。
作ったもの
ざっくりいうと、以下のような構成です。
法律のMarkdownデータ
↓
PostgreSQL
↓
Embedding
BAAI/bge-m3
↓
PostgreSQL + pgvector
↓
類似検索
↓
関連する法律条文
↓
LLM
↓
回答を生成
使用した主な技術は以下です。
- Python
- FastAPI
- PostgreSQL
- pgvector
- BAAI/bge-m3
- Ollama
- Docker
- LLM / RAG
法律データについては、公開されている日本の法令データをMarkdown形式で取得して利用しました。
なぜ法律を題材にしたか
RAGを試すなら、単純な文章検索よりも「質問に対して、関連する情報を探してくる」ということが分かりやすい題材の方がいいと思いました。
そこで考えたのが法律です。
例えば、
他人のお金を盗むとどうなる?
という質問に対して、単純なキーワード検索なら「お金」「盗む」などの文字列を探すことになります。
一方RAGでは、質問をEmbeddingに変換して、意味的に近い条文を検索できます。
実際に検索してみると、
刑法 第二百三十五条(窃盗)
他人の財物を窃取した者は、窃盗の罪とし、
十年以下の拘禁刑又は五十万円以下の罰金に処する。
といった条文を取得できます。
まずはLLMを使わずに「条文を検索する」ところから
最初からLLMに回答させるのではなく、まずはRAGの前半部分だけを作りました。
つまり、
質問
↓
Embedding
↓
ベクトル検索
↓
関連条文
までです。
例えば、
curl -X POST "http://localhost:8000/search" \
-H "Content-Type: application/json" \
-d '{"query":"他人のお金を盗むとどうなる?","top_k":5}'
とすると、以下のような検索結果が返ってきます。
{
"query": "他人のお金を盗むとどうなる?",
"top_k": 5,
"results": [
{
"law_name": "刑法",
"article_number": "235",
"article_caption": "窃盗",
"similarity": 0.7276,
"citation": "刑法 第二百三十五条(窃盗)"
},
{
"law_name": "刑法",
"article_number": "235の2",
"article_caption": "不動産侵奪",
"similarity": 0.7016
}
]
}
この段階では、まだLLMは使っていません。
まず、
「質問に対して、ちゃんと関係する条文を検索できているか?」
を確認しました。
検索結果を評価してみる
せっかくなので、検索結果を目視するだけではなく、評価用の質問を20件ほど用意しました。
例えば、
- 他人のお金を盗むとどうなる?
- 暴力を使って他人のお金を奪ったら?
- 落ちていた財布を持って帰ったら?
- 仕事で預かっているお金を使い込んだら?
- 他人のコンピュータに勝手にログインしたら?
などです。
そして、期待する条文が検索結果の何位に入るかを調べました。
最初の評価では、
Top-1 Accuracy: 45.0%
Top-3 Recall: 70.0%
Top-5 Recall: 75.0%
という結果になりました。
ただ、調べてみると評価用のテストデータがDBに残っていたことが分かり、それが検索結果に影響していました。
また、法律の場合は質問の内容によって複数の条文が妥当になる場合があります。
例えば「他人の物を勝手に使った」というだけでは、具体的な状況によって適用される条文が変わる可能性があります。
そのため、評価データについても見直しました。
評価対象として妥当な範囲を整理した結果、Top-1 Accuracyは、
45%
↓
70%
となりました。
このあたりは、実際にRAGを作ってみて、
「検索モデルだけでなく、評価データや評価方法も重要なんだな」
と感じたところです。
次にLLMを導入
検索部分が一通り確認できたので、ここでLLMを導入しました。
流れとしては、
ユーザーの質問
↓
Embedding
↓
pgvector
↓
関連する法律条文を取得
↓
取得した条文をLLMに渡す
↓
LLMが回答を生成
です。
これでようやくRAGらしい構成になりました。
例えば、
他人のお金を盗むとどうなる?
という質問に対して、検索した刑法235条などをLLMに渡します。
LLMには、
- 検索結果として渡された条文を根拠にする
- 条文に書かれていないことを断定しない
- 回答に法律名・条番号を含める
- 検索結果から判断できない場合は、判断できないと回答する
といった指示を与えています。
これによって、
質問
↓
関連条文を検索
↓
その条文をLLMに渡す
↓
条文の内容を説明
という一連の処理ができるようになりました。
今回作ってみて分かったこと
今回、実際にRAGを一から作ってみて、個人的には「LLMに質問すれば答えてくれる」という部分よりも、その前段の検索がかなり重要だと感じました。
LLMがどれだけ賢くても、そもそも必要な条文を検索できなければ、回答の根拠となる情報がありません。
今回も、
質問
↓
Embedding
↓
検索
の部分だけを先に作って評価しました。
その結果、
「検索結果として何が返ってきているのか」
を確認した上でLLMを導入できました。
個人的には、この順番で実装したことでRAGの仕組みをかなり理解しやすくなりました。
今後試してみたいこと
今回はかなりシンプルなRAGなので、まだ改善できそうなところがあります。
例えば、
- 1条文 = 1チャンクではなく、段落単位で分割する
- ベクトル検索とキーワード検索を組み合わせる
- Rerankerを導入する
- 条文間の参照関係を利用する
- 検索精度の評価ケースを増やす
- LLMの回答自体も評価する
- 法律の改正を考慮してデータを更新する
などです。
特に、法律のような文書では「意味が似ている条文」が大量に存在するため、単純なベクトル検索だけでは限界がありそうです。
このあたりは今後試してみたいと思っています。
まとめ
今回、Pythonを使って、
法律データ
↓
PostgreSQL
↓
Embedding
↓
pgvector
↓
関連条文検索
↓
検索結果の評価
↓
LLM
↓
RAGによる回答
というところまで、一通り実装してみました。
RAGというと、
「LLMに検索結果を渡せばいい」
くらいのイメージでしたが、実際に作ってみると、データの分割方法や検索結果、評価方法など、LLM以外の部分もかなり重要だと分かりました。
今回はあくまで技術検証用の小さなプロジェクトですが、Python + RAG + Vector DB + LLMを一通り触るという意味では、かなり勉強になりました。
詳細なソースコード、Docker環境、DB構成、API、Embedding生成、検索評価などについてはGitHubにまとめています。
※このプロジェクトは法律相談を目的としたものではなく、RAG技術の検証を目的としたものです。