🧾

Parse Guard:LLMアプリに読んだつもりをさせない入力検証パターン

に公開

はじめに

LLM アプリケーションを作っていると、モデルの回答精度やプロンプト設計に目が行きがちです。

もちろんそれらは重要ですが、本番運用で地味に危ないのは、モデルが入力を十分に読めていないのに、読めた前提で次の処理へ進むことです。

たとえば、次のようなケースです。

  • 添付ファイルを読む前に要約してしまう
  • RAG の検索結果が空なのに、知っている前提で回答する
  • 一部のログしか読んでいないのに、障害原因を断定する
  • 必須フィールドが欠けているのに、申請を処理する
  • ユーザーの発言を部分的にしか解析していないのに、次の action を生成する

これは、いわゆる hallucination とは少し違います。

問題は「モデルが嘘をつくこと」だけではありません。

システム側が、入力を処理済みとして扱ってよい条件を明示していないことです。

この記事では、そのための小さな設計パターンとして Parse Guard を紹介します。

Parse Guard とは何か

Parse Guard は、LLM に渡す前、または LLM の出力を次の処理へ渡す前に、入力が最低限の条件を満たしているかを確認する gate です。

簡単に言うと、こういうものです。

Input

Parse / Extract / Validate

Parse Guard

LLM / Workflow / Action

Parse Guard は、次のような問いを確認します。

  • 入力は存在するか
  • 必須フィールドは揃っているか
  • 添付ファイルや外部文書は本当に読めたか
  • 検索結果は空ではないか
  • 必要な範囲を十分にカバーしているか
  • 失敗しているなら、処理を止めるべきか、限定モードで進めるべきか

重要なのは、LLM に「ちゃんと読んだ?」と聞くことではありません。

LLM に渡す前のシステム側で、読めた状態を構造化して判定することです。

なぜ必要なのか

LLM は、入力が足りないときでも、それらしい出力を作れてしまいます。

これは便利でもありますが、本番では危険です。

たとえば問い合わせ対応で、次のような入力があったとします。

{
  "ticket_id": "T-123",
  "customer_message": "先週の請求について確認したいです",
  "attachments": []
}

この状態で AI に「返信案を作って」と渡すと、モデルは何かしらの返信案を出せます。

しかし、本来は請求情報や契約情報が必要かもしれません。

つまり、問題はモデルの文章生成能力ではなく、その入力だけで次へ進んでよいかです。

ここを明示しないと、システムは読めたつもりで最後まで進みます。

まず Observation Status を持つ

最小設計として、入力に observation_status を持たせます。

type ObservationStatus =
  | "pending"
  | "parsed"
  | "partial"
  | "blocked";

それぞれの意味はこうです。

status 意味 次に進めるか
pending まだ parse / extract が終わっていない 進めない
parsed parse / extract は完了し、充足判定に進める 条件を満たせば進める
partial 一部だけ読めた 限定的に進める
blocked 読めなかった / 権限がない 進めない

ここで大事なのは、parsed をただの雰囲気で付けないことです。

ただし、この記事では parsed を「入力の parse / extract が完了し、充足判定に進める状態」として扱います。
その入力だけで通常処理に進めるかどうかは、required_fieldspresent_fieldsmissing_fieldssource_refs などを見て別途判定します。

つまり、parsed は「読めた気がする」ではありません。
しかし、「すべての業務条件を満たした」という意味でもありません。

最終的に normal で進めるか、limited に落とすか、blocked にするかは、Parse Guard の判定で決めます。

最小スキーマ

たとえば、問い合わせ対応の入力を次のように表せます。

type ParsedInput = {
  input_id: string;
  source_type: "ticket" | "email" | "slack" | "document";
  observation_status: ObservationStatus;
  required_fields: string[];
  present_fields: string[];
  missing_fields: string[];
  source_refs: string[];
  warnings: string[];
};

ここでの missing_fields は、できれば required_fieldspresent_fields から再計算できる値として扱います。

外部から渡された missing_fields をそのまま信用すると、present_fields との不整合が起きる可能性があります。
実装では、guard 側で不足項目を再計算し、記録用に missing_fields を残すくらいの扱いにすると安全です。

