Published on

OpenAI API+LangChain RAG 환각 줄이는 7가지 튜닝

Authors

RAG(Retrieval-Augmented Generation)는 “검색으로 근거를 주입해 환각을 줄인다”는 약속으로 시작하지만, 실제 운영에서는 여전히 그럴듯한 거짓말이 자주 발생합니다. 원인은 대개 모델 자체보다 파이프라인의 사소한 설정(청킹, 검색, 재랭킹, 프롬프트, 디코딩, 컨텍스트 구성, 후처리/검증)에서 생깁니다.

이 글은 OpenAI API와 LangChain 조합을 전제로, 현업에서 바로 적용 가능한 “환각 감소 튜닝 7가지”를 체크리스트 형태로 정리합니다. 평가 자동화까지 연결하고 싶다면 LangChain+OpenAI로 RAG 환각 줄이는 평가 자동화도 함께 보세요. 더 넓은 전략은 OpenAI+LangChain RAG 할루시네이션 차단 전략에 정리해 두었습니다.

1) 청킹: “짧게”가 아니라 “질문 단위로 끊기”

RAG 환각의 시작은 검색 실패입니다. 검색 실패는 종종 임베딩 모델 문제로 오해되지만, 실제로는 청킹 설계가 더 큰 비중을 차지합니다.

실전 가이드

  • 문서를 고정 길이로 자르지 말고, 가능한 한 의미 단위(제목, 섹션, 표 캡션, FAQ 항목)로 끊습니다.
  • 청크에 “헤더 경로”를 메타데이터로 함께 저장합니다. 예: doc_title, section_path, updated_at.
  • 오버랩은 무조건 늘리는 게 답이 아닙니다. 오버랩이 과하면 유사 청크가 과다 검색되어 컨텍스트가 오염됩니다.

LangChain 예시 코드

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=900,
    chunk_overlap=120,
    separators=["\n\n", "\n", ". ", " "]
)

docs = splitter.create_documents(
    [raw_text],
    metadatas=[{"doc_title": title, "section_path": "A/B", "updated_at": "2026-02-01"}]
)

튜닝 포인트

  • chunk_size는 “모델 컨텍스트 최대치”가 아니라 “검색 단위의 의미 밀도”에 맞춥니다.
  • 문서가 정책/규정/가이드처럼 문장이 길고 참조가 많다면 청크를 더 작게(예: 500~900) 잡는 편이 환각이 줄어드는 경우가 많습니다.

2) 검색: 하이브리드(키워드+벡터)와 메타 필터로 실패율 낮추기

벡터 검색만 쓰면 숫자/코드/약어/고유명사에서 미끄러질 때가 많습니다. 반대로 키워드만 쓰면 의역에 약합니다. 운영 RAG에서는 하이브리드가 안전합니다.

실전 가이드

  • 가능하면 BM25 계열(키워드) + 벡터를 결합합니다.
  • 최소한 메타데이터 필터는 적극적으로 사용합니다. 예: updated_at 최신 문서 우선, 제품 버전 필터, 국가/리전 필터.

LangChain(개념) 예시

사용하는 벡터 DB에 따라 구현이 다르지만, 핵심은 “질문에서 필터를 추출해 검색 범위를 줄이는 것”입니다.

from langchain_core.documents import Document

def build_filter(user_query: str) -> dict:
    # 예: "v2" "KR" 같은 힌트를 룰 기반으로 뽑아 필터링
    f = {}
    if "v2" in user_query:
        f["product_version"] = "v2"
    if "한국" in user_query or "KR" in user_query:
        f["region"] = "KR"
    return f

filter_ = build_filter(query)
retrieved_docs = vectorstore.similarity_search(query, k=8, filter=filter_)

운영 팁

벡터 DB에 업서트가 몰리거나 재시도 설계가 허술하면 인덱스가 부분적으로 비어 검색 품질이 급락할 수 있습니다. 대량 업서트/타임아웃/429 대응이 필요하다면 Pinecone 업서트 429·타임아웃, 배치·재시도 설계처럼 “인덱싱 안정화”도 환각 감소의 중요한 전제입니다.

3) 재랭킹: k를 키우기보다 “정렬 품질”을 올리기

