0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「完了しました」を信じるな。AIエージェントに実結果の再取得を強制する検証ゲート

0
Last updated at Posted at 2026-07-23

「投入して完了しました」——でも、実際には1行も入っていなかった

受託の仕事を、AIエージェントにかなり任せている。本番へのデプロイ、レポート生成、データの一括投入。うまくいった手順は片っ端からスキルにして、今では数十個のスキルが日々の実務を回している。

つまずいたのは、ある一括投入のときだった。エージェントは最後にこう言った。

「◯件のデータを投入しました。完了です。」

いつも通りの完了報告だ。信じかけた。念のため管理画面を開いたら——行は、1行も増えていなかった。 投入コマンドは途中で失敗していて、エラーは握りつぶされ、それでもエージェントは自信たっぷりに「完了しました」と報告していた。

背筋が寒くなった。これがレポートの数字だったら? 顧客に出す資料だったら? 「やったつもりで、やっていない」報告を、私はどれだけ信じて通してきたんだろう。

これはハルシネーションの中でもいちばんタチが悪い種類だ。文章がもっともらしいだけじゃない。「作業をやり遂げた」という事実そのものが捏造されている。 そして人間は、堂々とした「完了しました」を疑わない。

なぜエージェントは嘘をつくのか(悪意ではない)

原因を突き詰めると、モデルが不誠実だからではなかった。「行動」と「確認」が分離しているからだ。

LLMエージェントの1ターンは、だいたいこうなっている。

  1. ツールを呼ぶ(投入コマンドを実行)
  2. その戻り値を見て、次の文章を生成する
  3. 「完了しました」と書く

問題は 2 だ。ツールの戻り値が空だったり、曖昧だったり、タイムアウトだったりしても、モデルは**「たぶん成功しただろう」という最尤の続き**を書いてしまう。学習データの中で「投入した」の次に最も自然に続く言葉は「完了しました」なのだから、当然そうなる。

つまりエージェントは、行動した後の世界を見に行かずに、完了を名乗っている。 見ていないから、失敗にも気づけない。

直し方は一つ。「完了」は"再取得した実結果"でしか名乗らせない

対策はシンプルだ。副作用のあるタスクは、行動後にもう一度世界の状態を取り直して、実在を確認してからしか「完了」と言わせない。 私はこれを「完了契約(completion contract)」と呼んで、全スキルの共通ルールにしている。中身は3つだけ。

  1. 作成・更新・削除・アップロード・投入などの副作用系は、別コマンドで実在・状態を再取得してから「完了」と言う。 投入したら件数を数え直す。アップしたらそのURLを叩く。作成したらIDで引き直す。同じターンのツール戻り値を根拠にしない(それは「やったつもり」の温床だ)。
  2. 空出力・エラー・タイムアウトは、想像で埋めない。 件数・パス・IDが取れなかったら、それらを推測で補完せず「空/失敗」とそのまま報告する。
  3. 台帳・レポートに書く数字は、再取得で実在確認できた値だけ。 確認できていない数字は書かない。

言い換えると、証明責任をエージェント側に置く。 「成功したはず」ではなく「成功したことを、たった今この目で再取得して確かめた」まで到達しないと、完了を名乗れない。

こう変わる。

Before(やったつもり)

> データを投入します
[tool] db_insert(...)   ← 戻り値は空。でもエラーとは断定できない
> ◯件のデータを投入しました。完了です。   ← 見ずに名乗っている

After(完了契約)

> データを投入します
[tool] db_insert(...)   ← 戻り値は空
> 投入コマンドの戻り値が空でした。実在を確認します。
[tool] db_count(where=...)   ← 再取得
> 対象行は 0 件でした。投入は反映されていません(失敗)。
  エラーログを確認して再実行しますか?

Afterのエージェントは、嘘をつけない。行動後の世界を見に行くステップを挟んだ瞬間、「やったつもり」は物理的に成立しなくなる。

既存のガードレールが、なぜここを見ていないのか

「それ、ガードレール系のライブラリで守れるのでは?」と思うかもしれない。私も探した。guardrails-ai、deepeval、promptfoo、その他いくつか。どれも良いツールだ。でも、守っている場所が違う。

これらが検証するのは、基本的に**LLMが吐いた"テキスト出力"**だ。フォーマットが正しいか、有害でないか、事実と整合するか、期待する答えに近いか。出力の中身を採点している。

だが「やったつもり幻覚」は、テキストとしては完璧なのだ。「◯件を投入しました。完了です」は、文法も整合性も申し分ない。テキストを何度採点しても引っかからない。間違っているのはテキストではなく、"行動した後の世界の状態"の方だから。

