PDFファイルを使ったRAGに挑戦(3)LangChain
この記事では、LangChainライブラリを使って、前回変換したMarkdownファイルをデータベース化する方法を紹介します。これにより、前回のようにMarkdownコンテンツを直接LLM(大規模言語モデル)に送る方法に比べ、圧倒的に処理速度を向上させることができました。
LangChainを用いたRAGの基本実装例
以下に、LangChainを使用した実装コードを示します。このコードでは、Markdownファイルをデータベース化し、質問に対する回答を生成する仕組みを構築しています。
from langchain.chains import RetrievalQA
from langchain.chains.question_answering import load_qa_chain
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.text_splitter import CharacterTextSplitter
from langchain.text_splitter import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter
from langchain.document_loaders import TextLoader
from langchain.document_loaders import UnstructuredMarkdownLoader
from langchain.document_loaders import PDFPlumberLoader
from langchain.document_loaders import UnstructuredPDFLoader
from langchain.chat_models import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from langchain.prompts import HumanMessagePromptTemplate, SystemMessagePromptTemplate, ChatPromptTemplate
import os
class QASystem:
def __init__(self, persist_directory="/content/drive/MyDrive/work/chroma_db"):
# Anthropic APIキーの設定
os.environ["ANTHROPIC_API_KEY"] = claude_api_key
os.environ["OPENAI_API_KEY"] = openai_api_key
self.embeddings = OpenAIEmbeddings(
model="text-embedding-3-large",
openai_api_key=openai_api_key,
chunk_size=1000,
max_retries=3
)
self.llm = ChatAnthropic(
model="claude-3-5-sonnet-20241022",
temperature=0.0,
anthropic_api_key=claude_api_key,
max_tokens = 1000
)
self.persist_directory = persist_directory
# 既存のDBがあれば読み込み、なければ新規作成
if os.path.exists(persist_directory):
self.vectorstore = Chroma(
persist_directory=persist_directory,
embedding_function=self.embeddings
)
else:
self.vectorstore = None
def add_documents(self, file_path):
loader = UnstructuredMarkdownLoader(file_path)
documents = loader.load()
headers_to_split_on = [
("#", "header1"),
("##", "header2"),
("###", "header3"),
]
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
filename = os.path.basename(file_path)
# メタデータを保持しながらドキュメントを分割
md_splits = markdown_splitter.split_text(documents[0].page_content)
for split in md_splits:
split.metadata.update({
'filename': filename,
'header1': split.metadata.get('header1', ''),
'header2': split.metadata.get('header2', ''),
'header3': split.metadata.get('header3', '')
})
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=100,
separators=["\n\n", "\n", "。", "、", " ", ""],
keep_separator=True
)
texts = text_splitter.split_documents(md_splits)
if self.vectorstore is None:
self.vectorstore = Chroma.from_documents(
documents=texts,
embedding=self.embeddings,
persist_directory=self.persist_directory
)
else:
self.vectorstore.add_documents(texts)
def ask_question(self, filename, question: str) -> str:
if self.vectorstore is None:
return "ドキュメントが登録されていません。"
# システムメッセージとヒューマンメッセージを作成
system_template = """あなたは優秀なアナリストです。与えられた企業レポートを読みユーザからの質問に回答します。
企業レポートはmarkdown形式で記述されています。セクションや表の構造を意識しながら注意深く分析してください。
<<システムプロンプト 手順と制約事項>>
企業レポート:
{context}"""
human_template = "{question}"
system_message_prompt = SystemMessagePromptTemplate.from_template(system_template)
human_message_prompt = HumanMessagePromptTemplate.from_template(human_template)
# チャットプロンプトテンプレートの作成
chat_prompt = ChatPromptTemplate.from_messages([
system_message_prompt,
human_message_prompt
])
retriever = self.vectorstore.as_retriever(
search_type="mmr", # Maximum Marginal Relevance
search_kwargs={
"k": 5,
"filter": {"filename": filename},
"fetch_k": 30, # より多くの候補から選択
"lambda_mult": 0.7 # 関連性と多様性のバランス
}
)
qa_chain = RetrievalQA.from_chain_type(
llm=self.llm,
chain_type="stuff",
retriever=retriever,
return_source_documents=True,
chain_type_kwargs={"prompt": chat_prompt}
)
result = qa_chain.invoke({"query": question})
answer = result['result']
sources = [doc.page_content[:100] + "..." for doc in result['source_documents']]
return f"回答: {answer}\n"
#return f"回答: {answer}\n\参照元:\n" + "\n".join(sources)
コード説明
__init__()
- QASystemの初期化を行なっています。
- 使用する埋め込みモデルやLLMモデルを設定します。
- 既存のdbがある場合は読み込み、なければ新規作成します。
add_documents(file_path)
- 指定されたMarkdownファイルを分割・埋め込みし、データベースに追加します。
- Markdownの構造(ヘッダー情報など)を保持するようにファイル分割します。
- メタ情報として、ファイル名を追加します。
ask_question(filename, question)
- 指定したファイル名と質問を元に、関連情報を検索し、回答を生成します。
- MMR(Maximum Marginal Relevance)を使用して、関連性と多様性を両立した検索を実現します。
使用方法
最初に、add_documents()で、全てのMarkdownファイルをデータベース化します。
qa_system = QASystem()
# ドキュメントの追加
qa_system.add_documents("/content/drive/MyDrive/work/doc/1.pdf")
回答を得るには、ファイル名と質問とをask_question()に渡します。
answer = qa_system.ask_question(pdf_name,question)
今回は、ここでのanswerは中間情報として関連するすべての情報を出力するように指示しました。
このあと、質問とanswerとを入力として、LLM(gpt-4o-mini)に最終的な回答を生成させています。
LangChain(PDF)
PDFファイルを使った場合、add_documents()は以下のようになります。
def add_documents(self, file_path):
loader = PDFPlumberLoader(file_path)
documents = loader.load()
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=100,
separators=["\n\n", "\n", "。", "、", " ", ""],
keep_separator=True
)
filename = os.path.basename(file_path)
# PDFの各ページにメタデータを追加
for doc in documents:
doc.metadata.update({
'filename': filename,
'page_number': doc.metadata.get('page_number', ''),
})
texts = text_splitter.split_documents(documents)
if self.vectorstore is None:
self.vectorstore = Chroma.from_documents(
documents=texts,
embedding=self.embeddings,
persist_directory=self.persist_directory
)
else:
self.vectorstore.add_documents(texts)
PDFのLoaderにはいくつか種類がありましたが、今回は、表の情報が重要かと思いましたので、PDFPlumberLoaderを使っています。
参考までに、各Loaderの特徴を記載しておきます。
各Loaderの比較表
Loader | 使用ライブラリ | 特徴 | 適用場面 |
---|---|---|---|
PyPDFLoader | PyPDF2 | シンプルで軽量、精度は中程度 | 簡単なテキスト中心のPDF |
PDFMinerLoader | PDFMiner | レイアウト情報をある程度保持 | レイアウトが重要なPDF |
PyMuPDFLoader | PyMuPDF (fitz) | 高速で複雑なPDFにも対応可能 | 高速処理やマルチメディアPDF |
UnstructuredPDFLoader | unstructured | 構造化データの解析が可能 | 複雑なPDFや高度な内容解析が必要な場合 |
PDFPlumberLoader | PDFPlumber | 表や画像も正確に処理可能 | 表や複雑なレイアウトが重要な場合 |
OnlinePDFLoader | PyPDF2など | URLから直接処理可能 | WebからPDFを取得して処理する場合 |