Agent Loop 深度拆解:上下文组装、工具决策与观察循环
上一篇文章追踪了用户输入到 Agent 输出的主执行链路,但有一个核心机制被刻意搁置:当单次大模型调用无法完成任务时,系统如何通过多轮迭代逐步逼近目标? 本篇完全聚焦 Agent Loop 的内部运转——拆解每轮迭代中的上下文组装、模型调用、工具路由、执行反馈,以及循环如何被约束在可控边界内。
架构图

上图对应本文的拆解主线。箭头流向即为一轮迭代的完整数据路径:assistant 发起调度 →query 管控预算与状态 → 模型返回决策 → 工具包执行副作用 → 结果回填对话历史。读者可将后续每个章节对应到图中的一段箭头。
五个核心模块的职责边界
Agent Loop 不是单一文件,而是五个模块在运行时拼合出的虚拟状态机。明确各自边界,才能理解后续协作链路。
| assistant | src/assistant/ | index.test.ts 注释 "Coordinates core workflows and business logic"。 | |
| query | src/query/ | transitions.ts)、 token 预算(tokenBudget.ts)、终止钩子(stopHooks.ts)、运行时配置(config.ts、deps.ts)。不产生副作用,只做决策与约束。 | |
| constants | src/constants/ | apiLimits.ts 定义迭代轮数上限、单次请求上限等不可逾越的常量;tools.test.ts 和promptEngineeringAudit.test.ts 说明此目录还承载工具定义审计与提示词合规检查。 | |
| hooks | src/hooks/ | useScheduledTasks.test.ts 表明 hooks 层处理定时任务调度;replBridgePermissionHandlers.test.ts 和swarmPermissionPoller.test.ts 说明 hooks 同时参与权限回调与 Swarm 模式的轮询协调。在单 Agent Loop 中, hooks 主要负责权限拦截与任务队列管理。 | |
| proactive | src/proactive/ | useProactive.ts 是一个 React 风格的 hook ,基于state.baseline.test.ts 的存在推断它维护独立的状态机,用于在循环的观察阶段主动触发后续动作——例如当工具返回"文件未找到"时, proactive 层可以决定是否主动追加一次搜索操作,而非被动等待模型重新决策。 |
依赖方向是单向的:assistant → query → constants(配置读取),assistant → hooks(权限与调度),assistant → proactive(主动行为注入),assistant → tools(副作用执行)。没有反向依赖。
模块分层图

上图对应这条依赖链。assistant 位于 Application 层顶部,向下依赖query、hooks、proactive 的控制信号,再向下调用builtin-tools 和agent-tools。读者可沿纵向箭头确认:控制流自上而下,数据流自下而上回填。
协作链路一: agent-loop-iteration —— 一轮迭代的完整闭环
这是最核心的链路,按输入 → 处理 → 输出/副作用 展开。
输入:用户消息进入循环
用户请求经过 CLI 或 Bridge 层到达assistant 模块。assistant 做两件事:1. 将用户消息追加到对话历史。2. 调用query/config.ts 获取当前运行时配置,调用query/tokenBudget.ts 检查剩余预算。
如果预算耗尽,循环在此直接终止,返回截断提示。否则进入处理阶段。
处理:上下文组装 → 模型调用 → 意图解析
上下文组装(对应下一节详述的context-assembly 链路):assistant 将系统提示词、项目记忆(来自memdir 模块)、完整对话历史、可用工具定义拼装为一个请求体。query/transitions.ts 将状态机推进到THINKING 状态。
模型调用:请求体发送给大模型。
意图解析:模型返回两种可能:- 纯文本(content)→ 无工具调用,循环准备终止。-tool_use 请求 → 包含工具名和参数,循环继续。
输出/副作用:工具路由 → 执行 → 结果回填
query/transitions.ts 将状态从THINKING 推进到ACTION。assistant 解析tool_use 中的工具名,执行路由(对应第三节tool-call-routing 链路),分发到builtin-tools 或agent-tools。
工具执行完成后,结果被封装为ToolResultMessage,追加到对话历史。transitions.ts 将状态推进到OBSERVATION。
此时proactive 模块介入:useProactive.ts 检查工具结果,判断是否需要主动追加操作。如果需要,直接注入一条额外的工具调用到当前轮次,无需再等模型决策;如果不需要,transitions.ts 将状态推回THINKING,assistant 拉起下一轮模型调用。
一轮迭代的完整数据流:
用户消息→assistant(追加历史)→query/tokenBudget(预算检查)→assistant(组装上下文)→大模型(推理)→assistant(解析意图)├─纯文本→终止└─tool_use→assistant(路由)→builtin-tools/agent-tools(执行)→assistant(结果回填)→proactive(主动行为判定)→query/transitions(状态→OBSERVATION→THINKING)→回到"组装上下文",开始下一轮核心流程图

