ある夜、自作のリリースノート生成エージェントを動かしていて、出力を3回ともそのままコピペしないまま捨てたことがありました。間違いではないのです。ただ、何かが「足りない」。読み返すと、最初の段落だけが妙に他人事のような、Webから拾ってきたような文体になっています。エージェントは正しく動いていて、それでも私はその出力を信じきれませんでした。
このとき気づいたのは、私が欲しかったのは「もう一度生成し直す機能」ではなく、「自分の出力を一度疑ってから提出するエージェント」だったということです。LLMがワンショットで出した最初の答えは、ある種の独白に近い。一方、人間の熟練した書き手は、一度書いた原稿を必ず読み返します。読み返した瞬間に文体が変わり、根拠が補強され、不要な部分が削られる。この「読み返し」を構造として持たないエージェントは、いくらモデルを強くしても、ある一線以上の品質には届かないように感じています。
ここではAntigravity 上で稼働させているエージェントに自己批評ループ(self-critique / reflection)を組み込むための4つの設計パターンを、実装コード・停止条件・コスト制御まで含めて整理します。Reflexion 系の研究を踏襲しつつ、個人開発の小規模プロジェクトでも壊れない形に落とし込んだものです。
なぜ「もう一度生成」では足りないのか
最も素朴な品質改善策は、温度を下げて再サンプリングすることです。temperature=0.2 で再生成して、複数候補から最良を選ぶ。これで一定の改善は得られます。
ただ、再サンプリングが対処できるのは「揺らぎ」だけです。モデルがそもそも誤った前提に立っていたり、要件の一部を読み飛ばしていたりする場合、何度サンプルを取っても同じ盲点を踏みます。「3回振って3回とも違和感のある出力が出てきた」というのは、ノイズではなく構造的な見落としのサインです。
自己批評ループが扱おうとしているのはここから先で、つまり「出力そのもの」ではなく「出力が要件をどう取りこぼしているか」を1段抽象化して評価し、次のサンプルへフィードバックを戻す設計です。
公式ドキュメントには「複数の Sub-agent を立てて検証フェーズを別工程にする」とだけ書かれていることが多いのですが、実際に運用してみると、この「分けるか、一体化するか」「何をもって停止と判定するか」の判断こそが、エージェントの品質を決める一番の勘所でした。
パターン1: Critic-Generator 分離型
最も基本となる構成です。生成役(Generator)と批評役(Critic)を別のプロンプトに分離し、Critic の指摘が「合格」になるまで Generator がリトライします。
# self_critique_loop.py
from google import genai
from pydantic import BaseModel
from typing import Literal
client = genai.Client()
class CriticVerdict(BaseModel):
"""Critic の構造化出力。verdict と issues を分離することで Generator が次に何をすべきか明確になる"""
verdict: Literal["approve", "revise"]
confidence: float # 0.0 - 1.0
issues: list[str] # revise の場合の具体的指摘
GENERATOR_PROMPT = """あなたはリリースノートの執筆者です。
変更内容: {changes}
過去の指摘: {feedback}
過去の指摘がある場合は、それを最優先で反映して書き直してください。"""
CRITIC_PROMPT = """あなたは技術編集者です。以下のリリースノート草稿が
公開して問題ない品質か判定してください。
草稿:
{draft}
要件:
- 利用者目線で「自分にどう影響するか」が分かる
- マーケティング過剰な形容詞がない
- 既知の制約・回避策が漏れていない
verdict が revise の場合、issues に具体的な書き直し方針を 1-3 件挙げてください。"""
def self_critique(changes: str, max_iterations: int = 3) -> tuple[str, list[str]]:
feedback: list[str] = []
history: list[str] = []
for i in range(max_iterations):
# 1. Generator が草稿を出す
gen_resp = client.models.generate_content(
model="gemini-2.5-pro",
contents=GENERATOR_PROMPT.format(
changes=changes,
feedback="\n".join(feedback) if feedback else "(なし)",
),
)
draft = gen_resp.text
history.append(draft)
# 2. Critic が判定する
critic_resp = client.models.generate_content(
model="gemini-2.5-pro",
contents=CRITIC_PROMPT.format(draft=draft),
config={"response_mime_type": "application/json",
"response_schema": CriticVerdict},
)
verdict: CriticVerdict = critic_resp.parsed
if verdict.verdict == "approve" and verdict.confidence >= 0.8:
return draft, history
feedback = verdict.issues
# 上限到達 — 最終草稿を返す(呼び出し側で人間レビューに回す想定)
return history[-1], history
max_iterations を 3 に設定している点が大事で、これは経験則ですが、4 回以上回しても多くのケースで品質はサチります。サチる前にコストだけが線形に増えていくので、停止条件は早めに置いた方が結局健全です。
「max_iterations を上げれば品質も上がる」と最初は私も思っていたのですが、実測してみると逆で、上限を 5 や 7 にすると Critic の指摘自体が暴走し始めることが何度かありました。「もう少し丁寧に」「読みやすさをさらに」と Critic が無限に注文を付け続け、Generator はそれを真に受けて文体が痩せていく。最終的にはリリースノートとは思えない、無味乾燥な文章が出てきます。これは私自身が原稿を書きすぎて疲れたときに陥る状態とそっくりで、「批評にも疲労がある」という感覚を持つようになりました。
パターン2: Self-Reflection 一体型(同一エージェント内)
Critic を別エージェントにせず、生成と内省を同じエージェントの中で連結する形です。エージェントが自分の出力を読み返し、修正案を出してから最終提出する設計で、コストは Generator-Critic 分離型より約 30〜40% 安く済みます。
SELF_REFLECT_PROMPT = """次の手順で回答してください。
ステップ1: 草稿を書く
ステップ2: 自分で読み返し、要件との差分を3項目で指摘する
ステップ3: 差分を反映した最終版を書く
最終提出は<final></final>タグで囲んでください。
依頼内容: {task}"""
def self_reflection(task: str) -> str:
resp = client.models.generate_content(
model="gemini-2.5-pro",
contents=SELF_REFLECT_PROMPT.format(task=task),
config={"thinking_config": {"thinking_budget": 8192}}, # 内省用の思考予算
)
text = resp.text
# <final></final> を抽出
import re
m = re.search(r"<final>(.*?)</final>", text, re.DOTALL)
return m.group(1).strip() if m else text
このパターンが効くのは、要件が明確に言語化できる短いタスク(コミットメッセージ、PR タイトル、API 説明文など)です。逆に、長い文書やマルチステップのコード生成では、自己内で完結する内省は「自分のバイアスごと修正できない」現象が顕著になります。これがパターン3の必要性につながります。
パターン3: 多視点レビュー型(複数 Critic)
ひとつの Critic では拾えない論点を、視点の異なる複数の Critic で多角的に検証する設計です。Antigravity の Manager Surface では、これを Sub-agent として並列起動するのが自然な実装になります。
例として、私が運用しているドキュメント生成エージェントでは、次の 3 視点の Critic を並列で動かしています。
- 技術 Critic: 動作するコード・正しい API 使用法・型整合性
- 編集 Critic: 文体の一貫性・冗長表現・読みやすさ
- 利用者 Critic: 「初見の開発者がこの説明で詰まらないか」を仮想ユーザーとしてシミュレーション
import asyncio
async def multi_perspective_review(draft: str) -> list[CriticVerdict]:
perspectives = [
("技術 Critic", "動作するコード・型整合・API 使用法を厳しく検証してください"),
("編集 Critic", "文体の一貫性・冗長表現・読みやすさを評価してください"),
("利用者 Critic", "プログラミング歴1年程度の開発者として読み、つまずく箇所を指摘してください"),
]
async def run_one(role: str, instr: str) -> CriticVerdict:
prompt = f"あなたは{role}です。{instr}\n\n草稿:\n{draft}"
# ... (CriticVerdict を返す API 呼び出し)
return await call_critic_async(prompt)
return await asyncio.gather(*[run_one(role, instr) for role, instr in perspectives])
3 つの Critic が同時に「approve」を返した場合のみ最終出力とし、ひとつでも「revise」が混じったら、3 つの issues を統合して Generator にフィードバックします。
ここでハマりやすいのが、Critic 同士の指摘が矛盾するケースです。「技術 Critic が冗長と指摘した部分を、利用者 Critic はこのくらい説明があった方が良いと評価する」のような対立は珍しくありません。私はこれを「対立解消役(Resolver)」と呼ぶ追加のエージェントで仲裁する形にしました。Resolver には「3 つの Critic の指摘を読み、矛盾するものは利用者 Critic を優先せよ」という優先順位を明示しておきます。
優先順位を Resolver が知らないと、Generator が宙吊りになります。これは実運用で何度か事故が起きた箇所で、最初は「Critic が多ければ多角的になるだろう」と楽観的に作っていたのですが、視点を増やしただけ意思決定の責任所在が曖昧になり、出力品質が逆に下がる時期がありました。視点を増やすときは必ず「最終決裁者は誰か」を設計に書き込むのが鉄則です。
パターン4: 信頼度ゲート型(Confidence-Gated Critic)
すべての出力に Critic を回すのは、コストが見合わない場合があります。1日に何百件と動くエージェントでは、Critic を呼ぶかどうかを Generator 自身に判定させる設計が現実的です。
class GeneratorOutput(BaseModel):
answer: str
self_confidence: float # 0.0-1.0 で自己評価
risky_points: list[str] # 自分で「ここは怪しい」と思う箇所
def confidence_gated(task: str, threshold: float = 0.85) -> str:
gen_resp = client.models.generate_content(
model="gemini-2.5-pro",
contents=f"次のタスクに回答し、自己信頼度と懸念点を返してください: {task}",
config={"response_mime_type": "application/json",
"response_schema": GeneratorOutput},
)
output: GeneratorOutput = gen_resp.parsed
if output.self_confidence >= threshold and not output.risky_points:
return output.answer
# 信頼度が低い、または懸念点があるときだけ Critic を起動
return run_critic_loop(output.answer, hints=output.risky_points)
このパターンの肝は「自己信頼度のキャリブレーション」です。LLM が出す self_confidence は、そのままだと過信気味(みなが 0.95 を返してくる)か、逆に過小気味(保身的に 0.6 ばかり返す)かのどちらかになりがちです。私の運用では、過去 100 件分の (self_confidence, 実際の人手レビュー結果) を記録して、補正係数をかけてからしきい値判定するようにしています。
最初に threshold=0.85 でリリースしたら、Critic 起動率が 5% を切ってしまい、結果的にすり抜けたバグが本番にいくつか出ました。今は実測値を見ながら 0.7〜0.8 で調整していて、Critic 起動率が 20〜30% になるあたりが私のプロジェクトでは最も効率的だと感じています。
内省の暴走を止める3つの停止条件
自己批評ループで一番怖いのはコスト爆発です。Critic が無限ループに入ると、1 タスクで通常の 10 倍以上のトークンを消費することもあります。次の 3 つの停止条件を必ず併用してください。
- 反復回数上限:
max_iterations を 3 に固定します。これ以上は人間レビューに回します
- トークン予算: タスク開始時に予算を決め、累積使用量が超えたらループ離脱
- 改善停滞検知: 直前 2 回の Critic verdict が同じ issues を返した場合、エージェントは「同じ場所で躓いている」と見なしてループ離脱
def has_stalled(history: list[list[str]]) -> bool:
if len(history) < 2:
return False
return set(history[-1]) == set(history[-2])
改善停滞検知が地味に効きます。Critic は「同じ指摘」をテキストの言い換えで返してくることがあるので、issues を集合として比較するのではなく、埋め込みベクトル類似度(コサイン類似度 0.9 以上を同一視)で判定する実装に切り替えてから、停止判定の精度が大きく上がりました。
本番運用でハマった失敗例
設計パターン以外にも、実装してから「これは事前には気づけなかった」と感じた落とし穴がいくつもあります。痛みの大きかった順に整理しておきます。
ひとつめは Critic プロンプトの劣化です。最初に書いたときは明確な品質基準を込めていたはずなのに、半年後に見返すと、エッジケース対応のための小さな修正が積み重なって長く矛盾した文書になっていました。Generator がそもそも満たせない要求を含んでしまっている、ということが何度かありました。今は Critic プロンプトもコードと同じようにバージョン管理し、修正のたびに変更ログを残し、四半期ごとに「いまでは不要になった条項」を削除する見直しをかけています。Critic を一回作って終わりの成果物ではなく、継続的にメンテナンスするインフラとして扱うのが鉄則です。
ふたつめはタスク間でのフィードバック汚染です。コスト削減のために過去の Critic フィードバックをキャッシュすると魅力的ですが、無関係なタスクへ意図しないフィードバックが混入することがあります。一度、リリースノート用の指摘をキャッシュしていたら、コードコメント生成タスクが「マーケティング調すぎる」という見当違いの理由で却下され続けるという事故を起こしました。フィードバックは原則としてタスク境界をまたがせません。明示的に「全タスクに適用すべき教訓」と判断したものだけを、システムレベルのルールに昇格させる運用にしています。
みっつめは Critic と Generator の暗黙の共謀です。両者が同じ基盤モデルから派生している以上、共通の盲点を持ちます。同じ盲点で意気投合すると、両プロンプトは満たしているのに本来の要件は満たしていない、という奇妙な合意が生まれます。Pattern 1 の分離はこれを部分的にしか防ぎません。私はサンプル抽出した出力に対して、Critic を別モデル系列(例えば Generator が Gemini 2.5 Pro なら、Critic を Claude 系に切り替える)で再評価し、却下率を比較するようにしています。クロスモデル Critic の方が大幅に却下率が高ければ、デフォルト Critic が Generator のバイアスに寄りすぎているサインです。
よっつめは反復による退行です。反復2回目の方が1回目より悪くなる、ということが時々起きます。Critic が「もう少し詳しく」と要求して、Generator が真面目に詳しくしたら、追加した部分のせいで全体構造が崩れた、というパターンです。今は全反復をメモリに残しておき、ループ終了時に「最新の出力」ではなく「Critic が一番高く評価した出力」を選べるようにしています。これだけで反復による劣化のリスクが大きく減りました。
いつつめは易しいケースへの過適合です。収束に成功した出力だけを見て Critic の品質基準を調整すると、収束しなかった難しいケースで何が起きていたかが見えなくなります。私は反復上限・トークン上限に達した「エスカレーション出力」を、収束したものとは別カテゴリで定期的にレビューしています。エスカレーションの中にこそ、ループ自身では直せない構造的な失敗モードが眠っているからです。
パターンの組み合わせ — どこで層を重ねるか
4つのパターンは排他的ではありません。本番では、ほぼすべての場合で複数を重ねて使っています。
低リスク・大量タスク(自動コミットメッセージ、内部ログ要約など)は Pattern 4 単体です。信頼度ゲートで Critic 起動率を低く保ち、起動した場合も Pattern 2 の単一エージェント内省を使うことで、コストを最小化します。
中リスク(リリースノート、ステータスレポート、顧客向けメール下書きなど)は、Pattern 4 をゲートにし、Critic 経路を Pattern 1 の Generator-Critic 分離型にしています。反復上限は2〜3回。これで内省単体より深い検証を入れつつ、コストは抑えられます。
高リスク(公開ドキュメント、ブログ、ブランドを代表する出力)はゲートを通さず、最初から Pattern 3 を起動します。3つの Critic を並列で動かし、Resolver で優先順位を明示します。コストは単純呼び出しの 5〜7 倍になりますが、出力の失敗コストもそれだけ大きいので合理的です。
判断は「どのパターンが最良か」ではなく、「この出力タイプではバグが滑り込んだ場合のコストはいくらか、安いタスクではループのどこを省略できるか」という形になります。この枠組みで考え始めると、層の重ね方はおのずと見えてきます。
評価ハーネス: ループの効果を測る
自己批評ループは「導入して終わり」ではなく、「導入後に本当に品質が上がっているか」を継続的に測る必要があります。Antigravity の評価フレームワークと組み合わせる場合、私は次の 3 指標で追跡しています。
- 人手承認率: 人間レビュアーが「修正なしでマージ」した割合
- 平均反復回数: ループが何周回って合格に至るか
- コスト/承認: 1 件の人手承認を得るために費やしたトークン代
人手承認率は分かりやすい指標ですが、これだけ見るとコストが見えません。逆にコストだけ見ると品質が見えません。「コスト/承認」を主指標にすると、両方の最適化を同時に促せます。私の場合、自己批評ループ導入で人手承認率は 62% → 87% に上がりましたが、コスト/承認は 1.3 倍に増えました。許容範囲かは事業判断になりますが、少なくとも「効いている」のは数字で言えるようになりました。
評価フレームの詳細は AIエージェントの実本番品質保証フレームワーク と 完了検証の設計 を併読すると理解が早いと思います。
実装に入る前に一度通読しておくと、自分の設計がどの研究の文脈に立っているかが見えやすくなります。
「賢く疑うエージェント」を作るということ
LLM の出力を素朴に信じるのではなく、一度疑い、自分で根拠を確認してから提出するという発想です。これは技術的には自己批評ループですが、感覚としては「人間の編集者をエージェント内に住まわせる」ことに近いと感じています。良い文章を書く人は、ほぼ例外なく自分の原稿を疑う癖を持っています。エージェントにも同じ習慣を持たせて初めて、ワンショット生成の限界を超えられる。
ただし、内省は無限に良いものではありません。Critic を増やしすぎれば判断責任が曖昧になり、反復を増やしすぎれば文体が痩せる。「どこで疑うのをやめて、提出を信じるか」を設計者が決め切ることが、最終的にエージェント全体の人格を決めると思っています。
次の一手として、まずは現在運用しているエージェントの出力ログを 50 件ほど取り出し、「これは Critic がいれば防げたか」「これは Critic がいてもすり抜けたか」を手で分類してみてください。どのパターンを採用するかの判断材料が、自分のプロジェクトに固有の形で見えてくるはずです。