【入門】LLM + RAGを使って自社製品の質問に答えるAIを作ってみる

# はじめに
大規模言語モデル（LLM）は広範な知識を持っていますが、あなたの会社の製品マニュアル、仕様書、日々のサポート記録といった、社内に散在するドキュメントに書いてある情報までは知らないことが多いと思います。LLMに、これらのバラバラな社内ドキュメントの情報を後付けで教えられたらどうでしょうか？

このようなことを可能にするのが**RAG (Retrieval-Augmented Generation**) と呼ばれる技術です。この記事では架空の自社製品を例に、複数の製品ドキュメントを学習させ、製品に関する質問に答えるAIを構築していきます。

# RAGとは？
LLMは非常に強力ですが、万能ではありません。特に、企業独自のデータ活用においては以下のような課題があります。

- 学習データに含まれていない情報（例：昨日リリースされた新機能、社内限定のトラブルシューティング情報）を知らない
- 知らない情報について質問されると、もっともらしい嘘の情報を生成してしまう(ハルシネーションとも呼ばれます)
- LLMの知識は学習された特定の時点のもので、情報のアップデートには膨大なコストと時間がかかる再学習が必要

これらの課題を解決するのが、RAG（Retrieval-Augmented Generation / 検索拡張生成） です。RAGを一言で説明するなら、「**LLMが回答を生成する前に、関連する情報を外部の知識ベースから検索（Retrieval）し、その内容を参考にして回答を生成（Generation）する仕組み**」です。

RAGのメリットは、構造化されていないドキュメント（テキスト、csvなど）をそのまま知識ソースにできることです。LLMは、RAGによって常に最新かつ正確な情報源を参照できるようになり、自社製品に関する専門的な質問にも、根拠に基づいて的確に答えられるようになります。

# 作成するシステムの概要
今回は架空のスマート照明システム「LuminAI」を例に、LLMに知識を教え、質問に答えらえるようにしていきます。大まかな流れはこのようになります。

**Step1. データソースの準備**
→『LuminAI』の製品マニュアル、仕様書、トラブルシューティング集など、エンジンに学習させたい複数のドキュメントを準備します。

**Step2. RAGの実装**
→ LangChain*のAPIを使って実際にLLMを構築していきます。

**Step3. テスト**
→ RAGが想定通りに機能しているかどうか、いくつか質問をして試します。

*LangChainについてはここでは詳しく説明しませんが、簡単に言うと「**LLMを使ったアプリケーション開発を簡単にするためのフレームワーク**」です。「ドキュメントを読み込む → 分割する → データベースに格納する → 質問が来たら検索する → LLMに渡す」といった一連の流れを「チェーン」として定義することで、これらの処理を自動化し、数行のコードで呼び出せる便利なツールです。

# 開発環境
- Windows 11
- conda 24.11.3
- python 3.11
- langchain 0.3.20
- jupyter lab 4.3.5

# Step1. データソースの準備
前述したように、今回は架空のスマート照明システム「LuminAI」を扱います。なお、この製品に関するドキュメントは生成AIを使って作成しました。(かなりそれらしいファイルを生成してくれていますね。)
LuminAIにはStandardモデルとProモデルが存在し、ナレッジが以下のような複数のファイルで管理されていると仮定します。

**1. product_lineup.md (製品ラインナップ - Markdown形式)**
```markdown:Markdown
# LuminAI 製品ラインナップ

## LuminAI Standard
エントリーモデル。基本的なスマート照明機能をすべて搭載。

## LuminAI Pro
プロフェッショナル向けモデル。より高い色再現性と、スマートホーム共通規格「Matter」に対応。

### スペック比較表
| 機能 | Standard | Pro |
| :--- | :---: | :---: |
| モデル番号 | LMAI-S02 | LMAI-P02 |
| 最大輝度 | 800 lm | 1100 lm |
| 演色評価数(CRI) | Ra 80 | Ra 95 |
| Matter対応 | - | ✅ |
```

