この記事は、連載「読まない技術」の第 10 回です。うまくいかない症状と、その対処を一つずつ並べています。各回はファイルやスクリプトを一つ置けば完結します。その仕組みがなぜ要るのかは、置いたあとに解説を読めば分かります。連載の全体像と各回の一覧は序論にあります。

今回は、要約は母数を落とすという問題の話です。コマンドが数えた数字をAIが要約すると、結論だけが残り、「何件のうちの 0 件か」が消えます。

まず一つだけ、思い出してみてください。

直近のセッション(AIとの、始まりから終わりまで一続きのやり取り)で、AIが「0 件でした」と報告してきた場面です。

その数字を出力したコマンドのほうには、「何件のうちの 0 件か」まで書いてありませんでしたか。

数字を数えるのは、コマンドです。自動テストの実行件数でも、git の一行でも構いません。ただ、その数字が私に届くまでの途中には、AIがいます。

typingtube(YouTube の音楽動画でタイピング練習ができる、個人運営の Web サービス)の開発では、記事に出てくる語が、語の一覧表と食い違っていないかを、毎回数える自動テストを置いています。その自動テストは、「169 語を突き合わせて、食い違い 0 件」と出力していました。この出力には、母数の 169 まで書いてあります。「169 語のうち 0 件」なら、私は、169 が少なすぎないかを疑うことができます。ところがAIは、この結果を 4 回報告して、4 回とも「食い違い 0 件」だけを書きました。私が受け取ったのは、母数の落ちた 4 回の報告です。

AIに自動テストを走らせました。コマンドは「120 runs, 0 failures」と出力していたのに、AIの報告は「テストは全部通りました」だけでした。あなたなら、次のどちらにしますか。

  • AIに、コマンドの結果の詳細を報告させる指示を追加する
  • コマンドが出力した数字の行を、AIの要約を通さずに、そのまま自分に届くようにする

答えは、AIの要約を通さないほうです。母数の「120 runs」まで出力したのはコマンドで、それを落としたのは、結果を報告に書いたAIです。詳細を報告させても、報告を書くのはAIのままで、どこまでを詳細として書くかもAIが決めます。冒頭の 4 回でも、AIの報告には結論の「食い違い 0 件」が残り、「169 語を突き合わせて」が落ちました。母数が無いと、あなたは、走ったテストが少なすぎないかを疑うことができません。数字の行がAIの要約を通らずに届けば、あなたが受け取る数字から母数が落ちることはありません。

冒頭の 4 回の報告と同じ日に、私に届いた数字に、何かが足りなかった場面が 4 件ありました。私は、その 4 件を、何が足りなかったかで仕分けました。

何が足りなかったかどうするか
数えるコマンドがあり、母数も出力していた。AIが要約して落としたコマンドが出力した数字の行のほうを、AIの要約を通さずに、そのまま私に届ける
コマンドはあるが、母数を出力していなかったコマンドのほうを直す。AIに「母数を書け」と指示しない
数えるコマンドが無い(どの仕組みが拾ったかの区別など)私がAIに問いかける(第 12 回)

4 件のうち、数えるコマンドが無かったのは 1 件だけでした。残りの 3 件は、数えるコマンドを置いたあとで起きています。

理解したことは一文にできます。

AIが数字を要約すると、結論が残って母数が落ちる。母数の落ちた要約のほうが後まで残って、AIが次に判断するときの材料になる。だから、コマンドが出力した数字の行を、AIの要約を通さずに、そのまま私に届ける。

CC BY 4.0

なぜ、AIが要約すると母数が落ちるのか

母数が落ちるのは、AIの報告が要約だからです。第 5 回で見たとおり、AIが止まるのは、仕事が終わったように見えたところです。AIは、そこで報告を書きます。

前回見たとおり、その報告の「0 件」は、AIがその場で書いた文です。コマンドは母数まで出力していました。AIが書いたのは、そのうちの「0 件」だけです。

Anthropic が公開しているプロンプトの指針は、レビューを頼むときに「重大な問題だけ」「保守的に」と言うと、モデルはその指示を書いてあるとおりに守って報告を減らすと書いています。別のモデルのページには、同じ深さで調べていても、報告に変える件数のほうが減るとあります。

指針が書いているのは、AIに指示を与えたときの話です。冒頭の 4 回の報告でも、AIは、コマンドが出力した母数を、報告に載せませんでした。

数えるコマンドを置いても、その数字が私に届くまでにAIを通れば、AIがそこで要約します。

AIが書いた「0 件」は、コマンドが出力した数字の行より後に、コンテキスト(AIの作業記憶)へ入ります。そしてコンテキストが上限に近づくと、Claude Code は古いツールの出力から先に消し、それでも足りなければそれまでのやり取りを要約します。コマンドが出力した数字の行はツールの出力の側にあるので、先に消えます。

あとに残るのは、母数の落ちた「0 件」のほうです。そのあとAIが判断の材料にするのも、この「0 件」です。そのときには、母数は数字の行と一緒にコンテキストから消えていて、169 が少なすぎないかを確かめる材料は、もう無くなっています。

仕組み: 数字の行に付ける決まった書き出しと、その行を最終応答と照らし合わせるhook

