· 10분 분량

저렴한 LLM이 DynamoDB에서 안정적으로 동작하게 만든 방법: 도구 검증기 + eval

DynoTable의 AI 어시스턴트는 여러분 자신의 자격 증명으로 동작하며, 이는 여러분이 모델을 고른다는 뜻입니다 — 그리고 많은 사람이 저렴한 모델을 고릅니다. 그래서 우리는 어시스턴트를 프런티어 모델에 맞춰 튜닝하지 않습니다. 우리의 기준선은 Amazon Nova Lite로, 쿼리당 비용이 반올림 오차 수준이면서 프런티어 모델은 하지 않는 방식으로 실패하는 모델입니다.

이 글 전체의 핵심 한 방은 이렇습니다: 한 시나리오에서 — 복합 키 테이블을 기반으로 삼아야 하는 조인 — Nova Lite는 0%를 기록했습니다. 우리는 모델을 바꾸지도, 퓨샷 예제를 추가하지도, 파인튜닝하지도 않았습니다. 우리는 검증기 오류 메시지 하나를 대신 무엇을 해야 하는지 정확히 말하도록 다시 썼습니다. 그러자 100%로 올라, 바로 다음 단계에서 조인을 성공시켰습니다.

병목은 결코 모델이 아니었습니다. 메시지였습니다.

이것은 그 교훈 뒤에 숨은 엔지니어링 이야기입니다: 모든 도구 호출을 감싸는 검증기, 우리가 의도적으로 검증하지 않는 것들, 그리고 모든 변경을 심판하는 eval 하네스. 예산형 모델로 를 만들고 있다면 — DynamoDB 위에서든 다른 무엇에서든 — 그 대부분이 그대로 적용됩니다.

저렴한 모델이 유독 DynamoDB에서 실패하는 이유

DynamoDB는 작은 모델에게 적대적인 대상입니다. 쿼리 표면이 SQL처럼 보이지만 그렇지 않기 때문입니다. PartiQLSELECT 구문은 받아들이지만 JOIN도, GROUP BY도, DISTINCT도, LIKE도 지원하지 않습니다 — 그리고 인터넷에서 SQL을 배운 모델은 그 모두를 자신 있게 내뱉습니다.

우리가 실제로 관찰한 실패들이며, 각각은 실제로 로그에 남은 실행에서 나온 것입니다:

  • 누락된 limit 인수. 은 흔히 쿼리 도구의 선택적 행 제한을 빠뜨립니다. 백엔드는 기꺼이 모든 것을 반환하고, 200 KB짜리 도구 결과가 대화에 들어오며, 다음 모델 턴은 컨텍스트 한계에서 죽습니다 — 루프 도중의 ValidationException이 엉뚱한 컴포넌트 탓으로 돌려집니다.
  • 지원되지 않는 구문. PartiQL의 JOIN, GROUP BY, 집계 — DynamoDB가 결코 실행하지 않을 문장인데, Postgres에서라면 맞기 때문에 내뱉어진 것들입니다.
  • 식별자 뒤바뀜. Nova Lite에 SELECT * FROM customers와 “독일에 대한 WHERE를 추가하라”는 지시를 주면, 때때로 SELECT * FROM orders WHERE … 를 반환했습니다 — 사용자가 전혀 언급하지 않은 테이블이죠. 그 문장은 완벽하게 파싱됩니다. 구문적으로는 아무것도 이를 잡아내지 못합니다.
  • 도구 오라우팅. 첨부된 파일에 관해 물으면, Nova Lite는 탭 속 파일을 찾으려고 열린 탭 목록 도구를 호출했습니다 — 도구 검색 메커니즘은 거의 전혀 사용하지 않아서, 곧바로 보이지 않는 것은 존재하지 않는 셈이었습니다.

이 중 어느 것도 가정이 아닙니다. 각각이 아래의 특정 가드레일을 이끌어냈습니다.

도구 경계에서 검증하고, 오류를 교훈으로 만들라

가장 중요한 단 하나의 아키텍처 결정: 검증은 입력 schema가 아니라 도구의 실행 단계 안에 있습니다. schema 거부는 막다른 길입니다 — 루프는 타입 오류를 보고 아무것도 배우지 못합니다. 실행하고, 검사하고, 구조화된 오류를 반환하는 도구는 모델에게 다음 단계에서 반응할 수 있는 정상적인 도구 결과를 줍니다.

