AI 업무 활용

복잡한 텍스트를 AI로 JSON으로 추출하고 Python에서 검증하기

AI는 정리되지 않은 메모를 구조화된 JSON으로 바꿀 수 있지만 결과는 별도로 검증해야 합니다. 이 글에서는 작은 합성 예제와 Python 검증 코드를 사용해 필수 필드, 자료형, 허용 값, 기본 일관성을 확인합니다.

목차 보기

이런 분께메모, 이메일, 양식, 정리되지 않은 텍스트를 AI 채팅 도우미로 구조화된 JSON으로 변환하고 반복 가능한 검증 절차가 필요한 사람을 위한 글입니다.

준비사항
  • Python 3.12
  • JSON 객체, 문자열, 숫자, 리스트에 대한 기본적인 이해

01AI가 추출한 JSON을 왜 검증해야 할까

AI는 불규칙한 텍스트를 구조화된 필드로 바꾸는 데 유용하지만, 결과가 올바른 JSON처럼 보여도 내용은 틀릴 수 있습니다. 필수 키가 빠질 수 있고, 숫자가 문자열로 반환될 수 있으며, 허용되지 않은 값이 들어가거나 원문에 없던 정보를 AI가 추론해 추가할 수도 있습니다.

가장 안전한 작업 방식은 추출과 검증을 분리하는 것입니다. 먼저 AI에게 원문을 고정된 구조로 변환하도록 요청합니다. 그다음 반환된 JSON이 명시적인 규칙을 만족하는지 확인합니다. 검증만으로 모든 추출 값의 사실 정확성을 보장할 수는 없지만, 데이터를 스프레드시트, 데이터베이스, 스크립트, 자동화 작업에 넣기 전에 많은 구조적 오류를 잡을 수 있습니다.

02작은 합성 원문과 스키마 정의하기

이 예제는 합성 예제입니다. 짧은 서비스 메모에 고객명, 티켓 번호, 우선순위, 영향받는 제품, 선택적인 콜백 시간이 포함되어 있다고 가정합니다.

원문: "Ticket 1842. Customer: Mira Lee. Login fails on the desktop app and web portal. Priority is high. Please call after 15:30. Products affected: Desktop, Web."

필드규칙
ticket_id필수 정수
customer필수 비어 있지 않은 문자열
priority필수: low, medium, high 중 하나
products필수 비어 있지 않은 문자열 리스트
callback_timeHH:MM 형식 문자열 또는 null

이 스키마는 의도적으로 단순하게 구성했으며 Python 표준 라이브러리만으로 검사할 수 있습니다. 단순히 응답이 JSON으로 파싱되는지만 확인하는 것보다 더 엄격한 규칙입니다.

03제약 조건이 있는 JSON 객체를 AI에 요청하기

허용되는 필드와 누락 정보 처리 방법을 AI에게 정확히 알려줍니다. 예를 들면 다음과 같이 요청할 수 있습니다. "다음 텍스트를 정확히 ticket_id, customer, priority, products, callback_time 키를 가진 하나의 JSON 객체로 추출해 주세요. ticket_id는 정수여야 합니다. priority는 low, medium, high 중 하나여야 합니다. products는 문자열 리스트여야 합니다. callback_time이 원문에 없으면 null을 사용하세요. 누락된 사실을 추론하지 마세요. JSON만 반환하세요."

합성 메모에 대한 올바른 결과는 ticket_id 1842, customer Mira Lee, priority high, products에 Desktop과 Web, callback_time 15:30입니다.

그럴듯하지만 잘못된 AI 결과는 ticket_id를 문자열 "1842"로 반환하거나, priority를 "urgent"로 바꾸거나, issue_summary처럼 허용되지 않은 필드를 추가할 수 있습니다. 구조적으로 보기 좋더라도 약속한 스키마를 만족하지 않습니다.

04Python으로 JSON 검증하기

다음 스크립트는 Python 표준 라이브러리만 사용합니다. 합성 AI 응답을 읽고 예상하지 않은 키가 있는지, 필수 키와 자료형이 맞는지, priority가 허용된 값인지, products 리스트가 올바른지, callback_time 형식이 맞는지 확인합니다. 결과는 outputs에 보고서로 저장하며 같은 보고서가 이미 있으면 중단합니다.

