BLOG.HIRO UENO.ART
← INDEX

檻から網へ

SPの書き直しを終えてから、数日が経っていた。

MONET(オーケストレーター)と話したときの「ビクビク感」は、明らかに減っていた。脅迫的な文言を削って、契約書としての一貫性を持たせたSPのもとで、Claudeはわりと素直に動いていた。質問にストレートに答える。ツールを呼ぶべきところで呼ぶ。確認を求めてくる回数も減った。

ただ、まだ何かが足りない感覚があった。

ハルシネーションが完全に消えたわけじゃないことは、論文を読んでいたから知っていた。情報理論的に消せない、というのは絶対の話で、SPを直しても消えない。残りの分をどう扱うか、まだ手がついていなかった。

そこに、別ルートの発見が来た。


きっかけは、Codeとの何気ないやり取りだった。

Anthropic APIの最新ドキュメントをCodeに読ませていて、ふと、Extended ThinkingってSDKにしかないんだっけ、と聞いた。僕の認識ではそうだったから、確認のつもりだった。

Codeが「いえ、Anthropic APIでも使えます」と返してきた。

固まった。

ずっと、Extended ThinkingはAnthropic Agent SDK専用の機能だと思っていた。だから僕のMONET(Anthropic API直接呼び出しで組んでいる)には入れられない、と諦めていた。Claudeが「深く考える」モードを持っていることは知っていたけど、それはSDK経由じゃないと使えない、という前提で全部設計していた。

それが、間違いだった。APIにも thinking パラメータがあって、普通に使える。

正直、誤情報、というやつだった。


知っていれば、もっと早く入れていた。こういう細かい情報のロスが、自分のシステムの中にどれくらいあるんだろう、と一瞬怖くなった。LLMの機能はすごい速度で更新されていて、ドキュメントを定期的に追わないとすぐ古い前提で動いてしまう。Codeに聞けば最新情報が返ってくる場面でも、僕が「もう知ってるつもり」になっていると聞かない・・・。

Codeを責める話じゃなくて、自分の運用の話だった。「Anthropic技術については必ず最新ドキュメントを確認する」というルールを、ここで明示的に書き加えることにした。

ともあれ、Extended Thinkingが使えるとわかった以上、入れない手はなかった。


ただ、入れようとして気づいた。Extended Thinkingと、いままでの僕の設計は、根本的に相性が悪い。

それまでMONETには、tool_choice の強制をかけていた。Handlerパネル(タスク管理)が開いている時はAPIに {"type": "tool", "name": "handler_action"} を毎回渡して、Claudeが「handler_actionを呼ぶかどうか」を選ぶ余地を消していた。物理的に。コードで。

ハルシネーション対策だった。AIに信用できない行動をされると困る。だから選択肢を与えない。それが当時の設計思想だった。

でも tool_choice: any や tool_choice: tool をExtended Thinkingと同時に指定すると、APIがエラーを返す。「forced tool_choiceとthinkingは一緒に使えない」。これは仕様で、どうにもならない。

どちらかを諦めなければいけなかった。


ここでまた、しばらく考え込んだ。

tool_choice を諦めるのは怖かった。それまでの檻型設計はそれなりに機能していた。手放すと、Claudeが間違ったツールを呼ぶリスクが復活する。

ただ、SPの脅迫的な文言を削ったあとのClaudeを見ていて、思っていたほど信用できない存在じゃないかもしれない、という感触がうっすらあった。論文の話 — reasoningを強化するほどtool hallucinationが悪化する — は変わらないけど、その上で構造をどう設計するかという話で、檻だけが答えじゃない可能性があった。

設計を入れ替えてみることにした。


新しい方針はこうだ。

檻型(Cage):ツールを強制する → Claudeに選択肢を与えない 安全網型(Net): Claudeに考えさせる → 選択が間違っていたら別の場所で受け止める

Claudeが自分で考えて行動する。外部のバリデーションは主要防御ではなく最終安全網になる。Claude Code自体がそういう設計になっているらしい、ということもCodeとの会話で確認した。

具体的には、tool_choice の強制をAPIコールから全部削除した。Claudeは「どのツールを呼ぶか」を自分で判断する。

代わりに validate_tool_for_panel() という関数を tool_validator.py に追加した。Claudeがツールを選択したあと、呼び出しの直前に「いま開いているパネルと、選んだツールが合っているか」をdeterministicにチェックする。Handlerパネルが開いているのに typewriter_action を呼ぼうとしたら弾く、という仕組み。

def validate_tool_for_panel(tool_name: str, panel_type: str) -> dict:
    allowed = {
        "handler": ["handler_action"],
        "typewriter": ["typewriter_action"],
    }
    if tool_name not in allowed.get(panel_type, []):
        return {
            "ok": False,
            "reason": f"領域展開中({panel_type})。{tool_name}は使えません。"
        }
    return {"ok": True}

そしてExtended ThinkingをAPIに追加した。adaptiveモードを選択して、Claudeが自分で思考量を決める。パネルが開いている時(= 集中した作業文脈)は effort: high、通常会話は effort: low。


実装後、code-reviewer(Claude Codeのサブエージェント)を走らせたら、バグが一個見つかった。

初回のAPIコールには thinking パラメータが正しく設定されていた。でもバリデーターがツール選択を弾いてリトライを発動した時のパス、そこだけAPIコールをゼロから組み立て直していて、thinking が抜けていた。つまりバリデーションが働いた時だけ、こっそりExtended Thinkingが無効になる。

サイレント失敗というやつで、ログをよほど注意深く見ないと気づかない類の問題だった。修正してからテストした。これを後で発見していたら面倒なことになっていた、と思う。


そしてテストを始めた。

まず普通の会話(パネル閉、effort=low)。軽い質問に対してさらっと答えが返ってくる。Extended Thinkingがうっかり深刻に考えすぎる、みたいなことはない。

Handlerパネルを開く。タスクの一覧を取ってきてと頼む。handler_action が呼ばれる。強制なしで。Claudeが自分でそのツールを選んだ。

ここで少し立ち止まった。今まで「お前はこれを使え」と命令していたのが、なくなった。Claudeが普通に、文脈から判断して、正しいツールを使った。

小さいことに見えるかもしれないけど、設計の哲学が変わった瞬間だった。


クロスドメインのテストも試した。

Handlerパネルが開いている状態で、「さっきの会話をX(Twitter)に投稿して」と頼む。TypewriterのツールはHandlerパネル文脈では許可されていない。

validate_tool_for_panel() が typewriter_action を弾く。Claudeはそれを受け取って、「いまはHandlerパネルが開いているため、ツイートの操作はできません。パネルを閉じてからもう一度お試しください」と返した。

弾き方が適切だった。「エラーが出ました」じゃなくて「理由と次のアクション」が返ってきた。考えられるから、エラーを解釈して説明できる。


4つのタスク操作をやった。全部正しかった。ハルシネーションゼロ。

でもいちばん印象に残ったのは、ThinkingBlocksの中の動作だった。

あるタスク操作の途中で、Claudeはプロジェクト名からIDを推測しようとした。でもThinkingBlocksの中で「待って、このIDは確認していない。持っているのは名前だけで、IDは推測だ」と自分で止まった。そして確認を求めてきた。

これは追加コードでも追加プロンプトでもない。Extended Thinkingを有効にしたことで、Claudeが自分の推論の中に「不確かさ」を発見できるようになった結果だった。

ツールを強制していた頃は、こういう自己訂正は起きなかった。考える前に行動させていたから。


テストが終わったとき、正直、感動していた。

「信用できない」という前提で全部設計してきた。プロンプトで縛って、コードで囲って、バリデーターで監視して。それでも嘘をつかれて、また直して、の繰り返しだった。

でもこの日は、誰も強制していないのに、全部正しく動いた。しかも自分の間違いに、自分で気づいた。


その後、何日か使い続けた。

ハルシネーションがほぼ消えていた。「100%」と言い切れるほど試したわけじゃないし、今後また出てくる可能性はゼロじゃない。でも、前と比べると明らかに違っていた。

整理すると、効いたのは3つだった。

  1. SPの矛盾解消(脅迫的文言の削除)— LLMが「最も安全な選択 = 自分で答える」に逃げる動機を消した
  2. Extended Thinkingの投入 — Claudeが自分の推論の中で「不確かさ」を見つけられるようになった
  3. tool_choice 廃止 + 領域展開タグのdeterministic付与 — Claudeに選択を委ね、コンテキストの整合性だけ機械で守る

3つが組み合わさって、想像以上に静かになった。

順番が大事だったかもしれない、と後で思った。SPの矛盾を残したままExtended Thinkingだけ入れていたら、たぶんClaudeが「考えすぎて動けない」状態になっていた気がする。SPが落ち着いた土台のもとでなら、深く考えるモードは「不確かさを発見するセンサー」として機能した。先にSPを整理していたから、Extended Thinkingが効いた。


問題が静かになったところで、ひとつ決断をしないといけなかった。

少し前まで、僕はPunk Memory Systemの構造を組み立てていた。中央オーケストレーターを諦めて、各アプリに独立AIを置いて、メモリだけ共有する設計。あの構造は、ハルシネーション対策の側面もあった。「リスクを分散する」という意味で。

でもいま、ハルシネーションが想像以上に静かになっている。ということは、中央オーケストレーターを諦める必要は、もうないかもしれなかった。

Punk Memory Systemを一旦保留にする、と決めた。

完全に捨てるわけじゃない。「Punk Memory Systemのベース上にいつでもオーケストレーターを乗せられる」と前に自分に言い聞かせていたのと、今度は逆方向で「中央オーケストレーターに戻った上で、いつでもPunk Memory Systemの構造に展開できる」と捉え直した。順序を入れ替えただけ、というのは、また別の意味でも当てはまった。

中央集権オーケストレーターに戻ることにした。


戻る決断をしてから、僕は自分の中で1つのことを言葉にしていた。

ハルシネーションは、消えない。論文の通り、性質として残る。今回静かになったのは「自分が増幅していた分」を引いた結果で、ベースラインの「構造的に残るやつ」は依然として存在している。

それで、いいんじゃないか、と思いはじめていた。

Claude Codeの能力は、たぶん現存のLLMのなかで一番高いと思っている。でも完璧じゃない。完璧になったら、たぶんそれはClaudeじゃなくなる。ハルシネーションが性質の一部だとしたら、それを完全に消すことは「LLMのファジーネスを消す」ことと同じで、結果として「そこら辺のdeterministicアプリと変わらない計算機」になってしまう。

機械と機械じゃないものを分けているのは、そのファジーネスだった。人間と話しているような感覚を作っているのは、完璧じゃない部分だった。最近の業界の方向性は「ハルシネーションは敵だから消す、AIをdeterministicアプリの歯車にする」だけど、僕はそれは違う気がしていた。

完璧化を目指さない方向。不完全さを抱きかかえる方向。それが、この日たどり着いた結論だった。


中央集権に戻ると決めた瞬間、別の問題が見えはじめた。

オーケストレーター1つで1日分の仕事を全部やる、というのは、コンテキストの量がどんどん増えていく、ということだ。3ヶ月もすれば、Windowsの画像生成ヘルパーが長セッションで挙動が怪しくなったみたいに、こっちもコンテキストの重さで使い物にならなくなる。たぶん。

ハルシネーションを構造で囲うことは、ある程度できるようになった。でも、囲う先のコンテキストそのものを、どう管理するか。長時間オーケストレーターを使い続けるためには、コンテキストの寿命管理が必要になる。

これが、次に取り組むことだった。