에이전트는 결국 메시지와 도구 호출을 반복하는 단순한 루프라는 관점에서, 과도한 추상화가 어떻게 학습과 확장을 방해하는지 설명합니다.

에이전트는 그저 메시지의 for-loop일 뿐입니다. 에이전트가 가져야 할 상태는 오직 이것뿐이어야 합니다. 모델이 도구 호출을 멈출 때까지 계속 가는 것. 에이전트 프레임워크는 필요 없습니다. 다른 어떤 것도 필요 없습니다. 그저 도구 호출의 for-loop일 뿐입니다.
우리의 첫 Browser Use 에이전트들은 수천 줄의 추상화로 이루어져 있었습니다. 그것들은 작동했습니다. 무엇이든 바꾸려 하기 전까지는요. 모든 실험이 프레임워크와 싸워야 했습니다. 에이전트가 실패한 이유는 모델이 멍청해서가 아니었습니다. 우리가 그랬기 때문입니다.
끝까지 읽어보세요. Claude Code를 얼마나 쉽게 만들 수 있는지 보여드리겠습니다.
추상화의 핵심은 이것입니다. 추상화는 지능이 어떻게 작동해야 하는지에 대한 가정을 얼려버립니다. RL은 그 가정을 깨뜨립니다.
모델 동작 위에 "똑똑한" 래퍼를 하나 추가할 때마다 — 계획 모듈, 검증 레이어, 출력 파서 — 여러분은 모델이 무엇을 해야 한다고 생각하는지를 코드에 박아 넣고 있는 것입니다. 하지만 모델은 수백만 개의 예제로 학습되었습니다. 여러분이 예상할 수 있는 것보다 훨씬 더 많은 패턴을 이미 보았습니다. 여러분의 추상화는 모델이 학습한 것을 활용하지 못하게 막는 제약이 됩니다.
ML 연구의 Bitter Lesson은 분명합니다. 계산을 활용하는 일반적인 방법은 매번 사람이 손수 만든 지식을 이깁니다. 에이전트 프레임워크는 그 실수를 보여주는 최신 사례일 뿐입니다.

핵심은 이것입니다. 작업의 99%는 모델 자체 내부에서 처리됩니다. 그 주변에 고도로 추상화된 프레임워크는 필요하지 않습니다.
요즘 Claude Code는 AppleScript를 직접 작성할 수 있습니다. 어떤 생소한 Spotify 플레이어에서 정보가 필요하다고 해봅시다. Spotify computer-use 도구가 따로 필요한 것이 아닙니다. 그냥 macOS에서 AppleScript를 작성하면 됩니다. 완벽한 맥락을 갖고 있고, 이런 작업에 잘 학습되어 있습니다.
여러분이 모든 사용 사례를 미리 예상할 필요는 없습니다. 모델은 이미 알고 있습니다.
이로부터 중요한 결론에 도달했습니다.
에이전트 프레임워크가 실패하는 이유는 모델이 약해서가 아니라, 행동 공간이 불완전하기 때문이다.
가능한 모든 행동을 처음부터 정의하는 대신, 정반대의 가정에서 출발하세요. 모델은 거의 무엇이든 할 수 있습니다. 그다음 제한하세요.
LLM에 가능한 한 많은 자유를 주고, 그다음 evals를 바탕으로 vibe-restrict 하세요.
Browser Use의 첫 번째 버전은 전형적인 에이전트 프레임워크였습니다. 복잡한 메시지 관리자와 수많은 추상화로 모델을 감싸서 동작을 제어하려 했습니다. 작동은 했지만 확장하기가 너무 고통스러웠습니다. 모든 실험이 프레임워크와 싸워야 했습니다. 새로운 기능을 추가하는 일은 Bitter Lesson에 거스르는 것처럼 느껴졌습니다. (공정하게 말하면 작년 이후 모델들은 정말 MUCH 더 좋아지긴 했습니다)
그래서 우리는 한 걸음 물러서서 더 근본적인 질문을 던졌습니다.
LLM은 실제로 무엇에 대해 극도로 뛰어나도록 학습되어 있는가 — 그리고 모델이 더 좋아져도 무엇이 계속 참일 것인가?
우리는 기존 에이전트를 통째로 버리고 처음부터 다시 시작했습니다. "최소한"이 정말 무엇을 뜻하는지 이해하기 위해 Claude Code와 Gemini CLI를 역분석했습니다. 정말 훌륭하고 대체로 단순한 프리미티브를 만든 그들에게 찬사를 보냅니다. 내부적으로는 복잡하더라도, 밑바탕의 아이디어는 단순합니다.
지능을 과도하게 명세하지 말고, 모델이 추론하게 하라.
우리는 이 철학을 Browser Use를 구동하는 최소한의 에이전트 프레임워크인 BU Agent에 담았습니다.
깨지기 쉬운 "click / type / scroll" 프리미티브 몇 개만 노출하는 대신, BU Agent는 모델이 브라우저의 원시 제어 표면에 접근할 수 있게 합니다.
핵심에는 순수한 Chrome DevTools Protocol (CDP) 명령을 내보낼 수 있는 능력이 있습니다. 실제로 모델은 브라우저 안에서 거의 무엇이든 할 수 있습니다.
그 위에는 브라우저 확장 API가 있습니다. 이것들은 CDP만으로는 어색하거나 불가능한 특정 작업을 아주 쉽게 만들어 줍니다. 예를 들면 활성 창에 접근하거나 권한이 필요한 브라우저 상태를 다루는 일입니다.
CDP와 확장 API는 각각 사각지대가 있습니다. 하지만 둘을 함께 쓰면 거의 완전한 행동 공간을 이룹니다.
모델이 그런 자유를 가지면 중요한 일이 일어납니다. 한 접근이 실패하면 다른 경로로 우회합니다. 하나의 도구가 고장 나면 다른 길을 찾아냅니다.
원리상 모든 것이 가능하기만 하다면, LLM은 즉석에서 스스로를 고치는 데 매우 뛰어납니다.

