ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 근거가 이 문장을 정말 뒷받침하나 — 로컬 NLI로 매일 $0 사실 검증
    IT 2026. 7. 8. 21:00
    근거가 이 문장을 정말 뒷받침하나 — 로컬 NLI로 매일 $0 사실 검증

    1. 배경 — "그럴듯함"과 "사실임"은 다르다

    요약문이나 설명문을 기계가 자동으로 만들어내는 시대다. 직접 자동 생성 파이프라인을 만들면서 가장 먼저 부딪힌 벽이 있었다. 만들어진 문장이 읽기에 자연스러운 것실제로 근거에 부합하는 것이 완전히 다른 문제라는 사실이다.

    예를 들어 코드에는 토큰을 단순히 문자열로 비교하는 함수가 있는데, 자동 생성된 설명이 "이 함수는 토큰의 서명을 검증한다"라고 썼다고 하자. 문장 자체는 매끄럽고 문법도 완벽하다. 빠르게 훑으면 그냥 넘어간다. 하지만 이건 명백히 틀린 설명이다. 근거(코드)와 다른 일을 한다고 말하고 있기 때문이다.

    이런 류의 오류를 사실 불일치(factual inconsistency) 또는 흔히 환각(hallucination, 환각)이라고 부른다. 여기서 환각이란, 모델이 근거에 없는 내용을 그럴듯하게 지어내는 현상을 말한다. 내게 필요했던 것은 "이 문장이 자연스러운가"를 보는 채점기가 아니라, "이 문장이 주어진 근거에 의해 실제로 뒷받침되는가"를 판정하는 채점기였다.

    그 판정을 기계가 하도록 만드는 핵심 도구가 바로 자연어 추론(Natural Language Inference, NLI)이다. 풀어 쓰면, 두 문장을 놓고 한쪽이 다른 쪽을 논리적으로 뒷받침하는지 판단하는 작업이다.

    diagram

    다이어그램 설명. 이 그림은 글 전체가 풀려는 질문을 한눈에 보여준다. 같은 코드 근거를 두고도 "문자열로 비교한다"는 설명은 근거와 일치해 뒷받침되고, "서명을 검증한다"는 설명은 근거가 하지 않는 일을 말해 차단된다. 핵심은 문장이 매끄러운지를 묻는 게 아니라, 근거가 문장을 뒷받침하느냐는 관계를 기계가 자동으로 판정한다는 점이다. 함정은, 사람이 매끄러운 문장을 보면 사실 여부를 확인하지 않고 넘어간다는 데 있다.

    2. 문제 — 함의·모순·근거없음, 그리고 단위의 함정

    NLI란 무엇인가

    NLI(자연어 추론)는 두 문장의 관계를 판정하는 과제다. 한 문장을 전제(premise), 다른 문장을 가설(hypothesis)이라 부른다. 전제는 우리가 사실로 받아들이는 근거이고, 가설은 그 근거에 비추어 참인지 따져볼 대상이다. 코드 설명 검증으로 옮기면 전제는 인용된 코드 조각, 가설은 그 옆에 적힌 설명 문장이다. 판정은 셋 중 하나로 떨어진다.

    • 함의(entailed): 근거가 설명을 뒷받침한다. 설명의 모든 단언이 코드에 직접 드러나 있다.
    • 모순(contradicted): 근거가 설명과 충돌한다. 코드가 설명과 양립할 수 없는 일을 한다.
    • 근거없음(unsupported): 근거만으로는 설명의 참·거짓을 알 수 없다. 코드에 해당 정보가 없다.

    diagram

    다이어그램 설명. 이 그림은 하나의 코드 근거에서 세 판정이 어떻게 갈리는지 보여준다. "빈 입력에 예외를 던진다"는 코드가 실제로 하는 일이라 함의다. "0을 반환한다"는 같은 상황에서 코드가 하는 일과 정반대라 모순이다. "데이터베이스에 저장한다"는 이 코드 어디에도 근거가 없어 근거없음이다. 가장 중요한 함정은 근거없음과 모순을 헷갈리는 것이다. "근거가 없다"와 "근거에 반한다"를 구분하는 것이 NLI의 힘인데, 둘을 뭉뚱그리면 고치는 방법이 전혀 달라지므로 검증의 의미가 사라진다.

    배경이 된 사실 검증 데이터셋

    이 NLI 능력을 "사실 검증"이라는 실전 과제로 끌어온 대표 작업들이 있다. 배경 삼아 짧게만 짚는다. FEVER(Thorne et al., NAACL 2018)는 수십만 개의 주장을 위키백과 근거에 비추어 지지·반박·정보부족 셋으로 분류한다. NLI의 함의·모순·근거없음과 정확히 대응되는 구조다. SciFact(Wadden et al., EMNLP 2020)는 과학적 주장을 논문 초록을 근거로 검증한다. 둘 다 "주장 하나를 근거에 비추어 셋 중 하나로 판정한다"는 골격을 공유한다 — 내가 코드 설명에 적용한 바로 그 골격이다.

    함정 — 단위가 어긋나면 검증이 무너진다

    여기서 실전의 함정이 등장한다. 검증을 순진하게 하면 문서 전체(또는 코드 파일 전체)를 전제로, 거기서 뽑은 한 단락을 가설로 통째로 밀어 넣게 된다. 이렇게 하면 판정이 엉망이 된다. 긴 근거 어딘가에 답이 있어도, 정작 어느 부분이 어느 설명을 뒷받침하는지 정렬되지 않은 채 뭉뚱그려지기 때문이다.

    해법은 단위를 맞추는 것이다. 설명을 한 주장(claim)으로 쪼개고, 그 주장이 인용한 코드 구간(span) 하나를 짝지어, 주장×근거 쌍으로 판정한다. 자물쇠(판정기)는 열쇠 하나(주장 하나)에 맞게 깎여 있는데, 열쇠 꾸러미 전체(문서·파일 전체)를 욱여넣으려 했던 게 문제였다. 꾸러미를 풀어 열쇠를 하나씩 꽂으면 된다.

    3. 해결 — 주장×근거 쌍을 3분류로 판정

    내가 만들고 있는 코드 위키 자동 생성 시스템은 이 단위 맞춤을 그대로 구현했다. 앞 단계(결정적 문장 분해)에서 뽑은 주장마다, 그 주장이 가리키는 코드 앵커를 해석해 둘러싼 함수·클래스 전체를 근거 구간으로 잘라낸다. 그리고 (주장, 근거 구간) 쌍을 모델에게 보내 함의·모순·근거없음 셋 중 하나로 판정시킨다. 합성곱으로 점수 분포를 처리하거나 지지 구간을 따로 추출하는 복잡한 장치는 두지 않았다. 코드 설명 검증에서는 "이 주장에 대응하는 함수 한 덩어리를 보고 셋 중 하나로 답하라"가 단순하면서 충분했다.

    diagram

    다이어그램 설명. 이 그림은 설명 한 문단이 검증되는 전체 흐름이다. 핵심은 검증 단위를 "문단×파일"이 아니라 "주장 하나×그 주장이 인용한 함수 하나"로 좁혔다는 점이다. 그래서 판정기는 매번 짧고 명확한 한 쌍만 보고 답하면 된다. 함정은 근거 구간을 너무 좁게(인용한 한 줄만) 잘라내는 것인데, 그러면 함수의 동작을 설명하는 주장을 판정할 근거가 모자란다. 그래서 인용한 줄이 속한 함수 전체를 통째로 근거로 준다.

    4. 핵심 — 판정을 로컬 모델로 옮겨 매일 $0

    여기까지는 "어떻게 판정하느냐"였다. 진짜 어려운 문제는 "누가 판정하느냐"였다. 외부 유료 모델로 판정하면 정확하지만, 문서가 수백 페이지에 주장이 수천 개라 매일 돌리면 비용이 감당 안 된다. 그래서 처음엔 야간 자동 검증에서 유료 판정을 아예 생략했는데, 그러면 모순과 근거없음이 매일 무료로는 전혀 탐지되지 않는 빈틈이 생겼다.

    해법은 판정 자체를 이미 GPU에 띄워 둔 로컬 모델로 옮기는 것이었다. 같은 3분류 프롬프트를, 외부 API 대신 로컬 모델에 보낸다. 판정 출력은 라벨·신뢰도·짧은 근거를 담은 작은 JSON 몇십 토큰뿐이라, 긴 텍스트를 생성하는 작업과 달리 빠르게 끝난다. 외부 호출이 0이므로 야간 검증 비용이 진짜로 0이고, "예상 못 한 청구"가 구조적으로 불가능해진다.

    그렇게 만들어진 게 3계층 검증이다. 싼 단계로 대부분을 거르고, 비싼 단계는 정말 필요할 때만 쓴다.

    diagram

    다이어그램 설명. 이 그림은 비용과 정확도를 동시에 잡는 3계층 검증이다. 가장 싼 어휘 정합 단계는 "지어낸 포트·심볼·숫자"를 LLM 없이 잡아내고, 그다음 로컬 모델이 같은 3분류로 의미까지 따진다 — 여기까지가 모두 비용 0이다. 로컬 모델이 모순을 의심하거나 자신 없어 할 때만, 마지막 유료 모델이 정밀 재판정에 투입된다. 핵심은 대부분의 주장이 무료 두 계층에서 결판나고, 돈은 정말 따져볼 가치가 있는 소수의 꼬리에만 쓴다는 점이다. 함정은 로컬 모델이 유료 모델보다 판정 품질이 낮을 수 있다는 것인데, 그래서 모순 의심 건을 위로 올려보내는 에스컬레이션이 안전망 역할을 한다.

    구현상 영리한 점은, 판정기를 부르는 인터페이스를 "모델 이름과 프롬프트를 받아 결과를 돌려준다"는 한 가지 모양으로 통일했다는 것이다. 그래서 로컬 모델이든 유료 모델이든 같은 자리에 끼워 넣을 수 있고, 어느 계층에서 어느 모델을 쓸지는 플래그 한 줄로 바뀐다.

    5. 효과와 한계

    효과는 분명하다. 매일 밤, 생성된 모든 설명의 모든 주장을 코드 근거와 대조하는 일이 비용 0으로 돌아간다. 예전엔 "아직 판정 안 함" 상태로 비어 있던 자리가 실제 함의·모순·근거없음 수치로 채워진다. 어느 페이지 어느 줄의 어떤 주장이 코드와 어긋났는지 매일 추적된다.

    한계도 분명하다. 로컬 판정기는 유료 모델보다 판정이 무딜 수 있다 — 그래서 에스컬레이션이 받쳐 준다. 부정 표현, 수치 비교, 미묘한 시점 차이 같은 어려운 모순은 어떤 NLI 판정기든 자주 놓친다. 그리고 근거가 여러 함수에 흩어져 종합해야 하는 주장에는 약하다. 근거 구간을 함수 단위로 자르기 때문이다. 그럼에도 "주장과 근거의 단위를 맞춰 한 쌍씩 비교한다"는 발상은 자동 사실 검증의 가장 단단한 토대로 남는다.

    6. 내 코드 위키 자동 생성 시스템과의 연결

    코드를 근거로 설명 문서를 자동 생성하는 시스템에서는, 생성된 한 문장 한 문장이 실제 코드에 의해 뒷받침되는지가 신뢰의 전부다. 매끄럽게 읽히지만 코드와 어긋난 설명은, 차라리 없느니만 못한 잘못된 안내가 된다.

    그래서 내 시스템의 사실 검증은 이 글이 정리한 그대로 돌아간다. 설명을 주장으로 쪼개고(앞 글에서 다룬 결정적 분해), 각 주장을 인용한 코드 함수 구간과 짝지어, 함의·모순·근거없음으로 판정한다. 그리고 그 판정을 어휘 정합($0) → 로컬 NLI($0) → 유료 에스컬레이션이라는 3계층으로 흘려보내, 매일 모든 주장을 코드와 대조하면서도 비용은 정말 의심스러운 소수에만 쓴다. 직접 자동 생성 문서를 다뤄 보니, 결국 신뢰는 "예쁜 문장"이 아니라 "코드 근거에 한 줄씩 매여 있고, 그게 매일 검증되는 것"에서 나온다는 점을 거듭 확인하게 됐다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.