以下は例です。

{
  "input_id": "ticket_T-123",
  "source_type": "ticket",
  "observation_status": "partial",
  "required_fields": ["customer_message", "billing_history", "contract_plan"],
  "present_fields": ["customer_message"],
  "missing_fields": ["billing_history", "contract_plan"],
  "source_refs": ["ticket:T-123"],
  "warnings": ["billing context is missing"]
}

この状態なら、AI に「請求について断定的に回答させる」のは危険です。

一方で、「追加確認が必要であることを伝える返信案」を作るだけなら可能かもしれません。

つまり、Parse Guard は単に yes / no を返すだけでなく、どのモードなら進めるかを決める役割を持ちます。

Parse Guard の実装例

以下は、最小の TypeScript 実装です。

type ObservationStatus = "pending" | "parsed" | "partial" | "blocked";

type ParsedInput = {
  input_id: string;
  source_type: "ticket" | "email" | "slack" | "document";
  observation_status: ObservationStatus;
  required_fields: string[];
  present_fields: string[];
  missing_fields: string[];
  source_refs: string[];
  warnings: string[];
};

type GuardDecision =
  | {
      ok: true;
      mode: "normal";
    }
  | {
      ok: true;
      mode: "limited";
      limitations: string[];
    }
  | {
      ok: false;
      reason: string;
    };

function missingRequiredFields(input: ParsedInput): string[] {
  const present = new Set(input.present_fields);
  return input.required_fields.filter((field) => !present.has(field));
}

function parseGuard(input: ParsedInput): GuardDecision {
  const missingFields = missingRequiredFields(input);

  if (input.observation_status === "blocked") {
    return {
      ok: false,
      reason: "input is blocked",
    };
  }

  if (input.observation_status === "pending") {
    return {
      ok: false,
      reason: "input is not parsed yet",
    };
  }

  if (!input.source_refs.length) {
    return {
      ok: false,
      reason: "source_refs are missing",
    };
  }

  if (input.observation_status === "partial") {
    return {
      ok: true,
      mode: "limited",
      limitations: [
        "input is partially parsed",
        ...missingFields.map((field) => `missing field: ${field}`),
      ],
    };
  }

  if (missingFields.length > 0) {
    return {
      ok: true,
      mode: "limited",
      limitations: missingFields.map((field) => `missing field: ${field}`),
    };
  }

  return {
    ok: true,
    mode: "normal",
  };
}

この parseGuard を通すことで、入力が読めていない状態をそのまま次へ流さないようにできます。

limited mode を使う

実務では、入力が完全でないからといって、常に止めればよいわけではありません。

たとえば、次のような処理はできるかもしれません。

  • 不足情報を聞き返す
  • 「確認が必要です」と表示する
  • 追加調査の task を作る
  • draft だけ作る
  • support-only として要約だけする

そこで limited mode を用意します。

type WorkflowMode = "normal" | "limited" | "blocked";

function chooseWorkflowMode(decision: GuardDecision): WorkflowMode {
  if (!decision.ok) return "blocked";
  return decision.mode;
}

limited mode では、LLM に渡す prompt も変えます。

function buildPrompt(input: ParsedInput, decision: GuardDecision): string {
  if (!decision.ok) {
    throw new Error("blocked input must not build prompt");
  }

  const base = `
You are helping draft a response.
Input ID: ${input.input_id}
Source refs: ${input.source_refs.join(", ")}
`;

  if (decision.mode === "limited") {
    return `
${base}

Important:
The input is incomplete.
Do not make definitive claims.
Ask for missing information if necessary.

Limitations:
${decision.limitations.map((x) => `- ${x}`).join("\n")}
`;
  }

  return `
${base}

The input has passed parse guard.
You may draft a normal response based on the available context.
`;
}

ポイントは、入力が不完全なときに、LLM にもその事実を渡すことです。

ただし、prompt に書くだけでは不十分です。

limited mode では、後続の action も制限します。

また、ユーザーに出す文章についても、必要なら出力後の structured check をかけます。

たとえば limited mode なのに断定表現が含まれている、欠けている情報を根拠にした結論を書いている、source ref がない事実を述べている、といった場合は、そのまま表示せず、再生成または人間レビューに戻します。

