LLM을 믿기 전에, 의심부터 했습니다

LLM을 믿기 전에, 의심부터 했습니다

들어가며

프로젝트를 수행하면서 사람이 작성한 업무 문서를 후속 프로세스에서 사용할 수 있는 구조화 데이터로 정리해야 했습니다. 문서에는 업무 목적, 담당자, 처리 절차, 규칙과 예외가 자연어로 작성되어 있었습니다. 사람은 문맥을 읽고 이해할 수 있지만, 프로그램이 검색·분류·검토 같은 후속 작업에 사용하려면 일정한 필드와 형식으로 변환해야 합니다.

처음에는 문서와 변환 지침을 LLM에 전달하고 JSON으로 정리하도록 요청하면 충분할 것으로 생각했습니다. 실제로 LLM은 짧은 시간 안에 보기 좋은 결과를 만들었습니다. 그러나 원문에 없는 절차가 자연스럽게 추가되거나, 중요한 문장 하나가 결과에서 조용히 빠지는 경우가 있었습니다. 담당자가 수행하는 행동과 처리 결과가 서로 뒤섞이기도 했습니다.

예를 들어 원문에는 “검토자가 요청을 승인하거나 보완을 요청할 수 있습니다”라고만 작성되어 있는데, LLM이 일반적인 업무 관행을 근거로 “팀장이 최종 승인합니다”라는 규칙을 추가할 수 있습니다. 문장은 자연스럽지만 원문에는 없는 내용입니다. JSON 형식이 올바르다는 이유만으로 이 결과를 사용하면 모델의 추측이 업무상의 사실로 바뀌게 됩니다.

따라서 목표를 “LLM이 완성된 데이터를 만들게 하는 것”에서 LLM은 구조화 후보를 만들고, 시스템은 구조와 원문 근거를 검사하며, 사람이 최종 사용 여부를 결정하는 것으로 다시 정의했습니다.

이 글에서는 정해진 JSON 형식으로 응답받는 Structured Output, 출력 계약을 정의하는 JSON Schema, 원문 근거인 Evidence, 그리고 검증 오류를 이용한 제한적 Repair 과정을 소개합니다. 핵심은 확률적으로 생성된 결과를 어떻게 검증 가능한 데이터로 다루었는가에 있습니다.

먼저 살펴보는 최종 구조

image1.png

그림 1. 비정형 업무 문서에 Prompt와 JSON Schema를 함께 제공하고 결정적으로 검증하는 흐름

먼저 최종적으로 정리한 구조를 살펴보면 위와 같습니다. 원문과 Prompt, JSON Schema를 LLM에 함께 전달해 구조화 후보를 만들고, 시스템이 구조·입력 커버리지·원문 근거·누락 정보를 검사합니다. 여기서 결정적 검증은 같은 입력에 규칙을 적용하면 항상 같은 결과가 나오는 검사를 의미합니다. 검증에 실패하면 오류를 포함해 제한적으로 교정하고, 다시 실패하면 자동 반영하지 않고 수동 검토 대상으로 전환합니다.

이하에서는 처음 시도한 방식에서 어떤 문제가 발생했고, 이를 해결하기 위해 각 검증 단계를 어떻게 추가했는지 순서대로 설명하겠습니다.

처음 생각한 구조: 프롬프트와 JSON Schema면 충분하지 않을까

초기 입력은 사람이 작성한 업무 문서였습니다. 변환 과정을 쉽게 설명하기 위해 이 글에서는 “작업 요청 검토 절차”라는 간단한 예시를 사용하겠습니다. 아래 데이터 필드도 문서와 구조화 결과의 관계를 보여주기 위해 구성했습니다.

변환하려는 샘플 업무 문서

작업 요청 검토 절차
 
배경
현재 작업 요청을 메일과 메신저로 전달하고 있어 요청 상태와 처리 이력을
일관되게 확인하기 어렵습니다.
 
목표
요청자는 작업 내용을 등록하고 검토 결과를 확인할 수 있어야 합니다.
검토자는 검토가 요청된 작업을 승인하거나 보완을 요청할 수 있어야 합니다.
 
담당자
- 요청자는 작업 내용을 작성하고 검토를 요청합니다.
- 검토자는 검토가 요청된 작업 내용을 확인합니다.
 
처리 절차
1. 요청자가 작업 내용을 작성합니다.
2. 요청자가 검토를 요청합니다.
3. 검토자는 요청을 승인하거나 보완을 요청합니다.
4. 검토자는 처리 결과와 검토 의견을 기록합니다.
 
