为什么 Claude Code 选择了 shell,而不是 JSON Schema
- 作者:Bougie
- 创建于:2026-06-16
- 更新于:2026-06-16
我搞 shell 脚本从 2008 年开始。那时候刚从学校出来,第一份工作是维护公司里一堆 perl 和 shell 混着的旧系统。每天的工作就是:一翻代码,grep 一下,sed 一下,awk 一下,改两行,第二天再来一遍。搞过这事的人都知道,shell 写多了之后,看什么都想用管道接。你看到一串文本,第一反应就是 | 去。你看到要处理数据,第一反应就是写个 one-liner。
后来 JSON Schema 出来了。2019 年还是哪一年,反正大家都在吹。我记得第一次认真看那玩意儿的时候……
算了不记得了。总之过了几年,Function Calling 来了。OpenAI 2023 年推的。一下子所有做 AI 应用的人都在写 schema。你要让模型输出什么格式,你就得先定义一个 JSON Schema 出来。嵌套的类型、必填字段、enum 约束,一层套一层。我记得有一阵子我每天的任务就是写 schema。给工具写 schema,给返回结果写 schema,给子任务写 schema。代码里一半是业务逻辑,一半是 schema 定义。
然后有一天我需要做一个代码搜索的 Agent。就是那种:你说"帮我找到所有在 foo 函数里调用了 bar 的地方",Agent 要去搜代码库,返回结果,再决定下一步怎么做。你想啊,正常的思路是什么——你让 LLM 生成一条 shell 命令,比如 find 加 grep,然后把命令执行了,把结果扔回去,LLM 再决定要不要继续搜。这个流程自然得不能再自然了。

但是 Function Calling 来了之后呢?你得定义一个 tool schema 吧。这个 tool 要接受什么参数?文件路径?搜索词?要不要支持正则?你想得越多 schema 就越复杂。我当时真写了一个,用 Function Calling 来做这个 Agent。结果你猜怎么着——光 schema 就写了快 200 行。什么 oneOf,什么 dependencies,什么 if-then-else 全用上了。就为了描述"执行一条 shell 命令"这件事。最后跑起来,模型倒是能调用工具了,但是返回的数据格式对不对全靠猜,错误处理写到手软。
然后我看到 Claude Code 的实现方式。它直接让 LLM 生成 shell 命令,然后执行,然后把 stdout 扔回去。没有 schema。没有 Function Calling。就是最原始的 | 把程序的输出接成下一个程序的输入。
你知道我当时的感觉吗……
就是那种,绕了一大圈,终于回到原点的感觉。
# Claude Code 的核心循环
Claude Code 是 Anthropic 出的。它的核心设计思路就是:LLM 生成命令,shell 执行命令,结果返回 LLM。循环往复。搞过这事的人都知道,这套东西跑起来有多顺。你不需要告诉模型"这个函数要返回一个数组,数组里每个元素要有 name 和 type 字段"。你只需要说:ls -la 的输出长这样,grep 的输出长这样,awk '{print $1}' 的输出长这样。LLM 训练的时候见过太多 shell 输出了,它知道该怎么处理这些文本。

这不是因为 shell 古老所以好。
是因为 shell 天然就是为"把一个程序的输出接成下一个程序的输入"设计的。1973 年前后,Doug McIlroy 在贝尔实验室提了管道这个概念。他想要的就是一个机制:A 程序的输出直接变成 B 程序的输入,不需要中间文件,不需要临时变量,就那么一竖线,|,接上了。这就是 Unix 设计哲学的核心,也是 shell 一切的基础。
LLM 呢?你想啊,LLM 的本质是什么——读一段文本,生成一段文本。它不是读一个结构化对象,生成一个结构化对象。它是读文本,生成文本。
这两件事是同构的。
你把 shell 输出的文本扔给 LLM,LLM 读这段文本,生成下一段文本(可能是另一个 shell 命令),这个命令再通过 shell 执行,输出又变成文本……整个流程从头到尾都是文本。没有 schema。没有类型校验。没有"字段不存在"这种报错。就是文本,就是管道,就是 |。
# MCP 是另一回事
我不是说 Function Calling 没用。它在一些场景下确实好用。你需要严格结构化输出的时候,你需要让模型从几个固定选项里选一个的时候,Function Calling 很强。但是你要是想让 LLM 和真实世界交互——搜代码、改文件、跑测试、查日志——Function Calling 就开始露怯了。你得写一堆胶水代码把 schema 转来转去,最后发现还不如直接让模型生成一行 shell 命令。
Claude Code 用的 MCP 又是另一回事。Model Context Protocol,听名字就知道,它不是用来定义输出格式的,它是用来扩展 LLM 能访问的工具的。你可以在 MCP 里定义一个工具,这个工具可以是任何东西——一个数据库查询,一个 API 调用,一个文件系统操作。关键在于,工具的执行结果是通过 stdout 返回的。stdout 是文本。LLM 读文本。管道继续接。
……
别跟我扯什么"这才是正确的方式"或者"未来的方向"。我见过太多这种话了。2008 年的时候有人跟我说 perl 是正确的方式,后来有人说 ruby 是正确的方式,再后来有人说 node 是正确的方式。每个时代都有正确的语言。
但是 shell 活到了现在。Claude Code 选它不是因为 Anthropic 的工程师怀旧,是因为它能跑。

# 透明性 vs 黑盒
一翻代码你就能看到,Claude Code 的核心循环简单得可怕。模型生成命令,命令执行,结果返回,循环。这个循环能成立的前提是:模型的输入输出和 shell 的输入输出是一回事。你不需要翻译层。你不需要适配器。直接接上就能跑。
这才是它好用的原因。不是因为古老。是因为它的设计——输入输出都是文本,工具通过 stdout 返回结果——天然适配 LLM 的工作方式。
你说 LLM 能不能学会处理复杂的 JSON Schema?当然能。你给它足够的 examples,它能生成符合 schema 的 JSON。你甚至可以用 few-shot 让它输出得准一点。但是这个过程是有损耗的。Schema 是给人看的,LLM 处理它需要额外的推理。而且 schema 一变,你的 few-shot examples 可能全得重调。
Shell 不一样。Shell 的输出格式是约定俗成的。ls 输出什么格式,grep 输出什么格式,awk 能把输出变成什么格式,这些东西 50 年没大变过。LLM 训练的时候见过足够多的例子,它知道怎么解析这些输出。你不需要给它看任何 examples。直接就能用。
我想过一个问题:如果当年 Doug McIlroy 设计 Unix 的时候,不是用文本流作为管道的内容,而是用某种结构化数据格式,shell 会不会变成另一个样子?大概会。然后今天我们可能就不会在这里聊 Claude Code 了。LLM 读结构化数据还是差点意思,读文本是它的本能。
现在我每天用 Claude Code 写代码。有时候它生成的命令莫名其妙,但是执行一下看看输出,它马上就能调整。这种来回折腾的过程……搞过这事的人都知道,跟调试自己写的脚本没什么两样。区别在于这次帮你写初始版本的是个 LLM,不是你自己。
倦了。但是能跑。
就这样。