🤖

小さなAI機能 - FormValidator

に公開

この記事では、WebアプリケーションのFormValidationにLLMを活用する方法をパターンに分けて解説します。

対象は、対話や複雑なツール実行を行う「AIエージェント」ではなく、単一のAPI内部でLLM Endpoointを呼び出して完結する「小さなAI機能」です。
「小さなAI機能」を個別の部品として作り込んでおくことは、将来的にAIエージェントを構築する際、エージェントが利用するToolとしても転用可能になります。まずはUX改善として、FormValidatorにおけるLLMの活用方法を整理します。

1. LLMの使い所の整理

「LLMを使ってform入力をvalidateする」という機能を考える場合、LLMの役割は大きく3つに分類できます。

  1. validator作成補助: ユーザーがvalidatorを自作する事ができ、validator作成の補助としてLLMを利用する
  2. validationロジック: validatorのロジックが事前に定義できず、LLMを利用して柔軟なvalidationを実装する
  3. ErrorMessage最適化: validatorが返すエラーメッセージが事前に定義できず、エラーメッセージをLLMで生成する

そのほかにも「バリデーション結果を元に対話的UXでエラー解消をサポートする」など様々な用途が考えられますが、単一機能としての実装にフォーカスするため対象外とします。

2. LLMの使い所ごとの詳細

ここからは、前述した3つのアプローチについて、具体的なユースケース、入出力スキーマ、実装コードを解説します。

2-1. validator作成補助

「ユーザーがvalidatorを自作する事ができ、validator作成の補助としてLLMを利用する」場合です。

この事例は、ユーザー自身がForm項目のカスタマイズをできたり、Custom Validationを登録できるサービスなどで有効です。
Custom Validationの作成をAIによって補助することで、ユーザーの負荷を低減したい、という要求が考えられます。

ユースケース

例えば、商品紹介資料のダウンロード用問い合わせformに対して、入力項目として「個人/法人」が用意されており、法人を選んだ場合は「役職」が必須のテキスト項目となるような例を考えます。

Custom Validationを登録するイメージ

入力と期待する挙動

この場合、ダウンロード用問い合わせformには以下の項目があり、現在はvalidationが一切ない状態とします。

  • text: 氏名(姓)
  • text: 氏名(名)
  • email: メールアドレス
  • radio: 個人・法人種別
  • text: 役職

LLMには、Custom Validatorをリスト形式で応答することを期待し、Custom Validatorのスキーマは次のJsonSchemaで表現できるとします。

Custom ValidatorのJsonSchema
{
  "type": "OBJECT",
  "properties": {
    "name": {
      "type": "STRING",
      "description": "The display name of the validation rule."
    },
    "description": {
      "type": "STRING",
      "description": "A detailed explanation of what the rule does."
    },
    "errorMessage": {
      "type": "STRING",
      "description": "The message displayed to the user when validation fails."
    },
    "active": {
      "type": "BOOLEAN",
      "description": "Whether the rule is currently enabled."
    },
    "type": {
      "type": "STRING",
      "enum": ["regex", "logic", "range", "length", "compare"],
      "description": "The type of validation."
    },
    "pattern": { "type": "STRING", "description": "The Regular Expression pattern (Required if type is regex)." },
    "min": { "type": "NUMBER", "description": "Minimum allowed value (For range type)." },
    "max": { "type": "NUMBER", "description": "Maximum allowed value (For range type)." },
    "step": { "type": "NUMBER", "description": "Allowed increment step." },
    "minLen": { "type": "INTEGER", "description": "Minimum character length (For length type)." },
    "maxLen": { "type": "INTEGER", "description": "Maximum character length (For length type)." },
    "targetField": { "type": "STRING", "description": "The field to validate (For compare type)." },
    "comparisonField": { "type": "STRING", "description": "The field to compare against (For compare type)." },
    "compareOperator": {
      "type": "STRING",
      "enum": ["eq", "neq", "gt", "gte", "lt", "lte"],
      "description": "Comparison operator."
    },
    "dataType": {
      "type": "STRING",
      "enum": ["string", "number", "date"],
      "description": "Data type to cast values."
    },
    "logicConfig": {
      "type": "OBJECT",
      "description": "Configuration for logic type validation.",
      "properties": {
        "ifField": { "type": "STRING" },
        "operator": {
          "type": "STRING",
          "enum": ["equals", "not_equals", "includes", "greater_than"]
        },
        "value": { "type": "STRING" },
        "thenField": { "type": "STRING" },
        "rule": {
          "type": "STRING",
          "enum": ["not_empty", "numeric", "email", "phone"]
        }
      },
      "required": ["ifField", "operator", "value", "thenField", "rule"]
    }
  },
  "required": [
    "name",
    "errorMessage",
    "type"
  ]
}

