🙆‍♂️

RAGとの出会い - 「知識を検索して答える」AIの魅力と挑戦

に公開

AIエンジニアを目指しているME_DE_AEです。

前回の記事では、LLMを活用したシンプルなチャットボットの開発について書きました。システムが初めて動いた時の感動は今でも鮮明に覚えています。そして次なる挑戦として、上司から「経歴書に関する質問に答えられるものを作りたい」という新たなミッションをいただきました。これが、私とRAGとの出会いでした。

単純なチャットボットから一歩進んで、「特定の情報を基に正確に答える」 システムへ。この挑戦を通じて、AIの可能性と奥深さを改めて実感することになりました。

RAGとは?

RAG(Retrieval-Augmented Generation)とは、簡単に言うと「必要な情報を検索してから、その情報を基に回答を生成する」技術です。

従来のチャットボットは、LLMが持つ一般的な知識のみで回答していました。しかし、RAGでは以下のような流れで動作します。

  1. ユーザーの質問を受け取る
  2. 関連する情報を知識ベースから検索する
  3. 検索した情報をコンテキストとしてLLMに渡す
  4. LLMがそのコンテキストを基に回答を生成する

これにより、特定の文書や資料に基づいた、より正確で信頼性の高い回答が可能になります。

技術選定

今回のRAG実装では、以下の技術スタックを選択しました。

技術 目的・役割
LangChain RAGパイプラインの構築
Azure OpenAI Service LLMとEmbeddingモデル
Chroma ベクトルストア(データベース)
FastAPI バックエンドAPI
Docker 環境の統一化

LangChainの活用

特に今回の開発で重要な役割を果たしたのがLangChainです。

LangChainとは、LLMを活用したアプリケーションを簡単に構築できるPythonライブラリです。RAGのような複雑な処理を、まるでブロックを組み立てるように、各機能を組み合わせて実装することができます。

LangChainの主な特徴

  • 豊富なコンポーネント
    • 文書読み込み、テキスト分割、ベクトル化、検索など、RAGに必要な機能が全て揃っている
  • チェーン機能
    • 複数の処理を連鎖させて、複雑なワークフローを簡潔に記述できる
  • 多様な連携
    • Azure OpenAI、OpenAI、Google、Anthropicなど、様々なLLMプロバイダーに対応

今回のRAGシステムでは、以下のような「チェーン」を構築しました。

retriever = vectorstore.as_retriever()

rag_chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

この数行のコードで、 検索→プロンプト構築→LLM実行→結果解析 という一連の流れが実現できます。もしLangChainを使わずに実装していたら、各ステップの連携処理だけで数十行のコードが必要になります。

なぜこれらの技術を選んだかというと、前回のチャットボット開発で使い慣れたAzure OpenAI Serviceとの親和性が高く、特にLangChainがRAG実装のための豊富な機能を提供してくれるからでした。

RAGシステムの技術構成と処理フロー

実装したRAGシステムは、以下のような流れで動作します。

シーケンス図で表すと、以下のようになります。図を見ると分かるように、LangChainが各コンポーネント間の複雑な連携を自動的に処理してくれています。

ドキュメントの前処理

まず、PDFの経歴書から情報を抽出し、チャンクに分割します。チャンクが大きすぎると検索精度が下がり、小さすぎると文脈が失われてしまうので注意が必要です。

# ドキュメントの読み込みと分割
loader = PyPDFDirectoryLoader("src/documents/")
docs = loader.load()

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000, chunk_overlap=200
)
splits = text_splitter.split_documents(docs)

ベクトル化と保存

分割されたテキストを、Azure OpenAIの埋め込みモデルでベクトル化し、Chromaデータベースに保存します。

embeddings = AzureOpenAIEmbeddings(
    model=llm_embed_model_name,
    openai_api_version=llm_embed_api_version,
    azure_endpoint=llm_embed_endpoint,
    openai_api_key=llm_embed_api_key,
)

vectorstore = Chroma.from_documents(documents=splits, embedding=embeddings)
retriever = vectorstore.as_retriever()

検索と生成

ユーザーからの質問に対して、関連する情報を検索し、それを基に回答を生成します。

rag_chain = (
        {"context": retriever, "question": RunnablePassthrough()}
        | prompt
        | llm
        | StrOutputParser()
    )

# ユーザーからの質問 
user_question = "この人の職歴を教えてください" 

# チェーンを実行して回答を取得
# チェーンに質問を渡すと、検索→プロンプト構築→LLM実行→結果解析の一連の流れが自動で処理される
response = rag_chain.invoke(user_question) 

