14장. 근거 기반 답변
Retriever — 검색을 부품으로 만든다
한 줄 요약
Retriever(리트리버) 는 "질문을 넣으면 관련 문서 k개가 나오는" 일을 하나의 부품으로 감싼 것입니다. 13장에서 만든 chroma_db/를 불러오기만 하고, 그 위에 Retriever를 얹습니다. 이번 장부터는 색인을 다시 하지 않습니다.
1. 저장된 벡터 DB 불러오기
13장에서는 Chroma.from_documents(...)로 청크를 넣었습니다. 이번 장은 넣지 않고 엽니다.
CHROMA_DIR = ROOT / "chroma_db"
NEED_13 = "13장 lesson13_rag_embedding.py 를 먼저 실행하세요."
if not CHROMA_DIR.exists():
raise SystemExit(f"[준비 필요] chroma_db/ 폴더가 없습니다. {NEED_13}")
vectorstore = Chroma(
collection_name="haru_policy",
embedding_function=embeddings,
persist_directory=str(CHROMA_DIR),
)
if not vectorstore.get(limit=1)["ids"]: # 폴더는 있는데 저장된 청크가 0개
raise SystemExit(f"[준비 필요] chroma_db/ 에 정책 문서가 없습니다. {NEED_13}")
| 13장 (색인) | 14장 (불러오기) |
|---|---|
Chroma.from_documents(documents=..., embedding=...) |
Chroma(embedding_function=...) |
| 청크 12개를 임베딩해 넣는다 | 이미 들어 있는 것을 연다 |
| 임베딩 API를 청크 수만큼 부른다 | 여는 데는 임베딩 API를 부르지 않는다 |
세 가지가 13장과 같아야 합니다.
| 같아야 하는 것 | 다르면 |
|---|---|
폴더 chroma_db |
13장에서 저장한 청크를 찾지 못합니다 |
컬렉션 이름 haru_policy |
13장에서 저장한 청크를 찾지 못합니다 |
| 임베딩 모델 | 검색은 되지만 거리가 의미 없는 값이 됩니다 (13장의 "같은 지도" 규칙) |
여는 앞뒤의 확인 두 줄
Chroma(...)는 폴더가 없으면 빈 폴더를 새로 만듭니다. 그러면 검색할 청크가 없는 채로 프로그램이 계속 돕니다. 그래서 여는 앞뒤로 확인을 하나씩 둡니다.
| 확인 | 언제 | 무엇을 보나 |
|---|---|---|
CHROMA_DIR.exists() |
Chroma(...)를 부르기 전 |
폴더가 있는가. 부른 뒤에는 폴더가 이미 만들어져 있어 확인할 수 없습니다 |
vectorstore.get(limit=1)["ids"] |
연 뒤 | 저장된 청크가 하나라도 있는가. get(limit=1)은 저장된 항목을 하나만 꺼내 달라는 뜻입니다 |
어느 쪽이든 비어 있으면 raise SystemExit(...)으로 안내 문구를 찍고 멈춥니다. 13장을 먼저 실행하라는 안내입니다.
2. Retriever로 감싸기
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
docs = retriever.invoke("단순 변심인데 반품 배송비 얼마예요?") # Document 4개
| 코드 | 뜻 |
|---|---|
as_retriever(...) |
벡터 DB를 Retriever로 감쌉니다 |
search_kwargs={"k": 4} |
검색할 때 쓸 설정. 여기서는 4개를 가져오라는 뜻 |
retriever.invoke(질문) |
질문을 넣으면 Document 목록이 나옵니다 |
13장에서는 vectorstore.similarity_search_with_score(q, k=2)를 직접 불렀습니다. 하는 일은 같습니다. 다른 점은 둘입니다.
- Retriever는 거리 점수를 돌려주지 않습니다.
Document목록만 줍니다. - 부르는 방법이
invoke(질문)하나로 통일됩니다.
3. 굳이 한 번 감싸는 이유
11장에서 모델을 LangChain으로 감쌌을 때와 같은 이유입니다. 부르는 쪽 코드를 바꾸지 않고 안쪽을 갈아 끼울 수 있게 하려는 것입니다.
"질문 → 관련 문서"를 해 주는 방법은 벡터 검색 말고도 있습니다. 단어로 찾는 검색도 있고, 둘을 합친 검색도 있습니다(15장). 어느 것이든 invoke(질문)으로 부를 수 있으면, 답변을 만드는 쪽 코드는 검색 방법이 무엇인지 몰라도 됩니다.
| 감싸지 않으면 | 감싸면 |
|---|---|
답변 코드가 similarity_search...를 직접 부른다 |
답변 코드는 retriever.invoke(질문)만 부른다 |
| 검색 방법을 바꾸면 답변 코드도 고친다 | 검색 방법을 바꿔도 답변 코드는 그대로다 |
4. 왜 k=4인가
13장의 검색 결과를 떠올려 봅니다. 1위와 2위의 거리 차이가 0.003밖에 안 되는 질문이 있었습니다. 순위가 근소한 차이로 정해지니, 1위 하나만 믿기는 어렵습니다.
| k | 일어나는 일 |
|---|---|
| 1 | 정답 청크가 2위로 밀리면 답할 근거가 없어집니다 |
| 4 | 2~3위에 있는 정답 청크도 프롬프트에 들어갑니다 |
| 12 | 전부 넣는 것과 같습니다. 검색을 한 의미가 없고, 입력 토큰이 그만큼 늡니다 |
4는 청크가 12개인 지금 문서에 맞춘 값입니다. 정답이 있는 숫자가 아니라 문서가 커지면 다시 실험해서 정하는 값입니다.
k를 늘리면 모델이 읽을 글이 늘어납니다. 청크 하나가 500자 안팎이므로 k=4면 질문마다 2,000자 가까운 발췌가 프롬프트에 들어갑니다. k는 곧 비용입니다.
핵심 정리
- 이번 장부터는
chroma_db/를 불러오기만 합니다. 색인은 13장에서 끝났습니다. - 폴더, 컬렉션 이름, 임베딩 모델이 13장과 같아야 합니다. 저장소가 비어 있으면 안내를 찍고 멈추도록 확인을 넣었습니다.
- Retriever는
invoke(질문)→Document목록이라는 통일된 모양으로 검색을 감쌉니다. - 감싸 두면 검색 방법을 바꿔도 답변 코드를 고치지 않아도 됩니다.
- k=4는 정답 청크가 2~3위에 있어도 놓치지 않기 위한 여유입니다. k가 크면 비용도 큽니다.