업무 규칙
- 검토를 요청한 작업만 검토할 수 있습니다.
- 검토 결과에는 처리자와 처리 시각이 포함되어야 합니다.

이 문서에서 얻고자 한 결과

제가 원한 결과는 문서를 짧게 요약한 글이 아니었습니다. 문서에 적힌 담당자와 처리 절차를 서로 연결하고, 프로그램이 검색하고 검토할 수 있는 구조화 후보를 만드는 것이었습니다.

샘플 문서의 내용

원하는 구조화 결과

배경과 목표

문서가 해결하려는 목적

(documentPurpose)

요청자와 검토자

식별자를 가진 참여자 목록

(participants)

작성·검토 요청·승인·보완 요청·결과 기록

담당자와 순서를 가진 처리 단계

(processSteps)

검토 조건과 기록 항목

별도로 관리할 업무 규칙

(rules)

문서에 없는 담당자 배정 기준

임의로 채우지 않은 null과 확인 질문

(missingFields)

결과의 출처

해당 값과 연결된 원문 인용

(evidence)

즉, 원하는 출력은 문서의 내용을 구조화한 값, 문서만으로 확정할 수 없는 질문, 각 값의 원문 근거를 함께 담아야 했습니다. 목표 출력이 정해져 있어야 원문에서 무엇을 가져왔고 무엇이 부족한지 일관되게 판단할 수 있습니다. 이 기준을 JSON Schema로 표현했습니다.

출력 계약으로 사용하는 간단한 JSON Schema

전체 흐름을 이해하는 데 필요한 항목만 남겨 단순화한 Schema 예시입니다.

{
  "type": "object",
  "additionalProperties": false,
  "required": [
    "documentPurpose",
    "participants",
    "processSteps",
    "rules",
    "assignmentRule",
    "missingFields",
    "evidence"
  ],
  "properties": {
    "documentPurpose": { "type": "string" },
    "participants": {
      "type": "array",
      "items": { "type": "object", "required": ["id", "name"] }
    },
    "processSteps": {
      "type": "array",
      "items": { "type": "object", "required": ["order", "actorId", "action"] }
    },
    "rules": { "type": "array" },
    "assignmentRule": {
      "type": ["string", "null"],
      "description": "원문에 배정 기준이 없으면 null"
    },
    "missingFields": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["fieldPath", "question"]
      }
    },
    "evidence": { "type": "array" }
  }
}

이 과정에서 Schema, Prompt와 Validator의 책임을 다음과 같이 나누었습니다.

Schema    : 필수 필드와 데이터 형식을 제한합니다.
Prompt    : 원문에 없는 값은 추측하지 않고 null과 확인 질문으로 반환하게 합니다.
Validator : 참조 관계, 원문 근거, null과 확인 질문의 대응 여부를 검사합니다.

예를 들어 Schema는 assignmentRule을 필수 필드로 요구하지만 null도 허용합니다. 샘플 문서는 검토자가 승인하거나 보완을 요청할 수 있다고만 설명할 뿐, 여러 검토자 중 담당자를 정하는 기준은 제공하지 않습니다. 그래서 Prompt는 이 값을 추측하지 않고 null과 확인 질문으로 반환하게 하며, Validator는 실제로 두 값이 함께 존재하는지 검사합니다.

문서에서 만드는 구조화 후보

위 문서를 프로그램이 읽을 수 있는 구조화 후보로 바꾸면 다음과 같은 형태가 됩니다. 이 데이터 구조는 글의 설명을 위해 만든 예시이며, 최종 정답이 아니라 검증과 사람의 확인을 거쳐야 하는 중간 결과입니다.

{
  "documentPurpose": "작업 요청의 진행 상황과 처리 이력을 일관되게 확인한다.",
  "participants": [
    { "id": "requester", "name": "요청자" },
    { "id": "reviewer", "name": "검토자" }
  ],
  "processSteps": [
    {
      "order": 1,
      "actorId": "requester",
      "action": "작업 내용을 작성한다."
    },
    {
      "order": 2,
      "actorId": "requester",
      "action": "검토를 요청한다."
    },
    {
      "order": 3,
      "actorId": "reviewer",
      "action": "승인하거나 보완을 요청한다."
    },
    {
      "order": 4,
      "actorId": "reviewer",
      "action": "처리 결과와 검토 의견을 기록한다."
    }
  ],
  "rules": [
    "검토를 요청한 작업만 검토할 수 있다.",
    "검토 결과에는 처리자와 처리 시각을 포함한다."
  ],
  "assignmentRule": null,
  "missingFields": [
    {
      "fieldPath": "assignmentRule",
      "question": "여러 검토자 중 담당자는 어떤 기준으로 정합니까?"
    }
  ],
  "evidence": [
    {
      "fieldPath": "processSteps[1]",
      "sourceExcerpt": "요청자가 검토를 요청합니다."
    }
  ]
}

