LangChain 챗봇8장 · 최종 프로젝트5 / 12← 이전목차다음 →MetaContents

8장. 최종 프로젝트

1~7장의 부품을 한 앱으로 묶는 짜임

조립식 가구를 사면 상자 안에 판자, 나사, 경첩이 따로 들어 있고 설명서가 그 차례를 알려 줍니다. 판자는 같아도 문짝만 유리로 바꿔 끼울 수 있습니다. 챗봇 앱도 1~7장에서 만든 부품을 차례대로 끼워 맞춘 것입니다. 문서를 읽고, 조각으로 나누고, 임베딩해서 색인에 넣고, 체인으로 답을 만들고, 화면에 보여 줍니다. 임베딩과 모델 자리는 연습용과 진짜를 바꿔 끼울 수 있게 비워 둡니다.

문서 읽기, 조각 나누기, 임베딩, FAISS 색인, 체인과 화면의 다섯 부품을 이은 흐름도

다섯 상자가 문서 챗봇 앱의 부품이고, 상자 아래에 그 부품을 만든 장을 적었습니다. PyPDFLoader 로 문서를 읽고(1, 6장), 조항이나 노트 단위로 나누고(2, 6장), BigramEmbeddings 나 OpenAIEmbeddings 로 숫자 목록을 만들고(3, 6장), FAISS 색인에 넣어 가까운 조각을 찾고(4, 6장), 프롬프트와 모델을 이은 체인으로 답을 만들어 Streamlit 화면에 보여 줍니다(5, 3·7장). 여기서 판자가 문서와 조각, 설명서의 차례가 화살표, 유리로 바꿔 끼우는 문짝이 임베딩과 모델입니다. 비유와 다른 점은, 가구는 다 맞추면 끝이지만 챗봇은 부품 하나를 바꿀 때마다 평가를 다시 돌려 나빠지지 않았는지 본다는 것입니다.

부품과 바꿔 끼울 것

부품키 없이(연습)키가 있으면(진짜)배운 장
문서 읽기PyPDFLoader같다6장
나누기조항·노트 단위 re.split같다6장
임베딩BigramEmbeddings(글자 두 개 묶음)OpenAIEmbeddings(model=EMBED_MODEL)6장
색인FAISS.from_documents같다(저장해 두면 다시 임베딩하지 않는다)6장
답하는 모델FakeListChatModelChatOpenAI(model=MODEL, temperature=0)1·2장
기억st.session_state 의 기록같다4·7장
도구@tool 함수를 직접 부르기create_agent 가 골라 부른다5장
화면AppTest 로 시험streamlit run 으로 브라우저에서7장

파일 두 개로 나누기

7장에서 본 것처럼 화면 코드와 챗봇 로직을 떼어 둡니다. 프로젝트 1과 3은 파일을 두 개 만듭니다.

파일담는 것시험하는 법
부품 파일(out_ch08_rules_bot.py)조각 나누기, 임베딩, 검색, 문턱, 답하기 함수보통 파이썬으로 불러 평가 질문을 돌린다
화면 파일(out_ch08_rules_app.py)제목, 사이드바, 말풍선, 입력창, 캐시AppTest 로 첫 화면과 질문 몇 개를 넣는다

짧은 예시

문서 → 색인 → 체인을 함수 하나로 꾸리고, 임베딩과 모델은 밖에서 받게 합니다. 키 없이 연습용 부품으로 돌려 봅니다.

Python
import zlib

from langchain_community.vectorstores import FAISS
from langchain_core.embeddings import Embeddings
from langchain_core.language_models.fake_chat_models import FakeListChatModel
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate


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)


def make_bot(texts, emb, llm):
    """문서 → 색인 → 체인을 한 번에 꾸려 ask 함수를 돌려준다. 임베딩과 모델은 밖에서 받는다"""
    db = FAISS.from_texts(texts, emb)
    prompt = ChatPromptTemplate.from_messages([
        ("system", "아래 근거로만 답한다.\n[근거]\n{context}"),
        ("human", "{question}"),
    ])
    chain = prompt | llm | StrOutputParser()

    def ask(question):
        doc = db.similarity_search(question, k=1)[0]
        return {"answer": chain.invoke({"context": doc.page_content, "question": question}), "source": doc.page_content}

    return ask


