智能体框架的苦涩教训
一个agent其实就是一个消息的for循环。agent唯一应该拥有的状态就是一直运行直到模型停止调用工具。你不需要agent框架。你不需要任何其他东西。它只是一个工具调用的for循环。我们最早的Browser Use agents有几千行的抽象代码。它们能工作——直到我们尝试修改任何东西。每次实验都在和框架对抗。agents失败不是因为模型笨而是因为我们自己笨。坚持看到最后我会给你展示构建Claude Code有多容易。1、为什么抽象会破坏学习抽象的问题在于它们冻结了对智能应该如何工作的假设。而RL会打破这些假设。每次你在模型行为周围添加一个“聪明”的包装器——规划模块、验证层、输出解析器——你就是在编码你认为模型应该做什么。但模型是在数百万个例子上训练的。它见过比你能预见到的多得多的模式。你的抽象变成了约束阻止模型使用它所学到的东西。机器学习研究中的Bitter Lesson苦涩教训很清楚利用计算的通用方法每次都能击败手工打造的人类知识。agent框架只是这个错误最新的一个例子。2、99%的工作在模型内部事情是这样的99%的工作是在模型自身内部完成的。我们不需要在它周围套一个高度抽象的框架。现在的Claude Code可以直接编写AppleScript。它需要某个冷门Spotify播放器的信息它不需要一个Spotify的computer-use工具。它只是在macOS上写AppleScript。它有完美的上下文。它在这方面训练得很好。你不需要提前预见每一个用例。模型已经知道了。3、关键洞见这引出了一个重要结论agent框架失败不是因为模型弱而是因为它们的行为空间不完整。与其提前定义每一个可能的动作不如从相反的假设开始模型几乎什么都能做。然后再加以限制。给LLM尽可能多的自由然后基于评估进行“vibe-restrict”氛围限制。4、为什么我们把一切都扔掉了Browser Use的第一个版本是一个经典的agent框架一个被复杂消息管理器包裹的模型里面有很多旨在控制行为的抽象。它能工作但扩展起来很痛苦。每一次实验都在和框架对抗。添加新能力感觉就像在违背Bitter Lesson。公平地说模型从去年以来已经变得好多了所以我们后退一步问了一个更根本的问题LLM实际上被训练得极其擅长什么——以及随着模型变得更好什么会保持不变我们扔掉了整个agent从零开始。为了理解“最小化”真正意味着什么我们逆向工程了Claude Code和Gemini CLI。向他们致敬他们创造了真正优秀且大多简洁的原语。虽然它们内部很复杂但底层理念很简单不要过度指定智能——让模型去推理。5、BU Agent实际应用我们把这个理念构建进了BU Agent——一个为Browser Use提供动力的极简agent框架。我们没有暴露一小套脆弱的“click / type / scroll”原语而是让BU Agent给模型提供对浏览器原始控制面的访问。核心是能够发出纯Chrome DevTools Protocol (CDP)指令的能力。实际上模型几乎可以在浏览器中做任何事情。在此之上浏览器扩展API。它们让某些用CDP单独做起来尴尬或不可能的任务变得 trivial——比如访问活动窗口或处理带权限的浏览器状态。CDP和扩展API各自都有盲点。但结合在一起它们形成了一个几乎完整的行为空间。当模型拥有这种自由时重要的事情发生了。如果一种方法失败它会绕过去。如果一个工具坏了它会找到另一条路径。只要原则上一切皆有可能LLM就极其擅长在飞行中自我修复。6、反转所以BU Agent是基于一个简单的反转构建的从最大能力开始然后再限制。给模型一个人类在浏览器中能做的任何事情的自由。只有在这之后才叠加安全、结构和约束。这正是让系统能随着更好模型一起扩展而不是对抗它们的原因。7、我讨厌其他所有LLM框架真的。它们实现LLM对象的方式让人痛苦。所以我写了自己的。超级简单的方式来调用。就这么简单——支持Anthropic、OpenAI和Google。根据我们的遥测数据这些占了95%的用例。pythonclass 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: ...同样的接口。对缓存、序列化、提供商 quirks 有完全控制。没有魔法。没有意外。自己做缓存和实现消息要容易得多。完全模型无关。你不会被锁定在一个提供商。你自己决定。8、临时消息Ephemeral messages我们为浏览器agents需要的一个有趣东西如果你请求浏览器状态它非常庞大。DOM快照、截图、元素索引——每次请求很容易50KB。如果没有临时消息会发生什么经过10次浏览器交互后你的上下文里就有500KB的状态。20次后就到了1MB。模型开始失去连贯性。它忘记了原始任务。它会幻觉已经不存在的元素。最终你达到上下文限制整个东西崩溃。所以我引入了临时消息。tool(Get browser state, ephemeral3) # Keep last 3 only async def get_state() - str: return massive_dom_and_screenshot如果你定义某个工具调用了X次它就会移除所有之前的输出。会稍微破坏一点缓存。但这是一个非常好的权衡——LLM反正无法真正处理海量上下文。模型只需要最近的状态旧的浏览器快照只是噪声。9、for-loop行不通直到你修复它天真的做法——当模型返回没有工具调用时就停止——效果不好。agents会过早结束。尤其是当它们缺少某些上下文时。你想要一个跟进但如果你用这种API就不可能。最好的修复方式是done tool。tool(Signal that the current task is complete.) async def done(message: str) - str: raise TaskComplete(message)当模型输出一个done工具调用时agent就终止。这迫使它显式完成而不是隐式的“我猜我们做完了”我们有两种模式CLI模式当LLM返回没有工具调用时停止快速交互自主模式仅在显式done()调用时停止Claude Code就是这么做的。Gemini CLI也是这么做的。现在你知道它为什么存在了。10、但你需要可靠的基础设施是的。for-loop很简单。让它可靠却不容易指数退避的重试速率限制处理连接恢复上下文压缩Token跟踪这是运维。已经解决的问题。必要——但不要把它和agent本身混淆。11、苦涩的真相每一个抽象都是负债。每 一个“helper”都是故障点。模型已经变好了。真的很好。它们在computer use、coding、browsing上经过了RL训练。它们不需要你的护栏。它们需要的是苦涩的教训你构建得越少它工作得越好。我们正在把这个开源为agent-sdk。如果你想你可以在生产环境中使用它但最好只是把上下文粘贴到Claude Code里用你正在编码的任何语言自己做一个。仓库里也包含了一个Claude Code重新实现的例子。总之这就是我们在构建bu.app时学到的东西。去试试吧。它很棒原文链接智能体框架的苦涩教训 - 汇智网