0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

社内文書検索AIを作って学んだこと──RAGの「詰まりどころ」を全部書く

0
Posted at

社内文書検索AIを作って学んだこと──RAGの「詰まりどころ」を全部書く

対象読者: RAGに興味があるが実装経験が少ない方、PDFをナレッジベースにしようとしている方

作ったもの

社内規則・経費マニュアル・リモートワーク規定などの PDF を取り込み、自然言語で質問すると該当箇所を引用しながら回答する 社内文書検索 AI を作りました。

技術スタック:

レイヤー 使用技術
Embedding OpenAI text-embedding-3-small
ベクトルDB Supabase pgvector
回答生成 Anthropic claude-sonnet-4-6(ストリーミング)
フロントエンド Next.js 16 App Router + SSE
認証 Supabase Auth(マジックリンク、ドメイン制限)

GitHubリポジトリ: https://github.com/nobunori47/rag-search-ai
デプロイURL: https://rag-search-ai.vercel.app


詰まったポイントと解決方法

1. PDFパース時の「Unicode互換漢字」問題

最初は類似度が 0.45 前後と低く、閾値 0.5 でほとんどヒットしませんでした。原因を調べると、PDFから抽出したテキストに Unicode互換漢字(互換文字)が混入していました。

たとえば「入社」が ⼊社(U+2F09)、「日本」が ⽇本(U+2F4F)として保存されており、Embedding モデルが「入」と「⼊」を別の文字として扱っていたのです。

解決策: 取り込み時に NFKC 正規化を1行追加しました。

text.normalize("NFKC").replace(/\s+/g, " ").trim()

これで互換漢字が標準漢字に統一され、类似度が若干改善しました。ただし、これだけでは不十分で、根本的な問題は別にありました(次節参照)。


2. チャンキング戦略の失敗と改善

類似度が低い本当の原因はチャンクの粒度が粗すぎることでした。

最初の実装は「1ページ = 1チャンク」でした。しかし、就業規則のような文書は1ページに「有給休暇」「時間外労働」「慶弔休暇」など複数のトピックが混在します。これを1チャンクにまとめると、クエリとの類似度が分散し、どの質問にもピンポイントでマッチしなくなります。

ベクトル検索で確認すると、類似度は 0.46 程度。閾値を下げれば拾えますが、それは「精度を犠牲にしてノイズを増やす」対症療法にすぎません。

根本解決: 章単位チャンキングへの変更

// 「第◯章」や「1. 」などの見出しパターンで分割
function splitByChapters(text: string): string[] {
  const chapterRe = /第[0-90-9一二三四五六七八九十百]+章/g;
  if (chapterRe.test(text)) return splitByHeadingPattern(text, chapterRe);
  // numbered headings fallback...
}

章単位に分割し直してデータを全件再取り込みしたところ、類似度が 0.62 まで向上。閾値 0.5 で正しく検索できるようになりました。

学び: 検索精度が低いとき、最初に疑うべきはモデルや閾値ではなくチャンク設計


3. ハルシネーション対策のプロンプト修正

初期プロンプトでは「文書にない場合は推測しないでください」程度の指示しかありませんでした。しかし実際に動かすと、タクシー利用の質問に対して「経理部にご確認ください」という文書に書かれていないアドバイスが回答に混じりました。

修正したプロンプト(抜粋):

- 文書に記載のない情報については、推測・補足・提案・一般論を一切含めず、
  「提供された文書には、該当する情報の記載が見つかりませんでした」とだけ回答すること
- 「〜と思われます」「〜にご確認ください」などの推測や誘導を含む表現は使用しないこと

「使ってはいけない表現のパターン」を具体例で列挙することで、ハルシネーションを大幅に抑制できました。


工夫した点

「縦切りで一周させてからPDFに移行」という開発順序

最初は TXT ファイル2本だけで ingest → search → answer の**一連の流れ(縦切り)**を完成させ、動作確認してから PDF 対応に着手しました。

PDF パース・NFKC正規化・章単位チャンキングは「拡張機能」として後から追加できたので、デバッグの切り分けが明確にできました。「全部同時に作って全部動かない」という状況を避けられたのが良かったです。

閾値を下げるのではなく根本原因を修正した判断

類似度が低いとき、手っ取り早い解決策は MATCH_THRESHOLD を 0.5 → 0.3 に下げることです。しかし、それをやるとノイズが増えてトークン消費が増加し、無関係な文書を参照した回答が生成されます。

「閾値を下げる前に、なぜ類似度が低いのかを確認する」という一手間が、精度を根本から改善できた決め手でした。


精度検証結果

12問の自動テストスクリプトを作成し、期待回答と比較しました。

指標 結果
正答数 11 / 12 問
正答率 92%
平均応答時間 2,622ms
5秒以内クリア 12 / 12 問

不正解の1問(Q9)は it-onboarding.pdf の特定セクションの類似度が惜しくも閾値未満で、正確な情報を引き出せませんでした。チャンキング粒度の更なる改善が課題です。


学んだこと・次に活かすこと

  1. RAGの精度はチャンク設計で決まる。Embeddingモデルの性能より、何をひとまとまりとして保存するかの方が影響が大きい。

  2. ハルシネーション対策は「禁止ルールの具体例列挙」が効く。「推測しない」より「〜という表現を使わない」の方が LLM には伝わりやすい。

  3. 縦切り開発(一本道で最初から最後まで)→ 横拡張の順序は、RAGに限らず有効な進め方だと実感した。

  4. テストスクリプトを早めに作る。12問の自動テストがあったことで、閾値変更やチャンク修正の効果を即座に数値で確認できた。

次は差分更新パイプライン(同一ファイルの再取り込み時の重複除去)と、管理画面(質問ログ・回答品質のモニタリング)を実装したいと思います。


使用ツール: Claude Code / Anthropic claude-sonnet-4-6 / Next.js 16 / Supabase / OpenAI

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?