AIは答えを渡さなかった — コマンドが生まれる瞬間を、一緒に見ていた話

「教えてもらう」のではなく、「一緒に確かめる」だった

今日、AIに何度も質問をした。「これはなぜ動かないのか」「次に何を試せばいいか」。そのたびに返ってきたのは、最初から答えを言い切る言葉ではなかった。「こういう可能性がある」「これを確認したい」「これを実行してもらえますか」——そういう言い回しがずっと続いた。

最初は少し物足りなく感じた。原因がすぐに分かるなら、すぐに教えてほしい。コマンドをすぐに出してほしい。そう思った場面もあったはずだ。だが終わってみると、その「すぐに答えを渡さない」姿勢こそが、今日の作業全体を成立させていた一番の要素だったと気づく。

コマンドは、いつも「仮説の翻訳」として出てきた

ここが今日の作業で一番見えにくく、しかし一番重要だった部分だと思う。AIは「PowerShellのコマンドを書く」という作業を、単独でやっていたわけではなかった。コマンドが出てくる前には、必ず一つの仮説があった。

たとえば、記事の保存処理が失敗していたとき、AIはまず言葉でこう説明していた。「Gutenbergエディタは『公開』操作自体もREST API経由で行うため、REST_REQUESTを理由に処理全体を止めると、公開保存ごと止まってしまう可能性がある」。この一文がまず提示され、その仮説を検証するための具体的な手段として、コードの修正やコマンドが続いた。

文字数の境界を探っていた場面はもっと分かりやすい。AIは「文字数の上限が原因かもしれない」という仮説を立て、それを確かめる方法として「半分の長さで送ってみる」というアイデアを出した。これはただの思いつきではなく、二分探索という考え方そのものが、コマンドの形に翻訳されていた。

$halfLength = [Math]::Floor($miyukiNewContent.Length / 2)
$miyukiHalf = $miyukiNewContent.Substring(0, $halfLength)

このコマンドの裏側には、「もし半分で成功したら、問題は後半のどこかにある。失敗したら、長さそのものが原因ではない」という、次の分岐の判断材料まで見越した設計があった。コマンド1行を出すたびに、その結果が次にどう解釈されるかまで、あらかじめ用意されていた。これは、コマンドが「作業の指示」ではなく「仮説を試すための装置」として作られていたということだ。

PowerShellとして、何をしているコードなのか

この2行は、見た目は単純だが、PowerShellならではの書き方がいくつか詰まっている。

[Math]::Floor(...) は、.NETのクラスを直接呼び出す書き方だ。PowerShellはMicrosoftの.NET基盤の上に乗っているため、[クラス名]::メソッド名(引数) という構文で、.NETのライブラリ関数をそのまま使える。Floorは「小数点以下を切り捨てる」関数で、文字数を2で割ったときに小数(例:17659 / 2 = 8829.5)が出ても、整数のインデックスとして使える形に変換している。

