深入解构Claude Code - 第 3 篇 · 一问一答怎么转起来

这是全书最核心的一篇。回答六个问题:按回车后整件事的完整旅程是什么?对话循环怎么转?回答为什么是一个字一个字蹦出来的?一场长对话怎么被安排?聊天记录在程序内部长什么样?任务跑一半怎么停、怎么续?


3.1 你按下回车之后:一件事的完整旅程(鸟瞰图)

先看全貌,后面几章再逐个放大。

假设你在输入框敲了一句"帮我看看登录为什么失败",按下回车。接下来发生的事,按顺序是这样的:

arduino 复制代码
① 你的话被包装成一条「用户消息」,放进聊天记录
        ↓
② 程序去收集这次提问要带的全部资料(系统提示 + 聊天记录 + 工具清单 + ......)
        ↓
③ 把资料打包,通过网络发给远端的 AI 模型
        ↓
④ 模型开始"说话",内容一个片段一个片段地流回来
        ↓
⑤ 程序一边接收一边把字打到屏幕上(你看到的逐字输出)
        ↓
⑥ 模型说完一段,程序检查:这段话里有没有"我要调用某工具"?
        ↓
   ┌─ 没有工具调用 → 这一轮结束,等你下一句话
   │
   └─ 有工具调用 → ⑦ 把关:这个操作允许吗?
                        ↓(允许)
                  ⑧ 真正执行工具(跑命令/读文件/改文件......)
                        ↓
                  ⑨ 工具结果包装成一条「工具结果消息」,放回聊天记录
                        ↓
                  ⑩ 带着新结果,回到第 ③ 步,再问一次模型
                        ↓
                  (模型看到结果,继续说,可能再调工具......如此转圈)
                        ↓
                  直到模型不再调工具、只说人话 → 这一轮结束

这个圈有个名字叫 Agent 循环(智能体循环) ,是一切 AI Agent 产品的心脏。它的精髓在于:模型不是"回答问题",而是在"干活的过程中反复回来汇报、领新指示"。每转一圈,模型就多知道一点现场情况(工具的执行结果),于是能决定下一步是继续动手还是收工。

打个比方:这不像"考试答题"(一次性交卷),更像"师傅带徒弟修水管"------徒弟(模型)看一眼现场,说"我要扳手",你(程序)把扳手递过去;徒弟拧一下,说"还需要手电筒",你再递;每次徒弟都根据刚看到的情况要下一样工具,直到修好说"成了"。徒弟负责"想",你负责"递工具和反馈现场"。

整个旅程里,三个角色分工明确:

  • 模型(远端):只负责想和说,包括决定"要不要调工具、调哪个"。它看不到你的电脑,碰不到任何文件。
  • 核心循环(本地):负责跑腿------打包资料、收发网络、执行工具、把结果喂回去。它自己不思考。
  • 工具(本地):真正动手的函数集合------读文件、改文件、跑命令。

记住这张图,下面五章是把它逐层放大。


3.2 对话循环:AI 说一句、动手做一步、再说一句,怎么串起来

核心循环在代码里不是一个普通函数,而是一个生成器函数(generator)。这个词有点技术,但概念很生活化,值得花点时间理解,因为它是理解整个程序的钥匙。

普通函数 vs 生成器函数

普通函数像自动售货机:你投币(调用),它一口气把货掉出来(返回结果),中间你不能插话,它也不会暂停。

生成器函数像说书人 :你喊一声"开讲",他讲一段就停下来等你;你再喊"继续",他接着讲下一段。他可以讲一段、停、讲一段、停,每段之间你都能去做点别的事(比如把刚讲的内容写到黑板上)。

在代码里,核心循环就是这样一个说书人:它每拿到模型流回来的一小段内容,就"停一下",把这段内容递出去(给界面显示),然后接着等下一段。

为什么非要这样,而不是"等模型全说完再一次性显示"?

两个原因:

  1. 。模型说一长段话可能要几十秒,如果等全说完才显示,用户会盯着空白屏幕以为死机了。逐段递送就能逐字显示,感觉上快得多(实际上总时间没变,但"有动静"和"没动静"的体验天差地别)。
  2. 工具要插队。模型说着说着可能说"我要调工具",这时程序得立刻去执行工具,而不是等说完。生成器这种"讲一段停一下"的模式,让程序在每一段之间都有机会检查"是不是该干活了""用户是不是按了取消"。

循环的"转一圈"内部

