常驻式Agent并非真在线:五个原语让它感觉始终在线
过去几个月,我们看到常驻式 Agent 重新兴起。常驻式 Agent 是有状态(stateful)且主动的(proactive),也就是说它们能跨会话管理上下文,并且在无需直接监督的情况下采取行动。底层上,它们与其他 Agent 运行在相同的核心循环上。区别在于外部架构,这套架构由若干原语构成,控制着 Agent 何时被唤醒、每次运行之间保留什么、以及它的工作成果如何触达用户。
这次复兴主要得益于近期模型能力的进步,这让常驻式 Agent 更容易落地。首先,最新模型拥有更好的判断力:它们能更可靠地理解用户意图,也知道何时该介入确认。其次,智能的成本在下降,长周期任务获益最大。例如,在某项长周期编码基准上,Opus 5.5 每个已解决任务的成本比 Opus 5 低 78%,而在典型工作负载上则低 40%。
客户已经开始询问我们如何将这类体验集成到自己的产品中。有些公司想让现有 Agent 变得更主动,另一些则希望从零开始构建。
本文将拆解常驻式 Agent 背后的原语,并讲解它们如何组合在一起。
常驻式 Agent 背后的循环
常驻式 Agent 实际上并不是“始终在线”。这只是对用户体验的描述。
实际上,Agent 大部分时间处于休眠状态,周期性地醒来做一些工作。每一段工作被称为一次运行(run):Agent 收到一个事件,完成任务,然后保存发生过的信息。一次运行在会话(session)内进行,会话是消息和工具调用的记录。运行结束后,Agent 重新休眠,然后这个循环无限重复。
核心原语
常驻式 Agent 的外部架构包含五个核心原语:事件收件箱、定时器、记忆、权限和发件箱。我们逐一来看。
事件收件箱
大多数 AI Agent 只在用户发送消息时运行。相比之下,常驻式 Agent 可以被多种类型的事件触发,包括 webhook、定时任务和后台任务。
一种诱人的做法是让每个事件源直接调用 Agent,但这会带来几种失败模式。例如,两个事件同时到达可能启动相互竞争的运行,而一次崩溃可能完全丢失一个事件。
这时就需要事件收件箱。每个来源不是直接调用 Agent,而是将事件发送到收件箱,由 worker 取走处理。
最简单的版本包含三个组件:
- 持久化的事件列表,通常是数据库表;
- 处理器,通常是 HTTP 端点,负责将事件写入列表;
- worker,通常是后台服务,负责从列表读取事件。
收件箱应跟踪每个事件的状态,也可以根据来源的唯一标识符对事件去重。关系型数据库很适合作为收件箱,每个事件作为新行追加。我们建议每个用户一个收件箱起步,但如果需要更高的并行度,可以进一步拆分。
当新事件到达时,处理器将事件写入收件箱并唤醒 worker。worker 读取所有待处理事件并开始一次运行。延续先前工作的事件(例如用户的回复)会恢复现有会话,而其他事件则开启新会话。
接着,worker 将事件传递给 Agent 循环,等待运行完成,并将对应事件标记为完成。实现细节可能会比较棘手,所以这里提供几个最佳实践:
- 处理器应快速确认事件。某些来源(如 webhook 提供商)会把响应慢视为故障。处理器仍可运行轻量检查,但任何更深层的验证(例如检查提示词注入)应在运行期间进行。
- 收件箱应当是事实来源。当 worker 被唤醒时,它应首先从收件箱拉取最新事件。这样,如果 worker 在运行中崩溃,新的 worker 可以拾取同一事件并重试。
- 一个收件箱一次只能有一个活跃 worker。这可以防止两个运行重复工作或相互覆盖。当运行中途出现新事件时,你可以将其推入下一次运行、合并进当前运行,或中断并重启。
- worker 应将待处理事件批量合并。例如,如果用户连续发送多条消息,worker 可以将它们合并成一次运行,这样 Agent 只需回复一次。
定时器
定时器与事件收件箱一起,让常驻式 Agent 能够主动发起行动。定时器通常分为两类:跟进(follow-up)或排程(schedule)。跟进是 Agent 为自己设定的截止时间,例如检查是否收到回复。排程则是按已知节奏发生的重复任务,例如每日简报。
定时器存放在自己的持久化存储中,通常是另一个数据库表,与收件箱分开。收件箱只应保存可以立即处理的事件。跟进和排程可以共享相似的表结构,但排程还需要记录自身的重复规则。
定时器不会自行启动运行。相反,调度器(通常是后台服务)以固定间隔轮询存储,触发任何到期项。每一轮调度器都会:
- 扫描存储并找出到期的定时器。
- 根据类别更新每个定时器。
- 跟进应标记为完成。
- 排程的到期时间应更新为下一次发生时间。
- 向收件箱添加事件并唤醒 worker。
对于调度器,你可以自己构建服务,也可以使用持久化执行引擎。
记忆
常驻式 Agent 需要跨多个不连续运行工作,因此依赖记忆来保持连续性。
与事件收件箱和定时器类似,记忆应有自己的存储。在我们的产品中(如 Claude Code 的自动记忆和 Messages API 的记忆工具),这是以文件目录形式实现的。对于常驻式 Agent,目录应映射到持久化存储,通常是数据库或对象存储。这样记忆才能比 worker 更长寿。
一个经验法则是:记忆只保存 Agent 无法再次查找到的内容。如果某些信息已经存在于其他系统中(如问题跟踪器),你应该保存一个指针而不是复制内容。以下是常驻式 Agent 应存储的记忆类型分类:
- 用户指令:任何关于 Agent 行为的用户指令都应保存在自己的文件中,并在每次运行开始时加载。用户不会总在身边指导 Agent,所以指令必须跨运行延续。
- 用户笔记:Agent 应保存它了解到的用户信息,例如用户的角色、偏好,或用户给出的纠正。你可以从一个文件起步,随着笔记增长再拆分为几个小文件。在后一种情况下,每次运行开始时只加载索引,但 Agent 可以在需要更多细节时将单个文件拉入上下文。
- 过往消息:Agent 应记录已经告诉过用户的内容,避免重复。它可以存储在单独的文件中,在每次运行开始时加载,也可以让 worker 加载 Agent 与用户的最近几条消息。
- 任务进度:对于每个进行中的任务,Agent 应保留一个进度文件,记录已完成和剩余的工作。任何新的运行都可以读取该文件并从上次中断处继续。与用户笔记类似,每次运行开始时只加载索引,但 Agent 可在需要更多细节时将单个文件拉入上下文。
下图展示了一种示例配置。
以上内容都是在运行期间保存的记忆。你还可以加入“做梦”(dreaming)机制,即回顾过去的会话以整理记忆。做梦可以替换过时条目,并发现只有跨多次运行才能显现的模式。如果你想了解更多,我们关于 Agent 记忆的文章有更详细的介绍。
上下文窗口内部
在执行任何工作之前,Agent 会加载系统提示词、工具定义和记忆文件。这些初始上下文“开销”包含在每次运行中,因此我们建议保持精简,并使用提示词缓存来降低成本。
新会话在启动时只包含这些上下文开销和触发运行的事件。而现有会话则会带回之前运行的消息和工具调用。
如果会话变得过长,你可以压缩旧消息,或开启新会话。两种做法都会丢失细节,因此任何需要持久保留的内容都应沿途保存到记忆中。
权限
权限是常驻式 Agent 风险最高的部分。一次运行持有用户的访问权限,并且按设计无人值守。对于每个工具调用,Agent 需要决定是自行继续、阻止该调用,还是等待批准。
权限使用的组件与其他 Agent 相同:
- 凭据存储:常驻式 Agent 通常需要敏感的用户凭据(如密码或访问令牌)才能发挥作用。推荐模式是在 Agent 环境之外设置一个代理,为出站请求添加凭据。Agent 发送不带凭据的请求,代理在转发前注入真实凭据。
- 自动模式:常驻式 Agent 应能在没有用户持续输入的情况下工作。我们已经在 Claude Code 的自动模式中解决过类似问题。自动模式使用分类器审查高风险操作,并阻止被识别为危险的操作,以便 Claude 找到更安全的办法。不过,对于最重要的决策,用户审查仍然是必要的。
- 延迟工具调用:在 Claude Code 中,hook 可以延迟工具调用并等待用户的额外输入。同样的模式适用于常驻式 Agent,它们在执行某些操作前可能需要澄清或批准。当调用被延迟时,运行结束,会话在调用中途保存。请求会展示给用户(例如通过使用 MCP Apps 构建的交互式卡片),用户的回复会作为事件进入收件箱。该事件启动新的运行,新的运行使用用户的输入恢复已保存的会话。在延迟调用等待响应期间,worker 可以处理其他事件,因此 Agent 不会被阻塞。
发件箱
大多数 AI Agent 在生成回复时将其流式传输给用户,因此用户可以跟随 Claude 的推理过程、工具调用和答案撰写。
而常驻式 Agent 大多在后台完成工作。用户得到结果和少量细节,而不是完整的逐步过程。
一次运行可能以三种方式结束:
- Agent 向发件箱发送消息,
- Agent 决定无需发送任何内容,或
- Agent 使用延迟工具调用来请求权限或更多输入。
大多数常驻式 Agent 可以用一个直接将消息发送给用户的“发送”工具。只有当你想控制 Agent 的消息节奏,或允许 Agent 从错误中恢复时,才需要正式的发件箱。
发件箱是待发送消息的单独表,类似于事件收件箱。另外还有一个调度器(通常是后台服务),类似于 worker。调度器读取待处理消息,决定投递什么,并发送给用户。延迟工具调用的请求既可以通过发件箱发送,也可以直接在界面中展示。
除了投递,调整 Agent 发送消息的数量也很重要。发送太多的 Agent 会变得吵闹,而发送太少的则会漏掉用户需要知道的信息。我们建议构建评估来找到恰当的平衡。在实践中,Agent 最好先保持安静,然后逐步赢得用户更多关注。
所有部分如何组合在一起
以下是五个原语如何组合在一起:
每次运行都从收件箱中的事件开始,无论是用户消息、webhook 还是定时器。当 worker 拾取事件时,它启动一次运行,并在新会话或现有会话中唤醒 Agent。Agent 读取记忆以获得基线,在权限范围内采取行动,然后用学到的新内容更新记忆。运行结束后,Agent 向发件箱推送一行记录,调度器将消息投递给用户。
合在一起,这些就形成一个循环。一次运行可以设置一个定时器,稍后再次唤醒 Agent;当用户回复消息时,回复会作为事件回到收件箱。每次运行都很短暂,但这个循环无限持续,这正是让 Agent 感觉“始终在线”的原因。