LangChain 챗봇6장 · 문서 기반 챗봇12 / 21← 이전목차다음 →MetaContents

6장. 문서 기반 챗봇

근거 보여 주기와 모르면 모른다고 답하기

약국에서 약사에게 "이 약 같이 먹어도 돼요?"라고 물으면, 좋은 약사는 "설명서의 이 부분에 적혀 있습니다"라고 짚어 주고, 설명서에 없는 것은 "의사에게 물어보세요"라고 합니다. 모르는 것을 그럴듯하게 지어내지 않습니다. 약관 챗봇도 이렇게 답해야 합니다. 답 아래에 근거 조항과 쪽을 붙이고, 약관에 없는 질문에는 "약관에서 찾지 못했습니다"라고 답합니다.

검색하기, 가장 가까운 거리 확인하기, 근거를 넣어 묻기, 답과 출처 보여 주기의 네 걸음을 이은 흐름도

네 상자가 근거를 챙기는 챗봇의 차례입니다. 둘째 상자에서 가장 가까운 조각의 거리가 문턱보다 멀면 모델을 부르지 않고 바로 "찾지 못했습니다"로 끝낼 수 있습니다. 셋째 상자의 프롬프트에는 "근거에 없으면 모른다고 답하라"는 규칙을 적고, 넷째 상자에서 검색한 조각의 조항과 쪽을 답 아래에 붙입니다. 여기서 설명서의 그 부분이 근거 조각, "의사에게 물어보세요"가 "약관에서 찾지 못했습니다"입니다. 비유와 다른 점은, 약사는 설명서에 없다는 것을 스스로 알지만 챗봇은 거리 문턱과 프롬프트 규칙이라는 두 겹의 장치로 막아야 한다는 것입니다. 둘 다 완벽하지 않습니다.

모르면 모른다고 답하게 하는 두 장치

장치하는 일좋은 점약한 점
거리 문턱가장 가까운 조각도 멀면 모델을 부르지 않는다요금이 들지 않는다. 지어낼 틈이 없다알맞은 문턱을 정하기 어렵다. 맞는 질문도 막을 수 있다
프롬프트 규칙"근거에 없으면 '약관에서 찾지 못했습니다'라고만 답하라"뜻을 보고 판단한다모델이 규칙을 어길 때가 있다
출처 붙이기답 아래에 조항과 쪽을 적는다손님이 원문을 확인할 수 있다검색이 빗나가면 엉뚱한 출처가 붙는다

짧은 예시

조항 네 개짜리 벡터DB 에서 거리 문턱 1.75 를 두고, 약관에 있는 질문과 없는 질문을 넣어 봅니다. 모델은 부르지 않고 "모델을 부를지"만 정합니다. 첫 블록은 앞 절과 같은 준비입니다.

Python
# 준비: "FAISS 벡터DB 에 저장하고 검색하기" 절과 같은 연습용 임베딩과 조항 네 개짜리 벡터DB
import zlib

from langchain_community.vectorstores import FAISS
from langchain_core.documents import Document
from langchain_core.embeddings import Embeddings


class BigramEmbeddings(Embeddings):
    """글자 두 개 묶음(바이그램)을 세어 숫자 목록을 만드는 연습용 임베딩"""

    def __init__(self, size=512):
        self.size = size

    def _vector(self, text):
        text = text.replace(" ", "").replace("\n", "")
        vec = [0.0] * self.size
        for i in range(len(text) - 1):
            # 글자 두 개를 crc32 로 0~511 사이의 칸 번호로 바꿔 그 칸을 1 올린다
            vec[zlib.crc32(text[i:i + 2].encode("utf-8")) % self.size] += 1.0
        norm = sum(v * v for v in vec) ** 0.5 or 1.0
        return [v / norm for v in vec]

    def embed_documents(self, texts):
        return [self._vector(t) for t in texts]

    def embed_query(self, text):
        return self._vector(text)


emb = BigramEmbeddings()
docs = [
    Document("제5조 출발 10일 전까지 취소하면 전액 돌려준다.", metadata={"article": "제5조"}),
    Document("제10조 국내선 상품은 15kg 짐 한 개가 포함된다.", metadata={"article": "제10조"}),
    Document("제12조 반려동물은 7kg 이하의 개와 고양이만 된다.", metadata={"article": "제12조"}),
    Document("제14조 여권의 남은 기간이 6개월 이상이어야 한다.", metadata={"article": "제14조"}),
]
db = FAISS.from_documents(docs, emb)
Python
THRESHOLD = 1.75     # 가장 가까운 조각의 거리가 이보다 크면 "찾지 못함"


