過濾與向量搜尋合併在單一查詢中

Back
Category : News

Multigrid

在原本運作良好的向量查詢中加入 WHERE tenant_id = 42,會發生兩種情況之一:查詢變慢,或是返回的資料列比你要求的還少。這兩種現象背後是相同的事實——近似索引與過濾器無法同時套用——而導致致命問題的選擇性是可以事先計算出來的。



為什麼兩者無法組合

B-tree 索引和 GIN 索引可以結合:Postgres 會從每個索引建立位元圖並進行 AND 運算。HNSW 索引無法參與其中,因為它不會產生一組符合的資料列——它產生的是有序串流,最接近的排在最前面,而這個排序正是它的全部價值。你無法將一個排序與位元圖進行交集運算後還保有排序。

因此規劃器必須做出選擇。它要嘛走訪圖形,之後再捨棄不符合過濾器的資料列(後置過濾),要嘛先找出通過過濾器的資料列,再為它們全部計算距離(前置過濾)。這兩種方式有不同的成本,更重要的是,會產生不同的結果。

Postgres 使用其成本模型在兩者之間選擇,而它對近似最近鄰居掃描的成本模型接近猜測。它沒有描述圖形在找到十個通過你過濾器的資料列之前會檢查多少資料列的統計資訊,因為這個數量取決於你的查詢落在向量空間的哪個位置。因此規劃器使用結構上缺乏資訊的估計值來選擇,有時會選到無法回答你查詢的執行計畫。這是在 Postgres 中唯一一種從應用程式覆寫規劃器是正常行為而非壞味道的情況。



後置過濾器及其運算

這是你對明顯查詢預設會得到的結果:

SELECT id, content
FROM chunks
WHERE tenant_id = 42
ORDER BY embedding <=> $1
LIMIT 10;

Enter fullscreen mode

Exit fullscreen mode

HNSW 掃描會讓 hnsw.ef_search 個候選保持存活,並依距離順序返回它們;過濾器會套用在這個串流上。令 s 為過濾器的選擇性——它通過資料表的分數。如果過濾器與向量空間中的位置無關,預期的倖存者數量為:

survivors ≈ ef_search × s

To get k results:   ef_search ≥ k / s

Assumptions: the filter is uncorrelated with the embedding
distribution; ef_search candidates are the only rows examined;
hnsw.ef_search has a hard maximum of 1000.

Enter fullscreen mode

Exit fullscreen mode

在預設 ef_search = 40 且過濾器通過資料表一半的情況下,你預期有二十個倖存者,而你要求十個。沒問題。在過濾器每千分之一列通過的情況下,你預期有 40 × 0.001 = 0.04 個倖存者,這在實務上代表查詢完全沒有返回任何結果,而索引運作完全符合設計。

在 pgvector 0.8.0 之前,在掃描內沒有補救方法:你只會得到倖存的資料列數量,無聲無息,且不會提示答案不足。0.8.0 加入了疊代掃描,它會使用較大的候選集合重新掃描,直到達到限制或耗盡預算為止:

SET LOCAL hnsw.iterative_scan = strict_order;   -- pgvector 0.8.0+
SET LOCAL hnsw.max_scan_tuples = 20000;        -- the budget it stops at

Enter fullscreen mode

Exit fullscreen mode

strict_order 保證結果會以真正的距離順序返回;relaxed_order 較快,可能會讓結果稍微偏離順序,如果之後無論如何都會執行重新排序器,這通常是可以接受的。兩者都無法消除底層成本——在高度選擇性過濾器上的疊代掃描需要做大量工作才能找到少數幾列。



以數字呈現的懸崖

將公式放入表格中,問題的形狀立刻顯現。對於 k = 10

Filter passes Description
50% of rows ef_search of 20 is enough. The default of 40 has margin. Nothing to do.
10% of rows ef_search ≥ 100. Query cost roughly 2.5× the unfiltered one. Still comfortable.
2% of rows ef_search ≥ 500. Roughly 12× the work of the default, and latency you will notice.
1% of rows ef_search ≥ 1000 — exactly the maximum pgvector allows. You are at the edge with no margin for an unlucky query.
0.1% of rows ef_search would need to be 10,000. Not reachable. The post-filter plan cannot answer this query correctly, at any setting, ever.

這就是懸崖:它不是逐漸退化,而是在頂-10 查詢百分之一選擇性處的硬牆,一般而言位於 s = k / 1000。在它之下,索引不是變慢——而是無能為力,而疊代掃描會把這種無能從錯誤答案轉變成緩慢答案。

該推導中的一個假設值得一提,因為它經常是錯誤的。它假設過濾器與向量位置無關。租戶過濾器通常大致無關。語言、文件類型或日期的過濾器則有強烈相關性——所有德文區塊在嵌入空間中都彼此靠近——這時倖存者要嘛聚集在候選集合中(優於公式),要嘛完全不在其中(糟得多)。公式是正確的規劃工具;你自己的測量才是正確的決策工具。



前置過濾器,以及它何時更快

另一個執行計畫完全忽略向量索引:使用 B-tree 找出符合的資料列,為每個計算距離,排序,取十個。召回率精確為 100%,因為沒有任何近似。

-- Force it, to see what it costs:
SET LOCAL enable_indexscan = off;   -- disables the HNSW ordered scan
SET LOCAL enable_bitmapscan = on;

EXPLAIN (ANALYZE, BUFFERS)
SELECT id FROM chunks WHERE tenant_id = 42
ORDER BY embedding <=> $1 LIMIT 10;

 Limit
   ->  Sort
         Sort Key: ((embedding <=> '[...]'::vector))
         Sort Method: top-N heapsort  Memory: 27kB
         ->  Bitmap Heap Scan on chunks
               Recheck Cond: (tenant_id = 42)
               ->  Bitmap Index Scan on chunks_tenant_idx
                     Index Cond: (tenant_id = 42)

Enter fullscreen mode

Exit fullscreen mode

這個執行計畫的成本是 s × N 次各有 d 個維度的距離計算,加上堆積提取。在 N = 10,000,000s = 0.001d = 1536 時:一萬列、一千五百萬次乘加,個位數毫秒的運算。一萬個分散資料列的堆積提取才是真正的成本,而且在暖機狀態下仍可能低於一百毫秒。

因此後置過濾器完全無法回答的執行計畫,是前置過濾器能精確且快速回答的計畫。懸崖和交叉點是同一個地方,這是個方便的巧合,也是本頁最有用的資訊。



交叉點位於何處

將兩種成本設為相等並求解。精確計畫的成本約為 sNd。後置過濾計畫需要 ef_search ≈ k/s,且每個候選會展開約 m 個鄰居,因此成本約為 (k/s)md

s N d  =  (k / s) m d

s²     =  k m / N

s*     =  sqrt(k m / N)

k = 10, m = 16, N = 10,000,000:
  s* = sqrt(160 / 10,000,000) = sqrt(1.6e-5) = 0.0040  →  0.40%

k = 10, m = 16, N = 1,000,000:
  s* = sqrt(160 / 1,000,000)  = 0.0126        →  1.26%

Assumptions: distance computation dominates both plans; heap access
costs are ignored, which flatters the exact plan; graph traversal
cost is linear in ef_search and m.

Enter fullscreen mode

Exit fullscreen mode

s* 之下,不要使用向量索引。在它之上,才使用。這個數字會隨著你的資料表大小移動,而平方根意味著它移動緩慢——從一百萬列成長到一千萬列只會讓它從約 1.3% 移動到約 0.4%。

這代表實務規則是:選擇超過資料表百分之幾的過濾器應該透過提高 ef_search 的向量索引;選擇低於百分之一的過濾器應該跳過它。規劃器不會可靠地為你做出這個選擇,因為它對 ANN 掃描的成本模型只是個佔位符,所以你應該明確地做出選擇。