Parse Guard と action boundary をつなぐ

前の記事では、AI の処理を次の3つに分けました。

  • support-only
  • review-only
  • effect-bearing

Parse Guard は、この境界とも相性がよいです。

たとえば、入力が partial のときは、effect-bearing を禁止します。

type Boundary = "support-only" | "review-only" | "effect-bearing";

function allowedBoundaries(decision: GuardDecision): Boundary[] {
  if (!decision.ok) {
    return [];
  }

  if (decision.mode === "limited") {
    return ["support-only", "review-only"];
  }

  return ["support-only", "review-only", "effect-bearing"];
}

これにより、入力が不完全な状態で外部状態を変更することを防げます。

たとえば、添付ファイルが読めていないのに自動返信する、請求履歴が取れていないのに返金処理を行う、といった事故を防げます。

ここで注意したいのは、limited mode で作った review-only 提案を、後からそのまま effect-bearing に昇格させないことです。

limited mode の提案は、「不完全な入力に基づく提案」です。
そのため、承認されたとしても、実行前には不足情報を埋めて Parse Guard を再実行するか、少なくとも effect-bearing 用の gate で再確認する必要があります。

partial input

limited review-only proposal

missing information resolved

Parse Guard again

effect-bearing gate

これを入れておかないと、入力不完全なまま作られた提案が、人間レビューを経由しただけで実行可能になってしまいます。

RAG でも同じ問題が起きる

Parse Guard は、RAG でも有効です。

RAG では、「検索した」ことと「必要な情報が見つかった」ことが混同されがちです。

検索結果が空でも、LLM は回答できてしまいます。
検索結果が薄くても、LLM はもっともらしい説明を作れてしまいます。

そこで、検索結果にも guard をかけます。

type RetrievedChunk = {
  id: string;
  source: string;
  text: string;
  score: number;
};

type RetrievalCheck = {
  status: "sufficient" | "insufficient" | "empty";
  chunks: RetrievedChunk[];
  reason?: string;
};

function checkRetrieval(chunks: RetrievedChunk[]): RetrievalCheck {
  if (chunks.length === 0) {
    return {
      status: "empty",
      chunks,
      reason: "no retrieved chunks",
    };
  }

  const topScore = Math.max(...chunks.map((chunk) => chunk.score));

  if (topScore < 0.7) {
    return {
      status: "insufficient",
      chunks,
      reason: "retrieval score is too low",
    };
  }

  return {
    status: "sufficient",
    chunks,
  };
}

この RetrievalCheckemptyinsufficient のときは、回答を止めるか、限定モードに落とします。

重要なのは、「RAG を使ったから根拠がある」と見なさないことです。

検索結果そのものも検証対象です。

ここで使っている 0.7 という閾値はあくまで例です。実際には、検索方式、score の意味、対象ドメイン、評価データに合わせて調整します。

ファイル添付でも同じ

添付ファイルを扱う LLM アプリでは、ファイルを受け取ったことと、ファイルを読めたことを分ける必要があります。

type AttachmentParseResult = {
  file_id: string;
  filename: string;
  received: boolean;
  parsed: boolean;
  parse_complete: boolean;
  pages_total?: number;
  pages_parsed?: number;
  error?: string;
};

PDF や Excel では、次のようなことが普通に起きます。

  • ファイルはアップロードされたが、parse に失敗した
  • 一部ページだけ読めた
  • 表や画像が読めていない
  • 文字化けしている
  • パスワード付きで開けない
  • ファイルサイズが大きすぎて途中で切れている

この状態で「添付を確認しました」と言わせるのは危険です。

システム側では、少なくとも次のようにチェックします。

function attachmentFullyParsed(result: AttachmentParseResult): boolean {
  if (!result.received) return false;
  if (!result.parsed) return false;
  if (!result.parse_complete) return false;

  if (
    typeof result.pages_total === "number" &&
    typeof result.pages_parsed === "number"
  ) {
    return result.pages_parsed >= result.pages_total;
  }

  return true;
}

添付が読めていないなら、LLM の回答もそれに合わせるべきです。

