随着 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 辅助开发的完整指南。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。