이 결과를 검색·검토 같은 후속 과정에서 사용하려면 JSON 문법이 맞는 것만으로는 부족합니다. 원문의 담당자나 절차를 빠뜨리지 않았는지, Evidence가 실제 원문에 존재하는지, 미확정 정책을 모델이 임의로 만들지 않았는지를 추가로 검증해야 합니다.

처음에는 충분히 상세한 Prompt와 JSON Schema를 제공하면 안정적인 결과를 얻을 수 있을 것으로 예상했습니다. 필수 필드, 데이터 타입과 객체 구조를 Schema로 제한하고, 응답은 Structured Output 기능을 사용해 JSON으로 받았습니다.

이 접근은 기본적인 문제를 상당수 해결했습니다. JSON 앞뒤에 설명이 붙는 문제, Markdown 코드 블록이 섞이는 문제, 필드명이 실행마다 달라지는 문제는 크게 줄었습니다. Schema 전달, 응답 파싱과 오류 처리 경로도 자동 테스트로 반복 검증할 수 있었습니다.

그러나 구조가 안정되자 오히려 더 중요한 문제가 선명하게 보이기 시작했습니다. JSON이 유효하다는 사실과 업무 내용이 정확하다는 사실은 전혀 다른 문제였습니다.

첫 번째 문제: 형식이 맞아도 내용은 틀릴 수 있었습니다

평가 과정에서 자주 확인한 문제는 담당자, 행동과 처리 결과의 혼동이었습니다. 원문이 “검토자는 요청을 승인하거나 보완을 요청합니다”라고 설명해도, 일부 응답은 승인자라는 새로운 참여자를 만들거나 “검토 완료”라는 원문에 없는 단계를 추가했습니다.

이 결과도 JSON Schema를 통과할 수 있습니다. participants와 processSteps가 배열이고 각 값의 타입이 맞기 때문입니다. 그러나 원문에 없던 담당자나 절차가 추가되었으므로 업무 내용은 달라져 있습니다.

또 다른 문제는 참조 관계의 불일치였습니다. 처리 단계의 actorId가 participants에 존재하지 않거나, 같은 참여자가 서로 다른 ID로 표현되기도 했습니다. 각각의 객체만 보면 올바르지만 객체 사이의 관계는 깨진 상태입니다. 단순히 식별자 형식을 정규화해도 잘못 이해한 의미까지 올바르게 바뀌지는 않습니다.

이때부터 검증을 두 층으로 분리했습니다.

구조 검증
  - JSON 문법
  - 필수 필드
  - 타입
  - enum
  - key 형식
 
의미 보존 검증
  - 원문 항목 누락 여부
  - 참여자와 처리 단계의 참조 일관성
  - 처리 순서의 중복과 역전 여부
  - 원문 근거 존재 여부
  - 원문에 없는 값의 임의 생성 여부

Schema는 여전히 중요하지만 필요조건으로만 취급했습니다. 그 위에 같은 입력에는 항상 같은 판정을 내리는 규칙 기반 Validator를 추가하고, 원문과 결과의 차이를 별도 평가 데이터로 남겼습니다.

두 번째 문제: 모델이 읽지 않은 문장을 어떻게 찾을 것인가

LLM 응답에 일부 결과가 빠졌을 때, 그것이 정말 원문에 없는 정보인지 모델이 놓친 것인지 구분하기 어려웠습니다. 출력 결과만 검사해서는 이 차이를 알 수 없습니다.

이를 해결하기 위해 입력 문서를 비어 있지 않은 문장 단위로 나누고 식별자를 부여했습니다. 각 문장은 하나 이상의 결과 필드와 연결되거나, 변환되지 않은 이유를 가져야 합니다.

MAPPED    : 하나 이상의 결과 필드와 연결됨
UNMAPPED  : 현재 출력 구조에 대응하는 필드가 없음
EXCLUDED  : 변환 대상이 아닌 문장으로 명시적으로 제외됨

필수 필드의 완성도는 입력 문장 상태와 별도로 관리했습니다.

PRESENT      : 원문 근거를 가진 값이 존재함
NEEDS_INPUT  : 원문만으로 확정할 수 없어 사용자 확인이 필요함

