ANTIGRAVITY LABEN
記事一覧/Agents & Manager
Agents & Manager/2026-09-05中級

any_of と書いたカスタムツールは、エラーを出さないまま制約を失います

カスタムツールのパラメータ定義に含まれる綴りゆれのうち、どれが落ちてどれが黙って通るかを 10 種類で確かめました。正規化が入ったあとに引数の合否がどう変わるかと、事前の棚卸し手順もあわせて書き残します。

Antigravity363カスタムツール2JSON SchemaローカルLLM21Ollama13

ローカルの Ollama につないだエージェントが、パスの位置に数値を受け取ったまま平然と先へ進んでいたことに気づいたのは、うまくいかなかった実行のログを遡っていた夜でした。ツール定義には型を書いてあります。それなのに、どこでも弾かれた形跡がありませんでした。

原因はパラメータ定義の中の一語でした。私は any_of と書いていたのです。正しくは anyOf でした。

個人開発でツールを少しずつ足していくと、一度書いたパラメータ定義を読み返す機会はほとんどありません。私の場合も、動いているという事実だけを根拠に、そのまま何か月も置いていました。

厄介なのは、この綴り違いがどこからも警告として返ってこないことです。型を String と大文字で書いたときは即座に落ちるのに、結合子の綴りだけは何事もなかったように通ります。——落ちてくれるほうが、はるかに親切だったのだと気づいたのは、原因にたどり着いたあとでした。

Antigravity の changelog を見ると、2.12.2 でカスタム Python ツールのパラメータ定義が OpenAPI / JSON Schema 準拠の形(型は小文字、結合子はキャメルケース)で生成されるようになり、Ollama・LM Studio・vLLM のような OpenAI 互換のローカルエンジンに接続したときのスキーマエラーが解消される、と書かれています。これは修正としてありがたい変更です。ただ、修正が入るということは、それまで効いていなかった制約が効き始める、という意味でもあります。

以下の数値は、JSON Schema Draft 2020-12 を判定器(Python の jsonschema 4.26.0)として手元で実行した結果です。ローカルエンジン側の実装ではなく、changelog が準拠先として挙げている規格そのもので測っています。

大文字の型は落ちます。スネークケースの結合子は落ちません

同じ「意図」を持つ書き方を 10 種類つくり、それぞれについて 2 つのことを見ました。スキーマ自体がメタスキーマ検証を通るか、そして明らかに不正な引数を渡したときに違反として検出されるか、です。

書き方メタスキーマ検証不正な引数を渡したとき判定
type: "String"(大文字)エラー 1 件実行時に UnknownType 例外気づけます
type: "Integer"(大文字)エラー 1 件実行時に UnknownType 例外気づけます
required に文字列を渡すエラー 1 件違反 1 件気づけます
any_ofエラー 0 件違反 0 件黙って通ります
one_ofエラー 0 件違反 0 件黙って通ります
all_ofエラー 0 件違反 0 件黙って通ります
enum_valuesエラー 0 件違反 0 件黙って通ります
min_itemsエラー 0 件違反 0 件黙って通ります
exclusive_minimumエラー 0 件違反 0 件黙って通ります
additional_propertiesエラー 0 件違反 0 件黙って通ります

10 種類のうち 7 種類が、スキーマ検証も実行時チェックも素通りしました。理由は仕様どおりで、JSON Schema は知らないキーワードを「無視する」と決めているからです。min_items は誤りではなく、単に語彙の外にある注釈として扱われます。制約として登録されず、当然ながら違反も出ません。

型の綴りだけが落ちるのは、type が値の集合を厳密に決めているからです。ここだけは仕様が閉じているので、String は存在しない型として拒否されます。

気づける壊れ方と、気づけない壊れ方を、同じ「バグ」として数えないようにしています。 前者は放っておいても表に出てきます。後者は、こちらから探しに行かないかぎり出てきません。

正規化が入ると、これまで通っていた引数が拒否されます

