13.7.3 자기 수정(Self-Correction) 루프: 검증 에러 피드백을 통한 LLM의 재생성 유도
직전 13.7.2절에서 우리는 검증 실패를 삼분할하여, LLM에게 다시 기회를 주면 스스로 고칠 가능성이 높은 ’Type 2 에러(오라클 피드백 기반 LLM 자가 수정)’를 도출해 냈다.
이를 소프트웨어 아키텍처로 구현하기 위해, 파이프라인은 1회성 API 호출이 끝난 뒤 에러가 나면 멍청하게 프로그램을 종료해 버리는 ‘단방향(One-way)’ 흐름의 틀을 부수어야 한다. 대신 에러의 추적 단서를 들고 다시 모델의 입력단으로 회귀하는 지능적인 순환 루프백(Loop-back) 아키텍처를 장착해야만 한다.
우리는 백엔드의 Pydantic 모델과 커스텀 오라클 함수가 뱉어낸 차갑고 결정론적인 예외 문자열(Exception Stacktrace)을 포획하여, 이를 LLM이 이해하기 쉬운 자연어 ’오답 노트’로 예쁘게 포장한 뒤 다음 프롬프트에 주입하는 가장 강력한 피드백 컨트롤러(Feedback Controller)를 구현할 것이다.
1. LLM에게 체계적인 오답 노트(Negative Feedback) 주입하기
오답 처리된 LLM에게 무작정 하드 리셋(Hard Reset)을 걸고 “다시 추출해 봐“라고 맨땅에 지시하면, 놀랍게도 LLM은 높은 확률로 아까 냈던 그 틀린 오답 텐서를 토시 하나 안 틀리고 똑같이 뱉어낸다. 왜냐하면 LLM은 외부 환경(파이프라인)이 자신의 이전 출력을 왜 기각했는지 스스로 깨달을 수 있는 인지 채널이 단절되어 있기 때문이다.
따라서 자가 수정 루프(Self-Correction Loop)의 핵심 원리는, 파이썬 인터프리터가 잡아낸 ValidationError 객체의 __str__ 메시지 자체를 문자열로 추출하여 구조화된 대화 히스토리에 얹어 LLM의 컨텍스트 창(Context Window) 정중앙에 직접 때려 박는 것이다.
import instructor
from openai import AsyncOpenAI
import json
# Instructor 라이브러리를 통해 Pydantic 스키마 강제 추출 및 재시도 래퍼(Wrapper) 생성
client = instructor.patch(AsyncOpenAI())
async def extract_with_self_correction_loop(image_base64: str, max_retries: int = 3):
"""
오라클의 차가운 피드백을 적극 수용하며 스스로 환각을 고쳐나가는 AI의 재귀적 추출 루프
"""
# 1. 대화 히스토리 (오라클의 오답 노트를 누적할 배열 상태 머신)
messages = [
{"role": "system", "content": "당신은 비정형 영수증을 엄격한 JSON으로 추출하는 수석 회계사다."},
{"role": "user", "content": [
{"type": "text", "text": "이 영수증 이미지를 InvoiceMaster 스키마에 맞게 파싱하라."},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"}}
]}
]
for attempt in range(max_retries):
try:
# 2. LLM 비동기 추론 호출 (이 블록 내부에서 Pydantic 오라클 검증이 다중 격발됨)
response_model = await client.chat.completions.create(
model="gpt-4o",
response_model=InvoiceMasterOracleStage, # 13장 전체를 관통하는 통합 오라클 Pydantic 모델
messages=messages,
temperature=0.1 # 환각 억제를 위한 저온도 세팅
)
# 모든 지옥의 오라클 검증망을 무사히 통과했다면 즉시 리턴하고 당당하게 루프 탈출
return response_model
except Exception as e:
# 3. 오라클 검증망 격발 (1,2,3단계 중 어딘가에서 ValidationError 캐치됨)
error_message = str(e)
print(f"[Self-Correction] 시도 {attempt + 1}/{max_retries}회 차 오라클 검증 철퇴: {error_message}")
if attempt == max_retries - 1:
# 마지막 코인마저 소진했다면 즉각 Type 3 에러로 승격시켜 HITL 큐로 라우팅
raise ValueError(f"최대 재시도 {max_retries}회 완전 소진. 데이터 폐기 요망: {error_message}")
# 4. LLM에게 오라클의 오답 노트를 던져주며 시각적 어텐션(Attention) 교정 요구
correction_prompt = (
f"당신이 방금 추출한 백엔드 페이로드는 우리 파이프라인의 오라클 검증기에서 "
f"다음과 같은 치명적 에러를 격발시켰다.\n\n"
f"[오라클 시스템 피드백]\n{error_message}\n\n"
f"위 에러의 원인을 철저히 분석하고, 네가 놓친 이미지의 다른 기하학적 영역으로 시각적 어텐션을 이동시켜 "
f"오라클을 완벽히 통과할 수 있도록 데이터를 올바르게 다시 추출하고 수정해라."
)
# 실패의 기억과 오라클의 가혹한 꾸짖음을 프롬프트 컨텍스트 창에 누적 기록 (Self-Reflection 유도)
messages.append({"role": "assistant", "content": "제가 제출한 데이터가 실패했습니다. 오라클 피드백을 주십시오."})
messages.append({"role": "user", "content": correction_prompt})
2. 핀포인트(Pin-point) 에러 메시징 역량과 환각 치유율의 상관관계
이 자가 수정 루프를 설계할 때 MLOps 엔지니어가 절실히 명심해야 할 한 가지가 있다. LLM을 가르치는 오라클의 에러 메시지(ValueError)는 파이썬 개발자가 디버깅하기 위해서가 아니라, 결국 ‘LLM 모델 스스로가 읽고 깨닫기 위해’ 극도로 친절하고 서술형으로 튜닝되어 있어야 한다는 사실이다.
백엔드 코드에 단순히 raise ValueError("Total mismatch") 라는 무뚝뚝하고 의미론이 파괴된 짧은 스크립트를 박아두면, LLM은 무엇을 어떻게 고쳐야 할지 감을 잡지 못하고 무의미한 환각의 핑퐁 루프만 반복하게 된다.
그러나 13.4.2절에서 다루었듯, Pydantic의 내장 함수를 활용해 "산술 에러: 네가 추출한 LineItem 부분합의 총합은 150.00인데, 네가 지목한 영수증 Total 필드는 200.00이다. 중간에 $50.00 짜리 배송비(Shipping) 항목을 놓치지 않았는지 이미지 우측 하단을 다시 확인해라." 와 같이 인간 교사가 학생을 지도하듯 명확한 연산 모순 로그를 핀포인트(Pin-point)로 찔러 넣어준다면 어떻게 될까?
LLM의 자기장 수정(Self-Correction) 통과 성공률은 마법처럼 90% 이상으로 급격히 치솟게 된다. 이것이 파이프라인의 아키텍처 속에 스며든 진정한 메타 프롬프팅(Meta-prompting)의 예술이다.