利害関係の開示:筆者は API ゲートウェイ Kunavo を運営しています。この記事は実測記録をもとに、AI の支援を使って執筆・翻訳しました。ワークフローは OpenAI 互換の Chat Completions 接続を使います。別のサービスでは Base URL、モデル名、対応パラメーターの確認が必要です。
フォームの自由記述から氏名やメールアドレスを取り出す処理を LLM に任せると、たいていはうまくいきます。ただ、ときどき次のような問題が起きます。
- コードフェンス付きの JSON が返ってくる
- 存在しないメールアドレスをもっともらしく作ってしまう
n8n でこれを「検証 → 1 回だけ再試行 → それでもだめなら人に聞く」という形にしたので、実測の数字と一緒に紹介します。
全体の流れ
Form Trigger → Basic LLM Chain(JSON で返させる)→ Code(検証)→ IF
├ OK → 完了ページ
└ NG → 再試行チェーン(元の文・前回の出力・エラーを渡す)→ Code(検証)→ IF
├ OK → 完了ページ
└ NG → Form ページ(送信者本人に足りない項目を入力してもらう)→ 完了ページ
ポイントは 3 つです。
-
検証はモデルに任せない:Code ノードで JSON として読めるか、必須キーがあるか、メールの形式、
intentの値が列挙のどれかを確かめます。 -
再試行は 1 回まで:2 回目には、エラー文をそのまま渡します(例:
"intent" must be one of buy, support, partnership, other.)。1 件あたりのモデル呼び出しは最大 2 回です。 - 検証に失敗したら人に戻す:フォームの次のページで送信者本人に不足項目を入力してもらいます。ただし、形式が正しい架空の氏名やメールアドレスは、この検証を通る可能性があります。形式検証は事実性を保証しません。実運用では原文との照合や本人確認も必要です。
検証ノード(抜粋)
const INTENTS = ['buy', 'support', 'partnership', 'other'];
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
return $input.all().map((item) => {
const raw = String(item.json.text ?? '');
const errors = [];
let data = null;
try {
// 指示しても ``` で囲んでくるモデルがあるので剥がす
data = JSON.parse(raw.replace(/^\s*```(?:json)?\s*/i, '').replace(/\s*```\s*$/, ''));
} catch (e) {
errors.push('The answer is not valid JSON.');
}
if (!data || typeof data !== 'object' || Array.isArray(data)) {
errors.push('The answer must be a JSON object.');
} else {
if (data.email != null && !EMAIL.test(String(data.email))) errors.push(`"email" is not an email address: ${data.email}`);
if (!INTENTS.includes(data.intent)) errors.push(`"intent" must be one of ${INTENTS.join(', ')}.`);
if (!data.name && !data.email) errors.push('Neither a name nor an email was found.');
}
return { json: { valid: errors.length === 0, errors, data, raw } };
});
エラー文は英語のまま、再試行のプロンプトに入れます。モデルが直すべき箇所を名指しできるからです。
実測(n8n 2.41.4、モデル Claude Sonnet 5、2026-10-01)
| ケース | 結果 | モデル呼び出し | 1 回あたりの token(入力 / 出力) |
|---|---|---|---|
| 氏名・会社・メールを含む文 | 全項目正しく抽出 | 1 | 208–244 / 86–95 |
| 氏名もメールもない文(「昨日の障害の件で折り返し電話がほしい」) | 検証 NG → 再試行 → まだ NG → 本人入力ページ | 2 | 再試行は 191–242 / 274–394 |
2 つ目のケースが大事です。モデルは再試行でも名前を作らず、検証はそれを通しませんでした。そして本人に聞く画面まで進みました。「再試行すれば必ず埋まる」と考えて作ると、ここで捏造を受け入れることになります。
つまずいた点
-
Base URL は利用するサービスの API パスまで指定する。今回の接続先では
/v1が必要でした。トップページの HTML が 200 で返るような URL は、API の正常動作を確認したことにはなりません。接続テストだけでなく、実際に 1 件実行して JSON の応答まで確認します。 - temperature などの任意パラメーターは接続先で確認する。今回検証した Claude Sonnet 5 の接続先では、既定以外の値が 400 で拒否されました。これは今回の接続先での観測で、他社のサービス全般についての主張ではありません。
-
人の確認ステップは誰に見せるかを決めてから作る。フォーム起点のワークフローで Wait(フォーム再開)を使うと、その次のフォームはフォームの送信者に表示されます。社内レビュー用なら Webhook 起点にして、レビュー用リンク(
$execution.resumeFormUrl)をレビュー担当者にだけ送ります。
ワークフロー
インポート用の JSON は認証情報を外した状態で GitHub リポジトリ から配布しています。n8n 公式テンプレートライブラリへの掲載とは別の配布です。
Kunavo で試す場合の設定:n8n 連携ドキュメント