エージェントに「次の記事の候補を JSON で返して」と頼んだら、ほとんどの場合はきれいな JSON が返ってきます。ほとんどの場合は、です。週に一度くらい、説明文の前置きが混ざったり、末尾がカンマで終わったり、level に "beginner-intermediate" という存在しない値が入ったりします。
問題は、その壊れた一件が後続の自動処理に流れ込むことです。私は個人開発で複数のサイトを一人で運用していて、エージェントの出力を受けて MDX を組み立てる処理が動いています。JSON のパースが失敗すれば、その回の生成はまるごと無駄になります。賢いモデルでも、出力の境界に検証を置かない限り、この事故はゼロになりません。
エージェント出力は「信頼できない入力」である
最初に持つべき心構えは、エージェントの出力を外部からの入力と同じ扱いにすることです。
Web フォームから来た値を検証なしで DB に入れる人はいません。それなのに、生成 AI の出力はなぜか無検証で後段に渡してしまいがちです。出力が言語的に流暢だと、つい構造も正しいと錯覚するからです。流暢さと正しさは別物です。境界に検証レイヤを一枚置くだけで、後段のコードは「正しい形だけが来る」前提で書けるようになります。
JSON Schema で「形」を契約にする
まず、後段が期待する形をスキーマとして明文化します。プロンプトのなかの曖昧な日本語ではなく、機械が判定できる契約にします。
ARTICLE_PLAN_SCHEMA = {
"type" : "object" ,
"required" : [ "title" , "slug" , "category" , "level" , "tags" ],
"additionalProperties" : False ,
"properties" : {
"title" : { "type" : "string" , "minLength" : 8 , "maxLength" : 60 },
"slug" : { "type" : "string" , "pattern" : "^[a-z0-9-]+$" },
"category" : { "enum" : [ "agents" , "integrations" , "tips" , "editor" ]},
"level" : { "enum" : [ "beginner" , "intermediate" , "advanced" ]},
"tags" : {
"type" : "array" ,
"items" : { "type" : "string" },
"minItems" : 2 ,
"maxItems" : 6 ,
},
},
}
enum で level を3値に固定し、pattern で slug にピリオドや大文字が混ざらないようにしています。additionalProperties: False も重要で、モデルが気を利かせて余計なキーを足してきたら弾きます。スキーマは、プロンプトに書いた「お願い」を、破れない「約束」へ格上げする道具です。
壊れた出力を捨てる前に、一度だけ直させる
検証に失敗したとき、すぐに諦めて生成全体を捨てるのはもったいない選択です。多くの場合、壊れ方は軽微です。だから、失敗の内容をモデルに返し、修正を一度だけ求めます。
import json
from jsonschema import Draft7Validator
def validate_or_repair (agent, prompt: str , schema: dict , max_repair: int = 2 ) -> dict :
"""検証に通るまで、回数制限つきで自己修復させる"""
validator = Draft7Validator(schema)
raw = agent.run(prompt)
for attempt in range (max_repair + 1 ):
text = extract_json(raw) # 前後の地の文を剥がす
try :
data = json.loads(text)
except json.JSONDecodeError as e:
errors = [ f "JSON 構文エラー: { e.msg } (pos { e.pos } )" ]
else :
errors = [ f " { list (err.path) } : { err.message } "
for err in validator.iter_errors(data)]
if not errors:
return data # 検証通過
if attempt == max_repair:
break
# 失敗内容を具体的に伝えて修正を求める
raw = agent.run(
"前回の出力は次の理由で無効でした。"
"スキーマに合うよう、JSON だけを返してください。 \n "
+ " \n " .join( f "- { m } " for m in errors)
+ f " \n\n 前回の出力: \n{ raw } "
)
raise SchemaRepairFailed(errors)
要点は2つあります。ひとつは、エラーメッセージを具体的に返すことです。「無効です」とだけ伝えても直りません。「level が beginner-intermediate ですが、許される値は beginner / intermediate / advanced です」と伝えれば、次の試行でほぼ直ります。もうひとつは max_repair で回数を縛ることです。これがないと、直らない出力に対してリペアが無限に走り、コストだけが膨らみます。
リペアの前に置く小さな前処理
リペアループに入る前に、機械的に直せる崩れは前処理で吸収しておくと、リペア回数が目に見えて減ります。
import re
def extract_json (raw: str ) -> str :
"""コードフェンスや前置きを剥がし、JSON 本体だけを取り出す"""
# ```json ... ``` のフェンスを除去
fenced = re.search( r "``` (?: json ) ? \s * ( \{ . *? \} | \[ . *? \] )\s * ```" , raw, re. DOTALL )
if fenced:
return fenced.group( 1 )
# 最初の { から最後の } までを素朴に切り出す
start, end = raw.find( "{" ), raw.rfind( "}" )
if start != - 1 and end != - 1 :
return raw[start : end + 1 ]
return raw.strip()
私の手元では、検証失敗の原因のおよそ 6 割が「JSON の前後に説明文が付く」ことでした。この前処理を入れただけで、リペアまで進む件数が体感で 3 分の 1 以下になりました。モデルにお願いするより、機械的に剥がせるものは機械で剥がすほうが速くて確実です。
失敗したときに何を残すか
リペアを上限まで試しても直らないことは、現実には起きます。そのときの設計が、運用の安心感を決めます。
本番運用では、私は失敗時に次の3つを必ず行うようにしています。
その回の生成をスキップして、後段の処理を止めないこと。1件の失敗で夜間バッチ全体が落ちる作りにはしません。
最後の生出力と検証エラーをログへ残すこと。翌朝、何が起きたかを5分で把握できます。
同じ入力で3回連続して失敗したら通知を出すこと。一過性の揺れと、構造的な問題を区別するためです。
この3点を、リペアの実装より先に決めておくことを強く推奨します。失敗そのものをゼロにするより、失敗したあとに落ち着いて回避できる状態を作るほうが、長く運用するうえでは効いてきます。落とし穴はむしろ「直そう」と粘りすぎることのほうにあります。
def safe_plan (agent, prompt, schema):
try :
return validate_or_repair(agent, prompt, schema)
except SchemaRepairFailed as e:
log.warning( "schema repair failed" , errors = e.errors, prompt = prompt[: 200 ])
return None # 呼び出し側は None を見てスキップする
None を返してスキップへ倒すか、例外で止めるか。ここは扱う処理の性質で選びます。記事の下書きのように「1件落としても次がある」ものはスキップ、決済のように「落としてはいけない」ものは止める。同じリペアの仕組みでも、失敗の倒し方は用途で変えるべきだと考えています。
どこまで自動で直し、どこから人間に渡すか
リペアループは便利ですが、万能ではありません。回数を増やせば直る確率は上がるものの、3回で直らない出力は、たいてい5回でも直りません。プロンプト側の指示が曖昧か、スキーマ自体が現実と合っていないか、原因はモデルの外にあることが多いのです。
だから私は max_repair を 2 に固定しています。2回で直らなければ、リペアを増やすのではなく、スキーマかプロンプトを見直す合図だと受け取ります。自動修復は「軽い崩れを黙って直す」ためのもので、「設計の不備をごまかす」ためのものではありません。
AdMob 向けアプリの設定値をエージェントに組ませるときも、この境界設計をそのまま使っています。エージェントを賢くする工夫と同じくらい、エージェントの出力を信用しすぎない工夫が、長く安定して回す鍵になります。私自身、この境界を一枚置いてから、夜間バッチが朝に止まっている回数が目に見えて減りました。
お読みいただきありがとうございました。後段を壊さない境界の置き方として、参考になれば幸いです。