给 Agent 接上几十个工具,听起来是能力的胜利,实际上常常是一场退化。当工具数量越过某个阈值,模型不再“会用更多东西”,而是开始在相似的工具之间犹豫、误用参数、把简单任务拆成错误的步骤。我们最后选择了一条看起来“复古”的路:把一整套能力收敛成一个 app 伪 shell。
LLM 面对几十个工具的问题
每暴露一个工具,模型的决策空间就要膨胀一次。它需要在每一步回答两个问题:现在该用哪个工具,以及这个工具的参数到底长什么样。工具列表里塞了三十个函数时,这两个问题都会变难。
具体的失败模式很一致:
- 名字相近的工具被混用,比如“列出 Agent”和“列出计划任务”被张冠李戴。
- 参数 schema 越长,模型越容易漏填必填项或编造不存在的字段。
- 所有工具定义都要常驻上下文,挤占了本该留给任务本身的窗口。
换句话说,工具不是越多越好,而是越多越需要结构。问题不在能力,而在能力的呈现方式。
CLI 是 LLM 熟悉的接口
我们没有发明新范式,而是借用了一个模型早已见过无数次的接口:命令行。预训练语料里有海量的 shell 会话、--help 输出、man page 和 README 片段,CLI 的形状对 LLM 来说是熟悉且可预测的。
把能力组织成子命令,模型就能复用它对 CLI 的直觉:动词在前、子命令分层、选项用 --flag、状态码非零代表失败。于是工具系统只需要暴露一个入口:
app --help
app 之下挂着 mcp、agent、schedule 等子命令,每一类能力是一个命名空间,而不是顶层工具列表里又一个扁平的函数。
app –help 的渐进披露
渐进披露(progressive disclosure)是这套设计的核心。模型一开始只需要知道 app 存在,以及顶层有哪些子命令;具体某条命令的参数,等真要用时再问。
这正是 --help 天然提供的:
app agent list
app schedule list
需要更细的用法时,模型可以追加 --help 拿到该子命令的选项说明,而不必把所有定义预先塞进上下文。能力是“按需展开”的——这让常驻 token 大幅下降,也让模型的每一步决策都落在一个更小、更清晰的范围里。
–json / –dry-run / –yes 的统一语义
零散工具的另一个隐性成本是语义不一致:这个返回 JSON、那个返回自然语言;这个会直接执行、那个先要确认。模型必须为每个工具单独记住脾气。
伪 shell 让我们可以把一组横切语义统一成全局约定:
--json:输出机器可解析的结构化结果,便于模型继续处理或链式调用。--dry-run:只预演、不落地,先看清楚一条命令会改动什么。--yes:跳过确认,用于已经明确意图、需要自动执行的场景。
app mcp status --json
一旦这些约定在所有子命令里一致成立,模型就不必逐个工具地学习例外。它学会的是一套语法,而不是几十个特例。这里要诚实地说明:Deskmate 仍处于 early access,子命令的覆盖面和这些 flag 的一致程度还在补齐,个别命令可能尚未支持全部约定。
和 MCP 的关系
伪 shell 不是要取代 MCP,而是和它分工。MCP 负责把外部世界——文件系统、浏览器、第三方服务、你自己的脚本——接入 Agent;app 则负责把这些接入后的能力,以及 Deskmate 自身的内建能力,统一成一致的调用界面。
你可以把 MCP 理解成“插上更多能力”,把 app 理解成“用同一种语法去驱动它们”。app mcp status 这类命令本身就是用伪 shell 去观察和管理 MCP 的状态——两层是协作关系,而非二选一。
未来扩展方向
这套设计最让我们满意的一点,是它的扩展成本几乎恒定。新增一类能力,就是新增一个子命令,而不是往工具列表里再塞一个需要模型重新认识的函数。顶层接口不变,渐进披露的机制不变,--json / --dry-run / --yes 的语义也不变。
接下来的方向大致有几条:让 --help 输出更适合模型消费,补全更多子命令的横切 flag,让 --dry-run 的预演结果更结构化。它们都不需要重做接口,只是在同一个形状里继续填充。
工具不是越多越好。真正的杠杆,是给模型一个它早就认识的接口,然后把能力按需、按一致的语义交到它手上。