You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
-
Calcit 是一个同时在做语言、运行时、编译器、文档和命令行工具的项目。随着
crCLI 逐渐承担更多查询、重构、验证和文档检索职责,它也自然变成了一个很适合观察 Agent 工作方式的实验场:Agent 需要先发现功能,再缩小范围,再做局部修改,最后还要能处理各种不完整输入、错误上下文和 shell 干扰。这篇文章不是泛泛而谈“AI 友好设计”,而是回顾 Calcit 在 editing history 和近期 commit history 里已经尝试过的做法,整理出一套更具体的经验。前半段简单交代项目背景,后半段尽量抽象成通用原则,但仍然只围绕已经试过的方案展开。
1. 用任务语言来组织命令,而不是实现语言
复杂 CLI 往往天然会长出很多内部术语,但 Agent 更擅长理解任务,而不是实现细节。一个更稳的做法,是尽量让命令名、子命令名和参数名贴近用户真正想完成的动作。
这背后有两层收益:
经验上,命令系统最好同时提供两层能力:
这样 Agent 可以先走最短路径,只有在任务确实复杂时才进入更通用的接口。
2. 优先暴露结构,而不是逼调用者自己还原结构
对复杂工具来说,文本永远是最后一层表现,结构才是更稳定的操作对象。只要命令行工具内部本来就理解节点、路径、模块、入口、文档元数据或结果分组,就应该尽量把这些概念直接暴露给调用者。
这并不意味着所有操作都要变成 AST 工具,而是说:CLI 最好提供一种比纯文本更稳定的定位方式。这样 Agent 可以先定位,再判断,再修改,而不是靠全文搜索和字符串替换碰运气。
一个常见的副作用是:一旦结构接口存在,命令之间就能共享同一套心智模型。查询、预览、修改、验证不再是四套各自独立的语言。
3. 默认输出要可收敛、可翻页、可预览
Agent 处理命令行输出时,最大的风险不是信息不足,而是信息没有边界。复杂工具的读取设计,应该默认假设结果会很大、会有重复、会有相似项。
因此更稳的设计通常包含三件事:
这类设计的关键不是某个具体参数名,而是把“完整信息”变成“分层信息”。Agent 不应该被迫在一次输出里消化所有结果,而应该能逐步缩小范围、逐步增加细节。
4. 用中间表示把复杂缩进问题拆成小问题
仓库里关于 chunked Cirru 的一组改动,背后其实体现了一种很值得复用的设计:当原始结构对 LLM 来说太大、太深、太容易因为缩进和局部细节失焦时,不要强迫模型直接消费原始整体,而是先生成一种可导航的中间表示。
原始问题很典型:
chunked display 的设计意图,并不是为了“把大块代码切开”这么简单,而是为了把一个复杂阅读任务重新组织成两层:
这样做以后,Agent 面对的就不再是一个需要一次性理解的大缩进结构,而是一组带锚点的小问题:
ROOT负责解释整体控制流;这里最重要的并不是“分片”本身,而是程序主动承担了逻辑正确性的那部分工作。模型不用自己猜“这一段是不是原来那一层的子块”,因为程序已经提供了:
换句话说,CLI 不是把大问题原样丢给 LLM,而是先把问题改写成一个更适合逐步求解的表示。LLM 负责理解每一块,程序负责保证这些块之间的引用关系和组合关系不出错。
这种模式对复杂 CLI 很通用,尤其适合下面这些场景:
从设计上看,这类中间表示最好满足几个条件:
这一点其实是“Agent 友好 CLI”里很容易被忽略的一层:不是所有复杂问题都该直接交给模型。有时候更好的做法,是先由程序把问题拆解、编号、排序、加锚点,再把缩小后的问题交给模型处理。
5. 把 LLM 不擅长完整理解的逻辑,前移到程序里
还有一种非常重要、但比“优化提示词”更根本的思路:凡是程序本来就能稳定判断的逻辑,就尽量不要留给 LLM 去脑补,而应该直接实现成 CLI 能调用的能力。
这类能力的价值,不在于替代模型,而在于把模型最脆弱的那部分工作前移到程序里。Calcit 里比较典型的就是两类:
这背后的原则很简单:
把这些能力暴露到命令行以后,Agent 的工作方式会发生很大变化。它不再需要“自己完整理解规则后再小心模拟执行”,而是可以把任务拆成更稳的回路:
这类设计其实是在重新划分职责边界:
对复杂 CLI 来说,这非常重要。因为一旦把“验证”和“规范化”显式做成命令,Agent 就不必在上下文里同时背住全部规则。它只需要记住:哪些问题可以直接交给工具验证,哪些问题才真的需要自己推理。
这也是为什么像批量类型检查、解析器试跑、格式化器试跑、文档示例校验这种能力,对 Agent 来说价值特别高。它们不只是“开发者工具”,而是把高复杂度、低可解释性的规则从 prompt 内部搬到了程序边界上。
6. 显式暴露上下文边界
很多复杂 CLI 的问题,不是缺功能,而是上下文默认值太隐蔽。一个查询、一次校验、一次文档检索,到底发生在哪个 entry、哪个模块集合、哪个文档范围、哪个运行上下文里,如果没有显式暴露,Agent 很容易在错误边界里做出“正确推理”。
所以好的设计不是一味减少参数,而是把那些真正影响结果边界的参数显式化:作用域、入口、模块、范围、过滤条件、精确匹配模式,都应当可见、可选、可组合。
上下文一旦清晰,很多“查不到”“命中太多”“结果不对”的问题,本质上都会缩小成配置问题,而不是推理问题。
7. 避免隐藏状态,必要时把关键状态落到固定文件
对复杂情况来说,CLI 还需要处理另一类很棘手的问题:状态。很多工具在内部维护了大量隐式状态,例如 watcher 最近一次编译结果、增量变更队列、缓存文档、上一次错误、当前载入的依赖集合。如果这些状态只存在于进程内部,或者散落在难以推测的位置,那么不只是 LLM,连人类程序员都会觉得难以捉摸。
Calcit 在一些地方采用了更适合 Agent 的做法:把关键状态变成固定文件或固定入口,让状态可以被查、被读、被刷新、被再次解释。
对照项目里的实现,可以看到几种很典型的模式。
7.1 把最近错误写入固定文件,而不是只留在运行时输出里
在
src/bin/cli_handlers/edit.rs的handle_inc(...)里,增量变更会写入.compact-inc.cirru,同时会主动清空.calcit-error.cirru,让 watcher 后续重新编译后把最新错误重新落盘。与之对应,src/bin/cli_handlers/query.rs的handle_error()并不是去问某个内存里的 watcher 状态,而是直接读取固定路径的.calcit-error.cirru,还会检查文件修改时间,提示它是否可能已经过期。这个设计的目标很明确:
cr query error间接读取,也可以在必要时直接读文件;7.2 把增量变更写成显式工件,而不是只靠进程间暗约定
同样在
handle_inc(...)里,.compact-inc.cirru不是一个内部细节,而是一个显式的增量变更字典文件。CLI 会把added、removed、changed、namespace 更新等内容序列化进去,再由 watcher 处理。这里的关键不只是“用了文件”,而是把本来可能藏在参数拼接、内存消息或一次性 shell 调用里的状态,变成了一个可检查、可重放、可解释的中间工件。对 Agent 来说,这意味着:
7.3 对缓存也要给固定位置和刷新机制
文档系统里有另一个很好的例子。在
src/bin/cli_handlers/docs.rs里,Agents 文档缓存路径通过get_agents_cache_path()固定到了~/.config/calcit/Agents.md。ensure_agents_cache(...)负责判断缓存是否过期、是否需要刷新,handle_agents(...)则会把实际读取的缓存路径打印出来,并支持--refresh强制刷新。这个设计的目标同样很适合 Agent:
7.4 让固定状态文件与命令行接口互相补充
光有状态文件还不够,最好还要有配套命令,把这些状态重新包装成更适合阅读或进一步处理的输出。Calcit 的做法基本符合这一点:
.calcit-error.cirru对应cr query errorcr docs agents --refresh这形成了一个很重要的组合:
对 Agent 来说,这是最稳的状态设计之一。因为模型既可以把它当作“命令式接口”来用,也可以在必要时退回到底层文件本身,甚至再调用其他命令对文件内容做进一步展开理解。
7.5 这样做的几个目标
把关键状态显式化,通常是在追求下面几件事:
这也是为什么“避免隐藏状态”对 Agent 特别重要。隐藏状态会把问题变成猜谜;显式状态则会把问题重新变成检查、比较和验证。
8. 把输出当成机器界面,而不只是人类终端输出
Calcit 近期很明确地在做一件事:把 CLI 输出当成一种“机器可消费的 UI”。这一点在 command echo、tips 收敛和 quiet output 上体现得非常明显。
5.1 用 command echo 承担“输入语境”
一旦命令回显足够语义化,正文里就不需要再重复
target/path/pattern/entry/deps。这样输出可以形成很稳定的两层:这对 Agent 很有帮助,因为它可以先确认 CLI 理解的参数是否正确,再解析结果正文。
5.2 把提示系统做成策略,而不是固定噪音
早期很多 CLI 的“帮助信息”都是硬编码附赠,结果对脚本或 Agent 来说会变成持续噪音。Calcit 后来把这部分做成了策略层:
--no-tips--tips-level minimal|full|none这比简单加一个 quiet flag 更实用,因为它允许同一套命令在“交互式学习”和“自动化执行”两种场景下切换输出密度。
5.3 正文只保留新信息
在 command echo 成熟之后,Calcit 又专门清理过一轮重复输出:把 query/tree/docs 正文里那些仅仅重复输入参数的部分删掉,让正文只保留结果、差异、上下文预览或者真正新增的信息。
对照代码看,这不是一句“减少噪音”那么简单,而是一种明确的输出分层策略:
src/bin/cli_handlers/command_echo.rs里,render_command_echo(...)和一组push_query/push_docs/push_tree/...helper 负责把命令参数统一整理成单行Command: ...。也就是说,target、path、pattern、entry、detail-offset、chunk 参数等输入语境,已经有一个固定出口。src/bin/cr.rs里,只要启用了这套 command echo,就会同步调用suppress_command_guidance()和calcit::set_quiet_tool_output(true)。这意味着 CLI 不只是“多打印一行命令”,而是顺手关闭那些会污染结果正文的环境噪音,比如版本、模块加载、平台 API 注册之类的提示。src/bin/cli_handlers/query.rs的handle_query_command(...)只负责把参数路由到具体查询函数,而handle_def/handle_peek/handle_examples/handle_schema/handle_usages这些函数在拿到参数后,直接输出类型、文档、schema、examples、usages 等结果,不再先重复打印一遍 target 摘要。src/bin/cli_handlers/tree.rs也一样。像handle_show(...)里,正文保留的是Type、chunked preview、JSON、路径错误时的局部导航提示,而不是再把target/path/depth重复一遍。这样做有几个很明确的目标。
7.3.1 把“调用语境”和“执行结果”拆开
如果输入参数和输出结果混在一起,Agent 在二次解析时就很容易把“用户输入了什么”和“程序新告诉了我什么”混为一谈。command echo 的做法,相当于先固定一个请求头,再把正文纯化成响应体。
7.3.2 降低重复 token 对注意力的污染
对 LLM 来说,重复出现但没有新增语义的内容,会不断抢占注意力。特别是
target/path/pattern这类字符串,本来就和结果正文高度相似;如果一段输出里前后重复三次,模型很容易把它们都当成“需要继续跟踪的重要线索”。删掉这些重复项,能让注意力更集中在真正的差异信息上。7.3.3 让结果正文更稳定、更适合程序消费
当输入参数由 command echo 统一承担后,正文格式就更容易长期稳定。无论是人、Agent,还是外部脚本,都更容易假设:正文里看到的字段,基本都代表执行结果,而不是参数回显。这种稳定性对自动化非常重要。
7.3.4 给后续降噪留统一入口
一旦命令参数收敛到 command echo,后续想调整输出风格时,只需要围绕这一个入口做改动,而不是去每个 query/tree/docs handler 里逐个清理重复摘要。也就是说,这不仅是显示优化,也是一次架构收敛。
这类“删东西”的优化,其实对 Agent 非常关键。因为模型最容易被误导的,往往不是缺失信息,而是重复但无增量的信息。
9. 好的兜底不是“报错”,而是“把用户送回正确轨道”
Calcit 在不少地方都已经试过“软兜底”的设计,而不是简单返回一个失败状态。
比较典型的模式包括:
rewrite在缺少--with时,不只是报错,还提示改用replace。这类设计背后的重点是:CLI 应该帮助 Agent 完成“下一步正确动作”,而不是只负责指出当前动作错了。
10. 写操作默认保守,读操作可以更积极
Calcit 有一个很值得保留的取向:对查看类命令可以更积极地做恢复,但对修改类命令要保守。
一个已经证明有用的例子,是
edit def --overwrite的元数据保留:覆盖代码时,尽量保留已有的doc、examples、schema,避免 Agent 在做全量改写时把附属信息意外丢掉。这类保护看起来细节,但对于自动化改动非常重要。同样的思路也适用于命令纠错:
原因很简单:读错一次通常只是浪费一次查询,写错一次可能就是错误修改。
11. 把 shell 当成不可靠环境来设计
最近一次很典型的经验,是 shell 对符号名的破坏。像
element->string这样的名称,未加引号时很容易被 shell 把>当成重定向符号处理。结果就是:CLI 收到的不是完整定义名,而是一个被截断的前缀。Calcit 近期的处理方式很有代表性:
stderr,避免stdout已经被 shell 重定向走。这里最值得总结的不是“猜名字”本身,而是以下三条更通用的原则:
与之配套的还有另外两个长期方向:
12. 文档本身也应该被设计成 Agent 可以调用的界面
Calcit 在文档体系上有两个很实用的尝试。
第一,是把文档做成可检索对象,而不是静态文件集合。前面提到的
aliases、entry_for、scope、module,本质上都是在把文档变成结构化索引。第二,是把文档示例做成可验证对象。
docs check-md相关演化说明了一件事:如果一篇文档会被 Agent 当成操作依据,那么它就不应只是“语法上像示例”,而应该尽量能在接近真实运行环境的条件下被验证。这会倒逼几件事情:
13. 一套更稳的总结
回顾这些已经试过的方案,我会把“Agent friendly CLI design”归纳成下面几条非常朴素、但确实有效的原则:
如果只看 Calcit 这段时间的演化,一个很明确的结论是:所谓“Agent 友好”,并不主要来自更复杂的 AI 集成,而来自更稳定的命令语义、更克制的输出、更显式的上下文边界,以及更谨慎的失败恢复。
Beta Was this translation helpful? Give feedback.
All reactions