添付ファイルは受け取りましたが、内容を確認できていません。

このように言える方が、無理に答えるより安全です。

Guard decision を記録する

Parse Guard の結果は、できれば記録しておきます。

単なるログではなく、「なぜその入力で次に進んだのか」をあとから確認できる形にします。

type ParseGuardRecord = {
  record_id: string;
  input_id: string;
  observation_status: ObservationStatus;
  decision: "allow" | "limit" | "block";
  reason?: string;
  limitations: string[];
  missing_fields: string[];
  source_refs: string[];
  warnings: string[];
  guard_version: string;
  created_at: string;
};

例です。

{
  "record_id": "pgr_001",
  "input_id": "ticket_T-123",
  "observation_status": "partial",
  "decision": "limit",
  "reason": "input is partially parsed",
  "limitations": [
    "missing field: billing_history",
    "missing field: contract_plan"
  ],
  "missing_fields": ["billing_history", "contract_plan"],
  "source_refs": ["ticket:T-123"],
  "warnings": ["billing context is missing"],
  "guard_version": "parse_guard_v1",
  "created_at": "2026-04-29T10:00:00+09:00"
}

これがあると、あとから次のことを確認できます。

  • 入力は完全だったのか
  • 何が欠けていたのか
  • なぜ通常モードではなく限定モードだったのか
  • どの source を見たのか
  • どの時点で判定したのか

AI アプリケーションでは、「何を答えたか」だけでなく、どの入力状態で答えたかが重要です。

よくあるアンチパターン

1. 「モデルに確認させる」だけ

この入力を十分に読めていますか?

と LLM に聞くだけでは不十分です。

LLM は、自分が見えていないものを正確には判定できません。

ファイル parse、検索結果、必須フィールド、権限エラーなどは、システム側で構造化して渡す必要があります。

2. 空の検索結果をそのまま渡す

RAG で検索結果が空なのに、通常回答 prompt に進むのは危険です。

空なら空として扱い、回答を止めるか、限定モードに落とすべきです。

3. partial を normal として扱う

一部だけ読めた状態を、完全に読めた状態として扱うと事故ります。

partialparsed ではありません。

4. UI だけで注意する

画面に「AI の回答は参考です」と書くだけでは、system action は止まりません。

backend 側の gate が必要です。

最小構成

最初から大きな仕組みにする必要はありません。

最低限、次の4つを入れるだけでも変わります。

1. observation_status
2. missing_fields
3. parseGuard()
4. ParseGuardRecord

もう少し実装寄りにすると、こうです。

Raw Input

Parser / Retriever / Extractor

ParsedInput

ParseGuard

normal / limited / blocked

LLM / Workflow

この流れを作るだけで、「読んだつもり」のまま AI が進む事故を減らせます。

まとめ

LLM アプリケーションで危ないのは、モデルが間違えることだけではありません。

入力を十分に読めていないのに、読めた前提で処理が進むことも危険です。

そのためには、入力に対して次を明示する必要があります。

  • 読めたのか
  • 一部だけ読めたのか
  • 読めなかったのか
  • 何が欠けているのか
  • どの source を参照したのか
  • 通常モードで進めるのか
  • 限定モードに落とすのか
  • 止めるのか

Parse Guard は、そのための小さな gate です。

これは大げさな仕組みではありません。

observation_status を持ち、required_fieldspresent_fields から不足を判定し、normal / limited / blocked を返すだけでも始められます。

AI に「読んだつもり」をさせない。

それだけで、本番の LLM アプリケーションはかなり扱いやすくなります。

補足: DEGRADE との関係

本記事の partial / limited / blocked という分け方は、以前書いた「DEGRADE(保留)を設計するとエージェントが“業務”になるオレオレ設計パターン」を暗黙に参照しています。

入力が足りない、根拠が薄い、添付を読めていない、検索結果が不十分である。
こうした状態を単純に失敗や拒否として扱うのではなく、「何が足りないのか」「どの条件なら再開できるのか」を明示して保留する、という考え方です。

Parse Guard は、その DEGRADE 的な考え方を LLM アプリケーションの入力検証に寄せたものです。

Discussion