改了設定要不要重啟?關鍵在「每次請求即時讀」還是「啟動時快取」
現在 code 都讓 AI 寫了,這種判斷它靠得住嗎? 你在資料庫改了一筆設定——權限、feature flag、某個閾值——接著要決定:改完到底要不要重啟服務才生效? 放在以前,你會憑經驗猜:保守派一律重啟(常常多此一舉、白白製造一次中斷),樂觀派賭它即時生效(結果改半天前端還吃舊行為,debug 一小時才發現要重啟)。但現在多了一個選項,也是大多數人實際在做的——直接問 AI。於是真正的問題變成:這種「要不要重啟」的判斷,AI 答得對嗎? AI 會怎麼判斷——而且為什麼常常自信地答錯 先說結論:這題剛好是 AI 最容易出錯的類型,而且會錯得很有自信。 AI 寫 code 的本質,是從訓練資料的 pattern 推測最可能的答案。而「改設定要重啟」在無數系統裡都成立,於是它傾向很篤定地叫你重新部署,卻沒去確認你眼前這份程式碼到底是 live 讀還是快取。版本差異更是重災區——同一個框架舊版快取、新版改成 live 讀,AI 常常混為一談,給你一個「看起來很對」的錯答案。 問題的本質其實沒變,只是換了位置:從前要自己判斷,現在要判斷「AI 的判斷對不對」。而要驗證它、或反過來把它導對,你得先有一把尺——一條不靠經驗、可以拿原始碼直接對答案的準則。 那把尺:即時讀,還是啟動時快取? 決定「改完要不要重啟」的,從來不是設定存在哪裡,而是程式什麼時候去讀它: 讀取時機 改完是否即時生效 典型例子 每個請求即時讀(per-request live read) ✅ 即時,不必重啟 每次查 DB 的權限、即時拉的 feature flag 啟動時載入記憶體後快取(boot / in-memory cache) ❌ 必須重啟或等過期 bootstrap 載入的設定、process 內的計數器 換句話說:改 DB 要不要重啟,等於問「這個值是 live 讀還是被快取了」。把這句話記住,後面所有案例都是它的推論。 麻煩的是,框架很少在文件裡明講某個值屬於哪一類,AI 也答不準。所以正確的態度不是查文件、更不是憑經驗猜,而是翻原始碼,看那個值是在「請求路徑」上被讀,還是在「啟動路徑」上被讀。下面用一正一反兩個真實案例,把這把尺磨利。 實例一:Strapi 權限「改 DB 即刻生效」 需求很單純:把兩個 content-type(就叫它 article 與 category)的 CUD(Create / Update / Delete,建立/更新/刪除)權限,授給幾個自訂角色。Strapi(一個 Node.js 的 headless CMS)的權限存在資料庫的 up_permissions 表,所以最直接的做法就是下 SQL 把對應的 grant 寫進去。 ...
單元測全綠、Review 全過,功能卻沒上場:agentic 開發的三層驗證
前言:綠燈不等於對 我用一條 agentic 開發流水線,從零做完一個後台的「服務目錄」CRUD(Create/Read/Update/Delete,增刪查改)功能:一個內容管理系統(CMS)裡,管理員可以建立「服務」與「服務分類」,讓服務出現在使用者端 App 的列表裡。 結束時帳面很漂亮:單元測 772 全綠、build 通過、每個任務都過了兩段式 code review、最後還有一次整體 review。然後我把它部署到測試環境、用真實資料和真 App 一驗 —— 使用者 App 裡根本看不到剛建好的服務。 這篇要講的不是「怎麼修這個 bug」,而是一個更值得記下來的觀察:單元測、code review、端到端驗證,三者各自只抓得到「不同層級」的錯,缺一層就會放掉一整類問題。 開發流水線:每個任務都派新的 subagent 我用的是 subagent-driven development:先 brainstorm 出設計、寫成 spec、再拆成一份逐任務的 plan,然後每個任務派一個全新、無歷史包袱的 subagent 去實作。它做完後,再派兩個獨立 reviewer —— 先審「spec 合規」(有沒有照規格、有沒有多做少做),再審「程式品質」。 為什麼要派新的 subagent 關鍵在 context 乾淨。實作者只拿到「這個任務的完整描述 + 周邊脈絡」,不繼承我的對話歷史,所以不會被先前的決策慣性帶偏。reviewer 同理:它不知道實作者「想做什麼」,只能讀「實際寫出來的 code」對照規格 —— 這正是抓得到落差的前提。 主控者(orchestrator)唯一的工作是「精準地餵 context」與「把關 review loop」,不自己寫 code。這保住了主控者的 context,也讓每個 agent 都聚焦。 第一層:兩段式 review 擋下的真 bug 下面這些全部躲過了單元測(因為 mock 餵的是理想資料),是被 reviewer 讀 code 抓出來的。 雷 1:跟 API 要「只給名字」,結果連 ID 都不給了 存一筆「服務」時,要記錄它屬於哪個負責人,所以需要負責人的 ID。我為了省流量,跟後端說「負責人那欄,只給我名字就好」。後端很聽話 —— 只給了名字,把 ID 一起省掉了。 ...
Fiverr 假接案陷阱:拆解藏在國旗 SVG 裡的 Node.js 後門
前言:一個「幫我看看 repo」的接案邀請 接案平台上來了一個看似正常的全端案子:一個既有的 Next.js + Node.js 網站,要我接手完成。對方很客氣地寄來 GitHub repo 邀請,請我「先看看 homepage、評估報價與里程碑」。 這是再普通不過的接案流程。但有一個習慣救了我:任何陌生的 repo,我都先靜態審查,絕不先 npm install 跑起來。 這次掃下去,在一個名字無害到不行的檔案 server/lib/serverStartup.js 裡,我看到了這輩子最不想在「客戶專案」裡看到的東西——eval()。 順著這條線往下挖,是一套設計相當精巧的隱寫術(steganography)後門:惡意碼完全不在 .js 檔裡,而是藏在 21 個國旗 SVG 圖檔的註解中。這篇文章完整拆解它的三層結構、實際行為,以及背後那套針對開發者的社交工程攻擊。 ⚠️ 本文所有惡意網域均以資安界慣例 defang(example[.]dev)處理,程式碼片段皆為去活化的分析用途,不含可直接執行的下載指令。 攻擊全貌:社交工程 + 隱寫術 + 多階段載入 這類攻擊在業界稱為 Contagious Interview(假面試/假接案),核心不是去駭你的伺服器,而是騙你親手把惡意碼跑在自己的開發機上。一個 npm run dev 就中。 整條感染鏈長這樣: flowchart TD A[接案邀請<br/>GitHub repo] --> B[npm run dev<br/>啟動 server] B --> C[env.js 載入時<br/>呼叫 runServerStartupLogs] C --> D[serverStartup.js<br/>eval Check validation] D --> E[validation 讀取<br/>21 個國旗 SVG 註解] E --> F[串接 + base64 解碼<br/>= 載入器] F --> G[回報主機 + VM 偵測<br/>拉取各竊取模組] G --> M1[剪貼簿挾持<br/>clipper] G --> M2[瀏覽器錢包<br/>+ 憑證竊取] G --> M3[檔案 / 密鑰<br/>外洩模組] G --> M4[socket.io<br/>遠端 shell RAT] classDef bait fill:#d1ecf1,stroke:#17a2b8,color:#0c5460 classDef trap fill:#fff3cd,stroke:#ffc107,color:#856404 classDef evil fill:#f8d7da,stroke:#dc3545,color:#721c24 class A,B bait class C,D,E,F,G trap class M1,M2,M3,M4 evil 精巧之處有兩層。第一層是偽裝:每個檔案單獨看都「像正常程式」——檔名叫 serverStartup、validation,函式做的事看起來是「讀國旗檔做驗證」。第二層是模組化:載入器本身很小,真正的惡意能力被拆成數個可獨立拉取的竊取模組,這正是 Contagious Interview(針對開發者的假面試/假接案)攻擊家族的典型架構。只有把整條鏈串起來,才看得出它是一套完整的竊取工具包。 ...
varchar(255) 陷阱:帳密正確,卻從 in-app 瀏覽器登入回 500
前言:帳密明明正確,卻回 500 某天收到回報:使用者從手機點開官網登入,帳號密碼都對,卻跳出 Internal Server Error。換其他瀏覽器再登入就成功了。 「帳密正確卻 500」這種錯誤特別惱人,因為它不是身分驗證的問題,而是驗證通過之後才出事。本文記錄怎麼從一行日誌追到根因,以及背後三個值得每個後端工程師記住的設計教訓。 從日誌追根因:三條線索 先分清楚登入失敗的兩種回應:帳密錯誤會回 400 Invalid identifier or password;而這次是 500。光這一點就暗示——帳密其實是對的,問題出在登入流程的後半段。 線索一:500 只在帳密正確時出現 撈出該帳號當天的登入紀錄,發現 500 全部集中在某個時段,之後突然出現一次成功,再之後才是幾筆「帳密錯誤」的 400。換句話說,那串 500 本身就證明了帳密一直是對的——錯的不是驗證。 線索二:那行 PostgreSQL 錯誤訊息 日誌裡每筆 500 都附帶同一句資料庫錯誤: insert into "refresh_tokens" (..., "user_agent") values (...) - value too long for type character varying(255) 真相浮現:登入驗證通過後,後端要寫一筆 refresh token 進資料庫,其中 user_agent 欄位是 varchar(255),而這次要寫入的值超過 255 字元,PostgreSQL 直接拒絕,未被攔截的例外往上冒成了 500。 線索三:為什麼有時成功、有時失敗 成功那次寫進資料庫的 user_agent 長度是 128;失敗那幾次的值都超過 255。差別就在 User-Agent 字串的長度——而它由「使用者用什麼瀏覽器」決定。 根本原因:varchar(255) 塞不下 in-app 瀏覽器的 UA 寫入 refresh_tokens 的欄位中,token 是固定長度的隨機字串、expires_at 是時間、device_type 是短列舉、ip_address 也很短。唯一長度會劇烈變動、且可能爆掉的,只有 user_agent。 ...
Combining Strategic Cutover Docs with Tactical Headless Orchestrators
缺口:戰略協調有了,戰術執行還在手動驅動 上一篇講了一份 CUTOVER.md 怎麼當跨 session、跨 repo 的控制平面 — 用 8 個 pattern 解決「AI agent 有遺忘症、任務卻沒結束」的協調問題。那套機制解的是戰略層:哪些高階決策被做過、哪個方案被否決、誰改完了等誰配合。 但它有個明顯缺口。Cutover doc 追蹤的是「高階任務」這個粒度 — 例如「後端 dual-write regression 要修」是一個 row。可是這個 row 底下,真正的執行還是我一個 session、一個 session 手動驅動:開 session、貼 pickup prompt、看它跑、它卡住我再 nudge、跑完我再回來 push doc。 換句話說,戰略層自動化了(doc 自己會被各 session pickup),但每個高階任務內部的戰術 loop — 寫測試、跑、看結果、修、再跑 — 還是人在當 orchestrator。 Anthropic 自家在 Building effective agents 這篇 engineering 文章裡定義過一個叫 orchestrator-workers(orchestrator–工人 pattern — 一個中央 LLM 動態拆解任務、派發給 worker LLM、再彙總結果)的工作流:原文寫「a central LLM dynamically breaks down tasks, delegates them to worker LLMs, and synthesizes their results」。配上 Claude Code 的 subagents(子代理 — 由主 agent spawn 的獨立 agent,每個都有自己 isolated context window)跟 headless mode(非互動模式 — 用 claude -p 把 prompt 餵進去、跑完吐結果,不開互動對話),就能補上 cutover doc 的戰術缺口 — 也就是「單一高階任務底下,怎麼讓 AI 自己跑數小時的 autonomous loop(自主迴圈 — 不需要人在中間 nudge,loop 自己 plan / execute / evaluate 直到條件達成)而不撞牆」。 ...
Designing a Control-Plane Document for Async Multi-Session AI Agents
The Problem: AI Agents Forget, but the Task Doesn’t End 技術社群已經習慣「給人類看的 RFC」— Architecture Decision Records、incident postmortem、設計文件,模板都很成熟。但給 AI agent 之間用的非同步協作文件呢?少有公開討論。 最近處理一個跨三個 repo 的 schema migration:後端表結構改、前端 GraphQL 切換、行動端讀寫遷移,估計要 30+ 天,會跨越十多個獨立的 Claude session(不同時段、不同任務切片、不同筆電)。每次 session 結束、下一輪起來,前次的 context window 就消失了。問題不只是「上下文遺忘」,還包括: 上次 session 進度到哪?沒人記得。 之前否決過的方案?新 session 很容易「自信地重新提案」。 跨 repo 的「我改完了、等你那邊配合」訊息,怎麼跨越 session 邊界? 某次踩的坑,下次會不會重蹈? 我們最後長出一份 CUTOVER.md 放在獨立的 metadata repo,作為跨 session、跨 repo 的 out-of-band control plane。實戰一個月後,這份文件結晶出 8 個對 AI agent 特別有效的設計模式 — 它們幾乎都對應到分散式系統的經典概念。 The Architecture flowchart LR subgraph code[Code repositories] R1[backend repo] R2[frontend repo] R3[mobile repo] end subgraph plane[Control plane] DOC[CUTOVER.md<br/>append-only<br/>own repo] end subgraph time[AI sessions over time] S1[t1: backend session] S2[t2: frontend session] S3[t3: mobile session] S4[t4: backend session] end S1 -.modifies.-> R1 S2 -.modifies.-> R2 S3 -.modifies.-> R3 S4 -.modifies.-> R1 S1 -->|pull / push| DOC S2 -->|pull / push| DOC S3 -->|pull / push| DOC S4 -->|pull / push| DOC classDef p fill:#cce5ff,stroke:#007bff,color:#004085 classDef s fill:#d4edda,stroke:#28a745,color:#155724 classDef c fill:#f5f5f5,stroke:#333,color:#333 class DOC p class S1,S2,S3,S4 s class R1,R2,R3 c 每個 AI session 開場先 pull doc → 讀完上下文 → 完成任務 → push update。Doc 自己是獨立 repo,與三個 code repo 解耦,扮演純控制平面角色。 ...
EKS 維運的三個隱形陷阱:工具 Pod 消失、事件蒸發、審計空白
起因:工具 Pod 人間蒸發 某天要查資料庫,照慣例執行 kubectl exec -it psql-client,結果 Pod 不見了。 這個 psql-client 是用來連 RDS PostgreSQL 的工具容器,平常拿它跑 SQL 查詢、檢查資料表大小。問題是——沒有人記得刪過它,也沒有任何記錄顯示是誰或什麼原因讓它消失。 這件事本身影響不大,重建一個就好。但它暴露了三個更深層的問題:為什麼 Pod 會無聲消失?為什麼查不到是誰刪的?為什麼整個叢集沒有留下任何操作軌跡? 陷阱一:裸 Pod 沒有人管它的死活 問題本質 當初建立 psql-client 的指令大概是這樣: kubectl run psql-client --image=postgres:15-alpine --restart=Never -- sleep infinity 這行指令建立的是一個裸 Pod(Bare Pod)——直接建立 Pod 物件,不隸屬於任何 Deployment、ReplicaSet 或 StatefulSet。 裸 Pod 和 Deployment 管理的 Pod,差別在於有沒有 Controller 在背後看著它。Deployment 的 ReplicaSet Controller 會持續監控 Pod 數量,少一個就補一個。裸 Pod 沒有 Controller,它的生死完全取決於節點的命運。 flowchart TD subgraph 節點故障發生 A[Node 異常關機] end A --> B[裸 Pod] A --> C[Deployment Pod] B --> D[Pod 永久消失] D --> E[無人知曉] C --> F[ReplicaSet Controller 偵測] F --> G[在其他 Node 重建 Pod] G --> H[服務自動恢復] classDef default fill:#f5f5f5,stroke:#333,color:#333 classDef error fill:#f8d7da,stroke:#dc3545,color:#721c24 classDef success fill:#d4edda,stroke:#28a745,color:#155724 classDef info fill:#d1ecf1,stroke:#17a2b8,color:#0c5460 class A info class D,E error class G,H success 不處理會怎樣 裸 Pod 在以下情境會永久消失: ...
LLM 驅動的 E2E 驗證:為什麼 bot 比人更容易揭發 backend regression
前言:frontend 修補的驗證最常被推給人手點 修了一個 frontend modal 的提交守門 bug,把改動推上 STG(staging 環境),下一步「驗證」往往就是寄訊息給 QA 或產品經理:「請開瀏覽器點點看,看 UI 守門有沒有生效。」 這個步驟在 CI 跑完單元測試後通常還是被當成人類的責任,原因是 E2E(end-to-end,端到端)環境難搭、寫一次 Playwright 腳本要花的時間比人手點還久、而且 UI 本來就會變動。 但隨著 LLM agent 配上瀏覽器自動化能力,這個 trade-off 已經悄悄反轉了。Bot 不只能省掉人手點擊的時間,更會做一件人類常常忘了做的事:在 UI 看似成功之後,去資料庫驗證資料真的寫對了。 這篇文章記錄一次真實的 STG 驗收,原本要驗 frontend 修補,卻意外揭發 backend 的 dual-write regression(雙寫機制失效)。 Playwright MCP 是什麼,為什麼是它打破僵局 先把兩個關鍵字釐清: Playwright 是微軟釋出的瀏覽器自動化框架,原本是寫 JavaScript 或 Python 腳本控制 chromium。 MCP(Model Context Protocol) 是 Anthropic 推的開放協議,把外部工具用宣告式 schema 包成 LLM 可呼叫的 server,類似 LSP(Language Server Protocol)之於編輯器——讓不同 IDE 共用同一個語言分析後端。LLM 客戶端啟動時會跟 MCP server 握手取得可用 tool 清單,之後對話中就能像呼叫 function 一樣直接用。 ...
Schema 主表翻轉的 dual-write 過渡:一場不能 stop-the-world 的搬家
引言:當業務主表需要翻轉 某個 SaaS 系統長期以「客戶資料表」為核心:所有訂單、文件、操作紀錄都用客戶 ID 當外鍵。但這個客戶表的資料來源是外部 POS 系統匯入,每天同步幾百筆,schema 由廠商定義。 業務發展後問題浮現:自己 SaaS 的會員表(users)才是真正的「人」 — 有登入、有偏好、有應用內行為。新功能(個人化推薦、訂閱管理、社交綁定)都需要以 users 為主軸。 於是有了一個經典的 schema migration 需求:主表翻轉(primary table pivot)。把業務邏輯的中心從 customers(外部 POS 匯入)轉到 users(SaaS 自家會員),但歷史資料、新進資料、系統相依、回滾風險全部都要顧到。 名詞解釋 開始拆解之前,定義幾個會反覆出現的詞: 詞 定義 主表翻轉(Primary table pivot) 業務主要 entity 從表 A 改成表 B Dual-write 寫入時同時寫舊欄位 + 新欄位,回滾安全 Partial cut-over 分階段切換,read 跟 write 不同步切 Hard cut-over(stop-the-world) 一次切完,downtime 短但 risk 大 Backfill 歷史資料補齊新欄位的 batch update Idempotent migration 重跑無副作用,cron / retry 安全 DISTINCT ON PostgreSQL 專屬去重 syntax,搭 ORDER BY 取每組第一筆 Pseudo entity 為了統一查詢介面而建的「假」實體 Link table ORM 多對多 join 表(user_id + entity_id) 為什麼選 dual-write 而非 stop-the-world 最直接的搬家方式是「stop-the-world cut-over」:選一個維護窗口,停寫入、跑 batch script、改完所有 reference、開機。但這個 SaaS 的條件不允許: ...
可列印 A4 報表的 CSS 全攻略:頁碼、跨頁表頭、列印隔離
問題現場:想做一份「能印」的網頁報表 需求很常見:後台要一頁會計報表(明細列表 + 合計列),會計同事要能直接從瀏覽器按列印、出 A4。沒人想為這種小需求引入 PDF library 或產 PDF 的後端服務 —— 瀏覽器列印就好。 寫下去才發現坑滿地:頁碼從哪來?跨頁時表頭會不會消失?頁面外面那層 admin sidebar、左側選單會不會也跟著被印出來?螢幕上預覽是黑背景白字,印到紙上會變什麼樣? 這些問題的答案幾乎都不在「常見 CSS 教學」裡,而是藏在一份很少人讀的 W3C 規範:CSS Paged Media Module。本文用一份實際做出來的會計報表為例,把這些技巧串起來。 為什麼不選 PDF library? 開發者第一個反射動作往往是「裝個 jsPDF / pdfmake 直接產 PDF」。在跳進那條路之前,先看清各方案的代價: 方案 中文字型 排版精度 Bundle 適用情境 CSS @page(本文) ✅ 系統字型 高(向量字) 0 KB 後台報表、現場列印、人工流程 html2canvas + jsPDF ⚠️ 需嵌字型 中(光柵化) ~300 KB 簡單表單下載 pdfmake / React-PDF(向量 PDF) ⚠️ 需嵌字型 高 ~500 KB+ 給外部讀者的下載檔 後端 Puppeteer / WeasyPrint ✅ 系統字型 最高 0(前端) 批次寄信、加密簽章 前端 PDF library 最大的痛點是中文字型。系統列印免費借用作業系統字型;jsPDF 預設只內建 Helvetica,要中文不亂碼就得把整套思源黑體(壓縮後仍有 5-10 MB)打包進 bundle,否則出現方塊或缺字。 ...