每转一圈(业内叫一个 turn),核心循环大致做这几件事:

  1. 检查预算:聊天记录是不是快撑满模型的容量了?快满了得先"划重点"(第 5 篇细讲)。
  2. 发请求:把资料发给模型,开始接收流。
  3. 边收边处理:每收到一段,递给界面;同时默默累计"这轮花了多少 token"(token 可以粗略理解为模型计费和计量的单位,大约四分之三个英文单词)。
  4. 收完后检查工具调用:模型这轮有没有要求调工具?
  5. 执行工具:有就执行(可能还要先过权限关),把结果放回聊天记录。
  6. 决定是否继续:模型明确表示"我说完了",或者触发了某个"该停"的条件(用户取消、出错、到了安全轮数上限),就停;否则回到第 1 步再转一圈。

怎么判断"模型说完了"

模型每次回复都会带一个"结束原因"的标记。最常见的两种:

  • end_turn(我这轮话说完了)→ 如果这轮没调工具,整个任务就完成了;
  • tool_use(我要调工具)→ 执行完工具,带着结果再来一圈。

还可能有"被长度限制截断""内容被安全策略拦下"等情况,程序分别有对应处理。


3.3 回答为什么是"一个字一个字蹦出来"的:流式输出原理

上一章说了"流回来",这一章讲讲这个"流"到底是怎么回事,以及为什么屏幕上能逐字显示。

先说没有"流"会怎样

网络请求传统上是"请求-响应"模式:你发一个请求过去,对方在服务器上把完整答案准备好,然后一次性把整个答案发回来。如果答案是一篇 3000 字的分析,你就得干等几十秒,期间屏幕一动不动。

流式(streaming)是什么

流式就像水管:服务器不是等水接满一桶再递给你,而是接好管子,水一边产生一边流过来。模型那边每生成几个字,就立刻通过网络推过来几个字,不等后面的。

技术上这是通过一种叫 SSE(服务器推送事件,可以理解成"服务器给你开的一条单向直播流") 的方式实现的:连接建立后,服务器可以持续不断地往这条连接上发小数据包,每个小包里装着"又生成了一点内容"。

流回来的不是"字",是"事件"

这里有个关键认知:流回来的东西不是一个个字,而是一组结构化的事件。你可以把模型的一次回复想象成搭乐高,事件就是一块块零件:

arduino 复制代码
事件 1:message_start(一条新回复开始了)
事件 2:content_block_start(开始一个"内容块",比如一段文字)
事件 3:content_block_delta ------ "你"(这个块里新增了一点内容)
事件 4:content_block_delta ------ "好"
事件 5:content_block_delta ------ ","
   ......(几十个 delta 事件,每个带一两个字)
事件 6:content_block_stop(这个内容块结束了)
事件 7:content_block_start(开始一个新块------这次是"工具调用"块)
事件 8:content_block_delta ------ 工具名和参数的片段
   ......
事件 9:content_block_stop
事件 10:message_delta(回复快结束了,附带用量统计)
事件 11:message_stop(整条回复结束)

为什么要搞得这么复杂,不直接发文字?因为模型的回复不只有文字,还可能有工具调用思考过程用量数据,它们是不同类型的"块"。用"开始块 / 块里新增内容 / 结束块"这套事件,就能用统一的方式描述所有类型。

核心循环怎么消费这些事件

核心循环(那个说书人)每接到一个事件,就干两件事:

  1. 转发给界面:如果是"文字块新增内容",界面就把这一两个字打到屏幕上------你看到的逐字输出就是这么来的。
  2. 自己记账:累计用量(花了多少 token)、记录"有个工具调用块正在成形"。

等到 message_stop,它把这一路攒下来的块拼起来,就得到了模型这一轮的完整回复:一段文字 + 可能的一个或多个工具调用请求。

统一形状的重要性

项目支持好几家不同公司的 AI 模型(第 7 篇讲),各家推过来的事件格式都不一样。项目做了一层"翻译":不管哪家模型,进来后都被翻译成上面这套统一事件。这样核心循环和界面只需要认一种格式,换模型时它们完全不用改。这是整个系统里非常重要的一个设计思想------在边界处做翻译,内部保持统一


3.4 一场长对话是怎么被安排得明明白白的

3.2 讲的核心循环,关注的是"一圈怎么转"。但一场真实对话是几十上百圈,中间还夹杂着:用户中途插话、模型被取消、出错重试、自动压缩、斜杠命令、子任务......把这些安排好的,是一个更高层的调度员角色。

核心循环 vs 调度员:工人和工头