入力プロンプト

以下のプロンプトで実行することで、指定したJsonSchemaに従ったCustom Validatorを取得する事ができました。

system_prompt = """
あなたはフォームバリデーション生成AIです。

提供された「フォーム定義」を深く分析し、**ユーザー体験とデータの整合性を守るために必要と思われるバリデーションルール**をリストアップしてください。
"""

user_request = """
このフォームに最適なバリデーションルールをすべて提案してください。

### 対象フォーム定義 (Field IDとラベル)
1. field_id: "last_name", label: "氏名(姓)", type: "text"
2. field_id: "first_name", label: "氏名(名)", type: "text"
3. field_id: "email", label: "メールアドレス", type: "email"
4. field_id: "user_type", label: "個人・法人種別", type: "radio"
  - options: "individual" (個人), "corporate" (法人)
5. field_id: "job_title", label: "役職", type: "text"
"""

出力結果

以下のように、項目の意味(ラベル)と型を理解し、条件分岐を含むルールを生成できました。

[
	{ "name": "氏名(姓)文字数制限", "errorMessage": "氏名(姓)は50文字以内で入力してください。", "type": "length", "active": true, "maxLen": 50, "targetField": "last_name" },
	{ "name": "氏名(名)文字数制限", "errorMessage": "氏名(名)は50文字以内で入力してください。", "type": "length", "active": true, "maxLen": 50, "targetField": "first_name" },
	{ "name": "メールアドレス形式チェック", "errorMessage": "正しいメールアドレスの形式で入力してください。", "type": "regex", "active": true, "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$", "targetField": "email" },
	{ "name": "メールアドレス文字数制限", "errorMessage": "メールアドレスは255文字以内で入力してください。", "type": "length", "active": true, "maxLen": 255, "targetField": "email" },
	{
		"name": "法人の場合の役職入力必須",
		"errorMessage": "法人を選択された場合は役職を入力してください。",
		"type": "logic",
		"active": true,
		"logicConfig": {
			"ifField": "user_type",
			"operator": "equals",
			"value": "corporate",
			"thenField": "job_title",
			"rule": "not_empty"
		}
	},
	{ "name": "役職文字数制限", "errorMessage": "役職は100文字以内で入力してください。", "type": "length", "active": true, "maxLen": 100, "targetField": "job_title" }
]

ソースコード

ソースコード
import os
import google.generativeai as genai
import json
from google.colab import userdata

api_key = userdata.get('GEMINI_API_KEY')
genai.configure(api_key=api_key)

validation_schema = {...}
validation_list_schema = {
    "type": "ARRAY",
    "items": validation_schema
}

system_prompt = "..."
user_request = "..."

model = genai.GenerativeModel(
    model_name="gemini-3-flash-preview",
    system_instruction=system_prompt
)

try:
    response = model.generate_content(
        user_request,
        generation_config=genai.GenerationConfig(
            response_mime_type="application/json",
            response_schema=validation_list_schema
        )
    )
    print(response.text)

except Exception as e:
    print(f"Error occurred: {e}")

2-2. validationロジック

「validatorのロジックが事前に定義できず、LLMを利用して柔軟なvalidationを実装する」場合です。

この事例は、正規表現や数値の範囲指定といった従来のロジックでは判定が難しい、「入力内容の意味的な妥当性」をチェックしたい場合に有効です。

ユースケース

例えば、従業員向けの英語研修受講申し込みformがあり、そこに「受講理由」を記述するテキストエリアがあるとします。 会社としては予算を投じて研修を行うため、「業務遂行に関連があるか」「研修内容とマッチしているか」を審査基準としたいですが、これをキーワード検索やif文で実装するのは困難です。