이렇게 분리한 이유는 원문에 아예 존재하지 않는 정보를 특정 입력 문장의 상태로 표현할 수 없기 때문입니다. 입력 커버리지는 “어떤 문장을 처리했는가”를 추적하고, 필드 완성도는 “필요한 값이 충분한가”를 판단합니다.

평가 문서에 적용한 뒤에는 처리되지 않은 문장을 바로 찾고, 모델의 누락과 출력 구조의 한계를 구별할 수 있었습니다. 모든 내용을 억지로 필드에 넣지 않으면서도 문장이 조용히 사라지는 문제를 막을 수 있었습니다.

세 번째 문제: 근거처럼 보이는 문장도 모델이 만든 문장일 수 있습니다

변환 결과의 신뢰도를 높이기 위해 각 결과 항목에 원문 근거인 Evidence를 붙였습니다. 처음에는 모델이 반환한 sourceExcerpt를 그대로 저장했습니다. 하지만 평가해 보니 Evidence 안에 원문 문장이 아니라 모델이 요약하거나 재작성한 문장이 섞여 있었습니다.

예를 들어 원문이 다음과 같다고 가정하겠습니다.

검토자는 요청을 승인하거나 보완을 요청할 수 있습니다.

모델이 아래 문장을 Evidence로 반환하면 의미는 비슷해 보입니다.

검토자는 모든 요청의 최종 처리 결과를 결정합니다.

하지만 두 번째 문장은 원문에 없습니다. 더 강한 권한을 암시하고 있으며, 모델의 해석이 들어갔습니다. 이를 근거로 인정하면 모델이 만든 주장을 다시 자기 결과의 증거로 사용하는 순환이 생깁니다.

그래서 공백만 정규화한 뒤 sourceExcerpt가 입력 문서에 실제로 존재해야 통과하도록 했습니다. 이를 통해 모델이 요약하거나 재작성한 문장이 Evidence로 승인되는 사례를 차단했습니다.

다만 Exact Match는 문장이 원문에 존재한다는 사실만 보장합니다. 그 문장이 연결된 필드를 실제로 뒷받침하는지는 별개의 문제이므로 Evidence 검증을 다음처럼 구분했습니다.

1단계: 원문성 검증
  sourceExcerpt가 입력 문서에 실제로 존재하는가?
 
2단계: 연결 검증
  Evidence가 가리키는 fieldPath가 실제 결과 항목과 일치하는가?
 
3단계: 의미 정합성 검증
  이 문장이 해당 결과를 충분히 뒷받침하는가?

원문성은 규칙으로 검사할 수 있었지만 의미 정합성은 사람의 검토가 필요했습니다. 검증기가 보장하는 범위를 원문 존재 여부까지로 명확히 제한했습니다.

모르는 값은 채우지 말고 질문하게 만들었습니다

LLM은 빈칸을 보면 자연스럽게 채우려는 성향이 있습니다. 일반적인 글쓰기에는 장점이지만, 업무 문서 구조화에서는 위험합니다. 원문에 업무 목적이나 책임 경계가 없을 때 모델이 업계의 일반적인 내용을 작성하면, 결과는 자연스럽지만 사실이 아닐 수 있습니다.

이를 막기 위해 필수 값이 원문에서 확인되지 않으면 값을 생성하는 대신 질문을 반환하도록 계약을 바꿨습니다.

{
  "missingFields": [
    {
      "fieldPath": "documentPurpose",
      "question": "이 업무가 최종적으로 달성해야 하는 결과는 무엇입니까?"
    }
  ]
}

이 설계에서 질문은 실패 메시지가 아닙니다. 변환 과정의 정상적인 결과입니다. 원문 정보가 부족한 상황과 모델이 내용을 놓친 상황을 구분하고, 사용자가 다음 단계에서 보완할 수 있게 합니다.

실제 평가에서는 사용자 확인 질문이 예상보다 많이 생성되는 경우가 있었습니다. 처음에는 질문 수를 줄여야 한다고 생각했지만, 내용을 살펴보니 그동안 모델이 조용히 추론해 넣던 불확실성이 가시화된 경우가 많았습니다. 질문의 개수만 줄이기보다 중복 질문을 합치고, 실제 결정에 필요한 질문을 우선순위화하는 편이 더 적절했습니다.

Repair Loop는 재시도가 아니라 검증 결과를 이용한 교정이었습니다

검증 실패 시 같은 요청을 그대로 다시 보내면 다른 결과가 나올 수 있지만, 무엇을 고쳐야 하는지 모델은 알지 못합니다. 그래서 최대 한 번의 Repair 요청을 허용하되, 두 번째 요청에는 다음 정보를 함께 제공했습니다.

