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_fields、present_fields、missing_fields、source_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_fields と present_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-onlyreview-onlyeffect-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,
};
}
この RetrievalCheck が empty や insufficient のときは、回答を止めるか、限定モードに落とします。
重要なのは、「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 として扱う
一部だけ読めた状態を、完全に読めた状態として扱うと事故ります。
partial は parsed ではありません。
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_fields と present_fields から不足を判定し、normal / limited / blocked を返すだけでも始められます。
AI に「読んだつもり」をさせない。
それだけで、本番の LLM アプリケーションはかなり扱いやすくなります。
補足: DEGRADE との関係
本記事の partial / limited / blocked という分け方は、以前書いた「DEGRADE(保留)を設計するとエージェントが“業務”になるオレオレ設計パターン」を暗黙に参照しています。
入力が足りない、根拠が薄い、添付を読めていない、検索結果が不十分である。
こうした状態を単純に失敗や拒否として扱うのではなく、「何が足りないのか」「どの条件なら再開できるのか」を明示して保留する、という考え方です。
Parse Guard は、その DEGRADE 的な考え方を LLM アプリケーションの入力検証に寄せたものです。
Discussion