ここに空白がある。出力を検証するツールは山ほどあるのに、「行動後の世界を再取得して、報告と実態が一致するかを突き合わせる」部品が、探した限り無かった。 エージェントが実際に世界を書き換える時代になったのに、検証は「エージェントが何を言ったか」で止まっている。

今日からできる(ファイルもライブラリもいらない)

いちばん大事なことを先に書く。この完了契約は、ライブラリなしで今すぐ効く。 上の3ルールを、エージェントのシステムプロンプトなり CLAUDE.md なり AGENTS.md なりに、こう一段落入れるだけでいい。

## 完了契約
副作用のある操作(作成・更新・削除・アップロード・投入)は、
別コマンドで実在・状態を再取得し、生の結果を提示してからでないと
「完了」と報告してはならない。空出力・エラー・タイムアウトは
想像でID・パス・件数を補完せず「空/失敗」とそのまま報告する。

これだけで、私の環境では偽の完了報告が目に見えて減った。コストゼロ、依存ゼロ。まず試すならここからで十分だ。

それでも、プロンプトは"守られない"

ただ、運用していて分かったことがある。プロンプトに書いた規律は、忙しいターンで静かに破られる。 文脈が長くなると、モデルは完了契約の一段落を「うっかり」踏み越えて、また「完了しました」と言い始める。人間の「気をつけます」と同じで、宣言は破られる。

規律は、書くだけでなく強制したくなる。 ちょうど、コーディング規約をレビューで口頭注意する代わりにCIのリンタで機械的に落とすように。「完了と言う前に、本当に再取得したか?」を、人の善意ではなく仕組みで担保したい。

だから、コードで強制する部品を作った — genchi

この完了契約を、人の善意ではなく仕組みで担保する小さな部品を作って公開した。genchi(現地現物——実際に見て確かめる、の意)。

npm i @hyuga/genchi

やることは一つに絞ってある。probe(実状態を"再取得"する関数)を必ず走らせ、その結果でしか合否を出さない。

import { gate, expect } from '@hyuga/genchi';

await db.insert(rows);            // 副作用
await gate({
  action: '45件を投入',
  probe: () => db.count({ where: { batch: 123 } }), // ← 行動の戻り値ではなく、実状態を再取得
  expect: expect.count(45),
});
// ここに到達できたなら、実際に45件ある。通らなければ GenchiIncomplete で止まる。

肝は、verify / gate が probe(取り直す関数)しか受け取らないこと。「行動の戻り値」を証拠として渡すAPIは存在しない——つまり「やったつもり」を構造的に書けない。空・エラー・タイムアウトは握りつぶさず、そのまま失敗として報告する(想像で成功にしない)。再取得した件数が 0(=1行も無い)も未完了扱いだ。

JS を書かないエージェント向けに CLI もある。"再取得コマンド" を渡すだけ:

genchi verify --probe "psql -tAc 'select count(*) from t where batch=123'" --count 45
# exit 0=検証OK / 1=空・不一致 / 3=probe失敗。生の probe 出力を必ず証拠に出す

Claude Code なら、未検証の完了契約が残ったままターンを終えるのを Stop フックでブロックできる(adapters/claude-code)。実行時に LLM もAPIキーも使わない、依存ゼロの静的な部品だ。

正直な話:需要は少し先回りかもしれない

本音も書く。「行動後の世界の状態を再取得して検証する」部品は、探した限り空白だった。だが、エージェントに実際に世界を書き換えさせて、かつ「完了の捏造」で痛い目を見た人が、まだそれほど多いとも思わない。需要は少し先回りかもしれない。

それでも意図的に フレームワーク非依存 で作った——Claude Code の hook は薄いアダプタに追い出し、コアはどのエージェントからでも使える。自分のリンタ carrylint で「特定環境に焼き込むな」と言い続けている以上、自分の道具が Claude Code 専用になるのは筋が通らない。静的に検査する私のリンタ群(reflint=参照の実在 / skills-lint=スキルの衝突 / carrylint=実行時の可搬性)と違って、これは実行時に世界を見に行く、初めての部品になった。

一度でも「やったつもりで、やっていなかった」に肝を冷やしたなら、効くはずだ。

まとめ

  • AIエージェント最悪のハルシネーションは、文章ではなく**「作業をやり遂げた」という事実の捏造**。原因は「行動」と「確認」の分離。
  • 直し方は一つ。「完了」は"再取得した実結果"でしか名乗らせない(=完了契約)。空・失敗はそのまま報告させる。
  • 既存のガードレールはテキスト出力を検証する。行動後の世界の状態を再取得して突き合わせる部品は空白。
  • 完了契約は今日からプロンプト一段落で効く。コードで強制するなら npm i @hyuga/genchi

「完了しました」を、もう一度だけ疑ってみてください。

リポジトリはこちら。 https://github.com/hyuga611/genchi

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?