Claude が出した「より軽い」推奨には隠れたコストがあった。一言の問い返しで全部出させた。
smarts という、Ethereum のスマートコントラクトを「開発者向けドキュメント」として描画する Rails アプリをいじっていた。先日、古い問題にぶつかった——コントラクトはアップグレードされる。アップグレード済みのプロキシコントラクトを古い ABI で描画してしまうと、システムがユーザーに嘘をついていることになる。
Claude にプロキシのアップグレード検出を一緒に設計してもらった。EIP-1967 / Beacon proxy / ZeppelinOS への対応まで含めた、しっかりした技術プランを書いてくれた。「いつチェックを発火させるか」の段になって、選択肢を 2 つ並べてきた:
# 選択肢 A: DB に last_proxy_check_at を追加、5min スロットル
if @contract.last_proxy_check_at.nil? || @contract.last_proxy_check_at < 5.minutes.ago
CheckProxyUpgradeJob.perform_later(@contract)
end
# 選択肢 B: Solid Cache でスロットル、より軽い
unless Rails.cache.exist?("proxy_check:#{@contract.id}")
Rails.cache.write("proxy_check:#{@contract.id}", true, expires_in: 5.minutes)
CheckProxyUpgradeJob.perform_later(@contract)
end
最後の一言:
B のほうがシンプル、列を追加する必要なし。
このコードを少し見つめた。B は確かにシンプルで、マイグレーションもなく、全部 cache 呼び出し、Rails らしさもある。でも、なんとなく腑に落ちない。
3 文字を打って Enter を押した:
a 推す?
その返事(コードブロックをいくつか省略):
考えを変える——A を推す。
理由:プロキシ検出の目的そのものが「ABI の鮮度を信用しない」こと。Solid Cache でスロットルするということは、「チェック済みかどうか」をまた揮発性ストレージに置くことになる:
- デプロイ / cache wipe 後、全コントラクトが一斉に「未チェック」状態になる → 次のアクセス波でチェック嵐が発生
- 「このコントラクトは最後のチェックから何日経った?」という質問に答えられない
- 定期的なフォールバックができない:アクセスが少ないコントラクトは何週間も lazy 発火されない可能性がある
last_proxy_check_at列を追加すると解禁できること:
- lazy 発火(B と同じロジック)
- recurring.yml で 24h 以上未チェックのコントラクトをバッチでキューに入れる定期フォールバック、ユーザーアクセスから独立
- UI に鮮度を出せる:「Implementation verified 12 min ago」
コスト:マイグレーション 1 回 + ジョブ完了時の
update_column1 回(callback を経由しないので、ほぼゼロコスト)。要するに:B は「より Rails らしく、軽く」見えるが、機能的には一段足りない。A こそが「ABI の鮮度を信用しない」というこの仕事を実際に成立させる選択肢。
2 回の推奨の間に何が起きたかに注目してほしい:
その間に新しい事実は入っていない。コード量、Rails の慣習、cache の挙動、すべて変わっていない。変わったのは、評価するときにどの層に立っていたかだけ。
1 回目はコード量を比べていた。2 回目は「この変更は本来解こうとしていた問題を本当に解いているか」を比べていた。
この 3 文字を打ったとき、Claude を何かに追い込もうとは思っていなかった。B のコードのところで止まって、もう一度好みを聞きたくなっただけだ。
でも、効果は「2 つから選ぶ」状態から「なぜこれをやるのか」に引き戻すことだった。この落差がすべて。
LLM の推奨はほぼ必ず局所最適から始まる:どの diff が小さいか、どのスニペットが慣用的か、どの実装が読みやすいか。どれも真っ当な軸ではある。でも、それらは「この PR が何を解こうとしているのか」と切り離されていることが多い。
A と B は「5 分スロットル」という軸では等価。だが A は B にできない 3 つを副次的に解禁する——定期フォールバック、可観測性、UI での鮮度表示。スロットル層だけ見るなら B、なぜこのジョブが存在するかを気にするなら A。
Claude が 1 回目にこの層を見られなかったのは、見られないからではない。「これらの選択肢を比べる」のデフォルトの視点が「シンタックス層 / Rails 層」だからだ。一言の問い返しで「問題層」まで引き上げると、B のコストは自分で計算できる。
自分のワークフローに加えた小さな調整:
Claude が「シンプル / 軽い / より自然」で締めくくったら、もう一度見る。 これらは美意識の言葉で、判断の言葉ではない。「読みやすい」を語っているだけで、「ちゃんと動く」を語っているわけではない。
もう一度聞くコストはほぼゼロ。 「X 推す?」——3 文字。Claude はまだ立ったことのない角度から再評価せざるを得なくなる。最終的に同じ推奨に着地しても、根拠はより堅くなり、信用するかの判断材料になる。
推奨だけじゃなく、コストを並べさせる。 今回うまくいったのは A をゴリ押しした結果じゃない。Claude が B の隠れたコスト(cache wipe 嵐、可観測性の欠如、定期処理の行き止まり)を書き出してくれたからだ。書き出される前は、自分でも言語化できていなかった。
最終的に A を投入した:last_proxy_check_at 列を追加、lazy 発火 + 24h の定期フォールバック、UI に Implementation verified 12 min ago の一行。3 日後、別の作業をしているときに、この列のおかげで Contract.where("last_proxy_check_at < ?", 24.hours.ago).find_each というクエリが書けた——B にしていたら絶対に書けなかったやつ。
3 文字の問い返しの価値は、だいたい 1 週間後のリファクタリング 1 回分。