次に確かめたのは、綴りが直ったときに何が変わるかです。型はすべて小文字にしたまま、結合子と制約キーワードだけをスネークケースで書いたパラメータ定義を用意し、正規化の前後で同じ引数を通しました。

渡した引数正規化前正規化後
想定どおりの引数通過通過
mode に数値を入れる通過拒否(違反 1 件)
tags を空配列にする通過拒否(違反 1 件)
定義していない depth を足す通過拒否(違反 1 件)

メタスキーマ検証は正規化の前後どちらも 0 件でした。つまり、どちらのスキーマも JSON Schema としては「正しい」のです。それでいて、壊れた引数 3 種類は前者では全部通り、後者では全部止まりました。

ここが実務でいちばん効いてくるところだと感じています。正規化はエラーを消す変更として案内されますが、自分のツールの側から見ると、これまで受け入れていた引数が突然拒否されるようになる変更でもあります。エージェントが投げてくる引数は毎回同じではありませんから、通っていたものが止まるのは、たいてい忙しい日の途中です。

正規化そのものは正しい方向の修正です。問題は、修正のあとで初めて自分の定義の甘さが表に出る、という順番のほうにあります。

アップデートの前に、自分のツール定義を数えます

必要なのは、綴りが正しいかどうかではなく、制約として評価されていないキーワードが何個あるかを数えることです。60 行ほどのスクリプトで足ります。

# schema_lint.py — ツールのパラメータ定義から、評価されないキーワードを数える
import json, sys
from jsonschema import Draft202012Validator as V
 
VALID_TYPES = {"string", "number", "integer", "boolean", "object", "array", "null"}
 
def collect_keywords():
    """メタスキーマから Draft 2020-12 の語彙を機械的に集めます"""
    ks = set()
    def walk(node):
        if isinstance(node, dict):
            for k, v in node.items():
                if k == "properties" and isinstance(v, dict):
                    ks.update(v.keys())
                walk(v)
        elif isinstance(node, list):
            for x in node:
                walk(x)
    walk(V.META_SCHEMA)
    return ks
 
KEYWORDS = collect_keywords() | {"nullable", "examples", "title", "description", "default"}
 
def lint(schema, path="$"):
    out = []
    if not isinstance(schema, dict):
        return out
    parent = path.split(".")[-1]
    for k, v in schema.items():
        here = f"{path}.{k}"
        if k not in KEYWORDS and parent not in ("properties", "$defs", "patternProperties"):
            out.append((here, f"未知のキーワード '{k}' — 制約として評価されません"))
        if k == "type":
            for t in (v if isinstance(v, list) else [v]):
                if isinstance(t, str) and t not in VALID_TYPES:
                    out.append((here, f"型 '{t}' は不正です(小文字のみ)"))
        if k == "properties" and isinstance(v, dict):
            for pn, ps in v.items():
                out += lint(ps, f"{path}.properties.{pn}")
        elif isinstance(v, dict):
            out += lint(v, here)
        elif isinstance(v, list):
            for i, x in enumerate(v):
                out += lint(x, f"{here}[{i}]")
    return out
 
if __name__ == "__main__":
    findings = lint(json.load(open(sys.argv[1])))
    for p, m in findings:
        print(f"  {p}: {m}")
    print(f"検出 {len(findings)} 件")
    sys.exit(1 if findings else 0)

親が properties のときだけ判定を飛ばしているのは、そこに並ぶのは語彙ではなく利用者が決めたパラメータ名だからです。ここを飛ばし忘れると、pathlimit も「未知のキーワード」として報告され、出力が読めなくなります。私自身、最初はこの guard を入れ忘れて 100 件近い偽陽性を出し、レポートを一度捨てました。

壊れた定義を通すと、こう出ます。

$ python3 schema_lint.py tool.json
  $.properties.path.type: 型 'String' は不正です(小文字のみ)
  $.properties.limit.min_items: 未知のキーワード 'min_items' — 制約として評価されません
  $.properties.mode.any_of: 未知のキーワード 'any_of' — 制約として評価されません
  $.additional_properties: 未知のキーワード 'additional_properties' — 制約として評価されません
