社内文書を検索して答えさせる仕組みは、作るだけなら短時間で動きます。
難しいのは運用開始後です。実際に必要になったものを整理します。
文書が更新されたときの反映
最初に詰まるのがここです。元の文書が更新されても、
取り込み済みのベクトルは古いままです。
class Document(models.Model):
source_id = models.CharField(max_length=100, unique=True)
content_hash = models.CharField(max_length=64) # 中身のハッシュ
indexed_at = models.DateTimeField(null=True)
ハッシュを持たせ、変わったものだけ再取り込みします。
new_hash = sha256(content.encode()).hexdigest()
if doc.content_hash != new_hash:
delete_vectors(doc.source_id) # 古いものを消してから
index_document(doc, content) # 入れ直す
削除を忘れると、古い情報と新しい情報が両方ヒットして、
矛盾した回答が返るようになります。実際に起きた事故です。
精度を測る仕組みを先に作る
「なんとなく良くなった気がする」では改善できません。
評価用の質問セットを最初に作ります。
EVAL_SET = [
{"q": "経費精算の締日は?", "must_include": ["毎月20日"]},
{"q": "有給の申請方法は?", "must_include": ["勤怠システム", "3営業日前"]},
]
チャンクサイズや検索件数を変えたとき、
このセットの通過率で比較します。30問もあれば傾向は十分見えます。
「答えられない」と言わせる
RAG で最も危険なのは、関連文書が無いのに答えてしまうことです。
if not hits or hits[0].score < THRESHOLD:
return "関連する社内文書が見つかりませんでした。"
スコアのしきい値を設け、下回ったら答えさせません。
業務では「間違った答え」より「答えられない」のほうが遥かに安全です。
出典を必ず返す
回答: 経費精算の締日は毎月20日です。
出典: 経理規程_2024.pdf(3ページ)
出典があると、利用者が自分で検証できます。
これが無い回答は業務では使われません。信頼できないからです。
権限を検索段階で効かせる
全社員が同じ文書を見てよいとは限りません。
回答を生成してからフィルタするのでは遅すぎます。
hits = search(query, filter={"allowed_roles": user.role})
検索の時点で絞るのが原則です。
コスト管理
埋め込みの生成と回答生成の両方に費用がかかります。
- 埋め込みは変更分だけ(ハッシュ比較で無駄を消す)
- よくある質問は回答をキャッシュ
- 検索件数を増やしすぎない(文脈が長い=高い&遅い)
key = sha256(f"{normalized_query}|{doc_version}".encode()).hexdigest()
if cached := cache.get(key):
return cached
文書バージョンをキーに含めるのがポイントです。
更新時に自動でキャッシュが外れます。
まとめ
- 文書のハッシュを持ち、変更分だけ入れ直す(削除を忘れない)
- 評価用の質問セットを先に作り、変更を数値で比較する
- スコアのしきい値を設け、「答えられない」と言わせる
- 出典を必ず返す
- 権限は検索段階で効かせる
- キャッシュキーに文書バージョンを含める