跳到正文

智能体框架的惨痛教训

所有价值都在经过强化学习训练的模型中,而不在你那一万行抽象代码里。

本文目录文章开头
部分图表可能显示不完整

The Agent Loop

智能体只是消息的 for 循环。智能体唯一应该拥有的状态是:持续运行,直到模型停止调用工具。你不需要智能体框架。你不需要其他任何东西。它只是一个工具调用的 for 循环。

我们最初的 Browser Use 智能体有数千行抽象代码。它们能用——直到我们试图改动任何东西。每次实验都在与框架对抗。智能体失败不是因为模型笨,而是因为我们笨。

请看到最后,我会向你展示构建 Claude Code 有多容易。

为什么抽象会阻碍学习文章图表

关于抽象的问题在于:它们固化了关于智能应如何运作的假设。RL 打破了这些假设。

每次你为模型行为添加一个"智能"包装器——规划模块、验证层、输出解析器——你都在编码你认为模型应该做什么。但模型是在数百万个样本上训练的。它见过的模式比你能预料的更多。你的抽象变成了约束,阻止模型使用它所学到的东西。

ML 研究中的苦涩教训很明确:利用计算的通用方法每次都能击败手工打造的人类知识。智能体框架只是这个错误的最新实例。

99% 的工作在模型内部文章图表

99% model, 1% your code

事实是:99% 的工作是在模型内部完成的。我们不需要在它周围套一个高度抽象的框架。

Claude Code 现在可以直接编写 AppleScript。它需要从某个冷门的 Spotify 播放器获取信息?它不需要 Spotify 的 computer-use 工具。它直接在 macOS 上编写 AppleScript。它拥有完美的上下文。它在这方面训练得很好。

你无法预料每一个用例。模型已经知道了。

关键洞察文章图表

这引出了一个重要的结论:

智能体框架失败不是因为模型能力弱,而是因为它们的动作空间不完整。

不要预先定义每一个可能的动作,而是从相反的假设出发:模型几乎可以做任何事。然后再加以限制。

尽可能给 LLM 自由,然后基于评估进行 vibe 限制。

为什么我们把一切都扔掉了文章图表

Browser Use 的第一个版本是一个经典的智能体框架:一个模型被包裹在一个复杂的消息管理器中,带有大量旨在控制行为的抽象。它能用,但扩展起来很痛苦。每次实验都在与框架对抗。添加新能力感觉像是在违背苦涩教训。(公平地说,模型从去年以来进步了很多)

所以我们退后一步,问了一个更根本的问题:

LLM 实际上被训练得极其擅长什么——并且随着模型变好,什么会保持不变?

我们把整个智能体扔掉,从头开始。为了理解"最小化"真正意味着什么,我们逆向工程了 Claude Code 和 Gemini CLI。向他们致敬,创造了真正优秀且基本简洁的原语。尽管它们内部很复杂,但底层思想很简单:

不要过度指定智能——让模型去推理。

BU Agent:应用文章图表

我们将这一理念融入了 BU Agent——一个驱动 Browser Use 的极简智能体框架。

BU Agent 没有暴露一小组脆弱的"点击/输入/滚动"原语,而是让模型访问原始的浏览器控制界面。

核心在于:能够发出纯 Chrome DevTools Protocol(CDP)指令。实际上,模型几乎可以在浏览器中做任何事情。

在此之上:浏览器扩展 API。它们让某些仅靠 CDP 会很别扭或根本不可能的任务变得轻而易举——比如访问活动窗口或操作带权限的浏览器状态。

CDP 和扩展 API 各有盲区,但结合起来就构成了一个近乎完整的动作空间。

当模型拥有这种自由时,一件重要的事情就会发生。如果一种方法失败,它会绕过去。如果一个工具坏了,它会找到另一条路。

只要原则上一切皆有可能,LLM 就极其擅长在运行中自我修复。

反转文章图表

Traditional vs The Inversion approach

所以 BU Agent 是基于一个简单的反转构建的:

先赋予最大能力,然后再加以限制。