그래서 BU Agent는 단순한 발상의 전환에서 만들어졌습니다.
최대의 능력에서 시작하고, 그다음 제한하라.
모델에게 사람이 브라우저에서 할 수 있는 모든 일을 할 자유를 주세요. 그다음에야 안전장치, 구조, 제약을 덧붙이세요.
그래야 시스템이 더 나은 모델과 함께 확장할 수 있고, 모델과 싸우지 않게 됩니다.
진심입니다. 그들이 LLM 객체를 구현하는 방식은 고통스럽습니다.
그래서 직접 썼습니다. 호출을 아주 쉽게 처리하는 방식입니다. 그게 전부입니다. Anthropic, OpenAI, Google용입니다. 우리 텔레메트리 기준으로 이 셋이 사용 사례의 95%를 차지합니다.

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: ...
같은 인터페이스입니다. 캐싱, 직렬화, provider 특유의 동작을 완전히 제어할 수 있습니다. 마법은 없습니다. 예상 밖의 일도 없습니다.
캐싱을 처리하고 메시지를 직접 구현하는 편이 훨씬 쉽습니다. 완전히 모델 불가지론적입니다. 특정 provider에 묶이지 않습니다. 그냥 직접 결정하면 됩니다.
브라우저 에이전트에 필요한 흥미로운 점 하나가 있습니다. 브라우저 상태를 요청하면 그 크기가 엄청납니다. 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은 거대한 맥락을 제대로 처리하지 못합니다. 모델에 필요한 것은 최근 상태뿐이고, 오래된 브라우저 스냅샷은 잡음입니다.

순진한 접근법, 즉 모델이 도구 호출 없이 응답하면 멈추는 방식은 잘 작동하지 않습니다. 에이전트가 너무 일찍 끝나버립니다. 특히 어떤 맥락이 빠져 있을 때 그렇습니다. 후속 응답을 원하게 되지만, 이런 종류의 API를 쓰고 있다면 그건 불가능합니다.
이 문제를 해결하는 가장 좋은 방법은 done 도구입니다.
@tool('Signal that the current task is complete.')
async def done(message: str) -> str:
raise TaskComplete(message)
모델이 done 도구 호출을 출력하면 에이전트가 종료됩니다. 이렇게 하면 암묵적인 "이제 끝난 것 같네?" 대신 명시적인 완료를 강제할 수 있습니다.
우리에게는 두 가지 모드가 있습니다.
Claude Code도 이렇게 합니다. Gemini CLI도 이렇게 합니다. 이제 왜 이런 방식이 존재하는지 아셨을 겁니다.
맞습니다. for-loop은 단순합니다. 하지만 그것을 견고하게 만드는 일은 그렇지 않습니다.
그건 운영입니다. 이미 해결된 문제들입니다. 필요하긴 합니다. 하지만 그것을 에이전트 자체와 혼동하지는 마세요.
모든 추상화는 부채입니다. 모든 "도우미"는 실패 지점입니다.
모델은 좋아졌습니다. 정말 많이 좋아졌습니다. computer use, coding, browsing에 대해 RL 학습을 받았습니다. 그들에게 필요한 것은 여러분의 가드레일이 아닙니다. 필요한 것은 이것입니다.

쓰디쓴 교훈: 덜 만들수록, 더 잘 작동한다.
우리는 이것을 agent-sdk로 오픈소스화하고 있습니다.
원한다면 프로덕션에서 사용해도 좋습니다. 하지만 가능하면 그냥 맥락을 Claude Code에 붙여 넣고 직접 만드세요. 여러분이 코딩하는 어떤 언어로든요. 이 저장소에는 Claude Code를 다시 구현한 예시도 들어 있습니다.
어쨌든, 이것이 우리가 bu.app을 만들며 배운 내용입니다. 한번 써보세요. 정말 멋집니다!!