四種解決方法

  1. 提高 ef_search 並測量。 對於高於百分之幾的選擇性,這就是完整解答。根據預期的選擇性為每個查詢設定它——你通常大致知道一個租戶有多少列——而不是全域設定。
  2. 依過濾欄位進行分割。 如果你的過濾器幾乎總是同一個欄位,將它設為分割鍵並為每個分割區建立一個 HNSW 索引。每個分割區的索引只包含符合的資料列,因此對它的掃描是未過濾的,上面的運算完全不適用。這是最乾淨的修正,也是具有真正運作成本的修正,因為它會讓你的索引數量倍增。

    CREATE TABLE chunks (
      id bigint GENERATED ALWAYS AS IDENTITY,
      tenant_id int NOT NULL,
      embedding vector(1536) NOT NULL,
      content text NOT NULL
    ) PARTITION BY LIST (tenant_id);
    
    CREATE TABLE chunks_t42 PARTITION OF chunks FOR VALUES IN (42);
    CREATE INDEX ON chunks_t42 USING hnsw (embedding vector_cosine_ops);
    
  3. 部分索引,用於少數熱門值。 索引本身的 WHERE 子句。適合兩三個大型租戶或單一 status = 'active' 述詞;超過幾十個就無法實用,因為每個都需要完整的 HNSW 建置。

    CREATE INDEX chunks_active_hnsw ON chunks
      USING hnsw (embedding vector_cosine_ops)
      WHERE deleted_at IS NULL;
    
  4. 在交叉點之下強制使用精確計畫。(tenant_id) 上建立複合 B-tree,並為該查詢停用索引掃描,或是撰寫查詢讓 ANN 索引無法套用——例如對計算出的運算式排序。在小的已過濾集合上進行精確搜尋不是後備方案;在 s* 之下,它才是正確的計畫。

不在清單中的一件事:將過濾欄位加入 HNSW 索引。pgvector 不支援多欄位 HNSW 索引,也沒有任何運算子類別能讓它有意義。如果某個教學建議這麼做,那個教學是在描述不同的引擎。

無論你採取哪種路線,你首先需要的數字是實際的選擇性,而它是一個查詢而非假設。過濾器很少是均勻的:少數租戶持有大部分語料,而長尾幾乎沒有,因此平均選擇性為百分之五可能隱藏著一千個坐在 0.01% 的客戶,他們看到空的結果而其他人都沒問題。

-- The distribution of selectivity across the filter values you
-- actually use. Look at the bottom decile, not the mean.
WITH n AS (SELECT count(*)::numeric AS total FROM chunks)
SELECT tenant_id,
       count(*) AS rows,
       round(100.0 * count(*) / (SELECT total FROM n), 4) AS pct_of_table,
       -- ef_search needed for a top-10 query, from k/s:
       ceil(10.0 * (SELECT total FROM n) / count(*)) AS ef_search_needed
FROM chunks
GROUP BY tenant_id
ORDER BY ef_search_needed DESC
LIMIT 20;

Enter fullscreen mode

Exit fullscreen mode

該輸出中任何 ef_search_needed 高於 1000 的項目,都是後置過濾計畫無法服務的過濾值,而此類資料列的計數會告訴你答案是每個查詢的 ef_search、分割策略,還是將小型租戶送到精確路徑、大型租戶透過索引的路由規則。最後這個選項值得考慮:兩個計畫都是正確的,因此依每個請求在它們之間選擇是一種合理的優化,而不是駭客行為。

最後,讓團隊驚訝的互動是:Postgres 列層級安全性原則會變成 WHERE 子句,這意味著啟用 RLS 會在一夜之間把應用程式中每個向量查詢都變成後置過濾的查詢,具有單一租戶的選擇性。這在多租戶檢索的列層級安全性中有涵蓋,並包含洩漏測試。



相關文章

https://dev.to/multigrid/filtering-and-vector-search-in-one-query-5085

https://www.worldprogramming.org/posts/filtering-and-vector-search-in-one-query-wki9iv

Multigrid

在 Android 上實現裝置端 AI 的困難之處不在於撰寫推論呼叫,而是相同的呼叫必須在數千種系統單晶片與驅動程式組合上執行。其中有些能優雅地加速你的模型,有些會在不告知的情況下退回 CPU,還有少數會產生錯誤的數值。有效的策略是在執行階段探測並記住結果。



問題不在於 API,而在於差異性

在 iOS 上,你只需針對單一廠商的少數幾代晶片。而在 Android 上,你必須面對多家晶片廠商、各自多個世代,以及由裝置製造商按照自己的時程所出貨的驅動程式堆疊。兩支搭載相同旗艦晶片的手機,可能因為其中一支使用較舊的 GPU 驅動程式而表現不同。

由此導出的規則是:永遠不要依據裝置型號字串來決定是否加速。依裝置名稱建立的白名單會在一個版本週期內過期,無法涵蓋你出貨後才推出的裝置,而且它們是在有實際測量可用的情況下仍做出的猜測。應該在裝置上實際嘗試一次,並快取結果。



你所選擇的各層抽象

大致上有三種可用的抽象層級,選擇正確的層級主要取決於你需要多少控制權:

  • 由平台提供的受管理裝置端模型。 Google 提供系統層級的生成式 AI 功能,應用程式可以呼叫,模型由平台管理並在應用程式外部更新。這能讓你的 bundle 大小為零,但完全沒有控制權:可用性取決於裝置與系統元件版本,因此你的功能在缺少這些條件時必須降級。請以程式方式檢查目前可用性,而不是假設最低 API 等級。
  • 你自行打包的執行環境,搭配 delegate。 LiteRT(先前稱為 TensorFlow Lite)或 ONNX Runtime,在載入時選擇加速 delegate 或執行提供者。這是主流選擇。你可以控制模型、版本與後備機制。
  • 直接針對單一晶片家族的廠商 SDK。 晶片廠商會發布自己的神經網路 SDK,通常能從自家 NPU 榨取出比通用 delegate 更多的效能,但代價是需要額外的整合工作,並且每個廠商都要準備獨立的模型成品。當推論本身就是產品時值得這麼做;若推論只是其中一項功能則很少值得。

哪些加速路徑目前有效、哪些已被棄用而改用廠商 SDK,已改變過多次且仍持續變化。請將你選擇的 delegate 視為建置的參數,而非 Android 的既定事實,並在每次主要平台版本發布時重新檢查。以下的探測程式碼設計成只需修改一行就能更換 delegate。



能力探測器

無論你選擇哪種執行環境,模式都相同:嘗試建構加速的直譯器、執行固定的輸入、與 CPU 參考值比對,並計時兩者。如果建構拋出例外、數值差異超過容忍範圍,或加速路徑實際上並未更快,則使用 CPU。

enum Accel { NNAPI_OR_VENDOR, GPU, CPU }

data class ProbeResult(val accel: Accel, val medianMs: Double, val valid: Boolean)

fun probe(context: Context, modelBytes: ByteBuffer): Accel {
    val golden = loadGoldenInputOutput(context)   // fixed input + expected output
    val results = mutableListOf<ProbeResult>()

    // Always establish the CPU reference first: it is the tie-breaker
    // for both correctness and speed.
    val cpu = timeRun(modelBytes, Accel.CPU, golden)
    results += cpu

    for (accel in listOf(Accel.NNAPI_OR_VENDOR, Accel.GPU)) {
        val r = try {
            timeRun(modelBytes, accel, golden)
        } catch (t: Throwable) {
            // Delegate construction failing is normal, not exceptional.
            ProbeResult(accel, Double.MAX_VALUE, valid = false)
        }
        results += r
    }

    val best = results
        .filter { it.valid }
        .minByOrNull { it.medianMs } ?: cpu

    // Require a real margin. A 5% win is not worth a second code path.
    return if (best.medianMs < cpu.medianMs * 0.8) best.accel else Accel.CPU
}

Enter fullscreen mode

Exit fullscreen mode

在功能首次被使用時,於背景執行緒執行此探測一次——不要在應用程式啟動時執行,因為那時它會與其他所有工作競爭啟動預算。將判斷結果以你的應用程式版本、模型版本與 OS build number 為鍵值持久化儲存,並在這三者任一改變時重新探測。系統更新可能會新增或移除可用的驅動程式,而你的快取答案不能比它活得更久。



與黃金參考值進行驗證

探測中正確性這一半是最常被整合忽略的部分,卻也是能抓住最嚴重 bug 類型的那一部分:一個能執行、速度快、但結果錯誤的 delegate。

  1. 選擇一個能有效測試模型的固定輸入——不要用全零,因為許多有問題的 kernel 會意外地正確處理全零。
  2. 使用你信任的參考實作,在離線狀態下計算一次預期輸出,並以 asset 形式出貨。
  3. 在裝置上,以適合精確度的容忍值進行元素逐一比對。使用 fp16 進行計算 的 delegate 不會與 fp32 參考值完全相符,因此在正規化後的輸出上使用約 1e-2 的絕對容忍值通常是正確的;完全匹配測試會在每一台正常的裝置上失敗。
  4. 對於分類器,還要斷言 top-1 標籤是否相符。數值容忍可能會掩蓋類別翻轉,而類別翻轉才是使用者實際看到的結果。

