🔎

法務RAGシステムの性能改善ハッカソンを題材にしたRAGの基礎学習

2024/12/21に公開

LLMにおけるRAGの重要性が認識され始めたことによりRAGに特化したコンペのraggle開催が始まりました。
今回はこのコンペを題材にRAGの基礎を学びたいと思います。

ゴール

法務RAGハッカソンにおいての動作確認である2つの質問に正しく答えられるようなRAGを開発する。

質問
1.ソフトウェア開発業務委託契約について、委託料の金額はいくらですか?
→ 委託料の金額は金五百万円(税別)です。

2.グラフィックデザイン制作業務委託契約について、受託者はいつまでに仕様書を作成して委託者の承諾を得る必要がありますか?
→ 2024年10月31日です

コンペの内容

コンペの概要にあるようにこのコンペでは、ユーザーが質問に対してPDFデータを元にした正しい回答を生成することを目指します。PDFのデータはリンクで与えられており、このPDFのデータは一般的な情報ではないため通常のChatGPTのモデルでは回答できない内容となってます。
このデータをRAGで取込むことで正しい回答が行えるようなシステムを構築します。

LangChain

LLMを利用したアプリケーション開発を行う際の便利なツールにLangChainがあります。
RAGを利用していく上でも欠かせないツールですので、概要を理解しておきましょう。
LangChainは大きく下記の要素で構成されてます。

要素 説明 種類/関連メソッド
Models 利用するモデルを定義 LLM、Chat Model、Text Embedding Model
Prompts LLMに入力するテキストを管理 -
Indexes LLMがドキュメントとやり取りできるようにするためにドキュメントをベクトル化、ベクトル化したデータはVectorStoreというDBに格納して管理 VectorStoreの種類
①Chroma:一番有名な軽量VBで、構築は非常に簡単なので、VBのチュートリアルコードにはよく使われる
②Qdrant:性能とスピードが良いので人気
③Faiss:Facebookが開発した大規模データ向けのVB、検索スピードが非常に速い
④LanceDB:マルチモダール特化のVBなので、画像とか検索する場合は他のVBより精度高い
Chains 複数のプロンプト入力を実行する機能 ①Simple Chain:複数のChainを繋げていく上で最小単位となるChain(LLMChain)
②Sequential Chain:複数のChainを繋げたChain(SimpleSequentialChain)
③Custom Chain:自由にChainを繋ぎ合わせることができるオリジナルのChain(ConcatenateChain)
Memory ChainsやAgentsの内部における状態保持をする機能 ConversationBufferMemory:覚えられるだけ覚え
ConversationBufferWindowMemory:過去N回分のやりとりだけ覚える
ConversationSummaryBufferMemory:過去の内容を要約して覚える
Agents 言語モデルに渡されたツールを用いて、モデル自体が、次にどのようなアクションを取るかを決定・実行・観測・完了するまで繰り返す機能 ①Tools:Agentというロボットが外界とやり取りをするための機能
②Agents:プロンプトの内容に応じてツールを使い分け、自動で解法を生成してくれるロボットの機能
③Toolkits:特定のユースケースに応じて、ツールを初期搭載したAgentの機能
④Agent Executor:Agentの行動を実行するための機能

ConversationalRetrievalChain

ConversationalRetrievalChainはCustomChainの1つで、シンプルな質問応答モデルを実装するためのChainです。

上記のように3ステップで最終的な質問に対する回答を生成します。
1.最初に質問と会話履歴をLLMに送信し、単独の質問(生成質問)を生成
2.生成質問をもとにVectorStoreから関連情報(チャンク群)を抽出
3.生成質問+関連情報を再度LLMに送信し最終的な回答を取得

プロセス

それでは実際にRAGを実装してみましょう。
プロセスは下記の通りです。

1.必要ライブラリのインポート
2.LLMのAPIキー設定
3.データの分割
4.Modelの設定
5.Embeddingした値をVectorStoreに格納
6.メモリの用意
7.プロンプトの生成
8.ConversationalRetrievalChainの実行

1.必要ライブラリのインポート

langchainやopenai等の必要なライブラリをインストールします。

!pip install -U langchain-google-genai langchain langchain-community langchain_openai
!pip install openai chromadb tiktoken pypdf
!pip install pdfminer.six pi_heif

