プログラマのための IT 教科書

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] → 回答
RAG がうまくいかない時の切り分け

「回答の精度が低い」と言われた時、原因は2つのどちらかです。

1. 検索が悪い   → そもそも正しい文書が取れていない
2. 生成が悪い   → 正しい文書を渡しているのに、答えが変

必ず分けて確認してください。 手順はこうです。

  1. 検索結果だけを取り出して、人間が見て正解が含まれているかを確認する
  2. 含まれていないなら、検索側の問題(チャンクの切り方、埋め込み、検索方式)
  3. 含まれているのに答えが変なら、プロンプト側の問題

デバッグの技術のデバッグと同じで、範囲を半分に切るのが基本です。

チャンク(文書の分割)の設計は、地味ですが効きます。

□ 細かすぎる → 文脈が失われ、意味が通らない断片になる
□ 大きすぎる → 無関係な部分が混ざり、コストも増える
□ 表・コード・見出しの構造を壊さない切り方をする
□ 出典(どの文書のどこか)を必ず一緒に持つ ← 回答に出典を示すために必要

ツールを使わせる(エージェント)

LLM に「関数を呼ぶ」判断をさせることができます。

利用者「先月の売上を教えて」
   ↓
LLM が getSalesReport(month='2026-07') を呼ぶと判断
   ↓
アプリが実際に関数を実行し、結果を返す
   ↓
LLM が結果を文章にする

便利ですが、ここから危険度が跳ね上がります。

□ 実行できる操作の範囲を、必要最小限にする
□ 破壊的な操作(削除・送信・課金)は、必ず人間の確認を挟む
□ ループの上限を決める(呼び出し回数・時間・コスト)
□ 何を呼んだかを必ずログに残す

AI に与えた権限は、そのまま攻撃者に渡りうる権限です。 この点は次章で詳しく扱います。

評価 — テストの代わりになるもの

これが AI 機能開発の中核であり、最も手を抜かれる部分です。

従来のテスト(テストを書く)は、期待値との一致で判定しました。 LLM の出力は毎回違うので、完全一致では判定できません。

ゴールデンデータセット

入力と、期待する出力(または満たすべき条件)のセットを、まず30〜100件作る

作るのは地味な作業ですが、これが無いと改善しているのか悪化しているのか分かりません。 プロンプトを変えるたびに、勘で判断することになります。

何で判定するか

方法使いどころ
形式の検証JSON スキーマに合っているか(必ず入れる)
完全一致 / 含有分類・抽出のように正解が決まっているもの
人手の評価最も信頼できるが、高コスト。定期的に少量
LLM による評価大量に回せる。ただし評価者も間違える
1件の例で判断しない

「この例だとうまくいきました」でプロンプトを変更するのは、 1件のテストだけ通してリリースするのと同じです。

プロンプトの変更は、ある入力を改善して別の入力を悪化させます。 必ずデータセット全体で前後を比較してください。

これは CI に組み込めます(CI/CD)。 プロンプトを変更する PR で評価を自動実行し、 スコアが下がったら気づけるようにします。

オフラインとオンライン

オフライン評価: リリース前に、データセットで測る
オンライン評価: 本番で、実際の利用者の反応を測る
                (フィードバックボタン、途中離脱、再質問の割合)

本番のログこそが、最良のデータセットの源です。 うまくいかなかった実例をデータセットに追加していくと、 評価が現実に近づいていきます。

観測とログ

監視とオンコールの可観測性が、そのまま必要です。加えて AI 固有の項目があります。

□ 入力プロンプトと出力(ただし個人情報の扱いに注意。エンジニアと法律)
□ 使ったモデルとバージョン
□ トークン数とコスト
□ レイテンシ
□ 検証に失敗した割合、再試行した割合
□ ツールを呼んだ場合、何を呼んだか
モデルは勝手に変わる

外部の API を使う場合、モデルは更新されます。

昨日まで期待どおりだった出力が、モデル更新で変わることがあります。

□ モデルのバージョンを明示的に固定する(可能な場合)
□ モデルのバージョンをログに残す
□ 更新時は、評価データセットで比較してから切り替える

「何も変えていないのに挙動が変わった」時、 デバッグの技術の思い込みの外し方を思い出してください。 自分のコードだけが変数ではありません。

失敗を前提に設計する

□ API が落ちた / レート制限に当たった → どう振る舞うか
□ 出力が検証に通らない → 何回まで再試行するか、その後どうするか
□ 応答が遅い → 待たせるのか、諦めるのか
□ 明らかにおかしい回答 → 利用者が報告できる導線があるか

そして、ハルシネーションを前提にした UX が必要です。

□ 出典を示す(RAG なら、参照した文書へのリンク)
□ 「AI による生成です」と明示する
□ 重要な操作の前に、人間の確認を挟む
□ 取り消せるようにする

LLM を使うべきでない場面

流行っているから使う、は最悪の理由です。

向かない理由
金額の計算決定的に、正確に計算すべき(ビットとバイト)
権限の判定確率的に判断してはいけない(認証と認可の実装)
一意な検索・集計SQL のほうが速く、正確で、安い
形式が決まった変換正規表現やパーサで足りる(正規表現)
法的・医療的な判断責任の所在が成立しない(エンジニアと法律・技術者の社会的責任)
まず LLM 無しで解けないか考える

分類タスクなら、キーワードとルールで8割解けることがあります。 その場合、残り2割だけを LLM に回すのが、速く・安く・安定します。

「全部を AI でやる」より「AI でなければ解けない部分にだけ使う」ほうが、 たいてい良い設計です。

問い合わせ内容を5つのカテゴリに自動分類する機能を作ります。設計として最も適切なのはどれですか。

実務の落とし穴まとめ

  1. コストを設計時に計算しない — リリース後に想定の数十倍になる
  2. 出力を検証せずに使う — LLM の出力は外部入力(TypeScript の型)
  3. 自由文で返させて正規表現で抜く — 壊れる。構造化出力を使う
  4. プロンプトをコードとして管理しない — 変更履歴も評価もできない
  5. 1件の例でプロンプトを変える — 他のケースが悪化しても気づけない
  6. 評価データセットを作らない — 改善しているか判断できない
  7. RAG の精度低下を切り分けない — 検索と生成のどちらが原因か分けて見る
  8. モデルのバージョンを固定・記録しない — 勝手に挙動が変わる
  9. エージェントに広い権限を与える — 次章の脅威に直結する
  10. 決定的であるべき処理を 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 Projecthttps://genai.owasp.org/

章末問題

社内文書を検索して答える RAG 機能で、「回答が的外れ」という報告が来ました。最初にすることは?

プロンプトを改善したところ、手元で試した3つの例では明らかに良くなりました。どうしますか。

なお、この章で作った AI 機能そのものが攻撃対象になります。 LLM には従来の脆弱性とは構造的に異なる問題があり、 それは第7部の「AI とセキュリティ」で扱います。

第5部の最後は、もう一度実行すればいい、が通用しない処理—— 在庫・予約・決済のような領域です。 ここまでの章(冪等性・Outbox・突合・金額の扱い)が、すべて必要になります。

読み終わったら記録しておくと、目次で進み具合が分かります。