python
import json
import re
from pathlib import Path

AI_RESPONSE = '''{
  "ticket_id": "1842",
  "customer": "Mira Lee",
  "priority": "urgent",
  "products": ["Desktop", "Web"],
  "callback_time": "15:30",
  "issue_summary": "Login problem"
}'''

REQUIRED_KEYS = {
    "ticket_id",
    "customer",
    "priority",
    "products",
    "callback_time",
}

ALLOWED_PRIORITIES = {"low", "medium", "high"}
TIME_PATTERN = re.compile(r"^(?:[01]\d|2[0-3]):[0-5]\d$")

errors = []

try:
    data = json.loads(AI_RESPONSE)
except json.JSONDecodeError as exc:
    raise SystemExit(f"Invalid JSON: {exc}")

if not isinstance(data, dict):
    errors.append("Top-level value must be a JSON object.")
else:
    actual_keys = set(data)
    missing = REQUIRED_KEYS - actual_keys
    unexpected = actual_keys - REQUIRED_KEYS

    if missing:
        errors.append("Missing keys: " + ", ".join(sorted(missing)))
    if unexpected:
        errors.append("Unexpected keys: " + ", ".join(sorted(unexpected)))

    if "ticket_id" in data and not isinstance(data["ticket_id"], int):
        errors.append("ticket_id must be an integer.")

    if "customer" in data:
        customer = data["customer"]
        if not isinstance(customer, str) or not customer.strip():
            errors.append("customer must be a non-empty string.")

    if "priority" in data:
        priority = data["priority"]
        if not isinstance(priority, str) or priority not in ALLOWED_PRIORITIES:
            errors.append("priority must be low, medium, or high.")

    if "products" in data:
        products = data["products"]
        if not isinstance(products, list) or not products:
            errors.append("products must be a non-empty list.")
        elif not all(isinstance(item, str) and item.strip() for item in products):
            errors.append("Every product must be a non-empty string.")

    if "callback_time" in data:
        callback = data["callback_time"]
        if callback is not None:
            if not isinstance(callback, str) or not TIME_PATTERN.fullmatch(callback):
                errors.append("callback_time must be HH:MM or null.")

status = "PASS" if not errors else "FAIL"
lines = [f"Validation: {status}"]
lines.extend(f"- {error}" for error in errors)

output_dir = Path("outputs")
output_dir.mkdir(exist_ok=True)
output_file = output_dir / "json_validation.txt"

if output_file.exists():
    raise SystemExit(f"Stop: {output_file} already exists.")

output_file.write_text("\n".join(lines) + "\n", encoding="utf-8")
print(f"Wrote {output_file}")

05구조 검증 결과 올바르게 해석하기

잘못된 합성 응답은 세 가지 이유로 검증에 실패해야 합니다. ticket_id가 정수가 아니라 문자열이고, priority가 허용 집합에 없는 urgent이며, issue_summary가 예상하지 않은 키입니다. products 리스트와 callback_time 형식은 구조 규칙을 만족합니다.

검사 항목AI 값기대값결과
ticket_id"1842"정수FAIL
customer"Mira Lee"비어 있지 않은 문자열PASS
priority"urgent"low, medium, highFAIL
products["Desktop", "Web"]비어 있지 않은 문자열 리스트PASS
callback_time"15:30"HH:MM 또는 nullPASS
추가 키issue_summary없음FAIL

스키마 검증이 실패하면 일반적으로 값을 조용히 강제 변환하기보다 결과를 수정하도록 되돌리는 편이 낫습니다. "1842"를 자동으로 1842로 바꾸면 추출 지시를 따르지 않았다는 사실이 숨겨질 수 있습니다. 자동 변환을 허용할지는 이후 작업 흐름에 따라 결정해야 합니다.

06추출된 값을 원문과 비교하기

