85,000점 vs 90,000점? 제 에이전트는 추측하지 않습니다
요약
이 글은 마일리지 항공권 검색의 복잡성을 해결하는 'Safari'라는 에이전트를 소개합니다. Safari는 여러 출처에서 상충되는 정보를 발견하면 추측하지 않고 작동을 멈추며, 오직 구조화된 그래프 탐색과 결정론적 솔버를 통해 가장 저렴하고 유효한 경로만을 제시합니다.
핵심 포인트
- 마일리지 항공권 검색의 복잡성(다중 출처/모순 정보) 해결에 초점.
- Safari는 LLM 추측 없이, 제한된 GROQ 쿼리와 그래프 탐색을 사용함.
- 정보 모순 발견 시 작동을 중단하고 사용자에게 경고하는 신뢰성을 갖춤.
- 결정론적 솔버를 통해 최저가 유효 경로와 최소성 증명을 제공함.
이 글은 Sanity Challenge, Path One: 실제 콘텐츠를 질의하는 에이전트 배포하기에 대한 제출물입니다.
내가 구축한 기능
대부분의 마일리지 항공권 검색 도구에 "내 포인트로 SFO에서 도쿄까지 비즈니스 클래스로 가장 저렴하게 가는 방법은 무엇인가요?"라고 물어보면 자신감 있는 숫자를 얻을 수 있습니다. 하지만 그 숫자가 어디서 왔는지 묻기 시작하면 이야기가 모호해집니다. 마일리지 항공권 분야는 서로 의견이 다른 출처들로 구축되어 있습니다. 인쇄된 마일리지 차트에서는 한 가격을 제시하고, 지난주에 발표된 가치 하락 공지(devaluation notice)에서는 또 다른 가격을 제시하며, 트랜스퍼 파트너 페이지에서는 세 번째 내용을 말합니다. 잘못된 정보를 선택하면 95,000 포인트를 원했던 프로그램에 90,000 포인트를 투입하게 될 수 있습니다.
Safari는 신중한 사람이 질문에 답하는 방식대로 작동하는 에이전트입니다:
- 후보 경로를 구성합니다. Sanity 내에서 포인트 통화 → 트랜스퍼 파트너 → 로열티 프로그램 → 마일리지 차트 항목 순으로 유형화된 그래프(typed graph)를 탐색하여 후보 경로를 만듭니다. 자유 텍스트 추측 없이, 하나의 제한된 GROQ 쿼리만 사용합니다.
- 출처가 불일치하면 작동을 멈춥니다. SFO→NRT 비즈니스 클래스에 대한 ANA 차트 항목은 모순을 안고 있습니다: 인쇄된 차트는 85,000 포인트를 말하지만, 가치 하락 공지에서는 2026-09-25부로 90,000 포인트라고 합니다. 이 모순이 해결될 때까지 Safari는 어떤 가격도 제시하지 않습니다. "주의 사항을 붙여서 가격을 매기는" 것이 아닙니다.
NOT_COMPUTED를 반환합니다. - 권위 있는 출처와 비교하여 충돌을 해결하고, 그 출처를 인용하며, 결정을 Sanity에 다시 기록하여 다음 실행에서 이 결정이 유지되도록 합니다.
- 답변을 증명합니다. 결정론적(deterministic) TypeScript 솔버가 단일 최저가 유효 경로와 최소성 증명(minimality proof)을 반환합니다: 즉, 다른 모든 유효 경로는 더 높은 가격으로 책정된다는 것을 순서대로 보여줍니다. 이 모델은 절대로 산술 계산을 수행하지 않습니다.
챌린지 프롬프트에는 문자 그대로 "어떤 트랜스퍼 파트너의 이야기가 현재 유효한지 풀어내는 마일리지 항공권 에이전트"가 명시되어 있었습니다. 저는 그 요구사항을 받아들인 후, 한 단계 더 나아가고자 했습니다. Safari는 단순히 어떤 이야기가 현재 유효한지만 알아내는 것이 아니라, 구조로부터 답변을 구축하고 가장 저렴한 옵션이 존재하지 않음을 증명합니다.
데이터는 합성적이며 모든 곳에서 그렇게 레이블링되었습니다. 실제 어워드 데이터는 스크래핑과 이용 약관의 지뢰밭이며, 데모에는 제가 통제하는 모순이 필요합니다. 여기에는 다섯 개의 프로그램(ANA, Virgin Atlantic, Air Canada Aeroplan, British Airways, Avianca LifeMiles), 세 가지 포인트 통화(Amex Membership Rewards, Chase Ultimate Rewards, Capital One Miles), 비율을 가진 여덟 개의 전환 파트너, 네 개 노선을 아우르는 열네 개의 어워드 차트 항목, 그리고 두 개의 조작된 모순이 포함되어 있습니다.
데모
실시간: https://safari-tan.vercel.app
접근 경로는 두 가지이며, 둘 다 동일한 Sanity 데이터에 대해 동일한 파이프라인을 실행합니다:
- /solver: 모델 기반(model-free) 양식입니다. LLM도 없고 비용도 들지 않으며 항상 작동합니다. 여기서 시작하세요.
- /agent: Amazon Nova Pro가 도구들을 연결하고 서술합니다. 모든 질문에 실제 돈이 들기 때문에 하루 50회, 방문자당 시간당 5회로 제한됩니다. 제한에 도달하면 페이지에 표시되며
/solver를 가리킵니다.
게이트가 작동하는 것을 보려면, 먼저 데모 초기화(Reset demo) 버튼을 누르세요. 데모 상태는 공유되므로, 이전에 다른 사람이 모순을 해결했다면
2. 지식 기반(Knowledge Base)도 독립적으로 동의합니다. 이 패널은 Sanity Context Knowledge Base에서 가져왔으며, Context MCP를 통해 읽어 들였습니다. 이것이 왜 흥미로운지 아래에서 더 자세히 설명하겠습니다.
3. 게이트가 작동합니다. 아직 가격은 없습니다.