上图对应这条完整链路。读者可沿流向线追踪:中间的分叉点即模型返回后的分流(直接输出 vs 工具调用),右侧的回环箭头即结果回填后重新进入"组装上下文"的闭环路径。
协作链路二: context-assembly —— 上下文组装与截断
上下文组装是每轮迭代的第一步,也是资源约束最集中的一步。
输入
assistant 从四个来源收集数据:1.系统提示词:硬编码的系统指令,定义 Agent 的行为边界。2.项目记忆:来自src/memdir/ 模块,包含findRelevantMemories.ts(相关记忆检索)和memoryScan.ts(记忆扫描),按相关性注入当前任务的上下文。3.对话历史:包含用户消息、模型回复、工具调用记录、工具返回结果。4.工具定义:当前可用的工具 schema ,告诉模型它可以调用什么。
处理:拼接与截断
四部分拼接后,总 token 数可能远超模型窗口。此时query/tokenBudget.ts 介入执行截断策略。
截断优先级(基于模块职责推断):1.系统提示词不截:这是行为底线,截断后模型可能失控。2.工具定义不截:缺少工具定义,模型无法发起调用。3.项目记忆按相关性裁剪:memdir 模块提供memoryAge.ts(记忆老化)机制,低相关性的记忆被优先丢弃。4.对话历史从最早处裁剪:早期消息被丢弃或压缩为摘要(src/components/__tests__/compactMessages.test.ts 的存在证实了消息压缩机制的存在)。
输出
一个符合tokenBudget 约束的请求体,发送给大模型。
设计取舍:截断历史意味着模型会丢失用户早期的指令细节(例如"所有变量名用驼峰")。好处是保证 API 调用不崩;代价是长任务后期可能偏离初始约束;风险边界由constants/apiLimits.ts 的硬上限兜底——即使截断策略有 bug ,单次请求的 token 数也不会超过 API 物理限制。
协作链路三: tool-call-routing —— 双轨工具路由
模型返回tool_use 后,assistant 需要将抽象的工具名映射到具体的执行器。
输入
模型返回的结构化对象,包含tool_name 和parameters。
处理:查表路由
项目在package.json 层面将工具物理隔离为两个包:
packages/builtin-tools:基础工具。推断包含文件读写、内容搜索、字符串替换等高频低风险操作。依赖链短,权限要求低。 packages/agent-tools:高级代理工具。推断包含外部通信、复杂多步操作链、需要更高权限的操作。依赖链长,可能引入第三方 SDK 。
路由逻辑(基于src/constants/tools.test.ts 的存在推断):系统维护一个工具注册表( Registry ),每个工具注册时声明自己所属的包。assistant 拿到tool_name 后查表,得到目标包和入口函数,分发执行。
输出/副作用
目标工具包执行具体操作,返回结果。
hooks 模块在此介入:根据src/hooks/__tests__/replBridgePermissionHandlers.test.ts 的存在,部分工具调用在执行前需要通过 hooks 层的权限回调确认。高权限操作(如执行 shell 命令)被拦截,等待用户确认或自动拒绝;低权限操作(如读取文件)直接放行。
三重终止防线
循环必须有确定的退出条件。系统在三个层级部署了拦截:
第一层:query/stopHooks.ts —— 语义终止
推断此模块实现业务层面的循环退出判定:- 模型返回纯文本(无tool_use)→ 自然结束。- 连续多轮工具返回相同错误 → 熔断退出。- 用户主动中断( Ctrl+C )→ 强制退出。
stopHooks 在每轮迭代的"意图解析"阶段被assistant 调用。如果命中任一终止条件,transitions.ts 直接将状态推到DONE,不再发起下一轮模型调用。
第二层:query/tokenBudget.ts —— 资源终止
即使语义上没有终止, token 预算耗尽也会强制截断。此时系统会将当前已有的对话历史和部分结果拼装为一条"预算不足"的终止消息返回给用户。
第三层:constants/apiLimits.ts —— 硬性天花板
前两层都是可配置的、有弹性的。apiLimits.ts 中的常量是最后兜底:最大迭代轮数、单次请求最大 token 数。无论前两层是否正常工作,这些硬编码的上限确保循环物理上不可能无限运行。
src/constants/__tests__/promptEngineeringAudit.test.ts 的存在说明,这些限制值不仅被使用,还被纳入提示词工程的审计测试中——确保模型在被强制截断时,仍然能产出可用的部分结果。
一个设计取舍的完整分析:双轨工具隔离
决策:将工具拆为builtin-tools 和agent-tools 两个独立包
好处:-依赖隔离:builtin-tools 只保留核心依赖,升级周期可控。agent-tools 引入的第三方 SDK 崩溃不会波及基础工具链。-权限分层:hooks 层可以根据工具所属包施加不同等级的权限检查。基础操作快速放行,高级操作严格审核。-独立演进:新工具的添加不会触发基础工具包的版本发布和回归测试。
代价:-路由复杂度:assistant 必须维护一个跨包的工具注册表,每次新增工具需要同步更新注册逻辑。-测试成本:工具的集成测试需要同时安装两个包, mock 链路更长。
风险边界:- 如果注册表与实际包的导出不一致(例如工具名拼写错误),循环会在运行时抛出"工具未找到"异常。stopHooks 的熔断机制会捕获这类异常,防止循环卡在重复重试上。- 如果未来工具数量膨胀到数百个,单表路由可能成为性能瓶颈。当前架构下,这个上限远未触及。
小结
Agent Loop 的运转依赖五个模块的精密协作:assistant 调度、query 管控、constants 兜底、hooks 拦截、proactive 主动补充。每轮迭代走完"组装 → 推理 → 路由 → 执行 → 观察"的闭环,三重防线确保循环不会失控。
下一篇跳出单 Agent 的循环,拆解多 Agent 协作架构:src/coordinator/ 的 Coordinator 模式、 Swarm 调度机制,以及 Worktree 如何为并行 Agent 提供文件系统隔离。
篇间导航
上一篇:主执行链路追踪:从用户输入到 Agent 输出的完整路径 下一篇:多 Agent 协作架构: Coordinator 模式、 Swarm 调度与 Worktree 隔离
夜雨聆风