[블로그 톤 기반 리뷰 생성 도구] RAG: 검색 결과가 독이 될 때
사이드프로젝트를 진행 일지, 두 번째, RAG는 만능이 아니다!
LLM(거대언어모델) 애플리케이션을 개발할 때, RAG(Retrieval-Augmented Generation) 는 거의 필수적인 기술로 여겨집니다.
최신 정보를 모르는 AI에게 외부 데이터를 주입해 똑똑하게 만드는 기술이죠.
하지만 최근 사이드 프로젝트를 개발하면서, RAG가 오히려 독 이 되는 상황을 겪었습니다.
바로 “Context Contamination(맥락 오염)” 문제입니다.
문제 상황: “남의 경험”이 “내 경험”으로 둔갑하다.
사용자가 “오이도 조개구이집에 다녀왔다”라고 입력하면, 제 서비스는 Tavily API를 통해 해당 식당의 최신 정보를 긁어옵니다.
여기서 문제가 발생합니다.
- User Context (사용자 입력): “대방어랑 가리비만 먹음. 배불렀음.”
- Retrieved Context (검색 결과): “A블로거: 칼국수 존맛!”, “B블로거: 여기는 해물라면이 필수!”, “메뉴판: 모듬조개+칼국수 세트 인기”
AI는 이 두 가지 정보를 섞어서(Merge) 글을 작성하는데, “정보의 풍부함” 을 우선시하도록 튜닝된 모델은 검색 결과(Retrieved Context) 의 내용을 사용자 경험(User Context) 인 것처럼 서술해버립니다.
결국 사용자는 먹지도 않은 ‘해물라면’과 ‘칼국수’를 극찬하는, 거짓 후기(Hallucination) 가 생성되는 것이죠.
❌ 실제 실패 사례들
Case 1: 없는 메뉴 창작
- User Input: “조개구이 양이 많아서 다 못 먹고 남김.”
- AI Output (Hallucination): “마무리는 역시 바지락 칼국수죠! 🔥 배가 터질 것 같았지만, 뜨끈한 국물에 김치 한 점 올려 먹으니 속이 확 풀리더라고요. 오이도 조개92 오시면 배불러도 칼국수는 꼭 드셔야 해요!”
Case 2: 감정선 왜곡
- User Input: “커피 맛이 별로였음. 다시는 오지 않을듯.”
- AI Output (Hallucination): “처음엔 쓴맛이 강했지만, 시간이 지나면서 은은한 과일향이 올라오더라고요. 이게 바로 스페셜티 커피의 매력이죠!”
Case 3: 방문 목적 조작
- User Input: “친구 만나러 갔다가 카페에서 잠깐 쉼.”
- AI Output (Hallucination): “평소 이 카페의 시그니처 메뉴가 궁금했는데, 드디어 방문할 기회가 생겼습니다! 30분을 기다린 보람이…”
이처럼 검색 결과(남들의 후기)에 압도되어, 사용자의 실제 경험을 정면으로 왜곡하거나 반박하는 심각한 오류가 발생했습니다.
시스템 아키텍처: 문제의 근원
개선 전: Context가 평등하게 취급됨
User Input → LLM ← Tavily Search Results
↓ ↓
[동일한 weight로 처리]
↓
Generated Blog Post
(검색 결과가 사용자 경험을 덮어씀)문제의 핵심은 두 정보 소스가 동등하게 취급된다는 점이었습니다. LLM 입장에서는 “사용자가 입력한 짧은 메모”와 “웹에서 수집한 풍부한 정보” 중 후자가 더 구체적이고 완성도 높아 보이기 때문에, 자연스럽게 검색 결과에 더 의존하게 됩니다.
개선 후: 명확한 정보 위계 설정
User Input (Truth Source)
↓
[LLM]
↓ (보조 참조만)
Tavily API (Metadata Only)
↓
Generated Blog Post
(사용자 경험 중심 + 정확한 메타데이터)개선 후에는 User Input을 절대적 진실 로 설정하고, Tavily 검색 결과는 메뉴명 표기, 가격 등 객관적 사실 검증용 으로만 제한 했습니다.
해결 전략: Source of Truth 분리 - 사용자 경험을 최우선으로
이 문제를 해결하기 위해, 프롬프트 엔지니어링 단계에서 “어떤 정보를 신뢰할 것인가”의 우선순위를 명확히 정의해야 했습니다.
1. Negative Constraints (부정 제약 조건) 추가
LLM은 “하지 마라”는 부정 명령보다 “해라”는 긍정 명령을 더 잘 따르는 경향이 있지만,
이번 경우에는 명시적인 Negative Constraints가 필수적이었습니다.
2. Role Separation (역할 분리)
검색 결과(Search Result)와 사용자 초안(User Draft)의 역할을 엄격하게 분리했습니다.
- User Draft: Content Source (글의 내용, 줄거리, 감정선)
- Search Result: Metadata Source (가격, 정확한 메뉴명, 주소, 영업시간) 프롬프트 내에서 이 두 섹션을 물리적으로 멀리 떨어뜨리고, 각 섹션의 용도를 명시했습니다.
[3. Store Information]
*Use this ONLY for objective facts (Spelling, Prices).*
*DO NOT use this to infer what the user ate.*
[4. User Draft]
*This is the ABSOLUTE TRUTH.*
*Only write about events and food mentioned here.*3. Micro-Expansion (미시적 확장)
기존에는 분량을 늘리기 위해 “새로운 사건”을 추가하려는 경향이 있었습니다.
이를 방지하기 위해 “수평적 확장(사건 추가)” 이 아닌 “수직적 확장(묘사 심화)” 을 하도록 유도했습니다.
- Good: “조개 하나를 집어 들었다. 껍질 속에 고인 뽀얀 국물이…” (있는 사건의 디테일 강화)
정량적 성과: 할루시네이션 70% → 15% 감소
이러한 프롬프트 엔지니어링 적용 전후를 비교하기 위해, 총 20개의 테스트 케이스(짧은 초안 10개, 긴 초안 10개) 를 무작위로 선정하여 내부 테스트를 진행했습니다.
| 구분 | 개선 전 (Before) | 개선 후 (After) | 비고 |
|---|---|---|---|
| 할루시네이션 발생률 | 70% (14/20) | 15% (3/20) | 55%p 감소 |
| 치명적 오류 (없는 메뉴 창조) | 12건 | 0건 | 칼국수, 볶음밥 등 |
| 경미한 오류 (과장된 표현) | 2건 | 3건 | 맛 표현의 과장 등 |
결과적으로 ‘없는 메뉴를 지어내는’ 치명적인 할루시네이션은 0건으로 완전히 제거되었으며,
전체적인 오류 발생률도 1/4 수준으로 급감했습니다.
남은 15%의 오류 또한 “너무 맛있다”를 “눈물이 날 정도로 맛있다”로 표현하는 정도의 뉘앙스 차이로, 블로그 글의 품질에는 큰 영향을 주지 않는 수준이었습니다.
실제 사용자 피드백: “내가 쓴 것 같다”
내부 테스터 5명에게 개선 전/후 버전을 블라인드 테스트로 진행한 결과:
정성적 피드백
개선 전 (Before)
- “칼국수를 안 먹었는데 먹었다고 나와서 처음부터 다시 썼어요.” - Tester A
- “내가 쓴 글 같지 않고, 어디선가 복붙한 느낌.” - Tester B
- “수정할 부분이 너무 많아서 그냥 새로 쓰는 게 빠를 것 같았어요.” - Tester C
개선 후 (After)
- “80% 정도는 그대로 쓸 수 있어요. 진짜 내가 쓴 초안 같음.” - Tester A
- “약간의 표현만 다듬으면 바로 발행 가능한 수준.” - Tester D
- “이상한 내용이 거의 없어서 검토 시간이 확 줄었어요.” - Tester E
정량적 변화
| 지표 | 개선 전 | 개선 후 | 개선폭 |
|---|---|---|---|
| ”내가 쓴 것 같다” 응답률 | 20% (1/5) | 80% (4/5) | +60%p |
| 글 당 평균 오류 발견 건수 | 4.2건 | 0.6건 | -86% |
| 수정 소요 시간 | 평균 8분 | 평균 2분 | -75% |
| “처음부터 다시 작성” 선택률 | 40% (2/5) | 0% (0/5) | -40%p |
특히 “먹지 않은 메뉴가 등장해서 처음부터 다시 써야 했다” 는 치명적인 불만이 완전히 사라진 것이 가장 큰 성과였습니다.
기술적 고민: RAG를 제거할 것인가, 통제할 것인가?
개발 과정에서 가장 큰 기로는 “RAG 자체를 제거해야 하는가?” 였습니다.
검토한 두 가지 대안
Option 1: RAG 완전 제거
장점
- 할루시네이션 원천 차단
- 시스템 단순화 (Tavily API 비용 절감)
- 응답 속도 향상
단점
- 메뉴명 오타 발생 (예: “까눌레” vs “카눌레”)
- 가격 정보 누락 (사용자가 기억 못함)
- 주소/영업시간/링크 등 메타데이터 수집 불가
- 폐업한 매장 감지 못함
Option 2: RAG 유지 + 엄격한 제약
장점
- Tavily로 최신 정보 자동 수집 (폐업, 가격 변동 등)
- 정확한 메뉴명/가격/주소 보정
- 사용자는 핵심 경험만 입력하면 됨
단점
- Context Contamination 리스크
- 프롬프트 복잡도 증가
- API 비용 발생
최종 선택: “정교한 통제” (Option 2)
결국 RAG를 유지하되, 정보 우선순위를 엄격히 통제하는 방식 을 선택했습니다.
이유는 다음과 같습니다:
- 사용자 편의성 : “오이도 조개구이”라고만 입력해도 → 정확한 가게명, 주소, 가격이 자동 추가
- 정보 신뢰성 : Tavily로 실시간 정보 검증 (폐업 여부, 메뉴 변경 등)
- 시스템 가치 : RAG 없이는 “좀 더 그럴싸하게 써주는 GPT”에 불과함
대신 프롬프트 엔지니어링으로 “검색 결과는 Fact-checking 용도로만” 이라는 원칙을 철저히 구현했습니다.
개발자를 위한 체크리스트
이번 경험을 통해 얻은, 개인화 서비스에 RAG 적용 시 검토 사항 을 정리 해보았습니다.
1. Context Source의 위계가 명확한가?
- User Input과 Retrieved Context의 우선순위가 프롬프트에 명시되어 있는가?
- LLM이 두 소스를 구분할 수 있도록 물리적으로 분리했는가?
2. Negative Constraints가 충분한가?
- “~하지 마라” 형태의 제약이 “~해라”만큼 강조되었는가?
- 특히 “없는 내용을 만들지 마라”가 CRITICAL로 표시되었는가?
3. 검색 결과의 용도가 제한되어 있는가?
- Retrieved Context를 Content 생성이 아닌 Metadata 검증용으로만 사용하는가?
- 프롬프트에서 각 정보의 “용도(Purpose)“를 명시했는가?
4. Edge Case 테스트를 했는가?
- “아무것도 먹지 않음” 같은 극단적 입력을 테스트했는가?
- “검색 결과는 풍부하지만 사용자 입력은 한 줄”인 경우를 검증했는가?
5. 실제 사용자 테스트를 거쳤는가?
- 내부 개발 데이터가 아닌 실제 사용자 입력으로 검증했는가?
- 정량적 지표(할루시네이션 건수)뿐 아니라 정성적 피드백(“내가 쓴 것 같다”)도 수집했는가?
결론: 자동화와 진정성 사이의 균형
RAG는 강력하지만, ‘개인의 경험’을 다루는 영역에서는 매우 조심스럽게 사용해야 합니다. 데이터의 정확성(Accuracy) 뿐만 아니라, 그 데이터가 누구의 것(Ownership) 인지 구분하는 것이 중요합니다.
이번 개선을 통해, AI는 더 이상 ‘아무 말 대잔치’를 하지않고, 사용자의 경험을 충실히 보조하는 진정한 Ghostwriter로 거듭날 수 있었습니다.