AI 우선 은행 명세서 변환기를 만들었습니다. 모델이 거짓말하는 것을 막아준 단 하나의 규칙
요약
LLM을 활용한 은행 명세서 변환기 개발 과정에서 발생하는 환각(Hallucination) 문제를 해결하는 방법을 다룹니다. 모델의 완벽함에 의존하는 대신, 기초 잔액과 기말 잔액의 일치 여부를 확인하는 검증 단계를 통해 데이터의 정확성을 확보하는 전략을 제시합니다.
핵심 포인트
- LLM은 구조화된 JSON 출력에는 능숙하지만 미세한 수치 오류를 범할 수 있음
- 은행 명세서의 특성(기초/기말 잔액 일치)을 활용한 자체 검증 로직이 필수적임
- 모델의 완벽함보다 독립적으로 검증 가능한 결과물을 만드는 설계가 중요함
StatementDecoder를 만들기 시작했을 때, 저는 은행 명세서를 읽는 것이 가장 어려운 부분일 것이라고 생각했습니다.
하지만 그렇지 않았습니다.
명세서를 읽는 것은 비교적 쉬운 일이라는 것이 밝혀졌습니다.
진짜 어려운 부분은 출력이 틀렸을 때를 아는 것이었습니다.
LLM (Large Language Model)을 활용해 서비스를 만드는 대부분의 사람들처럼, 저의 첫 번째 버전도 단순했습니다. 모델에 명세서를 입력하고 구조화된 JSON (JavaScript Object Notation)을 요청하는 방식이었습니다. 한동안은 거의 마법처럼 느껴졌습니다. 제가 파서 (Parser)를 작성해 본 적 없는 테이블 (Table)들도 처리해 냈습니다. 다양한 레이아웃 (Layout)에도 잘 대응했습니다. 심지어 스캔된 PDF (Portable Document Format) 파일도 꽤 괜찮게 처리했습니다.
그러다 제가 사용해 왔던 몇 안 되는 샘플 명세서로 테스트하는 것을 멈추고, 실제 명세서들을 수집하기 시작했습니다.
모든 것이 무너졌습니다.
영국의 한 은행에서 온 PDF는 다른 은행의 것과 전혀 다르게 생겼습니다. Wise 명세서에는 이전에 본 적 없는 특이한 점들이 있었습니다. Revolut의 CSV (Comma-Separated Values) 파일은 제가 예상했던 것만큼 단순하지 않았습니다. 게다가 90년대 후반 이후로 한 번도 손대지 않은 것 같은 OFX (Open Financial Exchange) 파일들도 있었습니다.
모델은 여전히 잘 작동했습니다.
단지 완벽하지 않았을 뿐입니다.
그리고 그것이 바로 문제입니다.
수백 개의 거래 내역이 담긴 14페이지 분량의 명세서를 상상해 보세요. 모델이 단 하나의 행을 제외하고 모든 행을 정확하게 처리했습니다. 아마 금액에서 숫자 두 개를 서로 바꿨을 수도 있습니다. 혹은 거래 내역이 두 페이지에 걸쳐 나타나서 날짜를 잘못 추측했을 수도 있습니다. 어쩌면 두 개의 행을 조용히 하나로 합쳐버렸을지도 모릅니다.
JSON 결과물은 여전히 완전히 타당해 보입니다.
은행 명세서(Bank statements)는 자체적인 일관성 검사(consistency checks) 기능을 포함하고 있기 때문에 실제로 매우 유용합니다.
기초 잔액(opening balance), 모든 거래 내역(transaction), 그리고 기말 잔액(closing balance)은 모두 서로 일치해야 합니다.
누적 잔액(Running balances) 또한 일치해야 합니다.
이는 LLM, OCR, CSV 가져오기(import) 또는 OFX 파서(parser)를 통해 추출된 데이터인지에 관계없이, 모든 추출 데이터가 정확히 동일한 검증 단계(validation step)를 거칠 수 있음을 의미합니다.
모든 수치가 일치하면 좋습니다.
만약 일치하지 않는다면, 사용자가 데이터를 내보내기(export) 전에 무언가 잘못되었다는 것을 알 수 있습니다.
때로는 무엇이 일어났는지 명확할 때가 있습니다. 페이지가 누락되었거나, 차변(debit)이 대변(credit)이 되었거나, 행이 중복되었을 수도 있습니다. 때로는 검증기(validator)가 문제를 자동으로 수정할 수도 있지만, 그렇지 못할 때도 있습니다.
어느 쪽이든, 시스템은 모든 것이 괜찮은 척하지 않습니다.
이 경험은 제가 AI를 활용해 무언가를 구축하는 방식에 대한 생각을 바꾸어 놓았습니다.
모델이 완벽할 필요는 없습니다.
그저 독립적으로 검증 가능한 결과물을 만들어내기만 하면 됩니다.
우리를 그 단계로 이끈 몇몇 버그들은 놀라울 정도로 평범했습니다.
Wise 명세서의 경우, 설명(description) 컬럼이 완벽하게 추출된 것처럼 보였습니다. 모든 행에 텍스트가 포함되어 있었기 때문입니다. 불행히도, 그 텍스트는 대부분 내부 거래 ID(internal transaction IDs)였습니다. 가맹점 이름(merchant names)은 페이지의 다른 곳에 있었습니다.
Revolut은 다른 이유로 저를 잡아냈습니다. 그들의 CSV 내보내기 파일에는 CARD_PAYMENT나 TOPUP 같은 값을 가진 Type 컬럼이 포함되어 있습니다. 저는 처음에 그것이 거래 방향(transaction direction)이라고 가정했습니다. 하지만 그렇지 않았습니다. 해당 필드에서 차변(debit)과 대변(credit)을 추론하려는 시도를 멈추고 대신 부호가 있는 금액(signed amounts)을 신뢰하게 되자, 가져오기(import) 과정이 훨씬 더 안정적으로 변했습니다.
영국 은행 명세서는 또 다른 예외 사례(edge case)를 가져왔습니다. 꽤 많은 명세서들이 음수(negative numbers)를 전혀 사용하지 않습니다. 대신 입금(Money In)과 출금(Money Out) 컬럼이 별도로 존재합니다. 한 파서(parser)는 대부분 비어 있는 입금(Money In) 컬럼이 테이블의 일부가 아니라고 판단하여, 모든 거래 방향을 조용히 삭제해 버렸습니다.
그다음에는 SGML 내부에 CDATA를 감싸고 있는 호주의 OFX 내보내기(export) 파일이 있었습니다. OFX 1.02는 이미 제대로 된 XML이 아니었기에, 전처리(preprocessing)를 마칠 때쯤에는 제가 또 다른 AI 패스(pass)가 필요할 것이라고 예상했던 부분을 완전히 제거해 주는 두 개의 작은 함수를 작성하게 되었습니다.
그것은 아마도 이 프로젝트에서 가장 큰 교훈을 주었을 것입니다.
어렵게 보이는 모든 문제에 AI가 필요한 것은 아닙니다.
때로는 단지 20줄의 코드만 있으면 됩니다.
스캔된 PDF의 경우에도 똑같은 일이 일어났습니다.
원래 우리는 모든 PDF를 동일한 파이프라인(pipeline)을 통해 보냈습니다. 만약 문서에 텍스트 레이어(text layer)가 없다면, 우리는 모델에게 존재하지도 않는 텍스트를 추출해 달라고 요청하며 토큰(tokens)을 낭비했고, 결국 실패에 이르렀습니다.
이제 우리는 가장 먼저 텍스트가 존재하는지 여부를 확인합니다.
만약 텍스트가 없다면, 문서를 즉시 비전(vision) 모델로 라우팅(route)합니다.
이것이 더 빠르고, 저렴하며, 더 나은 결과를 만들어냅니다.
돌이켜보면, StatementDecoder의 흥미로운 점은 AI를 사용한다는 것이 아니라고 생각합니다.
많은 제품이 AI를 사용합니다.
흥미로운 점은 AI를 신뢰하지 않을 지점을 결정하는 것입니다.
모델은 지저분한 문서를 이해하는 책임을 집니다.
결정론적 코드(Deterministic code)는 구조화된 형식을 파싱(parsing)하는 책임을 집니다.
검증기(validator)는 결과가 믿을만한지 결정하는 책임을 집니다.
이것들은 세 가지 별개의 작업입니다.
이 작업들을 분리해 둠으로써, 모델이 모든 것을 수행하도록 시도하는 것보다 시스템을 훨씬 더 안정적으로 만들 수 있었습니다.
은행 명세서뿐만 아니라 어떤 종류의 추출 파이프라인(extraction pipeline)을 구축하더라도, 이것이 아마도 제가 훔쳐오고 싶은 아이디어일 것입니다.
모델에게 그것이 맞다는 것을 증명하라고 요구하지 마세요.
모델이 따라야 할 독립적인 무언가를 제공하세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기