検出 4 件

終了コードを返すようにしてあるので、CI にそのまま置けます。設定キーが黙って無視されたまま CI を通り抜ける経路については、無視された設定キーが CI を通り抜ける経路と、その塞ぎ方に別途まとめてあります。考え方はほとんど同じで、「読まれていないものを、読まれていないと言ってくれる仕組みを自分で足す」という話です。

直したあとに、一度だけ確かめておくこと

綴りを直す作業自体は機械的です。順番だけ気をつけています。

  1. まず schema_lint.py を全ツール定義に走らせ、直す前の検出件数を記録します。これが後で効きます。
  2. 綴りを直します。any_ofanyOfmin_itemsminItemsadditional_propertiesadditionalProperties。型は小文字へ。
  3. 直したスキーマに、これまで実際に流れていた引数を通します。ログから 20〜30 件ほど拾えば十分です。ここで拒否されるものが出たら、それは今まで黙って通っていた不正な引数です。
  4. 拒否されたものを見て、スキーマ側を緩めるのか、呼び出し側を直すのかを決めます。

3 番を飛ばさないでいただきたいのです。制約が効き始めるということは、エージェントの側が今まで送っていた引数のうち、いくつかが止まるということです。それを本番で知るのと、手元のログで先に知るのとでは、翌朝の気分がまったく違います。

ローカルエンジン側の接続そのものでつまずいている場合は、スキーマより先にそちらを片づけたほうが早いです。接続の組み方はAntigravity からローカル LLM を呼ぶ実用ワークフローにまとめました。また、ツールが読めるはずのファイルを読めていないときは、スキーマではなく除外ルールが原因のことがあり、その切り分けはGit が追跡しているのにエージェントが読めないファイルは、どこで外れているかで扱っています。

手を動かすなら、今日は 1 本だけ

いちばん引数の種類が多いカスタムツールを 1 つ選んで、schema_lint.py を走らせてみてください。検出 0 件なら、そこは心配しなくてよい場所だと分かります。1 件でも出たら、その制約は今まで一度も働いていなかったということです。

私は「エラーが出ていない」を「正しく動いている」の証拠として数えるのをやめました。黙っている仕組みには、黙っていることを報告させる係を別に立てる——それだけのことなのですが、この一手間を惜しんだ夜の遠回りは、しばらく忘れられそうにありません。

最後までお読みいただき、ありがとうございました。同じところで手が止まっている方の、少しでも近道になれば幸いです。

シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

Agents & Manager2026-04-09
Antigravity × Gemma 4:プロダクション・エージェントをローカルLLMで構築する
Ollama で Gemma 4 をセットアップし、Antigravity から接続してプロダクション水準のエージェントを構築する流れを解説します。AgentKit 2.0 の Tool Use と Memory の統合、並列実行によるマルチエージェント化、量子化の選び方まで踏み込みます。
AIツール2026-06-14
Antigravity でローカル LLM を併用し、機密コードを外に出さない振り分け設計
課金キーやアプリの売上ロジックを含むコードまでクラウドのエージェントに読ませてよいのか、という不安は個人開発でも現実的です。Ollama と Gemma によるローカル LLM を Antigravity と併用し、機密部分はローカル・一般部分はクラウドへ振り分ける設計を、判定ルールと計測込みで組み立てます。
連携・プラグイン2026-05-04
Antigravity に Gemma 4 ローカルモデルを統合する — オフライン/エアギャップ環境での AI 開発
Apache 2.0でリリースされた Gemma 4 を Antigravity に統合することで、機密プロジェクトやオフライン環境でも Antigravity のエージェント体験が可能になります。Ollama / vLLM 経由の接続設定、Architect / Builder の挙動チューニング、本番運用の注意点までを実装ベースで解説します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →