BLOG.HIRO UENO.ART
← index
2026.02.08

Handler から AI を抜く

昨日、Typewriter から AI を抜いて実行デバイスにした。今日は同じことを Handler でやる。二回目だから半日で終わるだろう・・・と思っていた。

Handler はタスク管理アプリ。中に AI が入っていて、MONET(メインLLM、Claude、オーケストレーター)から指示を受けて自律的にタスクを操作する設計だった。論文を読んだ後の判断で、Typewriter と同じくここからも AI を抜くと決めていた(経緯は前の記事で書いた)。


手はたしかに早かった。

ai_handler.py を消した。ファイル丸ごと。app.py から AI 系のエンドポイント(/chat/set-model あたり)を削除して、残ったのはタスクの CRUD だけのシンプルな Flask サーバー。mcp_server.py を書き直して、9個の CRUD ツールを並べた。

get_tasks
get_task_by_id
add_task
update_task
complete_task
delete_task
undo_complete_task
search_tasks
set_priority

MONET 側には、昨日 Typewriter でやったのと同じく handler_action という1つのツールに見せて、中で9つにディスパッチする(外から1ツール・中で複数、の Action enum 方式。詳細は前の記事)。

ここまでは前日の手順をなぞるだけ。tool_choice 強制と Watchman の枠組みもそのまま流用できるはず・・・と思った。


最初の引っかかりは小さかった。

set_priority のテストで、上の Claude が priority: "high" という文字列を投げてきた。Handler 側は 1〜5 の数値を期待してる。当然エラー。

コーディングしてくれている Claude Code(以降 Code)に頼んで、_parse_priority() という小さなヘルパーを挟んでもらった。"high" が来たら 1、"low" なら 5 に変換する緩衝材。

これ自体は些細な話。ただ、ちょっと引っかかった。AI を抜いたつもりでも、上にいる Claude の自然言語の癖を下のレイヤーで吸収する場面が出てくる。下は単なる実行デバイス、と言いながら、インターフェースの設計は上の挙動を見て決めるしかない。完全に分離されてるわけじゃなかった。


もう一つの引っかかりの方が大きかった。Watchman が重い。

Watchman は昨日の記事で書いた見張りの仕組み。入口で、ユーザーの発言を Haiku(小さい Claude)に見せて雑談か操作要求かを判定させる。そして出口で、上の Claude の返答を Haiku に見せて、ツールを呼ばずに嘘の報告をしてないかを判定させる。つまり1ターンに Haiku が2回走る。雑談でも2回走る。

Typewriter の時は、まあ動いてるならいいか、で見ないことにしてた。

でも Handler は雑談混じりに使うアプリ。今日のタスク見せて、ありがとう、じゃあそれ完了にして、みたいな流れが普通にある。その雑談まで毎回 Haiku を2回通すのは、さすがに重い。

そもそも雑談に Haiku の判定が要るのか・・・と考え始めた。


で、思いついた。tool_choice でツールを強制するなら、雑談も「ツール」として enum に入れちゃえばいいんじゃないか。

handler_action の action に chat という選択肢を足す。中身は空文字を返すだけのダミー。雑談したい時は action: "chat" を選んでもらう。

そうすると入口の Haiku が要らなくなる。雑談か操作かを判定する人を呼ばなくても、上の Claude 自身が enum で選んでくれる。

出口も分岐できる。タスク操作の時はツール結果が返ってきてるかを機械的にチェックするだけ。AI 不要。chat の時だけ Haiku を1回呼んで、chat を選んだのに本文でタスク操作したと嘘ついてないかを見張る。

Code に伝えたら、「これは結構いい設計じゃないか」みたいなトーンで返してきた。実装してもらった。


実測した。

旧(入口 Haiku + 出口 Haiku) 新(chat enum + 機械的判定)
通常会話 Haiku 2回 Haiku 1回
タスク操作 Haiku 2回 Haiku 0回

通常会話は2秒で完了。タスク操作は Haiku を一切呼ばない。これが一番効いた。タスク管理は本来、やった/やらなかったを機械的に判定できる作業のはず。それを毎回 Haiku に聞いてたのは、たしかに無駄だった。

捏造リスクは下がってない。タスク操作は tool_choice 強制で操作系の action を選ぶしかなくて、結果がなければ機械的に REJECT される。逃げ道は塞がれたまま。精度は同等、速度とコストだけ下がった。雑談まで重い判定をしてた昨日までの自分が、ちょっと笑える。


面白いのは、chat を「ツールの一種」として扱ったところだと思う。

普通、雑談はツール呼び出しの外側にある。ツールを呼ばない返事、として扱う。それを中に入れて並べた。何もしないツール、として。そうすると領域展開中の Claude は、雑談する時も「ツールを選んでます」という形で動く。tool_choice 強制は崩れないし、Watchman は何が選ばれたかを機械的に確認するだけでよくなる。

たぶんこれは Handler 専用の工夫じゃなくて、Typewriter にも当てられる。昨日作ったばかりの Watchman を、今日にはもう書き直す気になってる。


抜く作業そのものは、前日とほぼ同じだった。ファイルを消す、CRUD を並べる、1ツールに見せかける。違ったのは、目に入るものの方。

Watchman が重いとか、雑談まで判定するのは過剰だとか、一回目は動かすことで頭がいっぱいで、全部「動いてるからまあ」で流してた。二回目で同じ仕組みをもう一度組もうとした瞬間、初めて構造を疑う余裕が生まれた。同じ作業を二度やる効用って、たぶんこれなんだと思う。

アプリごとの違いも効いた。Typewriter は書いて投稿する道具だから、雑談の割合が低い。Handler は雑談混じりで使う道具。同じ Watchman を当てただけで、重さの見え方が変わった。仕組みの過剰さは、使い方の違うアプリに移植した時に初めて見える。

Typewriter と Handler、実行デバイスが二つ並んだ。中央が考えて、下が動く。今のところ上の Claude は行儀よく動いてる。前にも「これでもう嘘はつかないだろう」と思って裏切られたことは何度もあるから、確信はしない。それでも見張りは昨日より軽く、シンプルになった。二度目の手術は、一度目より上手くいった。