$miyukiNewContent.Length は、文字列が持つ.Lengthプロパティへのアクセスだ。PowerShellでは文字列もオブジェクトとして扱われるため、.(ドット)でプロパティやメソッドに直接アクセスできる。bashのように${#variable}のような特殊な記法を覚える必要がなく、「変数名.プロパティ名」という素直な書き方で済む。

.Substring(0, $halfLength) も同様にオブジェクトのメソッド呼び出しだ。第1引数が開始位置(0=先頭から)、第2引数が取得する文字数を表す。つまりこの行は「文字列の先頭から、半分の長さだけを切り出す」という処理を、.NETの文字列操作メソッドにそのまま委ねている。

結果を見た瞬間、次のコマンドが書き換わっていった

二分探索を続けていたとき、コマンドはどんどん精密になっていった。

最初は「半分」を試した。成功した。次は「4分の3」を試した。これも成功した。次は「8分の7」。成功した。境界がどんどん狭まっていくたびに、AIが出すコマンドの中の数値が、その都度書き換えられていった。

$midLength = 16555   # 1回目
$midLength2 = 17107  # 2回目
$midLength3 = 17383  # 3回目
$midLength4 = 17521  # 4回目
$midLength5 = 17590  # 5回目

これは、あらかじめ全部のコマンドを用意しておいて、順番に渡していたのではない。前のコマンドの実行結果を実際に見てから、次の数値が決まっていた。「保存された文字数: 17560」という報告を受けて、初めて「では次は17590を試そう」という判断が生まれていた。コマンドは、対話のたびに新しく組み立てられるものだった。

これが、AIが最初から「答えを知っていて、それを小出しにしていた」のではなく、本当に「結果を見るまで、次に何をすべきか分かっていなかった」ことの証拠でもある。もし最初から答えが分かっていたなら、5回も数値を変えてコマンドを出し直す必要はなかった。

なぜ変数名を毎回ナンバリングしていたのか

$midLength$midLength2$midLength3……と、変数名の末尾に数字を増やしていくこの書き方は、行き当たりばったりに見えて、実はデバッグ作業として理にかなっている。

PowerShellのセッション(コンソールやスクリプトを実行している1つの実行環境)は、変数を上書きすると、それまでの値は失われる。もし毎回$midLengthという同じ変数名を使い続けていたら、「2回目に試した17107という値は、何回目の結果で、その前は何文字だったか」という履歴が消えてしまう。

末尾に数字を振って変数を増やしていくことで、過去に試した数値とその結果を、セッションが続いている間ずっと参照できる状態に保っていた。二分探索の途中で「あれ、1回目はいくつだったか」と聞かれても、$midLengthをそのまま打てば即座に答えが返ってくる。これは、ログファイルに頼らずに、対話のその場で過去の試行を振り返れるようにするための、地味だが効果的な工夫だった。

なぜ「コマンドをまとめて全部出す」ことをしなかったのか

これは今日の進め方で、地味だが重要な特徴だったと思う。AIは一度に多くのコマンドをまとめて渡すことを、基本的にしなかった。1つのコマンドを渡し、その結果を見てから、次のコマンドを出す——このサイクルを律儀に繰り返していた。

効率だけを考えれば、「とりあえず10パターンのコマンドを全部書いて、まとめて実行してください」と言う方が早かったかもしれない。しかしそれをしなかったのは、結果を見ないと、次に何を試すべきか本当に決められなかったからだ。文字数の境界を探る場面でも、CSSが崩れた直後の対応でも、毎回「次にこれを確認できますか」という形で、1手ずつ進んでいた。

この「1手ずつ」という進み方は、もし途中で予想と違う結果が出たときに、被害を最小限にするための仕組みでもあった。実際、<!-- wp:html -->を除去するという対応は、その場では正しく見えたが、後になって別の問題(ビジュアルエディタでの崩れ)を引き起こしていたことが分かった。もしこの対応を「まとめて全記事に適用する」という形で一度に実行していたら、被害はもっと大きくなっていたはずだ。1手ごとに止まって確認する進め方が、結果的にその被害を1記事だけに留めていた。

コマンドの「説明文」が、毎回ついていた理由

今日出てきたコマンドには、ほぼ毎回、何のためにそれを実行するのかという説明が添えられていた。「これで、PHPが5つとも出力しているかが分かります」「これで、サーバー上に実際に何が保存されているかを直接確認します」。

これは単なる丁寧さの問題ではなかったと思う。コマンドだけを渡されても、人間の側がその意味を理解していなければ、結果を見たときに「これは予想通りなのか、予想外なのか」を判断できない。判断できなければ、次に何をすべきかの相談もできなくなる。コマンドと、その目的の説明は、いつもセットで渡されていた。それによって、結果を報告する人間の側も、ただ画面を写すだけでなく、「これは想定通りでしたか」というやり取りに参加できていた。

Invoke-RestMethodという、PowerShellならではの道具

今日の作業の土台になっていたのがInvoke-RestMethodというコマンドだ。これはPowerShellに標準で組み込まれているコマンドレット(PowerShell用の命令の単位)で、Web APIに対してHTTPリクエストを送り、その結果を直接PowerShellのオブジェクトとして受け取れるようにする。

$response = Invoke-RestMethod -Uri ".../posts/14188?context=edit" -Headers $headers -Method Get
$content = $response.content.raw

-Uriはリクエスト先のURL、-Headersは認証情報などを含む追加情報、-MethodはGET(取得)やPOST(送信)といったHTTP通信の種類を指定する引数だ。curlのようなコマンドラインツールと役割は近いが、大きな違いは戻り値の扱い方にある。curlは基本的に文字列(テキスト)をそのまま返すのに対し、Invoke-RestMethodはサーバーが返したJSONを自動的に解析し、$response.content.rawのように、ドットでネストした階層をそのままたどれるPowerShellオブジェクトに変換してくれる。

これにより、「JSONをパースするコードを別に書く」という手間が要らない。$response.content.rawと書くだけで、レスポンスの中のcontentというキーの中のrawというキーの値を取り出せる。今日の二分探索やwp:htmlの検証作業がスムーズに進んだ背景には、この「APIの結果がすぐに使える形で手元に来る」という、PowerShell側の特性も関わっていた。

-replace演算子も、今日繰り返し登場した重要な道具だ。

$newContent = $oldContent -replace [regex]::Escape($oldMarker), $newBlock

-replaceはPowerShellに組み込まれた文字列演算子で、左側の文字列の中から、右側で指定したパターンに一致する部分を、別の文字列に置き換える。[regex]::Escape(...)は、置き換えたい文字列の中に正規表現として特別な意味を持つ記号(.(など)が含まれていても、それを「ただの文字」として扱うための前処理だ。これを省略すると、本文の中に偶然正規表現の特殊文字が含まれていた場合に、意図しない範囲まで置き換えられてしまう危険があった。

コマンドが生まれる場所は、人間の報告の中にあった

二分探索が進んでいたある場面で、「二つに分けていいのでは?デザイン崩れないでしょ?」という提案があった。この瞬間、それまでAIが続けていた「境界を1文字単位まで絞り込む」という方向のコマンド生成は、完全に止まった。代わりに、まったく別の種類のコマンド——本文を2つのブロックに分割して、それぞれを別々のwp:htmlブロックとして保存する処理——が、新しく組み立てられていった。

これは、AIが一人でコマンドを生成し続けていたのではなく、人間からの一言が、次に生成されるコマンドの種類そのものを変えていたことを示している。コマンドは、AIの中だけで完結して生まれていたのではなく、人間との往復の中で、その都度形を変えながら生まれていた。

結論:コマンドは、対話の結晶だった

今日出てきたコマンドを後から並べてみると、それぞれが独立した「便利な命令」ではなく、その時点までのやり取りすべてを背負った「結晶」のようなものだったと分かる。

ある仮説が外れたという報告があったから、次のコマンドはその仮説を除外する形になっていた。ある数値で成功したという報告があったから、次のコマンドはその数値を起点に組み立てられていた。ある提案があったから、コマンドの方向性そのものが変わっていた。

PowerShellのコマンドは、画面に打ち込まれて初めて意味を持つものだ。しかしそのコマンドが生まれる場所は、画面の中ではなく、その手前にある一連の対話——仮説を立て、確認し、報告し、また次の仮説を立てるという、人間とAIの往復の中にあった。今日、目の前で動いていたのは、コマンドそのものというより、その対話が形になっていく過程だったのだと思う。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です