· 9분 분량

에이전트에게 모든 도구를 컨텍스트에 넣어줄 필요는 없습니다

DynoTable의 AI 에이전트는 도구 38개에 닿을 수 있습니다. 그 전부를 한 번에 보는 일은 드뭅니다. 모델이 즉시 받는 것과 직접 찾아 나서야 하는 것 사이에 어디로 선을 그었는가 — 이것이 툴킷 전체에서 가장 파급력이 큰 결정으로 드러났고, 그 선은 우리가 처음 그었던 자리 근처에도 남아 있지 않게 됐습니다.

모델이 작은 집합으로 시작해 나머지를 검색하도록 발견 메커니즘을 만들었습니다. 그러고 나서 저렴한 모델들이 그것을 쓰는 모습을 지켜본 뒤, 38개 도구 중 27개를 항상 보이는 코어로 되돌렸습니다. 메커니즘은 살아남았습니다. 누가 그것을 필요로 하는지에 대한 우리 이론은 살아남지 못했습니다.

스스로 찾아 나서리라고 믿을 수 없는 모델들을 위해 도구 표면을 만들며 배운 것들은 이렇습니다.

모델이 한 번도 호출하지 않는 도구도 비용을 물립니다

노출하는 도구 하나하나는 그 이름과 설명, 그리고 전체 입력 schema가 사용자가 무언가를 입력하기도 전에 요청에 직렬화되어 들어갑니다. 그런 것이 38개면 공짜일 리 없습니다.

토큰은 청구서의 작은 쪽 절반입니다. 진짜 비용은 선택 정확도입니다: 모델이 훑는 거의 똑같은 선택지가 많아질수록, 틀린 것을 고르는 일도 잦아집니다. 우리 카탈로그는 거의 똑같은 선택지로 가득한데, 의도한 것입니다. 우리 도구 중 다섯 개는 두 번씩 존재합니다 — openTableproposeOpenTable, openWorkbenchproposeOpenWorkbench, 그런 식입니다. 각 쌍은 같은 일을 합니다. 하나는 즉시 실행하고, 다른 하나는 사용자가 먼저 클릭할 칩을 내놓습니다. 그 구분은 안전성을 떠받치는 요소이면서, 이름만 나열된 평평한 목록에서는 거의 보이지 않습니다.

의 클라이언트 가이드도 바로 이 지점을 앞세웁니다: 모든 도구 정의를 미리 적재하면 토큰이 낭비되고, 지연이 늘고, 모델 성능이 떨어진다는 것이죠. 동의하기는 쉽습니다. 어떤 도구가 자리를 잃을지 정하는 데서 이야기가 재미있어집니다.

검색 도구를 만들었습니다. 최저 등급 모델은 호출하지 않았습니다.

메커니즘은 2계층입니다. 어떤 도구 집합은 첫 단계부터 활성 상태입니다. 나머지는 모델이 searchTools(query)를 호출하기 전까지 보이지 않습니다. 이 호출은 이름과 설명, 키워드를 기준으로 카탈로그를 채점해 일치 항목을 돌려주고, 이후 단계에서 모델이 호출할 수 있는 도구 집합에 그것들을 추가합니다.

도구 카탈로그에이전트 루프모델도구 카탈로그에이전트 루프모델1단계 — 활성 집합 = 인라인 코어2단계 — 활성 집합 확장searchTools("export csv")이름 + 키워드 채점startExport, getExportStatus,listActiveExports일치 항목 (이제 호출 가능한 이름들)startExport({tabId})

그런 다음 최저 등급 모델을 상대로 돌렸습니다. 우리는 이 에이전트를 프런티어 모델에 맞춰 튜닝하지 않습니다 — 여러분 자신의 Bedrock 자격 증명 위에서 돌아가기 때문에 사람들은 저렴한 모델을 고르고, 우리는 그중 가장 싼 것에 맞춰 최적화합니다. 첨부된 파일에 대해 물었더니, 그 모델은 대신 열린 탭 목록을 뒤지러 갔습니다. 검색 도구는 거의 호출하지도 않았습니다. 직접 보이지 않는 것은 그 모델에게 존재하지 않았습니다.

이 결과는 뻔한 설계를 무너뜨립니다. 발견이 어떤 도구에 닿는 유일한 경로라면, 그 도구가 필요한 모든 요청은 모델이 찾아 나서기로 선택하는 데 달리게 됩니다 — 그런데 도움이 가장 필요한 모델일수록 도움을 청할 가능성은 가장 낮습니다.

