01. Loop
Agent 的心跳:把「一次对话」拆开,你才看得懂它
序章里立了个 flag :一个 Agent 产品 = 模型(发动机)+ 一整套 Harness(控制面)。从这一篇起,我们就一层层把这套 Harness 拆开。第一站,正是最底层那个让 Agent 能「自己干活」的循环,可以比作 Agent 的心跳。前面埋下的小问题:你问它一句话,它内部到底跑了几个来回?为什么这个数,不能直接拿去给用户当「第几轮对话」的计数?这一篇就来展开说说
🪧 对一个数字的理解偏差,影响到界面做错
刚开始给 SmartWriter 做前端的时候,想在对话区角上标一个「第 N 轮」,就像很多聊天产品那样,让用户对写了多久有个体感
看起来也挺简单的,SDK 每次任务结束,都会回一个叫 num_turns 的字段,字面意思不就是「轮数」?看起来顺理成章,可实则暗藏玄机
譬如,让模型「帮我把第三段润色一下」,它还没跟我聊第二句呢,那个数字「啪」地跳到了 3。再发一句,它又蹦到了 7。用户明明只说了两句话,界面上却写着「第 7 轮对话」,谁看了不晕?
说到底:我对 turn 这个概念,从一开始就理解错了。 这一篇,就从这个小误会切入,揭秘「Agent 凭什么能自己干活」
🧭 基础概念:turn ≠「对话轮次」,是 Agent 的一次「心跳」
这里的 turn,并非用户眼中的「一轮对话」,它只是 Agent 内部的一次「工具往返」
要讲清楚这个,得先回到序章那个问题:同样是 Claude,为什么 Claude Code 能自己读代码、改文件、跑测试一路干到底,而我裸调 API 的程序只会「你说一句、我回一句」?差别就在这台引擎,业界称之为 Agent Loop(可以比作 Agent 的「心跳」,一个会自动转起来的循环)
它转起来是这样的:模型先产出一段输出,这段输出里可能夹着「工具调用」(比如「我要读一下这个文件」);如果有,系统就会拦截、替它把这个工具执行了,再把结果回灌给模型;模型拿到结果接着往下思考,可能又要调下一个工具。这么一圈一圈转,直到模型产出一段「不带任何工具调用」的纯文本,循环才停,控制权最终交还给你的代码
用一段伪代码看得更清楚:
# Agent Loop 的骨架(伪代码)
while True:
out = model.generate(context) # ① 模型产出(可能夹着工具调用)
if not out.tool_calls: # ② 没有工具调用了?
return out.text # 收工,把控制权还给你
for call in out.tool_calls: # ③ 有工具调用
result = run(call) # 替它执行
context.append(result) # 结果回灌进上下文
# 循环继续,进入下一个 turn
有了这个背景,前面那个看似"乱跳"的数字就变得顺理成章了:这个循环转一圈,就是一个 turn。 起初那句「润色第三段」,它内部转了好几圈:先 Read 读一遍原文(一个 turn),再 Edit 改写(又一个 turn),最后产出一句「改好了」的纯文本才收工。用户只说了一句话,模型内部却「心跳」了三下。num_turns 数的正是这个心跳数,不是我们跟它说了几句话
一个 prompt(你的一次发问),内部可能驱动好几个 turn(Agent 的工具往返)。turn ≠ 对话轮次
我个人觉得,这是理解 Agent 的第一道门槛,也是它和「聊天机器人」的核心区别。聊天机器人是「一问一答」,问和答严丝合缝地一比一;而 Agent 是「一次委托、内部自主折腾多步」,你给一个目标,它自己决定要转几圈才够。能自己决定转几圈,正是「自己干活」背后的技术真相
下面这张图,以「润色第三段」为示例,把内部的心跳节奏画出来:
🔩 Claude Code 是怎么做的:这颗心脏一直在跳
这套循环不是凭空设计的概念,Claude Code 内部就是这么跑的
Claude Code Harness Book 里专门有一章讲这个,叫「Query Loop: The Heartbeat」,直译就是「查询循环:心跳」。它把这个循环称作整台 Agent 的心脏:你在 Claude Code 里说一句「把这个模块的测试补齐」,它之所以能自己读文件、写代码、跑测试、看报错、再改一路折腾,靠的就是这颗心脏在不停地跳,一个 turn 接一个 turn,直到活干完(产出不再需要调工具时)才停下。Learn Claude Code 的源码解读系列第一篇也是讲这个循环,从实现视角把 query loop 的每一拍拆开来看,感兴趣的朋友可以对照着读
把它跟裸调 API 的区别摆一起,会更一目了然:
- 裸调 Messages API:你发一段话,模型回一段话,
结束。想让它「读个文件再回答」?需要你自己写代码去读、去拼进下一次请求,模型摸不到你的世界 - Claude Code 的 Query Loop:模型自己说「我要读这个文件」,循环就替它读了、把内容喂回去,它接着往下干。模型从此有了「伸手去够外部世界、再根据反馈继续」的能力
回想 Claude Code 的使用日常,心跳节拍能看得到。它每转一个 turn,终端里就动态刷新「读取文件」「修改代码」「运行测试」,你能感知到它一步步推进、一步步自我纠错。正因为过程摊开在眼前,用 Claude Code 的开发者天然就理解「我说一句、它内部走了十几步」这件事,不会觉得新奇
序章里那句「差的不是模型,是模型外面那一层」,落到最底处,第一层就是这颗心脏。有了它,模型才从一个「问答机」,变成一个「会自己推进多步任务的好帮手」,而它也是后面所有机制的地基,记忆、工具、权限,全都是挂载在其之上
🛠 SDK 提供了什么:一张概念对照表 + 五个「控制开关」
Claude Agent SDK 把 Claude Code CLI 的能力封装成了后端可编程的接口。落到 Loop 这一层,它提供了两样东西:一套概念对应关系,和一组控制循环怎么转的参数
先说概念
SDK 里的名词,和写作任务的概念,需要先对齐,不然后面全是鸡同鸭讲:
| 写作任务里的概念 | SDK 里的概念 | 一句话说明 |
|---|---|---|
| 一次写作任务 | 一个session(一条会话历史,落盘成 jsonl) | 从新建任务到定稿,中间多次交互共享同一段上下文 |
| 用户的一次发问 / 指令 | 一个prompt(驱动若干 turn) | 「帮我润色第三段」,内部可能 Read→Edit→产出,跨好几个 turn |
| Agent 的一次工具往返 | 一个turn | 用max_turns 限它的次数;它数的是工具往返,不是对话轮 |
| 流式输出的每个增量片段 | StreamEvent | 逐字打字效果的增量片段 |
| 每轮 Claude 的回复 | AssistantMessage | 里头装着文本块和工具调用块 |
| 每次工具执行的结果 | UserMessage(tool result) | 回灌给模型的那个结果 |
| 任务结束的终结信号 | ResultMessage | 含最终文本、token、花费、session_id,还有num_turns |
这张表看着琐碎,但它是整个专栏的「概念字典」。尤其头三行的锚点关系,一次写作任务 = 一个 session、用户一句话 = 一个 prompt、turn 是内部工具往返,后面每一篇都会反复用到
📐 深挖 · session、prompt、turn 是「三层套娃」
session(一次写作任务,一条会话历史) └─ prompt A(你说:帮我列个大纲) │ └─ turn 1(Read 底稿)→ turn 2(产出大纲文本,停) └─ prompt B(你说:把第二点展开) │ └─ turn 1(Read)→ turn 2(Edit)→ turn 3(产出,停) └─ prompt C ……一个 session 里,你会发好几个 prompt;每个 prompt 内部,又会转好几个 turn。越往里,越是「模型自己的事」,越不该拿给用户看:session 是用户能感知的(一个写作任务),prompt 是用户能感知的(他说的每句话),而 turn 藏在最里层,是模型自主折腾的过程,用户大概率也不关心
再说控制
这颗心脏不能让它无限乱跳,SDK 提供了五个「控制开关」,管理循环怎么转、转多久、花多少钱:
| 参数 | 管什么 | 一句话 |
|---|---|---|
| Model | 用哪个模型 | 质量、速度、成本的总开关 |
| Max budget | 单次任务的花费上限 | 命中即停,防长任务偷偷烧钱 |
| Effort | 模型推理的「用力程度」 | low / medium / high / xhigh / max,越使劲越慢越贵越深 |
| Permission mode | 工具调用要不要审批 | 决定它动手前要不要先问你(「权限」篇细讲) |
| Max turns | 心跳次数的硬上限 | 纯保护性的保险丝,防它抽风转成死循环 |
📐 深挖 · 循环凭什么会停?停不下来又怎么办
前面说循环「转到产出纯文本才停」,这里再抠一下结束判据:SDK 判断一个 prompt 是否结束,看的只有一件事,模型这一轮的产出里还有没有工具调用。有,就执行、回灌、继续转;没有(是一段纯文本回复),循环立刻收工,把控制权还给你的代码。注意它不看「任务是不是真的做完了」,只看「模型还想不想用工具」,这两者通常一致,但不总是
隐患就藏在这个「不总是」里:万一模型一直想用工具、迟迟不肯产出纯文本呢?现实里也会发生,比如某个工具反复报错、模型一根筋地反复重试;又比如它在两个改法之间来回横跳,震荡着收不了尾。这种时候,「无工具调用才停」这个判据永远够不到,循环理论上会一直转下去,token 哗哗地烧
这正是
max_turns的职责,它是一根保险丝。别的开关调的是质量,比如 effort 管的是「用多大力气推理」;max_turns只干一件事,心跳数一旦撞到上限,不管活干没干完,都强行熔断、把控制权交还给你。正常任务一般够不到它(一次润色也就三五个 turn),它只在「失控」时才现身,所以是纯保护性的,跟质量无关落到 SmartWriter,这根保险丝我按场景调了灵敏度:普通写作 prompt 给的上限是 30(够任何正常任务转完,又拦得住死循环);而 plan 阶段(只探索、出大纲、不动手)给得更短,10 上下。因为计划阶段本就不该有大量工具往返,把上限压低,给「防跑偏」装了个更灵敏的报警器,它要是转了十几圈还没交出大纲,多半是钻牛角尖了,早熔断比晚熔断省钱
一句话总结:结束判据管「正常怎么停」,
max_turns管「异常怎么兜」。 前者是循环的常态出口,后者是防它把你钱包烧穿的最后一道闸
SDK 把这五个开关都开放可调了。但「能调」不等于「都该甩给用户去调」。哪个交给用户、哪个系统在后台静默覆盖,这些都是做产品时要做的权衡和决策,也是我觉得最有嚼头的部分
🧩 SmartWriter 做了哪些定制
把不必要的认知负担,挡在用户写作之外
和开发者写代码稍有不同,我们在正常写文章时,往往不关心模型 Read 了几次、Edit 了几回,要看到的是模型如何捋清写作思路和优化策略,把稿子一点点变好,中间那些工具往返反而是噪声。因此在前端处理上,对流式返回结果做了专门的适配,会刻意突出 thinking block,同时弱化工具调用的呈现
另外,也对 SDK 开放的 5 个开关参数,按「用户该不该关心」切成了三档(参考下表)。出发点是让用户尽可能专注在写作本身,其它就交给系统来做合理把控和兜底。留给用户的判断简化为:复杂写作任务用高级模型,日常写作用轻量模型即可
| 参数 | 归谁管 | 为什么这么分 |
|---|---|---|
| Model | 🙋交给用户 | 它直接牵动质量和成本,用户有权选。SmartWriter 主写作默认 Claude Sonnet 5/DeepSeek-V4-Flash,用户能在设置里改默认、也能新建任务时临时覆盖 |
| Effort | ⚙️系统在后台管 | 「用多大力气推理」本来想按命令类型细分(润色低档、重写高档),摸底后发现这参数是跟 session 绑死的,一个任务中途没法按每句话切换档位,勉强分档反而可能拖累后面的步骤,索性统一交给模型自己判断,不拿这层专业细节烦用户 |
| Permission Mode | ⚙️系统在后台管 | 用户只需感知「在计划 / 在执行」「要不要我联网」,感知不到acceptEdits 这种模式名 |
| Max budget | 🔧纯后台保险丝 | 单次用户指令所触发的完整 Agent Loop 的「硬熔断器」,它累计该轮中的多次模型调用,包括输入上下文、输出、thinking 以及工具往返后的后续调用。主要为 Claude 模型的 Adaptive Thinking 设上限,DeepSeek 关闭此设置(因为 Claude CLI 不认识 DeepSeek 原生模型,会按昂贵模型错误估价,导致正常 DeepSeek 任务被误判超预算) |
| Max turns | 🔧纯后台保险丝 | 防失控的上限(比如单次给 30,计划阶段更短),用户根本不需要知道它存在 |
那个乱跳数字的正解:前端自己数,不用 num_turns
回到开头埋下的小问题。搞明白 turn 的定义,解法也就顺理成章:
num_turns 数的是 Agent 内部的工具往返,是「心跳数」;而用户一般关注在「第几轮」,是他自己开口的次数,是「对话数」。在界面输入框的处理上,我把它们按状态分开展示:模型工作过程中能看到 turns 的实时更新,一旦该轮工作完成,则显示当前对话在「第 N 轮」
⚖️ 说在最后:一个内部计数,和垂直场景的落地取舍
这一篇主要讲了两件事:turn 的本质(Agent 凭什么能自己干活),和参数的取舍(为什么不把所有参数放出来):
⚖️ 取舍现场 · 循环控制的五个参数,暴露还是藏起来
「偷懒」的默认做法 SmartWriter 的选择 为什么这么选 参数暴露 尽量都摆出来,证明「可配置、很强大」 只露 Model,其余系统兜 降低用户认知负担,effort 档位这种细颗粒度的调节交给系统控 turn 计数 直接拿 num_turns显示,省事前端自己按 prompt 数「第几轮」 心跳数 ≠ 对话数,混用不合理 设计立场 加法:能配的都给你满配 减法:先问「用户真需要看到吗」 按垂直产品的体验,作合理筛选过滤
写到这有点小感慨。System Prompt、权限、Subagent 那些机制听着高大上,但在做产品的日常,更多是这种「一个内部数字、一个调节参数该不该给用户看」的小决策。它们一个个都不起眼,可正是这一堆小取舍攒起来,才决定了我们做出来的到底是「又一个通用聊天框」,还是「一个真的懂业务场景的副驾」。魔鬼确实在细节里,只有扎进去、动手干,才能练出手感
好,关于 Loop,到这里算是摸了个大概:它会一圈一圈自转,直到活干完才停。但这里藏着一个新麻烦:它转起来是要吃「上下文」的,每转一圈,读进来的文件、工具结果、来回的对话,都在往那个有限的上下文窗口里塞。转得够久,窗口迟早会满。满了又会发生什么?系统会自动做一件叫「压缩」的事,把早期的内容摘成一段总结腾出地方。听着挺贴心,可对写作来说,这一压,很可能把你三段前刚定好的核心立意,悄悄给摘没了
这台引擎「转久了会失忆」的毛病,还有怎么帮它把写作时不能丢的东西抢救回来,是下一篇的事了。咱们继续聊