跟 Claude Code 说一句话,它到底在后台忙什么?

0 阅读7分钟

你说:"帮我把文件里的 moment 替换成 dayjs"

它说:"好的,我来处理" —— 然后你的终端就开始"抽搐"了。

今天咱们就来扒一扒,Claude Code 这类 AI 编程助手在终端里跟你对话时,背后到底发生了什么。别怕,不聊源码,聊人话。


一、早期体验:等得花儿都谢了

如果你用过早期的大模型工具,一定记得这种"抽风式"体验:

  1. 你敲下一句话,回车
  2. 白屏几秒
  3. 冒出一段内容
  4. 白屏几秒
  5. 工具调用出来了
  6. 白屏几秒
  7. 终于又冒出一段内容

整个过程就像在跟一个网络卡顿的异地恋对象打电话——说一句、卡三秒、再说一句、再卡三秒。你盯着屏幕,怀疑人生:"它是不是死机了?还是我网不好?"

为什么会这样?

因为早期的架构是 "要么输出,要么干活"

  • 模型要么在生成 token(输出文字)
  • 要么在调用工具(执行函数)
  • 两件事不能同时做

所以它得先想好、写完、停下来,才能去调工具;工具执行完、结果回来了,才能继续写。中间那几秒白屏,就是在等——等模型写完,等工具执行完。


二、现在的体验:边想边说边干

现在不一样了。你看到的是:

模型一边打字往外蹦 token,一边顺手就把工具调用给触发了。

这叫 流式架构(Streaming Architecture)

它带来的核心变化是:模型不再是"先想完再干",而是"边想边干边汇报"

打个比方:

  • 早期:你让助理去打印文件,他先站在原地想 10 秒钟"我要怎么走过去",然后走 3 秒钟,到了打印机前再想 5 秒钟"我要按哪个键"……
  • 现在:他一边走一边跟你说"我现在走向打印机,快到了,我按下打印键了,正在等出纸"——你全程都有反馈,体感快了好几倍

体感快 ≠ 实际快,但用户感知的流畅度直接决定了他会不会摔键盘。


三、SSE vs WebSocket:为什么不是 WebSocket?

一提到"服务端主动推消息",很多人第一反应是 WebSocket。毕竟它全双工、双向通信、听起来就很高级。

但 LLM 流式输出,用的却是 SSE(Server-Sent Events) 。为什么?

先看看两者的区别

特性WebSocketSSE
通信方向全双工(双向)单向(服务端 → 客户端)
底层协议独立的 WS 协议就是 HTTP 的一个特殊模式
连接状态建立后保持,直到主动关闭长连接,但可自动重连
复杂度较高很低,浏览器原生支持 EventSource
重连机制要自己实现协议自带 retry

为什么 LLM 场景选 SSE?

关键点在于:LLM 流式输出,本质上是"服务端一个劲地推 token,客户端根本不需要说话"。

  • 你发起一个 HTTP 请求:"帮我替换 moment"
  • 服务端建立 SSE 长连接
  • 服务端:token、token、token…… 一直推
  • 客户端:我只负责收,全程闭嘴

既然是单向推送,用 WebSocket 就是杀鸡用牛刀——还得处理心跳、断线重连、协议握手,纯属给自己找麻烦。

而 SSE:

  • 就是 HTTP,防火墙友好,不用开新端口
  • 协议自带重试机制,断了自动重连
  • 浏览器/终端实现都简单

所以,SSE 是 LLM 流式输出的天选之子

💡 一句话总结:WebSocket 是"电话",SSE 是"广播"。LLM 只需要广播,不需要跟你煲电话粥。


四、Tool Call:模型"挤牙膏"式的工具调用

这是最精彩的部分。

当模型决定调用工具时,它输出的不是一个完整的 JSON,而是一个 token 一个 token 往外蹦的:

text

'{"file_'
'path": "'
'src/uti'
'ls.ts"}'

对,你没看错——连 JSON 都是挤牙膏挤出来的

这就带来一个非常棘手的问题:你什么时候开始解析这段 JSON?

过早解读:翻车现场

如果你看到 {"file_ 就急着解析,会得到:

text

SyntaxError: Unexpected end of JSON input

或者更惨——调用了错误的工具。比如你以为是 read_file,结果参数还没传完,直接传了个 {"file_ 进去,工具直接崩溃。

过晚解读:效率低下

如果你非要等到整个 JSON 完全闭合} 出现)才开始处理,那前面那几个 token 的时间就白白浪费了,用户体验又回到了"白屏等几秒"。

