모델은 숫자를 계산하지 않는다
요약
개인 금융 앱의 AI 어드바이저가 실제 데이터를 사용하자 잘못된 숫자를 생성하는 문제를 겪었다. 개발자는 모델에게 지침을 주는 대신, 모든 수치를 SQL에서 계산하고 답변에 대한 검사 기능을 추가하여 신뢰성을 확보했다.
핵심 포인트
- AI가 임의로 숫자를 계산하는 위험성 인지
- 모델에게 '요청'하기보다 코드 레벨에서 '검증'이 필수적
- 모든 숫자 출처를 SQL 쿼리로 통일하고 읽기 전용 도구만 제공
내 재무 앱의 AI는 내가 틀렸다는 것을 알고 있는 것들을 나에게 알려주었다. 실제보다 훨씬 높은 지출액을 말해주었고, 몇 달 동안 발생하지 않았던 이번 달 숫자에 포함된 청구 내역까지 언급했다.
Money Track은 나의 개인 금융 앱이다. 은행 명세서를 동기화하고, 지출을 카테고리별로 분류하며, "이거 살 여유가 있을까?"와 같은 질문을 할 수 있는 AI 어드바이저를 갖추고 있다. 샘플 데이터로는 괜찮아 보였다. 하지만 내 실제 데이터를 사용하자 숫자를 꾸며내기 시작했다.
금융 앱에서 자신감 있게 틀린 숫자는 아무 답변이 없는 것보다 더 위험하다. 사용자들은 그 숫자를 기반으로 행동하기 때문이다.
모델에게 주의하라고 지시하는 것은 효과가 없었다
나의 첫 반응은 늘 그랬듯이, 모델에게 더 명확하게 알려주는 것이었다. 앱에서 알림을 작성하는 부분에는 이미 그러한 지침이 있었다. 이 부분은 오직 앱이 이미 계산한 수치만을 반복하도록, 스스로 숫자를 계산하지 못하도록 지시받았다.
효과가 없었던 점: 실제 데이터를 사용했을 때, 하나의 알림 메시지가 같은 항목에 대해 두 가지 다른 총액을 인용했다. 그 어떤 것도 모델에게 제공된 데이터에는 존재하지 않았다.
지침은 명확했지만 모델은 여전히 이를 어겼다. 프롬프트는 요청이다. 아무리 잘 작성해도, 일부 답변은 이 요청을 무시하며, 어느 것이 그런지 알 방법이 없다.
그것은 내가 문제에 대해 생각하는 방식을 바꾸었다. 목표는 결코 '주의 깊은' 모델 자체가 아니었다. 잘못된 숫자가 조용히 화면에 나타나는 것을 막는 것이 목표였다.
모델에게 지침을 주는 것은 요청이다. 코드에서 검사를 하는 것은 보장이다.
모든 숫자는 SQL에서 온다
그래서 나는 산술 계산 자체를 모델로부터 완전히 분리했다.
- 모든 수치는 앱 화면이 사용하는 것과 동일한 쿼리를 통해 SQL에서 계산된다. 채팅 기능과 대시보드는 하나의 출처를 읽기 때문에 서로 모순될 수 없다.
- 모델은 그 스냅샷과 정보를 조회할 수 있는 읽기 전용 도구(read-only tools)를 받는다. 모델은 설명하고 문장으로 표현할 뿐, 절대로 스스로 합계를 계산하지 않는다.
- 답변이 표시되기 전에, 검사 기능이 답변 내의 모든 합계와 모델에게 제공된 숫자를 비교한다. 존재하지 않는 수치는 걸러진다.
검사는 작습니다. 텍스트 내의 모든 숫자를 찾아 데이터에서 검색하며, 반올림에 대해 1%의 허용 오차를 적용합니다. 100 미만은 건너뛰는데, 카운트와 백분율은 돈의 합계가 아니기 때문입니다.
// worker/lib/ai/grounding.ts
export function numbersAreGrounded(text: string, known: Set<number>): boolean {
// spaces inside a number are thousands separators ("3 354")
...
이와 같은 종류의 검사는 날짜 및 일일 카운트에도 적용됩니다. 데이터베이스가 숫자를 생성하지 않았다면, 사용자에게 표시되지 않습니다.

채팅 답변의 모든 수치는 SQL에서 가져옵니다. 모델은 이를 설명할 뿐입니다.
검사는 잘못된 데이터를 고칠 수 없다
이 검사에도 한계가 있으며, 몇 주 후에 제가 그 한계에 부딪혔습니다.
앱은 영어 수업이 월 약 $31씩 저의 자금 소진 속도를 늦추고 있다고 알려주었습니다. 저는 이미 지난 6월에 해당 구독을 취소했습니다. 이 숫자는 실제였습니다: 데이터는 지난 90일 동안 각 상점의 지출액을 세로 나누어 계산했고, 마지막 청구 건은 6월에 있었습니다. 모델은 프롬프트에서 그 수치에 의존하지 않도록 경고받았지만, 그럼에도 불구하고 그것에 의존했습니다. 검사는 숫자가 실제로 데이터에 존재했기 때문에 반박할 수 없었습니다.
저는 더 많은 경고를 추가하는 것이 아니라, 해당 숫자를 제거함으로써 문제를 해결했습니다. 45일 동안 청구 건이 없는 상점은 이제 월별 수치 대신
- 우선은 단순 규칙(Plain rules)이 작동합니다: 이미 등록된 가맹점, 알려진 구독 서비스, 대부분의 사용자가 어떤 가맹점으로 분류한 내역, 은행의 가맹점 카테고리 코드 등을 활용합니다.
- 규칙으로 처리할 수 없는 행(row)만 모델로 넘어갑니다.
- 최근에는 TypeSafe의 작은 판단 모델인 Jev를 Claude Haiku 앞에 추가했습니다. Jev는 자신이 최소 80% 확신하는 경우에만 해당 행을 자체적으로 분류합니다. 그 외의 모든 것은 Haiku로 넘어갑니다.
이 80% 기준선은 추측이 아니었습니다. 평가(eval) 결과, Jev가 제시한 0.8 이상의 모든 답변은 정확했고, 놓친 모든 경우는 0.75 미만이었습니다. 이는 모델에 도달하는 행의 85%를 커버합니다.
이 순차적 처리 방식(cascade)은 Haiku 단독 사용과 동일하게 약 97%의 카테고리 분류 정확도를 달성하지만, 비용은 약 5분의 1 수준입니다. 또한 더 빠릅니다: 같은 평가 실행에서 Jev는 8초가 걸린 반면 Haiku는 42초가 걸렸습니다.
한 번도 미세 조정하지 않은 케이스에 대한 측정
제가 AI를 위해 구축한 가장 유용한 것은 프롬프트가 아니었습니다. 바로 평가 도구(eval)였습니다: npm run eval은 168개의 원본 은행 설명(raw bank descriptions)을 사용하여 실제 분류 코드를 실행하고 정확도와 비용을 함께 보고합니다. 이전에는 모든 프롬프트 변경 사항이 몇 개의 행을 읽고 인상을 형성하는 방식으로 판단되었습니다.
이는 두 번이나 결실을 보았습니다.
아무 의미 없는 98.9% 점수
저는 이 데이터셋으로 Jev를 미세 조정했고 98.9%의 점수를 얻었습니다. 그런 다음 한 번도 본 적이 없는 36개의 새로운 가맹점을 작성하여 다시 실행했습니다: 72%.
원인은 저에게 있었습니다. Jev의 지침은 제가 수동으로 작성한 카테고리 가이드의 더 짧은 복사본이었는데, 이 과정에서 브랜드 목록이 누락되었습니다. 같은 것의 두 가지 정의가 멀어졌던 것입니다. 이제 두 모델 모두 하나의 가이드를 읽게 되었으며, 그 36개 가맹점은 제가 한 번도 미세 조정하지 않는 테스트 케이스로 남아 있습니다. Haiku는 그중 35개를 정확히 분류하며, 전체 실행 비용은 $0.03에 불과합니다.
사용된 적 없는 캐시
평가 도구는 또한 프롬프트 캐싱이 카테고리 분류에는 전혀 효과적이지 않았음을 보여주었습니다. 캐시된 가이드는 Haiku가 최소로 캐시하는 토큰 수보다 18토큰 부족했습니다. API는 이 상황에서 실패하거나 경고하지 않습니다. 단지 매번 전체 프롬프트에 대해 비용을 청구하며, 작동하지 않는 캐시는 실제로 작동하는 것처럼 보이는 것과 똑같습니다.
저는 가짜 정보가 아닌 실제 지침으로 가이드를 채웠기 때문에 정확도도 높아졌습니다:
| 이전 | 이후 | |
|---|---|---|
| 실행당 청구되는 입력 토큰 수 | 187,532 | 4,498 |
| ... | ||
| A test now fails the build if the guide gets too short again. |
귀하의 제품에 미치는 영향
만약 AI 기능이 돈, 주식, 예약 또는 사람들이 행동하는 기타 것에 대한 답변을 제공한다면:
- 사실은 코드가 계산하게 하고 모델이 그것에 대해 이야기하도록 하세요. 그런 다음 사용자에게 도달하기 전에 모든 답변을 데이터와 비교하세요.
- 모델에 보내는 것도 확인하세요. 입력의 오해를 불러일으키는 숫자는 출력에 대한 어떤 검사도 통과할 수 있습니다.
- 모델이 할 수 없는 작업 규칙만 모델에 보내세요. 더 저렴하고 정확합니다.
- 프롬프트를 튜닝하기 전에 모델이 본 적 없는 테스트 케이스를 작성하세요. 그렇지 않으면 자체 프롬프트를 측정하는 것이며, 네 배나 비용이 드는 캐시(cache)를 알아차리지 못할 것입니다.
사용해 보세요
Money Track 데모는 6개월 분량의 샘플 데이터로 비공개 샌드박스를 생성하며, 이 데이터는 24시간 후에 초기화됩니다. 사례 연구에서는 아키텍처를 더 깊이 다루며, 코드는 오픈 소스입니다:
GitHub logo ITalik-gr / money-track
Cloudflare Workers에서 작동하는 AI 기반 개인 재무 추적기. React + TS. AI가 숫자를 임의로 꾸며내는 것을 방지하는 결정론 우선 파이프라인
Money Track
수치를 임의로 꾸며낼 수 없는 AI 어드바이저를 갖춘 개인 재무 추적기.
우크라이나 은행(Monobank)에 연결하거나 명세서 파일을 읽어 모든 거래를 결정론 우선 파이프라인으로 분류하고, 화면에 표시되는 동일한 표준 숫자로부터 귀하의 돈에 대한 질문에 답변합니다. 스스로 만든 숫자가 아닙니다. AI 답변에 포함된 모든 수치는 렌더링되기 전에 귀하의 데이터와 비교됩니다.
또한 사용자의 원장(ledger)을 Claude에 MCP 서버로 노출합니다. 이 앱은 MCP 리소스 서버이자 자체 OAuth 2.1 인증 서버 역할을 하므로, Claude Desktop, claude.ai 또는 모바일 앱과 연결하는 과정은 URL과 동의 화면으로만 이루어지며, 붙여넣거나 사용자의 디스크에 저장되는 것은 없습니다.
<table><tbody><tr><td width="50%"><a rel="noopener noreferrer" href="https://github.com/ITalik-gr/money-track/docs/screenshots/statistics-1.jpg"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FITalik-gr%2Fmoney-track%2FHEAD%2Fdocs%2Fscreenshots%2Fstatistics-1.jpg" alt="통계 — 개요"></a></td><td width="50%"><a rel="noopener noreferrer" href="https://github.com/ITalik-gr/money-track/docs/screenshots/statistics-2.jpg"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FITalik-gr%2Fmoney-track%2FHEAD%2Fdocs%2Fscreenshots%2Fstatistics-2.jpg" alt="통계 — 추세"></a></td></tr><tr><td><a rel="noopener noreferrer" href="https://github.com/ITalik-gr/money-track/docs/screenshots/chat.jpg"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FITalik-gr%2Fmoney-track%2FHEAD%2Fdocs%2Fscreenshots%2Fchat.jpg" alt="원장 기반 AI 채팅 답변"></a></td><td><a rel="noopener noreferrer" href="https://github.com/ITalik-gr/money-track/docs/screenshots/report.jpg"><img <table width="100%"> <tr> <td style="padding-right: 20px;"> <a rel="noopener noreferrer" href="https://github.com/ITalik-gr/money-track/docs/screenshots/report.jpg"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FITalik-gr%2Fmoney-track%2FHEAD%2Fdocs%2Fscreenshots%2Freport.jpg" alt="생성된 월간 보고서"></a></td> </td> <td style="padding-right: 20px;"> <a rel="noopener noreferrer" href="https://github.com/ITalik-gr/money-track/docs/screenshots/transactions.jpg"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FITalik-gr%2Fmoney-track%2FHEAD%2Fdocs%2Fscreenshots%2Ftransactions.jpg" alt="검색 및 필터가 적용된 거래 내역"></a></td> </td> <td> <a rel="noopener noreferrer" href="https://github.com/ITalik-gr/money-track/docs/screenshots/subscriptions.jpg"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FITalik-gr%2Fmoney-track%2FHEAD%2Fdocs%2Fscreenshots%2Fsubscriptions.jpg" alt="구독 및 정기 결제"></a></td> </tr> </table>직접 사용해 보세요:
/demo— 약 6개월 분량의 현실적인 데이터로 시드된 비공개 임시 샌드박스를 구동합니다. 가입할 필요가 없으며, 24시간 후에 초기화됩니다.
스크린샷은 다음에서 가져왔습니다…
당신의 모델도 숫자를 지어내나요? 댓글이나 [Telegram] 또는 X에 [@italikdev]에서 무엇을 만들고 있는지 알려주세요.
원래 [italik.dev]에 게시되었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기