- 최초 사용자 요청
- 최초 모델 응답
- Schema 및 결정적 Validator 오류
- 누락되거나 원문과 일치하지 않은 항목

처리 흐름은 다음과 같습니다.

Result first = generate(request);
Validation check = validate(first);
if (check.isValid()) return first;
 
Result repaired = repair(request, first, check.errors());
return validateOrSendToManualReview(repaired);

무한 재시도는 허용하지 않았습니다. 반복 호출로 우연히 통과한 결과는 품질 문제를 숨길 수 있기 때문입니다. 두 번째 결과도 실패하면 자동 처리를 중단하고 원 응답과 오류를 기록했습니다.

한 평가에서는 입력 문장 대부분이 올바르게 처리되었지만, Evidence 일부가 원문을 미세하게 재작성했습니다. 전체 결과가 상당히 좋아 보여도 시스템은 이를 승인 가능한 결과로 취급하지 않고 Repair 경로로 보냈습니다. 이 경험은 “대부분 맞음”을 시스템 성공으로 볼 것인지에 대한 기준을 다시 생각하게 했습니다.

적용 후 달라진 점

검증 구조를 적용한 뒤의 변화는 다음과 같이 정리할 수 있습니다.

이전

적용 후

누락 여부를 원문과 수작업으로 비교했습니다.

처리되지 않은 입력 문장을 바로 식별했습니다.

모델이 반환한 근거를 그대로 사용했습니다.

원문에 존재하지 않는 Evidence를 차단했습니다.

문서에 없는 값도 자연스럽게 보완되었습니다.

값을 확정하지 않고 확인 질문으로 전환했습니다.

검증 실패 시 같은 요청을 반복했습니다.

구체적인 오류를 전달해 제한적으로 교정했습니다.

실패 응답을 폐기했습니다.

검증 오류와 함께 회귀 평가 자료로 보존했습니다.

실행 기록과 확인된 데이터를 분리했습니다

같은 문서와 지침을 여러 모델에 적용해 보니 모두 JSON은 반환했지만 실패 지점은 달랐습니다. 어떤 모델은 규칙을 생략했고, 다른 모델은 원문에 없는 설명을 추가했으며, 또 다른 모델은 참조 관계에서 오류를 보였습니다. 따라서 최종 결과만 저장해서는 문제의 원인을 추적하기 어려웠습니다.

실행 기록에는 사용한 모델과 Prompt 버전, 입력 문서, 원시 응답, 파싱 결과, 검증 상태와 실패 사유를 남겼습니다. 이 기록은 확정된 업무 데이터가 아니라 디버깅과 회귀 평가를 위한 자료입니다. 검증을 통과하고 사람이 확인한 결과만 사용 가능한 데이터로 전환했습니다.

핵심 내용 요약

  • LLM은 확정 데이터를 만드는 주체가 아니라 구조화 후보를 만드는 역할로 제한해야 합니다.

  • Structured Output은 파싱 오류를 줄이지만 누락이나 잘못된 해석까지 막지는 못합니다.

  • 누락을 찾으려면 결과의 개수보다 입력 문장의 처리 상태를 추적해야 합니다.

  • Evidence의 원문 존재 여부와 해당 필드를 뒷받침하는지는 별도로 판단해야 합니다.

  • 실패 응답과 검증 오류를 함께 보존해야 Prompt, Schema와 Validator 변경 후 회귀 평가가 가능합니다.

마치며

이 작업을 시작할 때는 자연어 문서를 JSON으로 변환하는 기능이라고 생각했습니다. 구현을 진행하면서 실제 문제는 JSON 생성이 아니라 불확실한 해석을 어디까지 허용하고, 무엇을 기계적으로 검증하며, 언제 사람에게 다시 질문할 것인가라는 사실을 알게 되었습니다.

최종 구조는 JSON Schema로 형식을 제한하고, 입력 커버리지와 Evidence를 검사하며, 알 수 없는 정보는 질문으로 남기는 방향이었습니다. 생성형 AI를 업무에 적용할 때 중요한 것은 가장 자연스러운 답을 얻는 것보다, 틀린 결과가 다음 단계로 넘어가지 않게 만드는 것이었습니다.

제가 이 작업에서 얻은 결론은 LLM을 신뢰할 수 있게 만드는 것은 더 강한 프롬프트가 아니라, LLM의 출력을 의심하고 검증하는 시스템이라는 것입니다.

Brown

Site footer