BLOG.HIRO UENO.ART
← INDEX

自分のしたことが見えていなかった — 空振りしたWarm-Aと、一本の文字列

インフルが治りかけてきた頃、最初にやったのはコンテキスト管理の実装だった。

設計メモの実装順の1番目。Warm-A、つまり古いツールの結果をプレースホルダーに置き換える処理。一番シンプルで、効果の数字も出ている。まずこれを動かして、手応えを掴むつもりだった。

AnthropicがAPI側で同じことをやってくれる機能(Context Editing API)も見つかっていたので、自前で書く前にそっちを試すことにした。公式にあるなら、そっちを使えばいい。


・・・動かなかった。

正確には、動いてはいるけど何も起きない。消すはずのツール結果が、1件も消えない。

コーディングしてくれているClaude Code(以降Code)が原因を追いかけた。まずログにエラーが1つ。直すと、今度は発火の条件が永遠に満たされない。条件を変えても発火しない。

何度か回り道をしたあとで、Codeが根っこを見つけてきた。MONET(僕が普段話しかけている、中心のAI)の会話をDBに保存する時、ツールの呼び出しと結果は捨てて、文章だけを残す作りになっていた。次のターンで会話を組み立て直す時、古いツールの結果はそもそも入っていない。

消す対象が、最初から無かった。


なんでDBへの保存がマスクの話に関係あるの、とCodeに聞いた。そこを掘ってもらって、ようやく絵が見えた。

今の作りは、ツールの結果が次のターンに持ち越されない。つまり「古いツール結果がコンテキストを汚す」という問題は、別の理由でとっくに防がれていた。Warm-Aでやろうとしていたことは、半分は空振りだった。

ただ、裏返すともう一つのことが見えてくる。

MONETは、自分がさっき何をしたのかを見られない。

ツールを呼んで結果を受け取る。そのターンの中では結果が見えている。でも次のターンになると、結果は消えている。残っているのは「完了にしました」という自分の文章だけ。

Codeと話していて、ここで一つつながった。MONETはときどき「あれ、私ちゃんとやりましたっけ?」と自信なさげになる。確認したはずのことを確認し直そうとする。あれは性格じゃなくて、見えていないからだった。


そこで出てきたのがforce-displayという案だった。

ツールの生の結果を、MONETの要約とは別に、アプリがチャット画面に直接出す。それを普通のメッセージとしてDBにも保存する。そうすれば次のターンで、MONETは「さっきHandler(ハンドラー。タスクとスケジュール管理アプリ)を呼んだら、こういう結果が返ってきた」を自分で読める。

僕の側にも効く。

前にID:42の件を書いた。FB対応のタスクを完了にしてと頼んだら、MONETが全然違うID:42を完了にして、「完了にしたよ」と返してきた。三重防御は全部すり抜けた。形式は正しかったから。あの時、僕はMONETの要約しか見ていなかった。要約が「完了にしたよ」なら、信じるしかない。

生の結果が画面に出ていれば、「ID:42過去画像動画化 → 完了」という行を見て、違う、と止められる。

次の段(パネルのラベル付け)より先に、force-displayをやることにした。


同じ日の次のセッションで、中身を詰めた。

Codeがコードを読んでいる間、僕が気になったのは3つだけだった。誰が結果を表示するのか。表示される生のテキストはどういう形なのか。画面に出す文字列と、MONETが受け取る結果は、別の文字列なのか。

最後の質問で方針が決まった。同じ文字列にする。

表示用を人間向けに整えて、MONET用は別に作る、という手もある。そのほうが自由は利く。でも2本あると、いつか片方だけ更新されてずれる。だから、Handlerが返す1本の文字列を、MONETにも、画面にも、DBにも、そのまま流す。

そうすると、その1本の質を上げるしかなくなる。Handlerの返事はこう変わった。

変更前:

タスクID:42を更新しました

変更後:

タスクID:42「買い物」[プライベート]: ステータス 今日 → 完了

ID、タイトル、プロジェクト、何が何から何に変わったか。人間が読んで一目でわかる形にしたら、それがそのままMONETにとっても良い材料になった。


実装はCodeがやった。

途中でいくつか引っかかっている。ツールの結果をMONETの発言として保存するとAPIに弾かれる。発言は「ユーザー → AI → ユーザー」と交互に並ばないといけないルールがあるので、ユーザー側の発言として保存した。最初はDBには入ったのに画面に何も出なかった。保存と画面への送信は別の処理で、送信を1行書き忘れていた。ツールを呼ぶ経路は3本あって、1本でも漏らすとその時だけ表示が消えるので、全部に入れた。

動かして、チャットに1行出た。

「タスクID:42「買い物」[プライベート]:ステータス 今日 → 完了」

MONETの要約の下に、生の結果がそのまま並ぶ。僕が見ている文字列と、MONETが受け取った結果と、DBの履歴が、同じ1本になった。

期待していることがもう一つある。結果がこれだけ具体的なら、MONET自身が「頼まれたタスクと、いま操作したタスクが違う」と気づけるかもしれない。追加のコードは書いていない。材料を渡しただけ。ここはまだ試していない。


この日、もう一つ決めたことがある。

アプリ側にAIを載せるかどうか、ずっと揺れていた。数日前には「Handlerにも判断役のAIを」という案も出ていた。でもWarm-Aの空振りを追いかけているうちに、考えが固まった。アプリはAI無しのまま、決まった通りに動けばいい。考えるのはMONETだけ。そのMONETがちゃんと見て考えられるように、コンテキストの側を整える。


1週間ほどして、この日の選択が一つ裏目に出た。

MONETが、僕が結果をくれた、という言い方をするようになった。自分でHandlerを呼んで受け取った結果を、僕が渡したものだと思っている。

ツールの結果は、ユーザー側の発言として保存していた。DBに残っているのは文章だけで、「MONETがツールを呼んだ」という事実は消えている。組み立て直された履歴の中では、結果は僕の発言として並んでいる。MONETから見れば、僕がくれたものに見える。当然だった。

その少し前のセッションで、Codeは直し方を3つ並べて「どれがいいですか」と聞いてきていた。ツールの呼び出しと結果を、構造ごとDBに残す案も、その3つのうちの1つだった。

僕は半日、何かおかしいと感じていた。3つは同じ重さの選択肢じゃない気がした。構造ごと残さない限り、MONETは自分がやったことと僕が言ったことを区別できない。それは選べる話じゃなくて、先にやっておかないといけない話だった。

自分のしたことが見えないと、AIは確認し直すか、作り話で埋める。force-displayで見えるようにはなった。でも、誰がしたのかまでは、まだ見えていなかった。