그래서 이 분할은 “작은 코어, 큰 꼬리”이기를 그만두고 도구가 아니라 요청에 대한 질문이 됐습니다: 사용자의 표현이 그 도구를 지목하는가? 발견 가능한 상태로 남겨둔 11개 도구는 그 답이 예인 것들입니다. “이걸 CSV로 내보내줘”는 모델이 export 를 검색하게 만듭니다. “지난달 주문 보여줘”는 필터 설정 도구를 검색하게 만들지 않으므로, 그 도구는 인라인으로 남습니다. 인덱스 통계, 저장된 스펙, 관계 인트로스펙션, 스테이징된 변경 표면은 모두 사용자가 원할 때 이름을 대고 요청하는 것들이지, 암묵적으로 요구되는 법은 없습니다.

인라인 27개는 우리가 사전에 옹호했을 법한 숫자가 아닙니다. 실제로 출시 기준으로 삼는 모델과 부딪히고도 살아남은 숫자입니다.

발견 메커니즘을 소리 없이 무용지물로 만들 뻔한 경쟁 조건

발견에는 틀리기 쉽고 알아채기는 어려운 타이밍 제약이 있습니다.

검색 도구가 일치 항목을 돌려주면, 그 이름들은 모델의 다음 단계가 준비되기 전에 허용 집합에 합류해야 합니다. 그렇게 하기에 뻔한 자리는 한 단계가 완료될 때 발화하는 훅입니다. 그런데 그 훅은 일부 SDK 버전에서 다음 단계의 준비 이후에 발화한다고 문서화돼 있습니다 — 변경이 한 단계 늦게 도착한다는 뜻입니다.

실패 양상이 고약합니다. 모델이 검색합니다. 필요한 도구의 이름을 담은 올바른 결과를 받습니다. 바로 다음 단계에서 그 도구를 호출하면, 그런 도구는 없다는 답을 듣습니다. 간헐적이고, 어떤 SDK 버전이 해석됐는지에 달려 있으며, 망가진 하네스가 아니라 멍청한 모델처럼 읽힙니다.

해법은 검색 도구 자신의 실행 안에서 허용 집합을 변경하는 것입니다. 그 실행은 루프가 전진하기 전에 완료되는 것이 보장됩니다. 문장이 놓이는 자리의 한 줄 차이지만, 제대로 동작하는 발견 메커니즘과 아무도 원인을 제대로 짚어내지 못할 이유로 일정 비율에서 실패하는 메커니즘을 가르는 차이입니다.

검색은 세 번, 그다음은 멈춤

검색은 에이전트 턴당 3회 호출로 제한됩니다. 네 번째는 실행되는 대신 이것을 돌려줍니다:

{"error": "search-budget-exhausted", "budgetCap": 3}

이 상한이 존재하는 이유는 특정한 루프 때문입니다: 모델이 검색하고, 상상했던 것을 찾지 못하고, 동의어로 다시 검색하고, 그것도 찾지 못한 채, 데이터베이스는 한 번도 건드리지 않고 검색 도구 안에서 단계 예산을 전부 태워버리는 루프입니다. 상한을 두면 더 검색해도 남는 것이 없어지는 지점에서 결정을 강제합니다 — 이미 찾은 도구 중 하나로 밀고 나가든지, 사용자에게 묻든지.

모델이 아직 발견하지 않은 도구를 호출할 때의 오류 메시지는 에이전트의 모든 검증기에 적용하는 것과 같은 원칙을 따릅니다:

Tool 'startExport' not in active set. Call searchTools(query='startExport')
to discover it, or use one of: <inline tool names>

회복 동작을 짚어주는 거부는 한 단계를 더 쓰는 데 그칩니다. 그냥 안 된다고만 하는 거부는 턴 하나를 통째로 날립니다.

도구마다 한 행, 나머지는 전부 파생

각 도구는 하나의 평평한 목록에 한 번만 선언되고, 그 행이 도구의 정체성 전부를 담습니다: 이름과 설명, 검색이 일치를 판정하는 키워드, 인라인으로 시작하는지 발견 대상으로 시작하는지, 어느 계층에서 실행되는지, 그리고 MCP로 어떻게 노출되는지.

이 계층은 가시성 분할만큼이나 중요합니다. 21개 도구는 무음 — 아무도 방해하지 않고 실행되는 읽기입니다. 16개는 인가 사다리 뒤에 게이트돼 있습니다. 정확히 하나는 어느 쪽에도 속하지 않는데, 검색 도구는 에이전트가 여러분의 데이터에 사용하는 능력이 아니라 루프 자체의 일부이기 때문입니다. MCP 노출은 같은 행의 세 번째 축입니다: 읽기 전용, 스테이징, 전체, 아니면 아예 제외 — 도구 3개가 여기에 해당합니다.