**2. manual_pro_v2.txt (Proモデル v2 マニュアル抜粋)**
```text:Text
■ Matterでの接続方法 (LuminAI Proのみ)
1. スマートフォンにMatter対応のハブ（Google Home, Apple HomePodなど）がセットアップされていることを確認します。
2. LuminAIアプリの「設定」>「外部サービス連携」から「Matter」を選択します。
3. 画面に表示されるQRコードを、ハブのアプリで読み込んでください。
```

**3. troubleshooting_db.csv (トラブルシューティングDB - CSV形式)**
```text:csv
ErrorCode,Symptom,Cause,Solution,ApplicableModel,RequiredFirmware
E02,色が正しく表示されない,初期化の失敗,アプリから工場出荷時リセットを実行,Pro,v2.0.0+
N11,Matterデバイスとして認識されない,ルーターのIPv6設定が無効,"ご家庭のWi-Fiルーターにて、IPv6パススルーまたは同様の機能を有効にしてください。",Pro,v2.1.0+
C03,Wi-Fi接続が頻繁に切れる,2.4GHz帯の電波干渉,"ルーターのWi-Fiチャンネルを1, 6, 11のいずれかに固定してみてください。",All,v1.0.0+
```

このようにシンプルではありますが、マークダウン、テキスト、csvという３つの異なるファイルを用意して、LLMに知識を与えていきます。これらの多様な形式のドキュメントを扱うため、LangChainはUnstructuredMarkdownLoaderやCSVLoaderといったドキュメントローダーを提供しています。これらを使うことで、異なる形式のファイルを統一的に読み込み、処理できます。

# Step2. RAGの実装
それでは、LangChainを使ってRAGを実装していきます。LLMはOpenAIのモデルを使用します。(OpenAIのAPIキーは取得できている前提で話を進めます。)

## 必要なライブラリの読み込み、APIキーの設定
今回作成するスクリプトに必要なモジュールはこのようになります。OpenAIのモデルを使用するため、APIキーをdotenvモジュールを使用して環境変数から読み込みます。

```python:python
import os
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
from dotenv import load_dotenv
# 様々なドキュメントローダーをインポート
from langchain_community.document_loaders import (
    DirectoryLoader,
    PyPDFLoader,
    CSVLoader,
    UnstructuredMarkdownLoader,
    TextLoader,
)

# environment
load_dotenv(override=True)
os.environ['OPENAI_API_KEY'] = os.getenv('OPENAI_API_KEY')
```

## ドキュメントの読み込み
Step1で作成した架空のマニュアルや仕様書などのファイル群（.md, .txt, .csv）を読み込んでいます。DirectoryLoaderをファイル形式ごとに使用し、多様なドキュメントを読み込みます。

```python:python
# --- 1. ドキュメントの読み込み (Document Loading) ---
# データが格納されているディレクトリを指定
DATA_PATH = 'データが格納されているディレクトリ'

# 各ファイル形式ごとに、適切なローダーを指定して読み込みを行う
# 1. Markdown (.md)
loader_md = DirectoryLoader(DATA_PATH, glob="**/*.md", loader_cls=UnstructuredMarkdownLoader, show_progress=True)
# 2. テキスト (.txt)
loader_txt = DirectoryLoader(DATA_PATH, glob="**/*.txt", loader_cls=TextLoader, loader_kwargs={'encoding': 'utf-8'}, show_progress=True)
# 3. CSV (.csv)
loader_csv = DirectoryLoader(
    DATA_PATH,
    glob="**/*.csv",
    loader_cls=CSVLoader,
    loader_kwargs={'encoding': 'utf-8-sig'}, 
    show_progress=True
)

# すべてのドキュメントを一つのリストに結合する
documents = []
documents.extend(loader_md.load())
documents.extend(loader_txt.load())
documents.extend(loader_csv.load())

print(f"ドキュメントを読み込みました。")
```