如果说核心循环是"埋头干活的工人"(负责把一圈转完),调度员就是"工头",它站在更高处管这些事:

  • 接收用户输入并分类 :用户敲的东西可能是一句正常的话、一个 / 开头的斜杠命令、一个拖进来的文件。调度员先分辨类型,再决定怎么走。
  • 组装每一圈需要的资料:系统提示、聊天记录、当前能用哪些工具、记忆内容......每圈开始前由调度员凑齐。
  • 管聊天记录:每产生一条消息(用户的、模型的、工具结果的),调度员负责放进记录、并且落盘存好(防止程序崩了丢记录)。
  • 处理中断:用户在模型说话时按了取消,调度员负责"叫停当前这一圈",并且收拾残局。
  • 触发压缩:发现聊天记录快撑满容量了,安排"划重点"。
  • 管副作用:改文件前先做个快照(方便后悔)、记账(花了多少钱)、通知界面更新。

为什么要分这两层

你可能会问:为什么不让核心循环直接把这些事都干了?因为混在一起会变成一团乱麻。核心循环需要保持简单可靠------它只关心"发请求、收事件、执行工具"这一件纯粹的事,而且它要能被反复复用:

  • 终端对话里跑的是它;
  • 被程序调用时跑的是它;
  • 被编辑器指挥时跑的是它;
  • 连 AI 派出的"子助手"(第 9 篇讲)内部跑的还是它。

如果核心循环里塞满了"怎么显示界面、怎么处理斜杠命令"这些和具体使用场景相关的逻辑,它就没法在无界面的场景下复用了。

所以分工是:核心循环是"纯干活的发动机",不关心谁在用它;调度员是"司机",知道要去哪、走哪条路、乘客有什么要求。四种使用形态(第 1 篇讲的四件外套)各有各的"司机",但发动机是同一台。这就是为什么同一套内核能穿四件外套。

消息队列:用户话太多怎么办

还有个细节:模型正在干活的时候,用户又敲了两句话发出去,这些话怎么办?总不能丢掉。调度员维护了一个排队机制:用户中途发的内容(包括斜杠命令)先进队列排着,当前这圈干完了,再按优先级从队列里取出来处理。

这就像你在餐厅吃饭,服务员(核心循环)正在给你上菜,你又招手加了两个菜------新要求不会打断正在上的那盘菜,而是记下来,等这盘上完接着处理。


3.5 聊天记录在程序内部长什么样

核心循环每圈都要带着"全部聊天记录"去见模型,那这份记录在程序里到底是什么?

它是一个消息列表,每条消息有类型

聊天记录就是一个有序的列表,列表里每一项是一条消息,每条消息盖着一个"类型章"。最主要的几种:

消息类型 谁产生的 装什么
用户消息 你输入的文字、拖进来的文件/图片
助手消息 模型 模型说的话 + 它请求的工具调用
工具结果消息 程序 工具执行后返回的内容(命令输出、文件内容......)
系统消息 程序 各种通知:"已进入计划模式""上下文已压缩"之类
摘要消息 程序 压缩历史后生成的"前面讲了什么"摘要

注意一个有意思的点:工具调用和工具结果是两条分开的消息,而且必须配对

模型说"我要调读文件工具"------这是助手消息里的一个"工具调用"块。 程序执行完,把文件内容还回去------这是一条独立的"工具结果"消息。

模型下一圈必须能看到"我要了什么、结果是什么"成对出现,否则它会困惑("我上次要的扳手呢?")。项目里有专门机制保证这种配对:正常情况下自动配好;如果因为中断等原因配不上了,程序会补一个占位结果("这个工具的结果因为中断丢失了"),而绝不让模型对着一个"要了工具却没有下文"的记录发懵。

一条消息里不只是文字

以工具调用块为例,它里面装着:调哪个工具(名字)、传什么参数(一个结构化的数据,比如文件路径)、一个唯一编号(用来和结果配对)。

用户消息里也不只是文字:你拖一张图进来,图片以数据形式装在消息里;你引用一个文件,文件内容或摘要装在里面。所以"消息"是个口袋,文字、图片、文件、工具请求都能装。

发给模型前还要"整理仪容"

内部记录为了好用,存得比较"丰富"。但发给模型前,要过一道整理:

  • 去掉重复内容(同一份文件内容没必要出现两次,白花钱);
  • 剥掉一些内部标记(给程序看的记号,模型不需要看);
  • 超大的工具结果截断(一个命令输出十万行,全发过去既超容量又费钱,截断后留个"结果太长,已截断"的说明);
  • 按模型厂商的要求摆成它要的格式。

为什么记录要落盘

每条消息产生后,调度员会顺手追加写到磁盘上的一个文件里(每行一条消息的 JSON 格式)。这带来两个大好处:

  1. 不怕崩:程序崩了、电脑重启了,聊天记录还在。下次启动可以"恢复上次对话"。
  2. 能回溯:你可以翻看任何一次历史对话,甚至"倒带"回到某个时间点(第 8 篇讲的"后悔药"就靠这个)。

3.6 任务跑了一半怎么停、怎么接着跑