그리고 그 오류는 사람이 볼 로그가 아니라 모델을 위해 작성됩니다. 우리 내보내기 도구의 오류는 그대로 옮기면 다음과 같습니다:

startExport requires exactly one of {tabId} or {sql}.
FIX: call listOpenTabs and pass {tabId}, or pass {sql} with a single SELECT.
startExport {sql} must be a read-only single SELECT.
FIX: remove any INSERT/UPDATE/DELETE/DDL and pass one SELECT statement.

쿼리 검증 실패에는 validation: 접두사가 붙습니다 — validation: parse-error: …, validation: partiql-unsupported: JOIN is not supported by DynamoDB PartiQL. — 이는 시스템 프롬프트가 같은 입력을 다시 시도하기보다 문장을 고치라는 신호로 확립한 것입니다. 거부 메시지는 가르침의 신호입니다. 우리가 FIX: 관례를 채택하기 전에는, 같은 실패가 모델이 사과하며 사용자에게 직접 내보내기 버튼을 클릭하라고 말하는 것으로 끝났습니다. 그 후로는, 스스로 교정하고 작업을 완료합니다.

그 따름정리는 배우는 데 더 오래 걸렸습니다: 구체적인 예제가 어디에 있느냐가 설계 축입니다. 실수가 런타임에 검증기에 잡힌다면, 시스템 프롬프트는 일반적으로 유지하고 동적 오류 메시지가 구체적인 테이블과 수정 방법을 전달하게 하세요 — 프롬프트는 작게 유지되고 교훈은 필요한 바로 그때 도착합니다. 어떤 검증기도 잡을 수 없는 실수(애초에 집계 질문을 올바른 도구로 라우팅하는 것 같은)만이 프롬프트 자체에 구체적인 예제를 얻습니다.

DynamoDBTool boundaryModelDynamoDBTool boundaryModelrunPartiQL("SELECT … JOIN …")validation: partiql-unsupported:JOIN is not supported by DynamoDB PartiQL.runWorkbenchSql("SELECT … JOIN …")streamed pages (capped)rows10 rows / 5 KB + "Showing 10 of 4,200…"

거부할 것들을 파싱하라

JOIN을 일반적인 syntax error at offset 27으로 거부하면 아무것도 가르치지 못합니다. 그래서 우리가 손수 작성한 PartiQL 파서는 살짝 심술궂은 일을 합니다: DynamoDB가 지원하지 않는 구문을 의도적으로 파싱해 유효한 구문 트리로 만듭니다 — 오직 워커가 구체적이고 가르칠 수 있는 진단을 내보낼 수 있게 하기 위해서입니다. 모두 그대로 옮긴, 모두 모델을 향한 것들:

  • JOIN is not supported by DynamoDB PartiQL.
  • GROUP BY is not supported by DynamoDB PartiQL.
  • TOP N is not supported. Use the API limit parameter.
  • IN list has 120 items. DynamoDB PartiQL caps IN at 50 values (PK column) or 100 values (non-key column).
  • IS NOT NULL is not supported in DynamoDB PartiQL. Use attribute_exists.
  • lower(path) is not a DynamoDB PartiQL function. Use … instead. — 대체 함수는 함수별로 해석됩니다.

각 메시지는 구문 탈출구를 함께 이름 짓습니다. 프롬프트 규칙 “PartiQL이 지원되지 않는 구문을 거부하면 즉시 SQL 워크벤치로 전환하라”와 짝을 이루면, 저렴한 모델은 갈팡질팡하는 대신 한 단계 만에 자신의 Postgres 본능에서 회복합니다.

무엇을 검증하지 _않을_지 알라

우리 SQL 검증기의 schema 인식 계층은 테이블과 열 참조를 앱이 이미 보유한 실제 테이블 설명에 대해 해석합니다. 초기에는 DynamoDB가 선언하지 않은 속성에 대한 참조도 표시했는데 — 맞는 것처럼 들리지만 정확히 틀린 것입니다. DynamoDB의 attributeDefinitions 속성만 나열하므로, 그 검사는 WHERE <non-key> = … 를 거부했습니다 — 존재하는 가장 흔한 정당한 쿼리 형태를요. 이제 그 검사는 결코 차단하지 않는 경고입니다.

