从互联网早期设计到 AI Agent
从互联网早期设计到 AI Agent
互联网早期的工程传统,塑造了一套可复用的“构建工具的方法论”:工具应当职责明确、可组合、可观察。AI Agent(以 Claude Code 为代表)并非脱离历史的全新范式,而是在这些传统之上,把“文本输入”升级为“意图输入”,把“输出文本”升级为“执行动作”。
阅读导引
- 叙事主线:Unix / CLI 传统与 REPL 的演化 → 「状态」的本质与 无状态 思想的历史脉络 → 无状态设计的收益与现实权衡 → 从聊天机器人到 Agent → 回扣开篇关于 grep / 无索引 的争议,形成闭环。
- 阅读方式:各章末均有 小结;第 5–6 章对应「为何有效」与「何时妥协」;第 8 章集中回应引言中的质疑。
核心命题(可先扫此处再读正文)
- 工具观:单一职责、组合性、透明性——在 Agent 时代体现为清晰的工具边界与可审查的调用链。
- 状态观:无状态不是“不保存任何数据”,而是审慎选择状态的存放位置与生命周期;错误的不是状态本身,而是失控的状态。
- 产品观:Agentic search(glob / grep) 与向量索引并非简单的“先进 / 倒退”,而是场景、确定性、隐私与运维成本之间的不同取舍。
章节目录
| 章 | 标题 |
|---|---|
| 0 | 引言:一个看似倒退的选择 |
| 1 | Unix 哲学与 CLI 的传统 |
| 2 | REPL 的演化史 |
| 3 | 理解状态的本质 |
| 4 | 无状态思想的历史脉络 |
| 5 | 无状态设计的优势 |
| 6 | 现实的权衡 |
| 7 | 从聊天机器人到 Agent |
| 8 | AI 时代的新思考 |
| 附录 | RAG(向量索引) |
0 引言:一个看似倒退的选择
最近,Claude Code 的技术选择引发了不少讨论。
有观点认为,Claude Code 与 Gemini 放弃代码索引是“一步烂棋”。Milvus 的技术博客更是直言不讳:
“Claude Code 的 grep-only 方式会烧掉太多 tokens”。
在 HackerNews 的讨论中,有开发者质疑:
“Claude 用 grep,Cursor 用向量搜索——我们是在技术倒退吗?”
当主流 AI 编程助手纷纷采用向量索引实现语义搜索时,Claude Code 却选择了 grep——这个诞生于 1973 年的命令行工具。它不建立持久的代码索引,不预测编码意图,每次搜索都是实时执行。
根据 Anthropic 团队在 5 月份的博客访谈《Claude Code: Anthropic’s Agent in Your Terminal》,他们测试了 RAG(向量索引)等多种方案后,最终选择了 “agentic search”——就是使用 glob、grep 这些常规的代码搜索。令人意外的是,这种方式“在性能上大幅超越了所有其他方案”。
这种取舍并非一时兴起,而是一条贯穿计算机科学 50 年的设计哲学,从 Unix 管道到 REST API,从 MapReduce 到 Serverless,无状态(Stateless)设计在计算机历史上一次次证明了它的价值:通过放弃复杂的状态管理,系统获得了更好的可组合性、可靠性和可扩展性。
本文将探讨“无状态”这个设计理念——它不是简单的“不保存数据”,而是关于如何明智地选择在哪里、以什么方式管理必要的状态。理解了这个设计哲学,你就会明白 Claude Code 的选择背后的深层逻辑。
每个计算时代都在重新发现同一个道理:有时候,遗忘比记忆更强大。
1 Unix 哲学与 CLI 的传统
Write programs that do one thing and do it well.
Write programs that work together.
Write programs that handle text streams.
— Doug McIlroy(Unix 管道的发明者)
1.1 Unix 哲学:单一职责、组合性、透明性
1969 年,Ken Thompson 和 Dennis Ritchie 在贝尔实验室创造了 Unix。它不只是一个操作系统,更是一套影响深远的设计哲学。Unix 哲学的核心是三条原则:
- 单一职责:每个程序只做一件事,并把它做好
- 组合性:程序之间通过标准接口(文本流)协作
- 透明性:程序的行为可预测、可观察
这些原则在 50 年后的 Claude Code 中依然清晰可见。
1.2 管道:最早的“工具调用”
Unix 管道(|)把复杂任务拆成可组合步骤:前一个程序的输出成为后一个程序的输入。
history | awk '{print $2}' | sort | uniq -c | sort -rn | head -10
执行链路如下:
history输出命令历史记录awk提取第二列(命令名)sort排序uniq -c统计重复次数sort -rn按数字倒序排列head -10取前 10 条
每个程序只做一件事,通过“管道”组合完成目标。Claude Code 的工具链也具有相同结构:用户意图会被拆解为一串小工具的调用与组合。
1.3 文件系统:统一的抽象
Unix 的另一个伟大设计是“一切皆文件”。网络连接、设备、进程信息等,都通过文件系统接口访问。统一抽象的价值在于:用一致的接口覆盖多种能力,从而降低认知负担。
Claude Code 继承了这个思想:无论是读取本地文件、调用 MCP 服务器,还是执行远程任务,都通过同一套工具调用机制完成。
1.4 标准输入输出:接口即契约
Unix 程序通过 stdin / stdout / stderr 通信,其价值在于解耦与可组合:程序不需要关心数据来源与去向,只需遵守输入输出契约。
Claude Code 的工具系统同样强调“契约”,以结构化输入与统一输出保证可组合性:
type Tool = {
name: string
description: string
inputSchema: ToolInputJSONSchema
execute(input: I, context: ToolUseContext): Promise<ToolResult<O>>
}
1.5 CLI 的黄金时代:为什么 CLI 适合 Agent
从 1970 年代到 2000 年代,CLI 工具长期统治软件世界(vim、emacs、make、grep、sed、awk 等)。它们之所以经久不衰,是因为 CLI 天生具备几类对 Agent 关键的工程属性:
- 可脚本化:天然可被编排、可自动化调用
- 可组合:管道与重定向让工具之间具备低摩擦协作方式
- 低开销:不依赖 GUI 生态,部署与运行门槛低
- 可远程:通过 SSH 等方式在任何地方工作
Claude Code 选择 CLI 作为主要界面,不是因为 GUI 不好,而是因为这些特性对“可执行的智能体”至关重要。
1.6 make:最早的“任务编排”
make 用依赖图描述任务,只重新构建需要更新的部分。这种“依赖 + 状态 + 增量执行”的思想,也出现在现代 Agent 的任务编排之中。
app: main.o utils.o
gcc -o app main.o utils.o
main.o: main.c
gcc -c main.c
utils.o: utils.c
gcc -c utils.c
1.7 从 CLI 到 TUI:终端的进化
随着终端能力增强,出现了 TUI(Terminal User Interface):在终端中渲染更复杂的交互界面。Claude Code 使用 Ink(React for CLI)构建 TUI 界面,带来组件化、声明式渲染与状态驱动的开发体验。
function App() {
return (
<Box flexDirection="column">
<Header />
<MessageList messages={messages} />
<InputBox onSubmit={handleSubmit} />
</Box>
)
}
1.8 Unix 哲学在 Claude Code 中的体现
| Unix 原则 | Claude Code 的实现 |
|---|---|
| 单一职责 | 每个工具只做一件事(例如 GrepTool 负责搜索,FileEditTool 负责编辑) |
| 组合性 | 工具可以链式调用,前一个输出可作为下一个输入 |
| 透明性 | 每次工具调用都可展示给用户,支持审查与中断 |
| 文本流 | 工具通过结构化文本(如 JSON)进行数据交换 |
| 可脚本化 | 支持非交互模式,可被脚本或流水线调用 |
| 管道 | 支持 stdin 输入(例如 cat file | claude "分析这个") |
1.9 小结
Unix 的三条原则(单一职责、组合性、透明性)本质上是在约束“工具应该怎样被构建”。Claude Code 站在这套传统之上,把原则延伸到 AI Agent:用明确的工具边界保证可控,用组合能力放大效率,用透明可观察保障可靠性。
2 REPL 的演化史
REPL(Read–Eval–Print Loop)是最小化的交互式编程环境。它持续降低“从意图到执行”的门槛,而 Claude Code 将这条演化路径推进到了“行动可验证”的阶段。
“The read-eval-print loop is the simplest possible interactive programming environment.”
2.1 什么是 REPL
REPL 是 Read–Eval–Print Loop 的缩写:
- Read:读取用户输入
- Eval:执行或求值
- Print:打印结果
- Loop:回到第一步
这是最简单的交互式编程环境。你输入一行代码,立刻看到结果。
$ python
>>> 1 + 1
2
>>> "hello".upper()
'HELLO'
>>> [x**2 for x in range(5)]
[0, 1, 4, 9, 16]
2.2 REPL 的起源:Lisp(1958)
REPL 的概念来自 Lisp,由 John McCarthy 在 1958 年发明。
Lisp 的 REPL 是革命性的,因为它打破了"编写-编译-运行"的传统循环,让程序员可以即时探索。
> (+ 1 2)
3
> (defun square (x) (* x x))
SQUARE
> (square 5)
25
这个"即时反馈"的思想,是所有现代交互式工具的基础。
2.3 Shell:操作系统的 REPL
Unix Shell(sh、bash、zsh)把操作系统能力暴露为一套可交互、可编排的命令环境:
$ ls -la
$ cd src
$ grep -r "TODO" .
$ git status
Shell 的 REPL 循环:
- Read:读取命令行输入
- Eval:解析命令,fork 子进程执行
- Print:显示输出
- Loop:显示新的提示符
Shell 的伟大之处在于它把操作系统的能力暴露给了用户,通过一个简单的文本界面。
2.4 Node.js REPL:JavaScript 的即时环境
2009 年,Node.js 带来了服务端 JavaScript,也带来了 Node.js REPL:
$ node
> const arr = [1, 2, 3, 4, 5]
undefined
> arr.filter(x => x % 2 === 0)
[ 2, 4 ]
> arr.reduce((sum, x) => sum + x, 0)
15
Node.js REPL 的特点:
- 支持多行输入
- 自动补全
- 历史记录
- 可以 require 模块
2.5 IPython/Jupyter:科学计算的 REPL
2001 年,Fernando Pérez 创建了 IPython,后来演化为 Jupyter Notebook。
Jupyter 把 REPL 的概念推向了新高度:
# Cell 1
import pandas as pd
df = pd.read_csv('data.csv')
df.head()
# 立刻显示表格
# Cell 2
df.describe()
# 立刻显示统计信息
# Cell 3
df.plot(kind='bar')
# 立刻显示图表
Jupyter 的创新:
- Cell 模型:代码分块执行,每块有独立输出
- 富媒体输出:不只是文本,还有图表、表格、HTML
- 持久状态:变量在 cells 之间共享
- 叙事性:代码和文档混合
Claude Code 支持 NotebookEditTool,可以直接编辑 Jupyter Notebook,正是因为 Notebook 已经成为数据科学工作流的核心。
2.6 ChatGPT:对话即 REPL
2022 年,ChatGPT 的出现把 REPL 的概念带到了自然语言层面:
用户:帮我写一个快速排序
ChatGPT:[给出代码]
用户:改成支持自定义比较函数
ChatGPT:[修改代码]
用户:加上单元测试
ChatGPT:[添加测试]
这是一个新型的 REPL:
- Read:读取自然语言输入
- Eval:LLM 理解并生成响应
- Print:输出文本/代码
- Loop:继续对话
但 ChatGPT 的 REPL 有一个根本限制:它只能生成文本,不能执行动作。
2.7 Claude Code:行动即 REPL
Claude Code 将自然语言 REPL 与工具执行结合,使“评估”不仅发生在模型内部,也发生在真实系统之中:
用户:帮我找出项目里所有未使用的导入并删除
Claude Code:
→ GlobTool: 找到所有 .ts 文件
→ FileReadTool: 读取每个文件
→ 分析未使用的导入
→ FileEditTool: 删除未使用的导入
→ BashTool: 运行 tsc 验证编译通过
→ 完成,共修改 23 个文件
这个 REPL 的特点:
- Read:读取自然语言意图
- Eval:规划并执行工具调用链
- Print:显示执行过程和结果
- Loop:等待下一个指令
2.8 关键维度:从“计算”到“行动”
| 系统 | 输入 | 执行能力 | 状态持久 | 输出 | 可组合性 |
|---|---|---|---|---|---|
| Lisp REPL | 代码 | 计算 | 会话内 | 值 | 低 |
| Shell | 命令 | 系统操作 | 会话内 | 文本 | 高(管道) |
| Jupyter | 代码 | 计算 + 可视化 | Notebook 内 | 富媒体 | 中 |
| ChatGPT | 自然语言 | 文本生成 | 对话内 | 文本 | 低 |
| Claude Code | 自然语言 | 系统操作 + 计算 + 工具链 | 对话内 + 文件系统 | 文本 + 动作结果 | 高(工具链) |
2.9 Claude Code 的 REPL 设计
Claude Code 的 REPL 实现在 src/entrypoints/cli.tsx,核心循环可概括为:
1. 显示提示符,等待用户输入
2. 解析输入(斜杠命令 or 自然语言)
3. 如果是斜杠命令,直接执行
4. 如果是自然语言,提交给 QueryEngine
5. QueryEngine 调用 Claude API,获取流式响应
6. 响应中包含工具调用时,执行工具
7. 工具结果回填到对话,继续生成
8. 显示最终结果
9. 回到第 1 步
这个循环有几个关键设计:
- 流式显示:Claude 的响应是流式的,用户不需要等待完整响应才能看到内容。
- 工具调用透明:每次工具调用都显示给用户,用户可以看到 Claude 在做什么。
- 可中断:用户随时可以按 Ctrl+C 中断当前操作。
- 历史记录:对话历史保存在本地,可以恢复之前的会话。
2.10 小结
REPL 的演化史是一部降低交互门槛的历史:
- Lisp REPL:让程序员可以即时探索代码
- Shell:让用户可以即时操作操作系统
- Jupyter:让数据科学家可以即时探索数据
- ChatGPT:让普通人可以用自然语言交互
- Claude Code:让开发者可以用自然语言操作代码库
每一步都在降低"从想法到执行"的摩擦。Claude Code 是这条路上目前最远的一步。要理解它为什么有效,我们需要先理解“状态”是什么。
3 理解状态的本质
3.1 什么是状态?
本节要解决的问题:把「有状态 / 无状态」从口号还原为可操作的工程判断——
Output = f(Input)与Output = f(Input, History)分别对应何种系统行为。
让我们从最简单的例子开始理解状态的本质。
想象两种不同的计算方式:
- 有状态的计数器:就像一个记账本,每次调用都会在之前的基础上累加。第一次调用返回1,第二次返回2,第三次返回3……它“记住”了之前发生的一切,每次的输出都依赖于历史。
- 无状态的加法器:就像一个计算器的加法功能,给它输入2和3,无论何时、无论调用多少次,结果永远是5。它不知道也不关心之前发生了什么,每次都是全新的计算。
这个区别看似简单,却是理解整个系统设计的关键。用数学语言表达:
无状态:Output = f(Input)
有状态:Output = f(Input, History)
3.2 生活中的状态
为了更好理解,看看生活中的例子:
- 银行账户是有状态的。你的余额是所有历史交易的累积结果。银行必须记住每一笔存取款,否则你的钱就消失了。
- 汇率转换是无状态的。100美元换人民币,只需要知道当前汇率,不需要知道你上次换了多少。
- 对话是有状态的。当朋友说“还记得昨天那件事吗?”,需要共同的记忆才能继续。
- 翻译是无状态的。把"Hello"翻译成“你好”,不需要知道之前翻译过什么。
这个区别看似简单,却深刻影响着系统设计的方方面面。理解了这一点,我们就能沿着历史看到“无状态”如何被一次次发明与复用。
4 无状态思想的历史脉络
4.1 数学起源:纯函数的优雅(17世纪)
无状态的思想并非始于计算机。17世纪,莱布尼茨和牛顿发展微积分时,数学函数就是无状态的。f(x) = x²,无论何时计算 f(3),结果都是 9。
这种确定性和可预测性,成为后来所有无状态设计的理论基础。
4.2 Unix 革命:管道的哲学(1973)
真正将无状态思想带入实践的,是 Unix 的创造者们。
1973 年,Doug McIlroy 在贝尔实验室提出了管道(pipe)概念。他用了一个绝妙的比喻:“我们需要某种方式把程序连接起来,就像花园里的水管——当需要以另一种方式处理数据时,只需拧上另一段管子。”
Unix 管道的优雅体现在它的组合方式上。想象一个处理日志文件的场景:你需要找出所有错误信息,统计每种错误出现的次数,并显示最常见的 10 种错误。
在 Unix 中,这个复杂任务可以通过管道符号(|)将五个简单工具串联完成:
# Unix管道的优雅
cat file.txt | grep "error" | sort | uniq -c | head -10
每个工具都是无状态的:筛选工具不知道数据来自文件读取,排序工具不关心会被统计工具处理。每个工具只做一件事,并把这件事做到极致。
这种设计带来了意想不到的威力——5 个简单工具理论上可以产生 120 种不同的组合方式。如果这些工具是有状态的、相互依赖的,大部分组合都会失效。这就是无状态设计的魔力:通过放弃记忆,获得了无限的组合可能。
4.3 函数式编程的批判(1977)
1977 年,John Backus 在图灵奖演讲中投下一枚炸弹:《编程能否从冯·诺依曼风格中解放出来?》
# 冯·诺依曼风格(有状态)
sum = 0
for i in array:
sum = sum + i # 不断修改状态
# 函数式风格(无状态)
sum = reduce(add, array) # 纯函数组合
他批判了主流的命令式编程方式。传统的求和方式就像记账:创建一个“总和”变量,初始值为 0,然后逐个遍历数组,每次都修改这个变量,把新的数字加到已有的和上。这个过程充满了状态变化——变量在不断被修改,每一步都依赖前一步的结果。
而函数式的方式完全不同:它把求和看作一个纯粹的数学运算,通过组合“加法”这个基本操作来处理整个数组。没有变量被修改,没有状态在变化,只有数据在函数间流动。
Backus 认为,状态修改是程序复杂性的根源。当程序中充满了不断变化的状态时,理解和调试都会变得困难。这个观点深刻影响了整整一代语言设计。
4.4 REST 与网络架构:无状态作为扩展前提(2000)
2000 年,Roy Fielding 在博士论文《Architectural Styles and the Design of Network-based Software Architectures》中提出 REST 架构,将无状态作为核心约束。他的理由很简单:在分布式系统中,状态是扩展性的敌人。
对比两种设计:
- 有状态:服务器记住用户会话,但请求被分配到其他服务器时就失效了。需要在所有服务器间同步会话,复杂且昂贵。
- 无状态:每个请求携带 JWT 令牌,包含所有必要信息。任何服务器都能处理,可随意增减服务器。
这就是为什么 REST 能支撑互联网规模的应用——以无状态为前提的设计降低了横向扩容的复杂度,成为互联网级系统的基础实践之一。
4.5 Serverless 与 Lambda:强制无状态的编程模型(2014)
2014 年,Lambda 带来了一个激进的承诺:“假装每次函数调用都在全新的机器上运行”。
这个设计哲学很纯粹:开发者写函数时必须假设什么都不会被保留——没有全局变量、没有临时文件、没有数据库连接。但实际执行时,Lambda 会悄悄复用容器来提升性能。这是个聪明的把戏:强制无状态的编程模型,但在实现层做优化。
为什么这么设计?看看传统服务器的痛点:
- 负载突增时手忙脚乱地加机器
- 闲置时还在烧钱
- 一台服务器挂了,上面的状态也没了
Lambda 的无状态让这些问题消失了。黑五促销?函数自动扩展到上万个实例。凌晨三点没人访问?成本归零。某个实例崩溃?下一个请求在新实例上执行,用户无感知。
但“纯粹无状态”在现实中总要妥协。Lambda 允许 512MB 的 /tmp 空间,容器复用时全局变量会保留。Cloudflare Workers 走得更极端——完全没有文件系统,10 毫秒 CPU 限制,换来的是全球任意节点执行。Azure Functions 则务实地提供了 Durable Functions,承认某些工作流确实需要状态编排。
Serverless 的“无状态”不是技术洁癖,而是一种交易——用编程模型的约束,换取运维的简单和成本的弹性。你失去了对执行环境的控制,但获得了不用管理服务器的自由。
5 无状态设计的优势
5.1 可组合性:乐高积木 vs 精密手表
无状态组件就像乐高积木,可以自由组合来解决不同问题:
# 今天的需求:找出错误日志中的IP地址
cat app.log | grep ERROR | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | sort -u
# 明天的需求:统计每个IP的错误次数
cat app.log | grep ERROR | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | sort | uniq -c
# 后天的需求:找出错误最多的前10个IP
cat app.log | grep ERROR | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | sort | uniq -c | sort -rn | head -10
注意到了吗?每个新需求都是在已有组合上的微调。我们不需要重写整个程序,只需要像玩乐高一样,替换或添加几个模块。每个工具都不知道会被如何组合,所以能被任意组合。
相比之下,有状态系统像精密手表——每个齿轮都精确咬合,牵一发而动全身。想要改变功能?对不起,你需要重新设计整个机芯。
这就是为什么 Unix 哲学历经 50 年依然生命力旺盛——通过保持每个组件的独立性,获得了应对未知需求的灵活性。
5.2 并行的自然性:无冲突的世界
这是我最喜欢展示的例子,想象你需要在一个大型代码库中搜索需要的文件:
传统串行方式:就像一个人拿着手电筒,逐个房间地搜索。搜索第一个文件,完成后搜索第二个,然后第三个……在我的测试中,搜索整个项目耗时 42 秒。
并行方式:就像派出 16 个人,每人负责一部分房间,同时搜索。结果呢?同样的任务只需要 3.8 秒。
10 倍的性能提升!这不是什么高深的优化技巧,仅仅是因为搜索工具是无状态的:
- 搜索文件 A 不会影响搜索文件 B
- 没有共享变量需要加锁保护
- 没有竞争条件需要小心处理
- 16 个 CPU 核心可以真正独立工作,互不干扰
如果搜索工具需要维护全局状态——比如记录搜索历史、更新进度条、或者保持结果的顺序——并行化会变得复杂且低效。你需要处理线程同步、锁竞争、死锁风险……最后可能发现,并行版本甚至比串行更慢。
无状态设计让并行变得自然而然。这就是为什么现代的大数据处理框架(MapReduce、Spark)都采用无状态的计算模型——不是因为他们不想要状态,而是因为只有这样才能真正发挥成千上万个核心的计算能力。
5.3 简单性:没有生命周期管理
看看有状态服务需要背负的沉重包袱:
启动时的仪式:
- 初始化连接池,确保有足够的数据库连接
- 加载配置文件,恢复上次的运行状态
- 检查未完成的任务,重建内存缓存
- 启动后台线程,开始心跳检测
关闭时的清理:
- 保存当前状态,以便下次恢复
- 等待所有请求处理完成,不能丢失用户数据
- 优雅地关闭所有连接,释放所有资源
- 通知其他服务自己要下线了
崩溃后的恢复:
- 检查数据一致性,是否有损坏?
- 恢复中断的事务,用户的钱有没有丢?
- 重建索引和缓存,性能能否恢复?
这就像经营一家 24 小时营业的便利店——开店要准备货架、收银系统、监控设备;关店要盘点库存、结算账目、清理卫生;如果突然停电,还要检查冷藏柜、恢复收银数据、安抚排队的顾客。
而无状态服务就像计算器——插电就能用,断电就停止,重启立即恢复。没有状态恢复流程,没有繁琐的清理工作,崩溃了重启就行。
这种简单性不仅降低了开发复杂度,更重要的是提高了系统的可靠性。复杂的生命周期管理往往是 bug 的温床——忘记释放资源导致内存泄漏,清理顺序错误导致死锁,恢复逻辑有漏洞导致数据不一致……
5.4 可测试性:确定性的力量
测试无状态函数就像测试数学公式——2+3 永远等于 5。不需要准备环境,不需要清理状态,不需要模拟依赖。
测试有状态系统则像做化学实验,处处是坑:
- 环境污染:其他测试留下的全局状态、数据库遗留数据
- 依赖地狱:模拟数据库、消息队列、外部 API
- 时序问题:测试顺序依赖、并发干扰、异步等待
而无状态则带来确定性——相同输入永远产生相同输出。测试失败时,你知道是逻辑错误,而不是环境问题。
6 现实的权衡
6.1 什么时候需要状态
有些场景,状态不是可选项,而是必需品:
游戏世界需要持续性。玩家辛苦打下的装备、升级的等级、探索的地图,这些都是游戏体验的核心。没有人愿意玩一个每次登录都要重新开始的 RPG 游戏。
用户界面需要响应性。购物车里的商品、表单填写的内容、页面滚动的位置——这些临时状态虽然不需要永久保存,但在用户会话期间必须保持,否则每次操作都要重新来过。
资源管理需要经济性。数据库连接、网络套接字、线程池——这些都是昂贵的资源。每次请求都创建新连接不仅慢,还可能耗尽系统资源。连接池通过复用连接,用少量的状态换取了巨大的性能提升。
6.2 如何选择?一个简单的判断标准
我最喜欢用这个问题来判断:
“如果系统崩溃重启,用户能接受从零开始吗?”
- 编译器崩溃了?重新编译就行 → 无状态
- 游戏崩溃了?存档丢失不可接受 → 有状态
- 搜索崩溃了?重新搜索就行 → 无状态
- 购物车崩溃了?商品丢失很恼人 → 有状态
这个简单的问题能帮你快速定位系统的本质需求。
6.3 混合策略:现实世界的智慧
无状态和有状态,看上去是布尔变量 true/false 般的两个完全对立的值。但这其实是中文语境里的一个误解——英文的 stateless/stateful 并非“无/有状态”的二元开关,更像 -less/-ful 的“程度词”:强调依赖的轻重与记录方式的取舍,而非是否“完全没有/完全存在”。
真实系统也很少是纯无状态或纯有状态,而是智慧地混合使用。
最常见的模式:无状态计算 + 有状态存储
这就像餐厅的运营模式:服务员(无状态)可以随时换班,任何服务员都能为任何桌子服务;但收银系统(有状态)必须准确记录每一笔账单。应用服务器是无状态的,可以随意扩展;数据库是有状态的,负责持久化。
现代云架构几乎都采用这种模式:
- 无状态的 API 服务器 → 有状态的数据库
- 无状态的 Lambda 函数 → 有状态的 DynamoDB
- 无状态的容器 → 有状态的 Redis 缓存
Event Sourcing:用无状态事件构建有状态系统
不直接存储状态,而是存储所有导致状态变化的事件。账户余额不是一个数字,而是所有存取款事件的累加结果。就像银行的流水账——每笔交易都是独立的、不可变的,但累加起来就是你的账户余额。这种方式既保留了完整的历史,又能灵活地重建任意时刻的状态。
6.4 核心洞察
选择无状态还是有状态,不是技术信仰的问题,而是工程权衡的艺术。无状态不是目的,而是手段——它帮助我们构建更简单、更可靠、更可扩展的系统。
状态并不是坏的,无管理的状态才是问题的根源。最好的设计不是完全无状态,而是在正确的地方、以正确的方式管理必要的状态。就像生活中的断舍离——不是要扔掉所有东西,而是只保留真正重要的,并且妥善管理它们。
7 从聊天机器人到 Agent
“An agent is a system that perceives its environment, makes decisions, and takes actions to achieve goals.”
—— Russell & Norvig,《人工智能:一种现代方法》
7.1 聊天机器人的本质局限
2022 年 ChatGPT 发布后,全世界都在讨论 AI。但大多数人没有意识到,ChatGPT 本质上是一个非常聪明的文本生成器。它的工作模式是:
输入文本 → [LLM] → 输出文本
这个模式有一个根本限制:它只能生成文本,不能改变世界的状态。你可以让 ChatGPT 写一段代码,但它不能帮你运行这段代码。你可以让它分析一个 bug,但它不能帮你修复文件。你可以让它设计一个系统,但它不能帮你部署。
文本生成 ≠ 行动执行。
7.2 工具调用:打破文本的牢笼
2023 年,OpenAI 引入了 Function Calling(函数调用),Anthropic 引入了 Tool Use(工具使用)。这是 AI 从“聊天机器人”向“Agent”演化的关键一步。
工具调用的机制:
用户:今天北京天气怎么样?
LLM 内部决策:我需要调用天气 API
→ 生成工具调用:get_weather(city="北京")
→ 系统执行工具,返回结果:{"temp": 15, "weather": "晴"}
→ LLM 基于结果生成回答:今天北京天气晴,气温 15 度。
这个机制让 LLM 从“只能说”变成了“能做”。
7.3 Agent 的定义
什么是 Agent?在 AI 领域,Agent 是一个能够:
- 感知环境(Perceive):读取文件、执行命令、获取信息
- 做出决策(Decide):规划下一步行动
- 执行动作(Act):调用工具改变环境状态
- 观察结果(Observe):看到动作的效果
- 循环迭代(Loop):基于结果调整计划
上述 感知 → 决策 → 行动 → 观察 → 再迭代 的闭环,是 Agent 区别于单次问答的核心骨架。
7.4 ReAct:推理与行动的结合
2022 年,Google 提出了 ReAct(Reasoning + Acting)框架,这是现代 AI Agent 的理论基础:
思考:我需要找出项目里所有的 TODO 注释
行动:GrepTool("TODO", path="src/")
观察:找到 23 个 TODO,分布在 8 个文件中
思考:我应该按优先级整理这些 TODO
行动:FileReadTool("src/main.ts")
观察:[文件内容]
思考:这个文件有 3 个 TODO,其中 2 个是高优先级
ReAct 的关键洞察:让 LLM 在行动之前先“思考”,能显著提高任务完成质量。
Claude Code 实现了这个模式。当你开启“扩展思考”(Extended Thinking)时,Claude 会在每次工具调用前生成思考块(thinking blocks),这些思考不会显示给用户,但会影响决策质量。
7.5 Claude Code 的 Agent 循环
Claude Code 的核心是一个 Agent 循环,在 src/query.ts 中实现:
┌─────────────────────────────────────────────┐
│ Agent 循环 │
│ │
│ 用户输入 │
│ ↓ │
│ 构建消息列表(含历史) │
│ ↓ │
│ 调用 Claude API(流式) │
│ ↓ │
│ ┌─────────────────────────────────┐ │
│ │ 响应解析 │ │
│ │ ├── 文本块 → 流式显示 │ │
│ │ ├── 思考块 → 内部处理 │ │
│ │ └── 工具调用块 → 执行工具 │ │
│ └─────────────────────────────────┘ │
│ ↓ │
│ 工具结果回填到消息列表 │
│ ↓ │
│ 是否需要继续? │
│ ├── 是 → 回到“调用 Claude API” │
│ └── 否 → 返回最终结果 │
└─────────────────────────────────────────────┘
这个循环有几个关键特性:
- 多轮工具调用:一次用户请求可能触发多轮 API 调用,每轮可能包含多个工具调用
- 并行工具执行:Claude 可以在一次响应中请求多个工具调用,这些调用可以并行执行
- 上下文累积:每轮的工具结果都会加入消息列表,Claude 能看到完整的执行历史
- 自动终止:当 Claude 认为任务完成时,它会停止请求工具调用,生成最终回答
7.6 聊天机器人 vs Agent:关键差异
| 维度 | 聊天机器人 | Agent(Claude Code) |
|---|---|---|
| 输出 | 文本 | 文本 + 动作 |
| 状态 | 无(每次独立) | 有(文件系统、任务状态) |
| 循环 | 单轮(输入→输出) | 多轮(感知→决策→行动→观察) |
| 工具 | 无 | 43 个内置工具 + MCP 扩展 |
| 目标 | 回答问题 | 完成任务 |
| 失败处理 | 无 | 可以重试、回滚、调整策略 |
| 时间跨度 | 秒级 | 分钟到小时级 |
7.7 Agent 的新挑战
Agent 能力的提升带来了新的挑战:
- 可靠性:Agent 执行多步骤任务,任何一步出错都可能导致整体失败。Claude Code 通过工具结果验证、错误重试、用户确认来提高可靠性。
- 安全性:Agent 能执行真实动作,错误的动作可能造成不可逆的损害。Claude Code 通过权限模型、操作确认、沙箱隔离来保证安全。
- 可观察性:用户需要知道 Agent 在做什么。Claude Code 通过透明的工具调用显示、实时流式输出来保证可观察性。
- 上下文管理:长任务会消耗大量 token。Claude Code 通过自动压缩(auto-compact)来管理上下文长度。
- 成本控制:多轮 API 调用成本高。Claude Code 通过 token 预算、成本追踪来控制成本。
7.8 从工具调用到 Agent 系统
工具调用是 Agent 的基础,但一个完整的 Agent 系统还需要:
工具调用
+
上下文管理(记住做了什么)
+
任务规划(知道下一步做什么)
+
错误处理(出错时怎么办)
+
权限控制(什么能做什么不能做)
+
状态持久化(任务中断后能恢复)
=
Agent 系统
7.9 小结
从聊天机器人到 Agent,本质上是从“生成文本”到“执行动作”的跨越。这个跨越需要:
- 工具调用机制(让 LLM 能调用外部功能)
- Agent 循环(感知-决策-行动-观察的迭代)
- 完整的工程系统(可靠性、安全性、可观察性)
Claude Code 是目前最完整的 AI Agent 工程实现之一。理解它的设计,就是理解 Agent 系统工程的现状。
8 AI 时代的新思考
本章作用:回到第 0 章的质疑,用「Unix—无状态—Agent」已建立的框架,解释 为何 grep / agentic search 在 Anthropic 的实测中能成为一条合理路线,并与索引类方案并置对照——不是谁绝对更好,而是各自默认优化的目标不同。
8.1 Claude Code 的选择:具体的技术对比
回到开头的质疑,让我们用事实说话。
根据 Anthropic 团队在 5 月份的博客访谈《Claude Code: Anthropic’s Agent in Your Terminal》,他们测试了 RAG(向量索引)等多种方案后,最终选择了 “agentic search”——就是使用 glob、grep 这些常规的代码搜索。令人意外的是,这种方式“在性能上大幅超越了所有其他方案”。
当前主流的 AI 编程助手采用了三种截然不同的技术路线:
- Cursor 的向量索引方案:采用 AI 原生设计,使用 Merkle 树快速索引代码库,将代码智能分块后生成向量嵌入,存储在远程向量数据库 Turbopuffer 中。优势是语义理解——搜索“用户认证”能找到 login、authenticate、verifyUser 等相关函数,即使没有准确关键词。
- JetBrains 的传统索引方案:经过 20 年打磨的索引系统,通过深度解析创建 PSI 树和 stub 索引。支持强大的代码导航、重构和智能提示,是企业级 IDE 的黄金标准。
- Claude Code 的无索引方案:Claude Code 选择了最简单直接的方案——每次搜索都是实时执行,不依赖任何预构建的索引。根据逆向工程分析,它内部包含 GrepTool(支持完整正则表达式的内容搜索)、GlobTool(文件模式匹配)等经典 Unix 工具(当然随着架构迭代现在可能有一定优化)。就像直接在终端输入 grep 命令一样简单纯粹。
| 维度 | JetBrains 传统索引 | Cursor 向量索引 | Claude Code 无索引 |
|---|---|---|---|
| 核心 | PSI 语法树 + 符号表 | Embedding + 向量库 | Agentic Search(grep+LLM) |
| 启动 | 慢(需建索引) | 慢(需建向量) | 极速(0 等待) |
| 实时性 | 增量更新 | 增量更新 | 100% 实时(读磁盘) |
| 搜索方式 | 精确符号匹配 | 语义相似度 | 文本 grep + LLM 推理 |
| 隐私 | 本地 | 本地向量库 | 纯本地、无存储 |
| 复杂度 | 极高 | 高 | 极简 |
| 适合 | 日常 IDE 智能(补全 / 跳转) | AI 语义检索、RAG | 安全、快速、临时分析 |
8.2 为什么“健忘”反而更好?
这个反直觉的选择,背后有四个关键优势:
-
零配置的自由:大型项目中,Cursor 需要上传生成嵌入,IntelliJ 需要构建索引——往往耗时几分钟。Claude Code?立即可用。更重要的是无状态命令带来的组合能力:
tail -f app.log | claude -p "如果看到异常就通过 Slack 通知我"这种管道组合的优雅,是索引系统永远无法提供的。
-
确定性的价值:向量搜索失败时,调试是噩梦——是嵌入质量?语义偏差?索引过期?grep 的行为完全可预测:搜索 “processPayment” 就是精确匹配,失败原因只有一个——关键词不匹配。这种确定性在调试复杂问题时无比宝贵。
-
隐私的根本保障:Cursor 需要上传代码生成嵌入向量。虽然声称“不存储”,但学术研究已证明可从嵌入反推原始内容。Claude Code 的 grep 完全本地执行,从架构上杜绝了泄露可能——不是通过加密,而是让泄露变得不可能。
-
维护的零成本:没有“索引卡住”,没有“缓存损坏”,没有后台进程偷偷吃 CPU。每次搜索都是全新开始,每次结果都是最新真相。
8.3 不同场景,不同选择
这不是技术优劣的绝对判断,而是设计哲学的不同选择。每种方案都有其最适合的场景:
Cursor 的向量索引适合创意编程场景——当你大概知道要什么但不确定具体名称时,语义搜索能帮你探索代码库。它特别适合学习新项目或寻找灵感。
JetBrains 的传统索引是企业级开发的黄金标准——当你需要可靠的重构、精确的类型检查、复杂的代码导航时,经过 20 年打磨的索引系统无可替代。
Claude Code 的 grep 方案则是 Unix 哲学的现代传承——当你重视简单、可控、可组合时,当你需要绝对的确定性和隐私保护时,这种“原始”的方案反而是最先进的选择。
正如 Boris 所说:
“Claude Code 不是一个产品,而是一个 Unix 工具。”
8.4 无状态之美:在 AI 时代的新意义
Claude Code 的选择,让我们重新思考什么是“智能”。在一个 AI 无处不在的时代,真正稀缺的不是智能,而是可预测性;不是功能的丰富,而是行为的确定;不是记住一切,而是知道何时遗忘。
这就是为什么 grep 在 2025 年依然重要,为什么 Unix 哲学历经 50 年依然闪光,为什么 Claude Code 的“倒退”其实是一种清醒。
简单的工具活得最久,“健忘”的设计最自由。
附录
A.1 RAG(向量索引)
RAG 是 Retrieval-Augmented Generation 的缩写,常译为“检索增强生成”。它的核心思路是:让模型在回答之前,先从外部知识库(文档、代码、Wiki、工单、规范等)检索出相关材料,再把这些材料作为上下文交给模型生成答案。
在工程实现中,“向量索引”通常指把知识库切分成一段段文本(chunk),为每段文本计算 embedding 向量,并存入向量数据库或本地向量索引。用户提问时,同样把问题计算为 embedding,通过相似度检索出 Top-K 片段,然后把这些片段拼到 prompt 里,模型基于“检索到的证据”生成回答。
RAG 主要解决三类问题:
- 上下文窗口有限:知识库很大,不可能一次性塞进模型上下文。RAG 通过“只取相关片段”让模型看见它需要的那部分信息。
- 语义检索需求:当你不确定关键词(例如想找“用户认证”但代码里叫 login/authenticate/verifyUser),向量相似度能比纯关键词匹配更容易找对位置。
- 减少幻觉与提高可追溯性:模型引用检索到的片段回答,回答更“有根据”,也更便于回溯来源(前提是检索结果足够准确)。
RAG 的代价与风险也很现实:
- 索引成本:需要切分、嵌入、建库;大仓库或频繁变更时,增量更新与一致性会变复杂。
- 片段质量依赖:chunk 的粒度、边界、元数据(路径、符号、语言)会直接影响检索质量,甚至决定答案能否“对得上”。
- token 成本:检索到的片段要被拼进上下文才有用,片段越多、越长,token 消耗越高;“烧 tokens”通常指这部分上下文注入成本。
- 隐私与合规:embedding 可能带来信息泄露风险;即便不保存原文,也可能存在从向量反推内容的风险与合规疑虑。
在 AI 编程助手语境里,RAG(向量索引)更像一种“把代码库变成可被语义检索的知识库”的方法:当你在探索陌生项目、关键词不确定、需要广泛联想时,它非常有用;当你更重视确定性、可控性、隐私与零维护时,经典 grep/glob 的无索引方式反而更贴近 Unix 的工程哲学。
更多推荐



所有评论(0)