texts = ["재택근무는 한 주에 이틀까지 할 수 있다.", "국내 출장의 하루 일비는 3만 원이다.", "연차 휴가는 3일 전까지 신청한다."]
# 키 없이: 연습용 임베딩 + 가짜 모델
ask = make_bot(texts, BigramEmbeddings(), FakeListChatModel(responses=["(가짜 모델) 한 주에 이틀까지입니다."]))
print(ask("재택근무는 한 주에 며칠까지 돼요?"))
# 키가 있으면 부품 두 개만 바꿔 끼운다(여기서는 부르지 않는다)
# ask = make_bot(texts, OpenAIEmbeddings(model=EMBED_MODEL), ChatOpenAI(model=MODEL, temperature=0))
출력
{'answer': '(가짜 모델) 한 주에 이틀까지입니다.', 'source': '재택근무는 한 주에 이틀까지 할 수 있다.'}

근거(source)는 연습용 임베딩이 실제로 찾은 조각이고, 답(answer)은 가짜 모델에 미리 적어 둔 글입니다. make_bot 의 안쪽은 그대로 두고 맨 아래 주석 줄처럼 임베딩과 모델만 바꾸면 진짜 챗봇이 됩니다. 이 교안은 gpt-4.1-mini 를 기본으로 하고, 교수자가 정한 모델이 있으면 .env 의 OPENAI_MODEL 로 바꿉니다.

코드가 의미하는 것

이 코드는 조립식 가구의 틀을 짜 두고 문짝만 바꿔 끼우게 만든 것입니다. 연습용 임베딩을 만들고, 문서·색인·체인을 함수 하나(make_bot)로 묶고, 연습용 부품을 끼워 한 번 묻는 세 동작입니다.

코드하는 일챗봇에서 뜻하는 것
class BigramEmbeddings(Embeddings)글자 두 개 묶음을 세어 숫자 목록을 만든다LangChain 의 Embeddings 꼴(embed_documents, embed_query)만 맞추면 FAISS 가 진짜 임베딩처럼 받아 씁니다. 그래서 키 없이 색인을 만들 수 있습니다
def make_bot(texts, emb, llm)임베딩과 모델을 인자로 받는다문짝 자리(임베딩, 모델)를 밖으로 빼 둔 것입니다. 안쪽 코드를 고치지 않고 연습용과 진짜를 바꿔 끼웁니다
FAISS.from_texts(texts, emb)글 목록을 임베딩해 색인에 넣는다6장의 색인 만들기입니다. make_bot 을 부를 때 한 번 만들고, 안쪽의 ask 가 계속 씁니다
prompt | llm | StrOutputParser()프롬프트, 모델, 글 꺼내기를 잇는다3장의 체인입니다. 근거({context})와 질문을 받아 답 글을 돌려줍니다
{"answer": …, "source": doc.page_content}답과 근거를 함께 돌려준다근거를 모델이 아니라 코드가 붙여 돌려주므로, 무엇을 보고 답했는지 늘 확인할 수 있습니다

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

  • 근거는 실제로 찾은 것입니다. source 의 "재택근무는 한 주에 이틀까지 할 수 있다."는 연습용 임베딩이 세 문장 가운데 실제로 고른 조각입니다.
  • 답은 미리 적어 둔 글입니다. answer 의 "(가짜 모델) 한 주에 이틀까지입니다."는 FakeListChatModel 에 넣어 둔 글이라 질문과 상관없이 나옵니다. 이 예시가 확인하는 것은 답의 내용이 아니라 부품이 맞물려 돈다는 것입니다.

주의할 점

  • 연습용 임베딩으로 만든 색인과 진짜 임베딩으로 만든 색인은 섞어 쓸 수 없습니다. 임베딩을 바꾸면 색인을 새로 만들고, 저장한 색인 폴더의 이름도 따로 둡니다(out_ch08_notes_faiss).
  • BigramEmbeddings 코드는 6·7장의 코드와 글자 하나 다르지 않게 씁니다. 크기 512 와 crc32 를 바꾸면 같은 문서에서도 검색 결과가 달라져 다른 장의 결과와 견줄 수 없습니다.
← 이전 절8장 · 설계서 쓰기다음 절 →8장 · 평가표: 적중률과 모르면 모른다