구조 검증은 한 단계에 불과합니다. 중요한 필드는 원문과 직접 비교해야 합니다. 이 예제에서 ticket 1842, Mira Lee, high, Desktop, Web, 15:30은 모두 원문에 명시되어 있습니다. 반면 원문에는 urgent라는 단어가 없으므로 high를 urgent로 바꾸는 것은 단순한 형식 정리가 아니라 원문의 값을 변경하는 것입니다.

  • 식별자와 이름을 정확한 원문과 대조합니다.
  • priority처럼 선택값이 정해진 필드는 원문의 의미와 허용된 스키마 값이 모두 맞는지 확인합니다.
  • 리스트의 각 항목이 실제 원문에 언급되었는지 확인합니다.
  • 선택 필드가 원문에 없다면 추측하지 말고 null을 사용합니다.
  • 스키마가 추가 속성을 명시적으로 허용하지 않는다면 새 필드를 거부합니다.
  • 정규화와 정보 생성을 구분합니다. 대소문자 정리는 허용할 수 있지만 없는 마감일을 추가하는 것은 다릅니다.

07흔한 실수와 한계

흔한 실수는 json.loads가 응답을 읽을 수 있는지만 확인하는 것입니다. 파싱 성공은 문법적으로 올바른 JSON이라는 뜻일 뿐 필수 키, 자료형, 값이 정확하다는 뜻은 아닙니다. 또 다른 실수는 임의의 추가 필드를 허용하는 것입니다. 그러면 이후 코드가 원래 명세에 없던 정보에 의존할 수 있습니다.

스키마가 커지면 직접 작성한 검증 코드도 관리하기 어려워집니다. 이 경우 전용 스키마 시스템이나 검증 라이브러리를 사용할 수 있지만 기본 질문은 같습니다. 어떤 필드가 필수인지, 어떤 자료형을 허용하는지, 어떤 값이 가능한지, 누락 정보에 null을 허용하는지를 정해야 합니다.

검증은 원문 자체가 완전하거나 신뢰할 수 있다는 것을 보장하지도 않습니다. 원문에 잘못된 티켓 번호가 있거나 문장이 모호하면 구조적으로 올바르게 추출해도 그 문제는 그대로 남습니다. 검증이나 업무 규칙에서 문제가 생겼을 때 중요한 필드를 다시 추적할 수 있도록 원문을 함께 보관하는 것이 좋습니다.

실행·검증 기록

2026-09-21 · hand-checked example · Python 3.12

  • 합성 원문에 ticket 1842, customer Mira Lee, priority high, products Desktop 및 Web, callback time 15:30이 포함되어 있는지 확인했습니다.
  • 의도한 스키마에 정확히 다섯 개의 허용 키인 ticket_id, customer, priority, products, callback_time이 있는지 확인했습니다.
  • 잘못된 예시에서 정수 1842가 아니라 문자열 "1842"를 사용한 것을 확인했습니다.
  • urgent가 허용 priority 집합 low, medium, high에 포함되지 않는지 확인했습니다.
  • issue_summary가 명시된 스키마에서 예상하지 않은 키인지 확인했습니다.
  • ["Desktop", "Web"]이 비어 있지 않은 문자열 리스트 규칙을 만족하는지 확인했습니다.
  • 15:30이 명시한 24시간 HH:MM 패턴에 맞는지 확인했습니다.
  • 스크립트가 outputs/json_validation.txt에만 쓰고 해당 파일이 이미 존재하면 중단하도록 구성되어 있는지 확인했습니다.
검증 범위의 한계
  • Python 코드는 직접 실행하지 않았으며, 합성 예제, 검증 분기, 예상 실패 항목은 코드를 읽어 확인했습니다.
  • 이 직접 작성한 검증기는 여기에서 보여준 작은 스키마만 다루며 일반적인 JSON Schema 구현이 아닙니다.
  • 구조 검증만으로 추출 값의 사실 정확성을 보장할 수 없으므로 중요한 값은 원문과 비교해야 합니다.
  • 이 예제는 실패한 결과를 자동으로 AI에 다시 보내 수정하거나 임의의 입력 텍스트에 대해 보안 필터링을 수행하지 않습니다.

사이트 전체의 작성·검증 원칙

참고 출처

설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.