重新定义 Agent:核心是日志而非模型
挑战常见认知,认为日志/上下文而非单纯模型权重才是 AI agent 的本质,提供新的架构思考视角。
挑战常见认知,认为日志/上下文而非单纯模型权重才是 AI agent 的本质,提供新的架构思考视角。
AI agents 正在达到同一个拐点。
大多数人认为 agent 就是模型、运行时或者当前执行任务的循环。这些东西确实很重要。但它们并不是 agent。
在这篇文章中,我会主张一个 agent 就是它的数据:具体来说是它的事件历史,我们称之为日志(log)。当日志构造正确时,仅从日志本身就可以恢复一个 agent。而这个单一的特性解锁了一整套能力,使得即使是高级 agent 用例也更容易推理和融入到日常应用中。
那么日志到底是什么?定义 agent 就是定义日志。在其核心,日志就是事件历史:每个用户输入、模型输出、工具调用和工具结果,这些在 agent 工作时不断累积——一份仅追加的完整记录。
日志还携带一个对会话定义的引用:系统提示词、工具描述和 agent 运行的技能。这些从轮次到轮次不会改变,所以它更像是一个版本化的常量而不是流的活跃部分。
它们共同形成了 agent 的完整状态。Agent 存在的任何东西都不在运行时、模型或工具里;它们只是解释器和追加器。它们读取可持久化的状态,作用于它,然后把下一个事件写回来。所以你可以把相同的日志交给一个全新的执行器,它会重新构造 agent 所在的确切位置并从那里继续。日志本身就足以恢复这个 agent。
一旦你把 agent 定义为日志,系统的其余部分就变得更容易理解了。对 agent 的每一个操作要么读取日志,要么向其追加内容,要么渲染日志的一个视图。模型读取一个视图并产生下一个操作。工具运行器执行一个工具调用并追加结果。UI 读取日志并渲染一个时间线;追踪系统读取它并渲染追踪;审计员读取它来重建发生了什么。这是数据库多年前采用的同一模式:表、索引和缓存都是对底层变更日志的投影。参考本文在线版本中改编自 @dexhorthy 关于 12-factor agents 的优秀探讨的图表——值得一读。
实际上,一个简化的循环看起来像这样:
看起来很简单,但关键洞察是每一次遍历声称一个临时租约,读取日志,推进一步,然后把结果写回去。这个循环是幂等的且容错的:只要每个有意义的状态转移都被持久化写入,任何执行器都可以接手这个会话并继续。
虽然日志可以无限增长,但模型对它的视图不能:上下文窗口是有限的。你不能在每次调用时把整个原始历史交给模型。这就是压缩的作用。而由于压缩用一个摘要替换它之前的所有东西,这不会破坏"日志是 agent"的声明吗?
不会。记住,压缩是有损的。一个压缩的摘要不能以更小的形式再现 agent 的状态;它丢弃了信息。
这实际上加强了这个声明。完整的日志是记录;压缩只是它的一个投影,就像物化视图不是数据库、摘要不是对话一样。保留原始日志,你总是可以从它生成新的投影。丢弃原始日志只保留压缩,你就丢失了 agent 的一部分。所以最干净的方法是把压缩看作一个尽力而为的、有损的分支,一个你作为新日志恢复的分支。
完整的会话日志流入一个更小的压缩立方体,沿途脱落细节。
有一个明显的反驳,值得认真对待:那些在日志外改变状态的工具呢?一个 agent 编辑一个文件、打开一个 GitHub issue、发送一封邮件。现在有状态存在于日志之外的某个地方。这不会破坏这个声明吗?
不会。你从日志重新推导 agent 的状态,而不是从世界:如果它已经发送了邮件,分叉回去不会取消发送,它编辑的文件可能已经在它下面改变了。日志不会让世界变得确定性或可逆。它保留了一份忠实的、可恢复的 agent 所做和所见的记录,这正是你承不起失去的东西。保存文件做了同样的事:它不包含 Skyrim 引擎或地图,只是把你放回去需要的玩家特定的状态。而如果自 agent 上次运行以来世界已经改变,agent 就像这个角色一样,当重新接触周围的东西时更新它的世界观。
用这种方式思考 agent 会给你一套不错的系统特性。它们不是独立的;它们都源自同一个假设——日志就是 agent。
可靠性。 考虑一下今天 Claude Code 会发生什么:如果 agent 到达权限提示并且进程死亡,你恢复它,权限提示就消失了,agent 已暂停。这在生产环境中是不可接受的。当日志是 agent 时,执行器被允许是有缺陷的:一个新的工作者接手会话,重新构造状态,权限提示就在它原来的地方。进程死了;agent 没有。
可靠性。 考虑一下今天 Claude Code 会发生什么:如果 agent 到达权限提示并且进程死亡,你恢复它,权限提示就消失了,agent 已暂停。这在生产环境中是不可接受的。当日志是 agent 时,执行器被允许是有缺陷的:一个新的工作者接手会话,重新构造状态,权限提示就在它原来的地方。进程死了;agent 没有。
可扩展性。 大多数框架每个 agent 运行一个进程,这意味着 agent 被绑定到运行它的机器。当日志是状态时,你翻转这个模型:一个进程可以推进数千个 agent,每一个都从日志在每个轮次重新构造它的状态。agent 不被绑定到任何单一的机器或工作者,使故障转移变得微不足道。而扩展出去就只是增加更多工作者:没有粘性会话,没有状态迁移,没有协调开销。
可扩展性。 大多数框架每个 agent 运行一个进程,这意味着 agent 被绑定到运行它的机器。当日志是状态时,你翻转这个模型:一个进程可以推进数千个 agent,每一个都从日志在每个轮次重新构造它的状态。agent 不被绑定到任何单一的机器或工作者,使故障转移变得微不足道。而扩展出去就只是增加更多工作者:没有粘性会话,没有状态迁移,没有协调开销。
分叉。 与其一条线性路径,你可以对日志进行分支。一个分支在 Claude 上运行,另一个在 GPT 上,再另一个在本地 Qwen 上,每一个都探索一种不同的策略,在不同的沙箱中,使用不同的工具。探索不同的方法在架构上变得简单得多。
分叉。 与其一条线性路径,你可以对日志进行分支。一个分支在 Claude 上运行,另一个在 GPT 上,再另一个在本地 Qwen 上,每一个都探索一种不同的策略,在不同的沙箱中,使用不同的工具。探索不同的方法在架构上变得简单得多。
多人协作。 共享一个 agent 不应该意味着把一份记录复制到 Slack。那就像共享一个数据库的截图并称之为复制。如果日志是 agent,共享意味着授予对他人可以检查、恢复或扩展的可持久化历史的访问权。
多人协作。 共享一个 agent 不应该意味着把一份记录复制到 Slack。那就像共享一个数据库的截图并称之为复制。如果日志是 agent,共享意味着授予对他人可以检查、恢复或扩展的可持久化历史的访问权。
迁移。 如果 agent 的身份被困在提供商特定的假设内,切换提供商就很痛苦。如果日志是 agent,迁移就变成一个适配器问题。不同的模型可能需要不同的投影,但那些是工程问题,不是身份问题。日志是连续性,agent 应该能够与任何模型提供商互换地接手和恢复。
迁移。 如果 agent 的身份被困在提供商特定的假设内,切换提供商就很痛苦。如果日志是 agent,迁移就变成一个适配器问题。不同的模型可能需要不同的投影,但那些是工程问题,不是身份问题。日志是连续性,agent 应该能够与任何模型提供商互换地接手和恢复。
这些只是几个例子。一旦你开始这样思考 agent,它们就变成了数据,许多你对数据能做的事情现在对 agent 也变成可能了。请在 omnara.com/blog/the-log-is-the-agent 阅读本文的其余部分。