はじめに
LLMと検索をうまく組み合わせて,AIによる回答精度を高める,RAG(Retrieval-Augmented Generation)というフレームワークがあります.最近では,Advanced RAGやGraph RAGといった,更に進展したRAGが見られるようになり,これからさらに回答精度を高くするための研究が進んでいくと考えられます.今回は基本的な初期のRAGを今さらながら構築します.
お題
今回は,私の最も好きな漫画である「進撃の巨人」についてLLMに回答させます.その際,元となるドキュメントを参照するとき(RAGあり)としないとき(RAGなし)で回答にどのような差が生まれるのか確認します.
元となるドキュメントは,進撃の巨人のWikipediaから物語に関連する部分を自分で抜粋したものになります.
構成
今回は以下のような構成で構築します.LangchainとPGVectorを用いて構築します.
Embedding Modelはtextembedding-gecko-multilingual@001
,Chat Modelはgemini-1.5-flash-001
を利用しました.
RAG構築
DBの準備
Dockerを用いてPGVectorを構築します.docker-compose.yml
に以下を記載し,コマンドを実行するのみです.
docker-compose up -d
services:
pgvector:
image: pgvector/pgvector:pg16
platform: linux/amd64
container_name: pgvector-container
environment:
POSTGRES_USER: langchain
POSTGRES_PASSWORD: langchain
POSTGRES_DB: langchain
ports:
- "6024:5432"
volumes:
- pgvector_data:/var/lib/postgresql/data
volumes:
pgvector_data:
Indexing
まずはデータベースを構築します.手順は以下です.
1. チャンクの作成: 元のドキュメントをある程度の単位で文章を区切る.
2. Embedding: 1で区切ったチャンクごとにベクトル化する
3. ベクトルDBに格納: ベクトル化したものをデータベースに格納する
embeddings = VertexAIEmbeddings(
model_name="textembedding-gecko-multilingual@001",
)
connection = "postgresql+psycopg://langchain:langchain@localhost:6024/langchain"
collection_name = "my_docs"
vectorstore = PGVector(
embeddings=embeddings,
collection_name=collection_name,
connection=connection,
use_jsonb=True,
)
with DOC_PATH.open() as f:
state_of_the_union = f.read()
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=200,
separators=[
"\n\n",
"\n",
" ",
".",
",",
"\u200b", # Zero-width space
"\uff0c", # Fullwidth comma
"\u3001", # Ideographic comma
"\uff0e", # Fullwidth full stop
"\u3002", # Ideographic full stop
"",
],
)
texts = text_splitter.create_documents([state_of_the_union])
vectorstore.add_documents(texts, ids=[doc.id for doc in texts])
PGVectorには以下のように,Embeddingしたベクトルと元のドキュメントが紐づく形で格納されます.
Retrieval and Generation
Prompt
ユーザーからの質問に対して,データベースに格納したドキュメントを参照するように指示を与える必要があります.プロンプトにその旨を記載します.以下がプロンプトで,LangSmith Hubにあるrag-prompt
を日本語にしたものです.
prompt = """
あなたは質問応答のアシスタントです。質問に答えるために、検索された文脈の以下の部分を使用してください。答えがわからない場合は、わからないと答えましょう。回答は2文以内で簡潔に。
質問: {question}
コンテキスト: {context}
答え:
"""
Chainの作成
上記のプロンプトと,LLMをつなぎ,ユーザーからの質問に対して回答させるためにチェーンを繋ぎます.
retriever = vectorstore.as_retriever()
llm = ChatVertexAI(model="gemini-1.5-flash-001")
prompt_template = PromptTemplate.from_template(prompt)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt_template
| llm
| StrOutputParser()
)
質問する
RAGの構築ができたので,実際にこのシステムを利用します.今回は,特定の文章をもとに回答する場合とそうでない場合で,回答がどのように異なるのか確認します.
質問: 主人公は誰ですか?
rag_chain.invoke("主人公は誰ですか?")
RAGありの回答
主人公はエレン・イェーガーです。彼は第104期訓練兵団の5席で、母親を巨人に殺された過去から巨人の駆逐を目標としています。
当たり前ですが,なんの脈略もなく「主人公は誰ですか?」と聞かれても,LLMは答えようがありません.一方RAGを構築した場合は,文書を元に回答するため,質問に対し,進撃の巨人の主人公を正しく回答できています.
質問: 第13代調査兵団団長は誰ですか?
RAGありの回答
第13代調査兵団団長はエルヴィン・スミスです。彼は卓越した統率力と決断力の持ち主で、くせ者揃いの調査兵団を取りまとめていました。
正しい回答はエルヴィン・スミスです.RAGなしの場合は嘘が多分に含まれています.物語を知らない人がこのようにやみくもにLLMに尋ねても嘘を回答され,それを信じてしまう可能性があり,LLMの利用の際には注意が必要です.RAGありの場合,元のWikipediaに書かれている内容をもとに回答を生成していることがわかります.
まとめ
このようにRAGを用いて,特定の文章に特化してAIに回答させることで,極力うそ(ハルシネーション)を回避することができます.
冒頭に少し触れた,Advanced RAGやGraph RAGを用いることで,更に回答精度を高めることができ,よりハルシネーションを回避できます.例えば,指示代名詞(彼,それ,あの,など)が文書に含まれていた場合,それ単独の文章では,何を指しているのかわかりません.その前の文や全体を理解しておく必要があり,その点がRAGの不得手な部分になっています.Graph RAGを用いると,単語同士の関係性をつなぎ,それを持って回答を生成するため,その点を改善してくれるようです.こちらについても自分の中で整理できたらまとめたいと思います.
なぐり書きですが,今回利用した環境やコードをこちらにまとめております.