실무 Multi-Agent 오케스트레이션 14장 · 근거 기반 답변 2 / 7 ← 이전목차다음 → TechLead Cro

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가 크면 비용도 큽니다.
← 이전 절왜 근거 기반 답변이 필요한가 — 찾은 것을 그냥 붙이면 안 되는 이유다음 절 →컨텍스트 주입과 출처 꼬리표 — 발췌를 어떻게 넣을 것인가
오명운 · macro@prag-ai.com