



WithCodeMedia-1-pc
WithCodeMedia-2-pc
WithCodeMedia-3-pc
WithCodeMedia-4-pc




WithCodeMedia-1-sp
WithCodeMedia-2-sp
WithCodeMedia-3-sp
WithCodeMedia-4-sp









生徒ChatGPTに「うちの会社の製品マニュアルを読んで質問に答えてほしい」と思っているんですが、汎用のChatGPTは社内データを知らないですよね。どうすればいいですか?



それはRAGという仕組みを使うんじゃ!RAGとは「質問が来たら関連ドキュメントを検索してから、その内容だけを参照してLLMに回答させる」構成じゃ。ファインチューニングよりずっと手軽で、社内ドキュメント・製品マニュアル・FAQなどをChatGPTに覚えさせることができるんじゃぞ。今日はPython・OpenAI API・Streamlitを使ったRAGチャットボットをコード付きで完全解説するぞい!
RAG(Retrieval-Augmented Generation)は、LLMによる回答生成の前に「外部データベースから関連情報を検索して取得する」ステップを挟むことで、モデルが学習していない独自情報でも正確に回答できるようにする手法です。社内FAQ・製品マニュアル・NotionのナレッジベースなどあらゆるデータをChatGPTに「参照させる」ことができます。本記事ではRAGの基本概念から実装コード全文・本番運用のポイントまで完全解説します。
RAGを一言で説明すると、「質問に答える前に関連ドキュメントを検索してから、その内容だけを参考にして回答を生成する」仕組みです。
【ChatGPT(汎用LLM)の限界】
❌ 学習データのカットオフ以降の情報を知らない
❌ 自社の製品情報・社内マニュアル・ナレッジベースを知らない
❌ ハルシネーション(もっともらしい嘘)を生成することがある
❌ 回答の根拠となるドキュメントを示せない
【RAGで解決できること】
✅ 自社ドキュメントを参照しながら回答できる
✅ 「どの文書を参考にしたか」をユーザーに提示できる(根拠の透明性)
✅ 情報を更新するたびにドキュメントを更新するだけで最新情報に対応できる
✅ ハルシネーションを大幅に削減できる(参照範囲を制限するため)| 観点 | RAG | ファインチューニング |
|---|---|---|
| 実装難易度 | 低い(APIキーとコードのみ) | 高い(学習データ整備・GPU必要) |
| コスト | 低い(Embeddings APIのみ) | 高い(学習費用 + 推論コスト) |
| データ更新 | 簡単(ドキュメントを差し替えるだけ) | 難しい(再学習が必要) |
| 回答の根拠提示 | 参照ドキュメントを表示できる | 根拠を示しにくい |
| 向いているケース | 社内ドキュメント・FAQ・マニュアル検索 | 特定の文体・フォーマットを学習させたい場合 |
| 必要なデータ量 | 数MB〜数GBの文書に対応 | 数千〜数万件の学習データが必要 |
結論:社内データを参照させたいだけなら RAG 一択です。ファインチューニングは「ChatGPTを特定の口調・形式で回答させたい」「専門タスクへの特化」という場合に向いています。
【使用技術スタック】
LLM: OpenAI API(gpt-4o-mini)
Embedding: OpenAI Embeddings API(text-embedding-3-large)
類似度計算: scikit-learn(cosine_similarity)
UI: Streamlit
データ取得: Google Drive API + google-auth
設定管理: PyYAML(config.yml)# プロジェクトフォルダを作成
mkdir rag-chatbot && cd rag-chatbot
# 仮想環境を作成・有効化
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# 必要なパッケージをインストール
pip install openai streamlit google-api-python-client \
google-auth-httplib2 google-auth-oauthlib \
scikit-learn pyyaml# ファイル構成
rag-chatbot/
├── app.py # メイン:StreamlitのUI・チャット処理
├── utils.py # ドキュメント取得・Embedding・類似度計算・LLM問い合わせ
├── config.yml # APIキー・モデル名・フォルダID等の設定
└── service_account_key.json # Google Cloud のサービスアカウントキー(Gitに含めない)service_account_key.json として保存する*@*.iam.gserviceaccount.com)を「閲覧者」として共有するfolders/ 以降の文字列をフォルダIDとして控えておく# .gitignore に必ず追加する
service_account_key.json
config.yml
.env
__pycache__/
venv/# config.yml
# ⚠️ このファイルはGitに含めないこと(.gitignoreに追加する)
# OpenAI設定
openai:
api_key: 'sk-xxxxxxxxxxxxxxxxxxxx' # OpenAI APIキー
embedding_model: 'text-embedding-3-large' # Embeddingモデル
chat_model: 'gpt-4o-mini' # チャットモデル(コスト・速度のバランス良)
# Google Cloud設定
google:
service_account_file: 'service_account_key.json' # サービスアカウントキーのパス
# Google Drive設定
google_drive:
folder_id: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' # ドキュメントフォルダのID# utils.py
import yaml
from googleapiclient.discovery import build
from google.oauth2.service_account import Credentials
from openai import OpenAI
from sklearn.metrics.pairwise import cosine_similarity
# 設定ファイルを読み込む
with open('config.yml', 'r') as file:
config = yaml.safe_load(file)
# OpenAI クライアントの初期化
client = OpenAI(api_key=config['openai']['api_key'])
# Google Drive クライアントの初期化
credentials = Credentials.from_service_account_file(config['google']['service_account_file'])
google_client = build('drive', 'v3', credentials=credentials)
def get_docs_list(folder_id: str) -> list[dict]:
"""
指定したGoogle DriveフォルダからGoogle Docsの一覧を取得し、
テキスト内容・ドキュメント名・URLをリストで返す。
"""
# フォルダ内のGoogle Docsを一覧取得
files = google_client.files().list(
q=f"'{folder_id}' in parents and mimeType='application/vnd.google-apps.document'",
fields="files(id, name)"
).execute().get('files', [])
docs_list = []
for file in files:
doc_id = file['id']
# ドキュメントをプレーンテキストとしてエクスポート
doc_content = google_client.files().export(
fileId=doc_id,
mimeType='text/plain'
).execute()
docs_list.append({
'name': file['name'],
'url': f"https://docs.google.com/document/d/{doc_id}/edit",
'content': doc_content.decode('utf-8')
})
return docs_list
def vectorize_text(text: str) -> list[float]:
"""
テキストをOpenAI Embeddings APIでベクトル(数値の配列)に変換する。
意味的に似たテキストは近いベクトルになる。
"""
response = client.embeddings.create(
input=text,
model=config['openai']['embedding_model']
)
return response.data[0].embedding
def find_most_similar(
question_vector: list[float],
vectors: list[list[float]],
documents: list[str],
top_n: int = 2
) -> list[str]:
"""
質問ベクトルとドキュメントベクトルのcosine類似度を計算し、
最も関連性の高い上位 top_n 件のドキュメントを返す。
"""
similarities = []
for index, vector in enumerate(vectors):
# cosine類似度:-1(真逆)〜 1(完全一致)の値を返す
similarity = cosine_similarity([question_vector], [vector])[0][0]
similarities.append([similarity, index])
# 類似度の高い順にソートして上位 top_n 件を返す
similarities.sort(reverse=True, key=lambda x: x[0])
top_documents = [documents[index] for similarity, index in similarities[:top_n]]
return top_documents
def ask_question(question: str, context: list[str]) -> str:
"""
質問と関連ドキュメントを組み合わせてGPTに問い合わせ、回答を返す。
context(関連ドキュメント)の範囲のみを参照するように指示する。
"""
# コンテキスト(関連ドキュメント)を結合
context_text = "\n\n---\n\n".join(context)
messages = [
{
"role": "system",
"content": (
"あなたは社内ドキュメントに基づいて回答するアシスタントです。"
"以下の情報のみを使用して回答してください。"
"提供された情報に回答がない場合は「提供されたドキュメントには該当する情報がありませんでした」と回答してください。"
)
},
{
"role": "user",
"content": f"質問: {question}\n\n参照ドキュメント:\n{context_text}"
}
]
response = client.chat.completions.create(
model=config['openai']['chat_model'],
messages=messages,
temperature=0.1 # 低い温度で事実に基づいた回答を促す
)
return response.choices[0].message.content# app.py
import streamlit as st
import yaml
from utils import vectorize_text, find_most_similar, ask_question, get_docs_list
# 設定ファイルを読み込む
with open('config.yml', 'r') as file:
config = yaml.safe_load(file)
@st.cache_data(ttl=3600) # 1時間キャッシュ(毎回Drive APIを叩かないようにする)
def load_documents():
"""
Google DriveからドキュメントとEmbeddingベクトルを取得する。
ttl=3600 でキャッシュすることでAPI呼び出し回数を削減する。
"""
docs = get_docs_list(config['google_drive']['folder_id'])
contents = [doc['content'] for doc in docs]
# すべてのドキュメントをベクトル化(初回のみ)
vectors = [vectorize_text(content) for content in contents]
return docs, contents, vectors
def main():
st.set_page_config(
page_title="社内ドキュメント検索チャットボット",
page_icon="📚",
layout="centered"
)
st.title('📚 社内ドキュメント検索チャットボット')
st.caption("Google Drive のドキュメントを参照して質問に回答します")
# ドキュメントとベクトルをキャッシュから取得
with st.spinner("ドキュメントを読み込んでいます..."):
docs, contents, vectors = load_documents()
st.success(f"{len(docs)} 件のドキュメントを読み込みました")
# チャット履歴の初期化
if 'messages' not in st.session_state:
st.session_state.messages = []
# チャット履歴の表示
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
# ユーザーの入力を処理
if user_input := st.chat_input("質問を入力してください(例:有給休暇の申請方法は?)"):
# ユーザーのメッセージを表示・保存
st.session_state.messages.append({"role": "user", "content": user_input})
with st.chat_message("user"):
st.markdown(user_input)
# 回答を生成
with st.chat_message("assistant"):
with st.spinner("ドキュメントを検索して回答を生成中..."):
# 質問をベクトル化して類似ドキュメントを検索
question_vector = vectorize_text(user_input)
similar_documents = find_most_similar(question_vector, vectors, contents)
# 関連ドキュメントをコンテキストとしてGPTに渡す
answer = ask_question(user_input, similar_documents)
# 参照したドキュメントへのリンクを付加する
response = f"{answer}\n\n---\n**📖 参照したドキュメント:**\n"
for doc in docs:
if doc['content'] in similar_documents:
response += f"- [{doc['name']}]({doc['url']})\n"
st.markdown(response)
# アシスタントの回答を保存
st.session_state.messages.append({"role": "assistant", "content": response})
if __name__ == "__main__":
main()# Streamlit アプリを起動する
streamlit run app.py
# ブラウザが自動で開く(またはhttp://localhost:8501 にアクセス)「なぜテキストをベクトルに変換するのか」「cosine類似度とは何か」を理解することで、RAGをより深く活用・カスタマイズできるようになります。
【Embeddingの直感的な理解】
テキスト → 数値の配列(ベクトル)に変換する処理
例:
"有給休暇の申請方法" → [0.21, -0.43, 0.87, ..., 0.12] # 3072次元のベクトル
"年次有給の取り方" → [0.19, -0.41, 0.89, ..., 0.11] # ← 意味が近いので値も近い
"プログラムのバグ修正" → [-0.78, 0.31, -0.22, ..., 0.55] # ← 意味が遠いので値も遠い
重要なポイント:
→ 意味的に似たテキストは似たベクトルになる
→ 文字が違っても「意味が近い」なら近いベクトルになる
「有給」と「年次有給」は文字が違うが同じトピックなので近いベクトルになる
OpenAI の text-embedding-3-large:
→ 3072次元のベクトルを生成する
→ 日本語テキストにも対応している
→ 1,000トークンあたり $0.00013(非常に安価)【Cosine類似度の直感的な理解】
2つのベクトルの「向きの近さ」を -1〜1 の値で表す。
cosine_similarity = 1.0 → 完全に同じ方向(意味が完全に一致)
cosine_similarity = 0.9 → ほぼ同じ方向(意味が非常に近い)
cosine_similarity = 0.5 → やや似ている
cosine_similarity = 0.0 → 無関係
cosine_similarity = -1.0 → 真逆(実際のテキストではほぼ発生しない)
【コードの動作】
# 質問:「有給休暇の取り方は?」
question_vector = [0.21, -0.43, 0.87, ...]
# ドキュメントA(有給申請マニュアル)のベクトル:
doc_a_vector = [0.19, -0.41, 0.89, ...]
# → cosine_similarity = 0.97(非常に近い → 上位に選ばれる)
# ドキュメントB(給与規定)のベクトル:
doc_b_vector = [0.10, -0.20, 0.45, ...]
# → cosine_similarity = 0.72(やや近い)
# ドキュメントC(サーバー構成図)のベクトル:
doc_c_vector = [-0.78, 0.31, -0.22, ...]
# → cosine_similarity = 0.21(遠い → 選ばれない)
→ 上位 top_n = 2 件(ドキュメントA・B)がコンテキストとしてGPTに渡される長いドキュメントをそのままEmbeddingすると、1つのベクトルが多くの情報を含みすぎて検索精度が下がります。ドキュメントを適切なサイズに分割(チャンク分割)してからEmbeddingすることで精度が向上します。
# utils.py に追加するチャンク分割関数
def split_into_chunks(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]:
"""
長いテキストを chunk_size 文字ごとに分割し、
overlap 文字のオーバーラップを持たせて文脈の途切れを防ぐ。
"""
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunk = text[start:end]
chunks.append(chunk)
start += chunk_size - overlap # オーバーラップ分を戻す
return chunks
# 使用例
doc_content = "...長いドキュメントのテキスト..."
chunks = split_into_chunks(doc_content, chunk_size=500, overlap=50)
# チャンクごとにEmbeddingを作成する
chunk_vectors = [vectorize_text(chunk) for chunk in chunks]
# 検索結果のチャンクを結合して回答に使用する
# → 大きなドキュメントでも関連する部分だけを正確に取得できる【ベクトルDBが必要になるタイミング】
現在の実装(in-memory):
→ 毎回すべてのドキュメントをEmbeddingする(起動が遅い)
→ ドキュメントが数十件なら問題ない
→ 数千件以上になると起動時間・メモリ使用量が問題になる
ベクトルDB(Pinecone / Chroma / Qdrant / Weaviate):
→ Embeddingベクトルを永続化して保存する
→ 高速な近似最近傍検索(ANN)を提供する
→ ドキュメントの追加・削除・更新が容易になる
# ChromaDB(ローカル・無料)の導入例
pip install chromadb
import chromadb
client = chromadb.Client()
collection = client.create_collection("company-docs")
# ドキュメントをベクトルDBに追加
collection.add(
documents=["ドキュメント1のテキスト", "ドキュメント2のテキスト"],
ids=["doc1", "doc2"]
)
# 類似ドキュメントを検索
results = collection.query(
query_texts=["有給休暇の申請方法"],
n_results=2
)【対応できるデータソースの例】
1. Notion(ノーションAPI)
→ Notion APIでページ一覧を取得してテキストを抽出する
2. ローカルのPDF・Wordファイル
→ PyMuPDF / python-docx でテキスト抽出
pip install pymupdf python-docx
3. Webページのスクレイピング
→ BeautifulSoup4 でHTMLからテキスト抽出
pip install beautifulsoup4 requests
4. Confluenceやその他のWikiシステム
→ Confluence REST APIで記事一覧・内容を取得
5. SlackのDM・チャンネル履歴
→ Slack APIでメッセージ履歴を取得
# PDFからテキスト抽出の例
import fitz # PyMuPDF
def extract_text_from_pdf(pdf_path: str) -> str:
doc = fitz.open(pdf_path)
text = ""
for page in doc:
text += page.get_text()
return text# utils.py - ストリーミングレスポンス版
def ask_question_stream(question: str, context: list[str]):
"""
GPTの回答をストリーミングで返すジェネレータ関数。
Streamlitの st.write_stream() と組み合わせて使う。
"""
context_text = "\n\n---\n\n".join(context)
messages = [
{"role": "system", "content": "以下の情報のみを使用して回答してください。"},
{"role": "user", "content": f"質問: {question}\n\n参照ドキュメント:\n{context_text}"}
]
stream = client.chat.completions.create(
model=config['openai']['chat_model'],
messages=messages,
stream=True # ストリーミングを有効化
)
for chunk in stream:
if chunk.choices[0].delta.content is not None:
yield chunk.choices[0].delta.content
# app.py - ストリーミングレスポンスの表示
with st.chat_message("assistant"):
response = st.write_stream(
ask_question_stream(user_input, similar_documents)
)# utils.py - スコアによるフィルタリング
def find_most_similar_with_threshold(
question_vector: list[float],
vectors: list[list[float]],
documents: list[str],
top_n: int = 2,
threshold: float = 0.7 # この類似度以上のドキュメントのみを返す
) -> list[str]:
"""
類似度が threshold 以上のドキュメントのみを返す。
閾値を設けることで無関係なドキュメントをコンテキストに含めるのを防ぐ。
"""
similarities = []
for index, vector in enumerate(vectors):
similarity = cosine_similarity([question_vector], [vector])[0][0]
if similarity >= threshold: # 閾値フィルタ
similarities.append([similarity, index])
similarities.sort(reverse=True, key=lambda x: x[0])
if not similarities:
return [] # 関連ドキュメントが見つからなかった場合
top_documents = [documents[index] for similarity, index in similarities[:top_n]]
return top_documents【ユースケース1:社内FAQ・人事情報の自動回答】
データソース:就業規則・有給申請方法・経費精算ルール・入社手続きドキュメント
効果:人事部への問い合わせ件数を削減できる
質問例:「有給休暇は入社何ヶ月後から取得できますか?」
「交通費の申請締め切りはいつですか?」
【ユースケース2:製品マニュアル・サポートチャットボット】
データソース:製品仕様書・操作マニュアル・FAQ・よくあるトラブルと解決策
効果:サポート対応時間を削減・24時間対応を実現できる
質問例:「エラーコード E-404 が表示されたときの対処法は?」
「〇〇機能の設定方法を教えてください」
【ユースケース3:過去案件・提案書の検索支援】
データソース:過去の提案書・案件報告書・顧客別資料・議事録
効果:類似案件の素早い検索・提案書作成の効率化
質問例:「小売業のDX支援で過去に提案した内容はありますか?」
「顧客の〇〇社との商談履歴を教えてください」
【ユースケース4:技術ドキュメント・API仕様の検索】
データソース:社内のAPI仕様書・アーキテクチャ設計書・コーディング規約
効果:新入エンジニアのオンボーディング効率化
質問例:「ユーザー認証APIのエンドポイントとパラメータは?」
「データベース接続の設定方法は?」
【ユースケース5:法的ドキュメント・契約書の参照】
データソース:各種契約書テンプレート・法的チェックリスト・コンプライアンスガイドライン
効果:法務確認の初期スクリーニングを自動化
質問例:「業務委託契約書の秘密保持条項のサンプルは?」
「個人情報取り扱いに必要な同意文書の要件は?」【エラー1: google.auth.exceptions.DefaultCredentialsError】
原因:サービスアカウントキーのファイルパスが間違っている
対処:config.yml の service_account_file のパスを確認する
service_account_key.json が存在するディレクトリから app.py を起動する
【エラー2: HttpError 403 when requesting Google Drive API】
原因:サービスアカウントにフォルダの閲覧権限がない
対処:Google Drive でフォルダを右クリック → 「共有」から
サービスアカウントのメールアドレスを「閲覧者」として追加する
【エラー3: openai.AuthenticationError: Incorrect API key provided】
原因:OpenAI APIキーが間違っているか、有効期限切れ
対処:platform.openai.com でAPIキーを再確認・再生成する
config.yml の api_key を更新する
【エラー4: openai.RateLimitError: Rate limit reached】
原因:短時間にAPI呼び出しが集中している
対処:time.sleep(1) で呼び出しの間隔を開ける
または Embeddings の計算結果をキャッシュする(@st.cache_data 活用)
【エラー5: 回答が「情報がありません」と表示される】
原因:類似度スコアの閾値が高すぎる / ドキュメントの内容が質問と一致しない
対処:threshold を 0.7 → 0.5 に下げて試す
ドキュメントのテキスト内容を確認し、質問と関連する情報が含まれているか確認するPythonの基礎(関数・辞書・リスト操作)がわかれば実装できます。本記事のコードはコピー&ペーストで動作するように設計されています。難しい数学的背景(線形代数・cosine類似度の計算式)は理解していなくても使えます。まずはコードをそのまま動かして動作を確認してから、自分のデータソースに置き換えるアプローチをお勧めします。
非常に安価です。Embeddingは1,000トークンあたり約$0.00013(0.02円程度)で、チャットには gpt-4o-mini(入力1Mトークンあたり$0.15)を使うため、月100回程度の質問なら数十円で運用できます。ただし毎回すべてのドキュメントをEmbeddingすると起動コストが嵩むため、@st.cache_data によるキャッシュを必ず活用してください。
対応できます。get_docs_list() 関数でテキストを取得する部分を置き換えるだけで、どんなデータソースにも対応できます。PDFなら PyMuPDF(pip install pymupdf)、NotionならNotion API、社内WikiならそれぞれのAPIを使ってテキストを取得する関数を書くだけで、Embedding・類似度検索・LLM問い合わせの部分はそのまま使えます。
OpenAI API経由で送ったデータは、デフォルトではモデルの学習に使用されません(API利用時のデータ利用ポリシーより)。ただし、高度な機密情報(個人情報・営業秘密・未公開財務情報)を扱う場合は、Azure OpenAI Serviceの使用を推奨します。Azure OpenAIではデータがユーザーのAzureリソース内に留まり、Microsoftにデータが送られないことが明示されています。
LangChainはRAGを含むLLMアプリケーション構築のためのフレームワークです。本記事の実装は「RAGの仕組みを理解するためにスクラッチで書いた最小構成」です。本番運用ではLangChainやLlamaIndexを使うことで、チャンク分割・ベクトルDB連携・会話履歴管理・エラーハンドリングを簡単に実装できます。まずは本記事の最小実装でRAGの動作原理を理解してから、LangChainに移行するアプローチを推奨します。