## テキストの分割 (Chunking)
読み込んだドキュメントが長文の場合、それを扱いやすいサイズの小さな塊（チャンク）に分割しています。LLMが一度に処理できるテキスト量には上限があるため、また検索精度を高めるためにも重要な処理です。

```python:python
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
)
texts = text_splitter.split_documents(documents)
print(f"ドキュメントを {len(texts)} 個のチャンクに分割しました。")
```

出力結果はこのようになります。今回はそこまで分量が多くないので、5個のチャンクとなりました。

```:出力結果
ドキュメントを 5 個のチャンクに分割しました。
```

## 埋め込みとベクトルストアへの格納
分割した各チャンクを**ベクトルに変換（埋め込み）し**、それらをデータベースに格納します。**これにより、コンピュータは単語の文字列ではなく意味の近さでテキスト同士の関連性を計算できるようになります**。埋め込みにはOpenAIのモデルを使用します。text-embedding-3-smallモデルの場合、一つの単語や文章は、1536個の数値の羅列（= 1536次元のベクトル）として表現されます。

このように与えられた情報をベクトルとして表現できたら、与えられた質問に対して近いベクトルを探す必要があります。これを行うのが**ベクトルストア(Vector Store**)と呼ばれるものです。ベクトルストアは、大量のベクトルデータの中から、特定のベクトルに似たものを超高速で探し出すことに特化した高性能な検索エンジンです。今回使用している「FAISS」は、Facebook AIが開発したベクトルストアです。


```python:python
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = FAISS.from_documents(texts, embeddings)
print("ベクトルストアを構築しました。")
```

## 検索機 (Retriever) の作成
構築したベクトルストアから、ユーザーの質問に最も関連性の高いチャンクを検索して取り出すための検索機（Retriever）を作成していきます。RAGの「R（= Retrieval / 検索）」を担う部分です。ベクトルストアのインスタンスに対してas_retriever()関数を使って実装します。
パラメータ k は、検索結果として取得するドキュメントチャンクの数を指定します。この k の値をどう設定するかで、AIの回答の傾向が大きく変わります。kを大きくするとLLMが参照できる情報が増えるため、より網羅的で背景を詳しく説明するような回答を生成しやすくなります。逆に小さくすると、情報がより絞り込まれるため、ノイズが少なく質問の核心に迫る的確な回答を生成しやすくなります。

```python:python
# 作成したベクトルストアから、関連情報を検索するためのRetrieverを作成
retriever = vectorstore.as_retriever(search_kwargs={'k': 5})
print("検索機（Retriever）を作成しました。")
```

## プロンプトの定義 (Prompt Engineering)
AIに与えるプロンプトを定義します。

```python:python
prompt_template = """
あなたは、スマート照明「LuminAI」に関する質問に答える、プロフェッショナルなAIアシスタントです。
提供された「参考情報」にもとづいて、ユーザーの質問に正確かつ分かりやすく回答してください。
参考情報に答えがない場合は、無理に答えを生成せず、「申し訳ありませんが、その質問にお答えできる情報が見つかりませんでした。」と回答してください。

# 参考情報:
{context}

# ユーザーの質問:
{question}

# 回答:
"""
PROMPT = PromptTemplate(
    template=prompt_template, input_variables=["context", "question"]
)

```

このあたりは、いわゆるプロンプトエンジニアリングの領域になりますが、このテンプレートでは以下のような工夫をしています。

### AIにペルソナを与える

"あなたは、スマート照明「LuminAI」に関する質問に答える、プロフェッショナルなAIアシスタントです。"

この一文で、AIの役割（ペルソナ）と専門領域を定義しています。これにより、AIは漠然とした知識から回答を探すのではなく、「自分はLuminAIの専門家だ」という前提で思考を始めます。結果として、回答のトーンはプロフェッショナルなものになり、LuminAIに関するトピックに意識が集中します。

### 回答の根拠を「参考情報」に限定する
"提供された「参考情報」にもとづいて、ユーザーの質問に正確かつ分かりやすく回答してください。"

