工具越多,Agent 越蠢?前 Manus 负责人扔掉了所有工具
公众号版 | 2026-03-13 | 约 2000 字
你花了多少时间给 AI Agent 设计工具库。
search_web、read_file、execute_code、send_email……
每个函数都有精心的 JSON Schema,参数有 description,enum 值一个不少。你觉得工具越完善,Agent 就越强大。
然后你发现,它还是会用错工具。还是会绕远路。还是在简单任务上卡住。
前 Manus 后端技术负责人 Morro Hsu 有同样的困惑。他花了两年时间,和这个问题正面对抗。
最后他做了一个让很多人觉得"开玩笑吧"的决定:把所有工具删了,换成一行:
run(command="...")
就这一个工具。然后接上 50 年前的 Unix 命令行。
你以为越精细越好,但 LLM 不这么想
Function Calling 是 OpenAI 2023 年引入的特性。逻辑很直觉:给模型一组工具,让它按 JSON 格式"调用"。
整个行业跟着走。教程里都是这么写的,官方文档也这么推荐。所以几乎所有 Agent 开发者都在做同一件事:设计工具库、写 Schema、调试工具选择。
但 Morro 在 Reddit 原帖里提出了一个关键问题:
LLM 的训练数据里,哪个更多——JSON 工具调用,还是 bash 命令?
答案显然是后者。GitHub 上数十亿行代码,shell 脚本、Makefile、README 里的命令示例、Stack Overflow 上的终端输出,全都在告诉模型"这就是怎么用工具的"。
Function Calling 的 JSON Schema 格式是 API 时代为人类开发者设计的。LLM 从来没被专门训练过如何优雅地使用它,只是学会了模仿人写过的少量示例。
Shell 才是 LLM 真正的母语。
<!-- diagram:compare -->
用工具库,实际上在做什么
Hsu 举了一个例子:
任务是"读取日志文件,筛选错误行,统计数量"。
Function Calling 的做法:三次调用——read_file() → filter_lines() → count_results()。
Shell 的做法:一行——cat app.log | grep ERROR | wc -l
哪个在训练数据里出现频率更高——答案不难猜。
更深的问题是:当你有 15 个工具,模型要在每一步决策时考虑哪个最合适。Context 在消耗,决策链在延伸,出错概率在累积。工具越多,噪声越大,Agent 越蠢。
Hsu 把这个现象总结为:工具集越丰富、越结构化,模型就越可能选错,或者采取低效路径。
这个结论和很多开发者的直觉相反。我们习惯于"精细化等于可靠",但对 LLM 来说,精细化的工具库在训练分布上就是一种外语。它只是在模仿,而不是在理解。
Shell-First 的三个核心技巧
换成命令行不是把问题推给 LLM,而是要把命令行改造成 Agent 友好的环境。Hsu 总结了三个关键做法:
1. 渐进式帮助发现
传统做法:在 system prompt 里塞一堆工具文档,让模型"提前知道所有工具"。
问题:context 预算浪费在大量用不上的文档里,模型反而因为信息过载而表现更差。
Shell-First 的做法:工具本身教会模型怎么用。
$ memory
Usage: memory <command> [options]
Commands: search, save, list, delete
Run 'memory <command> --help' for details
Agent 先空调用看用法,再探索具体参数。和人类学新命令一模一样——按需加载,而不是一次性注入。
这个设计的价值被严重低估:你不需要维护一份随着工具变化而不断更新的"工具说明书",模型自己会去读。
2. 导航式错误信息
这是 Hsu 最强调的一点,也是被低估最多的细节。
标准 Unix 报错:
cat: photo.png: Is a binary file
Agent 友好报错:
[error] cat: binary image file
Use: see photo.png
差别在哪。标准报错告诉模型"出错了",Agent 友好报错告诉模型"下一步该怎么做"。
Hsu 分享了一个真实案例:因为 stderr 被静默,一个 Agent 为了安装一个包,在 pip、uv、apt 之间盲目重试了 10 次,浪费了大量 token 和时间。
错误信息不是让人看的,是让 Agent 导航用的。这是一个架构层面的设计原则,不是细节。
<!-- diagram:workflow -->
3. 执行层与表现层分开
这是最底层的工程洞察。
执行层:命令实际运行,输出原汁原味的 Unix 数据流,不做处理。
表现层:给 LLM 看的输出,需要专门处理:
- 超长内容自动截断,但告诉模型完整路径在哪
- 二进制文件替换为提示信息
- 附加
[exit:0 | 12ms]这样的元数据
LLM 看到的输出 ≠ 系统实际执行的输出。表现层的设计决定了 Agent 学到什么、Context 里放了什么。这两层一旦混在一起,Context 就会被垃圾信息污染,Agent 会学到错误的模式。
举个具体例子:一个脚本输出了 10 万行日志,如果直接喂给模型,Context 窗口就爆了。表现层应该自动截断为"[输出已截断,完整日志在 /tmp/run.log,前50行如下]"——这才是 Agent 需要知道的。
Shell 是超集
Hsu 在讨论里回应质疑时说了一句话,我觉得讲清楚了核心:
Shell 是超集。你随时可以从 Shell 里调 Python,但从纯代码环境调 Shell,其实是绕了一段路。
这不是说 Function Calling 错了。对于简单场景、需要严格结构化输出的场景,它依然有效。
但对于需要长时间运行、多步决策、复杂工具组合的 Agent,Shell-First 的效率优势是实质性的。
你不是在给 Agent"降级",你是在给它换一种更接近它母语的工作方式。
说说我自己的想法
读完 Hsu 的分享,我想起了一个类似的反直觉场景:很多人给 LLM 的 prompt 越写越长、越加越复杂,然后发现效果越来越差。
原因一样——不是工具或指令"不够精细",而是 Context 里的噪声太多,模型找不到真正重要的信号。
Agent 设计的真实问题不是"工具够不够",而是每一步 Context 里,有效信号和噪声的比值是多少。
这个问题没有标准答案,但 Shell-First 至少给了一个可以实验的起点:从最小化开始,而不是从最大化开始。
删掉那些你以为有用、实际上只是让模型更迷惑的工具,也许是个好起点。
你现在在开发 Agent 吗?有没有遇到工具越多反而越难用的情况?欢迎在评论区聊聊你的做法。
参考来源