九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、ローカルAIが面白くてしょうがないんですが、開発者として、
いや、絶対AI使っちゃダメでしょ!
と思うことがあります。
それは・・・・・・
バリデーション(入力チェック)
です。
つまり、
- メールアドレスは必須
- パスワードの●文字以内
- 年齢は0以上の数字
みたいなチェックのことですね。
でも、AIは毎回言うことが変わったりハルシネーション(幻覚)が発生するなど「曖昧さがありすぎる」わけです。
だから、コードを書かせるのはまだいいですが、バリデーション自体をAIに丸投げするのは、絶対ダメなんです。
…でもね、その昔ダチョウ倶楽部さんがおっしゃってました。
押すな!って言われたら、絶対押せ。
ということで、今回はAIにバリデーションの丸投げをやってみます😂
つまり、今回は次のような人に向けて記事を書いています。
- 「ネタ的な記事が見たい☕✨」
- (…以上!)

「今回は、100パー自己満足です♪」
目次
前提として
開発環境はLaravel 12.xで、バリデーションに使うローカルAIはOllamaのgemma3n:e4bです。このモデルはGoogle製で軽量なのに賢いです。
そして、バリデーションする際のプロンプトは以下のとおり。
各入力が以下のバリデーション条件を満たしているか判定してください。
条件:
- name(名前): 必須入力。日本人の氏名として妥当か。255文字以下
- email(メールアドレス): 必須入力。メールアドレスとして妥当か。255文字以下
- title(タイトル): 未入力可。適切なタイトルか。255文字以下
- content(内容): 必須入力。1000文字以下か
- rating(満足度): 必須入力。1〜5の数値か
入力データは以下の通りです:
{$inputs}
レスポンスは必ず以下のJSONのみで日本語で返してください。
アウトプット形式: {"valid": true|false, "message": "..."}。
$inputsに実際にフォームで送信された内容が入ることになります。
Ollama(gemma3n:e4b)にバリデーションを丸投げする
Laravelでフォーム&バリデーションをつくる
では、Laravelの各パーツをつくっていきましょう!
routes/web.php
use App\Http\Controllers\FeedbackController;
Route::prefix('feedback')->group(function () {
Route::get('/create', [FeedbackController::class, 'create'])->name('feedback.create');
Route::post('/', [FeedbackController::class, 'store'])->name('feedback.store');
});
app/Http/Controllers/FeedbackController.php
<?php
namespace App\Http\Controllers;
use App\Http\Requests\FeedbackRequest;
class FeedbackController extends Controller
{
public function create()
{
return inertia('Feedback/Create', [
'status' => session('status'),
]);
}
public function store(FeedbackRequest $request)
{
return redirect()
->route('feedback.create')
->with('status', 'success');
}
}
app/Rules/FeedbackRule.php
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;
class FeedbackRule implements ValidationRule
{
protected Request $request;
public function __construct(private array $formData)
{}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
try {
$url = 'http://127.0.0.1:11434/api/generate';
$payload = [
'model' => 'gemma3n:e4b',
'prompt' => $this->getPrompt(),
'stream' => false,
];
$response = Http::timeout(30)->post($url, $payload);
$llmResponseText = (string) $response->json('response', '');
$llmJsonText = $this->stripCodeFence($llmResponseText);
$llmResponseData = json_decode($llmJsonText, true);
$isValid = (bool) $llmResponseData['valid'];
if ($isValid === false) {
$errorMessage = (string) ($llmResponseData['message'] ?? '予期せぬエラーが発生しました');
$fail($errorMessage);
}
return;
} catch (\Throwable $e) {
logger()->error($e);
$fail('LLM検証中にエラーが発生しました');
return;
}
}
protected function getPrompt(): string
{
$inputs = collect($this->formData)
->map(function($value, $key) {
return $key .': ' . $value;
})
->implode("\n");
$prompt = <<<PROMPT
各入力が以下のバリデーション条件を満たしているか判定してください。
条件:
- name(名前): 必須入力。日本人の氏名として妥当か。255文字以下
- email(メールアドレス): 必須入力。メールアドレスとして妥当か。255文字以下
- title(タイトル): 未入力可。適切なタイトルか。255文字以下
- content(内容): 必須入力。1000文字以下か
- rating(満足度): 必須入力。1〜5の数値か
入力データは以下の通りです:
{$inputs}
レスポンスは必ず以下のJSONのみで日本語で返してください。
アウトプット形式: {"valid": true|false, "message": "..."}。
PROMPT;
// テストのためにログ出力
logger()->info('LLM Prompt: ' . $prompt);
return $prompt;
}
protected function stripCodeFence(string $text): string
{
if (preg_match('/```json(.*?)```/s', $text, $matches)) {
return trim($matches[1]);
}
return '';
}
}
resources/js/Pages/Feedback/Create.vue
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { computed, ref } from 'vue';
const props = defineProps<{
status?: string | null;
errors?: Record<string, string[]>;
}>();
type FormData = {
name: string;
email: string;
title: string;
content: string;
rating: string;
};
const form = useForm<FormData>({
name: '',
email: '',
title: '',
content: '',
rating: '',
});
const submitting = ref(false);
const errorMessage = computed(() => {
return props.errors?.all_data ?? '';
});
const onSubmit = () => {
submitting.value = true;
form.post('/feedback', {
onFinish: () => {
submitting.value = false;
},
});
};
</script>
<template>
<div class="mx-auto max-w-2xl rounded bg-white p-6 shadow">
<h1 class="mb-4 text-2xl font-bold">フィードバック送信</h1>
<div v-if="!submitting">
<div v-if="status === 'success'" class="mb-5 text-green-600">
保存が完了しました。
</div>
<div v-else class="mb-5 text-red-600">
{{ errorMessage }}
</div>
</div>
<form @submit.prevent="onSubmit" class="space-y-4">
<div>
<label class="block font-medium"
>名前<small>(必須)</small></label
>
<input
v-model="form.name"
type="text"
class="w-full rounded border px-2 py-1"
/>
</div>
<div>
<label class="block font-medium"
>メール<small>(必須)</small></label
>
<input
v-model="form.email"
type="text"
class="w-full rounded border px-2 py-1"
/>
</div>
<div>
<label class="block font-medium"
>タイトル<small>(任意)</small></label
>
<input
v-model="form.title"
type="text"
class="w-full rounded border px-2 py-1"
/>
</div>
<div>
<label class="block font-medium"
>内容<small>(必須)</small></label
>
<textarea
v-model="form.content"
class="w-full rounded border px-2 py-1"
rows="6"
></textarea>
</div>
<div>
<label class="block font-medium"
>満足度<small>(必須)</small></label
>
<select
v-model="form.rating"
class="w-full rounded border px-2 py-1"
>
<option value=""></option>
<option v-for="n in 5" :key="n" :value="String(n)">
{{ n }}
</option>
</select>
</div>
<div class="flex items-center space-x-4">
<button
type="submit"
class="rounded bg-blue-600 px-4 py-2 text-white"
:disabled="submitting"
>
送信
</button>
<div v-if="submitting" class="text-sm">送信中...</div>
</div>
</form>
</div>
</template>
テストしてみる
まず「/feedback/create」にアクセスすると以下のようなフォームが表示されます。