많은 팀이 검색 결과가 불안정하면 k를 늘립니다. 하지만 k를 늘리면 상관없는 문서가 더 섞여 들어가 컨텍스트가 오염되고, 모델은 그럴듯한 조합을 만들어 환각을 강화하기도 합니다.

실전 가이드

  • 1차 검색은 넉넉히(예: 20) 뽑되, 2차 재랭킹으로 최종 컨텍스트는 타이트하게(예: 4~8) 유지합니다.
  • 재랭킹은 cross-encoder(문장쌍 분류) 계열이 강력하지만 비용이 듭니다. 비용이 부담되면 “질문-청크 제목/헤더 매칭 가중치” 같은 휴리스틱도 효과가 있습니다.

간단 재랭킹(휴리스틱) 예시

import re

def score(doc, query: str) -> float:
    text = (doc.page_content or "").lower()
    q = query.lower()
    # 매우 단순한 예: 쿼리 키워드 포함 개수 + 최신성 가중치
    tokens = [t for t in re.split(r"\W+", q) if len(t) >= 2]
    hit = sum(1 for t in tokens if t in text)
    freshness = 0.1 if doc.metadata.get("updated_at", "") >= "2026-01-01" else 0.0
    return hit + freshness

candidates = retrieved_docs  # k=20
reranked = sorted(candidates, key=lambda d: score(d, query), reverse=True)
final_docs = reranked[:6]

4) 컨텍스트 구성: “많이 넣기”가 아니라 “충돌 제거”

환각은 “없는 근거를 지어냄”도 있지만, 더 흔한 형태는 “서로 충돌하는 근거를 동시에 넣어 모델이 중간 어딘가를 만들어냄”입니다.

실전 가이드

  • 중복/유사 청크 제거(dedup)를 넣습니다. 예: 동일 문서의 인접 청크가 연달아 들어오면 하나만 남기기.
  • 문서 버전이 섞이지 않게 합니다. product_version 같은 메타데이터가 없다면 지금이라도 넣는 게 좋습니다.
  • 컨텍스트에는 “원문”만 넣지 말고 출처 키를 함께 넣습니다. 예: source_id, url, section_path.

컨텍스트 템플릿 예시

아래처럼 문서마다 출처 라벨을 붙여두면, 모델에게 “근거 기반 답변”을 강제하기 쉬워집니다.

def format_docs(docs):
    blocks = []
    for i, d in enumerate(docs, start=1):
        src = d.metadata.get("url") or d.metadata.get("source_id") or "unknown"
        path = d.metadata.get("section_path", "")
        blocks.append(
            f"[DOC {i}] source: {src} section: {path}\n{d.page_content}"
        )
    return "\n\n".join(blocks)

5) 프롬프트: “모르면 모른다”를 규칙으로 못 박기

RAG에서 프롬프트는 단순한 말투가 아니라 정책입니다. 특히 “근거가 없을 때의 행동”을 명확히 규정하지 않으면, 모델은 빈칸을 채우는 방향으로 움직입니다.

실전 프롬프트 규칙

  • 컨텍스트에 없는 내용은 답하지 말 것
  • 답변에 출처를 반드시 포함할 것
  • 불확실하면 추가 질문(clarifying question)으로 전환할 것

LangChain 프롬프트 예시

<> 문자가 일반 텍스트로 노출되지 않도록, 템플릿 변수는 중괄호만 사용합니다.

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", 
     "당신은 사내 문서 기반 QA 어시스턴트입니다. "
     "반드시 제공된 CONTEXT에 근거해 답하세요. "
     "CONTEXT에 없는 사실은 추측하지 말고 '근거 부족'이라고 말하세요. "
     "답변에는 사용한 DOC 번호를 각 문장 끝에 [DOC n] 형태로 표기하세요."),
    ("human", "QUESTION:\n{question}\n\nCONTEXT:\n{context}")
])

6) 생성 파라미터: temperature만 내리지 말고 “중단 조건”을 설계

환각을 줄이려고 temperature=0만 고집하면, 답변이 건조해지거나 오히려 “단정적인 틀린 답”이 나오는 경우도 있습니다. 중요한 건 샘플링보다 “답을 멈추는 조건”과 “출처 기반 구조”입니다.

