这个循环里住着整个系统的灵魂。

设计动机:从最简单的循环开始

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 循环的整体流程如下:

  1. 收到用户消息后,先组装系统提示词
  2. 动态组装当前可用工具集
  3. 进入 while 循环
  4. 每轮迭代中:检查上下文大小按需压缩,做消息整形,流式调用 LLM
  5. 如果 LLM 返回了工具调用,按并发安全性分批执行,结果写回上下文,继续循环
  6. 如果 LLM 只返回文字(没有工具调用),循环结束
  7. 循环结束后,异步触发后台任务:记忆提取、用户画像更新、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 之前,做两件防御性的事:

  1. 修复孤立的工具调用:如果上一轮有工具调用没有对应的执行结果(可能由中断产生),注入一条合成的错误结果,防止某些模型返回格式错误

  2. 注入动态内容:把当前日期、权限拒绝摘要追加到系统消息末尾(不放在开头,保护缓存命中)

调用 LLM 流式

发送消息和工具列表给 LLM API,接收流式响应。响应中的文字片段实时向上游发射事件,工具调用的 JSON 片段则在本地累积,直到一个完整的工具调用描述收齐。

特殊情况:如果收到上下文长度超限错误,尝试做一次紧急深度压缩,然后重试。

写入助手消息

有一个微妙的设计:如果这轮有工具调用,丢弃伴随的文字内容,只保留工具调用。

原因:某些模型在发起工具调用时会附带「我来看一下…」这样的说明文字,但这段文字在后续对话中没有用,还会浪费 token。让模型先行动后解释,比边解释边行动效率更高。

Anthropic 的原则差异: Anthropic 建议明确展示 Agent 的规划步骤,这是透明度优先的思路。Alice 选择丢弃的原因是:规划文字对下一轮的 LLM 推理贡献极小,却显著占用 token。在长任务中,这是一个重要的工程权衡。

无工具调用则退出

如果这一轮 LLM 没有发起任何工具调用,说明它认为任务已完成(或需要等待用户输入),循环结束。

执行工具(有工具时)

按工具声明的并发安全属性分批执行,详见第四章。

执行结果写回上下文,进入下一轮迭代。

循环的终止条件

循环在以下情况终止:

  1. LLM 没有发起工具调用(正常完成)
  2. 达到最大迭代次数(防无限循环)
  3. 接收到用户取消信号
  4. 发生不可恢复的错误

这四个条件的设计,背后有一个值得深想的张力。

最朴素的方案只有一个终止条件:LLM 不再调用工具。但这在生产环境里是不够的。LLM 可能进入工具调用的死循环,反复调用同一个工具、反复重试某个失败操作,从来不会自然停下来。所以需要迭代次数上限兜底。

但迭代次数上限本身也是一个两难:设太低,复杂任务被强制截断,用户体验差;设太高,失控的循环会烧掉大量 token 才被发现。没有一个对所有任务都对的数字。

更麻烦的是,迭代次数上限解决不了用户中途想停的问题。用户点击取消时,循环可能正卡在一个工具调用里等待结果。取消信号机制的作用是穿透所有等待层,立刻让整个调用链干净退出。

不可恢复错误是第四道防线。某些错误重试没有意义(上下文已损坏、模型返回非法结构、安全扫描失败),应该立即终止,消耗更多轮次只会加大损失。

四个条件加在一起,才能覆盖正常完成、超限终止、主动取消、异常退出四种不同的收尾路径。每一条路径对应不同的善后逻辑,终止条件因此不能简化。

收尾:后台任务

循环结束后,异步触发三个后台任务:

  • 记忆提取:从本轮对话中识别值得长期记住的信息,写入项目记忆
  • 用户画像更新:更新用户画像的各个维度
  • Skill 改进分析:分析本轮使用的 Skill 效果,更新技能文件

这三个任务都是触发后不等待的,不让它们影响用户看到响应的速度。

这也是 Alice 越用越懂你的底层机制。

渠道 Fallback:防止忘记发送

当对话来自外部渠道(如微信、Telegram)时,AI 应该主动调用「发送到渠道」工具把回复发出去。

但如果 AI 生成了文字回复,却忘记调用发送工具怎么办?

一个简单的防呆机制:循环结束后,检查是否调用过发送工具。如果没有,但有文字输出,自动补一次发送。

这类防呆机制在 Agent 系统里非常重要,不能假设模型总是记得做每一件事。

为什么选择事件流作为出口

Agent 循环的出口设计成事件流,背后有一个核心判断:Agent 的执行过程是一个持续产生事件的过程,回调或 Promise 的模型与之不匹配。

用回调实现并不难,但会把控制流和事件处理混在一起,调用方要同时管理启动、取消和事件响应,代码很快变得难以推理。事件发射器解决了事件分发的问题,但引入了另一个问题:发射者不知道消费者处理得多快,快速发射的事件会在内存里积压。

事件流(生成器模式)的好处是:消费者控制节奏,每次显式请求下一个事件才触发生产,背压自然形成。取消也很干净,主动关闭生成器后整条调用链都会收到停止信号。

但这个选择也有代价。生成器在中间层包装时比较繁琐。如果需要把子 Agent 的事件流合并进父 Agent 的流,需要显式处理多流合并逻辑,比广播模式复杂。调试执行顺序在某些场景下也比 Promise 链更难追踪。

选事件流模式,本质上是在调用方控制权和中间层灵活性之间做了一个取舍。对于 Alice 这种 UI 是主要消费者的场景,前者更重要。

上一章:整体架构 · 下一章:工具系统

本文由 洛小山 发布。引用或转载时,请保留原文链接。