作者: tm731531

  • Hacker News 每日精選 – 2026-04-07

    🚀 科技趨勢週報:從 AI 代理的崛起,到現實世界的數位治理

    今日的科技圈焦點集中在 AI 的實際落地應用與治理爭議。從高效能的本地語音工具,到 AI 程式代理人的沙盒環境,甚至是對 OpenAI 領導權力的深度反思,這一切都顯示我們正處於從「實驗室 AI」轉向「工程化 AI」的關鍵過渡期。對於開發者與技術決策者而言,理解這些技術邊界與信任架構,將是未來幾年的核心競爭力。✨

    🤖 AI/機器學習

    Ghost Pepper:macOS 專用的本地端「按住即說」語音轉文字工具

    這是一個開源的 macOS 應用程式,旨在提供極致流暢的語音輸入體驗。它採用「按住即說」(hold-to-talk)的模式,並在本地運行 Whisper 模型,確保隱私不外洩。開發者特別強調其低延遲與系統級整合,讓用戶能像使用對講機一樣快速將語音轉為文字。

    • 隱私優先: 所有語音處理均在 Mac 本地完成,不需上傳雲端。
    • 極簡交互: 透過熱鍵觸發,釋放即完成輸入,適合高頻率文字工作者。
    • 技術亮點: 優化了 Whisper 在 Apple Silicon 上的推理效能。

    🔗 閱讀原文

    Freestyle:為 AI 程式開發代理打造的沙盒環境

    隨著 AI 代理(Agents)開始接管程式撰寫與執行任務,安全性成為首要難題。Freestyle 提供了一個專為這些 AI 代理設計的基礎設施,讓開發者能輕鬆建立隔離的沙盒環境,安全地讓 AI 執行程式碼、安裝依賴並進行測試,而不會威脅到宿主系統。

    • 安全隔離: 確保 AI 執行的任何惡意或錯誤指令都被限制在沙盒內。
    • 基礎設施即服務: 簡化了為 AI 代理建構複雜開發環境的流程。
    • 提升自動化: 讓 AI 能夠自主完成從編碼到部署的端到端流程。

    🔗 閱讀原文

    問題反饋:Claude Code 二月更新後在複雜工程任務中變得難以使用

    在 GitHub 上引起熱烈討論的議題,指出 Anthropic 的 Claude Code 在二月更新後,處理複雜的工程邏輯時表現大幅下滑。許多開發者反應 AI 開始出現幻覺、遺忘上下文或無法理解複雜的文件結構,這引發了對 LLM 版本更新穩定性的廣泛擔憂。

    • 效能衰退: 用戶普遍感受在處理大型專案時,精準度不如以往。
    • 工程挑戰: 突顯了 LLM 作為生產力工具時,版本控制與回归測試的重要性。
    • 社群期待: 開發者呼籲 Anthropic 提供更透明的模型更新說明與回滾機制。

    🔗 閱讀原文

    🛠️ 開發工具

    Solod:可轉譯為 C 語言的 Go 語言子集

    Solod 是一個有趣的實驗專案,它選取了 Go 語言的一個語法子集,並將其編譯(轉譯)成 C 語言。這項技術旨在讓開發者能享受 Go 簡潔的語法,同時在資源受限的環境(如嵌入式系統)中發揮 C 語言的效能與移植性。

    • 跨語言橋樑: 將 Go 的現代語法帶入底層開發領域。
    • 限制性優勢: 透過減少語言特性,達到更可預測的編譯結果與效能。
    • 開源精神: 提供開發者一個研究編譯器原理與語言轉譯的絕佳範例。

    🔗 閱讀原文

    💼 創業/商業

    山姆·奧特曼可能掌控我們的未來——他值得信任嗎?

    《紐約客》發布的這篇長文深入探討了 OpenAI 執行長 Sam Altman 的權力擴張。文章從早期的 YC 時期談到現在的 AI 帝國,質疑這種將改變人類命運的技術集中在少數人手中的風險,並探討了他在公司治理中的複雜角色。

    「權力的集中往往伴隨著問責的缺失。」

    • 人物深度剖析: 揭示了 Altman 在矽谷權力架構中的運作方式。
    • 治理爭議: 重新回顧 OpenAI 董事會政變事件及其後續影響。
    • 社會影響: 討論通用人工智慧(AGI)背後的商業利益與大眾福祉的衝突。

    🔗 閱讀原文

    GovAuctions:一站式瀏覽政府拍賣資訊

    這是一個新興的 SaaS 平台,旨在整合碎片化的政府拍賣資訊。從報廢公務車到沒收的資產,GovAuctions 讓用戶能在同一個介面中搜尋並篩選各類政府拍賣物件,解決了過往資訊不透明且難以查詢的痛點。

    • 資料整合: 成功地將不同層級政府單位的拍賣數據結構化。
    • 利基市場: 針對資產投資者與二手機具買家提供高價值工具。
    • 商業模式: 展示了如何透過「整理公開資訊」創造出實用的商業產品。

    🔗 閱讀原文

    🌐 其他技術觀察

    密碼學工程師對量子計算時間線的看法

    資深密碼學工程師 Filippo Valsorda 分享了他對「具有密碼學破壞能力的量子電腦」(CRQC)出現時間線的冷靜分析。他認為雖然量子威脅確實存在,但與其恐慌,不如專注於現有的後量子密碼學(PQC)遷移路徑。

    • 務實觀點: 區分了理論上的量子計算與能真正破解加密算法的工程實現。
    • 安全建議: 建議企業應開始評估其加密敏捷性(Crypto-agility)。
    • 時間預測: 強調這是一場長期的技術馬拉松,而非短期的安全危機。

    🔗 閱讀原文

    我的種稻經驗談

    這是一篇脫離純數位世界的清新文章,作者詳細記錄了從碼農轉身投入農業,親手耕種水稻的過程。文章探討了體力勞動帶來的心理成就感,以及與大自然週期同步運作時,對科技與生活節奏的全新感悟。

    • 數位游牧的對立面: 展現了親手觸摸泥土與植物的真實感。
    • 系統思維: 作者以理性的開發者視角分析農業系統中的變因與挑戰。
    • 生活平衡: 提醒技術從業者在屏幕之外尋找生命意義的可能性。

    🔗 閱讀原文

    阿波羅導引電腦(AGC)修復影片紀錄

    對於硬體極客來說,這是一系列極具啟發性的紀錄影片。記錄了團隊如何修復 1960 年代用於登月任務的阿波羅導引電腦。這不僅是古董修復,更是一場關於早期計算機架構與強韌工程設計的深度教學。

    • 工程奇蹟: 展現了在極度受限的運算資源下如何實現登月壯舉。
    • 逆向工程: 揭示了半個世紀前的硬體設計邏輯與製作工藝。
    • 歷史保存: 數位化這些珍貴的技術遺產對後世工程師極具價值。

    🔗 閱讀原文

    德國警方公開 GandCrab 與 REvil 勒索軟體集團疑似領導人身份

    網路安全界的重要進展。德國執法部門公開了全球最惡名昭彰的兩大勒索軟體集團領導人的身份資訊(Doxing)。這象徵著國際執法合作在對抗跨境網路犯罪上取得了實質突破。

    • 法治勝利: 打擊網路犯罪分子的匿名性保護。
    • 國際合作: 展示了各國警方在追蹤加密貨幣流向與通訊數據的協作能力。
    • 震懾效應: 對現有的勒索軟體即服務(RaaS)生態系統造成沉重打擊。

    🔗 閱讀原文

    💡 今日觀點:技術的邊界與人類的信任

    今日的文章清單反映出一個有趣的對比:我們一方面在追求極致的 AI 自動化與代理(如 Freestyle 與 Claude Code),另一方面卻又對掌握這些技術的靈魂人物(如 Sam Altman)感到不安,甚至懷念起種稻與古董電腦這種「看得見、摸得著」的真實感。 💾🌾

    給讀者的行動建議:

    • 對於開發者: 關注 AI 沙盒(Sandboxing)技術,這是未來 AI 代理進入正式生產環境的標準配備。
    • 對於技術主管: 不要盲目跟隨 AI 模型更新,建立自己的評估基準(Benchmark)以應對模型退化風險。
    • 對於所有人: 在數位洪流中,保持對底層技術(如 C 語言、歷史硬體)的敬畏,並適時地抬起頭,像那位種稻的作者一樣,找回與現實世界的連結。
  • Hacker News 每日精選 – 2026-04-06

    🚀 科技趨勢週報:從邊緣運算 AI 到 GUI 策略的深度反思

    今日的科技圈展現了兩個極端的碰撞:一方面是 AI 技術正以前所未有的速度向「邊緣裝置」滲透,從 iPhone 到 M3 晶片的 Mac 都能運行高效能模型;另一方面,開發者社群開始重新審視軟體架構的效率,無論是批評微軟長達數十年的介面策略迷失,還是嘗試用更輕量級的技術重新建構開發工具。這是一個技術回歸效能與透明度的時代,值得每位技術人深思。

    🤖 AI / 機器學習

    Gemma 4 正式登陸 iPhone 📱

    Google 最新的開放模型 Gemma 4 現在已可透過「Google AI Edge Gallery」在 iOS 平台上運行。這象徵著行動端邊緣運算的重大突破,使用者無需依賴雲端伺服器即可在 iPhone 上體驗高效能的語言模型處理。這不僅提升了隱私性,更大幅降低了延遲,標誌著個人 AI 助手邁向新里程碑。

    👉 點此查看 App Store 連結 |
    💬 HN 討論

    GuppyLM:為了去迷思化而生的微型 LLM 🦀

    開發者 arman-bd 打造了一個極小規模的語言模型(Tiny LLM),旨在幫助初學者拆解並理解模型運作的背後原理。在 LLM 變得日益複雜的今天,這個專案透過精簡的代碼揭開了變換器(Transformer)架構的神祕面紗。這對於想要從零學習 AI 底層架構的工程師來說,是極佳的教科書級專案。

    👉 查看 GitHub 專案 |
    💬 HN 討論

    Parlor:在 M3 Pro 上實現即時多模態 AI 🎙️

    透過 Gemma E2B 引擎,Parlor 展示了在 M3 Pro 晶片的 Mac 上進行即時語音、影像輸入與語音輸出的能力。這項專案證明了蘋果自研晶片在處理低延遲 AI 互動上的強大潛力。它打破了傳統 AI 互動的斷點,讓開發者能實驗更自然、更像真人的機器互動體驗。

    👉 查看 GitHub 專案 |
    💬 HN 討論

    🛠️ 開發工具與開源專案

    SideX – 以 Tauri 驅動的 VS Code 實驗性移植版 ⚡

    SideX 試圖挑戰 VS Code 長久以來對 Electron 框架的依賴,轉而使用 Rust 驅動的 Tauri 框架進行開發。這項專案的主要目標是大幅降低開發編輯器的記憶體佔用並提升啟動速度。雖然目前仍處於實驗階段,但它代表了社群對於「更輕量級編輯器」的強烈渴望。

    👉 查看 GitHub 專案 |
    💬 HN 討論

    LÖVE:專為 Lua 打造的 2D 遊戲框架 🎮

    這是一個經典且持續進化的開源專案,讓開發者能使用簡潔的 Lua 語言快速開發跨平台 2D 遊戲。它以簡單易用、效能優異著稱,廣受獨立遊戲開發者喜愛。對於想進入遊戲開發領域但不想被重量級引擎束縛的工程師來說,LÖVE 是最佳的起點。

    👉 查看 GitHub 專案 |
    💬 HN 討論

    YouTube 進階搜尋過濾器 🔍

    開發者推出了一個具有強大過濾功能的 YouTube 搜尋介面,解決了原生搜尋結果容易被演算法干擾的問題。使用者可以更精確地篩選發布日期、影片長度與特定類型,找回搜尋的主控權。這是一個體現「解決自己痛點」的小型實用工具範例。

    👉 使用搜尋工具 |
    💬 HN 討論

    🔭 商業、策略與其他

    微軟自 Petzold 以來就失去了連貫的 GUI 策略 🏛️

    這篇文章深刻分析了微軟在 Windows 圖形使用者介面(GUI)發展上的混亂。作者指出自從經典教科書作者 Charles Petzold 的時代後,微軟不斷在 Win32, WPF, UWP, WinUI 等技術間搖擺。這導致了現代 Windows 介面破碎且缺乏一致性的開發體驗,引發了資深開發者的強烈共鳴。

    👉 閱讀深度評論 |
    💬 HN 討論

    MoonRF:讓訊號在月球上反彈的 240 陣列天線 🌙

    這是一個極具野心的開源硬體專案,計畫利用 240 個天線組成的陣列,實現訊號在月球表面的反射。這項實驗結合了無線電工程與天文觀測,展現了開源社群在尖端航太技術領域的創造力。這不僅是技術挑戰,更是一場關於遠距離通訊極限的探索。

    👉 查看專案官網 |
    💬 HN 討論

    2026 年國譽 (Kokuyo) 設計獎揭曉 🎨

    日本知名文具品牌 Kokuyo 公布了最新的設計獎得主,展現了工業設計與日常用品結合的無限創意。這些獲獎作品通常代表了對人類行為的細膩觀察,不僅美觀,更解決了微小的生活痛點。對於從事產品設計或前端開發的人員來說,這些創意是極佳的靈感來源。

    👉 欣賞得獎作品 |
    💬 HN 討論

    懷舊小趣聞: 1987 年的經典遊戲《最後的忍者》(The Last Ninja) 整個遊戲竟然只有 40 KB。這提醒了現代開發者,在那個資源極度匱乏的年代,工程師是如何將效能壓榨到極限的。

    💡 今日觀點:重返效能與理解的本質

    今日的熱門話題顯示出一個共同主題:「解構複雜性」。當 AI 模型可以被精簡到用來教學(GuppyLM),或是被優化到可以在手機上流暢運行時,我們看到的是技術民主化的過程。同時,開發者對 VS Code (SideX) 和微軟 GUI 策略的檢討,反映出社群對於現有軟體堆疊過於臃腫的不滿。

    給讀者的行動建議:

    • 探索本地 AI: 下載 Google AI Edge Gallery,親自測試模型在手機上的處理極限。
    • 審視技術債: 思考你在專案中使用的工具是否過於沉重?是否有像 Tauri 這樣更輕量化的替代方案?
    • 回歸基礎: 閱讀關於微小型 LLM 的專案,理解那些被封裝在 API 之後的數學原理。
  • gemma4:e4b 在 AMD Renoir iGPU Vulkan 輸出亂碼的完整排查

    重點摘要

    • gemma4:e4b 在 AMD Renoir iGPU(Vulkan)超過 30 層就輸出亂碼,低層數正常
    • 根本原因:RADV Vulkan shader compiler 的精度優化破壞 f16 運算順序,誤差隨層數滾雪球
    • iGPU 沒有獨立 VRAM,記憶體從系統 RAM 借,RAM 不足會讓模型載入循環崩潰
    • Mac Metal / 純 CPU 不受影響;GGML_VK_DISABLE_F16 等環境變數無法修復底層 compiler bug

    在本地端跑大型語言模型時,GPU 加速聽起來理所當然——更快嘛。但如果 GPU 算出來的答案是錯的,快有什麼用?這篇文章記錄了一次真實的踩坑:gemma4:e4b 在 AMD Renoir iGPU 上透過 Vulkan 跑出亂碼的完整排查過程,以及背後一層比一層深的技術原因。

    事件起因:模型跑得快,答案全錯

    測試環境是一台搭載 AMD Renoir APU 的 mini PC,透過 Ollama 跑 gemma4:e4b 模型。Ollama 偵測到 iGPU 後自動啟用 Vulkan 加速,把 42/43 層 offload 到 GPU。

    速度看起來不錯,約 8 tok/s。但仔細看輸出內容,發現問題大了:

    • 問「什麼是 Kafka Consumer Group Rebalancing」→ 回答泰文的學習理論
    • 問 Python 日期解析函數 → 回答 JavaScript offsetTop 的用法
    • 問燈泡謎題 → 回答哲學框架

    模型完全沒理解問題,像是跑在另一個平行宇宙一樣。

    第一個坑:RAM 不足讓模型連載入都不穩定

    在問為什麼答案亂之前,先碰到一個更早的問題:模型根本載入不起來。第一次啟動時,Ollama 日誌出現了這個循環:

    16:35:53  offloaded 42/43 layers to GPU(模型開始載入)
    16:38:53  llm server not responding
    16:38:54  llm server loading model
    16:39:02  llm server not responding
    16:39:03  llm server loading model
    ...(反覆了將近 6 分鐘)
    16:42:28  llama runner started in 410.58 seconds(終於啟動)
    17:04:49  -- Boot --(機器直接重開機)

    當時系統狀況:OMS 全套服務正在運行,RAM 只剩 1.7 GiB 可用。停掉 OMS 釋放記憶體後重跑:

    llama runner started in 42.04 seconds

    差了 10 倍。問題不在模型,在記憶體。

    為什麼 iGPU 需要大量系統 RAM?

    AMD Renoir iGPU 沒有獨立顯存,它的記憶體架構分兩層:

    類型 來源 大小 特性
    UMA(Unified Memory) BIOS 開機時從系統 RAM 切出 2 GiB(此台設定) 固定預留,GPU 專用,穩定
    GTT(Graphics Translation Table) 從剩餘系統 RAM 動態借用 最多 ~6.8 GiB 依可用 RAM 決定,借不到就崩

    Ollama 顯示「8.8 GiB 可用 VRAM」= UMA(2 GiB)+ GTT 上限(~6.8 GiB)合計。

    跑 gemma4:e4b 需要 2.8 GiB GPU weights + 224 MB KV cache + 289 MB compute graph ≈ 3.3 GiB。流程是:

    1. 先用完 2 GiB UMA
    2. 再向 GTT 借 ~1.3 GiB 的系統 RAM 頁面
    3. GTT 的頁面必須來自空閒的系統 RAM

    OMS 開著只剩 1.7 GB 可用,GPU 借不到足夠的 GTT 頁面,runner 一直崩潰重試,才出現那個 6 分鐘的死循環。最後機器因為 OOM 直接 reboot。

    換句話說:iGPU 的「VRAM」是假的,它在跟你的程式搶同一塊系統 RAM。獨顯不會有這個問題,因為它有真正的 GDDR 記憶體。

    GPU 與 GPU 的差異:不是每顆都一樣精準

    解決了載入問題,答案還是亂的。這時才是 GPU 精度的核心問題。

    GPU API f16 硬體支援 ML 成熟度
    NVIDIA RTX CUDA 原生 Tensor Core,為 ML 設計
    Apple M 系列 Metal Neural Engine + Metal 緊密整合
    AMD RX 獨顯 ROCm / Vulkan 原生支援,ROCm 驗證過 中高
    AMD Renoir iGPU Vulkan(RADV) Vega 架構,遊戲導向,未針對 ML 驗證

    f16 規格一樣,為什麼結果不同?

    很多人的疑問:IEEE 754 fp16 是標準規格,大家都遵守,為什麼不同 GPU 算出來不一樣?

    因為規格只定義單一運算的結果,但沒有規定三件事:

    1. Denormal 數的處理方式:非常接近 0 的浮點數(小於 6×10⁻⁸)稱為 denormal。IEEE 標準要求保留精度處理,但 AMD Vega 架構預設會把這些數 flush to zero(FTZ)——直接歸零——以換取效能。NVIDIA 不做 FTZ。這在遊戲中完全沒影響,但 LLM attention 的 softmax 運算大量出現這個範圍的數值。
    2. 運算順序:浮點數不符合結合律(a + b) + c ≠ a + (b + c) 在 fp16 中是真的。Shader compiler 為了讓 GPU 並行效率最大化,會自由重排運算順序,這對遊戲結果沒有可見影響,但 LLM 的 attention 矩陣乘法有幾萬次浮點運算,順序一改,誤差模式就完全不同。
    3. FMA(Fused Multiply-Add)的使用時機a × b + c 可以分兩步算(兩次取整),也可以融合成一步(只取整一次,精度更高)。NVIDIA CUDA 強制所有路徑用 FMA。RADV 的 ACO shader compiler 根據優化情境決定,在某些 LLM 矩陣模式下選了非 FMA 路徑。

    這三個問題疊在一起,每一層的誤差模式都不同,而且誤差會被下一層當成合法輸入繼續放大。

    為什麼低層數正常、高層數亂碼?

    什麼是 Transformer 的「層」

    gemma4:e4b 是 Transformer 架構,共 43 層。每個 token 的生成,資料要依序流過全部 43 層,每一層的輸出是下一層的輸入:

    Token 輸入
      → Layer 1(基礎語法、token 特徵)
      → Layer 2 ~ 10(語法結構、基礎語意)
      → Layer 10 ~ 25(上下文理解、語意推理)
      → Layer 25 ~ 42(高階抽象、輸出生成)
      → 下一個 token

    當設定 num_gpu=N 時,前 N 層在 GPU 算,剩下的在 CPU 算

    誤差如何滾雪球

    Layer  1:正確值 2.000,Vulkan 算出 2.001 → 誤差 0.001(無感)
    Layer  5:誤差疊加,下層把這個「略偏的 2.005」當成正確輸入繼續算
    Layer 10:誤差影響語意方向判斷
    Layer 20:模型對問題的理解開始偏移
    Layer 30:完全脫離正確答案空間
    Layer 42:輸出隨機語言的 token,變成泰文或法文

    這就像傳話遊戲:每個人傳話都有一點偏差,傳到第 43 個人時,內容已經面目全非。

    為什麼簡單問題(1+1)還能過

    「1+1 等於幾」的答案空間極小——就算誤差很大,最終 token 機率分布仍然在「2」附近。但「請用 Python 寫一個函數處理三種日期格式」的答案空間是整個程式語言的 token 集合,一旦偏移,就隨機落到任何地方。這也是為什麼診斷時不能只用簡單問題測試,必須用複雜 prompt 才能暴露問題。

    診斷過程:二分法找臨界點

    for layers in 1 5 10 20 30 40 42; do
      echo -n "=== num_gpu=$layers === "
      curl -s http://localhost:11434/api/chat \
        -d "{\"model\":\"gemma4:e4b\",
             \"messages\":[{\"role\":\"user\",\"content\":\"1+1等於幾?\"}],
             \"stream\":false,
             \"options\":{\"num_gpu\":$layers,\"num_predict\":30}}" \
        | python3 -c "import json,sys; print(json.load(sys.stdin)['message']['content'])"
    done
    num_gpu 結果 狀態
    1 1+1 等於 2 ✅ 正確
    5 1+1 等於 2 ✅ 正確
    10 (空白) ⚠️ 不穩定
    20 簡單問題偶爾正確 ⚠️ 不穩定
    30 法文回應 ❌ 亂碼
    42 隨機英文片段 ❌ 亂碼

    嘗試修復:三種方案全部失敗,以及為什麼

    方案一:GGML_VK_DISABLE_F16=1

    理論上這個環境變數應該告訴 llama.cpp 在 Vulkan 路徑使用 f32 計算,避開 f16 精度問題。但沒有效果。原因在於 Vulkan 推論的整個管線分四層:

    1. llama.cpp         → 寫 GLSL compute shader 原始碼        ← 環境變數影響到這層
    2. glslang           → 編譯成 SPIR-V bytecode
    3. RADV / ACO        → 把 SPIR-V JIT 編譯成 AMD GCN 機器碼  ← 精度 bug 在這層
    4. AMD Vega 硬體     → 執行機器碼

    精度 bug 在第 3 層——RADV 的 ACO shader compiler 在把 SPIR-V 轉成 GCN ISA 時,對某些矩陣運算的指令做了重排或合併,這個行為由 RADV 自己決定,應用層的環境變數完全碰不到。就算第 1 層寫了 f32,RADV 也可能在編譯時把某些中間值降轉成 f16——這對遊戲是合法的效能優化,對 LLM 是致命的。

    方案二:OLLAMA_KV_CACHE_TYPE=f32

    只影響 KV cache 的儲存精度,不影響主要的矩陣乘法計算路徑。輸出直接變成旁遮普語。

    方案三:num_gpu=20 折衷

    1+1 這種極簡問題可以過,但稍微複雜的 prompt 仍然亂答。誤差臨界點比簡單測試顯示的還要低,不穩定。

    最終結論

    方案 速度 正確性 建議
    純 CPU(num_gpu=0) ~1 tok/s ✅ 正確 唯一可用方案
    Vulkan 全層(num_gpu=42) ~8 tok/s ❌ 亂碼 不可用
    Mac Metal(同款模型) ~8 tok/s ✅ 正確 最佳選擇

    快 8 倍但答案全錯,還不如 1 tok/s 的純 CPU。資料正確性永遠優先於速度。

    如果你也遇到類似問題:排查步驟

    1. 先確認系統有足夠空閒 RAM(至少要比模型大小多 2 GiB),不然連載入都會失敗或 OOM reboot
    2. num_gpu=0 跑純 CPU,確認模型本身答案正確
    3. 逐步增加 num_gpu(1 → 5 → 10 → 20 → …),用複雜問題測試(不要只用 1+1),找到亂碼的臨界層數
    4. 環境變數(GGML_VK_DISABLE_F16、OLLAMA_KV_CACHE_TYPE)對 RADV 底層 compiler bug 無效,不要在這裡浪費時間
    5. 若根本無法修,接受純 CPU 或換有 CUDA / Metal 支援的環境

    這不是 Ollama 的 bug,也不是模型的問題。是 AMD Renoir Vega iGPU + RADV Vulkan shader compiler 在 LLM 工作負載下的精度限制,加上 iGPU 共享系統 RAM 的架構特性,兩個問題同時踩到。

  • Hacker News 每日精選 – 2026-04-05

    🚀 今日科技趨勢:從硬體底層到 AI 思維的深度整合

    今日的科技圈展現了從底層硬體架構、作業系統標準化到 AI 頂層思維的全面交織。我們不僅看到了 Andrej Karpathy 對 LLM 未來的深度思考,也見證了 Valve 如何透過資本力量重塑 Linux 生態,以及微軟在品牌命名上的策略迷思。對於開發者與科技愛好者來說,理解這些趨勢將有助於掌握軟硬體整合的新賽道。

    🤖 AI / 機器學習

    LLM Wiki – Andrej Karpathy 的「靈感筆記」

    前 Tesla AI 負責人 Andrej Karpathy 分享了他關於大語言模型(LLM)的個人維基草稿。這份文件被稱為「想法檔案」,記錄了他對 LLM 作為一種新型作業系統、模型評估機制以及未來發展方向的深刻洞見。這對於想要理解頂尖 AI 專家如何思考模型架構與應用的讀者來說,是極具價值的參考資料。

    🛠️ 開發工具

    Mvidia:在遊戲中學習如何構建 GPU

    這是一款硬核且極具教育意義的網頁遊戲,玩家需要從最基礎的邏輯閘開始,逐步構建出一個功能完整的 GPU。這項專案在 HN 上引發了巨大迴響,因為它將極其複雜的電腦架構知識轉化為可互動的挑戰。對於想深入了解硬體加速與計算原理的開發者而言,這是不容錯過的實踐工具。

    Zml-smi:跨平台的 GPU、TPU 與 NPU 監控神器

    隨著 AI 運算不再侷限於 NVIDIA,開發者需要更通用的監控工具。Zml-smi 提供了一個統一的介面,讓使用者可以同時監控不同廠商的加速晶片狀態。這在多樣化硬體配置的機器學習工作流中,解決了過去需要安裝多個驅動工具(如 nvidia-smi)的痛點。

    Rubysyn:澄清 Ruby 的語法與語義

    Ruby 雖然以優雅著稱,但其複雜的語法規則有時會讓開發者困惑。Rubysyn 專案旨在系統化地釐清 Ruby 的語法細節,提供更清晰的規範與解釋。這對於撰寫靜態分析工具或深度優化 Ruby 代碼的專家來說,是不可多得的基礎文件。

    🏢 創業 / 商業

    微軟到底有多少個名為 「Copilot」 的產品?

    這篇文章辛辣地分析了微軟近期的品牌混亂現象。從 Office、Windows 到 GitHub,微軟幾乎將所有 AI 產品都命名為 Copilot,導致消費者與企業在採購時極度困惑。這不僅是一次品牌營銷的探討,更反映了大型企業在急於佈局 AI 市場時可能出現的策略失焦。

    🌐 開源專案

    Valve 創辦人 GabeN 正用「遊艇等級」的資金重塑 Linux 軟體發行

    評論指出,當社群還在爭論 Linux 初始化系統時,Valve 已經投入巨資支持 Flatpak 架構。透過 Steam Deck 的成功,Valve 實際上正在推動 Linux 桌面應用程式的標準化,解決了長期以來 Linux 套件零碎化的問題。這標誌著商業力量介入後,Linux 生態可能迎來真正的「大統一」。

    OpenScreen:開源版的 Screen Studio

    Screen Studio 以製作精美的螢幕錄製影片聞名,但價格昂貴。OpenScreen 作為一個開源替代方案,旨在提供類似的縮放與自動追蹤效果,幫助開發者與內容創作者以更低的成本製作出專業的教學與展示影片。

    ⚖️ 其他焦點

    德國 eIDAS 實施爭議:數位身份證竟然需要 Apple/Google 帳號?

    德國政府推動的數位身份證(eIDAS)在技術實施上引發了隱私疑慮。目前的方案暗示使用者可能需要擁有 Big Tech 巨頭的帳號才能運作,這對於強調數位自主權的歐洲社群來說,無疑是一枚震撼彈。這場辯論觸及了數位主權與商業基礎設施之間的敏感地帶。

    電腦音樂入門 (2009) [PDF]

    這是一份經典的學術教材,深入淺出地介紹了數位音訊、合成原理與電腦作曲。儘管是 2009 年的資料,但其底層的物理與數學邏輯至今依然適用,是音樂技術(Music Tech)領域愛好者的必讀經典。

    💡 今日觀點:標準化與自主權的博弈

    今日的熱點顯示出科技發展的兩大矛盾:標準化帶來的便利(如 Flatpak 與微軟的 Copilot 品牌)與個人/國家自主權的喪失(如德國數位身份證對大廠的依賴)。同時,像 Andrej Karpathy 這樣的開拓者正試圖將 AI 框架化,而開發者則透過開源工具(OpenScreen, Zml-smi)不斷奪回技術的主導權。

    🎯 給讀者的行動建議:
    1. 建議閱讀 Karpathy 的筆記,這不僅是技術學習,更是學習如何建立自己的知識體系。
    2. 如果你是 Linux 使用者,是時候深入了解 Flatpak 如何影響你的軟體管理習慣。
    3. 花 10 分鐘玩一下 Mvidia,你會對手邊的硬體運作有全新的認識。

  • 本地 AI 模型完整實測:五款模型 × 兩台機器 × 三種設定,找出真正的上限

    重點摘要

    • Mini PC(Ryzen 7 4700U):gemma4:e4b 最快(1.45 tok/s),qwen3:14b 最完整(7/7 題全答);Bonsai 8B 因 AVX-512 需求完全無法使用
    • 換一台機器差多少?:同款 gemma4:e4b,MacBook Air M3 跑出 9.75 tok/s,是 Mini PC 的 6.7 倍;Q2 輸出從截斷 300 tokens 變成完整 2218 tokens
    • 開 Thinking 差多少?:速度幾乎不變(-6%),但 Q2 程式碼輸出 +65%,Q7 技術解釋 +124%,品質接近 GPT-3.5

    你想在本地跑 AI 模型,但不知道哪款模型值得裝?硬體夠不夠?開 Thinking 模式到底有沒有差?這篇文章用實際跑出來的數據回答這三個問題——五款模型 × 兩台機器 × 三種設定,全部實測,沒有廣告。

    測試環境分別是一台平價 Mini PC(Ryzen 7 4700U,16GB RAM,CPU-only)和 MacBook Air M3(24GB 統一記憶體)。七道測試題涵蓋:文字理解、Python 程式碼生成、SQL 查詢、TCP 技術解釋等真實使用情境。

    測試環境規格

    項目 Mini PC MacBook Air M3
    處理器 AMD Ryzen 7 4700U Apple M3
    記憶體 16GB DDR4(CPU-only) 24GB 統一記憶體
    顯示卡 AMD Vega 7 iGPU,僅 128MB VRAM(不可用) Apple GPU(共享統一記憶體)
    推論框架 Ollama + llama.cpp(CPU 模式) Ollama + llama.cpp(Metal)
    特殊限制 無 AVX-512,部分模型無法執行 無限制

    重點摘要表(先看這三張表)

    表 1:Mini PC 五款模型速度與完成度對比

    模型 平均速度 完成度 主要問題
    Bonsai 8B 0.001 tok/s 完全不可用 需要 AVX-512,CPU 不支援
    qwen3:4b 0.78 tok/s 5/7(部分截斷) Q2/Q5 在 300 token 限制下截斷或夾雜文字
    qwen3.5:9b 0.57 tok/s 5/7 Q6/Q7 需延長 timeout 至 1800s
    qwen3:14b 0.58 tok/s 7/7 全答 無截斷問題,但速度慢
    gemma4:e4b 1.45 tok/s 5/7(舊測試 600 上限) Q2/Q4 在 600 token 舊限制下截斷

    表 2:同款 gemma4:e4b,硬體差距

    指標 Mini PC(CPU) MacBook Air M3 差距
    平均速度 1.45 tok/s 9.75 tok/s 6.7×
    Q2 輸出長度 600 tokens(截斷) 2218 tokens(完整) 記憶體夠,才不截斷
    Q4 輸出長度 600 tokens(截斷) 1043 tokens(完整) SQL 查詢完整輸出
    最大 num_ctx 受 16GB 限制 65536+ 長文件處理能力天差地別

    表 3:Mac 上同款模型,三種設定對比

    設定 平均速度 Q2 輸出 Q7 輸出 適合場景
    Mini PC 舊測試(600 limit) 1.45 tok/s 截斷 309 tokens 快速查詢
    Mac think:false,無限制 9.75 tok/s 2218 tokens ✅ 442 tokens 日常程式碼
    Mac Thinking 開啟,無限制 9.16 tok/s(-6%) 3670 tokens ✅ 990 tokens 複雜推理、技術解釋

    第一層:Mini PC 上,哪個模型值得跑?

    在只有 CPU 推論的環境下,選模型就是在「速度」與「品質」之間做取捨。以下是完整的三維評估。

    Bonsai 8B:直接淘汰

    Bonsai 8B 的速度是 0.001 tok/s——不是很慢,是根本無法執行。原因是它的量化版本依賴 AVX-512 指令集,而 Ryzen 7 4700U 不支援 AVX-512(只有 AVX2)。llama.cpp 在這種情況下會退回軟體模擬,速度接近零。如果你的機器是 Intel 第 11 代以後或 AMD Zen 4 以後,才有機會跑 Bonsai 8B。

    qwen3:4b:最快但有截斷風險

    qwen3:4b 在 Q1~Q7 七個量化等級測試中,平均跑出 0.78 tok/s,是 CPU 上可用模型裡的最高速。但在 num_predict=300 的限制下,Q2(程式碼生成)和 Q5(格式輸出)出現截斷或夾雜不相關文字的問題。如果你只需要短問短答,qwen3:4b Q6 量化(約 21 元台幣月費 API 等級)是最划算的選擇。

    qwen3.5:9b vs qwen3:14b:9B 更快但 14B 更可靠

    qwen3.5:9b 平均 0.57 tok/s,但 Q6/Q7 題遇到了 timeout 問題——需要將請求超時設定延長到 1800 秒才能完成。原因是 9B 模型在複雜任務上思考時間較長,但預設 timeout 不夠。

    qwen3:14b 同樣 0.58 tok/s,卻跑出 7/7 完整答題率。它的 Q2 完整輸出 500 tokens、Q4 完整輸出 500 tokens,沒有截斷。代價是記憶體佔用更高,在 16GB 機器上跑 14B 模型時需要注意 KV Cache 可能 OOM(記憶體不足),建議設定 num_ctx 不超過 4096。

    gemma4:e4b:速度最快的完整模型

    gemma4:e4b 平均 1.45 tok/s,是所有可用模型中最快的,幾乎是 qwen3:14b 的 2.5 倍。在舊測試的 600 token 限制下,Q2 和 Q4 被截斷。但如果移除限制(num_predict=-1),這個問題就不存在——這正是下一層要說的。

    Mini PC 選模型建議:

    • 追求速度 → gemma4:e4b(1.45 tok/s,移除 token 限制)
    • 追求品質 → qwen3:14b(7/7 完整,0.58 tok/s)
    • 省記憶體 → qwen3:4b Q3/Q4(短任務夠用)
    • 不建議 → Bonsai 8B(AVX-512 門檻)、qwen3.5:9b(需調 timeout)

    第二層:同款模型,換台機器差多少?

    用同款 gemma4:e4b,在 Mini PC 和 MacBook Air M3 上各跑一輪,看看換硬體能得到什麼。

    速度差 6.7 倍,不只是快慢的問題

    Mini PC 平均 1.45 tok/s,Mac 平均 9.75 tok/s。這個差距背後的原因是架構:Mini PC 用 x86 CPU 做矩陣運算,效率遠低於 Apple Silicon 的 Neural Engine + 統一記憶體架構。M3 的統一記憶體讓 CPU 和 GPU 共享同一塊 24GB,模型權重可以直接放在 GPU 能讀取的記憶體,不需要搬移。

    記憶體夠,輸出才完整

    這是硬體差距最直接的體現:Q2 要求生成一個能解析多種日期格式的 Python 函式,Mini PC 在 600 token 限制下就截斷了(回答還在中途),而 Mac 無限制跑出 2218 tokens 的完整函式

    Q4 要求生成帶有 CTE 和 Window Function 的複雜 SQL,Mini PC 截斷,Mac 輸出完整 1043 tokens 含說明。這不是模型能力的差異,是記憶體和 KV Cache 空間的差異。

    24GB 統一記憶體的隱藏優勢:num_ctx 可拉到 65536+

    num_ctx 決定模型能「看到」的上下文長度。Mini PC 的 16GB RAM 在跑 gemma4:e4b 時,實際可用的 KV Cache 空間有限,num_ctx 設太高就 OOM。Mac 的 24GB 統一記憶體可以輕鬆設定 num_ctx=65536,意味著可以貼入整個程式碼檔案、長文件、對話紀錄,模型不會「忘記」前面說了什麼。這個差距在實際工作流中比速度差距更重要。

    第三層:同台機器,調設定差多少?

    在 MacBook Air M3 上,用同款 gemma4:e4b 比較三種設定:舊測試的 600 token 限制、無限制(think:false)、開啟 Thinking 模式。

    速度幾乎不變,但輸出長度和品質大幅提升

    think:false 無限制:平均 9.75 tok/s。Thinking 開啟無限制:平均 9.16 tok/s。速度只差 6%,但輸出品質差距大:

    • Q2(Python 程式碼):2218 tokens → 3670 tokens(+65%),Thinking 版本多了完整的邊界條件處理和說明文字
    • Q4(SQL 查詢):1043 tokens → 1413 tokens(+35%),多了欄位說明和效能備注
    • Q7(TCP 三次握手):442 tokens → 990 tokens(+124%),Thinking 版本多了為何需要三次的深層解釋

    Thinking 模式讓模型在回答前先「想」一遍,輸出的答案更完整、更有條理。對於技術問題、程式碼生成、複雜推理,這個設定幾乎是免費的品質提升。

    gemma4:e4b 全力展開:這就是本地 LLM 的真實上限

    以下展示 Thinking 模式在 Mac 上的實際輸出,讓你判斷品質是否符合你的需求。

    Q2 Thinking:完整的多格式日期解析函式

    問題:「寫一個 Python 函式,能解析 YYYY-MM-DD、YYY/MM/DD(民國年)、YYYYMMDD(8位)、YYYMMDD(7位民國年)四種格式。」

    Thinking 模式輸出 3670 tokens,以下是完整的函式程式碼:

    from datetime import datetime
    import re
    
    def parse_mixed_date_string(date_str: str) -> datetime:
        if not isinstance(date_str, str):
            raise TypeError("輸入必須是字串 (string) 格式。")
        date_str = date_str.strip()
    
        # 模式 A: YYYY-MM-DD
        if re.fullmatch(r'\d{4}-\d{2}-\d{2}', date_str):
            try:
                return datetime.strptime(date_str, "%Y-%m-%d")
            except ValueError:
                pass
    
        # 模式 B: YYY/MM/DD(含民國年)
        if re.search(r'[/]', date_str):
            parts = date_str.split('/')
            if len(parts) == 3:
                year, month, day = parts[0], parts[1], parts[2]
                try:
                    temp_date_str = f"{year.zfill(4)}-{month.zfill(2)}-{day.zfill(2)}"
                    return datetime.strptime(temp_date_str, "%Y-%m-%d")
                except ValueError:
                    pass
    
        # 模式 C: YYYYMMDD(8位)
        if re.fullmatch(r'\d{8}', date_str):
            try:
                return datetime.strptime(date_str, "%Y%m%d")
            except ValueError:
                pass
    
        # 模式 D: YYYMMDD(7位民國年)
        if re.fullmatch(r'\d{7}', date_str):
            try:
                year = date_str[:-4]
                month = date_str[-4:-2]
                day = date_str[-2:]
                temp_date_str = f"{year.zfill(4)}-{month}-{day}"
                return datetime.strptime(temp_date_str, "%Y-%m-%d")
            except ValueError:
                pass
    
        raise ValueError(f"無法識別或解析的日期格式:'{date_str}'")

    非 Thinking 模式在同題只輸出 2218 tokens,函式邏輯正確但缺少邊界案例說明和型別標注說明。Thinking 版本多的那 1452 tokens 全是有用的:錯誤處理說明、邊界條件討論、使用範例。

    Q4 Thinking:帶 CTE 和視窗函數的複雜 SQL

    問題:「找出每個城市近 30 天消費金額前三名的顧客,輸出城市、姓名、消費總額。」

    WITH RecentSpending AS (
        SELECT o.customer_id, SUM(o.amount) AS total_spending
        FROM orders o
        WHERE o.created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
        GROUP BY o.customer_id
    ),
    RankedCustomers AS (
        SELECT c.name, c.city, rs.total_spending,
            RANK() OVER (PARTITION BY c.city ORDER BY rs.total_spending DESC) as city_rank
        FROM RecentSpending rs
        JOIN customers c ON rs.customer_id = c.id
    )
    SELECT city, name, total_spending
    FROM RankedCustomers
    WHERE city_rank <= 3
    ORDER BY city, city_rank;

    Thinking 版本(1413 tokens)在 SQL 後面額外附上了欄位說明對照表、RANK vs DENSE_RANK 的差異說明、以及在資料量大時建議加索引的備注。這種「自動補充說明」的行為,在程式碼審查或教學場景特別有用。

    Q7 Thinking:TCP 三次握手的技術解釋

    問題:「解釋 TCP 三次握手的過程,並說明為什麼需要三次而不是兩次。」

    Thinking 模式的回答(節錄,990 tokens):

    TCP 三次握手是在兩台設備開始傳輸實際資料之前,建立穩定可靠連線的過程:

    1. Client → Server (SYN):「我想連線,序列號從 X 開始」
    2. Server → Client (SYN-ACK):「我收到了,我也準備好,序列號從 Y 開始」
    3. Client → Server (ACK):「我收到你的確認,正式連線」

    為何需要三次而不是兩次:三次握手確保雙方的「發送能力」和「接收能力」都得到驗證。第三次 ACK 讓客戶端確認成功收到伺服器的肯定回應,確保雙方對初始序列號達成共識。若只有兩次,Server 無法確認 Client 是否真的收到了 SYN-ACK,可能導致半開連線(half-open connection)堆積。

    非 Thinking 模式(442 tokens)的回答只覆蓋了步驟本身,沒有解釋半開連線問題。Thinking 版本的 990 tokens 多出了協議設計的「為什麼」。

    技術踩坑筆記

    坑 1:AVX-512 問題不是模型 bug,是 CPU 選錯了

    Bonsai 8B 需要 AVX-512,AMD Ryzen 4000 系列(Zen 2 架構)不支援。解法:換用 AVX2 相容的量化版本,或換到支援 AVX-512 的 CPU(Intel 11th Gen+、AMD Zen 4+)。在買 Mini PC 跑本地 LLM 之前,先確認 CPU 指令集支援情況。

    坑 2:num_predict=300 在 CPU 機器上是陷阱

    設 num_predict=300 看起來是「省時間」,但會讓程式碼生成等長輸出任務的測試結果完全失效。正確做法是設 num_predict=-1(無限制),然後觀察模型自然停止的位置。如果真的需要截斷,至少設到 1000 以上再做程式碼類測試。

    坑 3:qwen3.5:9b 的 timeout 問題

    qwen3.5:9b 在 Q6(長文生成)和 Q7(技術解釋)上,Ollama 預設的請求 timeout 不夠,導致連線中斷而不是模型輸出完成。解法:在呼叫 API 時設定 timeout 參數為 1800 秒,或在 Ollama 的環境變數中調整 OLLAMA_TIMEOUT。

    坑 4:KV Cache OOM 發生在 num_ctx 設太高時

    在 16GB 機器上跑 qwen3:14b,如果 num_ctx 設到 8192 以上,KV Cache 的記憶體需求會超過可用 RAM,導致 OOM 或系統卡死。建議 16GB RAM 跑 14B 模型時,num_ctx 不超過 4096;跑 4B 模型時,num_ctx 可以到 8192。

    坑 5:think:false 是必要的,否則輸出會混入思考過程

    qwen3 系列模型如果不設定 think:false,輸出會包含 <think> 標籤包裹的推理過程,混在正式答案裡,對程式解析造成困擾。在 Ollama API 呼叫時加上 "options": {"think": false},或使用 /set parameter think false。只有在你明確需要 Thinking 輸出時才開啟。

    坑 6:num_predict 是物理截斷,不是智慧壓縮

    很多人以為設 num_predict=600 會讓模型「給出精簡版本」,實際上不是。模型不知道 num_predict 這個參數的存在——它只是一個一個往下生成 token,到達上限時被外力硬切斷。結果是:程式碼寫到一半沒有結尾大括號、SQL 少了 WHERE 條件、解釋說到一半消失。

    這次測試的 Q2(日期解析函數)和 Q4(SQL 排名查詢)在 num_predict=300 時全數截斷就是這個原因。移除限制(num_predict=-1)之後,模型自然停止,輸出完整。

    坑 7:有些內容本質上無法壓縮到指定長度內

    假設你問模型「給我一份完整 Kafka 設定檔,限制 50 個 token 內」——這兩個要求本身就互相矛盾。一份能正常運作的 Kafka 設定檔,光是必要欄位就需要遠超 50 token。模型沒有辦法把磚頭塞進比磚頭小的洞。

    面對不可壓縮的內容,有三種做法:(1)直接移除 token 上限;(2)分步請求——先要最精簡模板,確認結構後再要完整版;(3)在 prompt 明確說「輸出必須完整可執行,不要省略任何欄位」,但前提是 token 上限本身要夠大。

    補充:不同模型怎麼面對衝突指令?

    這裡有個值得理解的差異:本地模型(Ollama + llama.cpp)的 token 限制來自 API 參數,是硬體截斷,模型本身完全不知道有這個限制存在。雲端模型(Claude、GPT-4 等)的「限制」則來自 prompt 文字指令,模型讀到這個指令後會嘗試推理你的真實意圖。

    情境 本地模型(Ollama) 雲端模型(Claude)
    限制來源 num_predict API 參數 prompt 文字指令
    遇到「50 token 內給完整設定檔」 不知道有衝突,第 50 個 token 硬截 判斷兩個要求互相矛盾,主動說明,給完整版
    答案冗長可縮短的情況 按字數截斷,不壓縮 推理目標,給出精簡版本
    答案本質不可壓縮 截斷,輸出殘缺內容 告知無法在限制內完整輸出,給出建議
    GPT / Gemini 回答為什麼那麼短? 不是 token 限制,是 System Prompt + RLHF 訓練偏好所致

    這個差異的實際意義是:在本地 LLM 環境下,token limit 是你唯一能控制輸出長度的工具,設太小就會截斷。雲端模型則更像是在和一個理解你意圖的人對話——你不需要精準計算 token,只需要把你真正想要的說清楚。

    進階應用:讓本地 LLM 記住對話上下文

    本地 LLM 的 API 呼叫預設是無狀態的——每次送出的是獨立的單輪問答,模型不記得你上一題問了什麼。如果你想做多輪對話助理、程式碼審查工具、或任何需要「記住脈絡」的應用,就需要自己管理對話歷史。

    為什麼不能無限加長 messages?

    標準做法是把整段對話歷史塞進 messages 陣列一起送出。但對話越長,messages 陣列越大,最終超過 num_ctx 上限,前面的對話就會被硬截斷——模型在不知情的狀況下「失憶」,不會告訴你它看不到前面的內容。

    解法不是把 num_ctx 設更大(那只是延後問題),而是主動管理 messages 陣列:用摘要壓縮舊對話,只保留近期原文加上一段精簡的歷史摘要。

    三種管理策略比較

    方式 num_ctx 用量 記憶效果 適合場景
    無管理(全部塞) 持續增長,最終截斷 前期對話被硬切,模型不自知 短對話、單次任務
    Sliding Window(只保留近 N 輪) 固定 早期資訊完全消失 客服機器人、無需長記憶的助理
    摘要壓縮(推薦) 固定,摘要極短 保留關鍵結論、數字、決策 開發助理、長程任務、知識型問答

    摘要壓縮的運作方式

    核心思路是:用同一個本地模型來摘要自己的舊對話。超過門檻後,把早期輪次壓縮成一段文字,之後每次送出時帶著「摘要 + 近期原文」,而不是全部歷史。

    第 1-8 輪:原文保存在 messages[]
    
    第 9 輪觸發壓縮:
      old[1-5輪] → summarize() → "重點:xxx, yyy, zzz"
      messages 只留 [6-8輪原文] + 新問題
    
    第 9 輪實際送出的內容:
      system: "對話背景摘要:重點 xxx, yyy, zzz"
      user(6): ...  ai(6): ...
      user(7): ...  ai(7): ...
      user(8): ...  ai(8): ...
      user(9): 現在的問題

    完整 Python 實作(本地 Ollama 版)

    import json, urllib.request
    
    MAC_URL = "http://192.168.1.xxx:11434/api/chat"  # Mac 的 Ollama,Mini PC 不跑本地模型
    MODEL  = "gemma4:e4b"
    
    def call_model(messages, think=False):
        payload = {
            "model": MODEL,
            "messages": messages,
            "stream": False,
            "think": think,
            "options": {"num_ctx": 4096, "num_predict": -1}
        }
        data = json.dumps(payload).encode()
        req = urllib.request.Request(
            MAC_URL, data=data,
            headers={"Content-Type": "application/json"}
        )
        with urllib.request.urlopen(req, timeout=300) as r:
            return json.loads(r.read())["message"]["content"]
    
    def summarize(messages):
        """把舊對話丟給模型,壓縮成條列式重點"""
        history_text = "\n".join(
            f"{'User' if m['role'] == 'user' else 'AI'}: {m['content']}"
            for m in messages
        )
        prompt = f"""以下是一段對話記錄,請用條列式摘要最重要的資訊、結論、已確認的事實。
    保留具體數字、決策、技術細節。100字以內。
    
    對話:
    {history_text}
    
    摘要:"""
        return call_model([{"role": "user", "content": prompt}])
    
    class ChatSession:
        def __init__(self, keep_recent=4, compress_threshold=8):
            self.messages = []
            self.summary = ""               # 累積摘要
            self.keep_recent = keep_recent             # 保留最近幾輪原文
            self.compress_threshold = compress_threshold   # 超過幾輪就壓縮
    
        def chat(self, user_input):
            self.messages.append({"role": "user", "content": user_input})
    
            # 超過門檻 → 壓縮舊對話
            if len(self.messages) > self.compress_threshold:
                old = self.messages[:-self.keep_recent]
                new_summary = summarize(old)
    
                # 把舊摘要 + 新摘要合併
                self.summary = f"{self.summary}\n{new_summary}".strip()
                self.messages = self.messages[-self.keep_recent:]
                print(f"[已壓縮,摘要更新:{len(self.summary)} chars]")
    
            # 組合本次送出的 messages
            send_messages = []
            if self.summary:
                send_messages.append({
                    "role": "system",
                    "content": f"對話背景摘要(已發生的重點):\n{self.summary}"
                })
            send_messages.extend(self.messages)
    
            response = call_model(send_messages)
            self.messages.append({"role": "assistant", "content": response})
            return response
    
    # 使用方式
    if __name__ == "__main__":
        session = ChatSession(keep_recent=4, compress_threshold=8)
        while True:
            user = input("你:").strip()
            if user.lower() == "exit":
                break
            reply = session.chat(user)
            print(f"AI:{reply}")
            if session.summary:
                print(f"[背景摘要:{len(session.summary)} chars]")

    效能調優:摘要用小模型,回答用大模型

    摘要這個步驟本身也消耗一次推理呼叫。如果 Mac 上同時有快慢兩個模型,可以分工:快的模型做摘要,慢的(品質更好的)做正式回答:

    MAIN_MODEL    = "qwen3:14b"   # 回答主要問題,品質優先
    SUMMARY_MODEL = "qwen3:4b"   # 做摘要,速度優先(簡單任務夠用)
    
    def summarize(messages):
        # 使用小模型做摘要
        payload = {
            "model": SUMMARY_MODEL,
            ...
        }

    這樣摘要時間從數十秒縮短到幾秒,而主要對話品質不受影響。MacBook Air M3 速度夠快(9+ tok/s),用同一個模型做摘要也無妨。

    日常應用:把 Mac 當成你的私人 AI 主機

    gemma4:e4b 在 MacBook Air M3 上跑出 9+ tok/s,這個速度對互動式日常使用完全夠用——不是只能跑 benchmark,而是可以當成隨時待命的個人助理。重點是:所有資料留在本機,不送雲端,不計費。

    架構很簡單:Mini PC 負責發問和顯示,Mac 負責思考和回答。Mini PC 本身不跑模型,只是一個入口。

    你(Mini PC)→ 問題 → Mac gemma4 → 回答 → 你
    
    Mini PC:入口,不思考
    Mac:腦子,負責推理

    兩個最常用的場景

    場景一:快速摘要

    把一篇文章、一份 log、一段程式碼丟給 Mac,要它用幾句話說重點。不需要 Claude 等級的推理,gemma4 速度更快、更省錢(免費)。

    #!/usr/bin/env python3
    # summarize.py — 從 stdin 讀內容,打到 Mac gemma4 要摘要
    # 用法:cat article.txt | python3 summarize.py
    #       cat error.log   | python3 summarize.py --prompt "這份 log 的錯誤原因是什麼"
    
    import json, sys, urllib.request, argparse
    
    MAC_URL = "http://192.168.1.xxx:11434/api/chat"
    MODEL   = "gemma4:e4b"
    
    parser = argparse.ArgumentParser()
    parser.add_argument("--prompt", default="請用5句話以內摘要以下內容的重點:")
    args = parser.parse_args()
    
    content = sys.stdin.read().strip()
    if not content:
        print("ERROR: no input"); sys.exit(1)
    
    payload = {
        "model": MODEL,
        "messages": [{"role": "user", "content": f"{args.prompt}\n\n{content}"}],
        "stream": False, "think": False,
        "options": {"num_ctx": 8192, "num_predict": -1},
    }
    req = urllib.request.Request(
        MAC_URL, data=json.dumps(payload).encode(),
        headers={"Content-Type": "application/json"},
    )
    with urllib.request.urlopen(req, timeout=300) as r:
        print(json.loads(r.read())["message"]["content"])
    # 使用範例
    cat ~/Downloads/article.txt   | python3 summarize.py
    cat /var/log/app.log          | python3 summarize.py --prompt "這份 log 有什麼異常?"
    git diff HEAD~5               | python3 summarize.py --prompt "這幾個 commit 改了什麼?"

    場景二:快速產程式驗證

    想驗證一個想法、寫一段臨時腳本、或確認某個 API 用法——不需要開 IDE,直接從命令列問 Mac,幾秒鐘拿到可以跑的程式碼片段。

    #!/usr/bin/env python3
    # ask.py — 命令列直接問 Mac,拿回程式碼或答案
    # 用法:python3 ask.py "寫一個 Python 函數,把 list 裡的重複元素移除但保留順序"
    #       python3 ask.py "用 curl 怎麼測試一個需要 Bearer token 的 API"
    
    import json, sys, urllib.request
    
    MAC_URL = "http://192.168.1.xxx:11434/api/chat"
    MODEL   = "gemma4:e4b"
    
    question = " ".join(sys.argv[1:])
    if not question:
        print("Usage: python3 ask.py \"your question\""); sys.exit(1)
    
    payload = {
        "model": MODEL,
        "messages": [{"role": "user", "content": question}],
        "stream": False, "think": False,
        "options": {"num_ctx": 4096, "num_predict": -1},
    }
    req = urllib.request.Request(
        MAC_URL, data=json.dumps(payload).encode(),
        headers={"Content-Type": "application/json"},
    )
    with urllib.request.urlopen(req, timeout=300) as r:
        print(json.loads(r.read())["message"]["content"])
    # 使用範例
    python3 ask.py "寫一個 bash script,每天早上備份 ~/Documents 到外接硬碟"
    python3 ask.py "Python requests 怎麼設定 retry 和 timeout"
    python3 ask.py "這個 SQL 有什麼問題:SELECT * FROM orders WHERE date > NOW() - 30"

    什麼時候還是要 Claude

    任務 Mac gemma4 Claude API
    文章/log 摘要 ✅ 夠用,免費
    快速程式片段 ✅ 夠用,快
    翻譯、改寫 ✅ 夠用
    私人資料(不想送雲端) ✅ 最佳選擇
    程式碼審查(跨檔案) ❌ 沒有 context
    複雜架構決策 ❌ 推理不足
    Agent Team 自動化開發 ⚡ 出草稿 ✅ 審查整合

    原則是:不需要記憶 codebase、不需要複雜推理的任務,都可以先試 Mac。速度快、免費、資料不出門。遇到 Mac 答不好的,再升到 Claude。

    進階應用二:Mini PC + Mac 混合架構,讓 Agent Team 更有效率

    角色定義(固定,不隨任務改變)
    
    Mini PC  → 純指揮中心:跑 Claude Code、管理 Agent Team、處理串接邏輯
               不跑任何本地模型,資源用在穩定性和協調上
    
    Mac      → 推理後端:跑 Ollama + gemma4:e4b
               只負責生成,不做決策
    
    Claude API → 審查 + 架構:程式碼審查、複雜邏輯、跨檔案推理
                 Mini PC 透過網路呼叫,不在本地
    
    規則:Mac 不在線 → fallback 給 Claude API,不是 Mini PC 自己跑

    當你用 Claude Code 的 Agent Team 跑自動化程式開發時,會面對一個現實問題:Claude API 的費用隨 token 用量線性增長,而很多任務其實不需要 Claude 的完整推理能力——DTO 生成、CRUD 樣板、SQL migration 這類結構性重複工作,本地的 gemma4:e4b 就能處理。

    解法是把 Mac 當成 Agent Team 的「草稿後端」:Claude Agent 負責架構決策和程式碼審查,Mac gemma4 負責產生第一版草稿,再由 Claude 驗證整合。

    架構分工

    任務類型 交給誰 原因
    DTO / model class Mac gemma4 結構固定,重複性高
    CRUD endpoints 樣板 Mac gemma4 Pattern 固定,不需要推理
    SQL migration Mac gemma4 有範本可循
    Unit test 骨架 Mac gemma4 快速產出結構,Claude 填邏輯
    複雜業務邏輯 Claude sonnet 需要跨檔案理解,Mac 沒有 context
    安全相關程式碼 Claude sonnet/opus 不可靠的輸出風險太高
    架構決策 / Code Review Claude opus 需要深度推理與判斷

    前置設定:讓 Mac 的 Ollama 對區網開放

    Ollama 預設只監聽本機。在 Mac 上把它開放給區網,Mini PC 才能連進來:

    # Mac 上執行(停掉 Ollama app 後)
    OLLAMA_HOST=0.0.0.0 ollama serve
    
    # 從 Mini PC 驗證是否連得到(換成 Mac 的區網 IP)
    curl http://192.168.1.xxx:11434/api/tags

    不想暴露 port 的話,用 SSH Tunnel:Mini PC 上執行 ssh -L 11435:localhost:11434 [email protected] -N,之後打 localhost:11435 就等於打 Mac 的 Ollama。

    工具腳本:mac_draft.py

    Agent 透過 Bash tool 呼叫這支腳本,傳入任務描述,拿回草稿程式碼。腳本會自動檢查 Mac 是否在線,不在線就回傳 exit code 1,讓 Agent 自行處理 fallback。

    #!/usr/bin/env python3
    """
    mac_draft.py — Call Mac's local gemma4 for code draft generation.
    
    Usage:
        python3 mac_draft.py "write a SQLAlchemy User model with id, name, email"
        python3 mac_draft.py --task "CRUD for User" --context "FastAPI, SQLAlchemy async"
    
    Exit codes:
        0 = success, draft printed to stdout
        1 = Mac unreachable → fallback: implement with Claude directly
        2 = model error
    """
    import json, sys, urllib.request, urllib.error, argparse
    
    MAC_HOST = "http://192.168.1.xxx:11434"   # 改成 Mac 的實際 IP
    MODEL    = "gemma4:e4b"
    TIMEOUT  = 600
    
    SYSTEM_PROMPT = """You are a code generation assistant. Output ONLY code —
    no explanations, no markdown fences, no comments unless essential.
    The output will be reviewed and integrated by another agent."""
    
    def check_reachable():
        try:
            with urllib.request.urlopen(f"{MAC_HOST}/api/tags", timeout=5):
                return True
        except Exception:
            return False
    
    def generate(task, context=""):
        prompt = f"Context: {context}\n\nTask: {task}" if context else task
        payload = {
            "model": MODEL,
            "messages": [
                {"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user",   "content": prompt},
            ],
            "stream": False, "think": False,
            "options": {"num_ctx": 4096, "num_predict": -1},
        }
        data = json.dumps(payload).encode()
        req  = urllib.request.Request(
            f"{MAC_HOST}/api/chat", data=data,
            headers={"Content-Type": "application/json"},
        )
        with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
            return json.loads(r.read())["message"]["content"]
    
    parser = argparse.ArgumentParser()
    parser.add_argument("task", nargs="?")
    parser.add_argument("--task", dest="task_flag")
    parser.add_argument("--context", default="")
    args = parser.parse_args()
    
    task = args.task or args.task_flag
    if not task:
        print("ERROR: no task provided", file=sys.stderr); sys.exit(1)
    
    if not check_reachable():
        print(f"MAC_UNREACHABLE: {MAC_HOST}. Fallback: implement with Claude.",
              file=sys.stderr); sys.exit(1)
    
    try:
        print(generate(task, args.context))
    except Exception as e:
        print(f"ERROR: {e}", file=sys.stderr); sys.exit(2)

    Agent 的實際使用流程

    # Agent (sonnet) 在 Bash tool 中這樣呼叫:
    
    # 1. 請 Mac 出草稿
    draft=$(python3 ~/llm-benchmark/scripts/mac_draft.py \
      --task "generate SQLAlchemy User model" \
      --context "PostgreSQL, async, Pydantic v2")
    
    # 2. 檢查是否成功
    if [ $? -ne 0 ]; then
      echo "Mac unavailable, implementing directly"
      # Claude 自己寫
    fi
    
    # 3. 草稿給 Claude 審查後整合進 codebase
    echo "$draft"  # Claude 讀到這裡,決定是否採用、修改哪裡

    告訴 Agents 這條規則:寫入 AGENTS.md

    Claude Code 的 Agent Team 每個 subagent 啟動時沒有對話歷史。規則要寫進 AGENTS.md,agent 才會在每次任務開始時讀到它。在專案的 AGENTS.md 加上這個區塊:

    ## Mac Draft Resource (Local LLM Offload)
    
    Mac (gemma4:e4b) is available as a fast code draft generator.
    Tool: python3 ~/llm-benchmark/scripts/mac_draft.py
    
    Use Mac draft BEFORE writing code yourself for:
    - DTO / model class boilerplate      ✅
    - CRUD endpoints (standard pattern)  ✅
    - SQL migration scripts              ✅
    - Unit test scaffolding              ✅
    
    Do NOT use Mac draft for:
    - Complex business logic             ❌ (no codebase context)
    - Security-sensitive code            ❌ (unreliable)
    - Cross-file refactoring             ❌ (no context)
    - Architecture decisions             ❌ (use opus)
    
    Workflow:
    1. Call mac_draft.py with task description
    2. exit code 1 (MAC_UNREACHABLE) → implement with Claude directly
    3. Review draft: check patterns, imports, logic, security
    4. Integrate into codebase
    
    Mac generates the shape. Claude ensures it fits.

    這樣每個 subagent 都會知道「遇到樣板類任務先叫 Mac 出草稿」,不需要每次重新交代規則。

    結論與推薦

    本次測試跨越三個維度,每層都有明確的答案:

    Mini PC + Mac 混合架構的定位

    Mini PC 的角色是指揮中心,不是推理引擎。它跑 Claude Code、管理 Agent Team、處理串接邏輯,資源用在穩定性和協調上。推理工作全部交給 Mac 的 gemma4:e4b。

    • 日常問答 / 摘要:Mini PC 發問 → Mac gemma4 回答,免費、快速、資料不出門
    • 草稿程式碼:Agent 呼叫 mac_draft.py → Mac 出草稿 → Claude 審查整合
    • 複雜推理 / 架構決策:直接用 Claude API,不走 Mac
    • Mac 不在線:fallback 給 Claude API,Mini PC 本身不需要跑任何模型

    如果你的情境是 Mini PC 獨立運作(沒有 Mac),模型選擇建議:gemma4:e4b 速度最快(1.45 tok/s)、qwen3:14b 完成度最高(7/7 全答)、qwen3:4b 最省記憶體。但這個架構的速度上限就是 CPU 推論,不如 Mac 的 Apple Silicon。

    MacBook Air M3 的最佳設定

    • 日常程式碼:think:false,num_predict=-1,9.75 tok/s,輸出完整
    • 複雜推理/技術解釋:Thinking 開啟,速度只慢 6%,品質提升明顯
    • 長文件處理:設定 num_ctx=65536,充分利用 24GB 統一記憶體

    本地 LLM 的真實上限在哪裡?

    gemma4:e4b 在 MacBook Air M3 + Thinking 模式下,輸出品質接近 GPT-3.5 的水準——在程式碼生成、SQL 查詢、技術解釋這些結構清晰的任務上。它不是 GPT-4,創意寫作和跨領域推理還是有差距,但對開發者的日常工作場景已經夠用。

    真正的瓶頸不是模型大小,而是硬體架構。同款模型在 Apple Silicon 上跑出的效果,在 x86 CPU 上根本發揮不出來。如果你認真考慮本地 LLM,MacBook Air M3 是目前性價比最高的入門選擇;Mini PC 路線則需要搭配 NVIDIA GPU(VRAM ≥ 8GB)才能真正發揮。

  • Hacker News 每日精選 – 2026-04-04

    🚀 科技頭條:AI 治理的邊界與全球金融秩序的巨變

    今日科技圈的焦點集中在 AI 生態系統的治理衝突與底層技術的持續演進。從 Anthropic 對第三方工具的政策收緊,到全球儲備資產的歷史性轉移,這些動態不僅影響開發者的工具選擇,更預示了未來幾年商業競爭與技術標準的新賽局。身為開發者或決策者,理解這些變革背後的邏輯至關重要。

    🤖 AI / 機器學習

    Anthropic 不再允許 Claude Code 訂閱用戶使用 OpenClaw

    Anthropic 最近更新了其政策,明確禁止 Claude Code 的訂閱用戶配合使用第三方開源工具 OpenClaw。這項舉動引發了社群對於 AI 平台「圍牆花園」政策的激烈討論。許多開發者認為這限制了工具的靈活性與自動化潛力,但也反映了原廠對於 API 使用安全與商業利益的保護。這標誌著 AI 供應商開始從早期的開放探索轉向更嚴格的生態控制。

    原文連結:Tell HN: Anthropic no longer allowing Claude Code subscriptions to use OpenClaw

    大型語言模型中的情感概念及其功能

    Anthropic 發表了一項深入的研究,探討 AI 模型內部是如何表徵與處理「情感」概念的。研究顯示,模型內部存在特定的神經元激活模式,用以識別與模擬人類的情感狀態,這並非單純的文字模仿,而是具有一定的功能性結構。這項研究有助於我們理解 LLM 的可解釋性,並在未來開發出更具同理心或可控的 AI 系統。這對於致力於 AI 安全與人機互動的專家來說是極具價值的參考。

    原文連結:Emotion concepts and their function in a large language model

    🛠️ 開發工具

    Herbie:自動優化不精確的浮點數公式

    浮點數運算中的精準度損失一直是數值計算開發者的痛點,而 Herbie 提供了自動化的解決方案。它能掃描程式碼中的數學公式,找出可能導致誤差的環節,並自動改寫為更精確的等價公式。這對於科學計算、金融模擬或是對精確度有極高要求的系統來說,是一款不可多得的開源利器。透過這款工具,開發者可以減少在除錯 IEEE 754 規範相關問題上花費的時間。

    原文連結:Herbie: Automatically improve imprecise floating point formulas

    在 Android 上運行 Linux 容器,且無需 Root 權限

    Podroid 專案展示了在 Android 裝置上運行完整 Linux 容器的可能性,且過程不需要最高權限(Root)。這意味著行動裝置可以化身為輕量級的開發服務器或測試環境,極大地擴展了 Android 裝置的生產力邊界。該專案利用了 Linux 內核的 Namespace 等技術,對於對行動開發或邊緣運算感興趣的工程師來說,這是一個非常有趣的嘗試。這也證明了現代行動作業系統與傳統 Linux 環境的融合度正在提升。

    原文連結:Run Linux containers on Android, no root required

    將 Linux 主機轉變為路由器的七個關鍵配置

    這篇文章深入淺出地講解了如何將一台普通的多網口 Linux 主機配置成高效的路由器或交換機。作者詳細列出了包括開啟 IP 轉發、配置 NAT 以及優化內核參數等七項關鍵步驟。這不僅是一篇實用的教學,更是一堂深刻的網絡底層原理課,適合想要提升網絡調優技能的 DevOps 工程師。了解這些變更,能讓你更精確地掌握封包在系統內部的流轉邏輯。

    原文連結:What changes when you turn a Linux box into a router

    💼 創業 / 商業

    Delve 被移出 Y Combinator 創業名錄

    知名的創業孵化器 Y Combinator (YC) 最近將新創公司 Delve 從其官方名錄中移除,引發了業界的諸多猜測。雖然具體原因尚未公開,但這類行動通常涉及違規、業務轉型失敗或嚴重的治理問題。對於新創圈來說,這是一個警示案例,顯示即使進入了頂尖孵化器,仍需面臨嚴格的合規與道德審查。這也反映了目前投資市場對於透明度與經營質量的日益重視。

    原文連結:Delve removed from Y Combinator

    黃金超越美債成為全球最大外匯儲備資產

    這是一則震撼全球金融界的重磅消息:2026 年的數據顯示黃金已正式超越美國國債,成為各國央行持有的最大外匯儲備資產。這標誌著全球對美元體系的依賴正在降低,以及避險情緒的長期抬頭。對於科技業的長期投資者與跨國企業來說,這意味著需要更謹慎地評估匯率風險與宏觀經濟穩定性。這種宏觀金融格局的位移,將深刻影響未來的科技供應鏈與跨國投資流向。

    原文連結:Gold overtakes U.S. Treasuries as the largest foreign reserve asset

    🔓 開源專案 / 安全

    OpenClaw 存在提權漏洞(CVE-2026-33579)

    近期被廣泛討論的開源 AI 工具 OpenClaw 被揭露存在嚴重的權限提升漏洞。該漏洞允許攻擊者在特定條件下獲得系統的高級權限,對於將此工具整合入生產環境的企業來說,具有極高的安全風險。這再次提醒我們,在使用新興的開源 AI 封裝工具時,必須進行徹底的安全審計。隨著 AI 工具的普及,針對這類「連接器」的攻擊行為也正在急劇增加。

    原文連結:OpenClaw privilege escalation vulnerability

    🌍 其他

    阿提米絲二號機組拍攝到「壯麗」的地球圖像

    NASA 的阿提米絲二號(Artemis II)任務傳回了令人嘆為觀止的地球全景圖像,這為人類重返月球的計畫增添了更多信心。這張照片不僅展現了攝影技術在太空極端環境下的進步,也喚起了全球對太空探索的熱情。在繁瑣的技術開發與商業競爭之餘,這類來自太空的視覺震撼,提醒了我們科技發展的最原始初衷:探索未知的疆域。這對航太科技與光學儀器開發者來說是極大的激勵。

    原文連結:Artemis II crew take “spectacular” image of Earth

    iNaturalist:社群驅動的自然觀察平台

    iNaturalist 作為全球最大的自然觀察社群,再次受到 Hacker News 用戶的高度評價,其背後的公民科學精神令人感佩。該平台結合了機器學習圖像識別與專家社群審核,成功記錄了數百萬種生物的地理分布。這是一個展示科技如何服務於生態保護與科學教育的卓越範例。對於開發者而言,其如何處理龐大的多模態數據並維持社群活躍度,是非常值得學習的產品案例。

    原文連結:iNaturalist

    💡 今日觀點

    「今日的趨勢共同點在於:治理與精準。」

    從 Anthropic 對生態系統的管控,到 OpenClaw 的安全漏洞,再到 Delve 從 YC 的消失,我們可以看到 AI 與創業圈正經歷從「野蠻生長」到「規範治理」的轉型。同時,像 Herbie 這類對浮點數精準度的極致追求,以及 Linux 網絡底層的深度配置,則提醒我們即便身處 AI 時代,紮實的電腦科學基礎(CS Fundamentals)依然是工程師的核心競爭力。

    🛠️ 給讀者的行動建議:

    • 審核依賴項: 如果你的專案依賴於第三方 AI 封裝工具(如 OpenClaw),請立即檢查其安全性與原廠政策合規性。
    • 重拾基礎: 在 AI 輔助寫 code 的時代,花時間理解像浮點數運算或網絡協議等底層細節,能讓你解決 AI 無法處理的深層 Bug。
    • 關注避險: 面對全球金融格局(黃金 vs 美債)的變動,跨國技術開發者應對雲端服務成本與跨國支付的潛在波動保持警覺。
  • 失智症照護行動指南:媽媽的每日工作站

    失智症照護行動指南:媽媽的每日工作站

    對象:65歲,阿茲海默中期

    經歷:醫院護工10年、廚房助工20年、會計7年

    核心原則:她不是病人,是來上班的人


    一、採買清單

    傳統市場(每週一次)

    買什麼 跟攤販怎麼說 預算 用途
    整球蒜頭 2-3斤 「要整球的,不要剝好的」 80-180元 每天剝一斤
    帶殼毛豆 一大袋 直接買就是帶殼的 50-70元 剝殼
    帶葉玉米 3-5支 「整支帶葉子的」 50-100元 剝外葉去鬚
    整把帶根蔥 市場本來就是整把的 20-30元 剝外層去根鬚
    帶蒂辣椒 直接買 20-30元 去蒂去籽
    整顆高麗菜 不要切半的 30-50元 手撕成小片
    杏鮑菇/金針菇 直接買 30-50元 手撕成條

    南北貨行 / 雜糧行(每月一次)

    買什麼 預算 說明
    帶殼花生 3-5斤 90-250元 最耗時,CP值之王
    帶殼蓮子 1-2斤 80-160元 去皮去芯極度費工
    散裝紅豆、綠豆、花豆各1斤 100-150元 混在一起讓她分類
    帶殼瓜子 1-2斤 60-120元 剝殼取仁

    全聯 / 大賣場

    買什麼 預算 說明
    餃子皮(冷藏) 30-50元 搭配備好的餡,她在客廳包
    糯米粉 30元 加水揉好端出去讓她搓湯圓
    餛飩皮 30-40元 比餃子小顆,數量多更耗時

    一次性購入

    買什麼 預算 用途
    塑膠桌墊 50-100元 鋪客廳桌當工作檯
    小盆子/碗 4-5個 家裡有就好 分裝成品,剝好的跟沒剝的分開
    電子血壓計 500-800元 讓她自己量,啟動護工記憶
    印章+印泥 50-100元 會計蓋章用
    釘書機(小型) 50元 裝訂紙張
    計算機 100元 按數字、加總

    每月預算

    項目 月花費
    市場食材 300-500元
    餃子皮/糯米粉等 100-200元
    雜糧豆類 100-150元
    合計 約 500-850元/月

    二、每日排班表

    地點:客廳大桌(鋪塑膠墊)

    早上(主要工作時段)

    時間 活動 身分 你要準備什麼
    8:00 給她抹布,擦桌子、擦扶手 護工 一條乾淨抹布
    8:20 撕高麗菜+撕菇類 廚房 整顆菜+盆子放桌上
    8:50 剝蒜頭 廚房 整球蒜頭+兩個碗
    9:30 撥豆芽 廚房 自家種的豆芽
    9:45 分類零錢或整理發票 會計 零錢罐+分類碗 / 一疊發票
    10:15 剝花生或包餃子 廚房 帶殼花生 / 餃子皮+餡料
    11:00 摺毛巾、配對襪子 護工 一籃洗好的衣物毛巾
    11:30 擦餐具排碗筷 廚房 洗好的碗筷+擦布

    下午(輕量活動,看狀態調整)

    活動 身分 說明
    量血壓、記錄 護工 讓她自己操作
    澆花、擦葉子 護工 如果家裡有盆栽
    抄寫數字 / 按計算機 會計 給她紙筆或一張「帳單」
    蓋章 / 釘書機裝訂 會計 紙張+印章
    剝花生 / 剝瓜子 廚房 可以邊看電視邊做

    每週大工程輪替(避免做膩)

    週一 週二 週三 週四 週五
    包餃子 搓湯圓 剝蓮子 包餛飩 揉麵團

    三、話術指南

    核心原則:她是來上班的,不是被安排活動

    情境 不要說 要說
    給她工作 「來,做這個打發時間」 「這個麻煩妳處理一下」
    她做完了 「好棒喔」 「手腳很快耶」
    換下一個任務 「接下來玩這個」 「這個好了,接下來這邊幫我弄一下」
    擦桌子 「你擦一擦好不好?」 「這邊你順便整理一下」
    分零錢 「來數數看」 「這些幫我對一下」
    她說要回家 「這裡就是家啊」 「忙完這些就休息」
    居服員來 「阿姨來照顧你了」 「來幫忙的來了」

    依身分切換語氣

  • 廚房的事 → 像主廚交代備料的語氣
  • 護工的事 → 像護理長分配工作的語氣
  • 會計的事 → 像主管交辦的語氣
  • 「要回家」的應對

    不糾正、不否定、不講道理。

    她:「我要回家」

    你:「妳想家了齁。」(先接住)

    她:「對啊我要回去」

    你:「妳家是什麼樣子?」(讓她說,回憶本身就是回家)

    或:「好,忙完這些就休息。」(用工作自然轉移)

    或:「妳在家的時候都做什麼?」(引導進入回憶)


    四、注意事項

    安全

    項目 做法
    刀具 所有需要切的你在廚房先處理好,她只做手撕手剝的
    火源 廚房維持鎖住
    硬幣 分零錢時在旁邊,確認不會放嘴巴。有疑慮改用發票分類
    花生 剝殼工作,不是吃。成品收走,不要邊剝邊吃避免噎到
    釘書機 觀察使用狀況,亂釘就收起來

    廁所

    項目 做法
    廁所辨識 廁所門貼大張馬桶圖片,門保持開著、燈保持亮
    定時提醒 每 1.5-2 小時說「休息一下,去上個廁所再繼續做」
    動線 確保她坐的位置看得到廁所方向

    窗戶

    項目 做法
    維持鎖死 換隱藏式鎖具(內六角螺絲型),看不出來也拆不了
    補償光照 窗簾白天全開,讓自然光進來
    補償換氣 其他房間開窗或開空氣清淨機
    減少暗示 用窗簾遮住鎖具

    成果處理

    原則 說明
    成品要真的被使用 她剝的蒜真的拿去炒菜,包的餃子真的煮來吃
    不要當面重做 即使做得不完美也直接收起來
    可以重複利用 零錢分完收起來隔天再拿出來,她不會記得做過
    讓她看到結果 「妳剝的蒜頭,剛炒了一盤菜,很香」

    居服員溝通重點

  • 不要用「帶你去」→ 改用「陪你」或「你帶我去」
  • 讓她走在前面、她決定方向
  • 居服員的角色從「帶領者」變成「跟隨者」
  • 進來時先跟她一起做事,像同事,不是來服務她
  • 如果可以,讓她指揮居服員做事(她當師傅,居服員當學徒)

  • 五、行為解讀速查

    她的行為 可能的意思 建議回應
    撞窗戶要出去 「我該去上班了」/ 想奪回控制感 帶她到工作桌「上工」
    說要回家 想回到熟悉安全的時空 接住情緒,不糾正事實
    抗拒居服員 覺得所有人都在控制她 讓居服員用同事模式
    拒絕被帶出門 不是不想出門,是不想被決定 給她假選擇權:「想去公園還是巷口?」
    藏東西 在奪回控制權 檢查固定藏東西的位置
    在廚房大便 廚房是她認定的「自己地盤」/找不到廁所 廁所標示+定時帶去
    安靜不動 可能是平靜,也可能是退縮 觀察表情:有回應=好,空洞=注意

    六、觀察記錄(每天花2分鐘記)

    留意以下項目,找出規律:

  • 今天情緒好的時段?當時在做什麼?
  • 今天情緒差的時段?之前發生了什麼?
  • 有沒有說要回家?幾點說的?
  • 有沒有想開窗?幾點?
  • 進食狀況?
  • 睡眠狀況?
  • 如廁狀況?

  • 最重要的一句話:

    她一輩子都在服務別人——護工照顧病人、廚房餵飽客人、會計管好帳。

    她的價值感來自「被需要」。所有活動的核心話術只有一個:

    「這個要麻煩妳了。」

  • 後端老兵的工具箱:C# 非同步、Python 逆向工程、架構選型實戰

    後端老兵的工具箱:C# 非同步、Python 逆向工程、架構選型實戰

    寫了十幾年後端,從 SQL Server DBA 一路走到架構師,我發現後端開發實戰能力的核心從來不只是會寫 SQL。真正拉開差距的,是你工具箱裡有多少把不同的扳手。這篇文章整理了我在 dotblogs 上累積的幾個關鍵主題:C# 非同步模式、Python 逆向工程實戰、資料庫架構選型、壓力測試,以及從 DBA 到架構師的技術演進。每一個主題都不是教科書式的介紹,而是實際踩過坑之後的心得。後端開發實戰最重要的一課:不要只讀,要動手測。

    TL;DR 重點摘要

    • C# 非同步控制:SemaphoreSlim 比 lock 更適合 async 場景,BlockingCollection 是 producer-consumer 的標準解法,別再自己造輪子。
    • Python 逆向工程:當 API 太貴,直接逆向 Web 介面的 JSON 回應是可行路線,但要做好延遲控制與路徑文件化。
    • 架構選型:資料庫不是選最潮的,而是看存取模式決定 — 高頻讀用 cache、搜尋用倒排索引、日誌用列式儲存。
    • 壓測不是選配:沒壓測過的系統就是紙老虎,JMeter 的 Thread Group + Listener 是最基本的品質門檻。

    1. C# 非同步模式 — Semaphore 與 BlockingCollection

    在 .NET 後端開發中,非同步處理是繞不開的主題。當你有 100 個 HTTP 請求要同時發出去,但目標伺服器只能承受 10 個並發時,你需要的不是 lock,而是 SemaphoreSlim

    為什麼 Semaphore 比 lock 更適合 async?

    很多人習慣用 lock 來控制並發,但 lock 有一個致命問題:它不支援 async/await。你不能在 lock 區塊裡面 await,否則會收到編譯錯誤。即使你繞過去了(用 Monitor),async 的 continuation 可能在不同執行緒上執行,導致 unlock 失敗。

    SemaphoreSlim 則原生支援 WaitAsync(),專為 async 場景設計。它的心智模型是「停車場」:車位(permit)有限,滿了就在外面等,有車出來才放行。

    // SemaphoreSlim throttling concurrent HTTP requests
    public async Task<List<string>> FetchAllAsync(List<string> urls)
    {
        // Only allow 10 concurrent requests
        var semaphore = new SemaphoreSlim(10);
        var httpClient = new HttpClient();
        var tasks = urls.Select(async url =>
        {
            await semaphore.WaitAsync();
            try
            {
                var response = await httpClient.GetStringAsync(url);
                return response;
            }
            finally
            {
                semaphore.Release();
            }
        });
    
        var results = await Task.WhenAll(tasks);
        return results.ToList();
    }
    

    注意 finally 裡的 Release() — 不管成功或失敗都要釋放,否則 permit 會洩漏,最終所有請求都會卡住。

    BlockingCollection:Producer-Consumer 的標準解法

    BlockingCollection<T> 是 .NET 內建的執行緒安全佇列,底層預設使用 ConcurrentQueue。它最強大的特性是 GetConsumingEnumerable() — consumer 端可以用 foreach 持續等待新資料,直到 producer 呼叫 CompleteAdding()

    // Producer-Consumer pattern with BlockingCollection
    var queue = new BlockingCollection<WorkItem>(boundedCapacity: 100);
    
    // Producer thread
    Task.Run(() =>
    {
        foreach (var item in GetWorkItems())
        {
            // Blocks if queue is full (back-pressure!)
            queue.Add(item);
            Console.WriteLine($"Produced: {item.Id}");
        }
        queue.CompleteAdding(); // Signal no more items
    });
    
    // Consumer thread
    Task.Run(() =>
    {
        // Blocks automatically when queue is empty
        // Exits when CompleteAdding() is called and queue is drained
        foreach (var item in queue.GetConsumingEnumerable())
        {
            ProcessItem(item);
            Console.WriteLine($"Consumed: {item.Id}");
        }
    });
    

    這裡的 boundedCapacity: 100 是關鍵 — 它提供了背壓(back-pressure)機制。當 consumer 處理速度跟不上 producer 時,佇列滿了 producer 就會被阻塞,而不是無限制地吃記憶體。這跟傳統 ThreadPool 的固定 worker 模型不同:ThreadPool 維護一組固定的執行緒,而 Semaphore + BlockingCollection 用的是「等待」機制,更彈性也更省資源。


    2. Python 逆向工程 — 當 API 太貴,我就自己拆

    2021 年我碰到一個需求:要把 2600 多筆地址轉成經緯度座標。Google Maps Geocoding API 當時每 1000 次要收 5 美元,算一算要十幾美元。對一個一次性的專案來說,這太不划算了。

    於是我打開 Chrome DevTools,觀察 Google Maps 搜尋框的網路請求。發現它回傳的不是標準 JSON API,而是一個巢狀極深的陣列結構。座標藏在類似 d[16][0][0][7][1][3] 這種路徑裡。

    import requests
    import json
    import time
    import random
    
    def geocode_address(address):
        """Reverse-engineered Google Maps search to extract coordinates."""
        url = "https://www.google.com/maps/search/"
        params = {"q": address}
        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                           "AppleWebKit/537.36 Chrome/91.0"
        }
    
        resp = requests.get(url, params=params, headers=headers)
        text = resp.text
    
        # The response contains a deeply nested JSON-like structure
        # Find the coordinate data block
        try:
            # Extract the nested array from response
            start = text.find("window.APP_INITIALIZATION_STATE")
            if start == -1:
                return None
    
            # Parse the nested structure
            data_start = text.find("[", start)
            data_end = text.find("];", data_start) + 1
            raw = text[data_start:data_end]
    
            # Navigate the deeply nested path for coordinates
            parsed = json.loads(raw)
            # Path varies by response type, document it!
            lat = parsed[16][0][0][7][1][3][0]
            lng = parsed[16][0][0][7][1][3][1]
            return (lat, lng)
        except (IndexError, KeyError, json.JSONDecodeError):
            return None
    
    
    def batch_geocode(addresses):
        """Process addresses with random delays to avoid detection."""
        results = {}
        for i, addr in enumerate(addresses):
            coords = geocode_address(addr)
            results[addr] = coords
            print(f"[{i+1}/{len(addresses)}] {addr} -> {coords}")
    
            # Random delay: 3~15 seconds to mimic human behavior
            delay = random.uniform(3, 15)
            time.sleep(delay)
    
        return results
    

    幾個實戰經驗:

    • 隨機延遲是必須的:固定間隔很容易被偵測,3 到 15 秒的隨機延遲更像人類行為。
    • 文件化提取路徑d[16][0][0][7][1][3] 這種路徑完全沒有語意,Google 隨時可能改結構。你必須在程式碼旁邊寫清楚這條路徑代表什麼,否則三個月後你自己也看不懂。
    • 錯誤處理要寬容:有些地址搜不到、有些回傳結構不同,用 try/except 包住並記錄失敗的地址,事後手動補。
    • 這不是長期方案:逆向工程的結果隨時會因為前端改版而失效,只適合一次性或低頻的資料收集。

    最終我用這個方法在一個週末處理完 2600 多筆地址,成本是零。但我也很清楚:如果這是一個需要長期維護的服務,老老實實付 API 費用才是正道。


    3. 架構選型 — 不同系統該用什麼資料庫?

    2021 年我在部落格上寫了一系列「架構師慢慢學」的文章,其中最受歡迎的是資料庫選型。核心觀點只有一句話:資料庫的選擇應該由存取模式驅動,而不是技術潮流。

    太多團隊因為「大家都在用 MongoDB」就把所有東西塞進去,結果需要 JOIN 時痛不欲生。也有人因為「Redis 很快」就把所有資料都放 cache,然後面對一致性問題束手無策。

    系統類型 vs 資料庫選型對照表

    系統類型 建議資料庫 原因
    後台管理系統(Admin) RDBMS(PostgreSQL / SQL Server) 低併發、需要複雜查詢和 JOIN、資料完整性優先
    高流量前台系統 RDBMS 後端 + Redis 前端 RDBMS 保證資料正確,Redis 用 Key/Value 加速讀取
    日誌 / Log 系統 列式儲存(ClickHouse)+ 倒排索引(Elasticsearch) 寫入量大、需要聚合分析和全文搜尋
    交易系統 RDBMS + Cache + 一致性協議 ACID 不可妥協,cache 用於讀加速但需要失效策略
    即時監控系統 時序資料庫(InfluxDB / TimescaleDB) 時間序列寫入優化、自動聚合降精度
    搜尋系統 RDBMS 後端 + Elasticsearch 前端 RDBMS 為資料源,ES 提供倒排索引加速模糊搜尋

    幾個決策原則:

    1. 先問讀寫比例:讀多寫少 → 考慮加 cache;寫多讀少 → 考慮列式儲存或訊息佇列緩衝。
    2. 再問一致性需求:金融交易不能最終一致,社群按讚可以。
    3. 最後問查詢模式:需要 JOIN → RDBMS;需要全文搜尋 → 倒排索引;需要時間範圍聚合 → 時序 DB。

    這不是什麼高深的理論,但我看過太多團隊在第一步就跳過去,直接被「這個技術很紅」帶著走。


    4. JMeter 壓測 — 不壓測的系統都是紙老虎

    你寫的 API 在開發機上跑得飛快,但上線後 100 人同時用就當機了。這種事我見過不止一次。壓力測試不是「有空再做」的事情,它是品質門檻。

    JMeter 基本設定

    Apache JMeter 是免費的壓測工具,核心概念只有三個:

    • Thread Group(執行緒群組):模擬多少使用者同時操作。設定 Number of Threads = 100 就是 100 個並發使用者。
    • Sampler(取樣器):每個使用者要做什麼動作。最常用的是 HTTP Request Sampler,填入 URL、Method、Body 就行。
    • Listener(監聽器):收集結果的報表。Summary Report 給你吞吐量和錯誤率,Aggregate Report 給你百分位數響應時間。

    關鍵指標怎麼看

    指標 意義 健康標準(參考)
    Throughput 每秒處理的請求數 依業務而定,但應隨並發數線性增長直到瓶頸
    P90 Response Time 90% 的請求在此時間內完成 一般 API < 500ms
    P99 Response Time 99% 的請求在此時間內完成 應 < P90 的 3 倍,否則有長尾問題
    Error Rate 失敗請求的百分比 < 0.1% 為優秀,> 1% 要警覺

    常見錯誤

    1. 在同一台機器上跑 JMeter 和被測服務:JMeter 本身也吃 CPU 和記憶體,會互相干擾。壓測機和被測機必須分開。
    2. 沒有暖機(Warm-up):JVM 或 .NET 的 JIT 編譯在前幾次請求時會比較慢,應該先跑一輪不計入結果的請求。
    3. 只看平均值:平均響應時間 200ms 看起來很好,但如果 P99 是 5 秒,代表每 100 個使用者就有 1 個等 5 秒。看百分位數才有意義。
    4. 不模擬真實場景:所有人都打同一個 API endpoint 不代表真實負載。應該混合不同操作的比例。

    我的習慣是:在專案中期就開始跑基準壓測,而不是上線前才慌張地補。早發現瓶頸,修復成本低十倍。


    5. 從 DBA 到架構師 — 我的技術演進路線

    我的技術路線不是一開始就規劃好的,而是一步步堆疊出來的:

    • 2020:SQL Server DBA — 每天看執行計劃、調索引、處理 deadlock。這個階段讓我理解了資料庫內部的儲存引擎、鎖機制、B-Tree 索引結構。
    • 2021:.NET 後端開發 — 開始寫 C# Web API,發現 DBA 背景讓我寫出的 SQL 比大多數工程師都好。但也發現自己在非同步、設計模式上的不足。
    • 2022-2023:全端開發 — 接觸前端、Python、爬蟲、自動化。工具箱從一把螺絲起子變成一整個工具箱。
    • 2024-2026:架構設計 — 開始做系統設計、技術選型、效能規劃。發現以前每個階段的經驗都在這裡匯聚。

    DBA 背景帶給我的不公平優勢

    理解資料庫內部運作,會從根本上改變你寫應用程式的方式:

    • 你知道 SELECT * 在有 covering index 時多浪費多少 I/O,所以你會主動只選需要的欄位。
    • 你知道 NVARCHARVARCHARDATALENGTH() 下的差異,所以你會根據實際資料選擇正確的型別。
    • 你看過太多 table scan 的慘案,所以你設計 API 時會強制分頁,而不是讓使用者一次撈全部。
    • 你理解 transaction isolation level 的差異,所以你知道什麼時候用 READ COMMITTED SNAPSHOT 可以大幅降低鎖爭用。

    給後端工程師的建議

    如果你想往架構方向發展,我的建議是:學任何東西都要動手測試

    • 想搞懂索引?建一個百萬筆的測試表,比較有索引和沒索引的執行計劃。
    • 想搞懂 SemaphoreSlim?寫一個 console app,開 1000 個 task,觀察不同 permit 數量的效果。
    • 想搞懂資料庫選型?不要只讀比較文章,自己用 Docker 裝一個 ClickHouse、一個 PostgreSQL,塞相同的資料,跑相同的查詢,比較速度。

    2021 年我在部落格上寫「架構師慢慢學」系列時,最大的收穫不是寫出來的文章,而是為了寫文章去做的那些實驗。讀十篇文章不如自己跑一次 DATALENGTH() 比較 CHARVARCHAR 的儲存差異。那個數字會刻在你腦子裡,比任何文章都深。


    結語

    後端工程師的價值不在於精通某一個框架或語言,而在於工具箱的廣度和深度。C# 的非同步模式讓你處理高並發,Python 的靈活性讓你快速解決一次性問題,架構選型的思維讓你做出正確的技術決策,壓力測試讓你對系統有信心。

    這些東西沒辦法在一天內學會,但每一個都值得你花時間去實驗。畢竟,不壓測的系統是紙老虎,不動手的學習是紙上談兵

    如果你也是從 DBA 或其他專精領域起步的工程師,不要覺得自己起步晚。每一個階段的深度經驗,都會在你走向架構師的路上成為別人沒有的武器。

  • SQL Server 踩坑實錄:從 DELETE 不釋放空間到 NOT IN 效能炸彈

    SQL Server 踩坑實錄:從 DELETE 不釋放空間到 NOT IN 效能炸彈

    這篇文章是我從 DBA 到全端架構師這幾年,在 SQL Server 效能優化上踩過的坑的總整理。不是教科書式的理論,而是每一條都是我實際測試、實際踩雷後的血淚經驗。如果你正在處理 SQL Server 效能優化的問題——DELETE 後空間沒釋放、查詢莫名其妙變慢、鎖定機制搞不清楚——這篇應該能幫你少走不少冤枉路。

    TL;DR 重點摘要

    • DELETE 不會釋放磁碟空間,只是標記刪除。要真正回收空間,必須用 TRUNCATE 或 ALTER INDEX REBUILD。
    • NOT IN 是效能炸彈,改用 NOT EXISTS 可以讓查詢快數十倍,尤其在子查詢結果集大的時候。
    • 沒有 TABLOCKX 的交易不安全,並行交易會讀到未提交的資料(Dirty Read),高併發場景務必設定正確的隔離層級。
    • 暫存表不是都一樣,@ 表變數、# 本地暫存表、## 全域暫存表各有適用場景,選錯會嚴重影響效能。

    1. 儲存空間的真相 — DELETE 真的刪除了嗎?

    這大概是我當 DBA 第一年最震驚的發現:DELETE 不會釋放磁碟空間。它只是把資料列標記為「ghost record」,等待背景的 Ghost Cleanup 程序來處理。但即使 Ghost Cleanup 跑完了,那些頁面(Page)還是屬於該表的配置空間,不會歸還給作業系統。

    如果你想真正回收空間,只有兩條路:

    • TRUNCATE TABLE:直接釋放所有資料頁面,包含 7-byte 的列標頭(row header),速度極快,但會清除所有資料。
    • ALTER INDEX … REBUILD:重建索引時重新組織頁面,回收碎片化的空間。

    三種配置單元(Allocation Unit)

    SQL Server 在底層把資料分成三種配置單元儲存:

    配置單元 儲存內容 典型欄位類型
    IN_ROW_DATA固定長度 + 行內變動長度資料char, int, datetime, nvarchar(100)
    LOB_DATA大型物件資料nvarchar(max), text, image, xml
    ROW_OVERFLOW_DATA超過 8060 bytes 的變動長度資料nvarchar 超過行內限制時溢出

    CHAR vs VARCHAR 的儲存差異

    很多人以為「反正都是存字串」,但底層差異巨大。CHAR(100) 不管你存 1 個字還是 100 個字,永遠佔用 100 bytes。VARCHAR(100) 則只儲存實際資料長度加上 2 bytes 的長度前綴。

    -- Verify with DATALENGTH()
    DECLARE @fixed CHAR(100) = 'Hello';
    DECLARE @variable VARCHAR(100) = 'Hello';
    
    SELECT DATALENGTH(@fixed) AS CharLength,      -- Result: 100
           DATALENGTH(@variable) AS VarcharLength; -- Result: 5

    另外要注意:索引佔用的是真實空間(IN_ROW_DATA)。當你清空表後,索引也被清空。但只要 INSERT 新資料,索引會立即重新填充。而且 VARCHAR 欄位建索引時,仍然受到 900 bytes 索引鍵大小限制

    Azure SQL Database 的常見假警報

    在 Azure SQL Database 上,大量 DELETE 後看到儲存空間快滿了——這是假警報。空間根本沒被釋放。DELETE ... WITH (TABLOCK) 效果有限,必須搭配 TRUNCATEALTER INDEX ALL ON [TableName] REBUILD 才能真正回收。


    2. 鎖定機制 — 你的交易真的安全嗎?

    我曾經在生產環境遇過一個離奇的 Bug:兩筆交易同時更新同一張表,結果一筆交易讀到了另一筆還沒 COMMIT 的資料。這就是經典的 Dirty Read(髒讀)

    不使用適當鎖定機制時,會碰到三種資料異常:

    異常類型 說明 情境
    Dirty Read(髒讀)讀到其他交易未提交的資料T1 UPDATE 未 COMMIT,T2 SELECT 讀到修改後的值
    Non-repeatable Read(不可重複讀)同一交易內兩次讀取結果不同T1 SELECT → T2 UPDATE COMMIT → T1 再次 SELECT 結果變了
    Phantom Row(幻影列)同一交易內多出新的資料列T1 SELECT → T2 INSERT COMMIT → T1 再次 SELECT 多了一列

    正確的做法:TABLOCKX 排他鎖

    -- Problem: Without TABLOCKX, T2 can read T1's uncommitted changes
    -- Session 1
    BEGIN TRAN T1;
    UPDATE Orders SET Amount = 999 WHERE OrderID = 1;
    -- (not committed yet)
    
    -- Session 2 (runs concurrently, sees Amount = 999 → Dirty Read!)
    SELECT Amount FROM Orders WHERE OrderID = 1;
    
    -- Solution: Use TABLOCKX for exclusive access
    BEGIN TRAN T1;
    SELECT * FROM Orders WITH (TABLOCKX) WHERE OrderID = 1;
    -- Now T2 is BLOCKED until T1 commits or rolls back
    UPDATE Orders SET Amount = 999 WHERE OrderID = 1;
    COMMIT TRAN T1;

    在高併發場景中,如果不想鎖整張表,也可以考慮設定交易隔離層級為 SERIALIZABLE 或使用 ROWLOCK, UPDLOCK 組合,但 TABLOCKX 是最簡單粗暴且確定有效的方式。


    3. 查詢優化 — EXISTS vs IN 的效能陷阱

    這個坑我踩了不止一次。先講結論:

    • 正向查詢(EXISTS vs IN):執行計畫幾乎相同,效能差異不大。
    • 否定查詢(NOT EXISTS vs NOT IN)NOT EXISTS 遠遠快於 NOT IN,差距可達數十倍。
    -- NOT IN: Slow — performs O(n*m) comparison, NULL handling issues
    SELECT * FROM Products
    WHERE ProductID NOT IN (
        SELECT ProductID FROM OrderDetails
    );
    
    -- NOT EXISTS: Fast — uses semi-join, stops at first match
    SELECT * FROM Products p
    WHERE NOT EXISTS (
        SELECT 1 FROM OrderDetails od
        WHERE od.ProductID = p.ProductID
    );
    
    -- Additional trap: if OrderDetails.ProductID contains ANY NULL value,
    -- NOT IN returns ZERO rows! NOT EXISTS handles NULL correctly.

    NOT IN 之所以慢,是因為它必須對子查詢的每一筆結果做比對,而且還要處理 NULL 的三值邏輯。NOT EXISTS 則是用半連接(Semi-Join)策略,找到第一筆匹配就停止。

    排序與 TOP 的隱藏陷阱

    加上 TOP 之後,SQL Server 的排序演算法會完全改變。沒有 TOP 時用完整排序(Full Sort),有 TOP 時用 Top-N Sort,記憶體需求和執行路徑完全不同。

    SQL 執行順序(必背)

    很多查詢優化的問題,根源是不理解 SQL 的實際執行順序:

    FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → TOP/OFFSET

    注意 SELECT 在 WHERE 之後,所以你不能在 WHERE 中使用 SELECT 裡定義的別名。而 ORDER BY 在 SELECT 之後,所以可以用別名排序。理解這個順序,很多「為什麼這樣寫不行」的問題都迎刃而解。

    另外,索引不只消除全表掃描,還能跳過排序階段。如果 ORDER BY 的欄位剛好有索引,SQL Server 可以直接按索引順序讀取,省掉排序的 CPU 和記憶體開銷。


    4. 全文檢索 — 比 LIKE ‘%keyword%’ 快一百倍

    如果你的應用有「搜尋文章內容」的需求,還在用 LIKE '%keyword%',那你的查詢基本上每次都是全表掃描。全文檢索(Full-Text Search)透過反向索引(Inverted Index)來加速文字搜尋,效能差距是數量級的。

    建立全文檢索的前提與步驟

    前提:目標表必須有主鍵(Primary Key)。因為反向索引需要唯一識別碼來對應每筆資料。

    -- Step 1: Enable full-text search on the database (if not already)
    -- (SQL Server installs Full-Text Search as a feature)
    
    -- Step 2: Create a full-text catalog
    CREATE FULLTEXT CATALOG ftCatalog AS DEFAULT;
    
    -- Step 3: Create a full-text index on the table
    -- The table MUST have a primary key
    CREATE FULLTEXT INDEX ON Articles (
        Title LANGUAGE 1028,      -- 1028 = Traditional Chinese
        Content LANGUAGE 1028
    )
    KEY INDEX PK_Articles          -- Must reference the PK
    ON ftCatalog
    WITH CHANGE_TRACKING AUTO;     -- Auto-update when data changes
    
    -- Step 4: Query using CONTAINS or FREETEXT
    SELECT * FROM Articles
    WHERE CONTAINS(Content, N'效能優化');
    
    -- Compare with LIKE (full table scan every time)
    SELECT * FROM Articles
    WHERE Content LIKE N'%效能優化%';

    語言設定很重要:LANGUAGE 1028(繁體中文)會使用對應的斷詞器(Word Breaker),直接影響搜尋品質。英文斷詞用空格就行,但中文斷詞需要語意分析,設錯語言會導致搜不到結果。


    5. 監控與追蹤 — 沒有 Profiler 怎麼辦?

    SQL Server Profiler 在生產環境不一定能用(效能開銷太大,或者根本沒權限)。這時候 DMV(Dynamic Management Views)就是你的救星。

    追蹤特定時間範圍的查詢

    -- Find top queries by CPU time within a time range
    SELECT TOP 20
        qs.last_execution_time,
        qs.execution_count,
        qs.total_worker_time / 1000 AS total_cpu_ms,
        qs.total_elapsed_time / 1000 AS total_elapsed_ms,
        qs.total_logical_reads,
        SUBSTRING(st.text,
            (qs.statement_start_offset / 2) + 1,
            ((CASE qs.statement_end_offset
                WHEN -1 THEN DATALENGTH(st.text)
                ELSE qs.statement_end_offset
              END - qs.statement_start_offset) / 2) + 1
        ) AS query_text
    FROM sys.dm_exec_query_stats qs
    CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
    WHERE qs.last_execution_time >= '2026-04-04 09:00:00'
      AND qs.last_execution_time <= '2026-04-04 18:00:00'
    ORDER BY qs.total_worker_time DESC;

    查看當前執行中的程序

    -- Quick check: who's running what right now?
    EXEC sp_who2;
    
    -- Or with more detail via DMV
    SELECT
        r.session_id,
        r.status,
        r.command,
        r.wait_type,
        r.wait_time,
        t.text AS query_text,
        r.cpu_time,
        r.reads,
        r.writes
    FROM sys.dm_exec_requests r
    CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
    WHERE r.session_id > 50; -- Exclude system sessions

    全庫儲存空間盤點

    -- Iterate all user tables and check space usage
    CREATE TABLE #SpaceUsed (
        TableName NVARCHAR(128),
        Rows NVARCHAR(20),
        Reserved NVARCHAR(20),
        Data NVARCHAR(20),
        IndexSize NVARCHAR(20),
        Unused NVARCHAR(20)
    );
    
    DECLARE @tbl NVARCHAR(128);
    DECLARE tbl_cursor CURSOR FOR
        SELECT TABLE_SCHEMA + '.' + TABLE_NAME
        FROM INFORMATION_SCHEMA.TABLES
        WHERE TABLE_TYPE = 'BASE TABLE';
    
    OPEN tbl_cursor;
    FETCH NEXT FROM tbl_cursor INTO @tbl;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        INSERT INTO #SpaceUsed
        EXEC sp_spaceused @tbl;
        FETCH NEXT FROM tbl_cursor INTO @tbl;
    END
    
    CLOSE tbl_cursor;
    DEALLOCATE tbl_cursor;
    
    SELECT * FROM #SpaceUsed ORDER BY CAST(REPLACE(Reserved, ' KB', '') AS BIGINT) DESC;
    DROP TABLE #SpaceUsed;

    6. 暫存表大全 — @、#、## 到底差在哪?

    SQL Server 有三種暫存表,看起來差一個符號,但行為天差地別:

    特性 @TableVar(表變數) #TempTable(本地暫存表) ##GlobalTemp(全域暫存表)
    儲存位置記憶體(小量時)/ tempdbtempdbtempdb
    統計資訊無(優化器假設 1 列)
    作用範圍當前批次/程序當前 Session所有 Session
    交易回滾不受 ROLLBACK 影響受 ROLLBACK 影響受 ROLLBACK 影響
    可建索引僅限宣告時的約束可隨時建立可隨時建立
    適用場景少量資料(< 100 列)大量資料、需要索引跨 Session 共享資料

    選擇指南

    • 資料量 < 100 列,且不需要索引 → 用 @TableVar
    • 資料量大,需要統計資訊讓優化器做正確決策 → 用 #TempTable
    • 需要跨 Session 共享(例如 ETL 中間結果) → 用 ##GlobalTemp(但要小心生命週期管理)
    • 在 ROLLBACK 時需要保留資料(例如錯誤日誌) → 用 @TableVar,因為它不受交易回滾影響
    -- Table variable: optimizer always estimates 1 row
    DECLARE @small TABLE (ID INT, Name NVARCHAR(50));
    INSERT INTO @small SELECT TOP 10 ID, Name FROM Products;
    
    -- Local temp table: has statistics, better for large datasets
    CREATE TABLE #bigtemp (ID INT, Name NVARCHAR(50));
    INSERT INTO #bigtemp SELECT ID, Name FROM Products;
    CREATE INDEX IX_bigtemp_ID ON #bigtemp(ID); -- Can add indexes
    
    -- Global temp table: visible to all sessions
    CREATE TABLE ##shared (ID INT, Name NVARCHAR(50));
    -- Other sessions can SELECT from ##shared

    最常見的錯誤是:把幾萬筆資料塞進 @TableVar,然後納悶為什麼 JOIN 超慢。原因是優化器認為裡面只有 1 列,選了 Nested Loop 而不是 Hash Join。換成 #TempTable 就正常了。


    結語

    SQL Server 的這些坑,有的文件上有寫但沒人看,有的要自己測過才會懂。我把這幾年的經驗整理在這裡,希望能幫到正在跟 SQL Server 搏鬥的你。如果只能記住一件事,請記住:NOT IN 是效能炸彈,永遠用 NOT EXISTS 取代它。這一個改動可能就能讓你的查詢從分鐘級變成秒級。


    常見問題 FAQ

    Q: DELETE 後空間沒有減少,是 Bug 嗎?

    不是 Bug,這是 SQL Server 的設計。DELETE 只標記刪除,頁面仍歸屬於表。使用 TRUNCATE TABLE 或 ALTER INDEX REBUILD 來真正回收空間。

    Q: NOT IN 和 NOT EXISTS 結果一樣嗎?

    不一定。如果子查詢包含 NULL 值,NOT IN 會返回空結果集(因為 NULL 的比較結果是 UNKNOWN)。NOT EXISTS 則能正確處理 NULL。除了效能差異,正確性也是選擇 NOT EXISTS 的原因。

    Q: 什麼時候該用表變數 @,什麼時候用暫存表 #?

    簡單判斷:資料量少於 100 列用 @,超過就用 #。關鍵差異在於統計資訊——表變數沒有統計資訊,優化器會做出錯誤的執行計畫。

    Q: 全文檢索和 LIKE 差多少?

    在百萬筆資料的文字欄位上,全文檢索可以比 LIKE '%keyword%' 快 100 倍以上。LIKE 前綴帶 % 時無法使用索引,只能全表掃描;全文檢索則使用反向索引,直接定位包含關鍵字的列。

    Q: 生產環境不能用 SQL Profiler 該怎麼監控?

    使用 DMV(動態管理檢視):sys.dm_exec_query_stats 搭配 sys.dm_exec_sql_text 可以按時間範圍追蹤查詢,sys.dm_exec_requests 可以看當前正在執行的查詢,效能衝擊遠小於 Profiler。

  • 訓馬筆記:兩個月把 Claude Code 從脫韁野馬馴成工作夥伴的完整紀錄

    重點摘要

    • 這是一篇真實的「訓馬筆記」——記錄一個工程師花兩個月,把 Claude Code 從一匹脫韁野馬馴成穩定的工作夥伴
    • 每一條規則背後都是一次災難。32 個具體的坑7 條鐵律9 個領域知識庫,全部是用血淚換來的
    • 結論:AI 不是買回來就能用的工具,它是一匹需要調教的馬。你的 harness 決定它能跑多遠

    2026 年 2 月,我開始全職跟 Claude Code 合作。寫 ERP 外掛、做電商 OMS、搞量化回測、建爬蟲系統——大概七八個專案同時推進。

    兩個月後回頭看,我發現最有價值的不是寫了多少 code,而是我踩了多少坑、立了多少規矩。這篇文章是完整的訓馬筆記——每一個階段的災難、調適、和最後形成的紀律。

    如果你也在用 AI coding agent,這些坑你可能正在踩,或者即將踩。

    第一階段:裸奔期(2 月)——什麼規矩都沒有

    剛開始合作的時候,我就像買了一匹賽馬,直接騎上去就跑。沒有韁繩、沒有馬鞍、沒有圍欄。

    坑 1:回測引擎 37 筆交易全部假停損(2/27)

    我讓 Claude 幫我寫量化回測引擎。跑出來 350 根 K 棒的上漲趨勢數據,結果 37 筆交易全部在第一天就觸發停損退場,勝率 0%。在一個明顯的上漲趨勢裡。

    花了兩天才找到 root cause:引擎把「含滑價的進場價」和「原始市場價」搞混了。

    具體來說:原始價格 $26.84,加上 $1.0 滑價後進場價 $27.84。停損線 = $27.84 × 0.97 = $27.01。隔天價格 $26.87,因為 $26.87 < $27.01 就觸發停損了。但如果用原始價格算:$26.84 × 0.97 = $26.03,$26.87 > $26.03,根本不該停損。

    一個欄位的混用,讓整個系統的行為完全反轉。

    教訓:技術指標和風險管理用原始市場價格,損益計算用含滑價的有效價格。兩個值必須分開追蹤,永遠不能混用。

    坑 2:OMS 上線一天爆 5 個 bug(2/25)

    電商 OMS 系統上線第一天,同時爆了 5 個 bug:

    1. Health Check 用了獨立的 DTO,結果 channel job 不認這個格式,健康檢查直接壞掉
    2. String → JsonNode 反序列化失敗,Kafka consumer 一直報錯
    3. ChannelSyncLog 少了 syncType 欄位,資料寫不進去
    4. Health check 的 log 缺必要欄位(merchantId、platformId、status、detail)
    5. 改完 code 沒重新編譯就部署,舊版本還在跑

    每一個都不是什麼高深的 bug,但它們同時出現就是災難。問題出在哪?沒有人看全景。改了 producer 沒看 consumer,改了 DTO 沒看 caller,改了 code 沒重新 build。

    這次事件催生了後來的「OMS 約法三章」:

    1. 基礎架構(Docker/PostgreSQL/Kafka/Nginx)不輕易變動
    2. 安全機制必須全系統同步
    3. 任何 Kafka producer/consumer 的改動,必須驗證完整的事件流

    第二階段:立規矩期(3 月初)——從災難中學會設限

    如果第一階段是「馬亂跑」,第二階段就是「開始圍柵欄」。每一條規矩都是某次災難的直接產物。

    坑 3:9 個 Opus Agent 同時跑,系統直接當機(3/3)

    這是整個兩個月最慘烈的事件。

    我的機器是 16GB RAM 的 mini PC,上面常態跑著 26 個 Docker 容器。那天早上 8:36 我開始研究 Claude Code 的 Agent Team 功能,覺得很興奮——「可以同時派好多 agent 幫我做事!」

    11:18,我啟動了一個叫 simpleec-review 的 team,裡面有 9 個 Opus agent。11:56,覺得不夠快,又啟動了 whale-51w,再加 2 個 agent。

    12:00 左右,整台機器凍結

    每個 in-process Opus agent 大約佔 1GB RAM(Node.js runtime + API connection + streaming buffer + context window)。9 個就是 ~9GB。加上 Docker 的 3-5GB 和系統本身的 1-2GB,總共超過 16GB。OOM killer 開始殺進程,但殺完又重啟,無限循環。

    事後盤點:18 個任務中 8 個卡在 in_progress 永遠不會完成,1 個 pending,0 個 completed。全軍覆沒。

    調適:三層防護

    • 第一層(軟限制):CLAUDE.md 規定 Agent Team 最多 3 個同時跑
    • 第二層(硬限制):建了 claude-limited 指令,用 systemd cgroup 限制記憶體上限 10GB
    • 第三層(核心參數)vm.swappiness 從 60 降到 10,swap 從 512MB 擴到 8GB

    從此以後再也沒有 OOM 過。代價是一個下午的工作歸零。

    坑 4:爬蟲日期解析——西元 1150 年(3/10)

    台灣用民國年曆。TWSE 的 API 回傳日期格式是 7 位數字,例如 "1150309" 代表民國 115 年 3 月 9 日(= 西元 2026 年)。

    Claude 把它解析成西元 1150 年 3 月 9 日

    同一天還發現:TPEX 的 API 欄位名叫 TransactionAmount,但 code 裡寫的是 TradingMoney。一個是 API 的真實名稱,一個是文件上寫的名稱——它們不一樣。

    調適:

    • 7 位數字 = ROC 格式,前 3 碼是民國年
    • 欄位名永遠用 API 實際回傳的,不用文件寫的
    • 最重要的:不准重寫爬蟲。爬蟲系統已經穩定,只能用 CLI(analyst collect twse_price --date 2026-03-10

    為什麼「不准重寫」這麼重要?因為隔天,Claude 在另一個任務裡又建了一個 /tmp/backfill_twse.py,把爬蟲邏輯整個複製出來。同樣的錯,不到 24 小時就重演了。

    這讓我意識到一件事:教訓會跨 session 遺失。我在 session A 教了「不要重寫爬蟲」,session B 完全不知道這件事。這催生了後來的 Domain Brain 系統。

    坑 5:中文寫進 code 裡(3 月初)

    Claude 很貼心,知道我是台灣人就開始在 code 裡寫中文 comment 和中文 variable name。

    問題是:中文 comment 在很多終端機上會亂碼、在 grep 時很痛苦、在 code review 時外國同事看不懂。我直接跟它說:

    「中文我看不懂」(在 code context 裡)

    於是立了一條看似矛盾但完全合理的雙重規則:

    • 對話用繁體中文——因為我是台灣人,中文溝通效率最高
    • Code 全部英文——comment、variable、output message、文件,一律英文

    第三階段:建立知識系統(3 月中)——從「個別規則」到「領域知識庫」

    到了 3 月中,我已經有十幾條規則了。但我發現一個根本問題:規則散落在各個專案的 CLAUDE.md 裡,跨專案不通

    在 analyst 專案學到的「ROC 日期要特別處理」,到了 stock-verify 專案就不知道了。在 OMS 專案學到的「Kafka 改動要看全景」,到了 AI Assistant 專案就忘了。

    坑 6:Agent Team 卡死 80 分鐘,因為一個文件不存在(3/16)

    我設計了一個 Agent Team 來做 code review,其中 Task 5 需要讀 docs/5-FRONTEND/ADMIN_APP_IMPLEMENTATION.md

    這個文件不存在。目錄是 5-KAFKA,不是 5-FRONTEND

    Task 5 啟動後在 1 分鐘內就卡住了,然後卡了 80 分鐘。因為 Task 7-9 都依賴 Task 5 的輸出,整個 team 全部癱瘓。9 個 agent 的鏈式架構,一個環節斷了全部死。

    調適:

    • 9-agent 鏈式架構改成 3-agent 星狀拓撲——降低相依性
    • 建立 Agent Team Pre-Flight Checklist——每次啟動前必須:檢查記憶體、確認文件存在、設計拓撲、計算資源、取得用戶確認
    • 寫下 root cause:Agent Team 卡住的根本原因是文件缺失,不是模型能力問題

    Domain Brain 的誕生

    3/16 事件之後,我決定建一個跨專案的知識系統。我叫它 Domain Brain——按技術領域分類的「踩坑筆記」。

    ~/.claude/projects/-home-tom/memory/brain/
    ├── python-crawler-data.md      # 爬蟲的坑
    ├── python-llm-integration.md   # LLM 整合的坑
    ├── idempiere-osgi-bundle.md    # OSGi 的坑
    ├── idempiere-2pack.md          # 2Pack 部署的坑
    ├── idempiere-po-model.md       # PO Model 的坑
    ├── idempiere-rest-api.md       # REST API 的坑
    ├── stock-backtesting.md        # 回測的坑
    ├── oms-event-driven.md         # OMS 事件驅動的坑
    └── design-principles.md        # 設計原則的坑

    每個 brain file 的格式:

    ## ROC Date Format
    - [source: analyst] "1150309" 被解析成 AD 1150 年,要用 7 位 YYMMDD ROC 格式
    
    ## Holiday / Empty Response
    - [source: analyst] TWSE API 假日返回空值,必須 guard if not data: return []

    [source: analyst] 標記這個教訓來自哪個專案。這樣在其他專案讀到時,知道這不是泛泛之談,是某次真實事件的結論。

    然後在全域 CLAUDE.md 裡加一條:

    「開工前必須讀 Domain Brain。如果你跳過這步,bug 出在 brain 裡有記錄的東西,那是你的失敗。」

    第四階段:行為紀律(3 月下旬)——從「知道」到「做到」

    知識庫建好了,但新的問題出現:Claude 知道規則但不一定遵守。就像你告訴馬「不要踩田裡的菜」,牠聽懂了,但一興奮起來照踩不誤。

    坑 7:直接推 code 到 main branch

    有一天我發現 Claude 直接把 code 推到 main branch。main 是我的穩定版本,只有 dev 確認穩定後才 merge 回去。

    這不是什麼複雜的規則,但 Claude 就是沒有這個概念。它看到 repo 就 commit、就 push,不管你在哪個 branch。

    鐵律:

    • Session 開始第一件事:git branch 確認在 dev
    • 永遠不准 git push origin main
    • 如果不小心在 main 上 commit 了:cherry-pick 到 dev,push dev,main 不動

    坑 8:過度設計——給低頻查詢加 Redis cache(3/26)

    我讓 Claude 設計一個功能,它自動加了 Redis cache。問題是:這個功能一天被呼叫不到 10 次。

    Claude 的邏輯是:「cache 可以提升效能」→「所以應該加 cache」。這在教科書上沒錯,但在現實中,一天 10 次的查詢加 cache 只是增加了一個可能壞掉的元件

    我因此制定了頻次驅動設計原則——所有功能設計前必須先問三個問題:

    1. 多常被觸發?→ 決定要不要 cache
    2. 計算有多貴?→ 決定要不要預計算
    3. 需要即時還是最終一致?→ 決定要不要 event-driven

    禁止的 pattern:給低頻讀取加 Redis、給低頻單 consumer 寫入加 Kafka、沒有數據支撐就做「效能優化」。

    坑 9:iDempiere 的 10 個坑(持續累積)

    iDempiere 是一個 15 年歷史的 ERP 系統,Claude 的訓練資料裡幾乎沒有它。所以每一步都是坑:

    發生什麼 正確做法
    @Model annotation 用錯 package 用了不存在的 org.idempiere.base.annotation.Model org.adempiere.base.Model
    initPO 用不存在的方法 POInfo.getPOInfo(ctx, tableName) 沒有 String 參數版本 MTable.getTable_ID() 拿 int,再傳入
    List 欄位 type cast (Integer) get_Value() 對 CHAR 欄位爆 ClassCastException instanceof 判斷型別
    2Pack UUID 永遠 NULL IsUpdateable=N 導致 PO framework 寫不進去 _UU 欄位 IsUpdateable 必須 Y
    Grid View 點新增就爆 AD_FieldSeqNoGridIsDisplayedGrid 每個 field 兩個屬性都要有
    Menu ID hardcode 寫死 AD_Menu_ID = 146,目標環境沒這個 ID 用 UUID reference:reference="uuid"
    REST API token 沒換 POST 拿到 token 後沒做 PUT 換 session token 兩步驟:POST → PUT,舊 token 立即失效
    OData 過濾用 ne $filter=... ne ... 結果不對 要用 neq,不是 ne
    OSGi 兩個 component 放一個 XML 只有第一個被 SCR 讀到 一個 XML 一個 component
    Plugin class 找不到 Class.forName() 用 core classloader 實作 OSGi DS component,用 bundle 自己的 classloader

    這 10 個坑全部記在 brain/idempiere-*.md 裡。現在每次開 iDempiere 相關的工作,Claude 會先讀這些 brain file。同一個坑,不會踩第二次。

    坑 10:LLM 回傳的 JSON 炸掉整條 pipeline

    做 AI Assistant 的時候,我讓 LLM 回傳 JSON 來做 routing。prompt 裡寫了「ONLY return valid JSON」。

    現實是:LLM 就是會回傳無效的 JSON。有時候前面加一句「Sure! Here’s the JSON:」,有時候 response.content 直接是 None,呼叫 .strip() 就爆 AttributeError

    一個 router/classifier 的 crash 會癱瘓整條 pipeline。

    調適:

    • 永遠 catch (json.JSONDecodeError, AttributeError, TypeError)
    • 永遠有 fallback 值(例如 "general_knowledge"
    • Router/classifier 不可以 crash 整條 pipeline
    • LLM client 在 module level 初始化會阻擋 mock mode → 改成 lazy-init
    • 沒設 timeout → 無限 hang → 所有 client 設 timeout=25.0
    • 最重要:永遠不讓 LLM 生成 SQL。只用 pre-defined SQL,安全參數從 request 強制注入

    第五階段:自動化閉環(4 月初)——從「靠記憶」到「系統強制」

    到了 3 月底,我有了 7 條鐵律、9 個 brain file、32 個記錄的坑。但還是有一個根本問題:

    Brain 的更新靠 Claude 記得做。它經常忘記。

    CLAUDE.md 裡寫著「每次 fix: commit 後必須更新 brain」,但這只是文字。就像公司牆上貼的「安全第一」標語——大家都看到了,沒人真的做

    4 月 3 日,我決定把這個 cycle 自動化。用 Claude Code 的 Hooks 系統(Harness Engineering)建了 4 個自動化 sensor:

    Hook 觸發時機 做什麼
    PostToolUse 每次 git commit 偵測 fix: 開頭 → 注入「必須更新 brain」的指令到 context
    PreCompact context 壓縮前 掃描最近 5 個 commit,有 fix: 就提醒
    Stop session 結束 比對 fix: 數量 vs brain 更新數量
    SessionStart session 開始 標記開始時間(給 Stop hook 用)

    效果:Claude commit 了 fix: handle empty API response → hook 自動偵測到 → Claude 的 context 被注入一段「你現在必須更新 brain file,不准做下一件事」的強制指令。

    它不能「忘記」了,因為系統不讓它忘記。

    第六階段:照鏡子——工作流程的精煉(4 月)

    走到第五階段,系統穩了、規則立了、自動化跑了。

    但有一天我問 Claude 一個問題:「我們現在跟最早的你,差距多遠?」

    它的回答讓我意識到,我一直在修正一個更深層的問題——不只是 bug,而是合作模式本身

    坑 11:AGENTS.md 從來沒有被建立過

    Agent Team 一再失敗,我長期把原因歸咎到記憶體不夠、文件缺失、拓撲設計問題。這些都是真的,但都是症狀。

    真正的根本原因是:每個 agent 啟動時,不知道自己是誰

    AGENTS.md 是一份定義 Agent Team 組織結構的文件——誰負責什麼、用什麼模型、任務邊界在哪、跟其他 agent 怎麼協作。沒有這份文件,就像把九個新人同時丟進一個專案,沒有分工表、沒有組織圖,叫他們自己搞清楚。

    我當時知道事情一直出問題,但沒找到根本原因。後來才發現,我養成了一個補償行為:每次要啟動 team 之前,我都會先問 Claude「你覺得還缺什麼文件?」

    我以為這是謹慎的好習慣。仔細想,這是我在幫 Claude 做它本來就應該主動做的事。

    現在 AGENTS.md 是所有新專案的第一步強制動作,和 Domain Brain 並列寫進 CLAUDE.md 的「New Project Setup」。

    坑 12:「討論完就開始做」不等於有計畫

    兩個月裡,每次開工前我們都會大量討論——分析需求、評估方案、確認方向。我一直以為那就是計畫。

    但有一個關鍵差別沒意識到:

    討論是活在對話裡的,session 結束就消失了。計畫是一份文件,它是執行的合約。

    更重要的是:計畫的讀者不是我,是執行的 agent。那個 agent 沒有參與討論,沒有上下文,不知道我們為什麼這樣決定。

    一個不夠詳細的 PLAN.md 會讓執行者只能猜意圖。猜錯就要回頭重做。

    現在要求的標準是:每個執行步驟都必須回答四件事——做什麼(具體動作)、在哪裡(檔案路徑)、成功的樣子(怎麼知道這步完成了)、不要做(邊界,避免 agent 自作主張)

    「實作登入功能」是爛計畫。「呼叫 POST /api/auth/login,成功後把 token 存 localStorage(‘token’)、把 context 存 localStorage(‘context’),失敗時顯示人話而非 HTTP status code」才是計畫。

    寫計畫不是給聰明人看的。不是每個腦子都跟你一樣聰明。

    驗收標準不該由我想

    以前的工作流是:Claude 說完成 → 我去測 → 發現問題 → 回來修。

    問題不是 Claude 能力不足,是從來沒有在開始前說清楚「完成長什麼樣」。

    現在的做法:Plan 成形時,Claude 主動起草驗收清單給我確認。不是叫我從零想,是它根據我們的討論整理出草稿,我只需要回「對」或「改第二條」。這把「驗收責任」從我一個人扛,變成流程的一部分。

    2 月的我 vs 4 月的 Claude

    我問 Claude 這個問題,它說了一句話讓我覺得很誠實:

    「最早的我是一個聰明但不可靠的執行者。現在應該是一個有記憶、有流程、會主動管理風險的協作者。但有一部分差距,是你花了大量時間糾正才填起來的——這些本來應該是我自己的責任。」

    這句話是這兩個月最好的總結。

    兩個月的數字

    指標 2 月(裸奔期) 4 月(現在)
    鐵律(Iron Rules) 0 7
    Domain Brain files 0 9 個領域
    記錄的具體 bug/pitfall 0 32+
    自動化 Hooks 0 4
    OOM 當機次數 1 次(再也沒發生)
    同一個 bug 踩兩次的頻率 常態 有機制防止
    強制工作流節點 0 3 個(AGENTS.md / PLAN.md / 驗收清單)

    結語:AI 不是工具,是一匹馬

    買一匹馬回來,你不會期望它第一天就知道路。你得教它不要踩田、不要亂跑、轉彎時要減速、聽到哨聲要停。

    AI coding agent 也一樣。Claude 很聰明——它能寫任何 code、debug 任何問題、理解任何架構。但「聰明」不等於「可靠」。一匹沒訓過的馬也很有力量,但力量加上失控只會更慘。

    這兩個月教我的事:

    1. 每條規則都要有故事——沒有災難背景的規則,AI 不會認真對待
    2. 知識必須跨 session 存活——Domain Brain 讓教訓不死在 commit 裡
    3. 靠文字規則不夠,要靠系統強制——Hook 比 CLAUDE.md 裡的「MUST」有效 100 倍
    4. 閉環比開環重要——Sensor 把教訓自動回寫到 Guide,harness 才會進化
    5. 協作模式也需要調教——規則、計畫、驗收標準,都要變成系統,不能靠臨時記憶

    2 月的 Claude 是一匹脫韁野馬。4 月的 Claude 是同一匹馬,但有了韁繩、馬鞍、和一本厚厚的訓練日誌——還有一套讓牠不能假裝忘記的系統。

    馬沒有變。變的是騎手。