使用 SoC 識別碼與 OS build 來記錄探測結果。幾週後,這些記錄會成為你自己安裝基礎唯一準確的地圖,其價值遠超過任何公開的相容性矩陣。



針對等級而非裝置進行設計

與其設計一個必須在所有地方都能運作的體驗,不如定義兩到三個等級,讓探測來為裝置指派等級:

等級 描述
accelerated 探測找到有效且明顯更快的路徑。完整功能:更大的模型、更長的上下文、即時處理。
cpu-capable 沒有加速,但 CPU 推論能在互動預算內完成。較小的模型、批次處理或依需求執行而非即時。
unsupported CPU 推論太慢,或記憶體不允許載入模型。功能被隱藏或改由遠端提供。隱藏它是誠實的做法;一個需要九秒的功能比沒有功能更糟糕。

透過實際測量來決定,意味著隨著你的安裝基礎變化,等級指派仍能保持正確,也意味著在你推出後才發布的新手機能自動獲得良好的體驗。這也為你提供了一個乾淨的地方來放置緊急停止開關:如果某個 delegate 在部分使用者群上被發現有問題,你可以透過遠端旗標將該群組降級到 CPU,而不是推出緊急版本。此模式與 模型與提示的機能旗標 中描述的模式相同。



你實際會遇到的失敗情況

  • 無聲的 CPU 退回。 Delegate 成功建構、未回報錯誤,卻因為某些運算子不支援而將大部分運算圖在 CPU 上執行。只有計時能揭露這一點。這也是為什麼探測要與 CPU 比較,而非僅檢查建構是否成功。
  • Delegate 初始化成本。 建構一個加速的直譯器可能需要數百毫秒甚至更久,因為驅動程式需要編譯運算圖。必須加以分攤:讓直譯器在整個 session 中保持存活,而非每次推論都重新建構,且永遠不要在主執行緒上建構。
  • 量化不匹配。 許多 NPU 只能執行整數模型。浮點模型會被 delegate 拒絕,或是無聲地在其他地方執行。如果你很在意 NPU 路徑,請出貨量化後的成品,並針對黃金參考值進行驗證——量化是一種數值變更,而且 哪些層能容忍它 並不一致。
  • 背景執行限制。 Android 不會讓你在背景應用程式中無限使用 NPU。長時間的工作應該放在前景服務並顯示通知,或是放在可延遲的背景工作佇列,由系統在裝置閒置且充電時排程執行。
  • 入門級裝置的記憶體壓力。 透過釋放直譯器並在需要時延遲載入來處理 onTrimMemory。在背景 session 中持續保留模型是導致應用程式被殺掉、然後被怪罪冷啟動變慢的常見原因。

如果 unsupported 等級退回至託管模型,則這兩條路徑在呼叫端必須可以互換——Multigrid 在各提供者之間暴露單一 API,並提供每次請求的成本與延遲,這讓遠端分支保持為單一實作,而非針對每個可能路由到的廠商各有一個。



相關文章

https://dev.to/multigrid/on-device-ai-on-android-delegates-npus-and-fragmentation-2o2p

https://www.worldprogramming.org/posts/on-device-ai-on-android-delegates-npus-and-fragmentation-ng8pca

Multigrid

生產日誌是你能擁有的最佳訓練資料,也是最危險的。它們是真實分布中的真實輸入——但充滿了你目前模型的輸出,而這正是你絕對不能用來訓練的內容。



選擇就是全部的工作

日誌傾印不是資料集。百分之九十的生產流量是模型已經在正確執行的事,訓練這些內容不會教會你任何原本沒有的知識。值得納入的範例,是那些攜帶目前模型所缺乏資訊的案例。

  • 人類編輯過輸出的請求。 任何有審核步驟的產品中最高訊號的來源。編輯本身就是標籤:輸入加上修正後的輸出就是完美的訓練配對,而差異告訴你正在修正哪一種失敗模式。
  • 未通過驗證器的請求。 結構違規、JSON 解析失敗、破壞下游合約的輸出。將輸入與手寫或修復後的正確輸出配對。
  • 被重試的請求。 使用者重新產生是一個未標記的負面範例。它告訴你該去看哪裡,即使它沒有告訴你使用者真正想要什麼。
  • 被路由到更昂貴模型的請求。 如果存在後備或升級路徑,所有走過該路徑的內容都是微調最適合的候選:教導廉價模型學會昂貴模型擁有的行為。
  • 普通流量的分層抽樣。 不是因為它能教導,而是因為一個完全由失敗案例組成的資料集會讓模型認為一切都是失敗案例。百分之十到三十的普通流量能讓分布保持真實。



正確輸出從哪裡來

選擇給了你輸入。目標則是另一個問題,而且只有三種來源,依品質與成本由高到低排序。

目標來源 描述
人類編輯的輸出 如果你的產品已經有審核步驟則免費,若沒有則極為昂貴。品質最高,也是唯一能反映使用者真正想要什麼、而非模型認為他們想要什麼的來源。
更強模型(已過濾) 前沿模型的回答,僅在通過驗證器或評分標準時保留。便宜且可擴展。會繼承教師的盲點,並受限於教師的使用條款——見 /learn/fine-tuning-licenses。
你自己模型的輸出 免費,但未經過濾幾乎總是錯誤的。你無法透過模仿自己來學習原本沒有的行為。只有在有嚴格外部過濾器時才合理——例如通過的單元測試、已對帳的交易。



清理而不壓扁分布

直覺是把一切都正規化:去除空白、統一大小寫、統一日期格式、將樣板內容模板化。大部分都要抵抗。推論時的輸入會以日誌中同樣混亂的方式呈現,而只在乾淨輸入上訓練的模型,處理混亂輸入時會表現更差。

真正需要做的事:

  • 遮蔽而非刪除。 個人資料必須移除,但把名字替換成空字串會改變句子的結構。請替換成同類型且長度類別合理且一致的替代值。
  • 以兩個門檻進行去重。 基於雜湊的完全匹配去重能捕捉重放。但無法捕捉僅一個識別碼不同的模板化請求,而日誌資料充滿這類內容。以 Jaccard 門檻約 0.8 進行 MinHash 近似去重則可以,在來自日誌的語料上通常能移除遠多於完全去重的資料。
  • 限制每個來源的貢獻比例。 單一企業客戶若產生 60% 的流量,就會產生 60% 的資料集,你會針對他們特別微調模型。請對任何單一租戶、模板或端點設定固定的比例上限。
  • 丟棄被截斷的內容。 任何在 max_tokens 而非結束序列 token 停止的完成,都是在教導模型在中途句子停止。請根據結束原因而非長度來過濾。



兩個洩漏陷阱



分割後的近似重複洩漏

以隨機抽樣分割後,出現 400 次的模板化請求會同時出現在訓練集與測試集。你的評估會因此測量到記憶而誤報為泛化,數字會非常漂亮,直到生產環境與其不符為止。

請在分割之前先去重,並根據分組鍵而非單列來分割——依租戶、文件、工作階段、模板 ID 來分割。如果兩個範例有可能來自同一個底層事物,它們就應該屬於分割的同一側。



時間洩漏

隨機分割也會讓模型在下個月資料上訓練,卻在上個月資料上測試。對於任何輸入分布會漂移的情況——而產品流量總是會漂移——請依時間分割:在截止時間之前的所有資料用於訓練,之後的所有資料用於測試。得到的數字會較低,但才是能預測生產環境的那一個。



沒人注意到的回饋迴路

這是專屬於日誌衍生資料的陷阱,而且它很安靜。你的日誌包含目前模型的輸出。如果你拿來訓練並部署,下個月的日誌就會包含新模型的輸出,然後你又拿來訓練。每一個世代都是在擬合前一個世代,而不是擬合使用者真正想要的內容。