import os
import logging
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.chat_models import ChatOpenAI #Models
from langchain.vectorstores import Chroma #Indexes
from langchain.chains import ConversationalRetrievalChain #Chains
from langchain.memory import ConversationBufferMemory #Memory
from langchain.schema.runnable import RunnablePassthrough
from langchain.prompts import PromptTemplate #Prompts

2.LLMのAPIキー設定

OpenAIのAPIキーを設定します。

# OpenAI APIキーの設定
import os
os.environ["OPENAI_API_KEY"] = "自分で取得したAPIキーを設定"

3.データの分割

今回学習させるPDFデータをURLから取得します。
その後、RecursiveCharacterTextSplitterメソッドを利用してチャンクサイズの制限を下回るまで再帰的に分割していきます。
今回はチャンクサイズを512文字、オーバーラップ(文字を重複させる分割)させる文字数を128と設定して分割してみようと思うので、chunk_size=512, chunk_overlap=128とパラメータ設定をしてます。

pdf_file_urls = [
    "https://storage.googleapis.com/gg-raggle-public/competitions/29676d73-5675-4278-b1a6-d4a9fdd0a0ba/dataset/Architectural_Design_Service_Contract.pdf",
    "https://storage.googleapis.com/gg-raggle-public/competitions/29676d73-5675-4278-b1a6-d4a9fdd0a0ba/dataset/Call_Center_Operation_Service_Contract.pdf",
    "https://storage.googleapis.com/gg-raggle-public/competitions/29676d73-5675-4278-b1a6-d4a9fdd0a0ba/dataset/Consulting_Service_Contract.pdf",
    "https://storage.googleapis.com/gg-raggle-public/competitions/29676d73-5675-4278-b1a6-d4a9fdd0a0ba/dataset/Content_Production_Service_Contract_(Request_Form).pdf",
    "https://storage.googleapis.com/gg-raggle-public/competitions/29676d73-5675-4278-b1a6-d4a9fdd0a0ba/dataset/Customer_Referral_Contract.pdf",
    "https://storage.googleapis.com/gg-raggle-public/competitions/29676d73-5675-4278-b1a6-d4a9fdd0a0ba/dataset/Draft_Editing_Service_Contract.pdf",
    "https://storage.googleapis.com/gg-raggle-public/competitions/29676d73-5675-4278-b1a6-d4a9fdd0a0ba/dataset/Graphic_Design_Production_Service_Contract.pdf",
    "https://storage.googleapis.com/gg-raggle-public/competitions/29676d73-5675-4278-b1a6-d4a9fdd0a0ba/dataset/M&A_Advisory_Service_Contract_(Preparatory_Committee).pdf",
    "https://storage.googleapis.com/gg-raggle-public/competitions/29676d73-5675-4278-b1a6-d4a9fdd0a0ba/dataset/M&A_Intermediary_Service_Contract_SME_M&A_[Small_and_Medium_Enterprises].pdf",
    "https://storage.googleapis.com/gg-raggle-public/competitions/29676d73-5675-4278-b1a6-d4a9fdd0a0ba/dataset/Manufacturing_Sales_Post-Safety_Management_Contract.pdf",
    "https://storage.googleapis.com/gg-raggle-public/competitions/29676d73-5675-4278-b1a6-d4a9fdd0a0ba/dataset/software_development_outsourcing_contracts.pdf",
    "https://storage.googleapis.com/gg-raggle-public/competitions/29676d73-5675-4278-b1a6-d4a9fdd0a0ba/dataset/Technical_Verification_(PoC)_Contract.pdf",
]

split_docs = []
for pdf_urls in pdf_file_urls:
  loader = PyPDFLoader(pdf_urls)
  pages = loader.load()

  text_splitter = RecursiveCharacterTextSplitter(chunk_size=512, chunk_overlap=128)
  split_docs.append(text_splitter.split_documents(pages)) #text_splitterの対象がドキュメントの場合はsplit_documentsを利用

4.Modelの設定

Modelの設定を行います。
今回は精度とコストを考えてModelはgpt-4o-miniを利用します。

base_llm = ChatOpenAI(model_name="gpt-4o-mini", temperature=0) #Models
llm = RunnablePassthrough() | base_llm #引数①

5.Embeddingした値をVectorStoreに格納

エンべディングモデルにはtext-embedding-3-largeを利用します。
精度は求めずに低いコストで実現したい人はtext-embedding-3-smallを利用しましょう。
ちなみにそれぞれ価格は下記の通りです。

text-embedding-3-small:1kトークンあたり$0.00002
text-embedding-3-large:1kトークンあたり$0.00013