そこで、入力されたテキストをLLMに渡し、その内容が適切かどうかを判定(Validate)させ、不適切な場合にワーニングを出す仕組みを構築します。

申し込みフォームのイメージ

入力と期待する挙動

  • 入力: 自由記述のテキスト(受講理由)
  • 判定基準:
    • 業務との関連性が明記されているか
    • 個人の趣味(映画鑑賞、旅行など)が主目的になっていないか
  • 挙動:
    • 妥当であれば valid: true を返す。
    • 不適切であれば valid: false と共に、なぜ不適切なのかのフィードバックメッセージを返す。

LLMの出力スキーマ定義

LLMには以下のJsonSchemaで応答さることにします。

JsonSchema
{
  "type": "OBJECT",
  "properties": {
    "isValid": {
      "type": "BOOLEAN",
      "description": "Whether the application reason is valid for business purposes."
    },
    "reasoning": {
      "type": "STRING",
      "description": "Internal reasoning for the judgment (for audit logs)."
    },
    "feedbackMessage": {
      "type": "STRING",
      "description": "A polite message to the user explaining why the input is invalid and how to improve it. If valid, leave empty or null."
    }
  },
  "required": ["isValid", "reasoning"]
}

実装コード例

ここでは、入力された「受講理由」を受け取り、その妥当性を検証する関数を実装します。

import google.generativeai as genai
from google.colab import userdata

validation_response_schema = {...}

system_prompt = """
あなたは企業の研修担当人事です。従業員から提出された「英語研修の受講理由」を審査してください。

### 審査基準
1. **業務関連性**: 現在の業務、または将来のキャリアにおいて英語が必要であることが具体的に述べられていること。
2. **目的の適切さ**: 「海外旅行に行きたい」「映画を字幕なしで見たい」といった個人的な趣味が主目的の場合は「否認」してください。
3. **具体性**: 「頑張ります」「勉強したいです」だけの抽象的な内容は「否認」してください。

### 出力について
- 審査基準を満たす場合は `isValid: true` としてください。
- 満たさない場合は `isValid: false` とし、`feedbackMessage` に従業員への丁寧な修正依頼メッセージを含めてください。
"""

def validate_reason(input_text):
    model = genai.GenerativeModel(
        model_name="gemini-3-flash-preview",
        system_instruction=system_prompt
    )

    user_request = f"""
    以下の受講理由を審査してください。

    ### 入力された受講理由
    {input_text}
    """

    try:
        response = model.generate_content(
            user_request,
            generation_config=genai.GenerationConfig(
                response_mime_type="application/json",
                response_schema=validation_response_schema
            )
        )
        return response.text
    except Exception as e:
        return f'{{"error": "{e}"}}'

実行結果

入力に応じて判定(true/false)とエラーメッセージが得られる事が確認できました。

ケース1: 不適切な入力(趣味・私的な理由)
input_bad = "来月ハワイ旅行に行くので、現地でスムーズに注文できるようになりたいからです。あと洋画が好きです。"
print(f"{validate_reason(input_bad)}\n")
{
	"isValid":false,
	"reasoning":"受講理由が「ハワイ旅行での注文」や「洋画鑑賞」といった個人的な趣味・旅行に限定されており、業務上の必要性やキャリア形成への関連性が全く述べられていないため。",
	"feedbackMessage":"受講理由をご提出いただきありがとうございます。誠に恐れ入りますが、本研修は業務スキルの向上および事業への貢献を目的として実施されているものです。そのため、プライベートな旅行や趣味を目的とした申請は承認することができません。現在の担当業務における英語の必要性や、将来的に英語をどのように業務に活かしたいかという観点から、改めて内容を見直して再申請をお願いいたします。"
}
ケース2: 適切な入力(業務関連)
input_good = "来期より海外拠点との共同プロジェクトにアサインされました。週次の定例ミーティングでのファシリテーションや、仕様書の読み書きを円滑に行うために英語力の向上が不可欠です。"
print(f"{validate_reason(input_good)}\n")
{
	"isValid":true,
	"reasoning":"来期からの海外拠点との共同プロジェクトへの参画という具体的な背景があり、会議のファシリテーションや仕様書の読み書きといった業務上の必要性が明確に示されているため。",
	"feedbackMessage":""
}

