# 策展 · X (Twitter) 🔥

> 📖 本站完整內容索引（documentation index）：[llms.txt](/llms.txt)

> 作者：PostHog (@posthog) · 平台：X (Twitter) · 日期：2026-06-05

> 原始來源：https://x.com/posthog/status/2062595534381326421

## 中文摘要

# Karpathy 的 Autoresearch 發現了我們查詢引擎中一個隱藏 3 年的 Bug（並提升了 11% 的效能）

幾週前，我們在里斯本舉辦了一場團隊外地辦公（offsite），當時我們將一個 AI Agent 指向我們的查詢引擎，餵給它來自正式環境的慢查詢，並讓它徹夜執行。

隔天早上，它發現了一個令人尷尬的事實：將近三年來，每一個帶有時間戳記（timestamp）篩選器的查詢，都沒有正確使用 ClickHouse 的主鍵。這個修正讓 ClickHouse 在基準測試查詢中需要掃描的顆粒（granules）數量減少了 62%，並使查詢本身的速度顯著提升。

這篇文章將介紹我們使用的設定、該 Bug 本身，以及我們目前正在開發的系統，以便讓這類分析能夠自動化執行。

# 什麼是 autoresearch？

這個概念並非我們首創。Andrej Karpathy 在 2026 年 3 月將其封裝並命名為「autoresearch」：給 AI Agent 一個小型但真實的系統、一個基準測試和預算，然後讓它循環執行；提出修改建議、執行基準測試、保留有用的變更、捨棄無效的變更。

Karpathy 針對一個深度為 12 的 nanochat 訓練執行了兩天的測試，發現了約 20 個能改善驗證損失（validation loss）的變更，其中一些甚至能遷移到更大的模型中。這種模式並不新鮮（DeepMind 的 FunSearch (2023) 和 Sakana 的 AI Scientist (2024) 都是更早期的例子），但 Karpathy 的儲存庫足夠小且具體，足以激勵你在一個下午內建立自己的版本。

對我們來說，最有趣的是二階效應：Agent 不會帶有長期在程式庫中工作所產生的偏見。對我們而言，`toTimeZone()` 的包裝一直都在那裡，屬於那種你看久了就會視而不見的程式碼。但 Agent 沒有先入為主的觀念。它會執行每一項診斷，閱讀周圍的 ClickHouse 和 PostHog 原始碼以獲取上下文，並以同樣的懷疑態度審視一個存在三年的表達式，就像審視你昨天寫的那一行程式碼一樣。

# 在黑客松中為 ClickHouse 設定 autoresearch

我們每年都會在公司外地辦公時舉辦黑客松。現在 PostHog 的許多功能（如 session replay、資料倉儲、日誌等）都是這樣開始的。在里斯本針對 Analytics Platform 和 Query Performance 團隊舉辦的小型聯合外地辦公中，我們的黑客松專案就是要做 Karpathy 那一套，但目標是 ClickHouse 的查詢效能。

我們使用的技術堆疊：

- **pi**：由 Mario Zechner 建立的一個小型終端程式開發 Agent。它能與你指定的任何 LLM 對話，提供一個小型 SDK，且體積小到你可以讀完整個程式庫。

- **pi-autoresearch**：由 davebcn87 開發的社群擴充功能，將 Karpathy 的循環機制整合進 pi。你給它一個目標、基準線、基準測試指令和目標指標。它會進行迭代、提交每個候選變更、執行基準測試，並保留日誌，確保執行過程在上下文重置後依然存在。

- **我們在 pi-autoresearch 之上編寫的 Campaign 編排合約**。當單一 ClickHouse 查詢有數百種合理的改寫方式時，基本的「嘗試、測量、保留或捨棄」循環顯得太過鬆散。如果沒有結構，Agent 可能會對查詢的某個角落反覆修改直到放棄；有了結構，你就能得到更接近人類進行調查的方式。我們將每次調查分為四個部分：

1. 一個包含一個慢查詢和一個 git 分支的 Campaign。

1. 這被拆分為多個「通道」（lanes），即針對懷疑瓶頸的優化方向：謂詞排序、JSON 解析、時區處理、主鍵使用等。當通道不再產生訊號時可以暫停，當發現包含兩個不同想法時可以拆分，或者當不同通道的優化結果可以合併時進行合併。

1. 每個通道內的一個具體、可測試的假設。

1. 每個假設內的一個實驗，包含一次執行、基準測試和判定。Agent 必須在每次實驗後進行明確的反思（reflection pass），而不是讓循環只會盲目地爬坡（hill-climb）。

- **一個拋棄式的 ClickHouse 測試叢集**：這保持了迭代的高速度，並使基準測試數據可預測。它擁有與正式環境相同的資料結構，但經過匿名化處理，並運行在專門分配給 Agent 的較便宜硬體上。在開發者的筆記型電腦上運行會太慢，無法進行有效的內循環；而在正式環境運行則意味著要與其他任務爭搶資源，並冒著干擾客戶查詢的風險。

範圍縮小（Range-narrowing）也有幫助。當目標查詢逾時時，Agent 會將範圍減半（30 天、14、7、3、1），直到它能在 1 到 10 秒內完成，然後針對該縮小版本進行優化。這個視窗短到足以進行快速迭代，但又長到足以讓索引和分區效應發揮作用。目前的最佳候選者會定期在完整範圍內重新測試；一旦在那裡完成，該 Campaign 就會「畢業」並從原始查詢繼續進行。

在黑客松期間，我們手動餵給它過去我們抱怨過的慢查詢，以及我們在 `system.query_log` 中手動發現的查詢（我們現在正在將此過程自動化）。

# 發現我們默默損壞的主鍵