エンべディングとは文字列をベクトル化する処理です。
詳細は下記等を参照ください。
https://future-coders.net/langchain入門-8embed-埋め込み/

エンべディングしたデータをVectorStoreに格納していきます。
今回はVectorStoreにChromaを利用してます。

embeddings = OpenAIEmbeddings(model="text-embedding-3-large")

for split_doc in split_docs:
  vectorstore = Chroma.from_documents(split_doc, embedding=embeddings, persist_directory=".")
  vectorstore.persist()

6.メモリの用意

chatモデルでは会話の履歴はデータとして保存されないため、明示的にメモリを用意しデータを保存しておく必要があります。LangChainではこれらのデータ保存のメモリ機能を有してます。
ここではConversationBufferMemoryを利用してメモリを構築します。
ConversationBufferMemoryの引数は下記の通りとなっており、今回はmemory_keyに"chat_history"を設定し、LLMとの会話履歴を保存します。

引数名 説明 デフォルト値
return_messages bool True の場合、会話履歴を文字列ではなく Message オブジェクトのリストとして返します。 False
input_key str 入力データのキー名を指定します。 None
output_key str 出力データのキー名を指定します。 None
memory_key str 会話履歴を保存する際のキー名を指定します。 "history"
memory = ConversationBufferMemory(
    memory_key="chat_history",
    return_messages=True,
    output_key="answer"
) #引数③

7.プロンプトの生成

PromptTemplateメソッドを利用してプロンプトを生成します。
ここで生成するプロンプトはConversationalRetrievalChainでLLMに送信する2回目のプロンプトに該当します。

template = """あなたは与えられた文書の内容に基づいて質問に答える専門家です。
質問に対して、文書の情報のみを使用して詳細かつ正確に回答してください。
文書に関連する情報がない場合は、「申し訳ありませんが、この文書にはその情報が含まれていません」と答えてください。

文脈情報:
{context}

質問: {question}

回答:"""

PROMPT = PromptTemplate(template=template, input_variables=["context", "question"])

8.ConversationalRetrievalChainの実行

ConversationalRetrievalChainのfrom_llmメソッドを利用してChainを作成します。
from_llmメソッドの引数は下記の通りです。

これまでに用意してきたretriever(VectorStore)やMemory,Prompt等のインスタンスを引数として指定します。

引数名 説明 必須かどうか デフォルト値
llm 使用する言語モデル(LLM)のインスタンスを指定します。 必須 なし
retriever ドキュメント検索のためのRetrieverのインスタンスを指定します。 必須 なし
memory 会話の履歴を保持するためのMemoryオブジェクトを指定します。 任意 None
return_source_documents 出力結果に関連ドキュメントを含めるかどうかを指定します。 任意 False
chain_type 使用するチェーンの種類を指定します(例: "stuff", "map_reduce" など)。 任意 "stuff"
verbose 実行中に詳細なログを出力するかどうかを指定します。 任意 False
input_key 入力データのキーを指定します(複数の入力がある場合に使用)。 任意 "question"
output_key 出力データのキーを指定します(複数の出力がある場合に使用)。 任意 "answer"
callback_manager 実行中のコールバックを処理するためのマネージャーを指定します。 任意 None
max_tokens_limit トークン数の上限を設定します。これを超えた場合、トランザクションを分割して処理します。 任意 None
qa = ConversationalRetrievalChain.from_llm(
    llm=llm,
    retriever=vectorstore.as_retriever(search_kwargs={"k": 3}),# indexから上位いくつの検索結果を取得するか
    memory=memory,
    return_source_documents=True,
    verbose=True,
    combine_docs_chain_kwargs={"prompt": PROMPT},
    chain_type="stuff",# 検索した文章の処理方法
    return_generated_question=False,
    output_key="answer"
)

9.実行

上記で作ったRAGに2つの質問を投げてみます。

質問1

下記の質問を投げて、「委託料の金額は金五百万円(税別)です。」と回答が出力されたら成功です。

1.ソフトウェア開発業務委託契約について、委託料の金額はいくらですか?

下記を実行します。

query = "ソフトウェア開発業務委託契約について、委託料の金額はいくらですか?"
chat_history = []

# invoke メソッドを使用
result = qa.invoke({"question": query, "chat_history": chat_history})

print("\n最終回答:")
print(result["answer"])

結果は下記の通り、正しい回答を得ることができました。

