
사기 매물은 들어오고, 신뢰할 수 있는 가격은 나간다: AI 가격 책정 엔진 뒤에 숨겨진 가드레일 스택 (Guardrail Stack)
요약
데이터셋이 부족한 시장에서 LLM을 활용해 실시간 검색 결과로 가격을 책정하는 엔진 구축 방법을 다룹니다. 검색 결과의 노이즈와 부정확한 데이터를 처리하기 위해 모델의 출력을 검증하는 6단계 가드레일 스택의 필요성을 강조합니다.
핵심 포인트
- 데이터셋이 없는 환경에서는 LLM의 실시간 검색 능력이 게임 체인저임
- 정답(Ground Truth)이 실시간 검색일 경우 데이터 품질 문제는 런타임 문제로 전환됨
- 단순 필터링을 넘어 호가와 거래가의 차이 등 구체적인 실패 모드 대응 필요
- 모델의 신뢰성을 확보하기 위한 다층적 가드레일 레이어 구축이 필수적임
누군가 깨끗한 데이터셋에 의존할 수 없는 상황에서 검색 결과의 노이즈(noise)를 어떻게 처리하는지 물었습니다. 여기 그에 대한 전체 답변이 있습니다: 모델을 전혀 신뢰하지 않는 6개의 레이어입니다.
데이터가 없는 시장을 위한 자동차 가격 책정 엔진 구축하기에 대해 글을 쓴 후, @topstar_ai가 댓글로 질문을 남겼는데, 이는 단순한 답변 이상의 가치가 있는 질문이었습니다:
The Next Billion Users Don't Come With Datasets, and That's the Opportunity에 대한 댓글
Luis Cruz Jul 15
LLM(대규모 언어 모델)이 요청 시점에 실시간 검색 결과로부터 가격을 합성할 수 있다는 아이디어에 깊은 인상을 받았습니다. 이는 전통적인 지도 학습 (supervised learning) 접근 방식을 완전히 뒤집는 것입니다. 데이터셋이 더 이상 전제 조건이 아니라 오히려 부산물(exhaust)이 된다는 사실은 데이터가 부족한 시장에서 AI 제품을 구축하는 데 있어 게임 체인저입니다. 저도 데이터 부족을 극복하기 위해 프록시 데이터 소스 (proxy data sources)와 영리한 피처 엔지니어링 (feature engineering)에 의존해야 했던 유사한 프로젝트들을 수행해 왔지만, LLM 접근 방식은 더 확장 가능하고 유연한 솔루션을 제공하는 것으로 보입니다. 검색 결과의 데이터 품질 및 노이즈 문제는 어떻게 처리하시나요? 그리고 모델의 출력이 신뢰할 수 있고 믿을 수 있도록 하기 위해 어떤 종류의 도메인 가드레일 (domain guardrails)을 구현하셨나요?
두 가지 질문이 담겨 있으며, 둘 다 적절한 질문입니다. 만약 당신의 정답 (ground truth)이 큐레이션된 데이터셋이 아니라 실시간 검색 결과라면, 데이터 품질 문제는 사라지지 않습니다. 그것은 이동할 뿐입니다. 그것은 매 요청마다 해결해야 하는 런타임 (runtime) 문제로 변합니다.
AutoValue에서 제가 실제로 수행하는 작업을 레이어별로 설명하겠습니다.
첫째, 노이즈가 실제로 어떻게 생겼는가
“저품질 소스를 필터링하라”는 식의 일반적인 조언은 구체적인 실패 모드(failure modes)를 알기 전까지는 쓸모가 없습니다. 제가 나이지리아 중고차 가격을 검색할 때 발생하는 사례는 다음과 같습니다:
- 거래 가격이 아닌 호가(Asking prices). Jiji에 등록된 모든 가격은 협상의 시작점일 뿐입니다. 나이지리아 구매자들은 관례적으로 호가보다 10~20% 낮은 가격으로 협상합니다. 등록된 숫자를 그대로 보고하는 모델은 시장 가격을 체계적으로 높게 책정하게 됩니다.
- 삭제되지 않은 가치 하락 전의 매물들. 나이라(naira) 환율은 달러당 약 ₦460에서 약 ₦1,650으로 급등했습니다. 2022년의 매물들이 여전히 인덱싱되어 2022년 가격을 보여주고 있습니다. 이 데이터들이 정확히 틀린 것은 아닙니다. 그것은 일종의 고고학적 데이터입니다.
- 트림 오염(Trim contamination). "Toyota Land Cruiser Prado price Nigeria"를 검색하면 대부분 VX 풀옵션 모델이 검색 결과로 나옵니다. 딜러들이 그것을 광고하기 때문입니다. 만약 이를 표준 트림의 기본 가격으로 간주한다면, 모든 Prado의 가격은 영원히 높게 책정될 것입니다.
- 통화 혼합(Currency mixing). 수출 및 경매 사이트는 USD(미국 달러)로 가격을 제시합니다. 나이지리아 매물 사이트는 나이라(naira)로 제시합니다. 이 두 가지가 모두 동일한 결과 집합에 포함됩니다.
- 사기 미끼(Scam bait). 250만 나이라짜리 레인지로버는 데이터 포인트가 아니라 미끼입니다. 하지만 이는 형식이 완벽하게 갖춰진 숫자이기 때문에, 어리숙한 파서(parser)라면 아무 의심 없이 평균값에 포함시켜 버릴 것입니다.
마지막 사례만이 일반적인 의미에서의 "나쁜 데이터"라는 점에 주목하십시오. 나머지는 모두 제 목적에는 맞지 않지만, 데이터 자체는 정확한(correct) 데이터입니다. 이 차이가 전체 설계의 핵심 동력이 됩니다.
레이어 1: 파싱(parsing)뿐만 아니라 검색 자체를 제한하라
가장 저렴한 필터는 노이즈가 아예 검색되지 않도록 차단하는 것입니다.
const query = `${year} ${make} ${model} price Nigeria site:jiji.ng OR site:cars45.com`;
const searchRes = await fetch("https://google.serper.dev/search", {
...
실제로 중요한 두 마켓플레이스로 검색 범위를 site: 스코핑(scoping)하고, gl: "ng"를 통해 검색 위치를 나이지리아로 지정합니다. 마지막 설정은 보기보다 훨씬 중요합니다. 이 설정이 없으면 동일한 차량에 대해 수입 경제 구조가 완전히 다른 시장임에도 불구하고, 달러로 표시된 미국 검색 결과가 나오게 됩니다.
또한 Google의 답변 상자(answer box)와 지식 패널(knowledge panel)이 존재하는 경우, 유기적 검색 결과(organic results)보다 우선순위를 둡니다. Google이 이미 여러 소스에 걸쳐 한 차례의 종합(synthesis) 과정을 거쳤기 때문입니다.
const priorityLines: string[] = [];
if (answerBox?.snippet || answerBox?.answer) {
priorityLines.push(`GOOGLE SUMMARY: ${answerBox.title ?? ""}\n${answerBox.snippet ?? ""}`);
...
레이어 2: 추론(Reasoning)으로부터 추출(Extraction)을 분리하기
이것은 다른 모든 것이 의존하게 될 구조적 결정이며, 제가 사람들이 가장 얻어갔으면 하는 핵심 내용입니다.
여기에는 완전히 다른 두 가지 작업이 있습니다. 하나는 _'이 지저분한 스니펫(snippets)들을 읽고 이 차량이 현재 얼마에 판매되는지 알려달라'_는 것입니다. 다른 하나는 _'기준 가격이 주어졌을 때, 이 특정 차량의 주행 거리, 상태, 트림 및 위치에 맞춰 가격을 조정하라'_는 것입니다. 유혹적인 방법은 이 두 가지를 모두 수행하는 하나의 프롬프트(prompt)를 만드는 것입니다. 하지만 그렇게 하지 마십시오.
제 파이프라인(pipeline)에서는 이를 두 개의 별도 Claude 호출로 분리하며, 그 이유는 감사 가능성(auditability) 때문입니다. 단일 호출 설계에서 가격이 잘못 나왔을 때, 모델이 스니펫을 잘못 읽은 것인지 아니면 조정 규칙을 잘못 적용한 것인지 구분할 수 없습니다. 이를 분리하면 모든 잘못된 출력은 특정 단계로 국한됩니다. 이 변경을 통해 디버깅(debugging) 시간이 그 어떤 프롬프트 개선보다 더 많이 단축되었습니다.
추출(Extraction)은 요청(request)이 아닌 절차(procedure)를 기반으로 Haiku에서 실행됩니다:
판매자가 받기를 희망하는 가격이 아니라, 이 차량이 실제로 판매되는
현실적인 거래 가격 범위(REALISTIC TRANSACTION PRICE RANGE)를 추출하세요.
...
이러한 규칙 하나하나가 모두 시행착오의 흔적(scar)입니다. STEP 2에서 ENTERPRISE를 별도로 분리한 이유는,
출력값은 생존한 클러스터(surviving cluster)의 25백분위수(25th percentile)와 75백분위수(75th percentile)입니다. 평균(mean)도, 최솟값(min)이나 최댓값(max)도 아닙니다. 250만 나이라(₦2.5M)짜리 사기 매물 하나가 평균을 망치고 최솟값을 망칠 수는 있지만, 25백분위수는 거의 움직이지 않습니다. 이상치(outliers)에 대한 강건성(Robustness)은 올바른 통계치를 선택함으로써 공짜로 얻을 수 있는 속성이며, 그 어떤 프롬프트 엔지니어링(prompt engineering)도 이를 대체할 수 없습니다.
데이터가 부족한 경우(narrow-data case)도 명시적으로 처리됩니다. 유효한 가격이 하나 또는 두 개만 남게 되면 백분위수는 의미가 없으므로, 규칙은 단일 관측값 주변의 고정된 스프레드(fixed spread)로 전환됩니다. 데이터가 희소할 때 어떻게 동작해야 하는지 명시하지 않으면, 모델은 무언가를 지어낼 것입니다.
Layer 4: 모델은 보고하게 하고, 결정은 코드가 하게 하라
오염(contamination)을 제거하는 것은 목록 중 가장 미묘한 실패 사례이며, 이에 대한 해결책은 가격 책정을 넘어 광범위하게 적용될 수 있습니다.
저는 추출 모델에게 "매물이 상위 트림(high trim)처럼 보이면 가격을 낮게 조정하라"고 요청할 수도 있습니다. 하지만 이는 숫자 내부에 숨겨진, 조용하고 감사 불가능하며 재현 불가능한 조정을 요구하는 것입니다. 대신 모델은 자신이 본 것을 별도의 필드로 보고(reports) 하고, 산술 연산은 제가 읽을 수 있는 코드 내에서 이루어집니다:
const TRIM_FACTOR: Record<string, number> = {
full_option: 1.12,
limited: 1.10,
...
이제 저장된 모든 앵커(anchor)는 기본 트림(base-trim) 가격이 되며, 트림 프리미엄은 나중에 판매자가 볼 수 있는 가시적인 항목(line item)으로 다시 더해집니다. 또한 저는 프롬프트를 수정하지 않고도 승수(multipliers)를 변경할 수 있습니다.
모델에게는 관찰 내용을 요구하십시오. 계산은 코드에 유지하십시오. 모델의 판단이 숫자를 움직여야 한다면, 출력 가능한 메커니즘을 통해 숫자를 움직이게 만드십시오.
Layer 5: 모델이 응답한 후의 결정론적 불변량 (Deterministic Invariant)
조정 모델에는 명시적인 퍼센트 규칙이 부여됩니다. 예를 들어, 내부 상태가 우수하면 +2%, 4~7년 된 차량의 경우 예상보다 낮은 주행거리는 10,000km당 +0.3%를 가산하는 식입니다.
이것이 항상 스스로의 규칙을 따르는 것은 아닙니다. 실제 사례를 통해 확인해 보면, 한 차량에 대해 시스템이 명시한 규칙으로는 0.3%가 산출되어야 함에도 불구하고 +7.8%의 주행거리 보너스를 적용한 경우가 있었습니다. 이는 극적인 의미에서의 환각 (Hallucination)은 아닙니다. 모델이 근사치 산술 (Approximate arithmetic)을 수행하다가 그럴듯해 보이는 지점에 도달한 것입니다.
따라서 응답이 돌아온 후 코드 상에서 실행되는 클램프 (Clamp)가 존재합니다:
const CONDITION_UPLIFT_KEYS = ["condition", "interior", "engine", "mileage"] as const;
function applyAgeConditionCeiling(estimate, base, carAge) {
...
도메인 추론 (Domain reasoning)은 코드만큼이나 중요합니다. 2년 된 차량에 대한 검색 앵커 (Search anchor)는 이미 '거의 최상급 상태'임을 가정합니다. 왜냐하면 시장에 나온 2년 된 차량들이 실제로 그렇기 때문입니다. 이 앵커 위에 우수한 외관, 우수한 내장, 우수한 엔진, 그리고 낮은 주행거리 보너스를 중첩해서 적용하면, 상태가 깨끗한 중고차 가격이 신차 가격처럼 책정되어 버립니다. 이러한 실패는 데이터가 부족한 모델 (Thin-data models)에서 가장 심각하게 나타났는데, 이 경우 검색 결과가 신차 딜러 가격뿐이라 앵커 자체가 이미 상한선(Ceiling)인 상황이기 때문입니다.
트림 (Trim) 및 지역 가산점은 상한선 적용에서 의도적으로 제외됩니다. 이는 상태 기반이 아닌 구조적 요소이기 때문입니다. 트림은 레이어 4에서 나누어 계산되었으며, 정당하게 다시 더해지고 있는 것입니다.
모델이 산술적 불변성 (Arithmetic invariant)을 보장할 것이라고 절대 믿지 마십시오. 특정 관계가 반드시 유지되어야 한다면, 사후에 코드에서 이를 강제하십시오.
레이어 6: 페일 클로즈 (Fail closed)
이 부분은 LLM 제품을 만드는 대부분의 사람들과 논쟁할 만한 지점입니다.
검색 결과가 사용할 만한 것이 없을 때, 엔드포인트는 모델 자체의 지식으로 폴백 (Fallback)하지 않습니다. 대신 503 에러를 반환합니다:
if (!searched) {
return NextResponse.json({
success: false,
...
대부분의 LLM 제품은 페일 오픈 (Fail open) 방식으로 작동합니다. 검색 결과가 비어 있어도 모델은 학습 데이터로부터 어떻게든 답변을 내놓고, 사용자는 근거가 있는 답변 (Grounded answer)과 추측을 구분할 수 없게 됩니다. 이는 성능이 약간 떨어지는 챗봇에게는 괜찮을지 모릅니다. 하지만 제품의 핵심 가치 제안이 "이 숫자는 신뢰할 수 있다"인 제품에게는 치명적입니다. 실패가 가장 해로운 순간에 그 실패가 눈에 보이지 않기 때문입니다.
이전 기사에서 더 심도 있게 다루었듯이, 관련하여 말씀드리자면: 모델이 생성한 가격이 한때 앵커(anchor)로서 캐싱(caching)된 후, 다음 요청 시 마치 시장 데이터인 것처럼 다시 주입되는 문제가 있었습니다. 이에 대한 해결책은 런타임 우회(runtime bypass) 방식에서 타입(type) 수준의 강제화로 더욱 견고해졌습니다. 저장된 앵커의 소스(source)는 오직 manual, serper_search, 또는 google_cse만 가능하며, 읽기 쿼리(read query)는 이 세 가지를 명시적으로 필터링합니다:
.in("source", ["serper_search", "google_cse", "manual"])
이제 모델의 출력값 자체는 시스템이 증거(evidence)로 취급할 수 없는 대상이 되었습니다.
여전히 수정해야 할 부분들
이 포스트들이 보통 생략하는 부분인, 존재하는 허점들에 대해 솔직하게 말씀드리겠습니다.
환율은 USD_TO_NGN = 1650이라는 하드코딩된 상수(constant)로 되어 있습니다. 오래된 통화 가정이 어떻게 내 가격 책정을 망가뜨렸는지에 대해 기사 한 편을 통째로 썼으면서, 정작 제 코드에 오래된 통화 가정을 넣어두었습니다. 이는 실시간 조회(live lookup) 방식으로 바뀌어야 하며, 곧 그렇게 될 것입니다.
백분위 컷오프(percentile cutoffs), 60%의 신선도 임계값(staleness threshold), 트리밍 승수(trim multipliers) 등은 결과 데이터(outcome data)에 맞춰 피팅(fitting)된 것이 아니라, 저의 시장 지식으로부터 보정(calibrated)된 것들입니다. 플랫폼을 통해 충분한 매물이 판매되고 나면, 실제 거래 가격이 제 추측을 대체하는 학습 신호(training signal)가 될 것입니다. 그때가 바로 배기가스(exhaust)가 마침내 데이터셋(dataset)이 되는 시점입니다.
요약
- 특정한 노이즈(noise)를 파악하세요. 제가 겪은 대부분의 나쁜 데이터는 거짓된 데이터가 아니라, 질문에 맞지 않는 올바른 데이터였습니다.
- 추출(extraction)과 추론(reasoning)을 분리하세요. 두 번의 호출, 두 개의 실패 영역(failure domains)을 가지면 디버깅이 가능해집니다.
- 강건한 통계(robust statistics)를 사용하세요. 평균(mean)보다는 백분위수(percentile)를 사용하십시오. 단 하나의 사기 매물이 당신의 답변을 흔들어서는 안 됩니다.
- 모델은 보고하고, 코드는 계산합니다. 숫자를 움직이는 모든 조정 사항은 가시적이어야 하며 버전 관리가 되어야 합니다.
- 모델 이후의 단계에서는 코드에서 불변량(invariants)을 강제하세요. 명시된 규칙이 반드시 보장되는 규칙은 아닙니다.
- 실패 시 폐쇄(Fail closed)하세요. 신뢰가 곧 제품인 경우, 데이터가 없는 것이 확신에 찬 추측보다 더 나은 답변입니다.
저는 autovalue.tech에서 AutoValue를 공개적으로 구축하고 있습니다. 만약 여러분이 소스(source)가 이토록 지저분한 시장에서 검색 기반(retrieval-grounded) 작업을 수행하고 있다면, 여러분은 어떤 레이어를 추가하고 싶은지 듣고 싶습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기