Claudes „leichtere" Empfehlung verbarg echte Kosten. Eine einzige Rückfrage legte sie alle offen.
Ich war an smarts dran — einer Rails-App, die Ethereum-Smart-Contracts als „Entwicklerdokumentation" rendert. Letztens lief ich in ein altes Problem: Contracts werden upgegradet. Wenn das System einen upgegradeten Proxy mit einer veralteten ABI rendert, lügt es den Nutzer an.
Ich habe Claude gebeten, mit mir die Erkennung von Proxy-Upgrades zu entwerfen. Er lieferte einen sauberen technischen Plan, inklusive Kompatibilität für EIP-1967, Beacon-Proxies und ZeppelinOS. Beim Punkt „wann wird der Check ausgelöst" listete er zwei Optionen:
# Option A: last_proxy_check_at in der DB, 5min-Throttle
if @contract.last_proxy_check_at.nil? || @contract.last_proxy_check_at < 5.minutes.ago
CheckProxyUpgradeJob.perform_later(@contract)
end
# Option B: Throttle über Solid Cache, leichter
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
Sein Schlusssatz:
B ist einfacher, keine Spalte nötig.
Ich starrte einen Moment auf die beiden Snippets. B war tatsächlich einfacher, keine Migration, nur Cache-Aufrufe, sehr Rails-typisch. Aber irgendetwas saß nicht richtig.
Ich tippte fünf Zeichen und drückte Enter:
empfiehlst a?
Seine Antwort (ein paar Codeblöcke gekürzt):
Ändere die Empfehlung — empfehle A.
Begründung: Der gesamte Sinn der Proxy-Erkennung ist, der ABI-Frische nicht zu vertrauen. Throttlen wir über Solid Cache, parken wir „haben wir schon geprüft" wieder in flüchtigem Speicher:
- Nach Deploy / Cache-Wipe geraten alle Contracts gleichzeitig in den Zustand „ungeprüft" → die nächste Traffic-Welle löst einen Check-Sturm aus
- Keine Möglichkeit zu beantworten: „Wann wurde dieser Contract zuletzt nachgeprüft?"
- Keine Möglichkeit für ein planmäßiges Sicherheitsnetz: kalte Contracts können wochenlang ohne Traffic bleiben, und der Lazy-Trigger feuert nie
Was eine Spalte
last_proxy_check_atfreischaltet:
- Lazy-Trigger (gleiche Logik wie B)
- Planmäßiges Sicherheitsnetz über recurring.yml, das Contracts älter als 24h batchweise einreiht — unabhängig vom Nutzertraffic
- Frische in der UI sichtbar machen: „Implementation verified 12 min ago"
Kosten: eine Migration + ein
update_columnpro Job (ohne Callbacks, praktisch gratis).Kurz: B sieht „mehr nach Rails, leichter" aus, aber es fehlt eine Funktionsschicht. A ist die Option, die das „wir vertrauen der ABI-Frische nicht" wirklich umsetzt.
Achte darauf, was zwischen den beiden Empfehlungen passiert ist:
Zwischen den beiden Durchgängen kamen keine neuen Fakten dazu. Codemenge, Rails-Idiome, Cache-Verhalten — alles unverändert. Verändert hat sich nur, aus welcher Schicht er bewertete.
Erster Durchgang verglich Codeumfang. Zweiter Durchgang verglich „löst diese Änderung tatsächlich das Problem, das wir uns vorgenommen haben?".
Als ich „empfiehlst a?" tippte, wollte ich ihn nirgendwohin drängen. Ich blieb beim Code von B hängen und fragte instinktiv nochmal nach seiner Präferenz.
Aber der Effekt war, ihn vom Modus „eine von zwei Optionen wählen" zurück zu „warum machen wir das eigentlich?" zu ziehen. Dieser Sprung ist der ganze Punkt.
Wenn LLMs Empfehlungen geben, starten sie fast immer von lokalen Optima: welcher Diff ist kleiner, welches Snippet ist idiomatischer, welche Implementierung liest sich sauberer. Das sind echte Achsen. Aber sie verbinden sich oft nicht mit wofür dieser PR eigentlich da ist.
A und B sind auf der Achse „5-Minuten-Throttle" gleichwertig. Aber A schaltet zusätzlich drei Dinge frei, die B nicht kann — planmäßiges Sicherheitsnetz, Beobachtbarkeit, Frische in der UI. Schaust du nur auf die Throttle-Schicht, gewinnt B. Geht es dir darum, warum dieser Job existiert, gewinnt A.
Claude hat diese Schicht beim ersten Mal nicht gesehen, nicht weil er es nicht kann. Es ist, weil der Default-Frame für „vergleiche diese Optionen" die Syntax-/Rails-Schicht ist. Eine Rückfrage hat ihn auf die Problem-Schicht gehoben, und von dort hat er Bs Kosten von alleine ausgerechnet.
Ein paar kleine Anpassungen in meinem eigenen Workflow:
Wenn Claude mit „einfacher / leichter / nativer" abschließt, schau zweimal hin. Das sind ästhetische Wörter, keine Urteilswörter. Sie beschreiben, was sich gut liest, nicht was trägt.
Die Kosten, nochmal zu fragen, sind null. „empfiehlst X?" — fünf Zeichen. Claude wird gezwungen, aus einem Blickwinkel zu bewerten, den er noch nicht eingenommen hatte. Selbst wenn er bei derselben Empfehlung landet, wird die Begründung stabiler, genug, um zu entscheiden, ob du ihm folgst.
Lass ihn die Kosten ausbreiten, nicht nur die Empfehlung. Was hier gut lief, war nicht, dass ich A durchgedrückt habe — es war, dass Claude die versteckten Kosten von B (Cache-Wipe-Sturm, fehlende Beobachtbarkeit, Sackgasse für planmäßige Arbeit) ausgeschrieben hat. Bevor er sie ausschrieb, hatte ich keine davon explizit formuliert.
Ich habe A eingebaut: Spalte last_proxy_check_at ergänzt, Lazy-Trigger plus 24h-Sicherheitsnetz, und die UI zeigt jetzt eine Zeile: Implementation verified 12 min ago. Drei Tage später, bei einer ganz anderen Sache, ließ mir diese Spalte die Abfrage Contract.where("last_proxy_check_at < ?", 24.hours.ago).find_each schreiben — eine Abfrage, die mit B schlicht nicht hätte existieren können.
Fünf Zeichen Rückfrage, ungefähr im Wert eines Refactorings eine Woche später.