> Entering new StuffDocumentsChain chain...


> Entering new LLMChain chain...
Prompt after formatting:
あなたは与えられた文書の内容に基づいて質問に答える専門家です。
質問に対して、文書の情報のみを使用して詳細かつ正確に回答してください。
文書に関連する情報がない場合は、「申し訳ありませんが、この文書にはその情報が含まれていません」と答えてください。

文脈情報:
れたマニュアル等一切の成果物をいう。 
(4) 「第三者ソフトウェア」 
第三者が権利を有するソフトウェアであって、本件ソフトウェアを構成する一部
として利用するため、第三者からライセンスを受けるものをいう。 
 
第 2 条 (業務内容) 
乙は、甲の本件ソフトウェアの開発に係る業務を受託する。 
 
第 3 条 (納期) 
本件ソフトウェアの納期は、20241231 日とする。 
 
第 4 条 (委託料) 
1. 甲は乙に対し、本件業務の対価として、本件業務の検収完了後 30 日以内に乙の指定
する銀行口座に金五百万円を振込む方法により支払う。ただし、振込手数料は甲の負
担とする。 
2. 本件業務の遂行に必要な旅行交通費、消耗品等にかかる費用は全て甲が負担し、乙は
甲に対し当該費用を委託料とは別途請求できる。 
3. 甲は、本契約が本件業務を完了せずに終了した場合には、第 1 項に定める金額のうち
既にした履行の割合に応じた金額を乙に支払う。ただし、当該終了が甲の責めに帰す
べき事由による場合には、甲は、第 1 項に定める額の全額を乙に支払う。

1 
ソフトウェア開発業務委託契約 
 
ネオスカイウェイブ合同会社(以下「甲」という。)とインフィニティクラウド株式会社
(以下「乙」という。)は、甲が乙に委託するソフトウェアの開発(以下「本件業務」とい
う。)につき、次のとおりソフトウェア開発業務委託契約(以下「本契約」という。)を締
結する。 
 
第 1 条 (定義) 
(1) 「本件ソフトウェア」 
本契約に基づき開発されるソフトウェアであって、プログラム、コンテンツ、デ
ータベース類及び関連資料等をいう。 
(2) 「仕様書」 
本件ソフトウェアの機能要件及び非機能要件が定められ、これに基づき本件ソフ
トウェアの入出力全般に関する仕様が定められた設計書をいう。 
(3) 「納品物」 
本件ソフトウェア及び仕様書その他本件業務の遂行に付随して甲のために作成さ
れたマニュアル等一切の成果物をいう。 
(4) 「第三者ソフトウェア」 
第三者が権利を有するソフトウェアであって、本件ソフトウェアを構成する一部
として利用するため、第三者からライセンスを受けるものをいう。 
 
第 2 条 (業務内容)

を相手方に交付しなければならない。 
2. 委託者又は受託者が相手方に「変更提案書」を交付した場合、その交付日から 7 日以
内に、委託者と受託者とで変更の可否について協議を行う。 
3. 前項の協議の結果、変更を可とする場合は、変更提案書の記載事項を承認の上、双方
記名押印する。仕様変更により、本件業務の業務委託料(第 4 条で定義する。)及び
納品期日の変更の必要が生ずる場合は、委託者と受託者にて業務委託料及び納品期日
等の条件の変更を協議・決定する。 
 
第 4 条 (委託料) 
1. 委託者は、受託者に対し、本件業務の対価(以下「業務委託料」という。)として、
金 300,0000 円(消費税別)を支払う。 
2. 前項の業務委託料は、本件業務の終了後 30 日以内に、受託者の指定する銀行口座へ振
込んで支払う。なお、振込手数料は、委託者の負担とする。 
3. 本契約が解除その他の事由により本契約の有効期間の途中で終了した場合であっても、
当該終了が受託者の責めに帰すべき事由によらないときは、第 1 項の業務委託料の全
額を受託者に支払う。 
 
第 5 条 (費用負担)

質問: ソフトウェア開発業務委託契約について、委託料の金額はいくらですか?

回答:

> Finished chain.

> Finished chain.

最終回答:
委託料の金額は金五百万円です。

質問2

下記の質問を投げて、「2024年10月31日です」と回答が出力されたら成功です。

2.グラフィックデザイン制作業務委託契約について、受託者はいつまでに仕様書を作成して委託者の承諾を得る必要がありますか?

下記を実行します。