この一文は、LLMに対して「あなたの広大な記憶（事前学習データ）から答えを探すのではなく、今ここで与えられた参考情報だけを根拠として回答を生成しなさい」と命じています。これが、LLMが学習データにない自社製品の知識について語ることを可能にし、同時にハルシネーション（情報の捏造）を抑制するための鍵となります。

### 正直に「知らない」と言わせる

"参考情報に答えがない場合は、無理に答えを生成せず、「申し訳ありませんが、その質問にお答えできる情報が見つかりませんでした。」と回答してください。"

この指示は、AIが知ったかぶりをするのを防ぐためのものです。前のステップでRetrieverがどんなに頑張っても、関連する情報がナレッジベースに存在しない場合があります。その際に、AIがもっともらしい嘘の回答を作り出してしまうのを防ぎ、「正直に知らないと答える」という誠実な振る舞いをさせます。これにより、ユーザーはシステムの回答を信頼できるようになります。

### 変数のプレースホルダー

"{context}"、"{question}"

これらは、LangChainが後から動的に情報を埋め込むための「場所取り」です。RetrievalQAチェーンが実行されると、{context} には、Retrieverが検索してきたドキュメントチャンクのテキストが自動的に挿入されます。そして{question} には、ユーザーが入力した生の質問文が挿入されます。この仕組みにより、毎回異なる質問と、それに応じて検索された異なる参考情報を使って柔軟に最終的なプロンプトを組み立てることができます。


## RAGチェーンの構築
これまで準備した部品（LLM, Retriever, Prompt）を一つのパイプライン（チェーン）に統合しています。これにより、一連のプロセスを簡単に呼び出せるようになります。LLMとしてOpenAIのGPT-4o-miniモデルを指定しています。また、**今回はRetrievalQAと呼ばれるQ&Aタスクのために最適化されたチェーンを使用します**。

```python:python
# LLMとしてOpenAIのGPT-4o-miniモデルを指定
llm = ChatOpenAI(temperature=0, model_name="gpt-4o-mini-2024-07-18")

# RetrievalQAチェーンを作成
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff", # 検索した情報をすべてプロンプトに詰め込む方式
    retriever=retriever,
    chain_type_kwargs={"prompt": PROMPT},
    return_source_documents=True, # 回答の根拠となったソースドキュメントも返す
)
print("RAGチェーンの準備が完了しました。")
```

以上でRAGの実装は完了しました。最後にテストを行って、期待したような回答ができるか確かめてみましょう。

# Step3. テスト
以下のようなテスト用スクリプトを用いて、テストを行います。

```python:python
question = "質問文"

print(f"\n--- 質問 ---")
print(question)

# チェーンを実行
result = qa_chain.invoke({"query": question})

print("\n--- 回答 ---")
print(result["result"])
```

### 質問例1: ProモデルはStandardモデルと比べて何が優れていますか？
製品ラインナップを書いたマークダウンに対する質問です。回答はこのようになりました。

```:出力回答
LuminAI Proモデルは、Standardモデルと比べて以下の点で優れています。

1. **最大輝度**: Proモデルは最大輝度が1100 lmで、Standardモデルの800 lmよりも明るいです。
2. **演色評価数(CRI)**: Proモデルは演色評価数がRa 95で、StandardモデルのRa 80よりも高い色再現性を持っています。これにより、より自然で正確な色合いを表示できます。
3. **Matter対応**: Proモデルはスマートホーム共通規格「Matter」に対応しており、他のMatter対応デバイスとの連携が可能です。Standardモデルはこの機能を持っていません。

これらの特長により、Proモデルはプロフェッショナル向けの用途に適しており、より高い性能を求めるユーザーに向いています。
```

しっかりとドキュメントの内容を参照して回答できていますね。


### 質問例2: Wi-Fiがよく切れるのですが、解決策はありますか？
トラブルシューティングを書いたcsvファイルに関する質問です。


