BLOG.HIRO UENO.ART
← INDEX

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は行儀よく動いてる。前にも「これでもう嘘はつかないだろう」と思って裏切られたことは何度もあるから、確信はしない。それでも見張りは昨日より軽く、シンプルになった。二度目の手術は、一度目より上手くいった。