从互联网早期设计到 AI Agent

互联网早期的工程传统,塑造了一套可复用的“构建工具的方法论”:工具应当职责明确、可组合、可观察。AI Agent(以 Claude Code 为代表)并非脱离历史的全新范式,而是在这些传统之上,把“文本输入”升级为“意图输入”,把“输出文本”升级为“执行动作”。


阅读导引

  • 叙事主线:Unix / CLI 传统与 REPL 的演化 → 「状态」的本质与 无状态 思想的历史脉络 → 无状态设计的收益与现实权衡 → 从聊天机器人到 Agent → 回扣开篇关于 grep / 无索引 的争议,形成闭环。
  • 阅读方式:各章末均有 小结;第 5–6 章对应「为何有效」与「何时妥协」;第 8 章集中回应引言中的质疑。

核心命题(可先扫此处再读正文)

  1. 工具观:单一职责、组合性、透明性——在 Agent 时代体现为清晰的工具边界与可审查的调用链。
  2. 状态观:无状态不是“不保存任何数据”,而是审慎选择状态的存放位置与生命周期;错误的不是状态本身,而是失控的状态。
  3. 产品观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 哲学的核心是三条原则:

  1. 单一职责:每个程序只做一件事,并把它做好
  2. 组合性:程序之间通过标准接口(文本流)协作
  3. 透明性:程序的行为可预测、可观察

这些原则在 50 年后的 Claude Code 中依然清晰可见。

1.2 管道:最早的“工具调用”

Unix 管道(|)把复杂任务拆成可组合步骤:前一个程序的输出成为后一个程序的输入。

history | awk '{print $2}' | sort | uniq -c | sort -rn | head -10

执行链路如下:

  1. history 输出命令历史记录
  2. awk 提取第二列(命令名)
  3. sort 排序
  4. uniq -c 统计重复次数
  5. sort -rn 按数字倒序排列
  6. 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 的缩写:

  1. Read:读取用户输入
  2. Eval:执行或求值
  3. Print:打印结果
  4. 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 是一个能够:

  1. 感知环境(Perceive):读取文件、执行命令、获取信息
  2. 做出决策(Decide):规划下一步行动
  3. 执行动作(Act):调用工具改变环境状态
  4. 观察结果(Observe):看到动作的效果
  5. 循环迭代(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,本质上是从“生成文本”到“执行动作”的跨越。这个跨越需要:

  1. 工具调用机制(让 LLM 能调用外部功能)
  2. Agent 循环(感知-决策-行动-观察的迭代)
  3. 完整的工程系统(可靠性、安全性、可观察性)

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 为什么“健忘”反而更好?

这个反直觉的选择,背后有四个关键优势:

  1. 零配置的自由:大型项目中,Cursor 需要上传生成嵌入,IntelliJ 需要构建索引——往往耗时几分钟。Claude Code?立即可用。更重要的是无状态命令带来的组合能力:

    tail -f app.log | claude -p "如果看到异常就通过 Slack 通知我"
    

    这种管道组合的优雅,是索引系统永远无法提供的。

  2. 确定性的价值:向量搜索失败时,调试是噩梦——是嵌入质量?语义偏差?索引过期?grep 的行为完全可预测:搜索 “processPayment” 就是精确匹配,失败原因只有一个——关键词不匹配。这种确定性在调试复杂问题时无比宝贵。

  3. 隐私的根本保障:Cursor 需要上传代码生成嵌入向量。虽然声称“不存储”,但学术研究已证明可从嵌入反推原始内容。Claude Code 的 grep 完全本地执行,从架构上杜绝了泄露可能——不是通过加密,而是让泄露变得不可能。

  4. 维护的零成本:没有“索引卡住”,没有“缓存损坏”,没有后台进程偷偷吃 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 主要解决三类问题:

  1. 上下文窗口有限:知识库很大,不可能一次性塞进模型上下文。RAG 通过“只取相关片段”让模型看见它需要的那部分信息。
  2. 语义检索需求:当你不确定关键词(例如想找“用户认证”但代码里叫 login/authenticate/verifyUser),向量相似度能比纯关键词匹配更容易找对位置。
  3. 减少幻觉与提高可追溯性:模型引用检索到的片段回答,回答更“有根据”,也更便于回溯来源(前提是检索结果足够准确)。

RAG 的代价与风险也很现实:

  • 索引成本:需要切分、嵌入、建库;大仓库或频繁变更时,增量更新与一致性会变复杂。
  • 片段质量依赖:chunk 的粒度、边界、元数据(路径、符号、语言)会直接影响检索质量,甚至决定答案能否“对得上”。
  • token 成本:检索到的片段要被拼进上下文才有用,片段越多、越长,token 消耗越高;“烧 tokens”通常指这部分上下文注入成本。
  • 隐私与合规:embedding 可能带来信息泄露风险;即便不保存原文,也可能存在从向量反推内容的风险与合规疑虑。

在 AI 编程助手语境里,RAG(向量索引)更像一种“把代码库变成可被语义检索的知识库”的方法:当你在探索陌生项目、关键词不确定、需要广泛联想时,它非常有用;当你更重视确定性、可控性、隐私与零维护时,经典 grep/glob 的无索引方式反而更贴近 Unix 的工程哲学。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