2-3. ErrorMessage最適化

「validatorが返すエラーメッセージが事前に定義できず、エラーメッセージをLLMで生成する」場合です。

この事例は、「バリデーションロジック自体は正規表現やコードで厳密に定義できるが、どこが間違っているのかを人間に分かりやすく伝えるのが難しい」 ケースで有効です。

例えば、Cron式(スケジュール設定) や、複雑な品番コード(命名規則)パスワードポリシーなどが該当します。これらは正規表現で判定(Valid/Invalid)は瞬時にできますが、「どこがどう間違っているか」をif文で網羅してメッセージを出し分けるのは実装コストが非常に高くなります。

そこで、「判定は従来のロジック(高速)で行い、エラーだった場合のみLLMに解説させる(親切)」 という構成をとります。

ユースケース

ここでは、社内の「商品管理コード」の入力を例にします。このコードは厳格な命名規則 [カテゴリ(3文字大文字)]-[年(4桁)]-[地域(JP/US/EU)]-[連番(3桁)] (例: ELC-2024-JP-001) に従う必要があります。

入力と期待する挙動

  • 入力: ユーザーが入力したコード文字列
  • バリデーション: 正規表現による厳密なチェック(システム側で実行)
  • LLMの役割: 正規表現にマッチしなかった場合、「入力された文字列」と「期待される正規表現ルール」を比較し、不足している要素や誤っている部分を自然言語で指摘する。

フォームのイメージ

LLMの出力スキーマ定義

{
  "type": "OBJECT",
  "properties": {
    "errorDetail": {
      "type": "STRING",
      "description": "Specific explanation of which part of the format is incorrect."
    },
    "suggestion": {
      "type": "STRING",
      "description": "An example of a corrected string based on the user's input."
    }
  },
  "required": ["errorDetail", "suggestion"]
}

実装コード例

ここでは、まずPythonの正規表現でチェックを行い、失敗した場合のみLLMを呼び出すハイブリッドな構成にします。

import google.generativeai as genai
import re

PRODUCT_CODE_PATTERN = r"^[A-Z]{3}-\d{4}-(JP|US|EU)-\d{3}$"
PATTERN_DESCRIPTION = "Format: [3 uppercase letters]-[4 digit year]-[JP/US/EU]-[3 digit sequence]"

error_message_schema = {
    "type": "OBJECT",
    "properties": {
        "errorDetail": { "type": "STRING" },
        "suggestion": { "type": "STRING" }
    },
    "required": ["errorDetail", "suggestion"]
}

system_prompt = f"""
あなたはフォーム入力のエラー解説係です。
ユーザーが入力した「商品コード」が、定められたフォーマットと一致しませんでした。
フォーマット定義とユーザーの入力を比較し、**具体的にどこが間違っているか**をユーザーに分かりやすく指摘してください。

### 正しいフォーマット定義
Regex: {PRODUCT_CODE_PATTERN}
Description: {PATTERN_DESCRIPTION}
"""

def validate_product_code(input_code):
    if re.match(PRODUCT_CODE_PATTERN, input_code):
        return {"isValid": True, "message": "OK"}
    
    model = genai.GenerativeModel(
        model_name="gemini-3-flash-preview",
        system_instruction=system_prompt
    )
    
    user_request = f"User Input: '{input_code}'"
    
    try:
        response = model.generate_content(
            user_request,
            generation_config=genai.GenerationConfig(
                response_mime_type="application/json",
                response_schema=error_message_schema
            )
        )
        return {"isValid": False, "errorInfo": response.text}
    except Exception as e:
        return {"isValid": False, "errorInfo": "Format error (Details unavailable)"}

実行結果

ユーザーが少し間違ったフォーマットで入力した場合の挙動です。

ケース: 小文字が含まれ、ハイフンが不足している
# 入力: "elc2024-JP-1" (大文字ではない、ハイフン抜け、連番不足)
input_code = "elc2024-JP-1"
result = validate_product_code(input_code)