怎么办?

成熟的实现会在流式解析和完整解析之间找平衡

  • 增量 JSON 解析器,边收边攒
  • 检测到关键字段(比如 tool_name)就先做预判
  • 等参数收完整了再真正执行

这就像你听别人报电话号码:听到 138 就知道是手机号,但你还得等他把后面 8 位报完才能拨出去。


五、灵魂拷问:所有工具都能"边说边调"吗?

不是。

有些工具可以边说边调,有些必须等模型把话说清楚

举个例子:

  • Read 文件:模型说"我看看这个文件",你立刻就能读,不影响
  • Edit 文件:模型说"我要把第 3 行的 moment 改成 dayjs"——等等,你确定是第 3 行?确定是 moment 不是 moment.js?确定要改的是这个文件?

如果 Edit 在参数还没说清楚时就执行,轻则改错文件,重则删库跑路

所以,"边说边调"的前提是:这个工具的执行是幂等的、低风险的、可回滚的


六、Claude Code 的并发安全判断:谁可以插队?

Claude Code 在工具调度上有一套并发安全规则,非常讲究:

工具类型操作性质能否并发
Read 文件只读✅ 可以并发
Glob / Grep只搜索✅ 可以并发
Edit 文件写入❌ 必须串行
Bash ls读取类✅ 可以并发
Bash rm删除类❌ 必须串行

为什么这么设计?

  • 只读操作:你读你的,我读我的,互不干扰,并发提速
  • 写入操作:两个 Edit 同时改同一个文件,谁覆盖谁? 必须排队,一个改完下一个再改
  • Bash 命令lsrm 天差地别——一个只是看看,一个是真的删东西。Claude Code 会根据命令语义判断,而不是一刀切

这就像图书馆:看书的人可以随便进,但搬书的人得排队,不然书架就乱了。


七、工具结果按什么顺序返回?

按调用顺序返回。

哪怕 Claude Code 内部并发执行了 5 个 Read 工具,结果也会按你调用的顺序一个个吐给你。

为什么要这么"死板"?

  1. 工程顺序性:后面的工具可能依赖前面的结果
  2. 可读性:用户看到的日志是连贯的,不是乱的
  3. 方便调试:出问题时能按时间线回溯

这就像快递分拣:包裹可以同时在多个传送带上跑,但送到你家门口时,必须按订单顺序


八、完整流程复盘:你敲下回车后发生了什么

来,我们把整个链路串一遍:

text

1. 流式输出
   └─ 模型一个 token 一个 token 往外蹦
   └─ 你看到文字在终端里"生长"

2. 流式结束,弹出审批弹窗
   └─ "我要执行 xxx 工具,是否允许?"

3. 用户确认审批
   └─ 你敲的 "yes",其实是发送了一个普通的 HTTP 请求
   └─ 不是魔法,就是个 POST

4. 新的 SSE 流开始
   └─ 工具执行结果通过新的 SSE 连接推回来
   └─ 模型继续边想边说

关键点:整个过程中,客户端和服务端之间可能建立多次 SSE 连接,而不是一个连接从头用到尾。

  • 第一次 SSE:模型输出 + 工具调用请求
  • 用户审批(普通 HTTP)
  • 第二次 SSE:工具执行结果 + 模型继续输出
  • ……循环往复

九、总结:一句话记住核心

LLM 流式输出的本质,是服务端用 SSE 一个劲地推 token;Tool Call 是模型"挤牙膏"式地吐 JSON;Claude Code 的并发安全,是"只读随便跑,写入排队走"。

下次你再跟 Claude Code 说"帮我把 moment 替换成 dayjs",它后台忙活的那几秒,你大概能脑补出它在干嘛了:

  • 它在边打字边解析 JSON
  • 它在判断哪些工具能并发
  • 它在按顺序把结果吐给你
  • 它在等你敲那个 yes

而你,只需要优雅地喝口咖啡,看它表演。☕


如果这篇文章让你对 AI 编程助手的内部机制有了新的理解,欢迎点赞、收藏、转发三连。毕竟——你敲的每一个 yes,背后都是一整套精密的流式架构在支撑。