赋予模型做人类在浏览器中能做的任何事情的自由。只有在此之后,才叠加安全性、结构和约束。

正是这一点让系统能够随着更好的模型而扩展,而不是与它们对抗。

我讨厌其他所有 LLM 框架文章图表

说真的。它们实现 LLM 对象的方式令人痛苦。

所以我写了我自己的。超级简单的调用方式。它们就只做这些——针对 Anthropic、OpenAI 和 Google。根据我们的遥测数据,这些占了 95% 的用例。

BaseChatModel Protocol with Anthropic, OpenAI, and Google

class ChatAnthropic:
    async def ainvoke(self, messages, tools) -> ChatCompletion: ...
 
class ChatOpenAI:
    async def ainvoke(self, messages, tools) -> ChatCompletion: ...
 
class ChatGoogle:
    async def ainvoke(self, messages, tools) -> ChatCompletion: ...

相同的接口。完全掌控缓存、序列化、提供商怪癖。没有魔法。没有意外。

自己做缓存和实现消息要容易得多。完全与模型无关。你不会被锁定在某一个提供商上。你说了算。

临时消息文章图表

浏览器代理需要的一件有趣的事情:如果你请求浏览器状态,它非常庞大。DOM 快照、截图、元素索引——每次请求轻松超过 50KB。

没有临时消息,情况是这样的:10 次浏览器交互后,上下文里就有 500KB 的状态。20 次后,达到 1MB。模型开始失去连贯性。它忘记了原始任务。它幻觉出已经不存在的元素。最终你会撞上上下文限制,整个系统崩溃。

所以我引入了临时消息。

@tool("Get browser state", ephemeral=3)  # Keep last 3 only
async def get_state() -> str:
    return massive_dom_and_screenshot

如果你调用工具 X 次(由你定义),它会移除所有之前的输出。会稍微破坏缓存。但这是一个非常好的权衡——反正 LLM 也处理不了巨大的上下文。模型只需要最近的状态;旧的浏览器快照就是噪音。

for 循环不起作用(直到你修复它)文章图表

Naive approach vs Done tool comparison

朴素的方法——当模型返回没有工具调用时停止——效果不好。代理会过早结束。尤其是当它们缺少某些上下文时。你想要一个后续交互,但如果你使用的是这类 API,这是不可能的。

最好的修复方法是 done 工具。

@tool('Signal that the current task is complete.')
async def done(message: str) -> str:
    raise TaskComplete(message)

当模型输出 done 工具调用时,它会终止代理。这强制要求显式完成,而不是隐式的"我猜我们完成了?"

我们有两种模式:

  • CLI 模式:当 LLM 返回没有工具调用时停止(快速交互)
  • 自主模式:仅在显式调用 done() 时停止

Claude Code 就是这样做的。Gemini CLI 也是这样做的。现在你知道它为什么存在了。

但你需要可靠的基础设施文章图表

是的。for 循环很简单。让它健壮就不简单了:

  • 带指数退避的重试
  • 速率限制处理
  • 连接恢复
  • 上下文压缩
  • Token 追踪

那是运维。已解决的问题。必要——但不要把它和代理本身混为一谈。

残酷的真相文章图表

每一个抽象都是一种负担。每一个"辅助工具"都是一个故障点。

这些模型变得很强了。真的很强。它们在计算机使用、编码、浏览方面经过了强化学习训练。它们不需要你的防护栏。它们需要:

The Entire SDK: A complete action space, A for-loop, An explicit exit (done tool), Context management

苦涩的教训:你构建得越少,它就运作得越好。

我们将其开源为 agent-sdk。

如果你想,可以在生产环境中使用它,但最好只是把上下文粘贴到 Claude Code 中,自己做一个。用你正在使用的任何编程语言。仓库中还包含一个 Claude Code 重新实现的示例。

总之,这就是我们在构建 bu.app 时学到的东西。试试看吧。太棒了!!

阅读显示设置

调整正文排版

当前浏览器记住选择,应用于文章正文。手机上正文宽度以屏幕可用空间为限。

图表完整性提示

普通文章遇到图表保存问题时显示提示。