4. 해결책, 출처 명시. 감가 상각 통지서(devaluation notice)의 권한이 더 높고 효력 발생일도 더 늦기 때문에 인쇄된 차트를 무효화합니다.

5. 증거. Amex → ANA로 90,000점이 승리합니다. 다른 모든 유효한 경로가 비용이 저렴한 순서대로 나열되어 있습니다.
6. 구조가 가치를 발휘하는 곳. JFK→LHR 비즈니스석의 경우, Capital One 마일만을 보유합니다. Avianca는 52,000 LifeMiles를 원하며, 이는 British Airways의 60,000마일보다 적습니다. 하지만 Capital One은 Avianca로 2:1.5 비율로 전환되므로, 이 52,000 LifeMiles는 본인의 마일 69,334마일에 해당합니다. 따라서 British Airways가 승리합니다. 차트 가격만 검색하면 잘못된 것을 고를 것이고, 답은 전송 파트너(transfer-partner)의 엣지(edge)와 그 비율을 따라가야만 존재합니다.

7. 선행 규칙(precedence rule)은 '가장 최신 정보'를 따르는 것이 아닙니다. JFK→LHR 이코노미석의 경우, 공식 Virgin 차트가 발표된 이후에 발행된 포인트 블로그에서는 20,000점을 제시했습니다. 하지만 차트는 25,000점이라고 명시합니다. Safari는 차트를 따릅니다: 출처의 권위(source authority)가 최신성(recency)보다 우선하기 때문에, 더 새롭지만 신뢰도가 낮은 출처로는 가격을 바꿀 수 없습니다.
에이전트 경로는 모델의 설명과 교차하며 동일한 카드를 렌더링합니다:
코드(Code)
- App (Next.js 16, Vercel AI SDK v7): https://github.com/lewisawe/safari
- Studio (Sanity v6 schema): https://github.com/lewisawe/studio-safari
읽어볼 만한 부분들:
solver/: 순수하고 결정론적인(deterministic) 솔버입니다. 이 솔버는COMPUTED { chosen, proof[], minimalityHolds }또는 가격 필드가 전혀 없는NOT_COMPUTED { reason, message }중 하나를 반환합니다. 이는 사용자의 통화에서 포인트를 최소화합니다 (ceil(points / transferRatio)). 세금은 표시되지만 정보 제공용으로 분류되어 최적화 과정에서는 제외됩니다.lib/groq.ts: 유일한 탐색 쿼리입니다.lib/context.ts및lib/traverse.ts: Context MCP 클라이언트, GROQ 리터럴 바인더, 그리고@sanity/client로의 폴백(fallback) 처리입니다.app/api/chat/route.ts: 에이전트, 그 도구들, 그리고 비용 가드(cost guard)가 포함되어 있습니다.
160개의 Vitest 테스트는 솔버, 해결 규칙, 실패 시 닫히는 경로(fail-closed routes), Context 클라이언트(모의 MCP 서버 대상), 비용 가드 및 오프라인 엔드투엔드 파이프라인을 커버합니다.
실패 시 닫힘(Fail-closed)은 핵심 설계 원칙입니다. 화면에 표시되는 모든 가격은 솔버의 타입화된 출력에서 나옵니다. 이는 네 군데에서 강제됩니다:
- 게이트(The gate). 트래버설은 각 차트 항목의 모순을 임베드합니다.
committedResolution이 없는 모순은 자신의 경로뿐만 아니라 전체 요청에 대한 가격 책정을 막습니다. - 솔버가 오류를 발생시키고, 경로가 저하됩니다. 불변성 위반(숫자가 없는 해결된 주장, 0의 전송 비율, 알 수 없는 출처 권한)이 타입 오류를 발생시킵니다. 모든 호출자는 이를 폴백 가격이 아닌
NOT_COMPUTED로 변환합니다. - UI의 구조적 가격 가드.
/agent에서 가격 셀은 오직runSolver도구 결과로부터 렌더링됩니다. 모델의 텍스트는 서술이며 절대로 가격 슬롯을 채울 수 없습니다. - 모델이 솔버에 정보를 제공할 수 없습니다.
runSolver는 통화, 출처(origin), 목적지(destination), 그리고 객실만 받으며 서버에서 트래버설을 다시 실행합니다. 저는 이 사실을 힘든 경험을 통해 배웠습니다 (아래 참고).
Sanity를 사용한 방법
콘텐츠 모델
Studio repo에는 7가지 문서 유형이 있습니다: source, pointsCurrency, program, transferPartner, awardChartEntry, contradiction (두 개의 인라인 claim 객체와 선택적 인라인 resolution), 그리고 userDecision입니다.
구조가 장식용이 아닌 지지 구조물이 되도록 만든 두 가지 선택은 다음과 같습니다:
- 모순(Contradictions)은 플래그가 아니라 일급 문서입니다. 각각의 모순은 자신이 차단하는 차트 항목을 가리키고, 두 개의 주장을 모두 포함하며, 각 주장마다 자신의
source를 인용합니다. 쿼리는 이들을 라우팅 행에 접어 넣기 때문에, 게이트는 경로를 찾는 것과 동일한 읽기로 결정됩니다. - 출처 권한(Source authority)은 하나의 공유 객체입니다.
AUTHORITY_RANK(가치 하락 공지 > 전송 파트너 > 공식 프로그램 > 애그리게이터)는 솔버와 Studio 스키마 모두가 임포트하는 의존성 없는 파일에 존재합니다. 스키마의 드롭다운 옵션은 이로부터 파생되므로, 두 가지가 어긋날 수 없으며, 알 수 없는 권한이 기본값으로 설정되는 대신 오류를 발생시킵니다.
Sanity Context, 두 모드 모두
저는 production 데이터셋과 이를 기반으로 구축된 지식 베이스(Knowledge Base)라는 두 개의 소스를 연결한 하나의 Context MCP 엔드포인트를 설정했습니다. 앱은 이 두 모드에서 열립니다.
GROQ 모드 (groq_query). 라우팅 순회는 Context MCP의 groq_query 도구를 통해 실행됩니다. 이는 앱이 평소에 @sanity/client를 통해 보낼 고정된 4단계(four-hop) 쿼리와 동일하며, 같은 행을 반환합니다. UI에는 "Context MCP를 통해 읽음 (GROQ 모드)" 배지가 표시되며, Context에 연결할 수 없으면 클라이언트로 폴백하고 그 사실을 알립니다.
지식 기반 모드 (initial_context, knowledge_base_read). 이 부분이 저를 놀라게 한 부분입니다. 저는 지식 기반을 다섯 가지 문서 유형(sources, chart entries, programs, currencies, transfer partners)이 포함된 데이터셋에 연결했습니다. 저는 의도적으로 contradiction 문서를 제외했습니다. 각 소스에는 짧은 산문 발췌문이 있습니다: 인쇄된 ANA 차트는 비즈니스 SFO→NRT가 85,000이라고 말하고; 평가절하 통지서는 2026-09-25부터 90,000이라고 말합니다.
빌드가 스스로 이를 플래그 지정했습니다:
SFO–NRT 비즈니스 클래스 수상 가격 항목은 현재 ANA 마일리지 클럽 가격을 편도 85,000 포인트로 나열하지만, 평가절하 통지서 항목은 이 가격이 2026-09-25부로 90,000 포인트로 인상되어 85,000 포인트 가격을 대체한다고 명시합니다. (충돌, 심각)
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기

