Reddit——互联网的心脏 跳至主要内容
打开菜单 打开导航 前往 Reddit 首页
r/LocalLLaMA
获取 App 获取 Reddit 应用 登录登录 Reddit
展开用户菜单 打开设置菜单
我曾是 Manus 的后端负责人。做了两年智能体后,我彻底不再使用函数调用。以下是我改用的方案。
英语不是我的母语。我先用中文写下这篇文章,再借助 AI 翻译。文字可能带点 AI 味道,但这些设计决策、生产事故,以及将它们沉淀为原则的思考——都出自我手。
在 Meta 收购之前,我曾在 Manus 担任后端负责人。过去两年里,我一直在构建 AI 智能体——先在 Manus,随后在我自己开源的智能体运行时(Pinix)和智能体(agent-clip)上继续。一路走来,我得出了一个连我自己都意外的结论:
一个采用类 Unix 命令风格的 run(command=...) 工具,胜过一整套带类型的函数调用目录。
以下是我的心得。
为什么选择 *nix
50 年前,Unix 做了一个设计选择:一切都是文本流。程序不交换复杂的二进制结构,也不共享内存对象——它们通过文本管道通信。小工具各司其职,通过 | 组合成强大的工作流。程序用 --help 自述,用退出码报告成功或失败,用 stderr 传递错误信息。
50 年后,LLM 做出了几乎同样的选择:一切都是 token。它们只理解文本,也只产生文本。它们的思考是文本,行动是文本,从世界获得的反馈也必须是文本。
这两个相隔半个世纪、出发点完全不同的决定,最终汇合到同一种接口模型上。Unix 为人类终端操作员设计的文本系统——cat、grep、pipe、退出码、man pages——不仅能被 LLM 使用,而且天然契合。就工具使用而言,LLM 本质上就是一个终端操作员——只是比任何人都快,并且在训练数据里早已见过海量 shell 命令与 CLI 模式。
这就是 *nix Agent 的核心哲学:不要发明新的工具接口。把 Unix 50 年证明有效的东西,直接交给 LLM。
为什么只用一个 run
单工具假设
大多数智能体框架会给 LLM 一份彼此独立的工具目录:
tools: [search_web, read_file, write_file, run_code, send_email, ...]
每一次调用之前,LLM 都必须做一次工具选择——选哪个?参数怎么填?工具越多,选择越难,准确率就越低。认知负担消耗在“用哪个工具?”上,而不是“我要完成什么?”上。
我的做法:只提供一个 run(command=...) 工具,把所有能力都以 CLI 命令的方式暴露出来。
run(command=cat notes.md)
run(command=cat log.txt | grep ERROR | wc -l)
run(command=see screenshot.png)
run(command=memory search deployment issue)
run(command=clip sandbox bash python3 analyze.py)
LLM 仍然需要选择用什么命令,但这与在 15 个不同 schema 的工具之间做选择,本质不同。命令选择是同一命名空间内的字符串组合;函数选择则是不同 API 之间的上下文切换。
LLM 本就会说 CLI
为什么 CLI 命令比结构化函数调用更适配 LLM?
因为 CLI 是 LLM 训练数据里最密集的工具使用模式。GitHub 上数十亿行文本里充满了:
# README install instructions
pip install -r requirements.txt python main.py
# CI/CD build scripts
make build make test make deploy
# Stack Overflow solutions
cat /var/log/syslog | grep Out of memory | tail -20
我不需要教 LLM 怎么用 CLI——它早就会。这种熟悉程度取决于模型与概率分布,但在实践里,它在主流模型上的稳定性出奇地好。
对比同一个任务的两种做法:
Task: Read a log file, count the error lines
Function-calling approach (3 tool calls):
1. read_file(path=/var/log/app.log) → returns entire file
2. search_text(text=entire file, pattern=ERROR) → returns matching lines
3. count_lines(text=matched lines) → returns number
CLI approach (1 tool call):
run(command=cat /var/log/app.log | grep ERROR | wc -l)
→ 42
一次调用顶三次。不是因为什么特殊优化——而是因为 Unix 管道天然支持组合。
让管道与链式执行真正可用
仅有一个 run 还不够。如果 run 一次只能执行一个命令,那么组合任务仍然需要多次调用。于是我在命令路由层做了一个 chain parser(parseChain),支持四个 Unix 运算符:
| Pipe: stdout of previous command becomes stdin of next
And: execute next only if previous succeeded
|| Or: execute next only if previous failed
; Seq: execute next regardless of previous result
有了这个机制,每一次工具调用都可以是一整段完整工作流:
# One tool call: download → inspect
curl -sL $URL -o data.csv cat data.csv | head 5
# One tool call: read → filter → sort → top 10
cat access.log | grep 500 | sort | head 10
# One tool call: try A, fall back to B
cat config.yaml || echo config not found, using defaults
N 个命令 × 4 个运算符——组合空间会急剧增大。而对 LLM 来说,这不过是一条它本来就会写的字符串。
命令行就是 LLM 的母语工具接口。
启发式设计:让 CLI 引导智能体
单工具 + CLI 解决了“用什么”。但智能体仍然需要知道“怎么用”。它不能 Google,不能问同事。我用三种渐进式的设计技巧,让 CLI 自己成为智能体的导航系统。
技巧 1:渐进式的 --help 发现
一个设计良好的 CLI 工具,不需要读文档——因为 --help 会告诉你一切。我把同样的原则应用到智能体上,并将其组织为渐进式披露:智能体不需要一次性加载全部文档,而是在深入时按需发现细节。
Level 0:工具描述 → 注入命令列表
run 工具的描述会在每次对话开始时动态生成,列出所有已注册命令及其一行摘要:
Available commands:
cat — Read a text file. For images use see. For binary use cat -b.
see — View an image (auto-attaches to vision)
ls — List files in current topic
write — Write file. Usage: write path [content] or stdin
grep — Filter lines matching a pattern (supports -i, -v, -c)
memory — Search or manage memory
clip — Operate external environments (sandboxes, services)
...
智能体从第一轮就知道有什么可用,但它不需要每个命令的每个参数——那会浪费上下文。
注:这里有个开放的设计问题:是注入完整命令列表,还是按需发现?随着命令变多,列表本身就会消耗上下文预算。我仍在探索合适的平衡点,欢迎建议。
Level 1:command(无参数)→ 用法
当智能体对某个命令感兴趣时,它就直接调用它。不带参数?命令返回自己的用法:
→ run(command=memory)
[error] memory: usage: memory search|recent|store|facts|forget
→ run(command=clip)
clip list — list available clips
clip name — show clip details and commands
clip name command [args...] — invoke a command
clip name pull remote-path [name] — pull file from clip to local
clip name push local-path remote — push local file to clip
现在智能体知道 memory 有五个子命令,clip 支持 list/pull/push。一条调用,零噪声。
Level 2:command subcommand(缺参)→ 具体参数
智能体决定用 memory search,但不确定格式?就继续向下钻:
→ run(command=memory search)
[error] memory: usage: memory search query [-t topic_id] [-k keyword]
→ run(command=clip sandbox)
Clip: sandbox
Commands:
clip sandbox bash script
clip sandbox read path
clip sandbox write path
File transfer:
clip sandbox pull remote-path [local-name]
clip sandbox push local-path remote-path
渐进式披露:概览(注入)→ 用法(探索)→ 参数(钻取)。智能体按需发现,每一层都只提供下一步所需的信息。
这与把 3,000 字的工具文档硬塞进系统提示词截然不同。那些信息大多数时候都无关——纯粹浪费上下文。渐进式 help 让智能体自己决定何时需要更多信息。
这也对命令设计提出了要求:每个命令和子命令必须有完整的 help 输出。它不只是给人看的——更是给智能体看的。一条好的 help 信息意味着一次就命中;缺失则意味着盲猜。
技巧 2:把错误信息当作导航
智能体一定会犯错。关键不在于杜绝错误——而在于让每个错误都指向正确方向。
传统 CLI 的错误是为能 Google 的人设计的。智能体不能 Google。所以我要求每个错误都必须同时包含“哪里错了”和“该怎么做”:
Traditional CLI:
$ cat photo.png
cat: binary file (standard output)
→ Human Googles how to view image in terminal
My design:
[error] cat: binary image file (182KB). Use: see photo.png
→ Agent calls see directly, one-step correction
更多例子:
[error] unknown command: foo
Available: cat, ls, see, write, grep, memory, clip, ...
→ Agent immediately knows what commands exist
[error] not an image file: data.csv (use cat to read text files)
→ Agent switches from see to cat
[error] clip sandbox not found. Use clip list to see available clips
→ Agent knows to list clips first
技巧 1(help)解决“我能做什么?”技巧 2(错误)解决“那我应该改做什么?”二者结合,智能体的恢复成本很低——通常 1–2 步就能回到正确路径。
真实案例:静默 stderr 的代价
有一段时间,我的代码在调用外部 sandbox 时会悄悄丢掉 stderr——只要 stdout 非空,就会丢弃 stderr。智能体运行 pip install pymupdf,得到退出码 127。stderr 里明明有 bash: pip: command not found,但智能体看不到。它只知道失败,却不知道原因,于是开始盲猜 10 种不同的包管理器:
pip install → 127 (doesnt exist)
python3 -m pip → 1 (module not found)
uv pip install → 1 (wrong usage)
pip3 install → 127
sudo apt install → 127
... 5 more attempts ...
uv run --with pymupdf python3 script.py → 0 ✓ (10th try)
10 次调用,每次推理大约 5 秒。如果第一次就能看到 stderr,一次调用就够了。
当命令失败时,stderr 恰恰是智能体最需要的信息。永远不要丢。
技巧 3:一致的输出格式
前两种技巧解决了发现与纠错。第三个技巧让智能体能随着时间推移越来越擅长使用系统。
我会在每次工具结果后追加一致的元信息:
file1.txt
file2.txt
dir1/
[exit:0 | 12ms]
LLM 会抽取两类信号:
退出码(Unix 约定,LLM 本来就懂):
- exit:0 — 成功
- exit:1 — 一般错误
- exit:127 — 未找到命令
耗时(成本感知):
- 12ms — 便宜,可频繁调用
- 3.2s — 中等
- 45s — 昂贵,应谨慎使用
当在一次对话里看过几十次 [exit:N | Xs] 之后,智能体会内化这个模式。它开始提前预期——看到 exit:1 就知道要检查错误;看到耗时很长就会减少调用。
一致的输出格式会让智能体随着时间变聪明。不一致会让每次调用都像第一次。
三种技巧形成一条递进链:
--help → What can I do? → Proactive discovery
Error Msg → What should I do? → Reactive correction
Output Fmt → How did it go? → Continuous learning
双层架构:把启发式设计工程化
上面的部分描述了 CLI 如何在语义层面引导智能体。但要在实践中跑通,还存在一个工程问题:命令的原始输出与 LLM 需要看到的内容,往往完全不同。
LLM 的两个硬约束
约束 A:上下文窗口有限且昂贵。每个 token 都要花钱、占注意力、拖慢推理速度。把一个 10MB 文件塞进上下文不仅浪费预算——还会把更早的对话挤出窗口,智能体会忘记。
约束 B:LLM 只能处理文本。二进制数据通过 tokenizer 会产生高熵的无意义 token。它不仅浪费上下文——还会干扰周围正常 token 的注意力分配,降低推理质量。
这两个约束意味着:命令的原始输出不能直接送给 LLM——它需要一个展示层进行处理。但这种处理不能影响命令执行逻辑——否则 pipe 会被破坏。因此必须分层。
执行层 vs. 展示层
┌─────────────────────────────────────────────┐
│ Layer 2: LLM Presentation Layer │ ← Designed for LLM constraints
│ Binary guard | Truncation+overflow | Meta │
├─────────────────────────────────────────────┤
│ Layer 1: Unix Execution Layer │ ← Pure Unix semantics
│ Command routing | pipe | chain | exit code │
└─────────────────────────────────────────────┘
当 cat bigfile.txt | grep error | head 10 执行时:
Inside Layer 1:
cat output → [500KB raw text] → grep input
grep output → [matching lines] → head input
head output → [first 10 lines]
如果你在 Layer 1 里截断 cat 的输出 → grep 只会搜索前 200 行,结果不完整。
如果你在 Layer 1 里加上 [exit:0] → 它会作为数据流进 grep,成为搜索目标。
所以 Layer 1 必须保持原始、无损、无元数据。所有处理只发生在 Layer 2——在 pipe 链完成之后、最终结果即将返回给 LLM 之前。
Layer 1 服务于 Unix 语义。Layer 2 服务于 LLM 认知。二者分离不是偏好——而是逻辑必然。
Layer 2 的四个机制
机制 A:二进制防护(对应约束 B)
在把任何内容返回给 LLM 之前,先判断它是否为文本:
Null byte detected → binary
UTF-8 validation failed → binary
Control character ratio 10% → binary
If image: [error] binary image (182KB). Use: see photo.png
If other: [error] binary file (1.2MB). Use: cat -b file.bin
LLM 永远不会收到它无法处理的数据。
机制 B:溢出模式(对应约束 A)
Output 200 lines or 50KB?
→ Truncate to first 200 lines (rune-safe, wont split UTF-8)
→ Write full output to /tmp/cmd-output/cmd-{n}.txt
→ Return to LLM:
[first 200 lines]
--- output truncated (5000 lines, 245.3KB) ---
Full output: /tmp/cmd-output/cmd-3.txt
Explore: cat /tmp/cmd-output/cmd-3.txt | grep pattern
cat /tmp/cmd-output/cmd-3.txt | tail 100
[exit:0 | 1.2s]
关键洞察:LLM 本来就会用 grep、head、tail 在文件里导航。溢出模式把“大数据探索”变成 LLM 已经掌握的技能。
机制 C:元数据页脚
actual output here
[exit:0 | 1.2s]
退出码 + 耗时,作为 Layer 2 的最后一行追加。既能给智能体提供成功/失败与成本信号,又不会污染 Layer 1 的 pipe 数据。
机制 D:stderr 附带
When command fails with stderr:
output + \n[stderr] + stderr
Ensures the agent can see why something failed, preventing blind retries.
经验教训:来自生产环境的故事
故事 1:一张 PNG 引发的 20 轮疯狂挣扎
用户上传了一张架构图。智能体用 cat 读取,收到了 182KB 的 PNG 原始字节。LLM 的 tokenizer 把这些字节变成了成千上万的无意义 token,硬塞进上下文。LLM 无法理解,于是开始尝试各种读取方式——cat -f、cat --format、cat --type image——每次都得到同样的垃圾。20 轮之后,进程被强制终止。
根因:cat 没有二进制检测,Layer 2 也没有防护。修复:增加 isBinary() 防护 + 错误引导 Use: see photo.png。教训:工具结果就是智能体的眼睛。返回垃圾 = 智能体变瞎。
故事 2:静默 stderr 与 10 次盲目重试
智能体需要读取一个 PDF。它尝试 pip install pymupdf,得到退出码 127。stderr 里有 bash: pip: command not found,但代码把它丢掉了——因为 stdout 有内容,而逻辑是“只要 stdout 存在就忽略 stderr”。
智能体只知道失败,却不知道原因。接下来就是漫长的试错:
pip install → 127 (doesnt exist)
python3 -m pip → 1 (module not found)
uv pip install → 1 (wrong usage)
pip3 install → 127
sudo apt install → 127
... 5 more attempts ...
uv run --with pymupdf python3 script.py → 0 ✓
10 次调用,每次推理约 5 秒。若第一次就能看到 stderr,一次调用足矣。
根因:InvokeClip 在 stdout 非空时静默丢弃 stderr。修复:失败时始终附带 stderr。教训:当命令失败时,stderr 正是智能体最需要的信息。
故事 3:溢出模式的价值
智能体分析一个 5,000 行的日志文件。如果不截断,全文(约 200KB)会被塞进上下文。LLM 注意力被淹没,回复质量急剧下降,早前对话也会被挤出上下文窗口。
启用溢出模式后:
[first 200 lines of log content]
--- output truncated (5000 lines, 198.5KB) ---
Full output: /tmp/cmd-output/cmd-3.txt
Explore: cat /tmp/cmd-output/cmd-3.txt | grep pattern
cat /tmp/cmd-output/cmd-3.txt | tail 100
[exit:0 | 45ms]
智能体先看到前 200 行,理解文件结构,然后用 grep 精准定位问题——总共 3 次调用,上下文不足 2KB。
教训:给智能体一张地图,远比把整片领土塞给它更有效。
边界与限制
CLI 并非银弹。在以下场景,带类型的 API 可能更合适:
- 强类型交互:数据库查询、GraphQL API 等需要结构化输入/输出的场景。schema 校验比字符串解析更可靠。
- 高安全需求:CLI 的字符串拼接天然存在注入风险。在不可信输入场景里,带类型参数更安全。agent-clip 通过 sandbox 隔离来缓解这一点。
- 原生多模态:纯音频/视频处理等二进制流场景,CLI 的文本管道会成为瓶颈。
此外,“不设迭代次数上限”不等于“没有安全边界”。安全由外部机制保证:
- Sandbox 隔离:命令在 BoxLite 容器内执行,不可能逃逸
- API 预算:LLM 调用有账户级的花费上限
- 用户取消:前端提供取消按钮,后端支持优雅终止
把 Unix 哲学交给执行层,把 LLM 的认知约束交给展示层,再用 help、错误信息、输出格式这三种渐进式启发式导航技术串起来。
CLI 就是智能体所需的一切。
源码(Go):github.com/epiral/agent-clip
核心文件:internal/tools.go(命令路由)、internal/chain.go(管道)、internal/loop.go(双层智能体循环)、internal/fs.go(二进制防护)、internal/clip.go(stderr 处理)、internal/browser.go(视觉自动附带)、internal/memory.go(语义记忆)。
也欢迎交流——尤其是如果你尝试过类似方法,或发现了 CLI 失效的边界案例。命令发现问题(注入多少 vs. 让智能体自己发现)也是我仍在积极探索的方向。