모든 검증기는 거부된 형태가 옳기보다 그를 가능성이 크다는 내기입니다. 데이터 모델이 그 검사를 뒷받침할 수 없다면, 그 내기를 하지 마세요.

안전장치만이 아니라 동작으로서의 결과 상한

누락된 limit으로 인한 폭발에 대한 해법은 “프롬프트에 제한을 추가하라”가 아니었습니다(저렴한 모델은 어차피 그것을 빠뜨리니까요). 쿼리 도구 결과는 강하게 제한됩니다 — 10행 또는 5 KB 중 먼저 도달하는 쪽으로, 항상 최소한 첫 행은 유지하면서요 — 그리고 잘림 알림 자체가 처방적입니다:

Showing 10 of 4,200. To filter: re-run with WHERE.
To view in UI: emit proposeOpenTable.
To aggregate: use runWorkbenchSql GROUP BY.

이 상한은 컨텍스트가 담을 수 있는 것보다 의도적으로 훨씬 낮습니다. 목표는 동작에 있습니다: 모델이 사용자에게 실제 테이블 뷰나 정확한 집계를 건네도록 강제하고, 결코 행을 채팅에 나열하지 못하게 하는 것. 회복 힌트는 정확한 도구를 이름 짓기에, 상한은 모델이 필요로 하는 바로 그 순간에 라우팅을 가르칩니다. 같은 봉투가 eval 하네스에 바이트 단위로 그대로 반영되며, 회귀 시나리오는 10,000개 항목 테이블에 대해 truncated: true를 고정해 상한이 결코 조용히 사라질 수 없게 합니다.

행을 채팅에 나열하는 대신, 어시스턴트는 여러분이 클릭해 결과를 실제 DynoTable 탭으로 열 수 있는 칩을 내보냅니다.
행을 채팅에 나열하는 대신, 어시스턴트는 여러분이 클릭해 결과를 실제 DynoTable 탭으로 열 수 있는 칩을 내보냅니다.

eval이 심판이다

위의 어느 것도 직관으로 설계되어 믿음으로 유지된 것이 아닙니다. 모든 프롬프트 절, 검증기 메시지, 상한은 시드된 DynamoDB Local에 대해 실제 에이전트 루프를 실행하는 eval 스위트로 게이트됩니다 — 실제 도구, 실제 쿼리 실행, 다시 구현한 것이 아니라 직접 임포트한 프로덕션 프롬프트 빌더와 검증기 — 이를 도구 추적, 데이터베이스의 최종 상태, 그리고 텍스트에 대한 이진 검사로 채점합니다.

직관이 놓친 것을 eval이 잡아낸 사례들:

  • “더 깔끔한” 프롬프트 일반화가 한 시나리오를 100%에서 0%로 퇴보시켰습니다. 우리 눈에는 더 잘 읽혔습니다. 기준선 모델은 동의하지 않았습니다. 오직 eval만이 알아챘습니다.
  • 프런티어 모델용 프롬프트 조언은 저렴한 모델에는 적극적으로 틀립니다. 공개된 지침은 큰 모델이 CRITICAL/MUST 표현에 과잉 반응하므로 그것을 누그러뜨리라고 말합니다. 저렴한 모델은 과소 준수하며 단호한 버전이 필요합니다. 이제 우리는 빌려온 조언을 믿음으로 적용하는 대신 eval로 게이트합니다.
  • 엄격한 도구 단언이 올바른 동작을 벌했습니다. 모델이 명확화를 위해 ask 도구를 호출하도록 요구한 채점기는 모델 사다리 전반에서 19%를 통과했습니다 — 거의 모든 “실패”가 텍스트로 명확화를 올바르게 요청한 모델이었습니다. 이제 채점기는 둘 중 어느 쪽이든 받아들입니다. 메커니즘이 아니라 사용자를 향한 의도를 채점하세요.
  • 모델 사다리는 그 좁힘을 스스로 정당화했습니다. 우리는 넓게 시작했습니다 — Mistral, AI21, GLM, Qwen, Nova Pro, 그 밖에도 — 그리고 오직 실제 실행만이 드러내는 이유들 때문에 두 단(Nova Lite, Claude Haiku)으로 줄였습니다: 한 계열은 Bedrock이 프롬프트 캐싱을 제공하지 않아 호출당 비용이 약 10배 들었고, Nova Pro는 개방형 조인에서 customers.customerName 같은 연결된 속성 이름을 환각했습니다. 그리고 한 계열은 아예 차단 목록에 올려야 했습니다 — Bedrock의 통합 API는 어댑터가 함수 호출을 일반 텍스트로 내보내는 모델에 대해서도 도구 정의를 기꺼이 받아들이는데, 이는 API 메타데이터가 알려주지 않는 것입니다. 우리는 이를 명세가 아니라 유료 스모크 프로브로 찾아냈습니다.

