隨着 AI 編碼助手在軟件開發中日益普及,我們發現傳統的 CLI 工具(主要為人類交互而設計)在與大語言模型 (LLM) 協作時往往顯得力不從心。本文記錄了我們如何重新設計 Calcit 的命令行界面,使其真正對 LLM 友好,在保持(甚至提升)開發體驗的同時,顯著降低了 Token 消耗。
背景:Calcit 快照格式
Calcit 是一門類似 Lisp 的函數式編程語言,使用 Cirru 語法(基於縮進的 S-表達式)。與分散在各個目錄中的傳統源文件不同,Calcit 將整個程序存儲在名為 compact.cirru 的單一結構化快照文件中。
快照結構
一個典型的 Calcit 快照包含:
{} (:package |app)
:configs $ {}
:init-fn |app.main/main!
:reload-fn |app.main/reload!
:version |0.10.4
:files $ {}
|app.main $ %{} :FileEntry
:defs $ {}
|main! $ %{} :CodeEntry
:doc |"Main entry point"
:code $ quote
defn main! ()
println |"Hello, Calcit!"
:examples $ []
|add $ %{} :CodeEntry
:doc |"Addition function for two numbers"
:code $ quote
defn add (a b)
&+ a b
:examples $ []
quote $ add 1 2
quote $ add 10 20
:ns $ %{} :CodeEntry
:doc |"Main application namespace"
:code $ quote
ns app.main $ :require
app.lib :as lib
:examples $ []
關鍵結構元素:
:configs- 項目元數據(入口函數、版本):files- 命名空間 -> 文件條目的映射%{} :FileEntry- 包含:defs(定義)和:ns(命名空間聲明)%{} :CodeEntry- 每個定義都有:doc、:code和:examples:code $ quote- 實際代碼以引用數據形式存儲(同質性)
對於一個簡單的 3 行函數,原始 JSON 表示會消耗約 300 個 Token。當探索擁有數十個函數的代碼庫時,Token 成本會迅速飆升。
洞察: 除非 LLM 需要通過程序操作代碼,否則它們並不需要 JSON。Cirru 語法完全可讀,且更加緊湊。
彌補語法鴻溝:cr cirru
我們發現的一個直接障礙是,雖然 Cirru 很緊湊,但 LLM 往往帶有“Lisp 包袱”——期望標準的括號,並難以理解 Cirru 特有的縮進和葉節點前綴(如字符串的 |)。
為了解決這個問題,我們提供了 cr cirru,這是一套轉換工具,允許智能體在提交修改前驗證其對語法的理解。
# 驗證 Cirru 字符串如何轉換為 JSON
$ cr cirru parse '|hello world'
"hello world"
# 驗證表達式如何映射到 AST 結構
$ cr cirru parse 'defn add (a b) (&+ a b)'
[["defn","add",["a","b"],["&+","a","b"]]]
我們還包含了一個 cr cirru show-guide 命令,這是一個 50 行的 Cirru 語法規則簡要總結。智能體被指示每會話閲讀一次,確保它們理解 $(嵌套)和 ,(註釋)等標記,而不需要成千上萬個 Token 的訓練數據。
繪製藍圖:高層級探索
在深入研究具體的代碼節點之前,LLM 智能體需要了解“地勢”。在傳統項目中,這通常涉及運行 ls -R 和 grep。在 Calcit 中,我們提供了一些結構化的切入點,它們直接使用 AST 的語言。
1. 列出命名空間:cr query ns
智能體直接查詢快照的模塊,而不是遍歷文件系統並猜測哪些文件是相關的。
$ cr query ns
Project namespaces: (6 namespaces)
app.$meta
app.comp.container
app.config
app.main
app.schema
app.updater
Tip: Use `--deps` to include dependency and core namespaces.
2. 結構分析:cr analyze call-graph
為了理解這些碎片如何組合在一起,智能體可以從配置中指定的入口點開始分析調用圖。
$ cr analyze call-graph
# Call Tree Analysis
**Entry Point:** `app.main/main!`
## Call Tree Structure
└── app.main/main!
├── app.main/render-app!
│ ├── respo.core/render!
│ ├── app.comp.container/comp-container
├── reel.util/listen-devtools!
└── app.main/persist-storage!
這個簡化後的樹告訴智能體哪些函數是關鍵的,以及它們如何相互依賴,在不閲讀任何實現邏輯的情況下提供了一張腦圖。
3. 定位目標:cr query search
一旦智能體知道要調查哪個命名空間或函數,它需要找到具體的邏輯所在。它不再需要閲讀數百行的函數並數括號,而是使用結構化搜索來找到精確的座標。
$ cr query search "render-app!" -f 'app.main/main!' -l
Results: 2 match(es) found in 1 definition(s):
● app.main/main! (2 matches)
[5,0] in render-app!
[6,3,2,0] in render-app!
這返回了準確的 AST 座標 ([5,0])。智能體不再需要具備完美的縮進空間推理能力;它只需跟隨搜索引擎提供的路徑進行精確編輯。
4. 生命週期管理:cr edit
當需要構建或重構時,智能體不會“創建文件”或“寫入字符串”。它使用帶有操作反饋的結構化 edit 命令。
$ cr edit def app.services/new-fn -e 'defn new-fn () (println |hello)'
✓ Created definition 'new-fn' in namespace 'app.services'
Next steps:
• View definition: cr query def 'app.services/new-fn'
• Find usages: cr query usages 'app.services/new-fn'
• Add to imports: cr edit add-import <target-ns> 'app.services' --refer 'new-fn'
通過提供高層級的生命週期命令並建議後續邏輯步驟,我們消除了 LLM 迷失方向或通過直接文本操作破壞快照結構化元數據的風險。
方案一:漸進式展示
對於一個簡單的 3 行函數,原始 JSON 表示會消耗約 300 個 Token。當探索擁有數十個函數的代碼庫時,Token 成本會迅速飆升。
洞察: 除非 LLM 需要通過程序操作代碼,否則它們並不需要 JSON。Cirru 語法完全可讀,且更加緊湊。
我們實現了一個三層探索模型:
第一層:cr query peek - 快速概覽
$ cr query peek app.main/add
Definition: app.main/add
Doc: Addition function for two numbers
Expr: defn add (a b) (&+ a b)
Examples: 2
Tips:
- cr query def app.main/add
- cr query examples app.main/add
- cr query usages app.main/add
- cr edit doc app.main/add '<doc>'
結果: 一個簡明的功能簽名和文檔摘要。非常適合掃描多個函數。
第二層:cr query def - 完整源碼
$ cr query def app.main/add
Definition: app.main/add
Doc: Addition function for two numbers
Examples: 2
Cirru:
defn add (a b)
&+ a b
Tips: try `cr query search <leaf> -f 'app.main/add' -l` to quick find coordination...
use `cr tree show app.main/add -p "0"` to explore tree for editing.
add `-j` flag to also output JSON format.
結果: 以可讀的 Cirru 格式顯示完整實現。僅在明確需要時通過 -j 標記提供 JSON,從而節省大量 Token。
第三層:cr query def -j - 程序化訪問
$ cr query def app.main/add -j
Definition: app.main/add
Doc: Addition function for two numbers
Examples: 2
Cirru:
defn add (a b)
&+ a b
JSON:
["defn","add",["a","b"],["&+","a","b"]]
Tips: ...
結果: 在需要機器處理時提供完整輸出。
細粒度導航:cr tree show
$ cr tree show app.main/add -p "0"
Location: app.main/add path: [0]
Type: list (4 items)
Cirru preview:
defn add (a b)
&+ a b
Children:
[0] "defn" -> -p "0,0"
[1] "add" -> -p "0,1"
[2] (2 items) -> -p "0,2"
[3] (3 items) -> -p "0,3"
Next steps: To modify this node:
• Replace: cr tree replace app.main/add -p "0" -j '<json>'
• Delete: cr tree delete app.main/add -p "0"
Tips: Use -j '"value"' for precise leaf nodes, -e 'cirru code' for expressions; add -j flag to also output JSON format
結果: 節點級別的探索,僅在明確要求時顯示 JSON。
查看示例:cr query examples
當函數有記錄的示例時,可以單獨查看:
$ cr query examples app.main/add
Examples for: app.main/add
2 example(s)
[0]:
add 1 2
JSON: ["add","1","2"]
[1]:
add 10 20
JSON: ["add","10","20"]
Tip: Use `cr edit examples app.main/add` to modify examples.
結果: 以 Cirru(用於閲讀)和 JSON(用於程序化使用)顯示示例,幫助 LLM 在不檢查整個代碼庫的情況下理解使用模式。
上下文提示:引導下一步
其中最具影響力的改進是在每個命令輸出中添加了 上下文 提示。我們不再提供通用的幫助文本,而是根據當前上下文提供具體的後續步驟。
示例:漸進式提示
搜索之後:
$ cr query search "render-app!" -f 'app.main/main!' -l
Search: Searching for:
render-app! (contains)
Filter: app.main/main!
Results: 2 match(es) found in 1 definition(s):
● app.main/main! (2 matches)
[5,0] in render-app!
[6,3,2,0] in render-app!
Next steps:
• View node: cr tree show '<ns/def>' -p "<path>"
• Batch replace: See tip below for renaming 2 occurrences
Tip for batch rename:
Replace from largest index first to avoid path changes:
cr tree replace 'app.main/main!' -p "6,3,2,0" --leaf -e '<new-value>'
cr tree replace 'app.main/main!' -p "5,0" --leaf -e '<new-value>'
⚠️ Important: Paths change after each modification!
查看節點之後:
$ cr tree show app.main/main! -p "5"
Location: app.main/main! path: [5]
Type: list (1 items)
Cirru preview:
render-app!
Children:
[0] "render-app!" -> -p "5,0"
Next steps: To modify this node:
• Replace: cr tree replace app.main/main! -p "5" -j '<json>'
• Delete: cr tree delete app.main/main! -p "5"
修改之後:
$ cr tree replace app.main/add -p "2,0" --leaf -e '*'
✓ Applied 'replace' at path [2,0] in 'app.main/add'
From:
"+"
To:
"*"
Next steps:
• Verify: cr query def 'app.main/add'
• Find usages: cr query usages 'app.main/add'
智能錯誤提示
當操作失敗時,我們提供可操作的指導:
$ cr tree show app.main/main -p "99,2,1"
Error: Invalid path
Path index 99 out of bounds at depth 0 (list has 10 items)
→ Longest valid path: root
→ Node at that path: defn main () ... (10 items)
Available: This node has 10 children (indices 0-9)
→ View it with: cr tree show app.main/main -p ""
Hint: First few children:
[0] "defn" -> "0"
[1] "main" -> "1"
[2] [] (0 items) -> "2"
... and 7 more
影響: LLM 可以自行糾正,而不需要人工干預。
文檔集成
我們將 Calcit 的指南直接集成到了 CLI 中:
$ cr docs search "macro"
Found 12 matches in 3 files:
quick-reference.md (quick-reference.md)
------------------------------------------------------------
54: ; Thread macro
55: -> data
56: filter some-fn
57: map transform-fn
features.md (features.md)
------------------------------------------------------------
9: - **Lisp syntax** - Code as data, powerful macro system
10: - **Hot code swapping** - Live code updates during development
...
27: - [Macros](features/macros.md) - Code generation and syntax extension
Tip: Use `cr docs read macros.md` to view full content
Use `cr docs read features/macros.md` for detailed guide
其他文檔命令:
$ cr docs list # 列出所有可用文檔
$ cr docs read macros.md -s 20 # 從第 20 行開始閲讀
$ cr docs read intro.md -n 50 # 閲讀前 50 行
結果: LLM 可以在不離開編碼上下文或調用外部來源 API 的情況下查詢文檔。
增量開發工作流
當這些工具組合在一起時,真正的力量就顯現出來了:
典型的 LLM 輔助開發過程
-
探索代碼庫:
cr query ns # 列出所有命名空間 cr query defs app.main # 命名空間中的函數 cr query peek app.main/add # 快速確認簽名 -
理解實現:
cr query def app.main/add # 完整代碼(僅 Cirru) cr query usages app.main/add # 在哪裏被使用了? -
定位修改點:
cr query search "+" -f app.main/add -l # 發現於路徑 [2,0] -
查看並修改:
cr tree show app.main/add -p "2,0" cr tree replace app.main/add -p "2,0" --leaf -e '*' -
增量驗證:
cr edit inc --changed "app.main/add" # Watcher 自動重新編譯 cr query error # 檢查問題
Token 效率: 與每個命令都輸出完整 JSON 和冗長錯誤消息相比,這套工作流消耗的 Token 顯著減少。
習得的設計原則
1. 漸進式展示優於完整性
不要一次性傾倒所有信息。根據可能的後續操作分層提供信息:
- Peek -> 簽名和元數據
- Read -> 完整實現
- JSON -> 程序化操作
2. 上下文引導優於通用幫助
每個輸出都應該建議最有價值的下一個命令:
- 搜索後 -> 展示如何查看結果
- 查看後 -> 展示如何修改
- 修改後 -> 展示如何驗證
3. 人讀優先,機器按需
默認使用 LLM 自然閲讀的格式(代碼語法,而非 JSON)。通過明確的標記(-j, --json)提供結構化格式。
4. 錯誤消息即導航輔助
失敗的操作應該:
- 解釋 什麼 地方出錯了
- 展示 最長有效路徑
- 列出 可用選項
- 建議 糾正性命令
5. 集成參考資料
不要假設能訪問外部文檔。為語言文檔、示例和 API 參考提供 search 和 read 命令。
對比:Calcit CLI 與傳統文件工具
在使用 LLM 輔助開發時,效率瓶頸通常在於智能體如何感知和修改世界。以下是 Calcit CLI 與傳統工作流(如 Rust 或 Python 配合 Copilot/Cursor)的對比。
1. 文檔訪問:窄上下文與寬上下文
傳統方式 (Rust/Python):
- 差距: LLM 通常依賴其訓練數據(可能已過時)或外部“網頁搜索”工具。
- 噪聲: 閲讀文檔通常需要抓取整個網頁或大型 Markdown 文件,消耗成千上萬個探索性 Token。
- 摩擦: 如果項目使用特定的內部庫,開發者必須手動將文檔複製粘貼到提示詞中。
Calcit CLI:
- 在上下文發現: 通過
cr docs search和read,LLM 可以精確查詢所需的章節(例如“宏如何處理 ~@”)。 - 集成庫:
cr libs readme讓智能體無需離開終端即可探索第三方模塊文檔,確保文檔和代碼版本始終同步。 - 效率: 智能體在一個針對性的命令中完成了從“我需要知道 X”到“我有了説明 X 的 20 行內容”的轉變。
2. 代碼修改:結構化與文本化
傳統方式 (基於文本的 Diff):
- “迷失文件”問題: 修改 500 行的文件時,LLM 經常遺漏章節(
// ... 現有代碼 ...)或產生行號幻覺,導致文件損壞。 - 縮進脆弱性: 在縮進敏感語言中,文本搜索替換中一個放錯位置的空格就會破壞整個模塊。
- 上下文開銷: 為了安全編輯一個函數,智能體通常覺得需要閲讀整個文件以確保不破壞周圍的範圍。
Calcit CLI (基於樹的編輯):
- 外科手術般的精度: 通過使用
cr tree show找到路徑(如[2,0,1])並使用cr tree replace更新它,智能體執行的是結構化修改。由於 CLI 處理了重構過程,因此不可能破壞縮進。 - 極簡上下文: 智能體只需要看到它正在修改的特定 AST 節點。它不需要加載同一個文件中的其他 20 個函數,僅僅是為了避免迷路。
- 驗證循環: CLI 立即返回修改前後的結構,允許 LLM 在不重新閲讀整個文件的情況下驗證其邏輯。
總結:信噪比 (SNR)
| 特性 | 傳統工作流 (標準文件) | Calcit CLI 工作流 (快照 + 樹) |
|---|---|---|
| 探索文檔 | 高噪聲 (瀏覽器抓取, 手動粘貼) | 高信號 (cr docs 針對性讀取) |
| 定位代碼 | 模糊 (grep/搜索通常缺乏結構) | 精確 (cr query search 返回 AST 路徑) |
| 修改代碼 | 風險 (diff, 行號, 縮進) | 安全 (結構化節點替換) |
| 驗證 | 沉重 (完整重解析, 手動檢查) | 輕量 (即時本地對比和 cr query error) |
通過將代碼和文檔視為可查詢的數據庫,而不是文本文件的集合,我們讓 LLM 能將更多時間花在“思考”上,而不是“排版”上。
對開發工作流的影響
雖然很難量化每個項目的確切 Token 節省量,但開發體驗的轉變是深遠的。通過針對 LLM 交互進行優化,我們觀察到了幾個定性的改進:
- 減少噪聲: 漸進式展示模型確保 LLM 只“看到”相關的代碼和元數據,防止模型被冗長的 JSON 結構淹沒。
- 提升自愈能力: 準確的錯誤消息和上下文提示允許 AI 智能體獨立解決失敗,大幅減少了在複雜重構期間對人類“手把手教”的需求。
- 降低認知負擔: 即使對於人類開發者,更清晰的 CLI 輸出也使得掃描定義和在 AST 中尋找特定節點變得更容易。
- 更快的迭代: 增量驗證和熱重載的結合帶來了一個緊湊的反饋循環,感覺比傳統的構建運行週期更具響應性。
數數難題:通過索引導航 AST
儘管基於樹的編輯精度很高,我們還是遇到了一個獨特的挑戰:LLM 的數數能力出奇地差。
在早期迭代中,我們注意到智能體通常需要 3-5 次嘗試才能命中正確的節點。當 LLM 看到一系列表達式時,它經常難以一致地將視覺元素映射到其確切的數值索引。在深層或寬大的 AST 結構中,這表現為一系列特定的失敗。
觀察到的退化
- 差一錯誤 (Off-By-One): 智能體在定位長列表中的兄弟節點時,可能明明想指 index 4 卻寫了 3。
- 深度路徑幻覺: 在像
[6,3,2,0,1]這樣的複雜嵌套結構中,智能體可能迷失層級並“捏造”出不存在的路徑。 - 索引漂移陷阱: 在執行多次編輯時,智能體經常忘記刪除或插入節點會改變後續所有兄弟節點的索引。
補救措施
為了減輕這些“數數幻覺”,我們演進了 CLI,由其代表智能體執行座標計算:
- 搜索優於計算: 而不是要求智能體“找到第 5 個參數”,我們提供了
cr query search,它根據內容識別出確切路徑(如[5,0])。智能體從 計算 座標轉變為 複製 座標。 - 顯式子節點路徑: 在
cr tree show中,我們用即插即用的 CLI 參數替換了內部表示顯示:[0] "render-app!" -> -p "5,0"。這鼓勵智能體將路徑視為一個字面量字符串,直接用於下一個命令。 - 錯誤中的路徑引導: 當智能體提供無效路徑時,CLI 不僅僅是報錯。它會列出有效的兄弟節點及其索引(例如
Available: indices 0-9),允許智能體通過“觀察”正確選項來糾正自己。 - 批量修改邏輯: 當發現多處匹配時,CLI 明確提供“逆序”操作命令(從最大索引開始)。這確保了儘管發生了之前的編輯,每個後續路徑依然有效,如果提供了這樣的序列,LLM 能夠很好地遵循這一概念。
通過承認 LLM 將代碼感知為 Token 序列而非結構化對象,我們將 AST 導航的重擔從 AI 的推理引擎轉移到了 CLI 的輸出中。
結論
我們這段旅程的關鍵洞察是:對 LLM 友好的工具同樣造福人類。
通過專注於:
- 漸進式展示
- 上下文引導
- 選擇性詳盡
- 集成文檔
我們創建了一個對 AI 助手高效且對人類開發者直觀的 CLI。顯著的 Token 減少直接轉化為成本節省,但更重要的是,它降低了 LLM 及其人類協作者的認知開銷。
隨着 AI 編碼助手變得無處不在,工具設計者應該問:“LLM 能高效使用它嗎?”答案往往會導向對每個人都更好的工具。
嘗試 Calcit: https://github.com/calcit-lang/calcit
CLI 文檔: 參見倉庫中的 docs/Agents.md,獲取 Calcit 與 LLM 輔助開發的完整指南。