RAG2026-5일25차시 · Generator Node 분리15 / 19← 이전목차다음 →MetaContents

25차시. Generator Node 분리

실습 1 — Generator Node 분리 실습 따라 하기

관련 수업 자료: 25차시 「5. 실습 ② — 단독 테스트」(10~11쪽), 25차시 「3. 실습 ① — Generator Node 완성」(7쪽) · 실습 코드 25/generator.py, 25/generator2.py

이 실습은 교안 해설(25_GeneratorNode분리.md)의 실습 순서를 옮긴 것입니다. OpenAI 임베딩이나 채팅 모델을 부르는 파일은 .env 에 API 키가 있어야 돌아갑니다. 키가 없으면 실행 결과 샘플로 출력 모양을 확인합니다.

수업 자료와 이어지는 곳

실습 코드원본 PDF이어지는 내용
25/generator.py10~11쪽25차시 「5. 실습 ② — 단독 테스트」 아래에 실었습니다
25/generator2.py7쪽25차시 「3. 실습 ① — Generator Node 완성」 아래에 실었습니다

실행 흐름

출력
근거가 있나 → LLM 호출 안 함  (없다)
근거가 있나 → V3 프롬프트  (있다)
V3 프롬프트 → 인용 검사
인용 검사 → answer insufficient has citation

실습 순서

근거 리스트가 빈 State를 만들어 generate가 LLM을 타지 않는 분기를 코드에서 찾습니다. 재시도 분기에 실패 이유가 문자열로 들어가는지 generator2.py에서 확인합니다.

스스로 확인

  • 생성 노드를 번역기로 말할 수 있는가?
  • 근거 0건에 모델을 부르지 않는 이유를 비용과 환각으로 말하는가?
  • 형식 검사와 내용 검사가 어느 노드인지 나누는가?

코드 설명

  • chain = prompt | _llm | StrOutputParser() 는 프롬프트, 모델, 출력 변환기를 한 줄로 잇습니다. chain.invoke({"context": ..., "question": ...}) 에 넘기는 딕셔너리 키는 프롬프트 안의 {context}, {question} 칸 이름과 같아야 합니다. 재시도 프롬프트는 {reason} 칸이 하나 더 있어 payload 에 reason 을 넣습니다.
  • build_context 는 enumerate(documents, start=1) 로 근거마다 [1] 파일명 p.페이지 머리를 붙입니다. 이 번호가 있어야 LLM 이 답에 [1] 을 달 수 있고, check_citation 이 그 번호를 검사할 수 있습니다.
  • re.findall(r"\[(\d+)\]", answer) 는 답에서 [숫자] 꼴을 모두 찾아 숫자 부분만 문자열로 돌려줍니다. 그래서 int(number) 로 바꾼 뒤 1 <= n <= 문서 개수 범위를 봅니다. 근거가 2개인데 [3] 이 나오면 환각 신호로 보고 has_citation 을 False 로 둡니다.
  • insufficient = NO_INFO in answer 는 답에 "자료에서 확인할 수 없습니다" 가 들어 있는지만 봅니다. 프롬프트가 시키는 문구와 NO_INFO 가 글자 하나라도 다르면 insufficient 가 항상 False 가 됩니다.
  • if not documents: 블록은 LLM 을 부르기 전에 돌아갑니다. 로그에 "근거 없음 - LLM 호출 생략" 이 남으면 비용이 들지 않은 것입니다. LLM 호출은 try 로 감싸 오류가 나도 gen_error 를 적고 정상 반환합니다.
  • generator2.py 는 from prompts import PROMPTS, RETRY_PROMPT 로 재시도 틀을 따로 가져옵니다. 원본 PDF 처럼 PROMPTS["retry"] 에 넣으면 13차시 비교 코드가 {reason} 없이 모든 틀을 돌리다 멈춥니다. 여기서 많이들 헷갈리십니다.
← 이전 절25차시 · 체크리스트와 과제다음 절 →25차시 · 퀴즈 1 — 재시도 여부로 프롬프트 고르기