คำแนะนำ "ที่เบากว่า" ของ Claude ซ่อนต้นทุนจริงไว้ คำถามย้อนแค่ประโยคเดียวก็ทำให้มันเปิดเผยออกมา
ผมกำลังแก้ smarts — แอป Rails ที่เรนเดอร์สมาร์ตคอนแทรกต์ของ Ethereum ออกมาเป็น "เอกสารสำหรับนักพัฒนา" วันก่อนเจอปัญหาเก่าอีกครั้ง คอนแทรกต์ถูกอัปเกรดได้ ถ้าระบบเรนเดอร์ proxy contract ที่ถูกอัปเกรดแล้วโดยใช้ ABI เก่า ก็เท่ากับโกหกผู้ใช้
ผมขอให้ Claude มาช่วยออกแบบกลไกตรวจจับ proxy upgrade ด้วยกัน มันให้แผนเทคนิคที่ค่อนข้างครบ รวมถึงรองรับ EIP-1967, Beacon proxy และ ZeppelinOS พอถึงขั้น "เมื่อไรจึงจะ trigger การตรวจสอบ" มันยกสองตัวเลือกออกมา:
# ตัวเลือก A: เพิ่ม last_proxy_check_at ใน DB, throttle 5 นาที
if @contract.last_proxy_check_at.nil? || @contract.last_proxy_check_at < 5.minutes.ago
CheckProxyUpgradeJob.perform_later(@contract)
end
# ตัวเลือก B: throttle ผ่าน 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 ง่ายกว่าจริง ๆ ไม่มี migration เป็นการเรียก cache ล้วน ๆ สไตล์ Rails ก็เข้ากัน แต่มีบางอย่างไม่เข้าที่
ผมพิมพ์ห้าตัวอักษรแล้วกด enter:
a ดีไหม?
คำตอบ (ตัด code block บางส่วน):
เปลี่ยนใจ — แนะนำ A
เหตุผล: เป้าหมายทั้งหมดของการตรวจ proxy คือ ไม่ เชื่อความสดของ ABI ถ้า throttle ผ่าน Solid Cache เราก็เอา "ตรวจไปแล้วหรือยัง?" ไปวางไว้ในหน่วยเก็บที่ระเหยได้อีกรอบ:
- หลัง deploy / cache wipe คอนแทรกต์ทุกตัวเข้าสถานะ "ยังไม่ตรวจ" พร้อมกัน → คลื่นทราฟฟิกถัดมาจะ trigger พายุการตรวจสอบ
- ตอบคำถาม "คอนแทรกต์ตัวนี้ไม่ได้ตรวจซ้ำมานานเท่าไหร่?" ไม่ได้
- ทำ ตาข่ายนิรภัยแบบมีกำหนดเวลา ไม่ได้: คอนแทรกต์ที่คนเข้าน้อยอาจไม่มีทราฟฟิกเป็นสัปดาห์ ๆ และ lazy trigger จะไม่ทำงานเลย
สิ่งที่คอลัมน์
last_proxy_check_atปลดล็อก:
- Lazy trigger (ลอจิกเดียวกับ B)
- ตาข่ายนิรภัยแบบมีกำหนดเวลา ผ่าน recurring.yml ที่ enqueue คอนแทรกต์ที่เกิน 24 ชม. เป็น batch — ไม่ขึ้นกับทราฟฟิกผู้ใช้
- โชว์ความสดบน UI: "Implementation verified 12 min ago"
ต้นทุน: migration หนึ่งครั้ง +
update_columnหนึ่งครั้งต่อ job (ไม่ผ่าน callback, แทบฟรี)สรุปสั้น ๆ: B ดู "เป็น Rails-native มากกว่า เบากว่า" แต่ขาดความสามารถไปทั้งชั้น A ต่างหากที่ทำให้สโลแกน "เราไม่เชื่อความสดของ ABI" เกิดขึ้นจริง
สังเกตสิ่งที่เกิดระหว่างคำแนะนำสองรอบ:
ระหว่างสองรอบไม่มีข้อเท็จจริงใหม่เข้ามา ปริมาณโค้ด ภาษาแบบ Rails พฤติกรรม cache เหมือนเดิมหมด สิ่งที่เปลี่ยนคือ มันยืนอยู่ที่ชั้นไหน ตอนประเมิน
รอบแรกเปรียบเทียบปริมาณโค้ด รอบสองเปรียบเทียบ "การเปลี่ยนแปลงนี้แก้ปัญหาที่ตั้งไว้แต่แรกได้จริงไหม?"
ตอนพิมพ์ "a ดีไหม?" ผมไม่ได้พยายามดันมันไปไหน แค่ติดอยู่กับโค้ดของ B แล้วถามถึงความชอบของมันอีกครั้งโดยสัญชาตญาณ
แต่ผลคือดึงมันออกจากโหมด "เลือกหนึ่งจากสอง" กลับไปที่ "ทำไมเราถึงทำสิ่งนี้?" ช่องว่างนี่แหละคือทั้งหมด
เวลา LLM ให้คำแนะนำ มันเริ่มจาก local optimum แทบทุกครั้ง: diff ไหนเล็กกว่า สนิปเป็ตไหนดูเป็นภาษามากกว่า การ implement แบบไหนอ่านสะอาดกว่า เป็นแกนที่จริงทั้งหมด แต่หลายครั้งไม่เชื่อมกับว่า PR นี้มีอยู่เพื่ออะไร
A กับ B สมมูลกันบนแกน "throttle 5 นาที" แต่ A ปลดล็อกเพิ่มอีกสามอย่างที่ B ทำไม่ได้ — ตาข่ายแบบมีกำหนดเวลา observability ความสดบน UI ถ้ามองแค่ชั้น throttle B ชนะ ถ้าสนใจว่าทำไม job นี้ถึงมีอยู่ A ชนะ
Claude ไม่เห็นชั้นนี้รอบแรก ไม่ใช่เพราะมันเห็นไม่ได้ แต่เพราะ frame เริ่มต้นของ "เปรียบเทียบตัวเลือกพวกนี้" คือชั้น syntax/Rails คำถามย้อนหนึ่งคำดึงมันขึ้นไปยังชั้นปัญหา จากนั้นมันก็คำนวณต้นทุนของ B เองได้
การปรับเล็ก ๆ ที่ผมเอาเข้า workflow ตัวเอง:
เวลา Claude ปิดด้วย "ง่ายกว่า / เบากว่า / native กว่า" ให้มองสองครั้ง คำพวกนี้เป็นคำเชิงสุนทรียะ ไม่ใช่คำตัดสิน บอกว่าอ่านสบาย ไม่ได้บอกว่ารับน้ำหนักได้
ต้นทุนของการถามซ้ำเป็นศูนย์ "X ดีไหม?" — ห้าตัวอักษร Claude จะถูกบังคับให้ประเมินจากมุมที่มันยังไม่เคยยืน ต่อให้สุดท้ายได้คำแนะนำเดิม เหตุผลก็จะหนักแน่นขึ้น พอให้คุณตัดสินใจได้ว่าจะเชื่อไหม
ให้มันแจกแจง ต้นทุน ออกมา ไม่ใช่แค่คำแนะนำ ผลที่ดีในรอบนี้ไม่ใช่เพราะผมดัน A — แต่เพราะ Claude เขียนต้นทุนซ่อนของ B (พายุหลัง cache wipe การไม่มี observability ทางตันสำหรับงานตามกำหนดเวลา) ออกมาเอง ก่อนหน้านั้นผมเองก็ยังคิดไม่ออกสักข้อ
สุดท้ายผมปล่อย A: เพิ่มคอลัมน์ last_proxy_check_at lazy trigger บวกกับตาข่าย 24 ชม. UI แสดงบรรทัด Implementation verified 12 min ago สามวันต่อมา ตอนทำงานคนละเรื่อง คอลัมน์นี้ทำให้ผมเขียน Contract.where("last_proxy_check_at < ?", 24.hours.ago).find_each ได้ — query ที่ถ้าเลือก B ตั้งแต่แรก จะเขียนไม่ได้เลย
ห้าตัวอักษรของคำถามย้อน มีค่าประมาณเท่ากับ refactor ครั้งหนึ่งในสัปดาห์ถัดไป