【授業レポート】システム開発入門 第27週:生成AIを安全に組み込む実装フロー

teacher asking a question to the class
Photo by Max Fischer on Pexels.com

システム開発入門の第27週では、前週に作成した設計図を、生成AI機能を備えた学習用コードへ落とし込みました。

入力を受け取ってから結果を表示するまでを一つの流れとして組み立て、途中で起こり得る失敗への備えも実装しました。

授業のねらい

今回のねらいは、設計書に記した安全対策がコード上でどのように働くかを確かめることです。

田中先生:「図にした設計は“考えの約束”。今日のゴールは、その約束をコードで確かめることです。安全対策は面倒に見えるかもしれないけれど、使う人のための大事な仕事です。」

授業では、外部に公開する鍵を置かず、ログ管理とAPI呼び出し上限を備えた学校の学習用環境を使いました。

入力から表示までの実装フロー

生徒たちは、入力の前処理、モデルの呼び出し、出力の検証、障害時の代替応答、画面表示という順序で処理を組み立てました。

生成AI機能の処理段階と確認した役割
処理段階 役割 授業での確認点
入力の前処理 送信前に入力を整える 個人情報のマスク、禁止語の確認、文字数の制限
モデル呼び出し 整えた入力から応答を受け取る タイムアウトと例外処理
出力検証 表示条件を満たすか判定する 形式、禁止語、事実確認が必要な内容
キャッシュ 同じ入力に対する確認済み応答を一時的に再利用する 保存時間と再利用の条件
フォールバック 通常の応答を返せないときに代替表示へ切り替える タイムアウト、例外、不適切な出力への対応
画面表示 判定結果に応じた内容を利用者へ伝える 注意書きと具体的なエラーメッセージ

この順序にすると、どの段階で入力を止め、どの段階で再試行し、どの段階で人の確認へ渡すのかを追いやすくなります。

授業で扱った擬似コード

次のコードは処理の考え方を学ぶための簡易例です。補助関数の実装は省略されており、そのまま実運用へ使うことは想定していません。認証情報は環境変数や秘密管理の仕組みで扱い、ソースコードへ直接書き込みません。

import ​os
import ​time
from ​typing ​import ​Tuple

CACHE ​= ​{}

def ​cache_get(key):
 ​ ​ ​ ​entry ​= ​CACHE.get(key)
 ​ ​ ​ ​if ​entry ​and ​time.time() ​- ​entry['ts'] ​< ​entry['ttl']:
 ​ ​ ​ ​ ​ ​ ​ ​return ​entry['value']
 ​ ​ ​ ​return ​None

def ​cache_set(key, ​value, ​ttl=300):
 ​ ​ ​ ​CACHE[key] ​= ​{'value': ​value, ​'ts': ​time.time(), ​'ttl': ​ttl}

def ​sanitize_input(text: ​str) ​-> ​str:
 ​ ​ ​ ​text ​= ​mask_email_phone(text)
 ​ ​ ​ ​if ​contains_prohibited(text):
 ​ ​ ​ ​ ​ ​ ​ ​raise ​ValueError('入力に不適切な語句が含まれています。')
 ​ ​ ​ ​return ​text[:1000]

def ​call_model_api(prompt: ​str, ​timeout=5) ​-> ​str:
 ​ ​ ​ ​api_key ​= ​os.getenv('SCHOOL_API_KEY')
 ​ ​ ​ ​if ​not ​api_key:
 ​ ​ ​ ​ ​ ​ ​ ​raise ​RuntimeError('APIキーが設定されていません。')
 ​ ​ ​ ​return ​safe_model_call(prompt, ​timeout=timeout)

def ​validate_output(output: ​str) ​-> ​Tuple[str, ​str]:
 ​ ​ ​ ​if ​not ​matches_expected_format(output):
 ​ ​ ​ ​ ​ ​ ​ ​return ​('retry', ​'出力形式が違います。再生成します。')
 ​ ​ ​ ​if ​contains_prohibited(output):
 ​ ​ ​ ​ ​ ​ ​ ​return ​('fallback', ​'不適切な表現が含まれていました。代替応答を表示します。')
 ​ ​ ​ ​if ​needs_fact_flag(output):
 ​ ​ ​ ​ ​ ​ ​ ​return ​('confirm', ​'一部情報は要確認です。')
 ​ ​ ​ ​return ​('ok', ​output)

def ​get_ai_response(user_input: ​str) ​-> ​str:
 ​ ​ ​ ​try:
 ​ ​ ​ ​ ​ ​ ​ ​clean ​= ​sanitize_input(user_input)
 ​ ​ ​ ​except ​ValueError ​as ​error:
 ​ ​ ​ ​ ​ ​ ​ ​return ​f'入力エラー:{error}'

 ​ ​ ​ ​key ​= ​f'ai:{clean}'
 ​ ​ ​ ​cached ​= ​cache_get(key)
 ​ ​ ​ ​if ​cached:
 ​ ​ ​ ​ ​ ​ ​ ​return ​cached

 ​ ​ ​ ​try:
 ​ ​ ​ ​ ​ ​ ​ ​response ​= ​call_model_api(make_prompt(clean), ​timeout=5)
 ​ ​ ​ ​except ​Exception:
 ​ ​ ​ ​ ​ ​ ​ ​return ​local_fallback_response()

 ​ ​ ​ ​status, ​body ​= ​validate_output(response)
 ​ ​ ​ ​if ​status ​== ​'ok':
 ​ ​ ​ ​ ​ ​ ​ ​cache_set(key, ​body, ​ttl=300)
 ​ ​ ​ ​ ​ ​ ​ ​return ​body
 ​ ​ ​ ​if ​status ​== ​'retry':
 ​ ​ ​ ​ ​ ​ ​ ​try:
 ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​response2 ​= ​call_model_api(
 ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​make_prompt(clean) ​+ ​'\n出力は箇条書きでお願いします。',
 ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​timeout=5,
 ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​)
 ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​status2, ​body2 ​= ​validate_output(response2)
 ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​if ​status2 ​== ​'ok':
 ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​cache_set(key, ​body2, ​ttl=300)
 ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​return ​body2
 ​ ​ ​ ​ ​ ​ ​ ​except ​Exception:
 ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​pass
 ​ ​ ​ ​ ​ ​ ​ ​return ​local_fallback_response()
 ​ ​ ​ ​if ​status ​== ​'fallback':
 ​ ​ ​ ​ ​ ​ ​ ​return ​local_fallback_response()
 ​ ​ ​ ​if ​status ​== ​'confirm':
 ​ ​ ​ ​ ​ ​ ​ ​return ​f'{body}\n※生成結果に重要な事実が含まれています。必ず確認してください。'