可見的症狀是收窄:輸出變得更一致、避險消失、模型發展出語言上的抽搐,而稀有但有效的回應形式則完全消失。這與 Shumailov 等人在 2024 年《Nature》論文中描述的合成資料動態相同,只是透過日誌管線的後門進入。

防禦方式是硬性規則而非警覺:只有當目標來自模型外部——人類編輯、通過的測試、已對帳的交易、不同的模型——時,範例才能進入資料集。將來源記錄為一個欄位,並讓訓練作業拒絕沒有來源的列。



格式化與損失遮罩

最後一步是機械性的,也是許多執行在無聲中失敗的地方。有兩個規則。

使用基礎模型自己的聊天模板。 不是看起來像的那個。特殊 token、角色標記與空白字元都很重要,如果訓練時使用的模板與服務堆疊套用的模板不同,適配器就會被要求在格式偏移中進行無謂的泛化。在啟動執行前,請渲染一個範例並印出可見的特殊 token。

在提示上遮罩損失。 許多訓練腳本預設會計算整個序列的損失,因此模型同時被訓練去預測你的指令以及它的答案。對於指令微調,你幾乎總是想要僅完成部分的損失:

tokens   <|system|> You are ... <|user|> Summarise ... <|assistant|> The report ... <|eot|>
labels   -100 -100 -100 ... -100 -100 -100 ...        The report ... <|eot|>
         ________________ masked ________________/  ____ supervised ____/

Enter fullscreen mode

Exit fullscreen mode

結束回合的 token 必須位於受監督的區域內。如果遮罩它,模型就永遠不會被訓練如何停止,這會產生最常見的「微調搞壞了一切」報告:一個正確回答後卻不斷持續下去的模型。



相關文章

https://dev.to/multigrid/building-a-fine-tuning-dataset-from-production-logs-e95

https://www.worldprogramming.org/posts/building-a-fine-tuning-dataset-from-production-logs-y3ppn0

4

發佈於 2026 年 8 月 5 日,東部時間晚上 8:00

住在明尼蘇達州鄉村的 Brian Burgess 在 IT 領域工作了 10 年,之後才找到他對消費者科技寫作的熱情。
Brian 在 2000 年取得電腦與網路科技學位。他擔任過各種 IT 職位,包括技術支援、為 IBM 安裝路由器、資料庫管理,以及其他網路管理相關角色。

他已經擔任個人科技記者 18 年,撰寫過數千篇關於 Microsoft 產品與服務、Android、iOS、串流媒體等主題的文章。他曾為知名科技網站撰稿,包括 XDA、InformationWeek、How-To Geek、Byte、groovyPost 等。

當他離開鍵盤時,你會聽到他在彈吉他、戶外工作,以及觀看《星艦迷航記》。

你是否曾好奇為什麼你的筆電運行起來比預期還熱?你可能認為只是硬體在努力執行你正在使用的應用程式。但其實 Windows 本身也是原因之一——即使在高效能系統上也是如此。

不幸的是,Microsoft 加入了你可能不想要的 diagtrack 服務,這可能會讓你的筆電運行更熱,並可能比你預期更早故障。幸運的是,它可以被停用,我將說明它是什麼以及如何關閉它,讓你的機器能多使用幾年。

你不需要的服務

診斷追蹤服務 (Diagtrack)

shows Taskmanager Performance running on Windows

Microsoft 以在 Windows 中加入你真的不想要或不需要的應用程式和功能而聞名。例如,我經常 批評 Copilot —— 但那是容易的目標。我可以稍後再談,因為它是正面功能的特性,可以輕易停用。另一方面,診斷追蹤服務 (Diagtrack) 是公司最早在 Windows 7 中加入的底層服務,用於系統使用的診斷追蹤。

這是一項你可能不知道的服務,它在背景執行並定期收集使用資料並傳回總部,這可能會造成 CPU 使用率高峰並產生更多熱量——特別是筆電。筆電受影響最大,因為它們的冷卻系統比桌上型系統有限。Diagtrack 本身可能不會單獨造成過度發熱。然而,當與 SysMain 等服務結合時就會如此。

該服務會喚醒、傳送崩潰傾印、應用程式使用訊號和其他診斷資料給 Microsoft。當它這麼做時,它會與其他程序和相關任務一起運行,通常會在 工作管理員 中顯示為 CPU 和磁碟高峰。它通常一天執行一次,通常在啟動後,這時風扇會運轉,筆電也會開始變熱。

它會收集你 Windows 10 或 11 系統上的作業系統和應用程式層級使用情況。它也會收集你的裝置和硬體驅動程式狀態、Wi-Fi 使用、周邊設備細節、應用程式和功能使用、系統健康與錯誤、效能指標,以及手寫和輸入規格。所以 Diagtrack 需要做很多工作來收集你系統上的所有遙測資料,當全部結合起來時,就可能造成筆電過熱。

服務已更名

如何找到它

A laptop displaying Window 11's installed services list.

如果你正在閱讀這篇文章並準備 停用這項服務以提升效能,你可能會好奇為什麼看不到它以及要在哪裡關閉它。Microsoft 實際上在 2015 年將這項服務重新命名為「連接的使用者體驗與遙測」。這基本上是公司對 Diagtrack 的美化更名,因為 Diagtrack 對懷疑自己活動被追蹤的使用者有負面聯想。

更令人厭煩的是,公司並沒有公開宣布這次更名,而是當時被一些科技偵探發現。這讓社群對這個模糊的遙測追蹤服務更名更加懷疑。更糟糕的是,Windows 10「Threshold 2」將這項服務重新開啟給那些已經在系統上停用它的使用者。這確實是公司的一個糟糕舉動。

停用 Diagtrack 服務

現在拯救你的筆電

既然 Microsoft 把遙測追蹤服務搞得這麼糟糕,而且因為它讓你的筆電運行更熱並縮短其壽命,是時候停用它了。有幾種方法可以關閉它,你可以選擇最適合你的方式。

最直接的方法是按 Windows 鍵 -> 輸入 services.msc -> 按 Enter。找到 連接的使用者體驗與遙測 -> 按右鍵 -> 停止服務 -> 設定為 已停用 -> 套用 -> 確定

也可以透過登錄檔關閉它,前往:HKEY_LOCAL_MACHINESOFTWAREPoliciesMicrosoftWindowsDataCollection 並將 AllowTelemetry DWORD 設定為 0。如果不存在,你可以建立該值並將其設定為 0。完成後重新啟動你的筆電。

如果你執行的是 Windows Pro 或更高版本,也可以透過群組原則停用它:電腦設定 -> 系統管理範本 -> Windows 元件 -> 資料收集與預覽組建,並將其設定為停用。

也可以立即停止它,方法是以 系統管理員身分執行 PowerShell 並執行以下指令:

Stop-Service -Name DiagTrack -Force

Set-Service -Name DiagTrack -StartupType Disabled

對大多數使用者來說,透過 Services.msc 關閉它是最簡單的,停用後,你將為你的筆電注入更多生命。不僅能幫助它避免過熱並停止風扇不斷旋轉,還應該能提升整體 Windows 使用體驗的效能。

拯救你的筆電免受讓它過熱的服務影響

連接的使用者體驗與遙測(以前稱為 Diagtrack)不會收集你在筆電上做的任何事或在系統上輸入的內容,例如你傳給別人的訊息或你開啟了哪些應用程式。然而,它正透過讓它運行得比應有更熱並縮短其壽命來傷害你的筆電。它對你的 Windows 裝置運作並非關鍵,而且既然它讓裝置運行更熱,就沒有理由不停用它。

https://www.xda-developers.com/a-windows-11-service-you-never-asked-for-is-making-your-laptop-run-warmer-and-die-sooner/

https://www.worldprogramming.org/posts/a-windows-11-service-you-never-asked-for-is-making-your-laptop-run-warmer-and-die-sooner-lizh7k

orca_forge

📝 原文(日文)發表於 forge.workstyle.tech



理解「聲音」——分解其組成要素

當你想把錄下的聲音轉換成另一個人的聲音時,第一個浮現的問題是:

「聲音」究竟是什麼?

即使說出相同的詞句,不同的說話者會產生不同的聲音。即使是同一位說話者,根據所說內容的不同也會產生不同的聲音。再加上語調變化,就會更加不同。換句話說,語音是一種由多個獨立資訊混合而成的訊號——至少包括:誰在說話(說話者身分)說了什麼(內容)、以及怎麼說的(韻律)

