跳到主要内容

cline — 本课题摘录

读了哪几篇: 01-agent-loop(内核:无状态的工具调用循环)、03-orchestration-safety(会话编排与安全护栏)。 其余四篇(工具集、LLM 网关、检查点与多 agent)本轮没读——它们属工具层、换厂商与多 agent 课题。

它对本课题回答了什么

决定一:循环放在哪一层

循环本身不记事,记事的是包在外面的那一层;每一轮都新造一个循环实例。 (依据:Agent 库 · Cline · 内核:无状态的工具调用循环 —— AgentRuntime 无状态,不负责持久化也不负责跨会话记忆,给它一组消息和一组工具、跑到收尾就完事)

外层每次开跑,把整个历史当作初始消息塞进新建的循环里,跑完再存回。 (依据:Agent 库 · Cline · 会话编排与安全护栏 —— 所有跨轮状态活在 SessionRuntime 里,每一轮新造一个一次性的 AgentRuntime,把完整历史当 initialMessages 喂进去,跑完丢弃)

为什么要这么切: 它自己给的理由是——内核不持有状态,重跑一遍就是干净的,于是登录失效后重试、把一次运行重放一遍都变得可行。 (依据:Agent 库 · Cline · 会话编排与安全护栏 —— 注释点明每轮新建是为了让 OAuth 重试与 run 重放可行,会话级状态活得比任何一个 AgentRuntime 都久)

决定二:怎么认出模型要调工具

模型是流式吐字的,工具调用的参数也是一段段碎着回来。所以"认出来"分两件事:

  1. 归并碎片——同一个调用 id 的多段参数文本拼到一起;
  2. 保住顺序——用一个序列数组按到达顺序记"这段是文本"还是"这里有个工具调用的占位",等流结束再回填,文本与工具调用的相对顺序不会乱。 (依据:Agent 库 · Cline · 内核:无状态的工具调用循环 —— 用 sequence 数组按到达顺序记录文本段与工具占位、用 Map 按 toolCallId 归并参数碎片,流结束后回填以保住相对顺序)

决定三:结果怎么回填

「错误即消息」是它最要紧的一条。 工具抛错、参数是非法 JSON,都不中断循环,而是包成一条带"这是错误"标记的结果喂回模型。 (依据: shelf=agent/cline#01-agent-loop @1de61b178aec 事实=executePreparedTool 用 try/catch 包住,工具抛错被包成 {output:{error}, isError:true} 当消息喂回模型,循环不终止)

参数解析失败时,它当场拼一句明确的纠错文本("你给的参数不是合法 JSON,错在……")一起喂回去。 (依据:Agent 库 · Cline · 内核:无状态的工具调用循环 —— parseToolInput 在 JSON.parse 失败时就地拼出 "emitted invalid JSON arguments: ${error}" 的纠错文本喂回模型,而不是直接崩)

决定四:什么时候停

三个退出口,外加两道护栏。

退出口触发
自然收尾模型这一轮不再要工具
显式收尾模型调用了一个被标成"完成工具"的工具且成功
兜底超过轮数上限,抛错

(依据:Agent 库 · Cline · 内核:无状态的工具调用循环 —— execute 的三个退出口是 toolCalls.length===0、findCompletingToolMessage 命中、iteration 超过 maxIterations)

「完成」是工具的一个属性,不是特判。 收尾不看工具叫什么名字,看它有没有被标上"这个工具跑完就算收工"。 (依据:Agent 库 · Cline · 内核:无状态的工具调用循环 —— 收尾判定看工具的 lifecycle.completesRun===true,不硬编码工具名,任何工具都能被标成完成工具)

两道护栏挂在循环外面,不写在循环里:

护栏怎么判反应
循环检测给每次工具调用算一个签名(工具名 + 排序后序列化的参数),数连续相同的次数连续 3 次:往对话里塞一句"检测到重复,换个思路"的提示,不阻断;连续 ≥5 次:交给熔断器强停
连错熔断一轮里有工具失败且没有任何工具成功才记一次错;只要有一个成功就清零默认连续 6 次就停,但停之前留一个逃生口:可以裁决"给条指导继续跑"并把计数清零

(依据:Agent 库 · Cline · 会话编排与安全护栏 —— 循环检测用 工具名+排序后序列化参数 当签名数连续相同次数,soft=3 塞恢复提示、hard=5 交给熔断;连错判定是"本轮有工具失败且无任何工具成功"才记一次,有成功即 reset)

它的做法(可以抄的部分)

扩展点做成七个钩子,护栏挂在钩子和事件上,不进循环。 一次运行的首尾、每次调模型前后、每个工具前后、每个事件,各留一个挂点;任何钩子都能返回"停下,原因是……"来主动中止。 (依据: shelf=agent/cline#01-agent-loop @1de61b178aec 事实=HookBag 提供 beforeRun/afterRun、beforeModel/afterModel、beforeTool/afterTool、onEvent 七个挂点,applyStopControl 让任何 hook 返回 {stop:true,reason} 中止运行)

这条的价值在于:循环的正文可以一直很短。 检查点挂在"调模型前",循环检测挂在"工具前",连错熔断挂在事件流上——内核对它们一无所知。

批准逻辑不写死在内核里。 内核只调一个"请求批准"的回调,弹窗问人、自动批、还是在无人值守时一律拒,由宿主决定。 (依据:Agent 库 · Cline · 内核:无状态的工具调用循环 —— 内核只调 requestToolApproval 回调,autoApprove===false 时才触发,具体批准方式由宿主决定)

它没回答什么

  • 一轮的输入怎么拼——它只说"把整个历史当初始消息传进去",没讲系统提示怎么组装、超长了怎么裁。这一块要看 headroomhermes-agentcowagentkun
  • 工具清单怎么给模型——内核自带零个工具,工具从哪来、要不要每轮变,它不管。要看 beeai-frameworkdeepagents
  • 不用 function calling 怎么办——它走的是模型原生的工具调用。文本协议那条路要看 crewaiagenticseeksmolagents

坑与代价

  • 轮数上限不设就能无限转。 它自己写明:防失控完全靠上层的循环检测和连错熔断。 (依据:Agent 库 · Cline · 内核:无状态的工具调用循环 —— maxIterations 未设时循环可以无限转,防失控完全靠上层的循环检测与连错熔断)
  • 每轮重建实例的代价是"历史必须显式传进去"。 它记了一个修过的真实 bug:曾经忘了传初始消息,导致跑完之后历史被整段覆盖丢失。 (依据:Agent 库 · Cline · 会话编排与安全护栏 —— 注释记录了一个修过的 P1 bug——之前没传 seed 导致历史被 replaceMessages(runResult.messages) 覆盖丢失)
  • 流式拼装假设各家事件长同一个样。 内核只认"文本碎片 / 工具调用碎片 / 结束"这三类事件,厂商差异由另一层网关抹平——换句话说,这条抹平的活总得有人干,只是不在循环里。