各処理が担う役割

サニタイズと入力拒否

この授業でいうサニタイズは、入力に含まれるメールアドレスや電話番号をマスクし、送信する文字列を整える処理です。

一方、禁止語を見つけて処理を止める部分は入力の検証に当たります。

入力を整える処理と、受け付けない入力を判定する処理を分けて考えると、テスト項目を整理しやすくなります。

タイムアウトとフォールバック

タイムアウトは、応答を待つ時間に上限を設ける仕組みです。

フォールバックは、通常の処理を続けられないときに、あらかじめ用意した代替応答へ切り替える仕組みです。

今回の擬似コードでは、モデル呼び出しで例外が起きた場合や、出力に不適切な表現が見つかった場合に代替応答を返します。

出力検証と人による確認

出力検証では、箇条書きなどの期待した形式か、不適切な表現を含まないか、事実確認が必要かを判定しました。

「要確認」の印は正しさを保証するものではなく、人が確認すべき出力を見落としにくくする合図です。

再生成しても条件を満たさない場合は、無理に表示せずフォールバックへ移る設計にしました。

キャッシュの使いどころ

キャッシュは、一度得た応答を一定時間保存し、同じ入力に再利用する仕組みです。

授業の例では、検証結果が「ok」の応答だけを300秒間保存します。

呼び出し回数と待ち時間を抑えられる一方、実運用では保存してよいデータか、いつ破棄するか、情報の更新が必要かを用途ごとに決める必要があります。

ハンズオンで得た気づき

  • 生成AIの応答をそのまま表示せず、検証の流れを用意する必要がある。
  • キャッシュは応答を速くし、API呼び出し回数の管理にもつながる。
  • フォールバックを用意すると、通常処理が失敗しても利用者へ状況を伝えられる。
  • 具体的なエラーメッセージは、利用者だけでなくテスト担当者の原因確認にも役立つ。

生徒たちは、機能が動くことと、失敗時にも利用者を置き去りにしないことを一つの設計課題として捉えました。

教師が示した運用上の確認点

  • 想定外の入力を受け付ける範囲を決め、入力検査で止める条件を明確にする。
  • 人による確認が必要な出力には、見分けられる印を付ける。
  • ログへ残す情報を選び、個人データはマスクして保存期間を短くする。
  • 単体テストと結合テストを用意し、通常時と失敗時の挙動を確かめる。

単体テストは関数などの小さな単位を確かめるテストで、結合テストは複数の処理をつないだときの動作を確かめるテストです。

利用範囲、入力データ、出力確認、ログ管理を実装前に整理する方法は、関連記事「生成AI導入で最初に決めるべき運用ルール」でも確認できます。

擬似コードを実運用へ進める前の確認

学習用コードから実運用の実装へ進む際は、次の点を具体化する必要があります。

  • mask_email_phoneなど、省略された補助関数の判定条件とテストを定める。
  • 例外を一括して処理するだけで終えず、利用者向け表示と調査用ログを分ける。
  • キャッシュへ保存できる情報、保存期間、削除条件を決める。
  • 「要確認」と判定した出力を表示するのか止めるのかを、用途と責任者に合わせて決める。
  • タイムアウト、形式違反、禁止語、再試行の失敗をテストケースに含める。

現在の擬似コードでは、「要確認」の場合に元の応答ではなく注意メッセージだけを返します。

実際の画面では、確認する担当者と確認後の表示方法まで設計すると、処理の行き止まりを防げます。

宿題

  1. 入力、サニタイズ、API呼び出し、出力検証、表示までのフロー図を作成する。
  2. サニタイズで扱った三つのルールと、それぞれを設けた理由を30〜80字で書く。
  3. 自分の実装で想定される失敗を二つ挙げ、対処方法を一つずつ示す。

次週の学習

次週は、作成したシステムを想定した負荷で動かす運用テストと、記録したログから誤応答の頻度や入力傾向を確かめる解析へ進みます。

続きは「システム開発入門 ​第28週目:運用テストとログ解析入門」で紹介しています。

第27週の到達点

生徒たちは、設計をコードに変える作業を通じて、生成AIの便利さだけでなく、入力、出力、失敗時の挙動まで実装する責任を学びました。

安全対策は最後に付け足す機能ではありません。

通常処理と失敗時の処理を同じフローの中で設計し、テストできる形にすることが、安心して使えるシステムへ近づく一歩です。

この記事に関連する株式会社greedenの取り組み

生成AIを組み込むシステムは、モデル呼び出しだけでなく、入力検査、出力確認、障害時の代替処理まで一続きで設計する必要があります。株式会社greedenは、Webシステム開発を要件定義から実装、テスト、保守改善まで支援しています。

投稿者 greeden

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)