結論:構造化出力を指定し、受信後にも検証する

構造化出力にも検証を入れる。出力形式を指定:対応するAPIの構造化出力を利用、スキーマに必要な項目と型を定義する。返却結果を検証:拒否・打ち切り・解析失敗を確認、スキーマに合っても内容は別に確認する。失敗を再処理:失敗理由に応じて再試行や保留、正規表現で本文を黙って切り出さない
XMLを使う場合も、エスケープと構造を検証する。

記事リライトのように title、summary、本文を決まった形で受け取りたい処理では、プロンプトだけで JSON を頼むより、Gemini API の responseMimeType: 'application/json' と responseJsonSchema を使う。これは応答を要求した形に寄せるための仕組みであり、保存前の型・長さ・業務ルールの検証まで置き換えるものではない。

// 完全な最小例: @google/genai を使う(依存版は lockfile で固定する)
import { GoogleGenAI } from '@google/genai';

const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const schema = {
  type: 'object', additionalProperties: false,
  required: ['title', 'summary', 'body'],
  properties: {
    title: { type: 'string', minLength: 1, maxLength: 120 },
    summary: { type: 'string', minLength: 1, maxLength: 240 },
    body: { type: 'string', minLength: 1, maxLength: 50000 }
  }
};

export async function rewriteArticle(source) {
  const response = await ai.models.generateContent({
    model: 'gemini-2.5-flash',
    contents: '次の記事を要約して書き直す。根拠のない事実は追加しない。\n' + source,
    config: { responseMimeType: 'application/json', responseJsonSchema: schema }
  });
  let parsed;
  try { parsed = JSON.parse(response.text); } catch { throw new Error('invalid structured response'); }
  if (!isRevision(parsed)) throw new Error('response failed application validation');
  return parsed;
}

function isRevision(value) {
  return value && typeof value === 'object'
    && typeof value.title === 'string' && value.title.length > 0 && value.title.length <= 120
    && typeof value.summary === 'string' && value.summary.length > 0 && value.summary.length <= 240
    && typeof value.body === 'string' && value.body.length > 0 && value.body.length <= 50000;
}

上の isRevision は説明用の最小検証である。実際の公開処理では HTML の許可リスト、リンクの検査、重複投稿の防止、監査ログも別途必要になる。出力が検証に失敗した場合は、失敗理由と request ID を記録して限定回数だけ再試行し、それでも失敗したものは人手レビューへ送る。

長文で失敗したときに確認する順序

  1. モデル名、SDK版、入力長、出力上限、レスポンス本文を秘密情報を除いて記録する。
  2. JSON Schema の上限が実データと矛盾していないか確認する。
  3. JSON.parse の成功後にアプリ側の検証と保存前の安全検査を通す。
  4. 切断や一時障害には指数バックオフを用い、無限再試行はしない。

過去に XML タグと正規表現で本文を切り出す案を紹介したが、閉じタグの欠落、本文内のタグ文字列、複数ブロック、途中切断を正規表現だけで安全に区別できない。スキーマにできない自由文を扱う互換策として XML を採るなら、XML パーサーで単一ルートと期待要素数を検証し、失敗を値なしとして扱う。タグが見つかっただけで公開本文に使ってはいけない。

計測結果の扱い

以前の失敗率や token 上限変更後の成功率は、母数、モデル版、入力分布、失敗定義を残していないため撤回する。方式を比べる場合は、同じ記事セット、実行日時、モデル、出力上限、試行回数、JSON 構文エラーと業務検証エラーを分けたログを残す。構造化出力は実装上の第一選択であって、無条件に失敗をゼロにする保証ではない。

参考資料