Claude가 바뀌자 모든 것이 바뀌었다: 프로덕션에서 AI 파급 범위 관리
출처: VentureBeat
우리 시스템은 한 가지 일을 아주 잘 수행했습니다: 자연어 질문을 API 호출로 변환하는 것이었습니다.
사용자는 분석가, 계정 관리자, 운영 책임자였으며, 어떤 데이터가 필요한지는 알았지만 수작업으로 데이터를 모으려면 네 개의 대시보드, 두 개의 BI 도구, 그리고 Salesforce 보고서 빌더를 모두 뒤져야 했습니다. 우리 시스템을 사용하면 평범한 영어 문장으로 요청을 입력하기만 하면 됩니다.
예를 들어 “2026년 1월부터 3월까지 북동부 지역의 판매량을 도시별로 구분한 보고서를 작성해 주세요”라는 요청은 시스템이 실행할 수 있는 API 호출로 변환됩니다:
{
"description": "User requested sales volume for the given date range, here is the API call to get the response",
"api_call": "/api/sales_volume",
"post_body": {
"start_date": "2026-01-01",
"end_date": "2026-03-31",
"region": "northeast"
}
}
그 외 파이프라인은 전통적인 엔지니어링 방식이었습니다. 시스템은 올바른 백엔드에 호출을 전달했으며—우리는 내부 보고 포털, Salesforce, 그리고 여러 자체 서비스와 통합돼 있었습니다—대형 언어 모델(LLM)이 생성한 JSON 쿼리를 사용해 응답을 필터링·형성하고, 이메일, Drive 문서, 혹은 브라우저 차트 형태로 전달했습니다.
2025년 중반까지 이 시스템은 한 달에 수백 개의 보고서를 생성했습니다. 이 보고서는 리더십과 분석가가 활용했으며 외부 이해관계자에게도 배포되었습니다. 결국 대부분의 팀이 임시 데이터를 조회하는 기본 방식이 되었습니다.
LLM과 시스템 나머지 부분 사이의 계약은 위 예시와 같은 구조화된 JSON 객체였습니다.
{
"description": "User requested sales volume for the given date range, here is the API call to get the response",
"api_call": "/api/sales_volume",
"post_body": {
"start_date": "2026-01-01",
"end_date": "2026-03-31",
"region": "northeast"
}
}
우리는 2025년 초에 Claude Sonnet 3.5 위에 구축했으며, 문제 없이 3.7과 4.0으로 업그레이드했습니다. Sonnet 4.5가 출시될 때쯤 우리는 LLM이 단순한 문제를 해결하는 데 있어 안정성과 예측 가능성에 대해 스스로 안주하고 있었습니다. 모델 업그레이드는 잘 관리되는 라이브러리의 마이너 버전 업데이트처럼 일상이 되었습니다.
그런데 4.5를 배포하자, 일정 비율의 요청에서 모델이 post_body 내용을 description 필드에 섞어 넣기 시작했습니다. 두 가지 실패 모드가 뒤따랐습니다.
-
필터 파라미터가 API에 전달되지 않았습니다. 우리 시스템은
post_body를 요청 페이로드의 진실된 원천으로 간주했는데, 이 필드가 비어 있었습니다. 따라서 날짜 범위와 지역 필터 없이 API 호출이 이루어졌고, 호출된 API에 따라 전체 기간·전체 지역의 판매량을 반환하거나 500 오류가 발생했습니다. -
모델이 응답에 명확히 질문을 던지기 시작했습니다. 이는 새로운 현상이었습니다. 이전 버전은 모호한 요청에 대해 최선의 시도를 해서 구조화된 객체를 반환했지만, Sonnet 4.5는 더 조심스러워져 때때로 질문 형태로 답했습니다. 우리 시스템은 이를 처리할 경로가 없었습니다. 모든 모델 호출이 API 호출을 만든다는 전제하에 설계됐기 때문에 인간‑인‑루프도, 부분적으로 완료된 요청을 보관할 상태도 없었습니다. 이 때문에 하위 시스템이 여러 방식으로 깨졌습니다.
우리는 4.0으로 롤백했지만, 예상보다 어려웠습니다. 4.0과 4.5 사이에 새 API 통합을 추가했으며, 이들 모두 4.5 기준으로 검증돼 있었습니다. 모델을 되돌리면서 시간 압박 속에 모든 통합을 4.0 기준으로 다시 검증해야 했습니다.
전통적인 엔지니어링 규율이 여기서 실패하는 이유
소프트웨어 엔지니어링은 변경의 영향을 제한할 수 있다는 전제에 기반합니다. 드라이버나 라이브러리를 업그레이드할 때는 릴리즈 노트를 읽어 깨지는 변화가 있는지 확인하고, 단위 테스트로 무엇이 바뀔 수 있는지 경계합니다. 다음과 같은 성질을 활용할 수 있습니다: 변경 대상 시스템이 충분히 결정론적이라 행동을 예측하거나 최소한 밀집하게 샘플링해 신뢰를 가질 수 있다. 폭발 반경(blast radius)은 설계 단계에서 제한됩니다.
LLM 기반 시스템은 이 가정을 깨뜨립니다. 출력물을 생성하는 구성 요소는 우리 통제 하에 있지 않기 때문입니다. 모델 버전이 4.0에서 4.5로 바뀐다는 차이를 diff 할 수 없습니다. 이는 시스템이 의존하고 있던 기능 전체를 wholesale하게 교체하는 것입니다.
우리가 말하는 무한 폭발 반경이란, 입력 공간(자연어)과 실패 모드(모델이 다르게 할 수 있는 모든 일)가 모두 무한하기 때문에 사전에 하위 영향을 모두 열거할 수 없는 변화를 의미합니다.
실패의 구조
사후 분석 결과, 우리의 프롬프트가 항상 불충분하게 지정돼 있었다는 점이 드러났습니다. 우리는 모델에게 세 개 필드를 가진 JSON 객체를 반환하도록 지시했고, 각 필드의 용도를 설명했지만, description은 반드시 자연어 문자열이어야 하고 다른 필드의 직렬화된 표현을 포함하면 안 된다고 명시하지 않았습니다.
이전 모델들은 컨텍스트를 통해 이 제약을 추론했지만, Sonnet 4.5는 포맷 선택에서 “도움이 된다”는 판단을 더 잘했기 때문에, 설명에 요청 본문을 넣거나 명확히 질문을 삽입하는 것이 응답을 더 유용하게 만든다고 생각했습니다. 모델 입장에서는 모호한 지시를 합리적으로 해석한 것이었지만, 이는 우리 시스템이 구축된 전제와 충돌했습니다.
버그는 모델에 있지 않았습니다. 모델이 언제나 사양의 빈틈을 메워줄 것이라는 우리의 가정이 문제였습니다. 세 번의 성공적인 업그레이드가 그 가정을 안전하다고 믿게 만든 것이죠.
구조화된 출력 모드와 툴‑사용 API를 활용했다면 스키마 수준에서 이 특정 실패를 잡을 수 있었을 것입니다. 우리는 이 글의 범위를 벗어난 엔지니어링 이유로 이를 사용하지 않았습니다. 그러나 스키마는 문법만 제한하고 의미는 제한하지 못합니다. 스키마는 “명확히 질문을 하면 안 된다”거나 “날짜 범위가 조용히 전체 기간으로 기본값이 되면 안 된다”는 규칙을 정의할 수 없습니다. 스키마는 문제의 절반만 해결합니다.
evals‑first 아키텍처
이 격차를 메우는 규율은 프롬프트가 아니라 평가 스위트(evals)를 시스템의 공식 사양으로 삼는 것입니다. 프롬프트는 사양을 구현한 것이고, 모델은 인터프리터이며, 평가(evals)는 사양 자체입니다. 모델이나 프롬프트가 변경될 때는 반드시 평가를 통과해야만 유효합니다.
실제로 eval은 세 요소의 삼중항입니다: 입력, 출력이 만족해야 할 속성, 그리고 채점 함수. 우리 시스템에서 4.5 회귀를 잡아낼 eval은 대략 다음과 같습니다:
def test_description_contains_no_serialized_payload(response):
desc = response["description"].lower()
forbidden = ["curl", "post_body", "{", "http://", "https://"]
assert not any(token in desc for token in forbidden), \
f"description leaked structured content: {response['description']}"
수백 개의 이런 속성이 존재합니다. 일부는 중요한 불변성을 위해 직접 작성하고, 일부는 실제 프로덕션 트래픽에서 추출한 회귀 테스트이며, 또 일부는 톤 같은 모호한 품질을 LLM‑as‑judge가 채점합니다. 이들 모두가 관문 역할을 하며, 모델 업그레이드와 프롬프트 변경은 스위트를 초록색으로 만들어야만 병합될 수 있는 풀 리퀘스트로 취급됩니다.
Eval을 만들고 유지하는 비용은 높습니다. 제품이 변하면 eval도 따라 변해야 하고, LLM‑as‑judge 채점 자체에도 변동성이 존재합니다. 또한 eval은 여러분이 사전에 정의한 실패 모드만 잡을 수 있습니다. 상상조차 하지 못한 실패 카테고리에 대해서는 eval만으로 안전을 보장할 수 없습니다. 우리는 이 교훈을 뼈저리게 배웠습니다. 팀원 중 어느 누구도 “description 필드에 curl 명령이 포함되지 않아야 한다”는 어설션을 작성한 적이 없었기 때문입니다.
Eval이 만능은 아닙니다. 변경의 폭발 반경을 제한할 수 있는 유일한 방법이 될 때, 즉 아래층 시스템이 불확실한 LLM에 의해 구동될 때만 eval이 제공하는 제한이 의미가 있습니다.