はじめに
n番煎じですが,OpenAI API+Langchain+Faissを使用するRAG環境の構築をします.RAG用のテキストファイルのサンプルは用意していないため,適当にテキストファイルを作成して使用してください.
ディレクトリ構成
ややこしいですが,ディレクトリ構成は以下のようになります.
llm_test
|- code
| |- main.py # mainの関数
|
|- workspace
| |- make_index.py # RAG用のindexを作成
| |- convert_prompt_json.py # LLMへ入れるプロンプトのテンプレートを作成
|
|- database
| |- source # RAG用のindexの元となるテキストファイル(自前で用意してください)
| | |- *.txt
| | |- *.txt
| | |- *.txt
| |
| |- index # RAG用のindexを保管 (make_index.pyで作成)
| | |- index.faiss
| | |- index.pkl
| |
| |- question_prompt_templete.txt # プロンプト文のテンプレートテキスト(自前で用意してください)
| |- question_prompt_templete.json # テンプレートテキストをlangchain用に変換したjson(convert_prompt_json.pyで作成)
|
|-Dockerfile
|-requirements.txt
.env # 環境変数を管理
docker-compose.yaml
以下でそれぞれの中身を説明します.
Dockerfile
Dockerファイルの中身は以下の通りです.中身はシンプルで,単にPython環境にpipで必用なモジュールをインストールしているだけです.今回はprod(本番環境用)しか作成していませんが,必用があればbaseを継承してdev環境を作成することもできます.
FROM python:3.11 AS base
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
RUN \
--mount=type=cache,target=/var/lib/apt/lists \
--mount=type=cache,target=/var/cache/apt/archives \
apt-get update \
&& apt-get install -y --no-install-recommends build-essential
WORKDIR /code
COPY ./requirements.txt /code/requirements.txt
RUN pip3 install --no-cache-dir -U pip && \
pip3 install --no-cache-dir -r /code/requirements.txt
FROM base AS prod
WORKDIR /code
COPY /code .
COPY workspace/ /workspace
requirements.txt
雑なのでバージョンは指定していません.今回必用なモジュールだけ入っています(langchain-communityはいらないかも).
langchain
langchain-core
langchain-community
langchain-openai
faiss-cpu
unstructured
openai
.env
OpenAIから取得したAPIキーを書くだけです.
OPENAI_API_KEY = "{ここにAPI KEYを書く}"
docker-compose.yaml
コンテナ1つだけだからdocker-composeを使う必要があるかは微妙ですが,実行するのが楽なので使います.databaseを分離する必要があるときには使えると思います.今回はbindマウントしてしまいます.
services:
llm_test:
image: llm_test
container_name: llm_test
env_file: ./.env
build:
context: ./llm_test
dockerfile: Dockerfile
target: prod
volumes:
- type: bind
source: ./llm_test/database
target: /database
workspaceの中身
convert_prompt_json.py
question_prompt_templete.txtに書かれたLLMへ入力する用の文をlangchainですぐに読み込めるようにjsonに変換しておくプログラムです.
question_prompt_templete.txtには以下のような文章が入っている想定です.
あなたは質問を受け付け、専門知識を基に回答するアドバイザーです。
ユーザからの質問と、その回答で利用できるドキュメントの情報があります。
[回答生成の手順]の通りに、ユーザの質問に対する回答を生成してください。
<回答生成の手順>
- [制約] [ドキュメントの内容]をすべて理解してください。
- [制約]は、回答を生成する際に守らなければならないことです。必ず従ってください。
- [ドキュメントの内容]には、参考にするべきドキュメントが含まれています。ドキュメントはデータベースに保存されており、質問の内容をもとに必用な情報が与えられます。
- このあとユーザからの質問が続きますので、ユーザの質問を理解してください。
- ユーザからの質問が、ユーザの最も知りたいことなので、最重視してください。
- [ドキュメントの内容]の中から、ユーザの質問に関連する情報を見つけ、その情報をもとに回答を行ってください。
- その際、[制約]に必ず従ってください。
</回答生成の手順>
<制約>
- 質問に関係のない[ドキュメントの内容]は無視し、質問に関係のあるドキュメントにのみ基づいて回答してください。
- 回答は平易な日本語で答えてください。
- 回答はユーザの読みやすさを考慮して必要に応じて箇条書きや構造的な文章にしてください。
</制約>
<ドキュメントの内容>
{context}
</ドキュメントの内容>
<質問>
{question}
</質問>
回答:
上記の文章をlangchain用のテンプレートへ変換するプログラムは以下の通りです.
from langchain_core.prompts import PromptTemplate
def main(prompt_path, output_path):
with open(prompt_path, "r") as f:
prompt = f.read()
prompt_template = PromptTemplate(
templete=prompt,
input_variables=['context', 'question']
)
prompt_templete.save(output_path)
if __name__ == '__main__':
prompt_path = "/database/question_prompt_templete.txt"
output_path = "/database/question_prompt_templete.json"
main(prompt_path, output_path)
make_index.py
今回は日本語の文章を対象にシンプルに"。"でチャンキングするようにします.Faissで
OpenAIの埋め込みモデルを使用してベクトル化後,indexにします.
import os
from langchain_community.document_loaders import DirectoryLoader
from langchain.text_splitter import CharacterTextSplitter
from langchain_community.vectorstores.faiss import FAISS
from langchain_openai import OpenAIEmbeddings
def make_text_document(dir):
loader = DirectoryLoader(dir)
document_list_simple = loader.load_and_split(
text_splitter=CharacterTextSplitter.from_tiktoken_encoder(
chunk_size=256,
chunk_overlap=50,
separator="。",
)
)
return document_list
def main():
text_dir = '/database/source'
output_dir = '/database/index'
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
document_list = make_text_document(text_dir)
embeddings = OpenAIEmbeddings(model='text-embedding-3-small', api_key=OPENAI_API_KEY)
os.makedirs(output_dir, exist_ok=True)
index = FAISS.from_documents(
documents=document_list,
embedding=embeddings,
)
index.save_local(
folder_path=output_dir,
index_name='index'
)
codeディレクトリの中身
main関数が入っています.質問を受け取って,langchainのRetrievalQAに突っ込むだけでRAGが実装できます.
import argparse
import os
from langchain_community.vectorstores.faiss import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_openai import ChatOpenAI
from langchain.prompts import load_prompt
from langchain.chains import RetrievalQA
def get_args():
parser = argparse.ArgumentParser()
parser.add_argument('--index_dir', type=str, default='/database/index')
parser.add_argument('--index_name', type=str, default='index')
parser.add_argument('--retrieve_search_num', type=int, default=6)
parser.add_argument('--prompt_templete_path', type=str, default="/database/question_prompt_templete.json")
parser.add_argument('-q', '--question', type=str)
# OpenAI
parser.add_argument('--openai_model', type=str, default='gpt-4o-mini')
parser.add_argument('--openai_embeddings', type=str, default='text-embedding-3-small')
parser.add_argument('--openai_key', type=str)
return parser.parse_args()
def main():
args = get_args()
if not args.openai_key:
try:
args.openai_key = os.environ['OPENAI_API_KEY']
except KeyError:
raise ValueError("Please provide the OpenAI API key. You can provide it as an argument or set it as .env file.")
embeddings = OpenAIEmbeddings(model=args.openai_embeddings, api_key=args.openai_key)
llm = ChatOpenAI(model=args.openai_model, api_key=args.openai_key)
index = FAISS.load_local(
folder_path=args.index_dir,
index_name=args.index_name,
embeddings=embeddings,
allow_dangerous_deserialization=True
)
question_prompt = load_prompt(os.path.join(args.prompt_templete_path))
chain = RetrievalQA.from_chain_type(
llm=llm,
retriever=index.as_retriever(
search_kwargs={'k': args.retrieve_search_num} # indexから上位いくつの検索結果を取得するか
),
chain_type_kwargs={"prompt": question_prompt}, # プロンプトをセット
chain_type="stuff", # 検索した文章の処理方法
return_source_documents=True # indexの検索結果を確認する場合はTrue
)
if args.question:
question = args.question
response = chain.invoke(question)
for i, source in enumerate(response["source_documents"]):
print(f"\nindex: {i+1}----------------------------------------------------")
print(f"{source.page_content}")
print("---------------------------------------------------------------")
response_result = response["result"]
print(f"\nAnswer: {response_result}")
動かし方
1 Dockerファイルをビルド
docker-compose build
# うまくいかないときは
docker-compose build --no_cache
2 コンテナを起動
docker-compose run -it llm_test bash
3 コンテナ内で以下を実行
python3 main.py -q "質問したい内容"
終わりに
LLM+RAGの環境を雑に作ってみました.ライブラリが整理されてるおかげでめっちゃくちゃ簡単に作れるのでぜひお試しください.