AI 리포트 수치 검증기 - 자기정합 검사만으론 못 잡는 오류들
문서 안에서는 앞뒤가 맞는데 원본 데이터와는 다른 숫자 — 그런 오류를 잡으려고 검증기를 새로 설계했습니다
자동매매 시스템은 매일 종목마다 AI가 작성한 리포트를 만들어냅니다. 사람이 그걸 하나하나 다 읽고 숫자를 확인할 수는 없습니다.
리포트 안에는 가격, 재무 수치 같은 구체적인 숫자가 많이 들어갑니다. 그 숫자가 틀리면 판단 근거 자체가 틀린 셈이라, 오래전부터 검사 로직을 붙여두긴 했습니다.
그런데 이번에 그 검사 로직을 다시 들여다볼 일이 생겼습니다. "검사하고 있다"는 게 정말 "확인해야 할 걸 확인하고 있다"는 뜻인지 스스로 의심이 들어서였습니다.
기존 검사가 실제로 하던 일
기존에 있던 검사는 리포트 문서 "안에서" 숫자들이 서로 맞는지를 봤습니다. 이를테면 리포트 앞부분에 쓴 숫자와 뒷부분에 다시 나온 같은 숫자가 일치하는지, 그런 자기정합(self-consistency) 검사였습니다.
이 방식은 리포트가 자기 자신과 모순되는지는 잡아낼 수 있습니다. 하지만 리포트가 처음부터 원본 데이터를 잘못 옮겨 적었는데 그 잘못을 문서 전체에서 일관되게 반복했다면, 이 검사는 아무것도 못 봅니다.
예를 들어 원본 데이터의 숫자를 다른 단위로 환산하면서 자릿수를 하나 잘못 옮겼다고 해봅시다. 리포트 안에서 그 잘못된 숫자를 계속 똑같이 인용하면, 문서 내부적으로는 완벽하게 앞뒤가 맞습니다. 자기정합 검사 입장에서는 "문제 없음"입니다.
그래서 다른 검증기를 새로 설계했다
필요했던 건 리포트 문장을 원본 입력 데이터와 직접 대조하는 검증기였습니다. 문서 내부가 아니라 문서 바깥의 참값과 비교하는, 완전히 다른 종류의 검사입니다.
리포트에 등장하는 숫자는 크게 두 갈래였습니다. 하나는 가격·기술적 지표처럼 캔들 데이터에서 나온 숫자, 다른 하나는 매출·시가총액 같은 재무 수치였습니다. 이 둘은 원본 데이터의 성격이 달라서 검증기도 가격 축과 금액 축, 두 갈래로 나눠서 설계했습니다.
이 구분이 중요했던 이유는 나중에 드러났습니다. 가격 축은 원본(캔들)이 매일 계속 쌓이니 언제든 소급해서 대조할 수 있습니다. 반면 금액 축은 그날 리포트에 실제로 입력된 재무 데이터 블록만 원본으로 남기 때문에, 지나간 날짜는 원본 자체가 없어서 소급 대조가 안 됩니다. 이 비대칭이 나중에 오류 규모를 재는 방식 자체를 바꿔놨습니다.
첫 버전은 너무 많이 울었다
가격 축 검증기의 첫 버전은 만들자마자 오탐이 폭증했습니다. 대상 종목 대부분에서 뭔가 걸렸는데, 들여다보니 실제 오류가 아니라 우연의 일치였습니다.
원인은 캔들 데이터가 워낙 많다는 데 있었습니다. 참값 후보가 수백 개나 되는 상태에서 느슨한 오차 범위로 대조하면, 리포트에 나온 웬만한 숫자는 그 후보들 중 하나와 우연히 비슷하게 맞아떨어집니다.
그래서 조건을 세 겹으로 조였습니다. 정확히 일치하는 값만 인정하고, 대조 범위를 최근 구간으로 좁히고, 같은 유형의 오류가 여러 건 함께 나올 때만 신뢰하는 식이었습니다. 결과적으로 재현율을 낮추는 대신 정밀도를 크게 높인 셈입니다.
이 트레이드오프는 의도적으로 선택한 겁니다. 무인으로 돌아가는 시스템에서 오탐이 계속 쌓이면 사람이 알림 자체를 무시하게 됩니다. 이럴 바엔 드물게 놓치더라도 조용한 검증기가 낫다고 판단했습니다.
배선하자마자 드러난 것들
가격 축과 금액 축 검증기를 실제 운영 파이프라인에 배선하고, 프로덕션 데이터로 첫 기저선을 쟀습니다. 가격 축은 오류가 거의 없었는데, 이건 기존에 알던 관측과 일치했습니다.
문제는 금액 축이었습니다. 하루치 리포트만 봐도 절반이 넘는 종목에서 재무 수치 오류가 나왔습니다. 실제 사례를 하나 확인해보니, 원본 데이터의 숫자를 다른 단위로 환산하는 과정에서 뒷자리를 잘못 잘라내는 바람에 실제보다 값이 크게 부풀려진 채로 리포트에 실려 있었습니다. 문서 안에서는 그 잘못된 숫자를 일관되게 썼기 때문에, 기존 자기정합 검사는 그동안 이걸 계속 통과시켜왔습니다.
배선 과정에서 예상 못 한 버그도 두 개 나왔습니다. 검증기가 돌려주는 결과 자료구조의 모양을 호출하는 쪽이 잘못 가정하고 있었는데, 이게 실제 프로덕션 데이터로 돌려보기 전까지는 드러나지 않았습니다. 하나는 정상적으로 오류를 잡아낸 결과가 "도구 자체의 오류"로 잘못 기록될 뻔했고, 다른 하나는 결과를 저장하는 과정 자체가 조용히 실패해서 그날 밤 산출물이 통째로 사라질 뻔했습니다. 둘 다 소스 코드만 읽어서는 못 찾고, 실제로 돌려봐야만 드러나는 종류의 결함이었습니다.
45일을 되짚어보니
배선 직후의 하루치 결과만으로는 이게 우연인지 일상인지 알 수 없었습니다. 그래서 과거 45일치 리포트를 다시 돌려서 같은 검사를 소급 적용해봤습니다.
가격 축은 원본이 매일 남아 있어서 소급이 쉬웠습니다. 지난 25거래일 전 구간을 다시 돌려도 오류는 거의 나오지 않았고, 이건 프로덕션에서 본 것과 같은 결론이었습니다.
금액 축은 사정이 달랐습니다. 원본 재무 데이터 블록이 하루치만 남아 있어서 정확한 소급 대조가 불가능했습니다. 대신 "물리적으로 있을 수 없는 금액"(국내 증시 전체 시가총액을 넘는 값 같은)을 대리 지표로 써서 오류를 대신 셌습니다.
그 결과, 지난 45일 동안 이 유형의 오류가 하루도 빠짐없이 나왔습니다. 하루하루 비율은 들쭉날쭉했지만 오류가 아예 없는 날은 단 하루도 없었습니다.
이 수치는 하한선이라는 점을 분명히 해야 합니다. 대리 지표는 값을 크게 부풀리는 방향의 오류만 잡을 수 있는 구조라서, 원본과 직접 대조가 가능했던 하루치를 기준으로 비교해보니 대리 지표가 잡아낸 건 실제 오류의 일부에 불과했습니다. 실제 오류 규모는 이보다 훨씬 컸을 가능성이 큽니다.
이 소급 조사를 하는 과정에서 검증기 자체의 또 다른 한계도 하나 발견했습니다. 특정 배율로 단위를 잘못 바꾼 오류 패턴을 이 검증기가 애초에 인식하는 목록에 넣어두지 않았던 겁니다. 검증기를 만드는 과정 자체가, 검증기의 사각지대를 계속 찾아나가는 과정이었던 셈입니다.
기존 검사는 왜 이 클래스를 구조적으로 못 봤나
정리하면 이렇습니다. 기존 자기정합 검사는 "리포트가 스스로와 모순되는가"만 물었습니다. 원본 데이터를 다시 들여다보는 절차 자체가 없었습니다.
이번에 찾은 오류 클래스는 원본 숫자를 리포트로 옮겨 적는 과정에서 단위나 자릿수를 잘못 바꾼 것이었습니다. 그 잘못된 숫자를 리포트 안에서 일관되게 반복하기만 하면, 문서 내부에는 아무 모순도 생기지 않습니다.
그러니까 "검사 로직이 돌고 있다"는 사실과 "이 오류를 검사하고 있다"는 사실은 서로 다른 문장이었습니다. 새 검증기를 만들어서 실제로 돌려보기 전까지는, 이런 오류가 매일 나오고 있다는 사실 자체를 몰랐습니다.
일반화하면
AI가 만들어내는 리포트나 텍스트를 검증할 일이 있다면, 이번 경험에서 몇 가지를 챙길 만합니다.
자기정합 검사와 원본대조 검사는 다른 걸 잰다. 문서가 스스로 모순되지 않는다는 것과 문서가 사실에 부합한다는 것은 별개의 명제입니다. 둘 중 하나만 있으면 절반만 검증하는 셈입니다.
검증기를 만들었다고 끝이 아니다. "이 검증기가 구조적으로 절대 못 보는 오류의 모양이 뭔가"를 스스로 물어야 합니다. 이번에도 검증기를 만드는 와중에 그 검증기 자신의 사각지대를 또 하나 찾아냈습니다.
오탐과 누락의 트레이드오프는 명시적으로 정해야 한다. 특히 사람이 매번 확인하지 못하는 무인 운영 환경이라면, 오탐이 쌓여서 알림 자체를 무시하게 되는 상황이 더 나쁠 수 있습니다. 어느 쪽을 우선할지 이유와 함께 정해두는 게 낫습니다.
실데이터로 배선해서 돌려보기 전엔 안심할 수 없다. 이번에 나온 인터페이스 관련 버그 두 개는 소스 코드 리뷰만으로는 안 보이고, 실제 프로덕션 데이터를 흘려보내야만 드러나는 종류였습니다.
참값이 일부만 있을 때는 대리 지표를 쓰되, 그게 하한이라는 걸 명시해야 한다. 대리 지표가 실제 규모를 과소평가할 수 있다는 걸 알고 있어야, 그 숫자를 안심의 근거로 잘못 쓰지 않습니다.
검증기 하나를 새로 붙일 때마다, "이 검증기가 지금 뭘 못 보고 있을까"를 다음 점검 목록에 올려두려고 합니다.