UKA-GYRE 開発記 ― 技術深掘り編 第1回
前回の記事:
この記事は「UKA-GYRE 開発記」 技術深掘り編 第1回 です。
読み物編では語らなかった設計判断の裏側を、技術的な根拠とともに掘り下げます。
1. 「今日のカロリーは多いですか?」にAIが答えられない
開発初期、最も素朴なプロンプトから始めた。
クライアントの食事データをそのまま渡して「この人にアドバイスしてください」。
簡単なことだ。
AIは賢い。
数字を見れば、何が問題かくらいわかるだろう。
甘かった。
「今日の摂取カロリーは1,820kcalです」。
この数字を10回AIに渡して、10回とも同じ判断が返ってくるか試した。
結果は ガチャ だった。
ある日は「少し不足気味ですね」、翌日は「良いバランスです」、次の日は「やや多めですね、気をつけましょう」。
同じ数字を渡して、真逆の判断が返ってくる。
理由は単純だ。
1,820kcalが「多い」のか「少ない」のかは、その人の目標次第で決まる。
目標2,200kcalの人にとっては明らかに不足。
目標1,500kcalの人にとっては明らかに超過。
AIはこの「目標との差分」を毎回異なる基準で解釈してしまうのだ。
ここで設計の根本を揺るがす問いに直面した。
AIに数値の解釈をさせてはいけないのではないか。
2. 「AIに判断させない」という哲学の発見
AIの役割を整理し直した。
AIが得意なのは 「状況を踏まえて、人に響く言葉を紡ぐこと」 だ。
データと数字を言語化し、人間にとって意味のある文章に変換する能力は、人間を凌駕する場面すらある。
一方、AIが苦手なのは「数値の良し悪しを一貫して判定すること」だ。
1,820kcalが目標に対して多いのか少ないのか。タンパク質90gは十分なのか不足なのか。
こうした判定は、目標値と実績値の差分計算という、機械的なルールで決まる話だ。
そこにAIの創造性は不要であり、むしろ有害だ。
得意なことだけをやらせる。 これがUKA-GYRE全体を貫く第一の設計哲学になった。
3. 内部ラベル自動計算 ―― 「判断済みの結論」だけを渡す
この哲学を実装に落とし込んだのが、 内部ラベル自動計算 だ。
食事データと目標値を比較して、「カロリー不足」「タンパク質十分」「体重減少傾向」といった 離散的なラベル に変換する。
📘 離散ラベルとは
連続的な数値(例:1,820kcal)を、「不足」「適正」「超過」のようなカテゴリに分類すること。温度計の数値をそのまま伝える代わりに「暑い」「ちょうどいい」「寒い」と言い換えるようなもの。カテゴリは有限個なので、誰が判定しても同じ結果になる。
病院のトリアージに近い。
患者の容態を医師の主観に頼らず、バイタルサインから機械的に重症度を判定するのと同じ発想だ。
AIには「1,820kcal」だけではなく「1,820kcal」「カロリー:やや不足」を渡す。
数字の解釈は終わっている。
AIに残された仕事は、「1,820kcalというやや不足している人に対して、どう声をかけるか」だけだ。
これならブレない。
ラベル判定のロジックは、実績値と目標値の差分パーセントから機械的に決定される。
- カロリー: 目標との差分が±5%以内なら「適正(on_target)」、-5%〜-20%なら「不足(deficit)」、+5%〜+20%なら「超過(surplus)」、±20%を超えれば「極端な不足/超過(extreme)」
- PFC(タンパク質・脂質・炭水化物): やや緩く、±10%以内が「適正」、±10%〜20%が「不足/超過」、±20%超が「極端」
この離散化により、AIの解釈のブレを完全に排除している。
さらに、カロリーやタンパク質などの状況を組み合わせた 複合キー を生成する。
「カロリーは不足気味だがタンパク質は十分」のように、栄養状態の全体像をひとつのラベルに圧縮する。
この複合キーは、後述するRAG検索で「同じ栄養状態の過去事例」をピンポイントで見つけるための核心部品になる。
ここにもう一つ工夫がある。方針に応じたノイズ除去だ。
たとえば糖質制限方針のクライアントに対して、脂質関連のラベルは意味をなさない。糖質を制限している人は脂質の摂取量を厳密に管理していないことが多く、脂質のラベルが検索キーワードに混じると、本来フォーカスすべき糖質管理のアドバイスがノイズに埋もれてしまう。方針ごとに「不要なラベル」を定義し、検索キーワードに含めないようにしている。
4. RAGアーキテクチャの選択 ―― OpenSearchへの未練を断ち切るまで
「AIに判断させない」と決めたら、次は「AIに何を参考にさせるか」の設計だ。
AIに過去の成功・失敗パターンを参考資料として渡す手法をRAG(Retrieval-Augmented Generation、検索拡張生成)と呼ぶ。
4.1. なぜ Fine-tuning ではなく RAG なのか
| 手法 | 知識更新 | 必要データ量 | コスト | UKA-GYREとの相性 |
|---|---|---|---|---|
| Fine-tuning | 再学習が必要 | 数千件〜 | 高い | × |
| RAG | 即時反映 | 数十件〜 | 低い | ◎ |
| プロンプトのみ | なし | なし | 最低 | △ |
UKA-GYREでは迷うことなくRAGを選んだ。知識が毎週増えるためFine-tuningの再学習は非現実的、運用開始時点ではデータゼロなので少量データから効果が出るRAGが必須、そして個人開発でFine-tuningのコストは論外だ。
📘 Fine-tuningとは
Fine-tuning(ファインチューニング)とは、既存のAIモデルに追加データを与えて再訓練し、モデル自体の振る舞いを変える手法。「AIの脳に知識を焼き付ける」イメージに近い。効果は高いが、再訓練のたびにコストと時間がかかり、知識の追加や修正がすぐには反映されない。RAGは「カンニングペーパーを毎回渡す」方式で、知識の追加はファイルを置くだけで済む。
4.2. ベクトルストアの選択 ―― 未練と決断
RAGの心臓部は「ベクトルストア」だ。テキストを数値の配列(ベクトル)に変換して格納し、意味的に近い文書を検索するデータベースのことである。
📘 テキストのベクトル化とコサイン類似度
テキストをベクトル(数百〜数千個の数値の配列)に変換すると、意味の世界が「空間」になる。「りんご」と「みかん」は近くに、「りんご」と「経済学」は遠くに配置される。2つのベクトルの 向き がどれだけ揃っているかを測る指標が コサイン類似度 で、向きが近い=意味が近い、と直感的に理解してよい。
正直に言えば、 OpenSearch Serverless への未練は長く残った。ベクトル検索に加えてキーワード検索も組み合わせる「ハイブリッド検索」ができる。精度面では最強だ。
しかし、 最低でも月額数万円の固定費。待機しているだけで課金される。個人開発にこの固定費は論外だった。
最終的に Amazon Bedrock Knowledge Bases(以下、ナレッジベース)+ S3 Vectors を選んだ。2025年に登場した新しいサービスだ。
| 選択肢 | 精度 | コスト | 運用性 | メタデータフィルタ |
|---|---|---|---|---|
| OpenSearch Serverless | ◎ | ×(高い) | △ | ◎(ハイブリッド) |
| Pinecone | ◎ | △ | ◎ | ○ |
| pgvector (RDS) | ○ | △ | △ | ○ |
| FAISS (Lambda内) | △ | ◎ | △ | × |
| Amazon Bedrock KB + S3 Vectors | ○ | ◎ | ◎ | ○ |
決め手は3つ。
- ゼロ運用 --- S3にファイルを置くだけでインデックスが構築される
- AWS内完結 --- 外部サービスへの依存ゼロ
- 従量課金 --- 使わなければほぼ課金されない
トレードオフとして ハイブリッド検索ができない が、この弱点をどう補ったかは第2回の「ラベルブースト」で詳述する。
📘 ハイブリッド検索とは
ベクトル検索(意味の近さで探す)とキーワード検索(単語の完全一致で探す)を組み合わせた検索手法。意味検索だけでは「停滞期」と「維持期」を区別しきれない場面でも、キーワード一致を併用すれば正確にフィルタリングできる。
5. 512次元から1024次元へ ―― 解像度の選択
ベクトルストアが決まったら、次はエンベディングモデル(テキストを数値の配列に変換するAIモデル)の選定だ。
UKA-GYREでは Titan Embeddings v2 を採用した。
AWSネイティブでAmazon Bedrock KBとの統合がシームレスであること、日本語対応、そして 次元数を256・512・1024から選べること が決め手だ。
📘 次元数のイメージ
次元数とは「1つのテキストを何個の数値で表現するか」の設定だ。白黒写真(低次元)とフルカラー高解像度写真(高次元)の違いに近い。256次元なら大まかな意味だけ捉えるが、1024次元なら「停滞期」と「維持期」のような微妙なニュアンスの差も見分けられる。ただし次元が増えるほどストレージと計算コストも増える。
最初は512次元で始めた。しかし、ダイエット用語には「停滞期」「維持期」「減量順調」「減量初期」など、意味的に近いが状況が全く異なる語が大量にある。512次元では「停滞期」と「維持期」のスコア差がわずか0.04で、ほぼ区別できていなかった。1024次元に変更すると差が0.24に開き、検索精度が劇的に改善した。
ただし、次元数を変えると全データの再インジェストが必要になる。512次元のベクトルと1024次元のベクトルは混在できないからだ。月に数ドルのコスト差を気にして512を選んだ判断は、振り返れば割に合わなかった。
6. チャンキング ―― 「1Artifact ≒ 1チャンク」の設計
エンベディングモデルが決まったら、次はチャンキング戦略だ。
チャンキングとは、長い文書を検索に適した小さな単位(チャンク)に分割すること。どのサイズで区切るかが検索精度に直結する。
📘 なぜ長い文書をそのまま使えないのか
長文をまるごとベクトル化すると、含まれる多数のトピックが平均化されて「何についても少しだけ似ている」ベクトルになる。特定の話題を検索しても的確にヒットしない。1つのトピックに集中した短い単位に分割することで、検索の精度が上がる。
最初に「Semantic Chunking」(意味の切れ目で自動分割)を試した。賢そうに見えたからだ。
しかし、結果を見て凍りついた。1つのArtifact(知識の1単位)が途中でぶった切られている。メタデータは前半のチャンクにだけ紐づき、後半は宙ぶらりん。日本語の段落分割は英語ほど安定しない。Semantic Chunkingにとって日本語は鬼門だった。
この恐怖体験から、設計の方針が決まった。
1Artifact ≒ 1チャンク。これが絶対条件だ。
UKA-GYREのRAGに格納されるのは、昇華バッチ(読み物編第2回参照)が週次で生成するArtifactで、1件あたり600〜800字程度。
チャンキング設定は Fixed-size、1,000トークン、オーバーラップ20% とした。
📘 トークンとオーバーラップ
トークン はLLMがテキストを処理する最小単位。英語では1単語≒1トークンだが、日本語では1文字がおよそ1〜2トークンに相当する。汎用的な多言語モデルでは日本語600〜800字が約600〜1,000トークン程度になるため、1,000トークンあれば1Artifactが1チャンクに収まる計算だ。 オーバーラップ はチャンク分割時の重複領域のこと。前のチャンクの末尾が次のチャンクの先頭に重なることで、分割境界での文脈断絶を緩和する。
1,000トークンの枠があれば、600〜800字のArtifactが「1チャンク」にきれいに収まる。これが重要だ。
1Artifactが1チャンクに収まるということは、チャンクとメタデータが1対1で紐づく。
後述するメタデータフィルタリングが、チャンクの境界をまたいで壊れるリスクがゼロになる。
固定サイズなら分割ルールは予測可能で、予測可能性は運用の安定性に直結する。
オーバーラップ20%は、チャンク境界での文脈断絶を緩和するためのものだ。前のチャンクの末尾20%が次のチャンクの先頭に重複して含まれる。
7. メタデータスキーマ ―― ベクトル検索を「賢く」する
ベクトル検索は「意味が近い文書」を返す。しかし、「意味が近い」と「状況が同じ」は全くの別物だ。「低脂質で停滞期のクライアント」に対して、「低脂質で減量順調のクライアント」のアドバイスが返ってくることがある。ベクトル的には確かに近いが、状況は正反対だ。
ここで メタデータフィルタリング が登場する。各Artifactに以下のメタデータを付与している。
- Artifactの種類 ―― 成功パターン、失敗パターン、お手本コメント、トレーナーの知見メモ、基本ルール集の5種類
- トレーナーが設定したラベル ―― 長期方針、現在の戦略、ダイエットのフェーズ
- 栄養状態の複合キー ―― 冒頭で設計した内部ラベルの複合キーがここで効いてくる。「カロリーは不足気味だがタンパク質は十分」のように、栄養状態の全体像を表す
これらを組み合わせることで、「低脂質方針×減量期×カロリー不足」のような多軸のフィルタリングが可能になる。
📘 ベクトル検索とメタデータフィルタリングの役割分担
本屋にたとえると、ベクトル検索は「内容が似た本を見つける」機能で、メタデータフィルタリングは「棚(ジャンルや出版年)で先に絞り込む」機能だ。ベクトル検索は意味の近さを見つけるのが得意だが、「方針が同じか」「フェーズが同じか」といった構造的な条件は苦手。メタデータで条件を絞り、その中で意味検索をかけることで的外れな結果を排除する。
7.1. 1,820kcal問題の着地点
ここで冒頭の1,820kcalに戻る。
あの数字は内部ラベル計算で「カロリー:やや不足」に変換された。タンパク質は「十分」、体重トレンドは「減少傾向」。これらが複合キーとして組み合わされ、メタデータフィルタの検索条件になる。
メタデータフィルタがナレッジベースに問いかけるのは、こういう質問だ。
「低脂質方針で、減量期で、カロリーやや不足でタンパク質十分の過去事例はあるか?」
返ってくるのは、 まさにその状況で過去にGood評価を受けたコメントのパターン だ。
「カロリーが少し足りないが、タンパク質は良好な日の声かけ」という、ピンポイントで同じ状況の成功事例。
AIにはこの参考事例と、「カロリー:やや不足」というラベルが渡される。1,820kcalが多いか少ないかを判断する必要はもうない。「やや不足している人に、過去にうまくいった声かけを参考にして書け」。これなら10回やっても10回とも一貫したコメントが返ってくる。
内部ラベル → 複合キー → メタデータフィルタ。
数字の解釈をコードが済ませ、AIには「判断済みの世界」だけを見せる。
8. RAGアーキテクチャの設計哲学
AIに判断させない。AIに何をさせないかを決めることが重要だった。
1,820kcalという数字を見て、AIは「多い」とも「少ない」とも言える。どちらも文章としては正しく、どちらも判断としては信用できない。数値の良し悪しを一貫して判定する能力は、AIの得意領域ではない。
だからUKA-GYREは、判断と表現を明確に分離した。
- 目標との差分計算、ラベルへの離散化、複合キーの生成 --- これらは全てルールベースの機械的処理
- AIに渡すのは「判断済みの結論」だけ
- AIの仕事は、その結論を「この人に響く言葉」に変換することだけ
この分離は、RAGアーキテクチャの全てに波及している。ベクトルストアの選定も、チャンキング戦略も、メタデータスキーマも、 「AIが判断しなくて済むように、事前に情報を構造化する」 という同じ哲学の上にある。得意なことだけをやらせる。それがこのシステムの第一原則だ。
次回は、この検索基盤の上で「100%の検索精度は存在しない」という現実にどう向き合ったかを語る。ティアード検索、ラベルブーストの加算→乗算の転換、Dualクエリによる多様性の確保。「妥協を設計する」という哲学の実装記録だ。
コラム:なぜLLMは「計算」できないのか
LLMが1,820kcalを正しく評価できない理由は、モデルの根本構造にある。
LLMは次のトークン(文字の断片)を確率的に予測する装置であり、内部で四則演算を実行しているわけではない。「1820 ÷ 2200」を見たとき、人間は割り算をするが、LLMは「この文字列の後に続きやすいトークン」を推測しているだけだ。
訓練データに似た計算例が多ければ正答率は上がるが、それは「暗記」であって「理解」ではない。
数値の大小比較すら、桁数が変わると精度が崩壊することが知られている。離散ラベルへの変換は、この根本的な限界を迂回する設計判断だ。
次回の記事:
「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回 ― システムアーキテクチャ総覧(完結)