query = "グラフィックデザイン制作業務委託契約について、受託者はいつまでに仕様書を作成して委託者の承諾を得る必要がありますか?"
chat_history = []

# invoke メソッドを使用
result = qa.invoke({"question": query, "chat_history": chat_history})

print("\n最終回答:")
print(result["answer"])

結果は下記の通り、正しい回答を得ることができました。

> Entering new LLMChain chain...
Prompt after formatting:
Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question, in its original language.

Chat History:

Human: コールセンター業務委託契約における請求書の発行プロセスについて、締め日と発行期限を具体的に説明してください。
Assistant: 申し訳ありませんが、この文書にはその情報が含まれていません。
Follow Up Input: グラフィックデザイン制作業務委託契約について、受託者はいつまでに仕様書を作成して委託者の承諾を得る必要がありますか?
Standalone question:

> Finished chain.


> Entering new StuffDocumentsChain chain...


> Entering new LLMChain chain...
Prompt after formatting:
あなたは与えられた文書の内容に基づいて質問に答える専門家です。
質問に対して、文書の情報のみを使用して詳細かつ正確に回答してください。
文書に関連する情報がない場合は、「申し訳ありませんが、この文書にはその情報が含まれていません」と答えてください。

文脈情報:
1 
グラフィックデザイン制作業務委託契約書 
 
株式会社ファントムホライズン(以下「委託者」という。)と株式会社レムナントソング
(以下「受託者」という。)は、委託者が受託者に委託するグラフィックデザインの制作に
つき、次のとおりグラフィックデザイン制作業務委託契約(以下「本契約」という。)を締
結する。 
 
第 1 条 (委託) 
委託者は、受託者に対し、以下の業務(以下「本件業務」という。)を委託し、受託者
はこれを受託する。 
(1) 業務内容:コーポレートロゴのグラフィックデザインの開発・提案(準委任) 
(2) 成果物:コーポレートロゴのグラフィックデザイン案(以下「本件成果物」とい
う。) 
(3) 仕様・コンセプト等:別途仕様書において定めるとおりとする。 
 
第 2 条 (業務内容) 
1. 受託者は、委託者からグラフィックデザインにかかる仕様・コンセプト等についての
ヒアリングを実施し、2024 年 10 月 31 日までに仕様書を作成して委託者の承諾を得
る。 
2. 受託者は、仕様書に基づき、2024 年 12 月 31 日までに、本件成果物を委託者に対して

る。ただし、過分の費用を要するときは、委託者・受託者間で別途協議する。 
 
第 4 条 (仕様の変更等) 
1. 委託者又は受託者は、仕様書の確定後であっても、相手方の同意を得て、仕様書を変
更することができる。 
2. 前項に基づき仕様書を変更する場合には、委託者及び受託者はあらかじめ本件業務の
スケジュール及び委託料の変更について協議を行う。

ヒアリングを実施し、2024 年 10 月 31 日までに仕様書を作成して委託者の承諾を得
る。 
2. 受託者は、仕様書に基づき、2024 年 12 月 31 日までに、本件成果物を委託者に対して
提出する。本件業務は、委託者による成果物の提出をもって完了する。 
3. 委託者の都合によりスケジュールが遅滞した場合には、双方協議の上、以後のスケジ
ュールを変更する。 
 
第 3 条 (委託料) 
1. 本件業務にかかる委託料は金 3,000,000 円(税込)とする。 
2. 委託者は受託者に対し、本件業務の委託料を、本件成果物の提出がされた日の翌月末
日までに、受託者の指定する銀行口座に振り込む方法により支払う。ただし、振込手
数料は委託者の負担とする。 
3. 本件業務の遂行に必要な旅行交通費、消耗品等にかかる費用は全て受託者が負担す
る。ただし、過分の費用を要するときは、委託者・受託者間で別途協議する。 
 
第 4 条 (仕様の変更等) 
1. 委託者又は受託者は、仕様書の確定後であっても、相手方の同意を得て、仕様書を変
更することができる。

質問: グラフィックデザイン制作業務委託契約において、受託者はいつまでに仕様書を作成して委託者の承諾を得る必要がありますか?

回答:

> Finished chain.

> Finished chain.

最終回答:
受託者は、2024年10月31日までに仕様書を作成して委託者の承諾を得る必要があります。

今回はRAGGLEのコンペデータを利用してRAGの基礎とLangChainの利用方法を学んでみました。

Discussion