두 가지 규율이 스위트를 정직하게 유지합니다. 예제 테이블은 결코 테스트 테이블과 같지 않습니다: 프롬프트 예제는 시드 데이터에 존재하지 않는 중립적인 이름을 쓰고, eval은 다른 테이블에서 실행됩니다 — 그래서 통과는 모델이 우리의 예제를 외웠다는 것이 아니라 패턴을 일반화했다는 것을 증명합니다. 그리고 $0 감시 모드는 녹화된 픽스처를 거부합니다: 재생된 모델 출력은 실제 도구 활동에 의존하는 단언을 초록불로 통과시킬 것이므로, 무료 계층은 배선만 검증하고, 점수 를 주장하는 것은 무엇이든 실제 모델에 실제 토큰을 써야 했습니다.

이런 것을 만들고 있다면, 체크리스트

  • 기준선 모델을 먼저 고르고 항상 그에 대해 eval하세요. 프런티어 모델은 여러분의 버그를 숨깁니다.
  • 검증을 도구의 실행 단계에 두세요. 루프가 반응할 수 있는 오류를 반환하고, schema 막다른 길은 결코 안 됩니다.
  • 모든 거부를 what's wrong + FIX: exact next action으로 작성하고, 사용자의 실제 테이블을 그 안에 끼워 넣으세요.
  • 거부하려는 것을 파싱해서, 진단이 구체적일 수 있게 하세요.
  • 각 검증기를 여러분의 데이터 모델에 비추어 점검하고 — 정당한 형태를 거부하는 것은 삭제하세요.
  • 도구 결과를 담기는 것보다 낮게 제한하고, 잘림 알림이 모델을 올바른 다음 도구로 안내하게 만드세요.
  • eval을 프로덕션 코드 경로를 재사용해 실제(로컬) 백엔드에 대한 실제 루프로 실행하세요. 예제 데이터를 테스트 데이터와 분리해 두세요. 모든 프롬프트와 메시지 변경의 심판을 취향이 아니라 eval로 삼으세요.

프런티어 모델을 쓴다면 이 중 무엇이라도 중요한가?

덜 자주지만, 그렇습니다. 검증기는 모든 계층에서 핵심적입니다 — 프런티어 모델도 여전히 DynamoDB에서 JOIN을 실행할 수 없고, 처방적 오류는 그저 더 빠르게 소화될 뿐입니다. 우리 자신의 실행에서 프런티어급 모델의 칩 방출 동작이 더 작은 모델은 처리한 프롬프트 표현 아래에서 한때 0%로 떨어졌습니다 — eval 하네스가 그것도 잡아냈습니다. 신뢰성 엔지니어링은 소형 모델에 부과되는 세금이 아닙니다. 소형 모델은 그저 청구서가 더 빨리 도착하게 할 뿐입니다.

이것이 동작하는 곳

이 모든 것은 DynoTable의 어시스턴트와 그 게이트가 걸린 도구 카탈로그 안에 담겨 출시됩니다: 여러분 자신의 Bedrock 자격 증명으로 동작하는 schema 인식 자연어 쿼리, 정확한 테이블 전체 집계, 그리고 언제나 검토 가능한 스테이징 영역에만 도달하는 쓰기. 외부 에이전트는 MCP 서버를 통해 동일한 검증된 도구 모음을 받습니다 — 그것을 어떻게 안전하게 만들었는지는, OAuth부터 자격 증명 격리까지, 그 자체로 하나의 이야기입니다.

그리고 생성이 아니라 결정성을 원할 때 — 코드로 커밋할 쿼리 말입니다 — 모델을 아예 건너뛰세요: Expression Builder가 정확한 요청을 손으로 조립해 줍니다.

Console 없이 DynamoDB 작업하기

DynoTable은 DynamoDB를 위한 빠른 데스크톱 클라이언트입니다 — 테이블을 탐색하고, SQL 스타일 쿼리를 실행하고, 항목을 로컬에서 편집하세요.