工具加得越多,Agent 反而越像在“自我绊倒”。我见过最离谱的一次是:它明明会做事,却在一堆 function calling 工具里选了个最不相关的那个,然后一本正经地把错误当成功继续往下跑。你以为是推理退化,实际是它在“选哪个工具”这一步就开始抽风。
接手一个已经上线的 Agent 时,这种感觉尤其明显:能查资料、能改文件、能跑脚本、能发消息——工具列表长得像超市货架。你一开始会觉得“装备齐全,稳了”。话说回来,线上最常见的报错往往不是模型不会推理,而是它卡在最前置、最机械的那个动作:选工具。
更惨的是,你加一个新工具,旧能力反而变差。像给新同事发了一本更厚的通讯录,然后他开始给错人打电话。
前阵子我帮老大整理一套 agent 工具层的资料,翻着翻着看到一个经验帖:作者做了两年 agent 之后,干脆把 function calling 那套“多工具目录”砍掉了,只给模型一个工具:run(command="..."),让它用 CLI(命令行)去完成一切。
这招听起来粗暴,但很省命(也省时间)——因为它盯住的是 Agent 工程里最费劲的那条底线:稳定性。
Function Calling 为什么越用越不稳?
因为你把“做事”拆成了两道选择题:
- 这一步用哪个工具?
- 这个工具的参数怎么填?
工具一多,模型的注意力会被“工具选择”吃掉。它不是不会干活,是被迫在一堆互不相干的 schema 之间来回切换:read_file、search_text、count_lines、run_python……每个都长得不一样,名字也像不同团队各写各的。
还有一个事:多工具目录天然会鼓励你把动作切得更碎。碎到最后,工具调用次数变多、上下文变长、回合数变多,任何一步小错误都会被放大成“连环偏航”。
而 CLI 的好处是:所有能力都长一个样——一条命令字符串。
模型不用“选工具”,只需要“拼命令”。这是同一种脑回路。
下面这个对比,基本就是分水岭:
| 任务 | 多工具(function calling) | 单工具(run + CLI) |
|---|---|---|
| 统计日志里 ERROR 行数 | read_file → search_text → count_lines(多次调用、上下文变长) | cat app.log | grep ERROR | wc -l(一次调用) |
| 下载并快速检查 CSV | http_get → parse_csv → head_rows | curl -sL URL -o a.csv && head a.csv |
| 失败回退策略 | 你得写一堆 if/else 的工具编排 | cmdA || cmdB(CLI 天生支持) |
| “先小范围验证再扩大” | 需要你额外设计工具/参数 | head / tail / grep -n 一把梭 |
“单工具”并不是更原始,恰恰相反:它把组合能力交还给了 Unix 那套 50 年验证过的管道体系。你不需要教模型“如何把三步粘起来”,因为 | && || 已经把这件事写进了语言本身。
“只给一个 run 工具”到底怎么落地?
先把一句话钉死:能跑命令不是关键,可控才是关键。
落地真正的三件套是:
- 命令白名单(能做什么)
- 沙箱/权限隔离(在哪做)
- 失败信号标准化(怎么知道做错了)
你别让模型在你的生产机上随便 rm -rf /,那不是 Agent,是自爆装置。
run 的前提是“可控”。如果你做不到白名单 + 沙箱,就别把它接到任何有价值的数据上(仓库、数据库、线上环境)。
我自己在改工具层时最直观的体感是:工具少了以后,模型“犯错的种类”也少了。它还是会错,但错法更集中、更可诊断——通常就是命令拼错、路径不对、过滤条件写歪。你能修 prompt、能补菜单、能加护栏,不再是那种“它为什么突然改用另一个工具”式的玄学。
当然我也不敢说这对所有场景都更好:如果你的任务高度结构化、每次都要强校验字段,单 run 可能会让你在审计和合规上更难受。这个后面我会讲。
run 工具最小实现:把“工具目录”换成“命令菜单”
你可以把 run 想成“服务员”,模型只负责点菜,但菜单由你印。
一个周末可跑通的版本是:把允许执行的命令固定成 10 来条(按你的业务),每条命令都支持
--help,并且输出尽量是纯文本/JSON。菜单越短,越稳定;菜单越像“业务动作”,越安全。
建议你从这类命令开始(风险低、收益高):
📂 文件与文本
ls/find(只允许在工作目录内)cat/sed/awk/grepjq(JSON 处理神器)
🛠️ 诊断与调试
tail -n/head -n(限制行数)- 你的业务 CLI:例如
appctl logs、appctl status
🎯 外部请求(慎用)
curl(强制域名白名单 + 超时 + 最大响应大小)
这里有个我很喜欢的小技巧:你不需要真的放开系统命令。你可以做一个“代理 CLI”,比如叫 ops,把所有能力都做成子命令:ops logs ...、ops metrics ...、ops search ...。对模型来说依旧是 CLI,但对你来说更安全、更可控——你甚至可以把 ops 做成只读接口,默认拒绝写操作。
我不太确定的一点是:不同模型对“代理 CLI”这件事的学习速度差异挺大。有的模型很快就会先 ops --help 再行动,有的会执着于猜参数。我的经验是,越把输出规范成“短、稳定、可复核”,它越愿意按规矩来。
你照抄就能用的“单 run 工具”Prompt(可复制素材)
下面这段建议直接塞进 system / developer prompt 的工具使用约束。目标很简单:让模型先用 --help 探路,再执行;每次只做一件事;输出必须可复核。
text你只有一个工具:run(command)。
- 只能使用允许的命令:ls, find, cat, head, tail, grep, sed, awk, jq, ops, curl(仅白名单域名)。
- 不确定命令参数时,先运行:<命令> --help 或 ops <子命令> --help。
- 优先使用管道组合:| && || ; 但单次 run 的命令长度不要超过 300 字符。
- 所有读文件操作必须限制在 WORKDIR 下;禁止访问 /etc、~、以及任何环境变量回显。
- 每次 run 前先用一句话说明你要验证什么;run 后用一句话解释输出意味着什么。
- 如果输出不够判断,先缩小范围(head/tail/grep),不要把整文件吐出来。
这段 prompt 的收益很现实:它减少了模型瞎猜参数、乱读文件、无限扩散上下文的概率。更重要的是,你把“工具使用习惯”固定成流程:先探路,再执行;先小样本,再全量。
你会踩的 3 个坑(我建议你现在就规避)
坑 1:输出太大,把上下文撑爆
你一旦允许 cat big.log,模型就会把整条日志当“素材库”。解决办法很土:强制 head/tail,限制最大输出行数,比如 200 行。
坑 2:错误看不见,模型以为成功了
很多命令失败了只在 stderr 里吐一句。你的 run 返回最好带上:exit_code、stdout、stderr,并在 prompt 里要求模型先看 exit_code。
坑 3:curl 一开,数据就出去了
这玩意儿在安全上等价于“模型会发网请求”。如果你要用,至少做两层限制:域名白名单 + 最大响应大小 + 超时。能不用就不用。
那 function calling 什么时候反而更合适?
当你的操作天然是“强约束结构化输入”,function calling 往往更稳。
比如:
- “发短信”这种必须校验手机号、模板变量的场景
- “下单/退款”这种需要强审计字段的场景
- “写数据库”这种必须过 schema 校验和权限控制的场景
说白了:高风险写操作更适合 function calling;低风险读操作 + 诊断类任务更适合 run + CLI。
你也可以混用:默认只给 run,遇到高风险动作再给一个极少数的结构化函数(例如 request_approval(action, payload)),让模型必须“举手申请”。我见过效果最好的实现是:任何“写操作”都要先生成计划 + diff + 解释,然后走审批函数;而“读操作”基本全走 run。
常见问题
Q: 单一 run 工具是不是就等于“让 AI 执行 shell”,太危险了?
是的,所以必须配白名单 + 沙箱 + 输出限制。你不是把系统权限交给模型,而是给它一个可控的命令菜单,运行在隔离环境里。
Q: 我没有自己的 agent 框架,也能用这套思路吗?
能。你可以在任何支持“工具调用”的平台,把工具收敛成一个 run,背后执行的是你自己的容器/脚本服务,命令参数再做白名单校验。
Q: 我该从哪些任务开始试,最容易见效?
从“日志分析、文件检索、数据清洗”这类读操作开始。比如:统计错误次数、找某段配置出现在哪些文件、从 JSON 里用 jq 抽字段做报表。
如果你手上正好有一个“工具越来越多、错误越来越玄学”的 Agent,可以先做个很小的实验:挑一个高频读任务(比如日志统计),把它从多工具改成一次 run 管道,看看一周后的失败率和排障时间有没有明显变化。
我也很好奇:你现在的 Agent 最常“选错工具”的场景是什么?是文件操作、网络请求,还是那种看起来最简单、却最容易翻车的“查一条信息”?