이것을 정직하게 유지하는 규칙은 시스템의 다른 모든 목록이 그 행들에서 파생된다는 것입니다 — 무음 계층 집합, MCP 범위 계층, 쓰기 범위 집합 — 그리고 그중 어느 것도 손으로 관리되지 않습니다. 손으로 관리하는 무음 목록 옆에 손으로 관리하는 MCP 목록을 두는 것이야말로, 어떤 도구가 채팅에서는 제대로 게이트되면서 외부 클라이언트에는 조용히 열려 있게 되는 경로입니다.

미처 예상하지 못한 제약은 선언 목록에 런타임 import가 하나도 없어야 한다는 것이었습니다. 이 목록은 데스크톱 UI와 백엔드가 공유하는데, import 하나가 어떤 도구의 구현을 거쳐 전이적으로 Node 전용 crypto 의존성에 닿습니다. 그것을 브라우저 번들로 끌어들이면 앱은 모듈 로드 시점에 죽습니다. 타입 검사기도 단위 테스트도 이것을 잡지 못합니다 — 둘 다 그 import를 아무 문제 없이 해석합니다. 잡아내는 것은 파일을 텍스트로 읽어서 import 문이 하나라도 있으면 실패하는 테스트인데, 처음으로 여러분을 구해주기 직전까지는 조잡하게 느껴집니다.

직접 만든다면 그대로 적용되는 것들

  • 아키텍처를 옹호하기 전에 도구 수를 세어보세요. 올바른 분할은 원칙이 아니라 측정값입니다.
  • 발견 메커니즘은 가장 약한 모델을 상대로 테스트하세요. 프런티어 모델은 검색해야 할 때 검색합니다. 그 사실은 여러분의 사용자가 고르는 모델에 대해 아무것도 말해주지 않습니다.
  • 가시성은 사용자 자신의 표현이 그 도구를 지목하는지로 정하세요. 암묵적으로 호출되는 도구는 인라인에 속하고, 사람들이 이름을 대고 요청하는 도구는 찾게 두면 됩니다.
  • 순서에 민감한 것을 프레임워크의 단계 훅에 넣기 전에, 그 훅이 실제로 언제 발화하는지 확인하세요.
  • 메타 도구에는 상한을 두세요. 실제 상태를 건드리지 않고 반복 호출할 수 있는 것은 결국 반복 호출되며, 검색에 쓰인 단계 예산은 통째로 날린 턴입니다.
  • 발견되지 않은 도구 오류도 다른 모든 검증기 오류와 마찬가지로 회복 호출을 짚어주게 만드세요.
  • 각 도구는 한 번만 선언하고 다른 모든 목록은 거기서 파생시키세요. 같은 도구를 담은 손 관리 목록 둘은 결국 어긋나고, 그 어긋남은 보안 경계에서 드러납니다.
  • 어떤 모듈이 컴파일러로는 표현할 수 없는, 떠받치는 제약을 지고 있다면, 그것을 텍스트로 강제하는 조잡한 테스트를 쓰세요.

이것이 돌아가는 곳

이 모든 것은 DynoTable도구 카탈로그 안에 실려 있습니다 — 여러분 자신의 자격 증명 위에서 도는 schema 인지 쿼리, 그리고 언제나 검토 가능한 스테이징 영역에만 안착하는 쓰기입니다. 같은 선언이 외부 에이전트가 접속하는 MCP 서버도 굴리는데, 여기서는 각 행의 노출 계층이 외부 클라이언트에 부여되는 범위가 됩니다. 그것을 어떻게 안전하게 만들었는지 — OAuth, 동의, 자격 증명 격리 — 는 별개의 이야기입니다.

그리고 이 모든 것의 밑에 깔린 계층, 즉 이 도구 하나하나를 저렴한 모델이 견뎌낼 수 있게 만드는 검증기들은 그 자체로 하나의 글입니다.

Console 없이 DynamoDB 작업하기

DynamoDB로는 실행할 수 없는 진짜 SQL(JOINs, GROUP BY, 집계)을 실행하는 빠른 DynamoDB 데스크톱 클라이언트. 시각적 편집과 여러분 자신의 Bedrock 키로 동작하는 AI 에이전트를 제공합니다.

30일 무료 체험, 신용카드 불필요 — 이후 기간 제한 없는 무료 요금제.