ClickHouse 之所以快，是因為它能跳過不必要的運算。我們的事件資料表是 `PARTITION BY toYYYYMM(timestamp)`，主鍵則是 `(team_id, toDate(timestamp), event, …)`。一個格式正確且帶有時間戳記邊界的查詢，應該能讓 ClickHouse 直接捨棄整個月的資料，然後直接跳轉到它需要查看的月份中的正確週次。

但實際情況並非如此。

當我們在 2023 年 4 月為 HogQL 加入各團隊時區支援時，我們做了自認為合理的事：將每個對 `timestamp` 的引用都包裝成 `toTimeZone(timestamp, team_tz)`，以確保顯示的日期正確。但我們沒意識到的是，ClickHouse 查詢規劃器無法看穿 `toTimeZone()`。這意味著它無法推導出以下邊界：

- 無法從 `toTimeZone(timestamp, tz) >= '2024-03-01'` 推導出 `toYYYYMM(timestamp)`，因此分區修剪（partition pruning）失效了。

- 無法推導出 `toDate(timestamp)`，所以主鍵的使用僅限於 `team_id` 和 `event`，之後就停止了。

之所以這件事沒能觸發我們的警報，是因為 ClickHouse 在 `timestamp` 上還有一個 MinMax 跳過索引。MinMax 索引會儲存每個「顆粒」（預設為 8,192 行）中欄位的最小值和最大值。當你將 `toTimeZone(timestamp, tz)` 與常數進行比較時，ClickHouse 仍然可以針對每個顆粒的 min/max 進行評估，並跳過範圍不重疊的顆粒。這比分區修剪弱得多，但它確實有效，所以查詢並不是災難性地慢，只是比應有的速度慢了一些。

這就是那種會永遠隱藏起來的 Bug。它很慢，但還沒慢到需要「呼叫值班人員」的程度。每個查詢都受到影響，所以沒人能與「良好」版本進行 A/B 比較。而且這個關鍵證據隱藏在 `EXPLAIN PLAN indexes=1, json=1` 的輸出中，除非已經懷疑有問題，否則沒人會去執行它。

在其中一個通道中，autoresearch 循環執行了 `EXPLAIN`。它注意到 `Partition: Condition='true'`（即沒有修剪），所以嘗試了兩件事：

1. 加入帶有原始時間戳記邊界的 `indexHint()`。

1. 改寫比較方式，讓欄位側保持原始狀態，並將時區標註在常數側。

第二種方法大獲全勝，這也是我們最終發佈的方案：

```sql
-- 修改前：規劃器無法看穿 toTimeZone
toTimeZone(timestamp, 'US/Pacific') >= '2024-03-01'

-- 修改後：左側為原始欄位，右側為帶有時區標註的常數
timestamp >= toDateTime64('2024-03-01', 6, 'US/Pacific')
```

語意是完全相同的，因為 `toTimeZone()` 只會改變顯示的元數據：底層的 epoch 並未改變。規劃器現在能看到原始的時間戳記，並能執行它的工作。

針對正式環境中某個團隊的 7 天漏斗分析（`load_balancing='in_order'`，確保每個變體都命中相同的分片，各執行五次，取中間三次的截尾平均值）：

![](https://pub-75d4fe1e4e80421b9ecb1245a7ae0d1a.r2.dev/curated/1780663847672-diaHJPSIea4AAhQlajpg.jpg)

速度提升在短日期範圍的查詢中最為顯著，因為這正是分區修剪最關鍵的地方。在 7 天的範圍內，如果規劃器配合，你可以捨棄大部分的分區。較寬的範圍無論如何都必須查看更多分區，因此相對優勢會縮小：90 天的查詢雖然還是變快了，但沒有提升 37% 那麼多。顆粒數量的減少在每個範圍內都是真實存在的；只是當絕對需要掃描的顆粒數較多時，它轉化為實際執行時間的改善幅度就較小。

這個 Bug 自從時區變更上線後就一直存在。大約三年了。

# 下一步：在沒有黑客松的情況下實現自動化

我們在外地辦公期間是手動將慢查詢餵給 Agent 的。這無法擴展。我們現在正在建立的管線更接近你真正想要的樣子：

1. 從 `system.query_log` 獲取慢查詢。執行此操作的編排器位於 `products/query_performance_ai/orchestrator/slow_queries.py`。

1. 為每個候選查詢啟動一個沙盒，這與我們用來執行 PostHog Code（我們的程式開發 Agent 和產品編輯器，目前處於 Beta 階段）的沙盒相同。

1. 在每個沙盒中執行 `pi-autoresearch`，每個都有自己的基準測試目標和預算。

1. 讓 LLM 對建議進行去重，並為每個存活的想法啟動一個 PostHog Code 工作階段。不同的沙盒經常會得出相同的想法，因此 LLM 會在分發前將它們合併。PostHog Code 會針對真實的程式庫編寫實際變更，並附帶測試和基準測試。

1. 將產生的 PR 發佈到我們的團隊 Slack 頻道，以便人類進行審查並合併。

如果這能成功，「我們程式庫中的某些查詢沒有正確使用主鍵」就會變成系統在我們睡覺時自動發現的問題，而不是需要花費三年時間和一場團隊外地辦公才能揭露的問題。一旦有了結果，我們會再寫文章分享二階效應。

同時，這個配方並不限於慢查詢。如果你的系統中有一個你一直默默容忍的指標（速度、記憶體、成本、準確度、錯誤率，任何你可以量化的東西），建立一個你可以低成本執行且不怕被「折磨」的 harness，將 Agent 指向它，看看會得到什麼結果。

作者：Robbie Coomber，PostHog 全端工程師

## 標籤

Agent, 自動化, Benchmark, ClickHouse
