UKA-GYRE 開発記 ― 技術深掘り編 第2回
前回の記事:
この記事は「UKA-GYRE 開発記」 技術深掘り編 第2回 です。
第1回で構築したRAG基盤の上で、検索品質をどう追求したかを掘り下げます。
1. 冒頭の絶望 ―― 検索結果を眺める日々
毎日がこんな感じだった。
- 「なぜこの検索結果が1位に来る?」 ......数時間の調査
- 「なぜ関連するはずのドキュメントが出てこない?」 ......さらに数時間
ベクトル検索は直感に反する挙動を見せることがある。「停滞期」と「維持期」がほぼ同じスコアで返ってくる。意味は全然違うのに、ベクトル空間上では近い。
📘 ベクトル類似度スコアの読み方
スコアは0.0〜1.0の範囲で、1.0に近いほど「意味的に近い」ことを示す。第1回で触れた0.24と0.04の差は、数字だけ見ると小さいが、検索ランキングでは「明確に区別できる」と「ほぼ区別不能」ほどの違いになる。
一番つらいのは「ここがゴール」という明確な指標がないことだ。検索精度100%はベクトル検索の性質上ありえない。「だいたい良さそう」という感覚で進めるしかない。感覚で進めるのは、エンジニアとしてはかなり怖い。
何度も思った。「もうChatGPTに直接聞いた方が早いのでは」と。でもRAGなしだと品質がブレる。結局またチューニングの日々に戻る。
ある日、ふと悟った。
100%を目指すのをやめよう。
完璧な検索が不可能だと分かった時、問いが変わった。
「どうすれば100%になるか」ではなく、「不完全さをどう設計に組み込むか」。
2. 「妥協を設計する」という哲学
完璧な検索結果は原理的に存在しない。ベクトル類似度は万能ではない。100%の精度を前提としたシステムは、現実の前で破綻する。
だから、 「妥協をコードに書く」 ことにした。
「すべての条件を満たす完璧な文書」を探す代わりに、「条件を段階的に緩和して、そのとき最も近い文書」を探す。どこまでは譲れない条件で、どこからは緩和してもいいのか。その線引きを明確にコードに落とし込む。これが 「妥協を設計する」 ということだ。
完璧を求めず、しかし最低ラインは守る。UKA-GYREの検索システムは、この思想の上に4つの設計を積み上げている。ティアード検索、 ラベルブースト、 Dualクエリ、 品質フィルタ だ。
3. ティアード検索 ―― 段階的フォールバック
UKA-GYREのメタデータ(方針・フェーズ・戦略)は離散的なカテゴリ値なので、検索フィルタは完全一致で設計した。しかし完全一致の条件を複数同時に適用すると、 全条件に合致する文書がゼロになる ケースが頻発する。
家を探すときと同じだ。「駅徒歩5分、3LDK、築5年以内、家賃10万円以下」で検索して0件なら、まず築年数を緩和する。それでも足りなければ、徒歩距離を広げる。全ての条件を同時に諦めるのではなく、 優先度の低い条件から順に緩和する。
UKA-GYREのティアード検索は2段階で構成されている。
Step 1:絞り込み検索。
メタデータの完全一致フィルタを使い、クライアントの状況にぴったり合う文書だけを検索する。
方針、戦略、フェーズといった複数の軸が全て一致する文書を探す。
Step 2:広域検索。
Step 1の結果が少ない場合、フィルタを大幅に緩和して広く検索する。
方針や戦略のフィルタを外し、ベクトル類似度と後述するラベルブーストに判断を委ねる。
ここで一つ、 緩和してはならない条件 がある。「特殊状況」のフラグだ。
たとえば「飲み会」というタグが付いた事例は、飲み会の日のリクエストにのみ使われるべきだ。通常の食事記録に混入してはならない。これを 完全隔離ポリシー と呼んでいる。どれだけフィルタを緩和しても、特殊状況の事例は同じ特殊状況のときにしか現れない。**「妥協にもルールがある」**ということだ。
4. 加算の罠と乗算の発見
ティアード検索で候補が集まったら、次は ラベルブースト でスコアを補正する。
これがUKA-GYREの検索品質を支える最も重要な仕組みであり、開発中に最も劇的な転換が起きた箇所でもある。
📘 ブーストとは
検索スコアに対する「重み付け」のこと。ベクトル類似度だけでは測れない「状況の一致度」をスコアに上乗せし、クライアントの状況により適した文書を上位に押し上げる。
4.1. なぜベクトル類似度だけでは不十分なのか
ベクトル類似度は「テキストの意味的な近さ」を測る。しかし、RAGに格納されたArtifactには、方針、戦略、フェーズ、栄養状態といった 構造化されたメタデータ が付与されている。この構造化データの一致度は、ベクトル類似度には反映されない。
「低脂質×停滞期」のArtifactと「バランス型×減量順調」のArtifactが、テキスト的に似たスコアを持つことがある。文章の内容が似ているのだから当然だ。しかし、クライアントの状況との適合度は全く違う。ラベルブーストは、この「メタデータの一致度」をスコアに織り込む仕組みだ。
4.2. 加算が壊れた日
初期の設計では 加算方式 を使っていた。ベクトル類似度に、メタデータの一致度に応じた固定値を足す。単純明快だ。
しかし、ある日、検索結果を眺めていて異変に気づいた。テキスト的には全く関係ないが、メタデータだけが一致する文書が上位に浮上している。ベクトル類似度が低くても、メタデータの一致ボーナスで総スコアが逆転する。「的外れだけどラベルは合っている」文書が、「内容は近いけどラベルが少し違う」文書に勝ってしまっていた。
これは本末転倒だった。ベクトル検索の存在意義を、加算方式が破壊していた。
4.3. 乗算で世界が変わる
乗算方式に変えた瞬間、この問題が消えた。
乗算方式では、ブースト係数がベクトル類似度に掛け算される。つまり、 ベクトル類似度が土台 になる。意味的に近い文書の中で、さらにメタデータが一致するものが上位に来る。「意味の近さ」という前提を壊さずに、構造化データで精度を底上げできる。
やったことは単純で、「足し算」を「掛け算」に変えただけだ。でもこの違いが大きかった。
- 足し算だと「ラベルが合っているだけの的外れな文書」が上位に割り込む
- 掛け算なら、まず内容が近いことが前提になり、そのうえでラベルの一致がボーナスになる
具体的な数字で比較すると、この差は一目瞭然だ。
加算方式では、ベクトル類似度にラベル一致のボーナスを 足す。
| 文書 | ベクトル類似度 | ブースト | 最終スコア |
|---|---|---|---|
| 高品質・ラベル不一致 | 0.72 | +0 | 0.72 |
| 低品質・ラベル一致 | 0.40 | +0.3 | 0.70 |
ほとんど差がない。ランキングが簡単にひっくり返る。
乗算方式では、ベクトル類似度にラベル一致の倍率を 掛ける。
| 文書 | ベクトル類似度 | ブースト | 最終スコア |
|---|---|---|---|
| 高品質・ラベル不一致 | 0.72 | x1.0 | 0.72 |
| 低品質・ラベル一致 | 0.40 | x1.3 | 0.52 |
ベクトル類似度の差が保持されるため、低品質文書が逆転することはない。
4.4. 5軸のブーストと汎用知識の扱い
ブースト計算では、5つの軸(長期方針、現在戦略、提案戦略、フェーズ、特殊状況)それぞれについて一致度を評価する。軸ごとにブーストの重みが異なるのは、検索精度への影響度が違うからだ。
昇華バッチがArtifactを作る際、「特定の方針に限定されない汎用的な知見」には 汎用ラベル が付与される。検索時、クライアントがどの方針であっても、汎用ラベルのArtifactは一定のブーストを受ける。完全一致ほどではないが、ゼロでもない。「8割の人に当てはまるアドバイス」を完全に無視するのはもったいないが、完全一致と同列に扱うのも不適切だ。この「ちょうどいい距離感」を、ブースト値の差で表現している。
さらに、 Artifactの種類によってブーストの計算方式を変えている。
- 成功パターン: 栄養状態の 完全一致 でブーストを与える。状況がピンポイントで一致してこそ価値がある
- 失敗パターン: 部分一致の度合い(Jaccard係数 --- 2つの集合の共通要素の割合を0〜1で表す類似度指標)でブーストを計算する
📘 なぜ成功と失敗で計算方式を変えるのか
成功パターンは「方針もフェーズも栄養状態も全て同じ」事例が最も参考になるため、完全一致でブーストする。一方、失敗パターンは教訓なので、状況が完全に同じでなくても共通要素が多ければ十分参考になる。Jaccard係数で「どれだけ重なっているか」を測り、重なりが大きいほどブーストを上げる。
失敗パターンは教訓だ。状況が完全に一致していなくても、共通する要素が多ければ参考になる。
成功パターンはピンポイント。失敗パターンは広めのネット。 この使い分けが、検索精度と網羅性のバランスを取っている。
5. Dualクエリ ―― Fisher-Yatesシャッフルで視野を広げる
内部ラベルが計算できたら、次はベクトル検索に投げるクエリを構築する。
ここで一つの問題が浮上する。
最初は素朴に、全キーワードを1つのクエリに詰め込んだ。「カロリー不足 タンパク質十分 脂質やや超過 体重減少傾向 低脂質方針」。返ってきた結果を見て、頭を抱えた。「全体的にそれっぽい」文書ばかりが並んでいる。カロリーにもタンパク質にも脂質にもまんべんなく触れた、当たり障りのない事例。本当に欲しかった「カロリー不足に鋭く切り込んだアドバイス」は上位に出てこない。
キーワードを全て1つのクエリに詰め込むと、各キーワードの影響が「薄まる」のだ。本当に欲しいのは、「カロリー不足に焦点を当てたアドバイス」と「脂質超過に焦点を当てたアドバイス」のように、 異なる角度からの参考資料 だ。
医者の問診を思い浮かべてほしい。患者を診るとき、一つの症状だけで判断しない。「血圧的にはこうだな」「血液検査的にはこうだな」と複数の視点で総合判断する。ダイエットコーチも同じだ。「カロリー的にはこうだな」「PFCバランス的にはこうだな」と、異なる軸で同時にデータを読む。Dualクエリはこの「複数の視点による同時分析」をシステムで再現する。
キーワードの配列をFisher-Yatesシャッフル(偏りのないランダム並び替えアルゴリズム)で2グループに分割し、それぞれを独立した検索クエリとして投げる。
📘 なぜ1回の検索では足りないのか
ベクトル検索は「聞き方」で結果が変わる。同じキーワードでも順序や組み合わせが異なると、ヒットする文書が変わる。1回の検索は「ある角度からの最善」に過ぎない。聞き方を変えて複数回検索すれば、異なる視点からの参考資料を同時に手に入れられる。
一方は「カロリー×体重トレンド」の事例を、もう一方は「PFCバランス」の事例を引き当てる。
分割はランダムなので、同じクライアントの同じデータに対しても、毎回異なる組み合わせが生まれる。毎回同じ参考資料ばかり参照されて「似たようなコメントしか出ない」問題を防いでいる。2つのグループから最終候補を選ぶ際は、 必ず異なる文書になるように重複排除 をかける。
6. 品質フィルタ3層構造
ブースト後のスコアで候補が並んだら、最後に「品質フィルタ」をかける。
RAGの知識は週次で増えていく。増えること自体は良いのだが、放置すると古い知識や質の低い知識がノイズとして蓄積する。知識は「量」ではなく「質」だ。
UKA-GYREでは 3層の品質管理 でこの問題に対処している。
第1層:利用実績の追跡。
各Artifactの「検索にヒットした回数」と「実際にプロンプトに注入された回数」を記録する。検索にはヒットするが注入されないArtifactは、品質に問題がある可能性が高い。逆に、頻繁に注入されてトレーナーから高評価を受けるArtifactは、品質スコアが上がる。
第2層:リアルタイム品質ガード。
品質スコアが低いArtifactや、問題が報告されたArtifactを自動的に除外する。ここで重要なのは 「推定無罪」の原則 だ。新しいArtifactは評価データがない。評価されていないものを排除するのではなく、まず使ってみて、結果が悪ければ淘汰する。
第3層:定期的な新陳代謝。
退役バッチによる古い知識や使われない知識の自動的な淘汰。この仕組みの設計詳細は第4回で語る。
この3層構造のポイントは、 「使えば使うほど、品質の低い知識が自然に淘汰される」 ということだ。
トレーナーが日々コメントを選択・評価する行動データが品質スコアに反映され、検索結果の精度が上がる。精度が上がれば、よりよいコメントが生まれ、トレーナーの信頼が増す。正のスパイラルだ。
7. 検索品質における設計哲学
完璧な検索は存在しない。だから、妥協をコードに書く。
「停滞期」と「維持期」のスコア差がわずかしかない世界で、100%の検索精度を前提にしたシステムは破綻する。完璧を追い求めるのではなく、「ここまでは譲れない、ここからは緩和してよい」という線引きをコードで明文化する。
- ティアード検索の段階的フォールバック
- ラベルブーストの乗算方式
- Dualクエリの揺らぎ
全て「不完全さを前提にした上で、そのときの最善を返す」ための仕組みだ。
そして品質フィルタが、トレーナーの日々の評価を自動的に検索精度に還元する。開発者が毎週チューニングしなくても、システムが自律的に良くなっていく。完璧を目指すシステムは現実の前で折れるが、妥協を設計したシステムは現実の中で改善し続ける。
次回は、この検索結果をAIにどう渡すかという話だ。プロンプトの「1行を変えただけで出力が劇的に変わる」不安定さをどう飼い慣らしたか。7層プロンプトの配置哲学と、Lost in the Middle問題への多層防御を語る。
コラム:検索ワードを増やすほど、AIは迷子になる
AIは言葉を「理解」しているのではなく、何千次元という空間の「座標」に配置している。
この発想の原点が、2013年にGoogleが発表した Word2Vec だ。
大量のテキストを学習させると、意味が似た単語は空間上で近くに集まるという性質が自然に現れた。有名な例がある。「王様」の座標から「男」の座標を引き、「女」の座標を足すと、「女王」の近くに着地する。
誰もそう教えていないのに、言葉の意味が足し算と引き算で計算できてしまう。
現在のベクトル検索は、このWord2Vecの子孫にあたる技術だ。ところが高次元空間には数学的な罠がある。 次元の呪い だ。
次元が増えるほど、全ての点が他の全ての点からほぼ同じ距離になってしまう。
3次元空間なら「近い」「遠い」がはっきりしているのに、数千次元では全てが「まあまあ遠い」になる。だから検索クエリにキーワードを詰め込みすぎると、特徴が薄まり「全体的にそれっぽい」結果ばかり返ってくる。
Dualクエリでキーワードを2つに分割したのは、この次元の呪いを回避するためだ。
次回の記事:
「UKA-GYRE」開発記 シリーズ目次
読み物編 --- エンジニアでなくても楽しめる!
- プロローグ ― UKA-GYRE 開発記
- 読み物 第1回 ― ISTJには数字を、ENFPには共感を ― AIに「性格」を与えた日
- 読み物 第2回 ― 深夜3時はAIの経験値稼ぎタイム ― 「昇華」のメカニズム
- 読み物 第3回 ― カロリーの数字の裏にある物語 ― AIが「今日のあなた」を理解するまで
- 読み物 第4回 ― 「何を書くか」より「どう伝えるか」 ― AIを使った大人の実験科学
- 読み物 第5回 ― 「AIが書いた」ではなく「AIと一緒に書いた」 ― Human-in-the-Loopという思想(完結)
- 読み物 番外 ― 自分が作ったAIに食べ過ぎを怒られ続けた話 ― 開発者の裏側
技術深掘り編 --- 設計判断と実装の詳細
- 技術 第1回 ― RAG設計とベクトル化戦略 ― AIに数値の解釈をさせてはいけない
- 技術 第2回 ― ベクトル検索の品質最適化 ― 100%の検索精度は存在しない(この記事)
- 技術 第3回 ― 7層プロンプトエンジニアリング ― たった1行がAIの人格を決める
- 技術 第4回 ― 自己改善ループとサーバーレス基盤 ― 「勝手に賢くなるAI」は存在しない
- 技術 第5回 ― システムアーキテクチャ総覧(完結)