1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

以前LM Studioの使い方とLangChain×ChatGPT×ChromaDBのRAGについて紹介しました ↓

今回の内容に関係するので読んでいただけると嬉しいです!

今回は、これらの記事で学んだ知識を組み合わせて、LM StudioとLangChainを使ったPDF質問応答システムの実装を詳しく解説します。完全にローカル環境で動作するため、プライベートな文書に対しても安心して使用できます。

システムの概要

今回構築するRAGの特徴は以下の通りです。

  • 完全ローカル動作 (インターネット接続不要)
  • プライバシー保護 (文書データが外部に送信されない)
  • 日本語対応 (日本語PDFの処理と回答生成)
  • 簡単セットアップ (必要な依存関係を最小限に)

LM Studioのセットアップ

前回の記事で詳しく説明したように、LM Studioをダウンロードしてモデルを準備します。

必要なPythonパッケージ

pip install langchain langchain-chroma langchain-openai langchain-huggingface langchain-text-splitters sentence-transformers fitz PyMuPDF

コード全体

import fitz
from langchain_chroma import Chroma
from langchain_openai import ChatOpenAI
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.documents import Document
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate

def load_pdf(pdf_path):
    doc = fitz.open(pdf_path)
    documents = []
    for page_num in range(doc.page_count):
        page = doc.load_page(page_num)
        text = page.get_text("text")
        if text.strip():  
            documents.append(Document(
                page_content=text,
                metadata={"page": page_num + 1, "source": pdf_path}
            ))
    doc.close()
    return documents

def create_qa_system(documents, lm_studio_url="http://localhost:1234/v1"):
    # テキストを分割
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
    splits = text_splitter.split_documents(documents)
    
    # 埋め込みモデル(ローカル)
    embeddings = HuggingFaceEmbeddings(
        model_name="sentence-transformers/all-MiniLM-L6-v2",
        model_kwargs={'device': 'cpu'}
    )
    
    # ベクトルストア作成
    vectorstore = Chroma.from_documents(documents=splits, embedding=embeddings)
    retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
    
    # LM Studio LLM
    llm = ChatOpenAI(
        base_url=lm_studio_url,
        api_key="lm-studio", 
        temperature=0.1
    )
    
    # プロンプトテンプレート
    prompt = ChatPromptTemplate.from_messages([
        ("system", "コンテキストに基づいて日本語で回答してください: {context}"),
        ("human", "{input}")
    ])
    
    # チェーン作成
    question_answer_chain = create_stuff_documents_chain(llm, prompt)
    qa_chain = create_retrieval_chain(retriever, question_answer_chain)
    
    return qa_chain

def main():
    pdf_path = "PDF_Path"
    
    documents = load_pdf(pdf_path)
    qa_system = create_qa_system(documents)
    
    print("PDFの質問応答システムが準備できました。質問を入力してください。")
    print("終了するには 'quit' または 'exit' と入力してください。")
    
    while True:
        query = input("\n質問を入力してください: ")
        if query.lower() in ['quit', 'exit']:
            print("プログラムを終了します。")
            break
        
        if not query.strip():
            continue
            
        result = qa_system.invoke({"input": query})
        print("\n回答:", result["answer"])

if __name__ == "__main__":
    main()

PDFファイルの読み込み

def load_pdf(pdf_path):
    doc = fitz.open(pdf_path)
    documents = []
    for page_num in range(doc.page_count):
        page = doc.load_page(page_num)
        text = page.get_text("text")
        if text.strip():
            documents.append(Document(
                page_content=text,
                metadata={"page": page_num + 1, "source": pdf_path}
            ))
    doc.close()
    return documents

PyMuPDF (fitz) を使用してPDFからテキストを抽出します。前回の記事で紹介したように、各ページを個別のDocumentオブジェクトとして管理し、メタデータにページ番号を含めています。

テキストの分割とベクトル化

text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
splits = text_splitter.split_documents(documents)

embeddings = HuggingFaceEmbeddings(
    model_name="sentence-transformers/all-MiniLM-L6-v2",
    model_kwargs={'device': 'cpu'}
)

長文のテキストを500文字ごとに区切りながら、隣り合うチャンク間で100文字の重複を持たせることで文脈を保ちつつ分割し、それぞれのチャンクを軽量かつ高性能な埋め込みモデル(all-MiniLM-L6-v2)を用いて数値ベクトルに変換しています。これにより、検索や質問応答などのタスクで意味的な一致を効率よく扱えるようになります。

LM Studioとの連携

llm = ChatOpenAI(
    base_url=lm_studio_url,
    api_key="lm-studio",
    temperature=0.1
)

ChatOpenAIは、LM Studioのローカルモデルに接続してPDF文書の内容に基づいた質問応答を生成するために使用されています。LM StudioがOpenAI互換のAPIを提供するため、base_urlをローカルサーバー(例:localhost:1234)に変更することで、OpenAIではなくローカルのLLMモデルを呼び出せます。
実際の役割はRAGシステムで検索されたPDF文書のコンテキストを基にユーザーの質問に対する日本語の回答を生成することです。

ChatOpenAIを流用できるのがすごいと個人的に思いました。

プロンプトテンプレート

prompt = ChatPromptTemplate.from_messages([
    ("system", "コンテキストに基づいて日本語で回答してください: {context}"),
    ("human", "{input}")
])

日本語での回答を指示し、検索されたコンテキストを基に回答するよう指示しています。

チェーン作成

question_answer_chain = create_stuff_documents_chain(llm, prompt)
qa_chain = create_retrieval_chain(retriever, question_answer_chain)

return qa_chain

