这个循环里住着整个系统的灵魂。
设计动机:从最简单的循环开始
Agent Loop 的核心是一个 while 循环:调用 LLM,有工具调用就执行并把结果写回上下文,没有工具调用就退出。
Anthropic 的说法是对的:这就是核心。
但从这几行伪代码到生产可用,中间有一条很长的工程路。每一处改动背后都有一个现实问题驱动。
与 Anthropic 官方的共识与分歧
参考文章: Building Effective Agents(Anthropic,2024.12)
Anthropic 在这篇文章里把 Agent 描述为:
“LLMs using tools based on environmental feedback in a loop.”
这是本质上的正确抽象。Alice 的实现完全认同这个核心模型。
但 Anthropic 的文章聚焦于何时用 Agent、如何设计工具接口,对 Agent Loop 内部的状态管理讲得比较少。我们在工程实践中遇到的很多真实问题,恰恰发生在循环内部。
主要分歧点及其背后的工程逻辑:
| 问题 | Anthropic 建议 | Alice 的做法 | 为什么这样选择 |
|---|---|---|---|
| 工具调用时的文字说明 | 透明展示规划步骤 | 丢弃工具调用伴随文字 | 规划文字对下一轮推理贡献极小,却显著占用 token;在长任务中积累的规划文字可以浪费大量上下文空间 |
| 上下文管理 | 建议简化消息历史 | 多层压缩策略(按消息类型和价值密度分级处理) | 不同类型的消息价值密度差异很大,应该用不同成本的策略处理 |
| 系统提示词 | 静态注入为主 | 动态组装,缓存友好 | LLM API 的缓存机制要求 prompt 前缀稳定,动态内容放在末尾可以大幅降低 API 成本 |
| 循环终止 | 最大迭代次数 | 迭代次数上限 + 中止信号 + 不可恢复错误 | 纯迭代次数不够:用户随时取消的需求需要中止信号机制;某些错误无法通过重试解决需要立即终止 |
关于丢弃规划文字的深层思考:
Anthropic 建议透明展示规划步骤,出发点是让 Agent 的推理过程可观察。这个目标是对的,但实现方式可以分离:把规划步骤记录到日志(调试面板),对用户展示;从消息历史里删掉,不送给下一轮 LLM。可观察性和 token 效率可以同时满足。
Agent Loop 全景
Agent 循环的整体流程如下:
- 收到用户消息后,先组装系统提示词
- 动态组装当前可用工具集
- 进入 while 循环
- 每轮迭代中:检查上下文大小按需压缩,做消息整形,流式调用 LLM
- 如果 LLM 返回了工具调用,按并发安全性分批执行,结果写回上下文,继续循环
- 如果 LLM 只返回文字(没有工具调用),循环结束
- 循环结束后,异步触发后台任务:记忆提取、用户画像更新、Skill 改进分析
循环的入口与出口
Agent 循环的入口:用户消息(文字,可选图片)+ 历史上下文 + 配置参数。
Agent 循环的出口:一个事件流,持续发射各类事件。包括流式文字 token、推理内容(如果模型支持)、工具开始执行和完成的通知、上下文压缩事件、token 用量统计、以及最终的完成信号。
出口设计为事件流有几个原因:
1. 用户体验更好(看到流式输出)
2. 支持长时间任务(工具执行可能需要几秒甚至几十秒)
3. 便于中途取消(消费者可以随时关闭流)
循环前的准备:系统提示词组装
进入 while 循环之前,先组装系统提示词。这是有优先级的多层叠加:外部传入参数最高优先级,其次是用户自定义内容、热更新内容,最后是内置默认模板。
系统提示词的内容结构包含:人格定义、实例和会话元信息、项目记忆、用户画像、可用 Skills 列表、外部工具说明、渠道特定前缀,以及动态追加内容。
每次对话时,系统提示词前缀是静态的,对 LLM 的缓存机制友好。动态内容(当前日期、权限拒绝摘要)追加在最后,不破坏前缀的缓存命中。
Anthropic 的思路对比: Anthropic 在上下文工程文章中强调只在需要时才注入信息。Alice 的分层注入和缓存优先策略正是这一思路的具体实现。
动态工具集:每次循环重新计算
工具列表不是固定的,每次循环开始时动态组装。
工具的来源包括:内置工具(固定集合)、子 Agent 派生工具、Skills 衍生工具、任务管理工具、MCP 工具、插件工具。这些来源汇聚后,再经过过滤逻辑:根据当前激活 Skill 的白名单配置、子 Agent 的工具限制、用户的禁用设置,得到最终工具集。
为什么工具集要动态计算?
同一个 Alice 实例,在不同的 Skill 场景下、不同的 Agent 角色下、不同的用户设置下,应该暴露给 LLM 不同的工具集。静态工具列表无法满足这个需求。
Anthropic 的思路: Anthropic 说工具定义和描述应该像整体提示词一样被精心设计。Alice 的工具白名单机制是这一原则的进一步落地:不只写好文档,还要控制工具可见性。
while 循环的内部结构
每一轮迭代包含以下几个环节:
上下文检查与压缩
检查当前消息序列的 token 估算值是否超过阈值,按需触发压缩。压缩是多层递进的:第一层移除冗余内容,如果仍然超标则做轻量压缩(压缩工具结果),继续超标则折叠历史轮次,最后兜底是完整重压缩。详见第五章。
消息整形
在发给 LLM 之前,做两件防御性的事:
-
修复孤立的工具调用:如果上一轮有工具调用没有对应的执行结果(可能由中断产生),注入一条合成的错误结果,防止某些模型返回格式错误
-
注入动态内容:把当前日期、权限拒绝摘要追加到系统消息末尾(不放在开头,保护缓存命中)
调用 LLM 流式
发送消息和工具列表给 LLM API,接收流式响应。响应中的文字片段实时向上游发射事件,工具调用的 JSON 片段则在本地累积,直到一个完整的工具调用描述收齐。
特殊情况:如果收到上下文长度超限错误,尝试做一次紧急深度压缩,然后重试。
写入助手消息
有一个微妙的设计:如果这轮有工具调用,丢弃伴随的文字内容,只保留工具调用。
原因:某些模型在发起工具调用时会附带「我来看一下…」这样的说明文字,但这段文字在后续对话中没有用,还会浪费 token。让模型先行动后解释,比边解释边行动效率更高。
Anthropic 的原则差异: Anthropic 建议明确展示 Agent 的规划步骤,这是透明度优先的思路。Alice 选择丢弃的原因是:规划文字对下一轮的 LLM 推理贡献极小,却显著占用 token。在长任务中,这是一个重要的工程权衡。
无工具调用则退出
如果这一轮 LLM 没有发起任何工具调用,说明它认为任务已完成(或需要等待用户输入),循环结束。
执行工具(有工具时)
按工具声明的并发安全属性分批执行,详见第四章。
执行结果写回上下文,进入下一轮迭代。
循环的终止条件
循环在以下情况终止:
- LLM 没有发起工具调用(正常完成)
- 达到最大迭代次数(防无限循环)
- 接收到用户取消信号
- 发生不可恢复的错误
这四个条件的设计,背后有一个值得深想的张力。
最朴素的方案只有一个终止条件:LLM 不再调用工具。但这在生产环境里是不够的。LLM 可能进入工具调用的死循环,反复调用同一个工具、反复重试某个失败操作,从来不会自然停下来。所以需要迭代次数上限兜底。
但迭代次数上限本身也是一个两难:设太低,复杂任务被强制截断,用户体验差;设太高,失控的循环会烧掉大量 token 才被发现。没有一个对所有任务都对的数字。
更麻烦的是,迭代次数上限解决不了用户中途想停的问题。用户点击取消时,循环可能正卡在一个工具调用里等待结果。取消信号机制的作用是穿透所有等待层,立刻让整个调用链干净退出。
不可恢复错误是第四道防线。某些错误重试没有意义(上下文已损坏、模型返回非法结构、安全扫描失败),应该立即终止,消耗更多轮次只会加大损失。
四个条件加在一起,才能覆盖正常完成、超限终止、主动取消、异常退出四种不同的收尾路径。每一条路径对应不同的善后逻辑,终止条件因此不能简化。
收尾:后台任务
循环结束后,异步触发三个后台任务:
- 记忆提取:从本轮对话中识别值得长期记住的信息,写入项目记忆
- 用户画像更新:更新用户画像的各个维度
- Skill 改进分析:分析本轮使用的 Skill 效果,更新技能文件
这三个任务都是触发后不等待的,不让它们影响用户看到响应的速度。
这也是 Alice 越用越懂你的底层机制。
渠道 Fallback:防止忘记发送
当对话来自外部渠道(如微信、Telegram)时,AI 应该主动调用「发送到渠道」工具把回复发出去。
但如果 AI 生成了文字回复,却忘记调用发送工具怎么办?
一个简单的防呆机制:循环结束后,检查是否调用过发送工具。如果没有,但有文字输出,自动补一次发送。
这类防呆机制在 Agent 系统里非常重要,不能假设模型总是记得做每一件事。
为什么选择事件流作为出口
Agent 循环的出口设计成事件流,背后有一个核心判断:Agent 的执行过程是一个持续产生事件的过程,回调或 Promise 的模型与之不匹配。
用回调实现并不难,但会把控制流和事件处理混在一起,调用方要同时管理启动、取消和事件响应,代码很快变得难以推理。事件发射器解决了事件分发的问题,但引入了另一个问题:发射者不知道消费者处理得多快,快速发射的事件会在内存里积压。
事件流(生成器模式)的好处是:消费者控制节奏,每次显式请求下一个事件才触发生产,背压自然形成。取消也很干净,主动关闭生成器后整条调用链都会收到停止信号。
但这个选择也有代价。生成器在中间层包装时比较繁琐。如果需要把子 Agent 的事件流合并进父 Agent 的流,需要显式处理多流合并逻辑,比广播模式复杂。调试执行顺序在某些场景下也比 Promise 链更难追踪。
选事件流模式,本质上是在调用方控制权和中间层灵活性之间做了一个取舍。对于 Alice 这种 UI 是主要消费者的场景,前者更重要。