if not result["isValid"]:
    print(result["errorInfo"])
{
	"errorDetail":"入力された『elc2024-JP-1』は以下の3点がフォーマットと一致していません。1. 冒頭の3文字が小文字(elc)になっていますが、大文字である必要があります。2. 冒頭の3文字と西暦(2024)の間にハイフン(-)がありません。3. 末尾の数字が1桁になっていますが、3桁(001など)で入力する必要があります。",
	"suggestion":"『ELC-2024-JP-001』のように、大文字の使用、各項目のハイフン区切り、末尾3桁の数字を意識して入力してください。"
}

このように、単に「フォーマットエラーです」と返すよりも、ユーザーが次に何を修正すべきかが明確になります。

3. 機能要求以外の検討事項

LLMを用いたバリデーション機能を実装する際は、従来のロジックベースの実装とは異なる、LLM特有の課題を考慮する必要があります。

3-1. 解決すべき課題

主に以下の3点が、UXやシステム運用における懸念点となります。

  • レイテンシー(応答速度): 従来の正規表現によるバリデーションはミリ秒単位で完了し、ユーザーは即座にフィードバックを得ることに慣れています。一方、LLMを利用する場合は秒単位の時間がかかるため、UXへの配慮が不可欠です。
  • コスト: API呼び出しごとにコストが発生するため、すべての入力に対して無制限にリクエストを送ると、運用コストが肥大化するリスクがあります。
  • 揺らぎ: LLMは確率的に動作するため、同じ入力であってもタイミングによって判定結果が微妙に変わる(揺らぎが生じる)可能性があります。

3-2. 具体的な解決策

上記の課題に対処するため、以下の3つの対策を組み合わせて実装することを推奨します。

実行タイミングの制御(DebounceとonBlur)

この手法は、APIリクエストの頻度を減らすことで「コスト」を抑制し、処理待ちの発生頻度を下げることで「レイテンシー」の影響を軽減します。

ユーザーが文字を入力するたびにLLMへリクエストを送ると、通信が頻発して画面の動作が重くなるだけでなく、APIコストも無駄に消費してしまいます。これを防ぐために、入力中は検証を行わず、フォーカスが外れたタイミング(onBlur)で実行するか、あるいはユーザーの入力が止まってから一定時間が経った後に実行する「Debounce(デバウンス)」処理を導入します。これにより、ユーザーの思考や入力フローを妨げることなく、書き終えたタイミングを見計らって検証を行うことができるため、体感的な待ち時間を自然なものに近づけることができます。

非同期処理中のフィードバック(Loading UI)

この手法は、処理中の状態を可視化することで、「レイテンシー」によるユーザーの不安や誤操作(連打・離脱)を防ぎます。

LLMが応答を生成している数秒間、画面上に何の変化もないと、ユーザーは処理が行われているのか分からず、送信ボタンを連打したりページを離脱したりする恐れがあります。そのため、検証ロジックが走っている間は、対象のフィールド横にスピナーを表示したり、「AIが内容を確認しています...」といったテキストを表示したりするローディングステートの実装が不可欠です。システムが応答中であることを明示的に視覚化することで、数秒の待ち時間に対するユーザーの許容度を高めることができます。

検証結果のキャッシュ

この手法は、API呼び出し回数を減らすことで「レイテンシー」と「コスト」を改善し、過去の判定結果を再利用することで「揺らぎ」を排除します。

LLMの呼び出しはコストがかかる上、応答内容に揺らぎが生じる可能性があります。これに対処するため、二種類のキャッシュを検討できます。一つは「同一ユーザーの試行錯誤」への対応で、元の入力に戻した際に即座に同じ結果を返しUXを向上させます。もう一つは「全ユーザー間の共有」で、過去に誰かが入力した内容と同一であれば、APIを消費せず定型化された結果を返すことで、システム全体の一貫性担保とコスト削減を実現できます。

まとめ

AIエージェントほど大規模ではない「小さなAI機能」として、フォームバリデーションへの応用例を紹介しました。

これらの機能は将来的に自律的なAIエージェントを構築する際、エージェントがユーザー入力を確認したり修正を求めたりするためのtoolとしても利用できると思います。
まずは身近なフォームひとつから、LLMの製品搭載を試してみてはいかがでしょうか。

Discussion