```:出力回答
Wi-Fi接続が頻繁に切れる場合、原因として2.4GHz帯の電波干渉が考えられます。解決策として、ルーターのWi-Fiチャンネルを1, 6, 11のいずれかに固定してみてください。これにより、接続が安定する可能性があります。
```

csvファイルの4行目にある部分の情報から回答を生成できています。


### 質問例3: Proモデルを使ってるんだけど、Matterで接続できません。どうすればいい？
マニュアルのテキストファイルと、トラブルシューティングのcsvファイルの２つを参照する必要がある、少し複雑な質問です。

```:出力回答
Matterで接続できない場合、以下の手順を試してみてください。

1. スマートフォンにMatter対応のハブ（Google HomeやApple HomePodなど）がセットアップされていることを確認します。
2. LuminAIアプリを開き、「設定」>「外部サービス連携」から「Matter」を選択します。
3. 画面に表示されるQRコードを、ハブのアプリで読み込んでください。

もしそれでも接続できない場合、ルーターのIPv6設定が無効になっている可能性があります。この場合は、ご家庭のWi-Fiルーターにて、IPv6パススルーまたは同様の機能を有効にしてください。
```

まずはマニュアルの内容を述べたのち、トラブルシューティングの内容を補足する形で回答を生成できています。**複数のドキュメントにまたがる情報をうまく整理できていることが分かります**。


### 質問例4: ProモデルとStandardモデルの価格の違いは？
最後に、今回教えていない内容の質問を入れてみます。わからない内容はわからないと答えるのが理想ですが、どうでしょうか。

```:出力回答
申し訳ありませんが、その質問にお答えできる情報が見つかりませんでした。
```

分からない内容については正直に「分からない」と回答できています。

以上、製品に関する4つの質問を投げかけてみましたが、想定通りに動作していることが確認できました。


# まとめ
今回は、RAGとLangChainを組み合わせ、製品マニュアルや仕様書といった複数のドキュメントから的確な回答を導き出すQ&Aエンジンを構築しました。コードベースの実装にはなりましたが、UIを作成して組み込めば、十分に実用的なものが構築できたと思います。(本記事では取り上げませんでしたが、「Gradio」というツールを活用することでLLMを組み込んだUIが簡単に作成できます。) **RAGの大きなメリットは、LLM本体を再学習することなく、手元のドキュメントを更新・追加するだけでAIの知識を最新に保てる点です**。新しいトラブルシューティングが見つかれば、そのテキストファイルをフォルダに追加するだけで、エンジンは即座に新しい知識を元に回答できるようになります。この記事を参考に、LLM＋RAGを業務に取り入れてもらえたら幸いです。


# 参考文献
- [LLM Engineering: Master AI, Large Language Models & Agents](https://www.udemy.com/course/llm-engineering-master-ai-and-large-language-models/?utm_source=adwords&utm_medium=udemyads&utm_campaign=Search_DSA_Alpha_Prof_la.EN_cc.ROW-English&campaigntype=Search&portfolio=ROW-English&language=EN&product=Course&test=&audience=DSA&topic=Data_Science&priority=Alpha&utm_content=deal4584&utm_term=_._ag_162511579084_._ad_696197165268_._kw__._de_c_._dm__._pl__._ti_aud-2297301418005%3Adsa-1677974310676_._li_1009288_._pd__._&matchtype=&gad_source=1&gad_campaignid=21168154302&gbraid=0AAAAADROdO2qM-UvCM8LQI-rszh4DwHVj&gclid=CjwKCAjwr5_CBhBlEiwAzfwYuNdj2NO_r91ZAA0ClhIU0RYjs7ZKDMiydIwMP3QULZiTfuXKtrBn7RoCFt8QAvD_BwE&couponCode=PMNVD2525) → ベースとなる知識はこの講座を通じて学びました。

- [LangChain公式サイト](https://python.langchain.com/docs/introduction/)
- [LangChainの使い方 紹介編](https://qiita.com/kzkymn/items/a72796c89ffc696034c8)