02 -- Bash 工具:一切能力的基础
一个反直觉的事实
20 行核心代码,1 个 bash 工具------读文件、写文件、搜索、执行脚本。一个"工具"撑起了 agent 与真实环境交互的全部能力。
你可能的第一反应是"不够用吧?读文件要 read_file,写文件要 write_file,搜索要 search......每种能力对应一个工具才合理"。
恰恰相反。bash 不是"一个工具",它是所有工具的超集。
| 能力 | bash 命令 |
|---|---|
| 读文件 | cat, head, tail |
| 写文件 | echo '...' > file, sed -i |
| 搜索 | grep, find, rg |
| 执行 | python, make, npm test |
| 系统操作 | git, curl, docker |
Bash 是 meta 工具,是通往整个 Unix 世界的入口。
核心命题:为什么 Bash 是 meta 接口
Unix 哲学的两个基石:一切皆文件,一切皆可管道。 Bash 是这个哲学的用户态入口。
操作系统已经把读写文件、搜索文本、管理进程、网络通信实现了一遍。每种能力都有对应的命令行工具,通过 stdin/stdout/stderr 通信。Bash 是调用它们的统一界面。
给模型一个 bash,等于给它整个操作系统。
传统 agent 框架为每种能力单独写工具函数------这是在应用层重新发明操作系统。read_file 做的事和 cat 一样,只是换了个接口。search 做的事和 grep 一样,只是加了层参数校验。你在用高级语言重写 Unix,而 Unix 本身就在那里。
最小实现:一个函数的全部
function execute(command):
result = shell(command)
return {
exitCode: result.code,
stdout: result.stdout,
stderr: result.stderr
}
三个返回值,没有异常,没有特殊分支。
整个函数只有一个设计决策,但这个决策至关重要。
不抛异常:失败是信息,不是错误
直觉做法是命令失败就抛异常。但对 agent 来说,失败本身就是最有价值的反馈。
exitCode: 1
stdout: 3 passed, 1 failed
stderr: FAIL src/utils.test > parseConfig > should handle empty input
Expected: {}
Received: undefined
哪个文件、哪个用例、期望值、实际值------全在 stderr 里。抛异常?模型只知道"调用失败",诊断信息全丢。结构化返回让模型拿到完整现场,自己读源码、修复、重跑。
同理:
command not found------ 工具未安装,换一个命令Permission denied------ 权限不够,调整策略No such file or directory------ 路径错了,先find再重试
exit code + stdout + stderr 是 agent 感知真实环境的全部通道。 模型不能直接看到文件系统、进程表、网络状态------它只能执行命令、阅读输出来理解世界。通道里的信息越完整,决策越准确。
从一个到多个:专用工具的演进逻辑
既然 bash 什么都能做,为什么还要 read_file、write_file、grep?
答案不是"bash 不够"。而是专用工具能约束行为、精确输出。
约束即引导
Bash 太自由。能执行任何命令,也意味着能执行任何不该执行的命令。专用工具通过参数 schema 收窄可能性:
// bash: 模型可以传入任意字符串
bash({ command: "rm -rf / --no-preserve-root" })
// read_file: 只能传路径和行范围
read_file({ file_path: "/src/main.py", offset: 0, limit: 100 })
read_file 的参数 schema 从结构上排除了"读文件"之外的一切操作。不是靠模型自觉,而是接口本身决定了它只能做对的事。
输出格式可控
cat file.txt 输出纯文本,没有行号------模型要定位第 42 行,得自己数。专用 read_file 直接返回带行号的格式化输出:
1 import sys
2 import os
3
4 def main():
5 print("hello")
grep pattern . 的输出格式取决于 grep 版本和参数。专用 grep 工具返回结构化数据------按文件分组、带行号、支持上下文行数------模型不用猜格式,直接用。
成本优化
同一个操作,bash 和专用工具消耗的 token 不同:
// bash
{ "name": "bash", "input": { "command": "cat -n src/main.py | head -100" } }
// read_file
{ "name": "read_file", "input": { "file_path": "src/main.py", "limit": 100 } }
单次差别不大。累计数千次调用,更短的参数、更规范的输出,省下的 token 可观。
演进路线图
阶段 1: 只有 bash
↓ "能用,但不够可控"
阶段 2: bash + read_file + write_file
↓ "读写有约束了,搜索还是靠 bash"
阶段 3: bash + read_file + write_file + grep + glob
↓ "常用操作全有专用工具,bash 兜底"
阶段 4: bash 只处理专用工具覆盖不到的场景
(运行测试、安装依赖、启动服务...)
每一步演进的动机都一样:不是"bash 做不到",而是"专用工具做得更好"。
但 bash 始终保留。 你永远无法穷举模型需要什么------npm install、docker build、git log、curl------长尾操作无限多,bash 是那个兜底的万能出口。
小结
语言模型只能生成文本。给它一个 bash,它就能感知和操作真实世界。
这就是 bash 工具的本质------不是一个功能,而是一次身份转换。从"语言系统"到"agent",中间只隔一个 shell 调用。读文件、写文件、搜索、执行,全部从这里长出来。后续所有专用工具,都是在这个地基上的精装修。
但现在有一个问题:bash 只能执行一次。发一条命令,收一个结果,然后呢?真正的任务需要多步操作------查结构、读代码、修改、跑测试、失败了再改。这需要一个闭环:执行、观察、决策、再执行。