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.py | 10~11쪽 | 25차시 「5. 실습 ② — 단독 테스트」 아래에 실었습니다 |
25/generator2.py | 7쪽 | 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}없이 모든 틀을 돌리다 멈춥니다. 여기서 많이들 헷갈리십니다.