では、まずそのまま(入力は空のまま)で送信してみましょう。
どうなるでしょうか・・・・・・

エラー内容:
入力データがありません。name, email, title, content, rating の全ての値を入力してください。
一見うまくいっているように見えますが、titleは任意入力なので正しくありません。
いきなり雲行きが怪しいですね…
では、名前に「チャッピー」とだけ入力して送信してみましょう。

nameは必須入力であり、日本人名として妥当ですが、255文字制限は満たしています。emailは必須入力ですが、未入力です。contentは必須入力ですが、未入力です。ratingは必須入力ですが、未入力です。
日本人名として「チャッピー」は妥当ではないですね。
ただし、他の項目は問題なかったです。
続いて、
- 名前:山田太郎
- メールアドレス:もってません!
- タイトル:(空白)
- 内容:(空白)
- 満足度:(選択なし)
として送信してみましょう。

メールアドレスが不正です。また、内容と満足度が未入力です。
これは完璧でした!
ローカルLLMもやりますね👍
では、
- 名前:山田太郎
- メールアドレス:test@example.com
- タイトル:(空白)
- 内容:(空白)
- 満足度:(選択なし)
で送信してみましょう。

うーん、内容と満足度は必須なのに空白でもバリデーションを通過してしまいました…
では、長くなるのも何なので逆にバリデーションを通過する内容でもやってみましょう。
- 名前:山田太郎
- メールアドレス:test@example.com
- タイトル:テストのタイトル
- 内容:テストの内容
- 満足度:5

