내가 만든 가장 신뢰할 수 있는 데이터 에이전트는 90%가 결정론적 코드임. LLM은 그저 의도를 파악하고 대화할 뿐. 반박 시 님 말이 맞음.
The most reliable data agent I've shipped is ~90% deterministic code. The LLM just parses intent and talks. Change my mind.
핵심 요약
데이터 에이전트의 신뢰성은 LLM이 아닌 결정론적 코드와 엄격한 검증 체계에서 나온다는 실무적 통찰.
- 컨텍스트 그래프 — 스키마 덤프 대신 비즈니스 개념과 실제 필드를 매핑하여 모델의 환각 방지
- 결정론적 워크플로우 — LLM이 추측하게 두지 않고 코드 기반의 상태 전이와 로직으로 흐름 제어
- 데이터 요약 — 원시 데이터 대신 요약된 정보만 모델에 전달하여 컨텍스트 윈도우 낭비 및 오류 방지
- 명시적 실패 처리 — 모호한 추측 대신 정보 부족 시 사용자에게 질문하거나 오류를 발생시켜 신뢰도 확보
BigQuery 웨어하우스와 미디어 믹스 모델링 플랫폼을 기반으로 마케팅 인텔리전스 에이전트인 MIA를 만들었다. 데이터 상태는 그야말로 개판이다. 채널 지출, 모델 결과값, 그리고 응답이 온통 중첩된 쓰레기 덩어리인 플래너 API까지, 아주 가관이다.
이걸 배포하고 나서 내린 결론은 이거다. 신뢰성은 LLM을 제외한 나머지 모든 것에서 나온다. 모델은 그냥 자연어 껍데기일 뿐이고, 의도를 파악해서 결과를 설명하는 역할만 한다. 신뢰할 수 있는 모든 부분은 결정론적이고, 타입이 지정되어 있으며, 테스트를 거쳤다. 이건 고백이 아니라, 이 바닥의 올바른 종착지라고 생각한다.
우리가 진짜 싸워야 했던 건 "에이전트는 신뢰할 수 있어야 한다"는 문제였다. 현실의 지저분한 데이터를 다룰 때, 에이전트는 그럴듯하게 말하는 데는 도사지만, 정작 맞는 말을 하는 데는 젬병이다. 쿼리 결과가 비어있으면 멋대로 컬럼을 지어내거나, 조인 키를 추측하거나, 숫자를 날조해서 CMO한테 아주 당당하게 내민다. 그래서 실제로 효과를 본 5가지 방법을 공유한다.
1. 스키마 덤프가 아니라 컨텍스트 그래프를 써라.
스키마를 프롬프트에 때려 박지 마라. 비즈니스 개념을 실제 물리적 필드, 조인 경로, 열거형 사전으로 매핑하는 그래프를 따로 뒀다. "매출(Revenue)"은 추측이 아니다. 그래프가 outcomeKPI + optimisedBudgetData.response라고 딱 정해준다. "현재 지출(Current spend)"은 모델이 멋대로 추측한(실제로는 존재하지도 않는) spend가 아니라, currentBudgetData.spend로 정확히 연결된다. 에이전트는 질문에 맞는 서브그래프만 가져온다. 그래프가 주지 않은 필드는 아예 참조조차 못 하게 막았고, 그래프는 오직 진짜 필드만 알고 있다.
이 그래프에는 지저분한 사내 지식도 다 녹아있다. 세 개의 status 컬럼 중 뭐가 진짜인지, mmmRequestId는 camelCase인데 다른 엔드포인트는 snake_case를 원한다는 거, currentBudgetData.spend에서 0은 "누락"이 아니라 "채널 잠금"을 의미한다는 것까지. 이런 디테일이 에이전트를 죽이는 주범인데, 이걸 프롬프트에 넣지 마라. 테스트 가능한 타입 레이어에 넣어라.
2. 결정론적 단계는 코드로 짜라, 느낌으로 하지 말고.
예전에는 우리 워크플로우(최적화 → 예측 → 페이싱)가 시스템 프롬프트에 "먼저 X를 하고, 그다음 Y, 그다음 Z를 해" 같은 식으로 적혀 있었다. 그러니까 모델이 단계를 건너뛰거나, 순서를 바꾸거나, 아예 없는 단계를 지어내더라. 그래서 뼈대를 실제 코드 워크플로우 그래프로 옮겼다. 순서, 게이팅, 상태 전환은 전부 결정론적으로 돌아간다. LLM은 딱 두 군데서만 작동한다. 사용자의 의도를 타입이 지정된 파라미터로 파싱하는 것, 그리고 최종 구조화된 결과를 설명하는 것. 모델이 절차를 추측할 권한은 없다. 그건 이제 모델의 업무가 아니니까.
경험칙 하나 주자면, 어떤 단계가 결정론적이라면 그걸 LLM이 하게 두는 건 기능이 아니라 부채다.
3. 도구는 원본 데이터가 아니라 요약본을 반환하게 해라.
도구가 19MB짜리 중첩 JSON을 모델한테 던져주면, 모델은 반드시 경로를 추측해서 탐색하려고 들고, 결국 틀린다. 도구 레이어에서 데이터를 추출하고 다이어트를 시켜라. 도구는 실제 계산된 값을 포함한 {summary, channels:[{channel, current_spend, optimised_spend, delta}]} 형태만 반환한다. 모델은 원본 중첩 데이터에 손도 못 대니까 경로를 추측할 일 자체가 없다. 덤으로, 컨텍스트 윈도우 터지는 문제도 해결됐다(예전엔 "모델 목록" 호출 한 번에 모델 객체 1000개가 다 튀어나와서 수백만 토큰을 잡아먹었는데, 이걸 제한하고 줄여버렸다).