def check(question):
    doc, score = db.similarity_search_with_score(question, k=1)[0]
    score = round(float(score), 3)
    if score > THRESHOLD:
        return f"찾지 못함 (가장 가까운 {doc.metadata['article']}, 거리 {score})"
    return f"모델에 물음 (근거 {doc.metadata['article']}, 거리 {score})"


for q in ["출발 10일 전에 취소하면 돌려받나요?", "환율이 얼마예요?", "강아지 데려가도 돼요?"]:
    print(q, "→", check(q))
출력
출발 10일 전에 취소하면 돌려받나요? → 모델에 물음 (근거 제5조, 거리 1.018)
환율이 얼마예요? → 찾지 못함 (가장 가까운 제5조, 거리 1.835)
강아지 데려가도 돼요? → 찾지 못함 (가장 가까운 제5조, 거리 1.855)

약관에 있는 취소 질문은 제5조가 거리 1.018 로 가까워 모델에 묻기로 했습니다. "환율"은 가장 가까운 조각도 1.835 로 멀어서 모델을 부르지 않고 "찾지 못함"으로 끝냈습니다. 그런데 "강아지" 질문도 1.855 로 멀어 함께 막혔습니다. 약관에 답이 있는데도, 연습용 임베딩이 "강아지"와 "반려동물"을 잇지 못해서입니다. 거리 문턱은 검색이 잘 될 때만 믿을 만한 장치라는 뜻입니다. 진짜 임베딩으로 바꾼 뒤의 모습은 실습 5에서 봅니다.

코드가 의미하는 것

이 코드는 약사가 설명서를 펴 보고 "여기 있다"와 "없다"를 가르는 첫 판단만 떼어 낸 것입니다. 첫 블록은 앞 절과 같은 벡터DB 준비이고, 둘째 블록이 가장 가까운 조각 하나의 거리를 문턱과 견줘 모델을 부를지 정합니다.

코드하는 일챗봇에서 뜻하는 것
THRESHOLD = 1.75거리 문턱을 정한다"이보다 멀면 근거가 없다"고 보는 선입니다. 연습용 임베딩에 맞춰 고른 값이라 임베딩이 바뀌면 다시 정합니다
similarity_search_with_score(question, k=1)[0]가장 가까운 조각 하나와 거리를 받는다근거 후보 가운데 1등의 확실성을 봅니다. 1등도 멀면 나머지는 더 멉니다
if score > THRESHOLD: return "찾지 못함 …"문턱보다 멀면 모델을 부르지 않고 끝낸다모델에게 지어낼 기회를 주지 않는 첫째 장치입니다. 요금도 들지 않습니다
doc.metadata['article']근거 조각의 조항 번호를 꺼낸다출처를 모델이 아니라 우리 코드가 붙이는 자리입니다. 없는 조항 번호가 나올 수 없습니다

결과에서 읽을 것은 두 가지입니다.

  • 문턱은 "없는 질문"을 막았습니다. "환율"은 거리 1.835 로 문턱 1.75 를 넘어 모델을 부르지 않았습니다. 약관에 없는 질문에 지어낸 답이 나갈 길이 막힌 것입니다.
  • 문턱은 "있는 질문"도 막았습니다. "강아지" 질문은 1.855 로 함께 막혔습니다. 문턱이 아무리 알맞아도 임베딩이 뜻을 못 이으면 맞는 질문까지 거절합니다. 문턱의 성능은 검색의 성능을 넘지 못합니다.

주의할 점

  • 거리 문턱은 임베딩마다 다릅니다. 연습용 임베딩에서 고른 1.75 를 진짜 임베딩에 그대로 쓰면 안 됩니다. 평가 질문을 모아 놓고 맞는 질문과 없는 질문의 거리를 견줘 정합니다.
  • 출처는 모델에게 쓰게 하지 말고, 검색한 조각의 metadata 에서 우리 코드가 붙입니다. 모델에게 맡기면 없는 조항 번호를 지어낼 수 있습니다.
  • "모른다"는 답도 좋은 답입니다. 약관 챗봇이 지어낸 환불 규정을 안내하면, 아무 답도 안 하는 것보다 손님과 회사 모두에게 해롭습니다.
← 이전 절6장 · RetrievalQA 와 LCEL 로 답하기다음 절 →6장 · 실습 지도