실전 가이드

  • temperature는 0~0.3 범위에서 시작하고, 대신 “출처 없는 문장 금지” 같은 구조적 제약을 둡니다.
  • 답변 길이를 제한하거나, 섹션 구조를 고정합니다. 예: 결론, 근거, 추가 확인 필요.
  • 가능하면 응답을 JSON 같은 구조로 받아 후처리 검증을 합니다.

OpenAI API 호출(예시)

SDK 버전에 따라 다를 수 있지만, 핵심은 “구조화된 출력”과 “낮은 변동성”입니다.

from openai import OpenAI

client = OpenAI()

resp = client.responses.create(
    model="gpt-4.1-mini",
    input=[
        {"role": "system", "content": "CONTEXT 기반으로만 답하고, 근거가 없으면 근거 부족이라고 말하라."},
        {"role": "user", "content": f"QUESTION:\n{question}\n\nCONTEXT:\n{context}"}
    ],
    temperature=0.2,
    max_output_tokens=500,
)

answer = resp.output_text

7) 후처리 검증: “근거 인용 검사”와 “자기검열 루프”를 붙이기

운영에서 환각을 체감상 크게 줄이는 방법은, 생성 직후에 가벼운 검증기를 붙이는 것입니다. 완벽한 사실 검증은 어렵지만, RAG에서는 최소한 아래 두 가지는 자동화할 수 있습니다.

(1) 인용 강제 검사

  • 답변 문장마다 [DOC n]가 붙었는지 확인
  • [DOC n]가 실제로 제공된 문서 범위인지 확인
  • 인용이 없는 문장이 있으면 “근거 부족”으로 재생성하거나, 해당 문장을 제거
import re

def validate_citations(answer: str, num_docs: int) -> list[str]:
    errors = []
    # 문장 단위는 단순화(운영에서는 더 정교한 분리 권장)
    sentences = [s.strip() for s in re.split(r"(?<=[.!?])\s+", answer) if s.strip()]
    for s in sentences:
        cites = re.findall(r"\[DOC\s*(\d+)\]", s)
        if not cites:
            errors.append(f"NO_CITATION: {s}")
            continue
        for c in cites:
            idx = int(c)
            if idx < 1 or idx > num_docs:
                errors.append(f"BAD_CITATION_DOC_INDEX: {idx} in '{s}'")
    return errors

(2) 자기검열(리뷰어) 체인

  • 1차 답변 생성
  • 2차로 “컨텍스트에 없는 주장 찾기” 리뷰 프롬프트 실행
  • 문제가 있으면 “수정 지시”를 내려 재작성

LangChain에서는 이런 패턴을 체인으로 쉽게 구성할 수 있고, 평가까지 자동화하면 회귀(업데이트 후 환각 증가)를 잡기 좋습니다. 이 부분은 LangChain+OpenAI로 RAG 환각 줄이는 평가 자동화 글에서 더 깊게 다뤘습니다.

튜닝 우선순위 체크리스트

아래 순서대로 적용하면 비용 대비 효과가 큰 편입니다.

  1. 청킹을 의미 단위로 재설계하고 메타데이터를 보강한다
  2. 메타 필터를 도입해 검색 공간을 줄인다
  3. 1차 검색 k를 키우기보다 재랭킹과 dedup로 컨텍스트 오염을 줄인다
  4. 프롬프트에 “근거 없으면 근거 부족”과 “문장별 출처 표기”를 규칙으로 넣는다
  5. 생성 파라미터는 낮은 변동성으로 시작하되, 구조와 중단 조건을 설계한다
  6. 인용 검사 같은 라이트한 후처리 검증을 붙인다
  7. 오프라인 평가를 자동화해 튜닝이 실제로 환각을 줄였는지 수치로 확인한다

마무리

RAG 환각은 한 방에 사라지지 않습니다. 대신 “검색 실패율을 낮추고”, “컨텍스트 충돌을 제거하고”, “근거 없는 문장을 시스템적으로 못 나오게” 만들면 체감 품질이 빠르게 올라갑니다. 특히 4번(프롬프트 규칙)과 7번(후처리 검증)은 구현 난이도 대비 효과가 커서, 지금 운영 중인 RAG에도 바로 붙이기 좋습니다.

다음 단계로는, 도메인별로 자주 틀리는 질문 세트를 만들고(회귀 테스트), 청킹/검색/재랭킹/프롬프트 변경이 실제 환각률을 어떻게 바꾸는지 자동 평가로 추적하는 것을 권합니다.