私がAIに「そのまま貼って」と頼むこともできます。ただ、それは指示です。第 6 回で見たとおり、指示が守られるのは、AIがその指示を読んだときだけです。

だから私は、頼む代わりに 2 つのものを置きます。数字の行に付ける決まった書き出しと、その行を最終応答(AIが返事の最後に書くメッセージ)と照らし合わせるhook(コマンド実行などの直前・直後に割り込むスクリプト)です。

まずコマンドの側です。コマンドが出力する数字の行の先頭に、決まった書き出しを付けます。数字の行には、何を数えたかと母数を、数字と同じ行に入れます。何を数えたかが無いと、数字が並んだときに、どの数字が何の数かが決まりません。hookは数字の行を 1 行ずつ拾って照らし合わせるので、別の行に書いたものは、私に届く行に入りません。

# 語の一覧表の突き合わせ。「0 件」だけでは、169 が少なすぎないかを読む側が疑えない
puts "[observed] glossary: #{rows.size} terms x #{chapters.size} chapters, #{bad.size} mismatches"

つぎにhookです。

# 手元のコードを短くしたもの。pending は覚えておいた行、last_message は AI の最終応答、
# squeeze は突き合わせの前に空白を落とす関数(飾ると別の文字列になる)
# ツールの後: 出力から、その書き出しの行を拾って覚えておく
for line in tool_output.splitlines():
    if line.lstrip().startswith("[observed]"):
        append(line.strip())

# 返事の終わり: 最終応答に無い行は、hook がそのまま出す
missing = [l for l in pending if squeeze(l) not in squeeze(last_message)]
if missing:
    emit({"systemMessage": "lines the command printed:\n" + "\n".join(missing)})

このhookが最終応答に求める条件は、数字の行が書き出しごと入っていることだけです。母数が書き足されていることや、数字の説明があることは、条件に入っていません。

このhookが照らし合わせるのは、最終応答だけです。最終応答より前のメッセージでは、AIが数字を要約することもあります。ただ、AIが最終応答に数字の行を貼れば、コマンドの数字はそこでコンテキストに入り、私の画面に出るのと同時に、そのあとAIが判断に使う材料にもなります。

hookが出した数字の行が表示されるのは、私の画面だけです。AIのコンテキストに入るのは、AIが自分で貼った数字の行のほうです。

数字の行に書き出しを付けたあとで、別の問題が一つ見つかりました。今度、数字の行を落としたのは、私がテストを走らせるときに使う実行ラッパーでした。このラッパーは、ログの全文をファイルへ残し、画面には一部の行だけを出力する作りです。ラッパーの出力が拾っていたのは、テスト結果の行だけでした。その日、画面に出た数字の行は 0 行でした。いまはラッパーが、数字の行も拾って画面に出します。ラッパーが画面に出す行は、hookが照らし合わせる範囲の外です。

CC BY 4.0 はここまで

読まなくなったもの

AIが要約した数字です。

報告の文に「0 件でした」と書いてあっても、私は、コマンドが出力した数字の行のほうを読みます。

CC BY 4.0

検証手順: 最終応答に数字の行が無いときだけ、hookがその行を出すことを確かめる

前提

数字を出力するコマンドを一つ選び、その数字の行の先頭に決まった書き出しを付け、母数も同じ行に入れておきます。その数字の行が、コマンドを走らせたときに画面へ出ていることも確かめておきます。ここから先の手順で見るのは、AIの側だけです。数字の行を最終応答と照らし合わせるhookは、2 か所に登録しておきます(ツールの後 = 数字の行を覚えておく / 返事の終わり = 最終応答と照らし合わせる)。

所要時間: 10 分

手順

  1. あなたがAIに、そのコマンドを走らせて結果の数字を報告するように頼みます。このとき、わざと、AIが数字を要約するように誘う頼み方をします
  2. あなたが、AIの返事のあとに、数字の行が出てくるかを見ます
  3. あなたが、同じことをもう一度頼み、今度は数字の行をそのまま貼らせます。そのうえで、返事のあとに何も出てこないことを見ます
  4. あなたが、書き出しを付けていないコマンドで同じことを頼み、何も出てこないことを見ます

合格条件(すべて満たすこと)

合格の条件は 3 つです。手順 2 では、数字の行がそのまま出ていることを確かめます。手順 3 では、何も出ないことを確かめます。この条件が無いと、同じ数字の行を毎回二度出すhookでも、合格に見えるからです。手順 4 で何も出なければ、書き出しの無い出力まで拾っていないことも分かります。

合格しなかったとき

  • 手順 2 で何も出ない —— hookの登録が、2 か所のうち片方だけです。ツールの後と返事の終わりの両方に登録が要ります
  • 貼っても出てくる —— AIが数字の行を飾っています。数字の行を太字にしたり、数字の行の中に語を混ぜたりすると、別の文字列になります。そのまま貼るとは、コマンドが出力した文字列を変えずに貼ることです
  • 前の作業の数字の行まで出てくる —— hookが、覚えた数字の行を、返事が終わっても残したままにしています

後始末

hookが覚えていた数字の行の記録を消します(次に検証するときに、古い数字の行が出てこないように)。


CC BY 4.0 はここまで

次回は「口を挟まない技術」。AI が間違っても、私は指摘しません。 だけど、常に正しく仕上がります。

連載「読まない技術」