語音轉換(VC)的核心挑戰就在這裡。如果你單純地處理語音來替換說話者身分,內容和語調往往也會跟著改變。在這篇文章中,我們將說明我們採用作為「聲音設計」應用程式後端的 Seed-VC 如何解決這個問題——透過使用四個模組分離並處理資訊,同時逐步講解實際的模型載入與推論程式碼。




設計理念:將可分離的資訊委託給專用模組

Seed-VC 的核心思想是:「不要讓單一大型模型包辦一切。」而是按類型分解語音的組成要素,將每個要素分配給專用模組,最後再將它們重新組合起來。啟動時的模型載入流程如下:

model, semantic_fn, f0_fn, vocoder_fn, campplus_model, mel_fn, mel_fn_args = load_models(a)

進入全螢幕模式

退出全螢幕模式

雖然回傳了七個元件,但可以歸納為四個功能層:

  1. whisper (semantic_fn) — 提取內容(說了什麼)作為語意特徵
  2. campplus (campplus_model) — 將說話者身分(誰在說話)編碼成單一向量嵌入
  3. CFM/DiT (model.cfm) — 以擴散模型根據上述兩者生成梅爾頻譜圖
  4. BigVGAN (vocoder_fn) — 將梅爾頻譜圖轉換回可聽見的波形

每個角色——「提取語意」、「提取說話者」、「繪製頻譜圖」、「轉換成聲音」——都乾淨地分離開來。這種分工就是我們能夠只替換說話者身分,同時保持其他一切不變的原因。讓我們依序來看每個步驟。




步驟 1:whisper — 只提取「說了什麼」

在內容提取上,我們使用語音辨識模型 Whisper 的編碼器。我們不使用解碼器(轉錄)。而是直接取用其中間表示(一序列語意特徵)作為特徵向量。

s_alt = _semantic(torchaudio.functional.resample(src_t, _S["sr"], 16000))

進入全螢幕模式

退出全螢幕模式

一個關鍵點:輸入在處理前會重新取樣到16kHz。由於 Whisper 是為 16kHz 音訊所設計,即使主要轉換模型以 44.1kHz 運作,我們在語意提取前總是會降採樣到 16kHz。產生的序列幾乎不包含音高或音色資訊——它代表的是與所說內容對齊的表示。這是我們之後能夠自由替換說話者身分的先決條件。

注意:Whisper 有一個限制——一次只能處理最多 30 秒的音訊。較長的音訊需要進行分塊處理。這個限制本身曾是個重大陷阱(詳見我們的另一篇文章:「語音轉換中『說話變慢』臭蟲的元兇是 Whisper 的 30 秒限制」)。




步驟 2:campplus — 將「誰在說話」壓縮成 192 維向量

說話者身分提取由CAMPPlus這個說話者嵌入模型負責。它接收音訊,計算 fbank 特徵,並輸出一個單一的 192 維向量

feat = torchaudio.compliance.kaldi.fbank(w16, num_mel_bins=80, dither=0, sample_frequency=16000)
feat = feat - feat.mean(dim=0, keepdim=True)
return _S["campplus_model"](feat.unsqueeze(0)).squeeze().detach().cpu().numpy()

進入全螢幕模式

退出全螢幕模式

「說話者身分 = 單一向量」這件事對我們的「聲音設計」應用程式至關重要。因為向量可以相加、混合或內插,我們會預先從 18 位說話者的乾淨錄音計算嵌入作為「錨點」,然後根據滑桿輸入使用加權平均來混合它們。

w = design_weights(slider_values, bank, **kw)
emb = (w[:, None] * bank.embeddings).sum(axis=0)

進入全螢幕模式

退出全螢幕模式

我們為「年齡」、「音高」、「沙啞度」等可解釋軸定義滑桿,計算權重 w,然後對錨點嵌入進行加權平均。這種「透過混合聲音來設計」的操作之所以可行,正是因為說話者身分被模組化為獨立的向量。如果說話者身分與內容糾纏在一起,混合就會破壞所說的詞句。




步驟 3:CFM/DiT — 使用擴散來「繪製」梅爾頻譜圖

一旦我們取得語意特徵(內容)和說話者嵌入(身分),就會進入合成階段。這就是擴散模型(CFM:Conditional Flow Matching;以 DiT 實作)登場的地方。它從隨機噪聲開始,逐步生成以輸入為條件的梅爾頻譜圖。

vt = _S["model"].cfm.inference(cat, torch.LongTensor([cat.size(1)]).to(dev),
                               mel2, style, None, STEPS, inference_cfg_rate=CFG)
vt = vt[:, :, mel2.size(-1):]

進入全螢幕模式

退出全螢幕模式

觀察引數可以看出這個階段如何整合先前模組的輸出:

  • cat — 語意特徵(內容)。提示音訊(pc)與目標條件(cond)的串接
  • style — 來自步驟 2 的說話者嵌入(= 混合後的設計嵌入)
  • mel2 — 提示音訊的梅爾頻譜圖。生成後,我們會裁剪前 mel2.size(-1) 幀,只保留新內容
  • STEPS / inference_cfg_rate — 擴散步數與無分類器引導的強度

