„Lżejsza" rekomendacja Claude'a kryła prawdziwe koszty. Jedno kontrpytanie wszystko ujawniło.
Pracowałem nad smarts — aplikacją Rails, która renderuje smart kontrakty Ethereum jako „dokumentację dla deweloperów". Niedawno wpadłem na stary problem: kontrakty są aktualizowane. Jeśli system renderuje zaktualizowany kontrakt proxy używając przestarzałego ABI, kłamie użytkownikowi.
Poprosiłem Claude'a o zaprojektowanie ze mną wykrywania upgrade'ów proxy. Wystawił solidny plan techniczny z kompatybilnością z EIP-1967, Beacon proxy i ZeppelinOS. Przy „kiedy uruchamiać sprawdzenie" wyłożył dwie opcje:
# Opcja A: dodać last_proxy_check_at do bazy, throttle 5 min
if @contract.last_proxy_check_at.nil? || @contract.last_proxy_check_at < 5.minutes.ago
CheckProxyUpgradeJob.perform_later(@contract)
end
# Opcja B: throttle przez Solid Cache, lżejsze
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
Końcowe zdanie:
B jest prostsze, kolumna niepotrzebna.
Przez chwilę gapiłem się na oba fragmenty. B faktycznie było prostsze, bez migracji, same wywołania cache, bardzo Rails-owo. Ale coś nie grało.
Wbiłem pięć znaków i nacisnąłem enter:
polecasz a?
Jego odpowiedź (kilka bloków kodu wycięte):
Zmieniam zdanie — polecam A.
Powód: cały sens wykrywania proxy polega na tym, żeby nie ufać świeżości ABI. Jeśli throttluje przez Solid Cache, znów kładziemy „czy już sprawdzaliśmy?" w pamięci ulotnej:
- Po deployu / cache wipe wszystkie kontrakty wchodzą jednocześnie w stan „niesprawdzonych" → następna fala ruchu uruchamia burzę sprawdzeń
- Nie ma jak odpowiedzieć na „kiedy ten kontrakt był ostatnio re-weryfikowany?"
- Nie da się zrobić zaplanowanego zabezpieczenia: zimne kontrakty mogą tygodniami nie mieć ruchu, a lazy trigger nigdy nie odpali
Co odblokowuje dodanie kolumny
last_proxy_check_at:
- Lazy trigger (ta sama logika co B)
- Zaplanowane zabezpieczenie przez recurring.yml, które wsadzanie partiami kontrakty starsze niż 24h — niezależnie od ruchu użytkowników
- Pokazanie świeżości w UI: „Implementation verified 12 min ago"
Koszt: jedna migracja + jeden
update_columnna job (bez callbacków, praktycznie za darmo).Krótko: B wygląda na „bardziej Rails-native, lżejsze", ale brakuje mu całej warstwy możliwości. A to opcja, która naprawdę realizuje „nie ufamy świeżości ABI".
Zauważ, co stało się między dwiema rekomendacjami:
Między oboma przebiegami nie pojawił się żaden nowy fakt. Objętość kodu, idiomy Rails, semantyka cache — wszystko to samo. Zmieniła się tylko warstwa, z której oceniał.
Pierwszy przebieg porównywał objętość kodu. Drugi porównywał „czy ta zmiana faktycznie rozwiązuje problem, który postawiliśmy?".
Kiedy wpisywałem „polecasz a?", nie próbowałem nigdzie go popchnąć. Po prostu zaciąłem się na kodzie B i odruchowo dopytałem o jego preferencję.
Ale efekt był taki, że wyciągnąłem go z trybu „wybierz jedną z dwóch opcji" z powrotem do „po co to robimy?". Ta różnica to cały punkt.
Kiedy LLM-y dają rekomendacje, prawie zawsze startują z lokalnych optimów: który diff jest mniejszy, który snippet bardziej idiomatyczny, która implementacja czyściej się czyta. To są prawdziwe osie. Ale często rozłączają się od tego, do czego ten PR w ogóle służy.
A i B są równoważne na osi „throttle 5-minutowy". Ale A dodatkowo odblokowuje trzy rzeczy, których B zrobić nie może — zaplanowane zabezpieczenie, obserwowalność, świeżość w UI. Jeśli patrzysz tylko na warstwę throttle, wygrywa B. Jeśli zależy ci, po co istnieje ten job, wygrywa A.
Claude nie zobaczył tej warstwy za pierwszym razem nie dlatego, że nie potrafi. To dlatego, że domyślny frame dla „porównaj te opcje" to warstwa składni/Rails. Jedno kontrpytanie wyciągnęło go na warstwę problemu, a stamtąd policzył już koszty B sam.
Kilka małych korekt w moim workflow:
Kiedy Claude kończy na „prostsze / lżejsze / bardziej natywne", popatrz dwa razy. To słowa estetyczne, nie osądzające. Opisują to, co dobrze się czyta, a nie to, co utrzyma obciążenie.
Koszt dopytania jest zerowy. „polecasz X?" — pięć znaków. Claude jest zmuszony ocenić z kąta, którego jeszcze nie zajął. Nawet jeśli wyląduje na tej samej rekomendacji, uzasadnienie będzie mocniejsze — wystarczająco, żebyś zdecydował, czy mu zaufać.
Każ mu rozpisać koszty, nie tylko rekomendację. To, że tym razem dobrze wyszło, nie wynika z tego, że dopchałem A — wynika z tego, że Claude sam wypisał ukryte koszty B (burza po cache wipe, brak obserwowalności, ślepy zaułek dla zaplanowanych zadań). Sam nie sformułowałem żadnego z nich, zanim on tego nie zrobił.
Ostatecznie wdrożyłem A: dodałem kolumnę last_proxy_check_at, lazy trigger plus 24-godzinne zabezpieczenie, UI pokazuje teraz linijkę Implementation verified 12 min ago. Trzy dni później przy zupełnie innej robocie ta kolumna pozwoliła mi napisać Contract.where("last_proxy_check_at < ?", 24.hours.ago).find_each — zapytanie, którego z B po prostu by nie istniało.
Pięć znaków kontrpytania to mniej więcej wartość refaktoryzacji tydzień później.