이 글은 누구를 위한 것인가
- AI가 존재하지 않는 사실을 만들어내는 문제를 겪는 팀
- 의료·법률·금융 도메인에서 AI 응답의 정확성이 중요한 팀
- RAG 시스템에서 환각을 최소화하고 싶은 엔지니어
들어가며
LLM은 자신감 있게 틀린 정보를 말한다. 의료 AI가 존재하지 않는 약물 상호작용을 말한다면 심각한 문제다. 환각을 100% 막을 수는 없지만, 탐지하고 최소화할 수 있다.
이 글은 bluefoxdev.kr의 AI 신뢰성 설계 를 참고하여 작성했습니다.
1. 환각 유형과 탐지 전략
[LLM 환각 유형]
사실 환각:
존재하지 않는 인물, 논문, 법률 인용
날짜·수치 오류
지식 컷오프 이후 정보를 아는 척
논리 환각:
전제에서 결론이 논리적으로 따르지 않음
자기 모순 (같은 대화에서 다른 답)
충실도 환각 (RAG):
제공된 문서에 없는 내용을 답변에 포함
문서 내용을 왜곡하여 인용
[탐지 전략]
1. 인용 강제화:
"근거 문장을 원문 그대로 인용하세요"
인용 없는 주장 = 환각 위험
2. 자기 일관성 (Self-Consistency):
같은 질문 N번 → 답변 불일치 = 환각 가능성
3. 역질문 (Reverse QA):
답변에서 질문 역생성 → 원 질문과 다르면 환각
4. NLI 검증:
답변이 소스 문서에 의해 지지되는지 확인
2. 환각 감지 및 방지 구현
import anthropic
import json
from dataclasses import dataclass
client = anthropic.Anthropic()
@dataclass
class GroundedResponse:
answer: str
citations: list[dict]
confidence: float
hallucination_risk: str # 'low', 'medium', 'high'
async def grounded_rag_response(
question: str,
source_documents: list[dict],
) -> GroundedResponse:
"""RAG 기반 그라운딩된 응답 생성"""
# 문서 번호 부여
numbered_docs = "\n\n".join(
f"[문서 {i+1}]: {doc['content'][:800]}"
for i, doc in enumerate(source_documents)
)
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1500,
messages=[{
"role": "user",
"content": f"""다음 문서들만을 근거로 질문에 답하세요.
문서에 없는 내용은 절대 추가하지 마세요.
확신할 수 없으면 "이 문서에는 해당 정보가 없습니다"라고 하세요.
문서:
{numbered_docs}
질문: {question}
JSON 형식으로 답하세요:
{{
"answer": "답변",
"citations": [
{{"doc_number": 1, "quoted_text": "원문 인용", "supports_claim": "지지하는 주장"}}
],
"confidence": 0.0-1.0,
"has_sufficient_info": true/false
}}"""
}]
)
result = json.loads(response.content[0].text)
hallucination_risk = "low" if result["confidence"] > 0.8 else \
"medium" if result["confidence"] > 0.5 else "high"
return GroundedResponse(
answer=result["answer"],
citations=result["citations"],
confidence=result["confidence"],
hallucination_risk=hallucination_risk,
)
async def self_consistency_check(
question: str,
n_samples: int = 5,
) -> dict:
"""자기 일관성으로 환각 위험 측정"""
responses = []
for _ in range(n_samples):
response = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=300,
messages=[{"role": "user", "content": question}],
)
responses.append(response.content[0].text)
# 핵심 사실 추출 및 일관성 비교
judge_response = client.messages.create(
model="claude-opus-4-7",
max_tokens=400,
messages=[{
"role": "user",
"content": f"""다음 5개 응답의 사실적 일관성을 분석하세요.
질문: {question}
{"".join(f"응답 {i+1}: {r[:200]}" + chr(10) for i, r in enumerate(responses))}
JSON으로:
{{
"consistent_facts": ["모든 응답이 동의하는 사실"],
"inconsistent_facts": ["응답마다 다른 사실"],
"consistency_score": 0.0-1.0,
"hallucination_risk": "low/medium/high"
}}"""
}]
)
result = json.loads(judge_response.content[0].text)
result["sample_responses"] = responses
return result
async def verify_factual_claim(
claim: str,
context: str = "",
) -> dict:
"""사실 주장 검증"""
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=300,
messages=[{
"role": "user",
"content": f"""다음 주장의 사실 여부를 검증하세요.
{"맥락: " + context if context else ""}
주장: {claim}
JSON으로:
{{
"verdict": "supported/refuted/unknown",
"confidence": 0.0-1.0,
"explanation": "이유",
"suggested_verification": "확인 방법 (URL, 기관 등)"
}}"""
}]
)
return json.loads(response.content[0].text)
def add_uncertainty_to_prompt(base_prompt: str) -> str:
"""불확실성 표현을 유도하는 프롬프트 래퍼"""
return f"""{base_prompt}
중요 지침:
- 확실하지 않은 내용은 "~인 것으로 알려져 있습니다" 또는 "~일 수 있습니다"로 표현
- 모르는 것은 솔직하게 "이 정보를 확인할 수 없습니다"라고 말하세요
- 숫자/날짜는 출처와 함께 제시하세요
- 추측은 명확히 "추측하건대"로 표시하세요"""
마무리
환각 방지의 최선은 RAG + 인용 강제화다. LLM이 소스 문서의 원문을 인용하게 하면 자의적 창작을 막는다. 고위험 도메인(의료, 법률)에서는 자기 일관성 체크와 사람 검토를 반드시 추가하라. "모른다"고 말하는 AI가 틀린 정보를 자신있게 말하는 AI보다 훨씬 안전하다.