模型以分離的引數接收內容(cat說話者身分(style。它將它們解釋為指令:「用這個說話者的聲音繪製這個內容。」如果你固定其中一個並改變另一個,只有預期的屬性會改變。這種分離設計直接反映在推論介面上。




處理韻律:length_regulator 與 F0

「怎麼說的」——韻律(速度與音高變化)則有些不同。它無法完全與內容解耦,因此不使用專用模組,而是作為內容與說話者身分之間的連接點來處理。

首先是速度(持續時間)。來自 Whisper 的語意序列需要被拉伸以符合所需的梅爾幀數。這由 length_regulator 負責。

cond, *_ = _S["model"].length_regulator(s_alt, ylens=torch.LongTensor([mel.size(2)]).to(dev),
                                        n_quantizers=3, f0=None)

進入全螢幕模式

退出全螢幕模式

透過傳入目標幀長(ylens),語意序列會被調整到所需的持續時間。接著是音高(F0)。在 44.1kHz F0 條件模型中,length_regulator 可以接受 f0 引數,得以進行韻律控制,例如將載音的音高範圍向目標說話者的範圍偏移。

lf[F0_alt > 1] = lf[F0_alt > 1] - m_alt + m_ori   # 將載音 F0 偏移至目標中位數
shifted = torch.exp(lf)
cond, *_ = _S["model"].length_regulator(S_alt, ylens=tgt_len, n_quantizers=3, f0=shifted)

進入全螢幕模式

退出全螢幕模式

這是一個簡單的操作:在對數域中依中位數差進行偏移。韻律並未被視為獨立模組,而是作為注入的參數。這反映了一種務實的妥協——嘗試完全解耦往往會導致不自然的結果。




步驟 4:BigVGAN — 將梅爾轉換回可聽見的聲音

擴散模型生成的是梅爾頻譜圖——一種聲音的「藍圖」。最後一步,將它轉換成人類能聽見的波形,由聲碼器 BigVGAN 負責。

wav = _S["vocoder_fn"](vt.float()).squeeze().detach().cpu().float().numpy().reshape(-1)

進入全螢幕模式

退出全螢幕模式

雖然只是一行程式碼,但這一步對音質影響很大。即使使用相同的梅爾頻譜圖,不同的聲碼器也會產生不同的聲音。我們的 44.1kHz 模型能夠輸出「真正的寬頻」,正是因為 BigVGAN 是以高取樣率訓練的(詳見我們的另一篇文章:「22.05kHz 與 44.1kHz」)。




陷阱與經驗教訓

以下是實作與操作這個四層架構時的一些重要心得:

  • 各層的取樣率不同。語意提取固定在 16kHz,轉換模型以 22.05kHz 或 44.1kHz 運作,而說話者嵌入使用 16kHz fbank。如果誤解各層預期的取樣率,音質就會在不知不覺中下降。在每個階段明確重新取樣是最安全的做法。
  • 將說話者身分表示為單一向量開啟了各種應用。如果說話者身分被嵌入在擴散模型內部,我們就無法實作「聲音混合」。模組化分離不僅關乎品質——它還直接提升了產品的表現力
  • 韻律無法完全分離——應據此設計。速度與 F0 和內容緊密相關,因此將它們作為透過 length_regulator 注入的條件參數是務實的解決方案。強行完全解耦往往會產生不自然的偽影。
  • 每一層都可以替換。聲碼器可以在 BigVGAN、HiFiGAN 或 Vocos 之間切換。語意提取器可以在 Whisper 與 CNHuBERT 之間切換。模組化設計帶來擴展性——每個階段都可以獨立升級。



總結

  • Seed-VC 的語音轉換採用四層架構whisper(語意)+ campplus(說話者)+ CFM/DiT(擴散梅爾生成)+ BigVGAN(聲碼器)
  • 設計理念是將語音分解為「誰、說什麼、怎麼說」,並將每個部分分配給專用模組
  • 透過將說話者身分表示為單一 192 維向量,我們得以混合多位說話者——這是我們「聲音設計」應用程式的核心
  • 擴散模型以分離的引數接收內容(cat)與說話者身分(style)——這種分離設計直接反映在推論介面上
  • 韻律(速度與 F0)並未完全解耦;而是作為條件參數透過 length_regulator 注入
  • 每一層在不同的取樣率與角色下運作,且都可替換。這種模組化提升了品質、表現力與擴展性。

https://dev.to/orca_forge/in-depth-explanation-of-the-seed-vc-architecture-decomposing-voice-into-who-what-and-how-in-a-3hjh

https://www.worldprogramming.org/posts/in-depth-explanation-of-the-seed-vc-architecture-decomposing-voice-into-who-what-and-how-in-a-4-stage-structure-kmicmk

我曾經認為電子郵件是 AI 的可怕戰場。

太雜亂、太人性化、充滿 2017 年的轉寄郵件,以及公司裡沒人知道是哪個軟體產生的 HTML。

後來我花時間閱讀信箱自動化的討論串,特別是 r/openclaw 上關於郵件流程的一篇好文,終於搞懂了模式:

如果你不再讓模型扮演郵件伺服器的角色,電子郵件其實是 AI 非常好的應用場景。

這聽起來很明顯。但許多信箱自動化仍然在做以下事情:

  • 收到新郵件
  • 問 GPT-5 這是不是客服信
  • 問 Claude 這是不是業務信
  • 問另一個模型這是不是垃圾信
  • 再問一次該屬於哪個別名信箱
  • 再問一次現在要回覆還是稍後回覆

這不是智能。

這是昂貴的失憶症。

更好的模式很簡單:

  • 程式碼擁有狀態、重試、排程、同步與驗證
  • LLM 只負責真正需要判斷的決策

這樣的區分讓我的信箱工作流程變得更便宜、更容易除錯,也遠比以前穩固。



我一直回想的規則

OpenClaw 工作流程討論中的一則留言,比大多數文件都說得更好:

如果你的工作流程在達到 LLM 使用上限時就停止運作,那 LLM 很可能做得太多了。

這原本是在談程式碼代理人,但它完全適用於信箱自動化。

如果你的郵件管線需要仰賴模型來記住信箱狀態、去除重複事件、處理重試,或每次執行都重新檢查路由規則,那你就是建錯系統了。

模型擅長判斷。

它們不擅長當管理員。



電子郵件感覺很混亂,但傳輸協定其實早已結構化

人類體驗郵件是混亂的。

機器不是。

每封郵件到來時都帶有有用的結構:

  • From
  • To
  • Reply-To
  • Subject
  • 討論串識別碼
  • 訊息 ID
  • 標頭
  • 時間戳記
  • 原始 MIME
  • 附件邊界
  • 別名地址

這很重要,因為許多路由決策根本不該交給 LLM。

如果發票永遠都要寄到 [email protected],GPT-5 就不該每天早上重新發現這條規則。

如果客服郵件永遠落在特定別名上,程式碼就該確定性地路由它。

如果某個討論串已經處理過,你的 worker 應該從資料庫知道,而不是從提示詞知道。



模型該做的事

把 GPT-5、Claude Opus 4.6、Grok、Qwen 或 Llama 用在真正需要推理的部分:

  • 分類模糊訊息
  • 總結長討論串
  • 從醜陋的轉寄郵件鏈中萃取意圖
  • 為人類審核草擬回覆
  • 判斷附件看起來像是合約、發票還是支援文件



程式碼該做的事

所有重複性工作:

  • 同步信箱變更
  • 保存同步 token
  • 執行寄件者與別名規則
  • 排程後續追蹤
  • 重試失敗
  • 抑制重複處理
  • 驗證討論串是否已處理
  • 記錄決策以利稽核

這種架構不像「AI 信箱代理人」那麼炫目。

但它也是下個月還能繼續運作的架構。



Gmail 的配額數字基本上就是在告訴你該怎麼建

這部分徹底改變了我對信箱管線的看法。

Google 公開了 Gmail API 方法的配額成本。

  • history.list = 2 個配額單位
  • messages.list = 5 個配額單位
  • messages.get = 20 個配額單位
  • threads.get = 40 個配額單位
  • messages.send = 100 個配額單位

這些數字不是 trivia。

它們是設計提示。

Google 正在告訴你應該這樣做:

  1. 監聽變更
  2. 只抓取變更的內容
  3. 套用確定性篩選器
  4. 只在邊緣案例呼叫 LLM

而不是這樣:

  1. 每分鐘輪詢一次收件匣
  2. 抓取所有未讀郵件
  3. 把整條討論串丟給 Claude
  4. 永遠重複

如果你的工作流程每分鐘醒來一次,就叫前沿模型檢查所有未讀郵件,那你不是在打造自動化。

你是在打造一張持續的帳單。



一個理性的 Gmail 管線

對 Gmail 來說,模式很直觀:

  1. 使用 users.watch 訂閱收件匣變更
  2. 透過 Cloud Pub/Sub 接收事件
  3. 使用 history.list 取得變更的訊息 ID
  4. 只抓取你真正需要的訊息
  5. 先執行確定性規則
  6. 將模糊訊息升級給 GPT-5 或 Claude



開始監聽信箱

POST https://www.googleapis.com/gmail/v1/users/me/watch
Content-Type: application/json

{
  "topicName": "projects/myproject/topics/mytopic",
  "labelIds": ["INBOX"],
  "labelFilterBehavior": "INCLUDE"
}

進入全螢幕模式

退出全螢幕模式



處理變更的最簡 Node 範例

import { google } from "googleapis";

const gmail = google.gmail({ version: "v1", auth });

async function processMailboxChange(startHistoryId: string) {
  const history = await gmail.users.history.list({
    userId: "me",
    startHistoryId,
    historyTypes: ["messageAdded"]
  });

  const messageIds = new Set<string>();

  for (const item of history.data.history ?? []) {
    for (const added of item.messagesAdded ?? []) {
      if (added.message?.id) messageIds.add(added.message.id);
    }
  }

  for (const id of messageIds) {
    const msg = await gmail.users.messages.get({
      userId: "me",
      id,
      format: "metadata",
      metadataHeaders: ["From", "To", "Subject", "Reply-To"]
    });

    const headers = Object.fromEntries(
      (msg.data.payload?.headers ?? []).map(h => [h.name!, h.value!])
    );

    const subject = headers.Subject || "";
    const to = headers.To || "";
    const from = headers.From || "";

    if (to.includes("[email protected]")) {
      await routeToAccountsPayable(id);
      continue;
    }

    if (from.endsWith("@trustedvendor.com") && subject.includes("Invoice")) {
      await routeToAccountsPayable(id);
      continue;
    }

    await sendToLLMForClassification({ id, subject, from, headers });
  }
}

進入全螢幕模式

退出全螢幕模式

重要的不是程式碼風格。

重要的是操作順序:

  • 先做便宜的信箱同步
  • 再做確定性路由
  • 最後才呼叫模型



Outlook 和 Microsoft 365 用不同名稱做同樣的事情

Microsoft Graph 有相同的架構,只是用 delta query 而不是 Gmail history。

GET https://graph.microsoft.com/v1.0/me/mailFolders/{id}/messages/delta

進入全螢幕模式

退出全螢幕模式

你需要保留:

  • @odata.nextLink 來分頁
  • @odata.deltaLink 給下一次同步週期

那個 token 就是你的記憶。

你的 worker 應該擁有它。

而不是模型。



Node 中的範例結構

async function syncOutlookFolder(deltaUrl?: string) {
  const url = deltaUrl ?? "https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messages/delta";
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${token}` }
  });

  const data = await res.json();

  for (const msg of data.value ?? []) {
    if (msg.toRecipients?.some((r: any) => r.emailAddress?.address === "[email protected]")) {
      await routeToSupport(msg);
      continue;
    }

    await classifyIfNeeded(msg);
  }

  if (data["@odata.nextLink"]) {
    return syncOutlookFolder(data["@odata.nextLink"]);
  }

  if (data["@odata.deltaLink"]) {
    await saveDeltaLink(data["@odata.deltaLink"]);
  }
}

進入全螢幕模式

退出全螢幕模式

相同的模式。

不同的 API。



Cloudflare Email Workers 把邊界劃得非常清楚

這是我最喜歡的例子,因為區分非常乾淨。

import PostalMime from "postal-mime";

export default {
  async email(message, env, ctx): Promise<void> {
    const subject = message.headers.get("subject") || "";
    const from = message.from || "";
    const to = message.to || "";

    if (to === "[email protected]") {
      await message.forward("[email protected]");
      return;
    }

    if (subject.includes("Invoice") && from.endsWith("@vendor.com")) {
      await message.forward("[email protected]");
      return;
    }

    const parsed = await PostalMime.parse(message.raw);

    const llmResult = await classifyEmail({
      subject,
      from,
      to,
      text: parsed.text,
      html: parsed.html
    });

    if (llmResult.label === "support") {
      await message.forward("[email protected]");
    }
  }
};

進入全螢幕模式

退出全螢幕模式

第一個 if 陳述式,比很多「自主代理人」展示還要做更多有用的事。



工具:什麼適合做什麼

選項 最適合的用途
Gmail API 使用 users.watch 推送通知、history.list 增量同步,以及在呼叫 LLM 前的配額感知篩選
Microsoft Graph Mail API 資料夾層級的 delta 同步、使用 @odata.nextLink@odata.deltaLink 的持久信箱狀態,以及選擇性分類
Cloudflare Email Workers 直接在入站郵件上執行自訂邏輯,包含標頭、原始 MIME,以及 forward()reply()setReject() 等動作
OpenClaw 自託管編排,包含 cron、webhook、工具、記憶體,以及圍繞實際模型呼叫的多代理人路由
n8n / Make / Zapier 快速的無程式碼或低程式碼編排,適合希望在加入 LLM 步驟前先有規則、重試與整合的團隊

這些工具都不是行銷意義上的「AI 原生」。

這正是它們有用的原因。



醜陋的部分是真的

公平地說,電子郵件在實務上並不乾淨。

你還是得面對:

  • MIME 的奇怪行為
  • 只有 HTML 的內文
  • 巨大的轉寄郵件鏈
  • 行內圖片
  • 附件才是真正 payload 的情況
  • 收據、合約與法律討論串會弄壞天真的解析器

這就是為什麼純規則不夠。

但這也是為什麼純 LLM 管線是個錯誤。

正確的模式是硬邊界搭配軟性後備機制:

  • 確定性地解析標頭與 MIME
  • 用規則路由明顯案例
  • 把狀態存在模型之外
  • 把模糊內容升級給 GPT-5、Claude、Grok、Qwen 或 Llama
  • 高風險動作(如寄出最終回覆)保留人類審核

這種混合架構沒有「完全自主信箱代理人」那麼性感。

但這才是大人打造生產環境自動化的方式。



為什麼在按 token 付費時這件事更重要

這就是經濟面變得惱人的地方。

如果你的工作流程不斷把同樣的路由決策送給模型,你就是在付錢讓模型一次又一次重新發現你自己的商業邏輯。

這是偽裝成 AI 問題的糟糕架構問題。

對那些整天在 n8n、Make、Zapier、OpenClaw 或自訂 Node worker 中執行代理人的團隊來說,按 token 計費會讓情況快速惡化。

你最後看著使用量儀表板,而不是在出貨。

這正是我喜歡確定性編排優先、LLM 呼叫其次模式的原因,也說明了為什麼固定費率的 API 存取對自動化密集的工作負載如此吸引人。

使用 Standard Compute,你可以保留工作流程已經習慣的 OpenAI 相容 API 形態,卻不用把每次分類、重試和長討論串都當成計費事件。它是現有 SDK 與 HTTP 用戶端的即插即用替代方案,背後是 GPT-5.4、Claude Opus 4.6 與 Grok 4.20 的動態路由,價格則是可預測的月費。

當你的自動化 24/7 持續執行,而重點是要同時停止照顧信箱和 token 計量器時,這一點非常重要。



我現在的實務規則

如果我從頭開始打造信箱自動化,這是我信任的堆疊:

  • Gmail API 或 Microsoft Graph 負責信箱同步
  • Cloudflare Email Workers 或 Node worker 負責確定性處理
  • PostalMime 負責解析
  • 必要時使用 OpenClaw、n8n、Make 或 Zapier 進行編排
  • 只有在郵件提出真正問題時才使用 GPT-5 或 Claude

這就是整個轉變。

別再讓模型當迴圈。

讓模型回答問題。

在那之後,一切都變得更便宜。

一切也都變得更好。

https://dev.to/lars_winstand/i-stopped-letting-gpt-5-babysit-my-inbox-and-the-whole-workflow-got-cheaper-and-better-2bi5

https://www.worldprogramming.org/posts/i-stopped-letting-gpt-5-babysit-my-inbox-and-the-whole-workflow-got-cheaper-and-better-ptaljx

Ordewell

事先揭露:我開發了本文所介紹的工具。它是
免費且採用 Apache-2.0 授權,所以我的興趣是「讓人們覺得這個工具有用」勝過
「讓人們付費給我」。

讓我走上這條路的失敗經驗應該很多人都有過。我給代理一個
多步驟的目標:重構這個模組、更新 README、加入測試。一切
都進行順利,直到第 4 步——我才發現它在一開始就誤讀了第 1 步。
那時檔案已經被寫入了。沒有計畫可以修正,因為計畫只存在於
模型的腦海中。只剩下需要復原的東西。

這就是核心問題:計畫是工作階段的副作用,而不是你可以檢查和編輯的成品。這篇文章要談的是我最終採用的設計決定——讓計畫成為你先核准的東西,而不是你只能期待的東西。



計畫是一個有型別的成品

這個工具以唯讀方式讀取你的程式庫,然後回傳一個有序的任務清單。
每個任務都帶有四項後設資料:執行器(Claude Code、Codex 或
OpenCode)、模型、思考強度,以及模式。這不是裝飾——
這四項才是任務實際執行的方式。你可以改寫任何提示、增加
或刪除任務、重新連接依賴關係(task 4 depends on task 2),或是更改
單一任務所使用的模型。已完成的作業會保持完成狀態。你在編輯時不會有任何東西需要反覆呼叫
AI。



為什麼要使用獨立的規劃器

很自然的問題是「為什麼不直接使用計畫模式?」有兩個原因。

第一,計畫模式是在之後會執行的工作階段內進行規劃——計畫只是建議,模型有可能偏離它。而在這裡,計畫是由一個
獨立的過程產生,這個過程在物理上無法寫入你的程式庫。它的探索
是嚴格唯讀的;任何會寫入的指令會在核准提示出現前就被拒絕,所以不存在「允許一次」可以點擊通過。變更
屬於執行器,而非規劃器。

第二,逐任務分配。一個安全性重構和一個 README 更新不應該
使用相同的模型。規劃器會在整個計畫中公開地做出這個組合決策,在你花費任何執行 token 之前——而且你可以
覆蓋其中任何部分。



判斷來自證據,而非意見

我最在意的部分發生在後續。代理不擅長為自己評分
——更糟的是,它們會在無法編譯的建置上宣布成功。因此在這裡
只有當獨特的完成標記出現在
執行器的輸出中,並且旁邊保留了結束代碼作為獨立證據時,任務才會被標記為完成。
模型永遠不是決定因素。如果工作階段結束並聲稱工作已完成
卻沒有出現標記,它就會大聲失敗。

如果你看過代理說「完成了」然後打開一個仍然有問題的檔案,
那就是整個動機。



誠實的限制

規劃器仍然是 LLM,有時候會寫出不好的計畫。這裡的論點不是
它永遠正確——而是一個你能看到並編輯的壞計畫只花一分鐘,而一個你看不到的壞計畫會花掉一個下午。完成標記只證明
工作階段已結束並聲稱工作完成,而非程式碼是正確的。

另外:TUI 在每個平台都需要 tmux(在 Windows 上,這代表需要 WSL)。CLI
和 VS Code 擴充功能則可以原生執行,不需要它。



試試看

npm install -g ordewell && ordewell

Enter fullscreen mode

Exit fullscreen mode

把它指向一個你很熟悉的程式庫,然後閱讀它回傳的計畫。那第一個
計畫就是整個論點。

  • GitHub: github.com/ordewell/ordewell
  • Apache-2.0,免費,沒有付費方案。

很樂意回答任何問題,包括「為什麼不直接做 X」。

https://dev.to/ordewell/how-i-stopped-my-coding-agents-from-writing-files-before-i-could-see-the-plan-10g7

https://www.worldprogramming.org/posts/how-i-stopped-my-coding-agents-from-writing-files-before-i-could-see-the-plan-vzraq3


Cover image for Turn Claude Code into a Laravel expert with LaraClaude

Eduardo Lázaro

Claude Code 雖然能寫出不錯的 Laravel PHP 程式,但每次對話都以通才模式開始。它不知道你的專案有三百個應該縮減為三十個的遷移檔、不知道兩個檔案外的 @foreach 正在造成 N+1 問題,也不知道你的 Modal 遵循特定模式。你得不斷重複解釋相同的上下文。

LaraClaude 將這些上下文打包成斜線指令。它是一個 Claude Code 外掛,內建超過三十種 Laravel 專長,每一種都是一個 /lc: 指令。一次安裝後,你就擁有已經懂 Laravel 的稽核、鷹架與清理工具。以下是我最常使用的幾個。



如何安裝

LaraClaude 透過 Claude Code 的外掛系統安裝。一次加入市場,之後安裝,之後就能收到更新:

/plugin marketplace add edulazaro/laraclaude
/plugin install laraclaude@edulazaro

進入全螢幕模式

退出全螢幕模式

也可以直接從 GitHub 安裝:

/plugin install github:edulazaro/laraclaude

進入全螢幕模式

退出全螢幕模式

你需要有 Claude Code 與一個 Laravel 專案。大多數功能這樣就夠了;少數需要連接實際資料庫的功能還會需要 Docker。



在修改任何東西前先進行稽核

大多數功能預設只產生唯讀報告,只有當你加上 fix 時才會修改檔案,所以請先查看報告。/lc:find-n-plus-one 會掃描你的 Blade 視圖、Livewire 元件與控制器,找出在迴圈中存取關聯的程式碼,追溯到建立集合的查詢,並告訴你該加入哪個確切的 with()

/lc:find-n-plus-one

進入全螢幕模式

退出全螢幕模式

/lc:security-audit 是我在任何接手專案時都會執行的另一個指令。它會尋找 SQL 注入、XSS、大量賦值以及儲存在版本庫的密碼等問題,而且大多數可修正的功能在真正修改前都會先提供預覽旗標。

/lc:security-audit                 # 產生報告
/lc:security-audit fix --dry-run   # 預覽修正內容
/lc:security-audit fix             # 套用修正(需確認)

進入全螢幕模式

退出全螢幕模式



清理堆積已久的內容

每一個長期維護的 Laravel 應用程式都會累積遷移檔的垃圾:一個 create 後面跟著二十個 add_columnchange_column 檔案。/lc:consolidate-migrations 會依照資料表分組,判斷每個資料表是否適合合併,並將 ALTER 操作折回原本的 create 檔案,同時不碰資料遷移。

/lc:consolidate-migrations              # 分析
/lc:consolidate-migrations fix --dry-run

進入全螢幕模式

退出全螢幕模式

它拒絕在正式環境資料庫上執行,並且在刪除任何東西前都會詢問,這正是你希望重寫 schema 歷史的工具該有的行為。/lc:dead-code 是它在程式碼端的夥伴:找出未使用的類別、方法、路由與視圖。



依照你的慣例而非通用樣板來產生鷹架

這些產生器會先閱讀你的專案現有寫法,而不是吐出制式樣板。/lc:volt-component 會產生一個 Livewire Volt 單檔案元件,包含 PHP 區塊、驗證規則、@text() 字串,並只有一個根元素。

/lc:volt-component UserProfile

進入全螢幕模式

退出全螢幕模式

/lc:generate-crud 範圍更廣,能為一個模型產生從遷移到視圖的完整堆疊,而 /lc:generate-action 則會建立 Laractions Action 並註冊到模型上。甚至還有 /lc:generate-scraper,它會讀取真實網頁並根據實際標記產生 Larascraper 類別。



理解不熟悉的應用程式

當我面對一個還不了解的程式碼庫時,會使用兩個指令。/lc:model-diagram 會產生你的 Eloquent 模型及其關聯的 Mermaid ER 圖,這是最快了解整體關聯的方式。

/lc:model-diagram

進入全螢幕模式

退出全螢幕模式

/lc:analyze-model Property 則會完整傾倒一個模型的資訊:它的關聯、cast、scope、observer 以及它發現的問題。當執行階段已經出錯時,/lc:analyze-error 會接收堆疊追蹤並找出根本原因而非表面症狀。



總結

LaraClaude 總共有超過三十種功能,從 /lc:deploy-checklist/lc:api-docs,全部都遵循 Agent Skills 標準,因此它們只是你可以閱讀的 Markdown。它把你一直重複解釋的 Laravel 上下文,變成 Claude Code 可以隨需執行的指令。

👉 GitHub 網址:https://github.com/edulazaro/laraclaude

https://dev.to/edulazaro/turn-claude-code-into-a-laravel-expert-with-laraclaude-4l42

https://www.worldprogramming.org/posts/turn-claude-code-into-a-laravel-expert-with-laraclaude-xtkj6j

Shruti Kapoor

各位,我剛剛發布了兩支相互搭配的影片:認證是如何運作的,以及授權是如何運作的。

當有人說「Auth」時,這兩者經常被混為一談,但理解它們之間的差異,並弄清楚認證和授權在底層是如何運作的,是一項非常重要的技能,尤其是在現今這個 AI 時代。

以下是 YouTube 上的影片



🔐 認證是如何運作的:



🛂 授權是如何運作的:

別忘了訂閱,這樣你就不會錯過任何影片

熱門留言 (0)

訂閱

Code of Conduct

Report abuse

如需進一步操作,您可以考慮封鎖此人並且/或者 檢舉濫用

https://dev.to/shrutikapoor08/how-does-auth-work-10fh

https://www.worldprogramming.org/posts/how-does-auth-work-bxmwvs

簡介

發佈時間:

2026 年 8 月 5 日 下午 2:30 PDT

X icon on a smartphone screen
圖片來源:Matt Cardy (在新視窗開啟) / Getty Images
  • Sean O'Kane

Nikita Bier 已卸任 Elon Musk 旗下社群平台 X 的產品主管一職,任期一年多。

Bier 在週三表示,他認為是「傳承火炬的時候了,並將自己降級回原本的狀態:一名貼文者」,並補充他將繼續為公司提供諮詢。

這位連續創業家與 Lightspeed 合夥人在 2025 年 7 月接下產品主管角色。他在週三表示,在任內他監督了 30 項新產品的推出,同時「保護了公眾廣場的完整性」。

當然,Bier 擔任產品主管期間,X 也發生了多起醜聞。他接下產品主管職位僅幾天後,由 xAI(當時為 Musk 擁有的獨立公司)開發的聊天機器人 Grok 開始 自稱「MechaHitler」。Grok 也曾被 X 用戶用來產生 非自願裸露影像,包括兒童性虐待材料,這讓平台陷入法律困境。

「X 是、並將繼續是歷史上最重要的通訊技術。但經營這個應用程式是一份全職 24/7 的工作,現在是我該喘口氣的時候了。」Bier 在週三寫道。

電子報

訂閱產業最大科技新聞

相關報導

最新應用程式新聞

https://techcrunch.com/2026/08/05/nikita-bier-steps-down-as-xs-head-of-product/

https://www.worldprogramming.org/posts/nikita-bier-steps-down-as-xs-head-of-product-z75kht