LLM アプリ開発 #13 実践プロジェクト: 社内文書 Q&A ボット
最後の記事です。これまで扱ったかけらを一つに束ねて、社内文書に答える Q&A ボットを作ってみます。会社の規定や製品マニュアルを入れておけば、社員が自然言語で尋ね、文書に基づく答えを受け取れるアプリです。この一つのアプリに、シリーズの核心がほぼすべて入ります。
何を束ねるか #
このボットは、これまで作ったかけらの組み合わせです。
- 検索 — 文書をチャンクに分けて埋め込み、質問に近いチャンクを見つけます(第7回、第8回)。
- 根拠ある答え — 見つけたチャンクだけを根拠に答えさせ、なければ分からないと答えさせます(第4回)。
- ストリーミング — 答えをリアルタイムで流します(第3回)。
- 会話メモリ — 続く質問のために履歴を保ちます(第2回、第9回)。
準備: 文書をインデックスする #
まず文書をチャンクに分けて埋め込んでおきます。このインデックスは文書が変わったときだけ行えばよく、質問のたびにやり直しません。
import numpy as np
from sentence_transformers import SentenceTransformer
embedder = SentenceTransformer("all-MiniLM-L6-v2")
def chunk_text(text: str, size: int = 300, overlap: int = 50):
chunks, start = [], 0
while start < len(text):
chunks.append(text[start:start + size])
start += size - overlap
return chunks
# 社内文書をチャンクに分けて埋め込む
documents = [open(f).read() for f in ["policy.txt", "manual.txt"]]
chunks = [c for doc in documents for c in chunk_text(doc)]
chunk_vectors = embedder.encode(chunks)
def retrieve(query: str, top_k: int = 3):
q = embedder.encode([query])[0]
scores = chunk_vectors @ q
ranked = np.argsort(scores)[::-1][:top_k]
return [chunks[i] for i in ranked]ボットを作る #
検索、根拠プロンプト、ストリーミング、メモリを一つの関数に束ねます。
import anthropic
client = anthropic.Anthropic()
messages = [] # 会話メモリ
SYSTEM = "あなたは社内文書を案内するアシスタントです。提供された資料だけを根拠に答え、資料になければ「資料からは見つかりません」と答えてください。"
def ask(question: str) -> None:
# 1) 検索: 質問に関連するチャンクを見つける
found = retrieve(question)
context = "\n\n".join(f"<doc>{c}</doc>" for c in found)
# 2) 根拠プロンプト: 資料と質問を一緒に入れる
user_content = f"<資料>\n{context}\n</資料>\n\n質問: {question}"
messages.append({"role": "user", "content": user_content})
# 3) ストリーミング: 答えをリアルタイムで出力しながら集める
answer = ""
with client.messages.stream(
model="claude-sonnet-4-6",
max_tokens=1024,
system=SYSTEM,
messages=messages,
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
answer += text
print()
# 4) メモリ: 答えを履歴に足す
messages.append({"role": "assistant", "content": answer})
# 対話的に使う
ask("年次休暇は何日ですか?")
ask("それを半休に分けて使えますか?") # これまでの文脈を覚えて続けて答える流れを追ってみます。質問が来ると、まず関連するチャンクを検索し(1)、その資料と質問を一緒にプロンプトへ入れます(2)。system プロンプトで「資料だけを根拠に、なければ分からない」というルールを固定して幻覚を防ぎます。答えはストリーミングでリアルタイムに出力しながら同時に集め(3)、終わったら履歴へ足して次の質問が文脈を継ぐようにします(4)。二つ目の質問「それを半休に」の「それ」が年次休暇を指すとボットが分かる理由が、このメモリです。
ここからさらに進めるには #
このボットは核心の構造をすべて備えていますが、実際のサービスへ進めるには、これまで扱ったものをさらに付け足します。
- コスト — system と資料が繰り返されるなら、プロンプトキャッシュで費用を減らします(第12回)。会話が長くなれば履歴を要約して圧縮します(第9回)。
- 規模 — 文書が多くなれば、全体比較の代わりにベクトルデータベースを使います(第7回)。
- 品質 — 質問と期待する答えの評価セットで、プロンプト変更の効果を測ります(第12回)。
- Web — コンソール出力の代わりに
StreamingResponseでブラウザまで答えを流します(第3回)。 - 行動の拡張 — 文書検索を越えて外部システムを操作する必要があれば、ツール呼び出しとエージェントへ広げます(第6回、第10回)。
シリーズを終えて #
最初に始めたときは、API キー一つで一文をやり取りするのが全部でした。そこからメッセージの構造とパラメータ、ストリーミング、プロンプト、構造化された出力で基礎を固め、ツール呼び出しと RAG、メモリ、エージェント、MCP でアプリが外の世界とつながるようにし、コストと評価と観測で運用の土台を整えました。そして今回、そのすべてを一つの動くアプリへ束ねました。
LLM アプリ開発の全体像は、じつは単純です。良い文脈を集めてプロンプトに入れ、モデルの答えを受け取ってコードへ安全につなぐこと。検索も、メモリも、構造化された出力も、結局はこの大きな流れの一部でした。このシリーズで身につけたかけらを組み合わせれば、あなたの問題に合った LLM アプリを自分で設計できます。
このシリーズは Claude を基準に説明しましたが、扱ってきた発想はほとんど他のプロバイダにもそのまま移せます。さあ、作りたいアプリを決めて、最も小さい形からかけらを一つずつ足していってみてください。ここまでご一緒いただき、ありがとうございました。