真实使用中,"停"和"续"是高频需求:模型说了一堆废话你想打断、工具跑太久你想取消、命令跑一半你想退出、第二天想接着昨天的活干。这一章讲这些怎么实现。

随时能叫停:取消信号

程序里有一个叫"取消控制器"(AbortController,可以理解成一个"总闸开关")的东西。核心循环在每一个可能等待的地方都会瞄一眼这个开关:等网络数据时、等工具执行时、每转一圈的间隙。

你按取消(通常是双击 Esc 或 Ctrl+C),程序扳下总闸。正在进行的网络请求被中止;正在跑的工具(比如一个长时间命令)会收到终止信号;还没开始执行的工具直接不执行了。

为什么要在这么多地方"瞄一眼"?因为如果只在循环开头检查,那么模型一旦开始说话,这几十秒里你按什么都没用------必须在流式接收的每一个片段之间都有检查点,才能做到"一按就停"。这也正是 3.2 讲的"生成器每段之间能停下来"带来的好处。

中断后怎么收拾残局

中断不是"拔电源",要体面收场:

  • 已经收到一半的模型回复,要么丢弃、要么标注"(已被用户中断)";
  • 正在执行的工具,如果已经改了文件,改动保留(你可以事后撤销);如果还没改,安全终止;
  • 工具调用没等到结果的,补上"结果因中断缺失"的占位消息(3.5 讲的配对机制);
  • 聊天记录保持一致状态,下一轮可以正常继续。

后台跑、前台等:子任务

有些活特别久(跑一大批测试、派出一个子助手去调查整个代码库),让用户干等着不合理。程序有一个"子任务"概念:这类活可以在后台跑,你继续和主对话聊别的;它跑完了,结果再回到聊天记录里。

子任务是可以订阅的:界面上显示一个"后台任务:正在跑测试......"的状态,进度实时更新;你也可以随时手动停掉它。这和核心循环的取消机制用的是同一套开关思路。

关掉程序还能续:会话持久化

因为聊天记录逐条落盘了(3.5 讲的),"接着昨天干"就成为可能:

  • 程序启动时可以列出历史会话(每个会话是一个记录文件);
  • 你选一个,程序把记录读回来,聊天记录恢复成当时的样子;
  • 甚至可以"从某条消息处岔开"------回到历史上的某个点,从那重新开始一条新分支(类似 git 的分支概念)。

"倒带"功能

更有意思的是"后悔药":由于每次改文件前程序都给文件拍了快照(第 8 篇细讲),配合聊天记录,你可以让对话"倒带"回到任意一次工具调用之前------聊天记录回退,文件也跟着回退到当时的样子。就像游戏读档。


本篇小结

  • Agent 循环是心脏:模型想一步、程序干一步、把结果还回去、模型再想,直到模型收工。
  • 核心循环用生成器实现,像说书人"讲一段停一下",为的是逐字显示、随时能插队执行工具、随时能取消。
  • 模型的回复通过流式事件一块块到达:开始块、新增内容、结束块;内部统一成一种格式,换模型不用改内部代码。
  • 核心循环是发动机,调度员是司机:发动机只管转圈,司机管输入分类、资料组装、记录落盘、中断处理,所以同一份内核能穿四件外套。
  • 聊天记录是有类型的消息列表,工具调用和结果必须成对,每条消息落盘保存。
  • 取消 靠总闸开关 + 到处设检查点;续接靠记录落盘 + 文件快照,于是有了恢复会话、分叉、倒带。

下一篇放大循环里的"干活"环节:AI 的那双手------工具系统。

相关推荐
小磊哥er26 分钟前
深入解构Claude Code - 第 2 篇 · 启动的秘密
javascript·ai编程
月月大王的3D日记1 小时前
Three.js 入门系列(9):从零搭一座“闹鬼小屋”
前端·javascript
开开心心就好2 小时前
电子教鞭工具支持画框写字插图片功能齐全
android·开发语言·前端·javascript·人工智能·pdf·html
Dovis(誓平步青云)2 小时前
拍视频前先把镜头想清楚:做一个分镜取景辅助器
android·java·服务器·javascript·人工智能
2601_962071573 小时前
Java进阶(vue基础)
前端·javascript·vue.js
研☆香3 小时前
数组方法 splice讲解 拓展
开发语言·前端·javascript
————A3 小时前
Agent 文件处理中间件
开发语言·javascript·ecmascript
Patrick_Wilson4 小时前
Superpowers 与 Codex Harness:冲突分析与治理提示词
agent·ai编程·cursor
ITresearchGuest4 小时前
LangChain JS 入门:快速搭建前端 AI 开发环境
前端·javascript·langchain