LLMとプロンプトを組み合わせて複数のドキュメントをまとめて処理するQAチェーン(stuff型)を作成します。次に、Retriever(検索エンジン)を用いて関連文書を取得し、それらをこのQAチェーンに渡して回答を生成する構成(retrieval chain)を作成します。

質問受付と回答生成ループ

    while True:
        query = input("\n質問を入力してください: ")
        if query.lower() in ['quit', 'exit']:
            print("プログラムを終了します。")
            break
        
        if not query.strip():
            continue
            
        result = qa_system.invoke({"input": query})
        print("\n回答:", result["answer"])

ユーザーが入力した質問に対して、事前に構築したQAシステムが回答を生成します。

デモ文章

# RAG検証用架空文書コレクション
## 文書 1: ヴェルナ王国の歴史
ヴェルナ王国は紀元前 500 年頃、伝説の王エルディウスによって建国されたとされる。この王国
は現在のヨーロッパ大陸の中央部に位置し、豊富な鉱物資源とクリスタルフラワーと呼ばれる特
殊な植物で知られていた。
12世紀に入ると、第 15代国王アルベルト 3世の統治下で「クリスタル革命」が起こり、魔法技
術と錬金術が飛躍的に発展した。この時期に建設されたソラリス大図書館は、現在でも世界最大
級の魔法書コレクションを誇っている。
1453年のドラゴン戦争では、古代ドラゴンのフレイムウィング族との激しい戦いが 3年間続いた
。最終的に王国軍が勝利し、平和協定「エメラルドの誓い」が締結された。この協定により、ド
ラゴンと人間の共存が実現し、現在に至るまで続いている。
## 文書 2: オービタル・コーポレーションの製品カタログ
オービタル・コーポレーションは 2089 年に設立された宇宙開発企業で、主力製品は量子駆動エン
ジン「ネブラドライブ」である。このエンジンは従来の化学推進システムの 100 倍の効率を持ち
、火星まで 72時間で到達可能である。
同社の宇宙ステーション「オービタル・ハブ 7」は地球軌道上に浮遊し、最大 5000 人の居住者を
収容できる。内部にはハイドロポニック農場、重力制御システム、そして銀河系最大のホログラ
ム・エンターテインメント施設が完備されている。
最新製品の「テレポート・ポッド」は、量子もつれ技術を応用した瞬間移動装置で、地球上の任
意の 2点間を 1秒以内で移動できる。ただし、1日の使用回数は安全上の理由から 3回までに制
限されている。
## 文書 3: 幻想料理研究所の調査報告
幻想料理研究所が発表した最新の調査によると、ミスティック・フルーツと呼ばれる果物が、食
べた人の記憶を一時的に鮮明にする効果があることが判明した。この果物は満月の夜にのみ収穫
でき、フェアリーフォレスト地域でのみ自生している。
また、シャドウ・ペッパーという香辛料は、料理に加えると食べた人を 30分間透明化させる効果
がある。ただし、この効果は食べた人の感情状態に左右され、怒りや悲しみを感じている場合は
効果が現れない。
レインボー・ソルトは 7色に輝く塩で、料理の味を食べた人の好みに自動的に調整する機能を持
つ。この塩は古代の海底遺跡からのみ採取でき、年間生産量は世界で 200グラムに限られている
。
## 文書 4: バイオリズム・シティの都市計画
バイオリズム・シティは 2156年に完成予定の未来都市で、住民の生体リズムに合わせて都市環境
が自動調整される革新的な設計となっている。街の照明は住民の概日リズムに合わせて色温度が
変化し、睡眠の質を向上させる。
交通システムは「フロー・チューブ」と呼ばれる磁気浮上式の透明チューブで構成され、住民は
個人用のポッドに乗って時速 300キロメートルで移動できる。AIシステム「シティ・マインド」
が全ての交通を管理し、渋滞は完全に解消されている。
住宅は「アダプティブ・ハウス」と呼ばれる自己変形建築で、住民のライフスタイルの変化に応
じて間取りが自動的に変更される。また、建物の外壁は光合成機能を持つ特殊な素材で作られて
おり、大気中の二酸化炭素を酸素に変換する。
## 文書 5: 時間研究所の実験記録
時間研究所では、クロノ・クリスタルという特殊な鉱物を使用した時間操作実験が行われている
。実験番号 TC-4471 では、被験者が 10分間の時間を体験するのに実際には 3時間を要する「時
間拡張フィールド」の生成に成功した。
逆時間流動実験では、テンポラル・エンジンを使用して限定的な時間の逆行を実現している。現
在の技術では最大 24時間前まで遡ることができるが、対象物の質量が 1キログラム以下に制限さ
れる。
パラレル・タイムライン観測装置「クロノスコープ」により、別の時間軸での出来事を観測する
ことが可能になった。これまでに47の異なる時間軸が確認されており、その中には恐竜が絶滅し
なかった世界や、重力が現在の半分の世界も含まれている。

以前記載したものと同じPDFです。

任意のPDFでも問題ありません。

生成結果

スクリーンショット 2025-07-13 223048.png
上手くいくとこのようにヴェルナ王国について回答が生成されます。

最後に

今回は以前利用したLM StudioとLangChainを組み合わせて、完全ローカルで動作するPDF質問応答システムを作ってみました。インターネット接続なしで動くので個人メモや社内文書など機密性の高いPDFにも安心して使えます!

LangChainやLM Studioのセットアップもそこまで難しくなく柔軟にカスタマイズできるので、応用次第でいろいろな使い方ができそうです。たとえば、別の埋め込みモデルに切り替えたり、検索アルゴリズムを工夫したり、Web UIを載せたりと、アイデア次第でさらに便利になります!!!

完全にローカルのLLMを使ったRAG構成に興味がある方の参考になれば嬉しいです。
ご質問やフィードバックがあれば、ぜひコメントで教えてください!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?