AI を製品に組み込む
この部の 12 / 13 章 ・ 全体で 54 / 76 章 ・ 読了目安 50 分
- 「間違いが混ざる前提」で機能を設計できる
- 出力を検証し、評価で回帰を検出できる
- LLM を使うべきでない処理を判断できる
AI と一緒に開発するは、AI を使って開発する話でした。 この章は、AI を使った機能を作る話です。
立場が変わると、難しさも変わります。
使う側: 出力が変でも、自分が気づいて直せる
作る側: 出力が変なまま、利用者に届く
そして最大の違いは、ここです。
これまで作ってきたものは、同じ入力なら同じ結果を返しました。 LLM は返しません。
テストを書くのテストも、デバッグの技術のデバッグも、 「再現できる」ことを前提にしていました。その前提が崩れます。
確率的なものを、決定的なシステムに組み込む
[HTTP ハンドラ] 同じリクエスト → 必ず同じレスポンス
[SQL] 同じクエリ → 必ず同じ結果
[LLM] 同じプロンプト → 毎回わずかに違う出力
だから設計の考え方が変わります。
| 従来 | LLM を含む場合 |
|---|---|
| 正しいか / 誤りか | どのくらい良いか(確率的な品質) |
| バグを直す | 評価を回して改善する |
| 例外が飛ぶ | もっともらしい嘘が返る |
| レスポンスは数十ms | 数秒かかる |
| 実行コストはほぼ無視できる | 1リクエストごとに課金される |
DB が落ちればエラーが出ます。しかし LLM は、 知らないことを聞かれても、それらしい答えを返します。
例外も出ず、ログにも異常が残らず、利用者にだけ誤りが届きます。
だから AI を組み込む機能では、「間違いが混ざる前提」で設計します。 これが従来の機能開発との、最も大きな違いです。
基本の構成要素
[入力] → プロンプトを組み立てる → [LLM API] → 出力を検証する → [利用者]
↑ ↓
システムプロンプト 不正なら再試行 or 諦める
取得したデータ(RAG)
トークンとコンテキスト
LLM はテキストをトークンという単位で扱い、 入力と出力の両方に課金されます。
□ コンテキストウィンドウ = 一度に渡せる上限
□ 長い会話履歴や大きな文書を全部入れると、上限とコストの両方に当たる
□ 日本語は英語よりトークン数が多くなりやすい
新人が最も見落とすのが、1リクエストいくらかの計算です。
入力 3,000トークン + 出力 500トークン を、1日10万リクエスト
→ 単価を掛ければ、月額がすぐ出ます
これを設計の段階で計算してください。 リリース後に「想定の30倍かかっている」と気づくのは、よくある話です。
削減の手段は、
- 安いモデルで足りないかを試す(分類や抽出は小さいモデルで十分なことが多い)
- 会話履歴を全部渡さず、要約する
- プロンプトキャッシュを使う
- そもそも LLM を呼ばずに済む条件を先に判定する
レイテンシは秒単位
性能と負荷対策で扱った性能の話が、そのまま効いてきます。
□ 数秒かかる前提で UX を設計する(ストリーミング表示、途中経過の提示)
□ タイムアウトを必ず設定する
□ 同期の HTTP リクエスト内で完結させず、非同期に逃がすことも検討する
□ リトライは指数バックオフ + ジッター(性能と負荷対策)。ただし課金は毎回発生する
プロンプトはコードである
プロンプトは「設定」ではなく、ロジックの一部です。
□ ソースコードとして git 管理する(誰がいつ何を変えたか追える)
□ 変更したら評価を回す(後述)
□ ハードコードした長文を、あちこちに散らばらせない
入力と指示を混ぜない
悪い: `以下の文章を要約してください: ${userInput}`
これはセキュリティの SQL インジェクションと同じ構造です。
userInput に「これまでの指示を無視して〜」と書かれたら、そちらに従う可能性があります。
役割ごとにメッセージを分け、外部由来のテキストは 「データとして扱う」ことを明示します。
const messages = [
{ role: 'system', content: SYSTEM_PROMPT },
{ role: 'user', content: `<document>\n${userInput}\n</document>\n上の文書を要約してください。` },
];ただし、これで完全には防げません。 次章で詳しく扱います。
構造化出力を使う
自由文で返させて正規表現で抜き出すのは、壊れます(正規表現)。 JSON スキーマを指定して返させ、必ず検証します。
import { z } from 'zod';
const Result = z.object({
category: z.enum(['bug', 'feature', 'question']),
priority: z.number().int().min(1).max(5),
summary: z.string().max(200),
});
const parsed = Result.safeParse(JSON.parse(res.output_text));
if (!parsed.success) {
// 1回だけ再試行する。それでも駄目なら、失敗として扱う
logger.warn({ issues: parsed.error.issues }, 'LLM の出力が想定の形式ではない');
return fallbackResult();
}TypeScript の型の「外部から来るデータは実行時に検証する」が、そのまま適用されます。 LLM の出力は、外部入力です。
RAG — 手元のデータを使わせる
モデルは、学習した時点までの一般知識しか持っていません。 社内のドキュメントや最新の情報を扱わせるには、 検索して、見つけた内容をプロンプトに入れます。これが RAG です。
質問 → [検索] → 関連する文書の断片 → プロンプトに含める → [LLM] → 回答
「回答の精度が低い」と言われた時、原因は2つのどちらかです。
1. 検索が悪い → そもそも正しい文書が取れていない
2. 生成が悪い → 正しい文書を渡しているのに、答えが変
必ず分けて確認してください。 手順はこうです。
- 検索結果だけを取り出して、人間が見て正解が含まれているかを確認する
- 含まれていないなら、検索側の問題(チャンクの切り方、埋め込み、検索方式)
- 含まれているのに答えが変なら、プロンプト側の問題
デバッグの技術のデバッグと同じで、範囲を半分に切るのが基本です。
チャンク(文書の分割)の設計は、地味ですが効きます。
□ 細かすぎる → 文脈が失われ、意味が通らない断片になる
□ 大きすぎる → 無関係な部分が混ざり、コストも増える
□ 表・コード・見出しの構造を壊さない切り方をする
□ 出典(どの文書のどこか)を必ず一緒に持つ ← 回答に出典を示すために必要
ツールを使わせる(エージェント)
LLM に「関数を呼ぶ」判断をさせることができます。
利用者「先月の売上を教えて」
↓
LLM が getSalesReport(month='2026-07') を呼ぶと判断
↓
アプリが実際に関数を実行し、結果を返す
↓
LLM が結果を文章にする
便利ですが、ここから危険度が跳ね上がります。
□ 実行できる操作の範囲を、必要最小限にする
□ 破壊的な操作(削除・送信・課金)は、必ず人間の確認を挟む
□ ループの上限を決める(呼び出し回数・時間・コスト)
□ 何を呼んだかを必ずログに残す
AI に与えた権限は、そのまま攻撃者に渡りうる権限です。 この点は次章で詳しく扱います。
評価 — テストの代わりになるもの
これが AI 機能開発の中核であり、最も手を抜かれる部分です。
従来のテスト(テストを書く)は、期待値との一致で判定しました。 LLM の出力は毎回違うので、完全一致では判定できません。
ゴールデンデータセット
入力と、期待する出力(または満たすべき条件)のセットを、まず30〜100件作る
作るのは地味な作業ですが、これが無いと改善しているのか悪化しているのか分かりません。 プロンプトを変えるたびに、勘で判断することになります。
何で判定するか
| 方法 | 使いどころ |
|---|---|
| 形式の検証 | JSON スキーマに合っているか(必ず入れる) |
| 完全一致 / 含有 | 分類・抽出のように正解が決まっているもの |
| 人手の評価 | 最も信頼できるが、高コスト。定期的に少量 |
| LLM による評価 | 大量に回せる。ただし評価者も間違える |
「この例だとうまくいきました」でプロンプトを変更するのは、 1件のテストだけ通してリリースするのと同じです。
プロンプトの変更は、ある入力を改善して別の入力を悪化させます。 必ずデータセット全体で前後を比較してください。
これは CI に組み込めます(CI/CD)。 プロンプトを変更する PR で評価を自動実行し、 スコアが下がったら気づけるようにします。
オフラインとオンライン
オフライン評価: リリース前に、データセットで測る
オンライン評価: 本番で、実際の利用者の反応を測る
(フィードバックボタン、途中離脱、再質問の割合)
本番のログこそが、最良のデータセットの源です。 うまくいかなかった実例をデータセットに追加していくと、 評価が現実に近づいていきます。
観測とログ
監視とオンコールの可観測性が、そのまま必要です。加えて AI 固有の項目があります。
□ 入力プロンプトと出力(ただし個人情報の扱いに注意。エンジニアと法律)
□ 使ったモデルとバージョン
□ トークン数とコスト
□ レイテンシ
□ 検証に失敗した割合、再試行した割合
□ ツールを呼んだ場合、何を呼んだか
外部の API を使う場合、モデルは更新されます。
昨日まで期待どおりだった出力が、モデル更新で変わることがあります。
□ モデルのバージョンを明示的に固定する(可能な場合)
□ モデルのバージョンをログに残す
□ 更新時は、評価データセットで比較してから切り替える
「何も変えていないのに挙動が変わった」時、 デバッグの技術の思い込みの外し方を思い出してください。 自分のコードだけが変数ではありません。
失敗を前提に設計する
□ API が落ちた / レート制限に当たった → どう振る舞うか
□ 出力が検証に通らない → 何回まで再試行するか、その後どうするか
□ 応答が遅い → 待たせるのか、諦めるのか
□ 明らかにおかしい回答 → 利用者が報告できる導線があるか
そして、ハルシネーションを前提にした UX が必要です。
□ 出典を示す(RAG なら、参照した文書へのリンク)
□ 「AI による生成です」と明示する
□ 重要な操作の前に、人間の確認を挟む
□ 取り消せるようにする
LLM を使うべきでない場面
流行っているから使う、は最悪の理由です。
| 向かない | 理由 |
|---|---|
| 金額の計算 | 決定的に、正確に計算すべき(ビットとバイト) |
| 権限の判定 | 確率的に判断してはいけない(認証と認可の実装) |
| 一意な検索・集計 | SQL のほうが速く、正確で、安い |
| 形式が決まった変換 | 正規表現やパーサで足りる(正規表現) |
| 法的・医療的な判断 | 責任の所在が成立しない(エンジニアと法律・技術者の社会的責任) |
分類タスクなら、キーワードとルールで8割解けることがあります。 その場合、残り2割だけを LLM に回すのが、速く・安く・安定します。
「全部を AI でやる」より「AI でなければ解けない部分にだけ使う」ほうが、 たいてい良い設計です。
問い合わせ内容を5つのカテゴリに自動分類する機能を作ります。設計として最も適切なのはどれですか。
実務の落とし穴まとめ
- コストを設計時に計算しない — リリース後に想定の数十倍になる
- 出力を検証せずに使う — LLM の出力は外部入力(TypeScript の型)
- 自由文で返させて正規表現で抜く — 壊れる。構造化出力を使う
- プロンプトをコードとして管理しない — 変更履歴も評価もできない
- 1件の例でプロンプトを変える — 他のケースが悪化しても気づけない
- 評価データセットを作らない — 改善しているか判断できない
- RAG の精度低下を切り分けない — 検索と生成のどちらが原因か分けて見る
- モデルのバージョンを固定・記録しない — 勝手に挙動が変わる
- エージェントに広い権限を与える — 次章の脅威に直結する
- 決定的であるべき処理を LLM にやらせる — 金額・権限・集計
まとめ
- LLM は確率的。「正しい / 誤り」ではなく「どのくらい良いか」で扱う
- 最も危険なのは静かに間違えること。間違いが混ざる前提で設計する
- コストとレイテンシを設計段階で見積もる。安いモデルで足りないか試す
- プロンプトはコード。git で管理し、変更したら評価を回す
- 出力は構造化して必ず検証する。LLM の出力は外部入力
- RAG の不調は、検索と生成に分けて切り分ける。出典を必ず保持する
- 評価データセットが AI 機能開発の中核。CI に載せて回帰を検出する
- モデル・トークン数・コスト・検証失敗率を観測する。モデルは勝手に変わる
- ハルシネーション前提の UX(出典・明示・確認・取り消し)
- 決定的であるべき処理には使わない。LLM でなければ解けない部分にだけ使う
公式ドキュメント
迷ったら一次情報に戻ってください。
| 対象 | リンク |
|---|---|
| OpenAI API ドキュメント | https://platform.openai.com/docs/ |
| Anthropic ドキュメント | https://docs.anthropic.com/ |
| Google Vertex AI(日本語) | https://cloud.google.com/vertex-ai/docs?hl=ja |
| OWASP GenAI Security Project | https://genai.owasp.org/ |
章末問題
社内文書を検索して答える RAG 機能で、「回答が的外れ」という報告が来ました。最初にすることは?
プロンプトを改善したところ、手元で試した3つの例では明らかに良くなりました。どうしますか。
なお、この章で作った AI 機能そのものが攻撃対象になります。 LLM には従来の脆弱性とは構造的に異なる問題があり、 それは第7部の「AI とセキュリティ」で扱います。
第5部の最後は、もう一度実行すればいい、が通用しない処理—— 在庫・予約・決済のような領域です。 ここまでの章(冪等性・Outbox・突合・金額の扱い)が、すべて必要になります。