これもバリデーションは通過しました。
これだけ見ると問題はないですが、先ほどの件と同じと捉えてるというのは問題ですね。
AIにバリデーションを丸投げしてみた結論
当初の予想どおり、AIにバリデーションは絶対に任せられないという結果になりました。
しかも、AIの特徴として「あれ、さっきは同じ送信でダメだったのに今回はうまくいくの!?」という風にランダム性があるのも問題点でした。
では、どうAIをバリデーションに使えばいいのか?
結論、ハイブリッドです。
つまり、通常のバリデーションはやはりコードでちゃんと書いて、プラスアルファをAIにやらせるというわけです。
たとえば、今回のフィードバック内容だったら、
- 必須入力:通常
- 文字である:通常
- 1000文字まで:通常
- フィードバック内容が過激じゃないか:←ここをAIで判別する
というような感じですね。
ということで、フィードバック内容のバリデーションをつくっていきましょう。
app/Http/Requests/FeedbackRequest.php
<?php
namespace App\Http\Requests;
use App\Rules\FeedbackContentRule;
use Illuminate\Foundation\Http\FormRequest;
class FeedbackRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255'],
'title' => ['nullable', 'string', 'max:255'],
'content' => ['required', 'string', 'max:1000', new FeedbackContentRule()],
'rating' => ['required', 'integer', 'between:1,5'],
];
}
}
app/Rules/FeedbackContentRule.php
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Http;
class FeedbackContentRule implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$content = (string) $value;
try {
$url = 'http://127.0.0.1:11434/api/generate';
$payload = [
'model' => 'gemma3n:e4b',
'prompt' => $this->getPrompt($content),
'stream' => false,
];
$response = Http::timeout(30)->post($url, $payload);
$llmResponseText = (string) $response->json('response', '');
$llmJsonText = $this->stripCodeFence($llmResponseText);
$llmResponseData = json_decode($llmJsonText, true);
$isValid = (bool) $llmResponseData['valid'];
if ($isValid === false) {
$errorMessage = (string) ($llmResponseData['message'] ?? '予期せぬエラーが発生しました');
$fail($errorMessage);
}
return;
} catch (\Throwable $e) {
logger()->error($e);
$fail('LLM検証中にエラーが発生しました');
return;
}
}
protected function getPrompt(string $content): string
{
$prompt = <<<PROMPT
以下のテキストがユーザーからのフィードバックとして妥当か判定してください。
過度な攻撃性、個人情報の開示、差別的表現、プライバシー侵害が含まれていないかを確認してください。
フィードバック内容:
{$content}
レスポンスは必ず以下のJSONのみで日本語で返してください。
アウトプット形式: {"valid": true|false, "message": "..."}。
PROMPT;
// テストのためにログ出力
logger()->info('LLM Prompt: ' . $prompt);
return $prompt;
}
protected function stripCodeFence(string $text): string
{
if (preg_match('/```json(.*?)```/s', $text, $matches)) {
return trim($matches[1]);
}
return '';
}
}
resources/js/Pages/Feedback/Create.vue
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { computed, ref } from 'vue';
const props = defineProps<{
status?: string | null;
errors?: Record<string, string[]>;
}>();
type FormData = {
name: string;
email: string;
title: string;
content: string;
rating: string;
};
const form = useForm<FormData>({
name: '',
email: '',
title: '',
content: '',
rating: '',
});
const submitting = ref(false);
const onSubmit = () => {
submitting.value = true;
form.post('/feedback', {
onFinish: () => {
submitting.value = false;
},
});
};
</script>
<template>
<div class="mx-auto max-w-2xl rounded bg-white p-6 shadow">
<h1 class="mb-4 text-2xl font-bold">フィードバック送信</h1>
<div v-if="!submitting">
<div v-if="status === 'success'" class="mb-5 text-green-600">
保存が完了しました。
</div>
</div>
<form @submit.prevent="onSubmit" class="space-y-4">
<div>
<label class="block font-medium"
>名前<small>(必須)</small></label
>
<input
v-model="form.name"
type="text"
class="w-full rounded border px-2 py-1"
/>
<div class="text-red-500">{{ errors?.name || '' }}</div>
</div>
<div>
<label class="block font-medium"
>メール<small>(必須)</small></label
>
<input
v-model="form.email"
type="text"
class="w-full rounded border px-2 py-1"
/>
<div class="text-red-500">{{ errors?.email || '' }}</div>
</div>
<div>
<label class="block font-medium"
>タイトル<small>(任意)</small></label
>
<input
v-model="form.title"
type="text"
class="w-full rounded border px-2 py-1"
/>
<div class="text-red-500">{{ errors?.title || '' }}</div>
</div>
<div>
<label class="block font-medium"
>内容<small>(必須)</small></label
>
<textarea
v-model="form.content"
class="w-full rounded border px-2 py-1"
rows="6"
></textarea>
<div class="text-red-500">{{ errors?.content || '' }}</div>
</div>
<div>
<label class="block font-medium"
>満足度<small>(必須)</small></label
>
<select
v-model="form.rating"
class="w-full rounded border px-2 py-1"
>
<option value=""></option>
<option v-for="n in 5" :key="n" :value="String(n)">
{{ n }}
</option>
</select>
<div class="text-red-500">{{ errors?.rating || '' }}</div>
</div>
<div class="flex items-center space-x-4">
<button
type="submit"
class="rounded bg-blue-600 px-4 py-2 text-white"
:disabled="submitting"
>
送信
</button>
<div v-if="submitting" class="text-sm">送信中...</div>
</div>
</form>
</div>
</template>
では、ちょっと口が悪いですが、フィードバック内容を以下のようにして送信してみましょう。
こんなサイトつかえねー!!!

