[블로그 톤 기반 리뷰 생성 도구] RAG: 검색 결과가 독이 될 때

사이드프로젝트를 진행 일지, 두 번째, RAG는 만능이 아니다!

사이드프로젝트
Side Project, AI, RAG, LLM

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를 유지하되, 정보 우선순위를 엄격히 통제하는 방식 을 선택했습니다.

이유는 다음과 같습니다:

  1. 사용자 편의성 : “오이도 조개구이”라고만 입력해도 → 정확한 가게명, 주소, 가격이 자동 추가
  2. 정보 신뢰성 : Tavily로 실시간 정보 검증 (폐업 여부, 메뉴 변경 등)
  3. 시스템 가치 : 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로 거듭날 수 있었습니다.

관련 글