RAGって意外とシンプルな仕組みなんですね!「検索してからLLMに渡す」だけなんですね。実際に動かしてみたいです!



そうじゃ!RAGの本質は「検索(Retrieval)+生成(Generation)」のシンプルな組み合わせじゃ。今日の実装を動かしてみれば「あ、これだけで社内チャットボットが作れるのか」と驚くはずじゃぞ。WithCodeでPythonやJavaScriptの基礎を学んだ後、このRAGの実装に挑戦してみてくれ。AIを「使う」だけでなく「作る」側になれると、エンジニアとしての価値が一段上がるんじゃ!



まずは自分のGoogle Driveのドキュメントを使ってコードを動かしてみます!将来的には会社に提案できるようなチャットボットを作ってみたいです!
text-embedding-3-large が日本語にも対応RAGは「AIを使いこなすエンジニア」と「AIを使うだけのユーザー」を分ける技術のひとつです。APIキーとPython環境があれば今日から実装できるので、まず手を動かしてみましょう。


副業・フリーランスが主流になっている今こそ、自らのスキルで稼げる人材を目指してみませんか?
未経験でも心配することはありません。初級コースを受講される方の大多数はプログラミング未経験です。まずは無料カウンセリングで、悩みや不安をお聞かせください!
公式サイト より
今すぐ
無料カウンセリング
を予約!