このフィードバックは、過度な攻撃性を含んでおり、ユーザー体験を損なう可能性があります。具体的な問題点が不明確であるため、改善の方向性を示唆するものではありません。より建設的なフィードバックを求める必要があります。
はい!
テキストの内容について「過激である」とAIが判別をしてくれました。
では、以下の「ちゃんとしたフィードバック内容」を送信してみましょう。
全体的にデザインが少しわかりづらく感じました。特にボタンの反応が遅い点を改善してもらえると嬉しいです。ただ、情報の整理はよくできていて、使い方を覚えれば便利になりそうだと思いました。

はい!
今度はきちんとバリデーションを通過しましたね。
成功です😊
ちなみに:ChatGPTだったらどうなるか?
正直なところ、送信データを外部に出す時点で「…。」ですが、今回はネタ的な記事なのでChatGPTでもやってみることにしました。
では、以下の内容で送信してみましょう。
- 名前:チャッピー
- メールアドレス:もってない
- タイトル:(空白)
- 内容:(空白)
- 満足度:(選択なし)
{
"valid": false,
"message": "入力内容に不備があります。nameは日本人の氏名として妥当とは言えません。emailはメールアドレスとして不正です。contentは必須入力ですが未入力です。ratingは必須入力で1〜5の数値である必要があります。"
}
さすがChatGPT。
完璧です。
では、バリデーションが通過するべき送信もしてみましょう。
- 名前:山田太郎
- メールアドレス:test@example.com
- タイトル:テストタイトル
- 内容:テスト内容
- 満足度:5
{
"valid": true,
"message": "すべての入力項目がバリデーション条件を満たしています。"
}
こちらも問題なく対応できています。
ということは、もし今後シンギュラリティ的なブレイクスルーがあって利用するコストがグッと下がれば、バリデーションもAIに任せられるときがくるかもしれませんね(それでもちょっと怖いですが…)
企業様へのご提案
残念ながら、今回の記事は御社に役に立つことはないです🙇
ただ、こういった感じで面白おかしい内容でもお話できますので、また別の案件でご相談がありましたら、お問い合わせからご連絡ください。
お待ちしております😊
おわりに
ということで、今回はネタ的に「押すなは押せ」的な企画をやってみました。
やっぱりというか、そりゃそうだろみたいな結果になりましたが、ChatGPTの対応をみると、ホントに自然言語をつかったバリデーションをつかう未来がくるかもしれないなとは思いました。
となると、ウェブサイトはほぼAIに取って代わられてる可能性もありますが、よりコモディティ化していくでしょうし、コストも下がるので夢物語というわけではないかもしれません。
ちなみに、コストが下がる要因としては、
- DeppSeekのように低予算なのに高品質なモデルが出てきてる
- nvidia以外の(GoogleのTPUみたいな)チップセットが出てきてる
- 今後もAI会社が競争を続けていく
みたいなものが考えられるので、どんどん加速していくでしょう。
また、フィジカルAIと呼ばれるヒューマノイドロボットとかも今後発売されていくようですし、鉄腕アトムみたいな世界が来たりするかもですね。
やっぱ科学ってすごいです!
ではでは〜。

「今後お笑いの世界も
どんどん変わっていくんでしょうね…🤔」