print(response)

プロンプトとの格闘

RAG実装で最も苦労したのは、「LLMをどう制御するか」 という問題でした。データ分析の世界では、ハイパーパラメータを調整すれば結果が変わりますが、LLMの世界では 「プロンプト」 という言葉の選び方一つで結果が大きく左右されます。

初期の問題

最初の実装では、経歴書にない情報でLLMが勝手に回答してしまう現象が頻発しました。例えば

  • 質問:「この人はPythonができますか?」
  • 経歴書:「Java開発経験3年」
  • 悪い回答:「はい、Pythonも一般的な言語なので使えると思います」

これでは意味がありません!

解決への道のり

この問題を解決するため、以下の2つのアプローチを試しました。

temperature設定の調整

まず試したのが、LLMの応答の「創造性」を制御する temperature というパラメータの調整です。この値が低いほど、AIは決まった事実に基づいた回答をしやすくなり、逆に高いほど、より多様でクリエイティブな回答を生成します。
今回の目的は、経歴書にない情報をAIが創作してしまうのを防ぐことでした。そのため、 temperature0 に設定し回答のランダム性をなくし、提供された情報に最も忠実な応答をするように促しました。

llm = AzureChatOpenAI(
    deployment_name=llm_model_name,
    temperature=0,  # ランダム性を最小に
)

プロンプトテンプレートの改良

最終的に、以下のようなプロンプトテンプレートに落ち着きました:

    あなたは優秀で博識なアシスタントです。以下の手順で質問に答えてください。
    1. まず、提供されたコンテキスト情報を確認し、質問に答えられる情報があるか探してください。
    2. コンテキスト情報に答えがある場合は、その情報を最優先で使って正確に回答してください。
    3. コンテキスト情報に答えがない場合は、「わかりません」と正直に回答してください。

    コンテキスト: {context}
    質問: {question}

「言葉の選び方」でAIが変わる

この開発を通じて最も驚いたのは、プロンプトの微妙な違いがAIの振る舞いを劇的に変える ということでした。

データ分析の世界では

  • 学習率: 0.01 → 0.001
  • エポック数: 100 → 200
  • バッチサイズ: 32 → 64

このように数値で明確に調整できます。
しかし、LLMでは

  • 「答えてください」→ 「正確に答えてください」
  • 「情報を使って」→ 「情報を最優先で使って」
  • 「回答する」→ 「手順で回答する」

このような言葉の選び方一つで、全く違う結果になるのです。まさに 「言葉でプログラムする」 という新しい体験でした。

完成の瞬間

数々の試行錯誤を経て、ついにRAGシステムが期待通りに動作した瞬間が訪れました。

「この人の職歴を教えてください」という質問に対して、経歴書の内容を正確に要約して回答してくれた時の感動は、前回のシンプルなチャットボットとは全く違うものでした。

「AIが文書を理解して、的確に情報を取り出している!」

この瞬間、RAGという技術の可能性を肌で感じることができました。単なる会話ではなく、知識を活用した知的な対話 が実現できたのです。

学んだこと

今回のRAG実装を通じてAIエンジニアリングの奥深さについて学んだことは数多くありますが、特に印象深いのは以下の3点です。

  • プロンプトエンジニアリングは芸術であり科学

    • 数値的なハイパーパラメータ調整とは全く異なる、言語による制御の難しさと面白さを実感しました。
  • 検索の品質がすべてを決める

    • どんなに優秀なLLMでも、関連性の低い情報を与えられても意味のない回答しかできません。情報検索の精度向上が重要。
  • システム全体の設計思想が重要

    • 単一の技術ではなく、データ処理、検索、生成、UI/UXまでを含めた総合的な設計が求められます。

まとめ

RAGの実装を通じて、AIエンジニアとしての視野が大きく広がりました。前回のシンプルなチャットボットから、今度は「知識を活用するAI」へ。技術的な挑戦はより複雑になりましたが、その分得られる学びと達成感も大きくなりました。
特に、「プロンプトという言葉でAIを制御する」 という新しい感覚に触れることができたのは、大きな収穫でした。

異業種からAIエンジニアへの道のりは決して平坦ではありませんが、一歩ずつ確実に前進していることを実感しています。「AIという武器を手に、未経験分野でのモノづくり」 という目標に向かって、引き続き学習を続けていきます。

株式会社メンバーズ AIフォーオールカンパニー

Discussion