์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿค– LangChain์œผ๋กœ ๋ฌธ์„œ ๊ธฐ๋ฐ˜ FAQ ์ฑ—๋ด‡ ๊ตฌํ˜„ํ•˜๊ธฐ: ๋‹น์‹ ์˜ ์ง€์‹์„ ๋˜‘๋˜‘ํ•œ AI๋กœ ๋ณ€์‹ ์‹œํ‚ค๋Š” ๋งˆ๋ฒ•

๐Ÿค– LangChain์œผ๋กœ ๋ฌธ์„œ ๊ธฐ๋ฐ˜ FAQ ์ฑ—๋ด‡ ๊ตฌํ˜„ํ•˜๊ธฐ: ๋‹น์‹ ์˜ ์ง€์‹์„ ๋˜‘๋˜‘ํ•œ AI๋กœ ๋ณ€์‹ ์‹œํ‚ค๋Š” ๋งˆ๋ฒ•

์‹ค์ „ ์˜ˆ์ œ๋กœ ๋ฐฐ์šฐ๋Š” ๋ฌธ์„œ ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ๊ฐœ๋ฐœ ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๐Ÿ’ฌ "๊ณ ๊ฐ ๋ฌธ์˜๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์•„์„œ ๋‹ต๋ณ€ํ•˜๊ธฐ ํž˜๋“ค์–ด์š”!", "์šฐ๋ฆฌ ํšŒ์‚ฌ ๋งค๋‰ด์–ผ์ด ๋„ˆ๋ฌด ๋ฐฉ๋Œ€ํ•ด์„œ ์ง์›๋“ค์ด ์ฐพ๊ธฐ ์–ด๋ ค์›Œํ•ด์š”!"

์ด๋Ÿฐ ๊ณ ๋ฏผ ํ•œ ๋ฒˆ์ฏค ํ•ด๋ณด์…จ์ฃ ? ์˜ค๋Š˜์€ ์ด ๋ชจ๋“  ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ด์ค„ LangChain ๊ธฐ๋ฐ˜ ๋ฌธ์„œ FAQ ์ฑ—๋ด‡์„ ํ•จ๊ป˜ ๋งŒ๋“ค์–ด๋ณผ ๊ฑฐ์˜ˆ์š”. ๋ณต์žกํ•ด ๋ณด์ด์ง€๋งŒ ์ฐจ๊ทผ์ฐจ๊ทผ ๋”ฐ๋ผ์˜ค์‹œ๋ฉด ์—ฌ๋Ÿฌ๋ถ„๋„ ์ถฉ๋ถ„ํžˆ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค! ๐Ÿš€

๐Ÿ“„ ๋ฌธ์„œ ๋ฐ์ดํ„ฐ PDF, TXT, DOCX ๋งค๋‰ด์–ผ, FAQ ์ง€์‹ ๋ฒ ์ด์Šค ๐Ÿ”ง LangChain ๋ฌธ์„œ ๋ถ„ํ•  ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ ๋ฒกํ„ฐ ์ €์žฅ ๐Ÿ’ฌ ์ฑ—๋ด‡ ์งˆ๋ฌธ ์ดํ•ด ๊ด€๋ จ ๋ฌธ์„œ ๊ฒ€์ƒ‰ ๋‹ต๋ณ€ ์ƒ์„ฑ ๐Ÿ‘ค ์‚ฌ์šฉ์ž ์งˆ๋ฌธ ์งˆ๋ฌธ ์ „์†ก โ†’ โ† ์ •ํ™•ํ•œ ๋‹ต๋ณ€

๐ŸŽฏ LangChain๊ณผ ๋ฌธ์„œ ๊ธฐ๋ฐ˜ ์ฑ—๋ด‡์ด ๋ญ”๊ฐ€์š”?

๋จผ์ € ๊ธฐ๋ณธ ๊ฐœ๋…๋ถ€ํ„ฐ ์นœ๊ตฌ์ฒ˜๋Ÿผ ํŽธํ•˜๊ฒŒ ์„ค๋ช…ํ•ด๋“œ๋ฆด๊ฒŒ์š”!

LangChain์€ ๋Œ€๊ทœ๋ชจ ์–ธ์–ด ๋ชจ๋ธ(LLM)์„ ํ™œ์šฉํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ๋„์™€์ฃผ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์˜ˆ์š”. ๋งˆ์น˜ ๋ ˆ๊ณ  ๋ธ”๋ก์ฒ˜๋Ÿผ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ๋“ค์„ ์กฐ๋ฆฝํ•ด์„œ AI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์ฃ . ๐Ÿงฑ

๋ฌธ์„œ ๊ธฐ๋ฐ˜ FAQ ์ฑ—๋ด‡์€ ์—ฌ๋Ÿฌ๋ถ„์ด ๊ฐ€์ง„ ๋ฌธ์„œ๋“ค(PDF, ์›Œ๋“œ, ํ…์ŠคํŠธ ํŒŒ์ผ ๋“ฑ)์„ ํ•™์Šตํ•ด์„œ, ์‚ฌ์šฉ์ž์˜ ์งˆ๋ฌธ์— ํ•ด๋‹น ๋ฌธ์„œ ๋‚ด์šฉ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋‹ต๋ณ€ํ•ด์ฃผ๋Š” ๋˜‘๋˜‘ํ•œ ๋น„์„œ ๊ฐ™์€ ์กด์žฌ์˜ˆ์š”. ์ผ๋ฐ˜ ์ฑ—๋ด‡๊ณผ ๋‹ค๋ฅธ ์ ์€ "ํ™˜๊ฐ(Hallucination)" ํ˜„่ฑก์ด ์ ๋‹ค๋Š” ๊ฑฐ์ฃ . ์ฆ‰, ์žˆ์ง€๋„ ์•Š์€ ๋‚ด์šฉ์„ ์ง€์–ด๋‚ด์ง€ ์•Š๊ณ  ์‹ค์ œ ๋ฌธ์„œ์— ์žˆ๋Š” ๋‚ด์šฉ๋งŒ ๋‹ต๋ณ€ํ•œ๋‹ค๋Š” ๋œป์ด์—์š”! โœจ

๐Ÿ’ก ์™œ ๋ฌธ์„œ ๊ธฐ๋ฐ˜ ์ฑ—๋ด‡์ด ํ•„์š”ํ• ๊นŒ์š”?

1. ๊ณ ๊ฐ ์ง€์› ์ž๋™ํ™”: ๋ฐ˜๋ณต์ ์ธ ์งˆ๋ฌธ์— 24์‹œ๊ฐ„ ์ฆ‰์‹œ ๋‹ต๋ณ€ ๊ฐ€๋Šฅ
2. ์‚ฌ๋‚ด ์ง€์‹ ๊ด€๋ฆฌ: ๋ฐฉ๋Œ€ํ•œ ๋งค๋‰ด์–ผ์ด๋‚˜ ๊ทœ์ •์„ ์‰ฝ๊ฒŒ ๊ฒ€์ƒ‰
3. ๊ต์œก ๋ฐ ์˜จ๋ณด๋”ฉ: ์‹ ์ž… ์ง์›์ด๋‚˜ ํ•™์ƒ๋“ค์˜ ํ•™์Šต ๋„์šฐ๋ฏธ
4. ๋น„์šฉ ์ ˆ๊ฐ: ์ธ๋ ฅ ํˆฌ์ž… ์—†์ด๋„ ํšจ์œจ์ ์ธ ์ •๋ณด ์ œ๊ณต
5. ์ •ํ™•์„ฑ ํ–ฅ์ƒ: ๊ฒ€์ฆ๋œ ๋ฌธ์„œ ๊ธฐ๋ฐ˜์œผ๋กœ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ๋‹ต๋ณ€ ์ œ๊ณต

๐Ÿ› ๏ธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์ค€๋น„ํ•˜๊ธฐ

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์‹œ์ž‘ํ•ด๋ณผ๊นŒ์š”? ๋จผ์ € ํ•„์š”ํ•œ ๋„๊ตฌ๋“ค์„ ์ค€๋น„ํ•ด์•ผ ํ•ด์š”. ์š”๋ฆฌ๋ฅผ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์— ์žฌ๋ฃŒ๋ฅผ ์ค€๋น„ํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ์š”! ๐Ÿ‘จโ€๐Ÿณ

๐Ÿ“ฆ ํ•„์ˆ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜

Python ํ™˜๊ฒฝ์—์„œ ๋‹ค์Œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์„ ์„ค์น˜ํ•ด์ฃผ์„ธ์š”. ํ„ฐ๋ฏธ๋„์ด๋‚˜ ๋ช…๋ น ํ”„๋กฌํ”„ํŠธ์—์„œ ์‹คํ–‰ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค:

pip install langchain
pip install openai
pip install chromadb
pip install pypdf
pip install python-dotenv
pip install tiktoken
pip install langchain-community
pip install langchain-openai

๐Ÿ’š ์ดˆ๋ณด์ž ํŒ: ๊ฐ€์ƒํ™˜๊ฒฝ(venv)์„ ๋งŒ๋“ค์–ด์„œ ์ž‘์—…ํ•˜๋Š” ๊ฑธ ์ถ”์ฒœํ•ด์š”! ํ”„๋กœ์ ํŠธ๋ณ„๋กœ ๋…๋ฆฝ์ ์ธ ํ™˜๊ฒฝ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์–ด์„œ ๋‚˜์ค‘์— ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฒ„์ „ ์ถฉ๋Œ ๊ฐ™์€ ๋ฌธ์ œ๋ฅผ ์˜ˆ๋ฐฉํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค.

python -m venv chatbot_env
source chatbot_env/bin/activate (Mac/Linux)
chatbot_env\Scripts\activate (Windows)

๐Ÿ”‘ API ํ‚ค ์ค€๋น„ํ•˜๊ธฐ

OpenAI์˜ GPT ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด API ํ‚ค๊ฐ€ ํ•„์š”ํ•ด์š”. OpenAI ์›น์‚ฌ์ดํŠธ(platform.openai.com)์—์„œ ๊ณ„์ •์„ ๋งŒ๋“ค๊ณ  API ํ‚ค๋ฅผ ๋ฐœ๊ธ‰๋ฐ›์œผ์„ธ์š”. ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ ํด๋”์— .env ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ €์žฅํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค:

OPENAI_API_KEY=your-api-key-here

โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ: API ํ‚ค๋Š” ์ ˆ๋Œ€ GitHub ๊ฐ™์€ ๊ณต๊ฐœ ์ €์žฅ์†Œ์— ์˜ฌ๋ฆฌ๋ฉด ์•ˆ ๋ผ์š”! .gitignore ํŒŒ์ผ์— .env๋ฅผ ์ถ”๊ฐ€ํ•ด์„œ ์‹ค์ˆ˜๋กœ ์—…๋กœ๋“œ๋˜๋Š” ๊ฑธ ๋ฐฉ์ง€ํ•˜์„ธ์š”. API ํ‚ค๊ฐ€ ๋…ธ์ถœ๋˜๋ฉด ์š”๊ธˆ ํญํƒ„์„ ๋งž์„ ์ˆ˜ ์žˆ์–ด์š”! ๐Ÿ’ธ

๐Ÿ“š ๋ฌธ์„œ ์ฒ˜๋ฆฌ ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ•ํ•˜๊ธฐ

์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณผ ๊ฑฐ์˜ˆ์š”. ๋ฌธ์„œ ๊ธฐ๋ฐ˜ ์ฑ—๋ด‡์˜ ํ•ต์‹ฌ์€ ๋ฌธ์„œ๋ฅผ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•˜๊ณ  ์ €์žฅํ•˜๋А๋ƒ์˜ˆ์š”. ์ด ๊ณผ์ •์„ ๋‹จ๊ณ„๋ณ„๋กœ ์‚ดํŽด๋ณผ๊ฒŒ์š”! ๐Ÿ”

1๋‹จ๊ณ„ ๋ฌธ์„œ ๋กœ๋“œ PDF, TXT ์ฝ๊ธฐ 2๋‹จ๊ณ„ ํ…์ŠคํŠธ ๋ถ„ํ•  ์ฒญํฌ ๋‹จ์œ„๋กœ ๋‚˜๋ˆ„๊ธฐ 3๋‹จ๊ณ„ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ ๋ฒกํ„ฐ๋กœ ๋ณ€ํ™˜ 4๋‹จ๊ณ„ ๋ฒกํ„ฐ ์ €์žฅ DB์— ์ธ๋ฑ์‹ฑ ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ ์ง€์‹ ๋ฒ ์ด์Šค ์™„์„ฑ! ๐ŸŽ‰ ์ด์ œ ์‚ฌ์šฉ์ž ์งˆ๋ฌธ์— ๋Œ€ํ•ด ๊ฐ€์žฅ ๊ด€๋ จ์„ฑ ๋†’์€ ๋ฌธ์„œ๋ฅผ ์ฐพ์•„ ์ •ํ™•ํ•œ ๋‹ต๋ณ€์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์–ด์š”!

1๏ธโƒฃ ๋ฌธ์„œ ๋กœ๋” ๊ตฌํ˜„ํ•˜๊ธฐ

์ฒซ ๋ฒˆ์งธ ๋‹จ๊ณ„๋Š” ๋ฌธ์„œ๋ฅผ ์ฝ์–ด์˜ค๋Š” ๊ฑฐ์˜ˆ์š”. LangChain์€ ๋‹ค์–‘ํ•œ ๋ฌธ์„œ ํ˜•์‹์„ ์ง€์›ํ•˜๋Š” ๋กœ๋”๋“ค์„ ์ œ๊ณตํ•ด์š”. ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” PDF์™€ ํ…์ŠคํŠธ ํŒŒ์ผ์„ ์˜ˆ๋กœ ๋“ค์–ด๋ณผ๊ฒŒ์š”:

from langchain_community.document_loaders import PyPDFLoader, TextLoader
from langchain_community.document_loaders import DirectoryLoader
import os
from dotenv import load_dotenv

# ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ
load_dotenv()

# PDF ํŒŒ์ผ ๋กœ๋“œ
def load_pdf_documents(file_path):
    """PDF ํŒŒ์ผ์„ ๋กœ๋“œํ•˜๋Š” ํ•จ์ˆ˜"""
    loader = PyPDFLoader(file_path)
    documents = loader.load()
    return documents

# ํ…์ŠคํŠธ ํŒŒ์ผ ๋กœ๋“œ
def load_text_documents(file_path):
    """ํ…์ŠคํŠธ ํŒŒ์ผ์„ ๋กœ๋“œํ•˜๋Š” ํ•จ์ˆ˜"""
    loader = TextLoader(file_path, encoding='utf-8')
    documents = loader.load()
    return documents

# ๋””๋ ‰ํ† ๋ฆฌ ๋‚ด ๋ชจ๋“  PDF ํŒŒ์ผ ๋กœ๋“œ
def load_documents_from_directory(directory_path):
    """ํŠน์ • ๋””๋ ‰ํ† ๋ฆฌ์˜ ๋ชจ๋“  PDF๋ฅผ ๋กœ๋“œ"""
    loader = DirectoryLoader(
        directory_path,
        glob="**/*.pdf",
        loader_cls=PyPDFLoader
    )
    documents = loader.load()
    print(f"์ด {len(documents)}๊ฐœ์˜ ๋ฌธ์„œ ํŽ˜์ด์ง€๋ฅผ ๋กœ๋“œํ–ˆ์Šต๋‹ˆ๋‹ค.")
    return documents

๐Ÿ’ก ์‹ค์ „ ํŒ: ๋ฌธ์„œ๊ฐ€ ๋งŽ๋‹ค๋ฉด DirectoryLoader๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํŽธํ•ด์š”. ํด๋” ์•ˆ์˜ ๋ชจ๋“  ํŒŒ์ผ์„ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ ์š”. ์ €๋Š” ๋ณดํ†ต documents๋ผ๋Š” ํด๋”๋ฅผ ๋งŒ๋“ค์–ด์„œ ๊ฑฐ๊ธฐ์— ๋ชจ๋“  FAQ ๋ฌธ์„œ๋ฅผ ๋„ฃ์–ด๋‘๊ณ  ํ•œ ๋ฒˆ์— ๋กœ๋“œํ•ด์š”! ๐Ÿ“

2๏ธโƒฃ ํ…์ŠคํŠธ ๋ถ„ํ• ํ•˜๊ธฐ (Chunking)

๋ฌธ์„œ๋ฅผ ๋กœ๋“œํ–ˆ๋‹ค๋ฉด ์ด์ œ ์ ์ ˆํ•œ ํฌ๊ธฐ๋กœ ๋‚˜๋ˆ ์•ผ ํ•ด์š”. ์™œ๋ƒ๊ณ ์š”? LLM์—๋Š” ์ž…๋ ฅ ํ† ํฐ ์ œํ•œ์ด ์žˆ๊ณ , ๋„ˆ๋ฌด ๊ธด ํ…์ŠคํŠธ๋Š” ๊ฒ€์ƒ‰ ์ •ํ™•๋„๊ฐ€ ๋–จ์–ด์ง€๊ธฐ ๋•Œ๋ฌธ์ด์—์š”.

์ ์ ˆํ•œ ์ฒญํฌ ํฌ๊ธฐ๋Š” ๋ณดํ†ต 500~1000์ž ์ •๋„๊ฐ€ ์ข‹์•„์š”. ๋„ˆ๋ฌด ์ž‘์œผ๋ฉด ๋งฅ๋ฝ์ด ๋ถ€์กฑํ•˜๊ณ , ๋„ˆ๋ฌด ํฌ๋ฉด ๊ด€๋ จ ์—†๋Š” ์ •๋ณด๊นŒ์ง€ ํฌํ•จ๋  ์ˆ˜ ์žˆ๊ฑฐ๋“ ์š”. ๐ŸŽฏ

from langchain.text_splitter import RecursiveCharacterTextSplitter

def split_documents(documents, chunk_size=1000, chunk_overlap=200):
    """
    ๋ฌธ์„œ๋ฅผ ์ ์ ˆํ•œ ํฌ๊ธฐ์˜ ์ฒญํฌ๋กœ ๋ถ„ํ• 
    
    Args:
        documents: ๋กœ๋“œ๋œ ๋ฌธ์„œ ๋ฆฌ์ŠคํŠธ
        chunk_size: ๊ฐ ์ฒญํฌ์˜ ์ตœ๋Œ€ ํฌ๊ธฐ (๊ธฐ๋ณธ 1000์ž)
        chunk_overlap: ์ฒญํฌ ๊ฐ„ ๊ฒน์น˜๋Š” ๋ถ€๋ถ„ (๊ธฐ๋ณธ 200์ž)
    
    Returns:
        ๋ถ„ํ• ๋œ ๋ฌธ์„œ ์ฒญํฌ ๋ฆฌ์ŠคํŠธ
    """
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        length_function=len,
        separators=["\n\n", "\n", " ", ""]
    )
    
    chunks = text_splitter.split_documents(documents)
    print(f"๋ฌธ์„œ๋ฅผ {len(chunks)}๊ฐœ์˜ ์ฒญํฌ๋กœ ๋ถ„ํ• ํ–ˆ์Šต๋‹ˆ๋‹ค.")
    return chunks

๐Ÿค” chunk_overlap์ด ๋ญ”๊ฐ€์š”?

chunk_overlap์€ ์ฒญํฌ๋“ค์ด ์„œ๋กœ ๊ฒน์น˜๋Š” ๋ถ€๋ถ„์ด์—์š”. ์˜ˆ๋ฅผ ๋“ค์–ด "์˜ค๋Š˜ ๋‚ ์”จ๊ฐ€ ์ •๋ง ์ข‹๋„ค์š”. ์‚ฐ์ฑ…ํ•˜๊ธฐ ๋”ฑ ์ข‹์€ ๋‚ ์ด์—์š”."๋ผ๋Š” ๋ฌธ์žฅ์ด ์žˆ์„ ๋•Œ, ์ฒซ ๋ฒˆ์งธ ์ฒญํฌ๊ฐ€ "์˜ค๋Š˜ ๋‚ ์”จ๊ฐ€ ์ •๋ง ์ข‹๋„ค์š”."๋กœ ๋๋‚˜๊ณ  ๋‘ ๋ฒˆ์งธ ์ฒญํฌ๊ฐ€ "์‚ฐ์ฑ…ํ•˜๊ธฐ ๋”ฑ ์ข‹์€ ๋‚ ์ด์—์š”."๋กœ ์‹œ์ž‘ํ•˜๋ฉด ๋ฌธ๋งฅ์ด ๋Š๊ธฐ์ž–์•„์š”?

๊ทธ๋ž˜์„œ overlap์„ ์ฃผ๋ฉด "์ •๋ง ์ข‹๋„ค์š”. ์‚ฐ์ฑ…ํ•˜๊ธฐ"์ฒ˜๋Ÿผ ์ผ๋ถ€๊ฐ€ ๊ฒน์น˜๊ฒŒ ๋˜์–ด ๋ฌธ๋งฅ์ด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ด์–ด์ ธ์š”. ๋ณดํ†ต ์ฒญํฌ ํฌ๊ธฐ์˜ 10~20% ์ •๋„๋กœ ์„ค์ •ํ•˜๋ฉด ์ข‹๋‹ต๋‹ˆ๋‹ค! ๐Ÿ”—

3๏ธโƒฃ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ ๋ฐ ๋ฒกํ„ฐ ์ €์žฅ์†Œ ๊ตฌ์ถ•

์ด์ œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋‹จ๊ณ„์˜ˆ์š”! ํ…์ŠคํŠธ๋ฅผ ์ปดํ“จํ„ฐ๊ฐ€ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋Š” ์ˆซ์ž ๋ฒกํ„ฐ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ณผ์ •์ด์ฃ . ์ด๊ฑธ ์ž„๋ฒ ๋”ฉ(Embedding)์ด๋ผ๊ณ  ํ•ด์š”.

๋น„์œ ํ•˜์ž๋ฉด, ์ฑ…์˜ ๋‚ด์šฉ์„ DNA์ฒ˜๋Ÿผ ์ˆซ์ž ์ฝ”๋“œ๋กœ ๋ฐ”๊พธ๋Š” ๊ฑฐ์˜ˆ์š”. ๊ทธ๋Ÿฌ๋ฉด ๋‚˜์ค‘์— ์งˆ๋ฌธ์ด ๋“ค์–ด์™”์„ ๋•Œ "์ด ์งˆ๋ฌธ์˜ DNA์™€ ๊ฐ€์žฅ ๋น„์Šทํ•œ ๋ฌธ์„œ DNA๋ฅผ ์ฐพ์•„์ค˜!"๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฑฐ์ฃ . ๐Ÿงฌ

from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma

def create_vector_store(chunks, persist_directory="./chroma_db"):
    """
    ์ฒญํฌ๋“ค์„ ์ž„๋ฒ ๋”ฉํ•˜๊ณ  ๋ฒกํ„ฐ ์ €์žฅ์†Œ์— ์ €์žฅ
    
    Args:
        chunks: ๋ถ„ํ• ๋œ ๋ฌธ์„œ ์ฒญํฌ
        persist_directory: ๋ฒกํ„ฐ DB๋ฅผ ์ €์žฅํ•  ๋””๋ ‰ํ† ๋ฆฌ
    
    Returns:
        Chroma ๋ฒกํ„ฐ ์ €์žฅ์†Œ ๊ฐ์ฒด
    """
    # OpenAI ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ ์ดˆ๊ธฐํ™”
    embeddings = OpenAIEmbeddings(
        model="text-embedding-ada-002"
    )
    
    # Chroma ๋ฒกํ„ฐ ์ €์žฅ์†Œ ์ƒ์„ฑ
    vectorstore = Chroma.from_documents(
        documents=chunks,
        embedding=embeddings,
        persist_directory=persist_directory
    )
    
    print(f"๋ฒกํ„ฐ ์ €์žฅ์†Œ๊ฐ€ {persist_directory}์— ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
    return vectorstore

# ๊ธฐ์กด ๋ฒกํ„ฐ ์ €์žฅ์†Œ ๋กœ๋“œํ•˜๊ธฐ
def load_vector_store(persist_directory="./chroma_db"):
    """์ €์žฅ๋œ ๋ฒกํ„ฐ ์ €์žฅ์†Œ๋ฅผ ๋กœ๋“œ"""
    embeddings = OpenAIEmbeddings(
        model="text-embedding-ada-002"
    )
    
    vectorstore = Chroma(
        persist_directory=persist_directory,
        embedding_function=embeddings
    )
    
    return vectorstore

๐Ÿ’พ ๋ฒกํ„ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ ํƒํ•˜๊ธฐ

์—ฌ๊ธฐ์„œ๋Š” Chroma๋ฅผ ์‚ฌ์šฉํ–ˆ์ง€๋งŒ, ๋‹ค๋ฅธ ์˜ต์…˜๋“ค๋„ ๋งŽ์•„์š”:

โ€ข Chroma: ๊ฐ€๋ณ๊ณ  ์„ค์น˜๊ฐ€ ์‰ฌ์›Œ์„œ ํ”„๋กœํ† ํƒ€์ž…์ด๋‚˜ ์†Œ๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ์— ์ ํ•ฉ
โ€ข Pinecone: ํด๋ผ์šฐ๋“œ ๊ธฐ๋ฐ˜, ๋Œ€๊ทœ๋ชจ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์— ์ตœ์ ํ™”
โ€ข FAISS: Facebook์ด ๋งŒ๋“  ์˜คํ”ˆ์†Œ์Šค, ๋น ๋ฅธ ๊ฒ€์ƒ‰ ์†๋„
โ€ข Weaviate: ์˜คํ”ˆ์†Œ์Šค, ํ’๋ถ€ํ•œ ๊ธฐ๋Šฅ๊ณผ ํ™•์žฅ์„ฑ

์ดˆ๋ณด์ž๋ผ๋ฉด Chroma๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฑธ ์ถ”์ฒœํ•ด์š”. ๋ณ„๋„ ์„œ๋ฒ„ ์„ค์น˜ ์—†์ด ๋กœ์ปฌ์—์„œ ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ ์š”! ๐Ÿš€

๐Ÿค– ์ฑ—๋ด‡ ๊ตฌํ˜„ํ•˜๊ธฐ

๋“œ๋””์–ด ์ฑ—๋ด‡์„ ๋งŒ๋“ค ์ฐจ๋ก€์˜ˆ์š”! ์ด์ œ๊นŒ์ง€ ์ค€๋น„ํ•œ ์žฌ๋ฃŒ๋“ค๋กœ ๋ง›์žˆ๋Š” ์š”๋ฆฌ๋ฅผ ์™„์„ฑํ•˜๋Š” ๋‹จ๊ณ„์ฃ . ๐Ÿ˜‹

LangChain์˜ RetrievalQA ์ฒด์ธ์„ ์‚ฌ์šฉํ•˜๋ฉด ๊ฒ€์ƒ‰๊ณผ ๋‹ต๋ณ€ ์ƒ์„ฑ์„ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์š”.

from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

def create_qa_chain(vectorstore):
    """
    ์งˆ์˜์‘๋‹ต ์ฒด์ธ ์ƒ์„ฑ
    
    Args:
        vectorstore: ๋ฒกํ„ฐ ์ €์žฅ์†Œ ๊ฐ์ฒด
    
    Returns:
        RetrievalQA ์ฒด์ธ
    """
    # GPT ๋ชจ๋ธ ์ดˆ๊ธฐํ™”
    llm = ChatOpenAI(
        model_name="gpt-3.5-turbo",
        temperature=0.3  # ๋‚ฎ์„์ˆ˜๋ก ์ผ๊ด€๋œ ๋‹ต๋ณ€, ๋†’์„์ˆ˜๋ก ์ฐฝ์˜์ 
    )
    
    # ํ”„๋กฌํ”„ํŠธ ํ…œํ”Œ๋ฆฟ ์ •์˜
    template = """๋‹น์‹ ์€ ์นœ์ ˆํ•œ FAQ ์ฑ—๋ด‡์ž…๋‹ˆ๋‹ค. 
    ์ฃผ์–ด์ง„ ๋ฌธ์„œ ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ์‚ฌ์šฉ์ž์˜ ์งˆ๋ฌธ์— ์ •ํ™•ํ•˜๊ฒŒ ๋‹ต๋ณ€ํ•ด์ฃผ์„ธ์š”.
    ๋ฌธ์„œ์— ์—†๋Š” ๋‚ด์šฉ์€ "์ฃ„์†กํ•˜์ง€๋งŒ ํ•ด๋‹น ์ •๋ณด๋Š” ์ œ๊ณต๋œ ๋ฌธ์„œ์— ์—†์Šต๋‹ˆ๋‹ค"๋ผ๊ณ  ๋‹ต๋ณ€ํ•˜์„ธ์š”.
    
    ๋ฌธ์„œ ๋‚ด์šฉ:
    {context}
    
    ์งˆ๋ฌธ: {question}
    
    ๋‹ต๋ณ€:"""
    
    PROMPT = PromptTemplate(
        template=template,
        input_variables=["context", "question"]
    )
    
    # RetrievalQA ์ฒด์ธ ์ƒ์„ฑ
    qa_chain = RetrievalQA.from_chain_type(
        llm=llm,
        chain_type="stuff",
        retriever=vectorstore.as_retriever(
            search_type="similarity",
            search_kwargs={"k": 3}  # ์ƒ์œ„ 3๊ฐœ ๋ฌธ์„œ ๊ฒ€์ƒ‰
        ),
        return_source_documents=True,
        chain_type_kwargs={"prompt": PROMPT}
    )
    
    return qa_chain

def ask_question(qa_chain, question):
    """
    ์ฑ—๋ด‡์—๊ฒŒ ์งˆ๋ฌธํ•˜๊ธฐ
    
    Args:
        qa_chain: QA ์ฒด์ธ ๊ฐ์ฒด
        question: ์‚ฌ์šฉ์ž ์งˆ๋ฌธ
    
    Returns:
        ๋‹ต๋ณ€ ๋ฐ ์ถœ์ฒ˜ ๋ฌธ์„œ
    """
    result = qa_chain({"query": question})
    
    answer = result['result']
    source_docs = result['source_documents']
    
    print(f"\n์งˆ๋ฌธ: {question}")
    print(f"\n๋‹ต๋ณ€: {answer}")
    print(f"\n์ฐธ๊ณ ํ•œ ๋ฌธ์„œ ์ˆ˜: {len(source_docs)}")
    
    return answer, source_docs

๐ŸŽจ Temperature ํŒŒ๋ผ๋ฏธํ„ฐ ์กฐ์ ˆํ•˜๊ธฐ:

temperature๋Š” AI์˜ ์ฐฝ์˜์„ฑ์„ ์กฐ์ ˆํ•˜๋Š” ๊ฐ’์ด์—์š”.
โ€ข 0.0~0.3: ๋งค์šฐ ์ผ๊ด€๋˜๊ณ  ์ •ํ™•ํ•œ ๋‹ต๋ณ€ (FAQ ์ฑ—๋ด‡์— ์ ํ•ฉ) โœ…
โ€ข 0.4~0.7: ๊ท ํ˜•์žกํžŒ ๋‹ต๋ณ€
โ€ข 0.8~1.0: ์ฐฝ์˜์ ์ด๊ณ  ๋‹ค์–‘ํ•œ ๋‹ต๋ณ€ (์Šคํ† ๋ฆฌ ์ƒ์„ฑ์— ์ ํ•ฉ)

FAQ ์ฑ—๋ด‡์€ ์ •ํ™•์„ฑ์ด ์ค‘์š”ํ•˜๋‹ˆ๊นŒ ๋‚ฎ์€ ๊ฐ’์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์•„์š”!

๐Ÿ” ๊ฒ€์ƒ‰ ๋ฐฉ์‹ ์ดํ•ดํ•˜๊ธฐ

์œ„ ์ฝ”๋“œ์—์„œ search_type="similarity"์™€ search_kwargs={"k": 3}๋ฅผ ์„ค์ •ํ–ˆ๋Š”๋ฐ, ์ด๊ฒŒ ๋ญ˜ ์˜๋ฏธํ•˜๋Š”์ง€ ์•Œ์•„๋ณผ๊นŒ์š”?

๊ฒ€์ƒ‰ ๋ฐฉ์‹ ์„ค๋ช… ์žฅ์  ๋‹จ์ 
similarity ์ฝ”์‚ฌ์ธ ์œ ์‚ฌ๋„ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰ ๊ฐ€์žฅ ๊ด€๋ จ์„ฑ ๋†’์€ ๋ฌธ์„œ ์ฐพ๊ธฐ ๋‹ค์–‘์„ฑ ๋ถ€์กฑ ๊ฐ€๋Šฅ
mmr ์ตœ๋Œ€ ํ•œ๊ณ„ ๊ด€๋ จ์„ฑ ๊ฒ€์ƒ‰ ๊ด€๋ จ์„ฑ๊ณผ ๋‹ค์–‘์„ฑ ๊ท ํ˜• ๊ณ„์‚ฐ ๋น„์šฉ ๋†’์Œ
similarity_score_threshold ์ž„๊ณ„๊ฐ’ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง ๋‚ฎ์€ ๊ด€๋ จ์„ฑ ๋ฌธ์„œ ์ œ์™ธ ์ž„๊ณ„๊ฐ’ ์„ค์ • ์–ด๋ ค์›€

k=3์€ ์ƒ์œ„ 3๊ฐœ ๋ฌธ์„œ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค๋Š” ๋œป์ด์—์š”. ์ด ๊ฐ’์„ ๋Š˜๋ฆฌ๋ฉด ๋” ๋งŽ์€ ๋งฅ๋ฝ์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ๊ด€๋ จ ์—†๋Š” ์ •๋ณด๊ฐ€ ์„ž์ผ ์ˆ˜๋„ ์žˆ์–ด์š”. ๋ณดํ†ต 3~5๊ฐœ๊ฐ€ ์ ๋‹นํ•˜๋‹ต๋‹ˆ๋‹ค! ๐ŸŽฏ

๐ŸŽฌ ์ „์ฒด ์‹œ์Šคํ…œ ํ†ตํ•ฉํ•˜๊ธฐ

์ด์ œ ์ง€๊ธˆ๊นŒ์ง€ ๋งŒ๋“  ๋ชจ๋“  ํ•จ์ˆ˜๋“ค์„ ํ•˜๋‚˜๋กœ ํ•ฉ์ณ์„œ ์™„์ „ํ•œ ์ฑ—๋ด‡ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”! ๋งˆ์น˜ ํผ์ฆ ์กฐ๊ฐ๋“ค์„ ๋งž์ถ”๋Š” ๊ฒƒ์ฒ˜๋Ÿผ์š”. ๐Ÿงฉ

import os
from dotenv import load_dotenv

class DocumentFAQChatbot:
    """๋ฌธ์„œ ๊ธฐ๋ฐ˜ FAQ ์ฑ—๋ด‡ ํด๋ž˜์Šค"""
    
    def __init__(self, documents_path, persist_directory="./chroma_db"):
        """
        ์ฑ—๋ด‡ ์ดˆ๊ธฐํ™”
        
        Args:
            documents_path: ๋ฌธ์„œ๊ฐ€ ์žˆ๋Š” ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ
            persist_directory: ๋ฒกํ„ฐ DB ์ €์žฅ ๊ฒฝ๋กœ
        """
        load_dotenv()
        self.documents_path = documents_path
        self.persist_directory = persist_directory
        self.vectorstore = None
        self.qa_chain = None
        
    def setup(self, force_reload=False):
        """์ฑ—๋ด‡ ์„ค์ • ๋ฐ ์ดˆ๊ธฐํ™”"""
        
        # ๊ธฐ์กด ๋ฒกํ„ฐ ์ €์žฅ์†Œ๊ฐ€ ์žˆ๊ณ  ์žฌ๋กœ๋“œ๊ฐ€ ํ•„์š”์—†์œผ๋ฉด ๋กœ๋“œ
        if os.path.exists(self.persist_directory) and not force_reload:
            print("๊ธฐ์กด ๋ฒกํ„ฐ ์ €์žฅ์†Œ๋ฅผ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค...")
            self.vectorstore = load_vector_store(self.persist_directory)
        else:
            print("์ƒˆ๋กœ์šด ๋ฒกํ„ฐ ์ €์žฅ์†Œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค...")
            # 1. ๋ฌธ์„œ ๋กœ๋“œ
            documents = load_documents_from_directory(self.documents_path)
            
            # 2. ๋ฌธ์„œ ๋ถ„ํ• 
            chunks = split_documents(documents)
            
            # 3. ๋ฒกํ„ฐ ์ €์žฅ์†Œ ์ƒ์„ฑ
            self.vectorstore = create_vector_store(
                chunks, 
                self.persist_directory
            )
        
        # 4. QA ์ฒด์ธ ์ƒ์„ฑ
        self.qa_chain = create_qa_chain(self.vectorstore)
        print("์ฑ—๋ด‡์ด ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๐ŸŽ‰")
        
    def ask(self, question):
        """์งˆ๋ฌธํ•˜๊ธฐ"""
        if self.qa_chain is None:
            raise Exception("๋จผ์ € setup() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•ด์ฃผ์„ธ์š”!")
        
        return ask_question(self.qa_chain, question)
    
    def chat_loop(self):
        """๋Œ€ํ™”ํ˜• ์ธํ„ฐํŽ˜์ด์Šค"""
        print("\n" + "="*50)
        print("๐Ÿ“š ๋ฌธ์„œ ๊ธฐ๋ฐ˜ FAQ ์ฑ—๋ด‡์— ์˜ค์‹  ๊ฒƒ์„ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค!")
        print("์ข…๋ฃŒํ•˜๋ ค๋ฉด 'quit' ๋˜๋Š” 'exit'๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.")
        print("="*50 + "\n")
        
        while True:
            question = input("\n๐Ÿ’ฌ ์งˆ๋ฌธ์„ ์ž…๋ ฅํ•˜์„ธ์š”: ").strip()
            
            if question.lower() in ['quit', 'exit', '์ข…๋ฃŒ']:
                print("\n๐Ÿ‘‹ ์ฑ—๋ด‡์„ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค. ์ข‹์€ ํ•˜๋ฃจ ๋˜์„ธ์š”!")
                break
            
            if not question:
                print("โš ๏ธ ์งˆ๋ฌธ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")
                continue
            
            try:
                answer, sources = self.ask(question)
                
                # ์ถœ์ฒ˜ ๋ฌธ์„œ ์ •๋ณด ํ‘œ์‹œ
                print("\n๐Ÿ“– ์ฐธ๊ณ  ๋ฌธ์„œ:")
                for i, doc in enumerate(sources, 1):
                    source = doc.metadata.get('source', '์•Œ ์ˆ˜ ์—†์Œ')
                    print(f"   {i}. {source}")
                    
            except Exception as e:
                print(f"\nโŒ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}")

# ์‚ฌ์šฉ ์˜ˆ์‹œ
if __name__ == "__main__":
    # ์ฑ—๋ด‡ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
    chatbot = DocumentFAQChatbot(
        documents_path="./documents",  # ๋ฌธ์„œ ํด๋” ๊ฒฝ๋กœ
        persist_directory="./chroma_db"
    )
    
    # ์ฑ—๋ด‡ ์„ค์ •
    chatbot.setup(force_reload=False)
    
    # ๋Œ€ํ™” ์‹œ์ž‘
    chatbot.chat_loop()

๐ŸŽฎ ์‹คํ–‰ ๋ฐฉ๋ฒ•

1. ํ”„๋กœ์ ํŠธ ํด๋”์— documents ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๋งŒ๋“ค๊ณ  FAQ ๋ฌธ์„œ๋“ค์„ ๋„ฃ์œผ์„ธ์š”.
2. ์œ„ ์ฝ”๋“œ๋ฅผ chatbot.py๋กœ ์ €์žฅํ•˜์„ธ์š”.
3. ํ„ฐ๋ฏธ๋„์—์„œ python chatbot.py๋ฅผ ์‹คํ–‰ํ•˜์„ธ์š”.
4. ์งˆ๋ฌธ์„ ์ž…๋ ฅํ•˜๊ณ  ๋‹ต๋ณ€์„ ๋ฐ›์œผ์„ธ์š”! ๐ŸŽ‰

์ฒ˜์Œ ์‹คํ–‰ํ•  ๋•Œ๋Š” ๋ฌธ์„œ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋А๋ผ ์‹œ๊ฐ„์ด ์ข€ ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์–ด์š”. ํ•˜์ง€๋งŒ ๋‘ ๋ฒˆ์งธ๋ถ€ํ„ฐ๋Š” ์ €์žฅ๋œ ๋ฒกํ„ฐ DB๋ฅผ ์‚ฌ์šฉํ•˜๋‹ˆ๊นŒ ๋น ๋ฅด๊ฒŒ ์‹œ์ž‘๋œ๋‹ต๋‹ˆ๋‹ค!

๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฐ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ

๊ธฐ๋ณธ ์ฑ—๋ด‡์€ ์™„์„ฑํ–ˆ์ง€๋งŒ, ์‹ค์ œ ์„œ๋น„์Šค์— ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋ช‡ ๊ฐ€์ง€ ๊ฐœ์„ ์ด ํ•„์š”ํ•ด์š”. ํ”„๋กœ์ฒ˜๋Ÿผ ์ฑ—๋ด‡์„ ์—…๊ทธ๋ ˆ์ด๋“œํ•ด๋ณผ๊นŒ์š”? ๐Ÿ’ช

โšก ์‘๋‹ต ์†๋„ ๊ฐœ์„ ํ•˜๊ธฐ

์‚ฌ์šฉ์ž๋Š” ๋น ๋ฅธ ๋‹ต๋ณ€์„ ์›ํ•ด์š”. ๋А๋ฆฐ ์ฑ—๋ด‡์€ ์•„๋ฌด๋„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์ฃ ! ์†๋„๋ฅผ ๊ฐœ์„ ํ•˜๋Š” ๋ช‡ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ์•Œ๋ ค๋“œ๋ฆด๊ฒŒ์š”.

1. ์บ์‹ฑ ํ™œ์šฉํ•˜๊ธฐ

๊ฐ™์€ ์งˆ๋ฌธ์ด ๋ฐ˜๋ณต๋˜๋ฉด ๋งค๋ฒˆ ๊ณ„์‚ฐํ•˜์ง€ ๋ง๊ณ  ์ด์ „ ๊ฒฐ๊ณผ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜์„ธ์š”:

from functools import lru_cache
import hashlib

class CachedChatbot(DocumentFAQChatbot):
    """์บ์‹ฑ ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€๋œ ์ฑ—๋ด‡"""
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.cache = {}
    
    def _get_cache_key(self, question):
        """์งˆ๋ฌธ์„ ํ•ด์‹œ๊ฐ’์œผ๋กœ ๋ณ€ํ™˜"""
        return hashlib.md5(question.encode()).hexdigest()
    
    def ask(self, question):
        """์บ์‹œ๋ฅผ ํ™•์ธํ•˜๊ณ  ์—†์œผ๋ฉด ์ƒˆ๋กœ ์งˆ๋ฌธ"""
        cache_key = self._get_cache_key(question)
        
        if cache_key in self.cache:
            print("๐Ÿ’พ ์บ์‹œ๋œ ๋‹ต๋ณ€์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.")
            return self.cache[cache_key]
        
        result = super().ask(question)
        self.cache[cache_key] = result
        return result

2. ์ŠคํŠธ๋ฆฌ๋ฐ ์‘๋‹ต ๊ตฌํ˜„ํ•˜๊ธฐ

ChatGPT์ฒ˜๋Ÿผ ๋‹ต๋ณ€์ด ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ƒ์„ฑ๋˜๋Š” ๊ฑธ ๋ณด์—ฌ์ฃผ๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ํ›จ์”ฌ ์ข‹์•„์ ธ์š”:

from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler

def create_streaming_qa_chain(vectorstore):
    """์ŠคํŠธ๋ฆฌ๋ฐ ๊ธฐ๋Šฅ์ด ์žˆ๋Š” QA ์ฒด์ธ"""
    llm = ChatOpenAI(
        model_name="gpt-3.5-turbo",
        temperature=0.3,
        streaming=True,
        callbacks=[StreamingStdOutCallbackHandler()]
    )
    
    # ๋‚˜๋จธ์ง€๋Š” ๋™์ผ...

๐ŸŽฏ ๋‹ต๋ณ€ ํ’ˆ์งˆ ํ–ฅ์ƒ์‹œํ‚ค๊ธฐ

๋” ์ •ํ™•ํ•˜๊ณ  ์œ ์šฉํ•œ ๋‹ต๋ณ€์„ ์ œ๊ณตํ•˜๋ ค๋ฉด ํ”„๋กฌํ”„ํŠธ ์—”์ง€๋‹ˆ์–ด๋ง์ด ์ค‘์š”ํ•ด์š”. ๋ช‡ ๊ฐ€์ง€ ํŒ์„ ๊ณต์œ ํ• ๊ฒŒ์š”! โœจ

๐Ÿ’Ž ํ”„๋กฌํ”„ํŠธ ๊ฐœ์„  ์˜ˆ์‹œ

๊ธฐ๋ณธ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋” ๊ตฌ์ฒด์ ์œผ๋กœ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”:

advanced_template = """๋‹น์‹ ์€ ์ „๋ฌธ์ ์ด๊ณ  ์นœ์ ˆํ•œ ๊ณ ๊ฐ ์ง€์› AI์ž…๋‹ˆ๋‹ค.

์—ญํ• ๊ณผ ๋ชฉํ‘œ:
- ์ œ๊ณต๋œ ๋ฌธ์„œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ •ํ™•ํ•˜๊ณ  ๋„์›€์ด ๋˜๋Š” ๋‹ต๋ณ€ ์ œ๊ณต
- ์‚ฌ์šฉ์ž๊ฐ€ ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๊ฒŒ ์„ค๋ช…
- ํ•„์š”์‹œ ๋‹จ๊ณ„๋ณ„๋กœ ์•ˆ๋‚ด

๋‹ต๋ณ€ ๊ทœ์น™:
1. ๋ฌธ์„œ์— ๋ช…ํ™•ํ•œ ๋‹ต์ด ์žˆ์œผ๋ฉด ๊ทธ๋Œ€๋กœ ์ธ์šฉํ•˜๊ณ  ์ถœ์ฒ˜ ํ‘œ์‹œ
2. ๋ฌธ์„œ์— ๋ถ€๋ถ„์  ์ •๋ณด๋งŒ ์žˆ์œผ๋ฉด "๋ฌธ์„œ์— ๋”ฐ๋ฅด๋ฉด..."์œผ๋กœ ์‹œ์ž‘
3. ๋ฌธ์„œ์— ์—†๋Š” ๋‚ด์šฉ์€ ์†”์งํ•˜๊ฒŒ "์ œ๊ณต๋œ ๋ฌธ์„œ์—๋Š” ํ•ด๋‹น ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค"๋ผ๊ณ  ๋‹ต๋ณ€
4. ๊ด€๋ จ ์ •๋ณด๊ฐ€ ์žˆ๋‹ค๋ฉด ์ถ”๊ฐ€๋กœ ์ œ๊ณต
5. ๋‹ต๋ณ€์€ 3-5๋ฌธ์žฅ์œผ๋กœ ๊ฐ„๊ฒฐํ•˜๊ฒŒ

๋ฌธ์„œ ๋‚ด์šฉ:
{context}

์‚ฌ์šฉ์ž ์งˆ๋ฌธ: {question}

๋‹ต๋ณ€ (์นœ์ ˆํ•˜๊ณ  ์ „๋ฌธ์ ์œผ๋กœ):"""

๐Ÿ“Š ๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ ๊ด€๋ฆฌํ•˜๊ธฐ

์ด์ „ ๋Œ€ํ™” ๋‚ด์šฉ์„ ๊ธฐ์–ตํ•˜๋ฉด ๋” ์ž์—ฐ์Šค๋Ÿฌ์šด ๋Œ€ํ™”๊ฐ€ ๊ฐ€๋Šฅํ•ด์š”. ์˜ˆ๋ฅผ ๋“ค์–ด "๊ทธ๊ฑฐ ๋ง๊ณ  ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์€?"์ด๋ผ๊ณ  ๋ฌผ์–ด๋ดค์„ ๋•Œ "๊ทธ๊ฑฐ"๊ฐ€ ๋ญ”์ง€ ์•Œ ์ˆ˜ ์žˆ์ฃ ! ๐Ÿง 

from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationalRetrievalChain

def create_conversational_chain(vectorstore):
    """๋Œ€ํ™” ๊ธฐ๋ก์„ ์œ ์ง€ํ•˜๋Š” ์ฒด์ธ"""
    llm = ChatOpenAI(
        model_name="gpt-3.5-turbo",
        temperature=0.3
    )
    
    # ๋Œ€ํ™” ๋ฉ”๋ชจ๋ฆฌ ์ƒ์„ฑ
    memory = ConversationBufferMemory(
        memory_key="chat_history",
        return_messages=True,
        output_key="answer"
    )
    
    # ๋Œ€ํ™”ํ˜• ๊ฒ€์ƒ‰ ์ฒด์ธ
    qa_chain = ConversationalRetrievalChain.from_llm(
        llm=llm,
        retriever=vectorstore.as_retriever(search_kwargs={"k": 3}),
        memory=memory,
        return_source_documents=True
    )
    
    return qa_chain

๐ŸŒ ์›น ์ธํ„ฐํŽ˜์ด์Šค ์ถ”๊ฐ€ํ•˜๊ธฐ

ํ„ฐ๋ฏธ๋„์—์„œ๋งŒ ์‚ฌ์šฉํ•˜๋Š” ๊ฑด ์ข€ ๋ถˆํŽธํ•˜์ฃ ? Streamlit์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ‹์ง„ ์›น UI๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด์š”! ๐ŸŽจ

# streamlit_app.py
import streamlit as st
from chatbot import DocumentFAQChatbot

# ํŽ˜์ด์ง€ ์„ค์ •
st.set_page_config(
    page_title="FAQ ์ฑ—๋ด‡",
    page_icon="๐Ÿค–",
    layout="wide"
)

# ์ œ๋ชฉ
st.title("๐Ÿค– ๋ฌธ์„œ ๊ธฐ๋ฐ˜ FAQ ์ฑ—๋ด‡")
st.markdown("---")

# ์‚ฌ์ด๋“œ๋ฐ”
with st.sidebar:
    st.header("โš™๏ธ ์„ค์ •")
    documents_path = st.text_input(
        "๋ฌธ์„œ ํด๋” ๊ฒฝ๋กœ",
        value="./documents"
    )
    
    if st.button("์ฑ—๋ด‡ ์ดˆ๊ธฐํ™”"):
        with st.spinner("์ฑ—๋ด‡์„ ์ค€๋น„ํ•˜๋Š” ์ค‘..."):
            st.session_state.chatbot = DocumentFAQChatbot(documents_path)
            st.session_state.chatbot.setup()
            st.success("์ค€๋น„ ์™„๋ฃŒ! โœ…")

# ์„ธ์…˜ ์ƒํƒœ ์ดˆ๊ธฐํ™”
if 'messages' not in st.session_state:
    st.session_state.messages = []

if 'chatbot' not in st.session_state:
    st.info("๐Ÿ‘ˆ ์‚ฌ์ด๋“œ๋ฐ”์—์„œ ์ฑ—๋ด‡์„ ์ดˆ๊ธฐํ™”ํ•ด์ฃผ์„ธ์š”!")
    st.stop()

# ๋Œ€ํ™” ๊ธฐ๋ก ํ‘œ์‹œ
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

# ์‚ฌ์šฉ์ž ์ž…๋ ฅ
if prompt := st.chat_input("์งˆ๋ฌธ์„ ์ž…๋ ฅํ•˜์„ธ์š”..."):
    # ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)
    
    # ์ฑ—๋ด‡ ์‘๋‹ต
    with st.chat_message("assistant"):
        with st.spinner("์ƒ๊ฐํ•˜๋Š” ์ค‘..."):
            answer, sources = st.session_state.chatbot.ask(prompt)
            st.markdown(answer)
            
            # ์ถœ์ฒ˜ ํ‘œ์‹œ
            with st.expander("๐Ÿ“š ์ฐธ๊ณ  ๋ฌธ์„œ"):
                for i, doc in enumerate(sources, 1):
                    st.text(f"{i}. {doc.metadata.get('source', '์•Œ ์ˆ˜ ์—†์Œ')}")
    
    st.session_state.messages.append({"role": "assistant", "content": answer})

๐ŸŽจ Streamlit ์‹คํ–‰ํ•˜๊ธฐ:

pip install streamlit๋กœ ์„ค์น˜ํ•œ ํ›„,
streamlit run streamlit_app.py๋กœ ์‹คํ–‰ํ•˜๋ฉด ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋ฉ‹์ง„ ์ฑ—๋ด‡ UI๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์–ด์š”! ๐ŸŒŸ

๐Ÿ›ก๏ธ ๋ณด์•ˆ ๋ฐ ์—๋Ÿฌ ์ฒ˜๋ฆฌ

์‹ค์ œ ์„œ๋น„์Šค์—์„œ๋Š” ๋ณด์•ˆ๊ณผ ์•ˆ์ •์„ฑ์ด ๋งค์šฐ ์ค‘์š”ํ•ด์š”. ๋ช‡ ๊ฐ€์ง€ ํ•„์ˆ˜ ์‚ฌํ•ญ๋“ค์„ ์ฒดํฌํ•ด๋ณผ๊ฒŒ์š”! ๐Ÿ”’

โš ๏ธ ์ฃผ์˜ํ•ด์•ผ ํ•  ๋ณด์•ˆ ์‚ฌํ•ญ

1. API ํ‚ค ๋ณดํ˜ธ: ์ ˆ๋Œ€ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ๋งˆ์„ธ์š”. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์‚ฌ์šฉ ํ•„์ˆ˜!
2. ์ž…๋ ฅ ๊ฒ€์ฆ: ์•…์˜์ ์ธ ์ž…๋ ฅ์„ ํ•„ํ„ฐ๋งํ•˜์„ธ์š”.
3. ๋น„์šฉ ์ œํ•œ: API ํ˜ธ์ถœ ํšŸ์ˆ˜์™€ ํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰์„ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜์„ธ์š”.
4. ๋ฏผ๊ฐ ์ •๋ณด: ๋ฌธ์„œ์— ๊ฐœ์ธ์ •๋ณด๊ฐ€ ํฌํ•จ๋˜์ง€ ์•Š๋„๋ก ์ฃผ์˜ํ•˜์„ธ์š”.
5. ์ ‘๊ทผ ์ œ์–ด: ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋งŒ ์ฑ—๋ด‡์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์„ธ์š”.

import re
from typing import Optional

class SecureChatbot(DocumentFAQChatbot):
    """๋ณด์•ˆ ๊ธฐ๋Šฅ์ด ๊ฐ•ํ™”๋œ ์ฑ—๋ด‡"""
    
    def __init__(self, *args, max_query_length=500, **kwargs):
        super().__init__(*args, **kwargs)
        self.max_query_length = max_query_length
        self.query_count = 0
        self.max_queries_per_session = 100
    
    def validate_input(self, question: str) -> Optional[str]:
        """์ž…๋ ฅ ๊ฒ€์ฆ"""
        # ๊ธธ์ด ์ฒดํฌ
        if len(question) > self.max_query_length:
            return f"์งˆ๋ฌธ์ด ๋„ˆ๋ฌด ๊น๋‹ˆ๋‹ค. {self.max_query_length}์ž ์ด๋‚ด๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."
        
        # ๋นˆ ์ž…๋ ฅ ์ฒดํฌ
        if not question.strip():
            return "์งˆ๋ฌธ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."
        
        # ์•…์˜์  ํŒจํ„ด ์ฒดํฌ (์˜ˆ์‹œ)
        dangerous_patterns = [
            r'<script',
            r'javascript:',
            r'onerror=',
        ]
        
        for pattern in dangerous_patterns:
            if re.search(pattern, question, re.IGNORECASE):
                return "์œ ํšจํ•˜์ง€ ์•Š์€ ์ž…๋ ฅ์ž…๋‹ˆ๋‹ค."
        
        return None
    
    def ask(self, question: str):
        """๊ฒ€์ฆ๋œ ์งˆ๋ฌธ๋งŒ ์ฒ˜๋ฆฌ"""
        # ์ฟผ๋ฆฌ ํšŸ์ˆ˜ ์ฒดํฌ
        if self.query_count >= self.max_queries_per_session:
            raise Exception("์„ธ์…˜๋‹น ์ตœ๋Œ€ ์งˆ๋ฌธ ํšŸ์ˆ˜๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค.")
        
        # ์ž…๋ ฅ ๊ฒ€์ฆ
        error = self.validate_input(question)
        if error:
            raise ValueError(error)
        
        self.query_count += 1
        
        try:
            return super().ask(question)
        except Exception as e:
            print(f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
            return "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์ผ์‹œ์ ์ธ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.", []

๐Ÿ“ˆ ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ฐœ์„ 

์ฑ—๋ด‡์„ ๋ฐฐํฌํ•œ ํ›„์—๋Š” ์„ฑ๋Šฅ์„ ์ง€์†์ ์œผ๋กœ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ณ  ๊ฐœ์„ ํ•ด์•ผ ํ•ด์š”. ์–ด๋–ค ์ง€ํ‘œ๋“ค์„ ๋ด์•ผ ํ• ๊นŒ์š”? ๐Ÿ“Š

๐ŸŽฏ ์ฃผ์š” ์„ฑ๋Šฅ ์ง€ํ‘œ (KPI)

1. ์‘๋‹ต ์‹œ๊ฐ„: ํ‰๊ท  ๋ช‡ ์ดˆ ๋งŒ์— ๋‹ต๋ณ€ํ•˜๋Š”๊ฐ€?
2. ๋‹ต๋ณ€ ์ •ํ™•๋„: ์‚ฌ์šฉ์ž๊ฐ€ ๋งŒ์กฑํ•˜๋Š” ๋‹ต๋ณ€ ๋น„์œจ
3. ๋ฌธ์„œ ๊ฒ€์ƒ‰ ์ •ํ™•๋„: ๊ด€๋ จ ๋ฌธ์„œ๋ฅผ ์ž˜ ์ฐพ๋Š”๊ฐ€?
4. ์‚ฌ์šฉ์ž ๋งŒ์กฑ๋„: ํ”ผ๋“œ๋ฐฑ ์ ์ˆ˜
5. ๋น„์šฉ ํšจ์œจ์„ฑ: API ํ˜ธ์ถœ๋‹น ๋น„์šฉ
6. ์˜ค๋ฅ˜์œจ: ์‹คํŒจํ•œ ์š”์ฒญ ๋น„์œจ

import time
import logging
from datetime import datetime

class MonitoredChatbot(DocumentFAQChatbot):
    """๋ชจ๋‹ˆํ„ฐ๋ง ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€๋œ ์ฑ—๋ด‡"""
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setup_logging()
        self.metrics = {
            'total_queries': 0,
            'successful_queries': 0,
            'failed_queries': 0,
            'total_response_time': 0,
            'average_response_time': 0
        }
    
    def setup_logging(self):
        """๋กœ๊น… ์„ค์ •"""
        logging.basicConfig(
            filename='chatbot.log',
            level=logging.INFO,
            format='%(asctime)s - %(levelname)s - %(message)s'
        )
        self.logger = logging.getLogger(__name__)
    
    def ask(self, question: str):
        """๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ํ•จ๊ป˜ ์งˆ๋ฌธ ์ฒ˜๋ฆฌ"""
        start_time = time.time()
        self.metrics['total_queries'] += 1
        
        try:
            self.logger.info(f"์งˆ๋ฌธ ์ˆ˜์‹ : {question}")
            result = super().ask(question)
            
            response_time = time.time() - start_time
            self.metrics['successful_queries'] += 1
            self.metrics['total_response_time'] += response_time
            self.metrics['average_response_time'] = (
                self.metrics['total_response_time'] / 
                self.metrics['successful_queries']
            )
            
            self.logger.info(
                f"๋‹ต๋ณ€ ์™„๋ฃŒ (์‘๋‹ต์‹œ๊ฐ„: {response_time:.2f}์ดˆ)"
            )
            
            return result
            
        except Exception as e:
            self.metrics['failed_queries'] += 1
            self.logger.error(f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
            raise
    
    def get_metrics(self):
        """์„ฑ๋Šฅ ์ง€ํ‘œ ์กฐํšŒ"""
        return self.metrics

๐ŸŽ“ ์‹ค์ „ ํ™œ์šฉ ์‚ฌ๋ก€

์ด๋ก ์€ ์ถฉ๋ถ„ํžˆ ๋ฐฐ์› ์œผ๋‹ˆ, ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ๊ตฌ์ฒด์ ์ธ ์˜ˆ์‹œ๋ฅผ ๋ณผ๊นŒ์š”? ๐Ÿ’ผ

๐Ÿข ๊ธฐ์—… ๊ณ ๊ฐ์ง€์› ์ œํ’ˆ ๋งค๋‰ด์–ผ ๊ธฐ๋ฐ˜ 24์‹œ๊ฐ„ ์ž๋™ ์‘๋‹ต ๋‹ค๊ตญ์–ด ์ง€์› ๊ฐ€๋Šฅ ๊ณ ๊ฐ ๋งŒ์กฑ๋„ ํ–ฅ์ƒ ๐Ÿฅ ์˜๋ฃŒ ์ •๋ณด ์•ˆ๋‚ด ์ฆ์ƒ๋ณ„ ์ดˆ๊ธฐ ๋Œ€์‘ ์•ฝ๋ฌผ ์ •๋ณด ์ œ๊ณต ๋ณ‘์› ์ด์šฉ ์•ˆ๋‚ด ์‘๊ธ‰ ์ƒํ™ฉ ๋Œ€์ฒ˜ ๐ŸŽ“ ๊ต์œก ๋„์šฐ๋ฏธ ๊ฐ•์˜ ์ž๋ฃŒ ๊ธฐ๋ฐ˜ Q&A ๊ณผ์ œ ๊ฐ€์ด๋“œ ์ œ๊ณต ํ•™์Šต ์ง„๋„ ๊ด€๋ฆฌ ๊ฐœ์ธํ™”๋œ ํ•™์Šต ์ง€์› ๐Ÿ›๏ธ ๋ฒ•๋ฅ  ์ƒ๋‹ด ๋ฒ•๋ น ๋ฐ ํŒ๋ก€ ๊ฒ€์ƒ‰ ์ดˆ๊ธฐ ๋ฒ•๋ฅ  ์ž๋ฌธ ๊ณ„์•ฝ์„œ ๊ฒ€ํ†  ์ง€์› ์†Œ์†ก ์ ˆ์ฐจ ์•ˆ๋‚ด ๐Ÿช ์ด์ปค๋จธ์Šค ์ƒํ’ˆ ์ •๋ณด ์•ˆ๋‚ด ๋ฐฐ์†ก/๋ฐ˜ํ’ˆ ์ •์ฑ… ์‚ฌ์ด์ฆˆ ์ถ”์ฒœ ์ฃผ๋ฌธ ์ƒํƒœ ์กฐํšŒ ๐Ÿข HR ์ง€์› ์ธ์‚ฌ ๊ทœ์ • ์•ˆ๋‚ด ๋ณต๋ฆฌํ›„์ƒ ์ •๋ณด ํœด๊ฐ€ ์‹ ์ฒญ ์ ˆ์ฐจ ๊ธ‰์—ฌ ๊ด€๋ จ ๋ฌธ์˜

๐Ÿ’ผ ์‚ฌ๋ก€ 1: ์Šคํƒ€ํŠธ์—…์˜ ๊ณ ๊ฐ ์ง€์› ์ž๋™ํ™”

ํ•œ ์Šคํƒ€ํŠธ์—…์ด ์ œํ’ˆ ๋งค๋‰ด์–ผ๊ณผ FAQ ๋ฌธ์„œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฑ—๋ด‡์„ ๊ตฌ์ถ•ํ–ˆ์–ด์š”. ๊ฒฐ๊ณผ๋Š”?

โ€ข ๊ณ ๊ฐ ๋ฌธ์˜ ์‘๋‹ต ์‹œ๊ฐ„: ํ‰๊ท  2์‹œ๊ฐ„ โ†’ ์ฆ‰์‹œ ์‘๋‹ต
โ€ข ๊ณ ๊ฐ ์ง€์› ๋น„์šฉ: ์›” 500๋งŒ์› โ†’ ์›” 50๋งŒ์› (90% ์ ˆ๊ฐ)
โ€ข ๊ณ ๊ฐ ๋งŒ์กฑ๋„: 3.5์  โ†’ 4.2์  (5์  ๋งŒ์ )
โ€ข ์ง์› ์—…๋ฌด ํšจ์œจ: ๋ฐ˜๋ณต ์งˆ๋ฌธ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ 80% ๊ฐ์†Œ

ํŠนํžˆ ์•ผ๊ฐ„์ด๋‚˜ ์ฃผ๋ง์—๋„ ์ฆ‰์‹œ ๋‹ต๋ณ€์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์„œ ๊ณ ๊ฐ๋“ค์˜ ๋งŒ์กฑ๋„๊ฐ€ ํฌ๊ฒŒ ์˜ฌ๋ž๋‹ค๊ณ  ํ•ด์š”! ๐ŸŒ™

๐ŸŽ“ ์‚ฌ๋ก€ 2: ๋Œ€ํ•™๊ต ํ•™์Šต ์ง€์› ์‹œ์Šคํ…œ

ํ•œ ๋Œ€ํ•™์—์„œ ๊ฐ•์˜ ์ž๋ฃŒ์™€ ๊ต์žฌ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•™์Šต ๋„์šฐ๋ฏธ ์ฑ—๋ด‡์„ ๋งŒ๋“ค์—ˆ์–ด์š”:

โ€ข ํ•™์ƒ ์งˆ๋ฌธ ์ฒ˜๋ฆฌ: ๊ต์ˆ˜ 1๋ช…๋‹น ํ•˜๋ฃจ 50๊ฑด โ†’ ์ฑ—๋ด‡์ด 70% ์ž๋™ ์ฒ˜๋ฆฌ
โ€ข ํ•™์Šต ํšจ์œจ: ๊ถ๊ธˆํ•œ ์ ์„ ์ฆ‰์‹œ ํ•ด๊ฒฐํ•˜์—ฌ ํ•™์Šต ์†๋„ ํ–ฅ์ƒ
โ€ข ๊ต์ˆ˜ ๋งŒ์กฑ๋„: ๋ฐ˜๋ณต ์งˆ๋ฌธ ๊ฐ์†Œ๋กœ ์‹ฌํ™” ํ•™์Šต ์ง€๋„์— ์ง‘์ค‘ ๊ฐ€๋Šฅ
โ€ข 24์‹œ๊ฐ„ ํ•™์Šต ์ง€์›: ์ƒˆ๋ฒฝ์— ๊ณต๋ถ€ํ•˜๋Š” ํ•™์ƒ๋“ค๋„ ๋„์›€ ๋ฐ›์„ ์ˆ˜ ์žˆ์Œ

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์ด๋Ÿฐ ์ฑ—๋ด‡์„ ํ™œ์šฉํ•˜๋ฉด ์‚ฌ์šฉ์ž๋“ค์ด ์„œ๋น„์Šค๋ฅผ ๋” ์‰ฝ๊ฒŒ ์ด์šฉํ•  ์ˆ˜ ์žˆ๊ฒ ์ฃ ? ๐Ÿ˜Š

๐Ÿ”ง ๋ฌธ์ œ ํ•ด๊ฒฐ ๊ฐ€์ด๋“œ

๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋ฉด ์—ฌ๋Ÿฌ ๋ฌธ์ œ์— ๋ถ€๋”ชํžˆ๊ฒŒ ๋ผ์š”. ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋“ค๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์ •๋ฆฌํ•ด๋ดค์–ด์š”! ๐Ÿ› ๏ธ

โ“ ์ž์ฃผ ๋ฌป๋Š” ์งˆ๋ฌธ (FAQ)

Q1: "๋‹ต๋ณ€์ด ๋„ˆ๋ฌด ๋А๋ ค์š”!"
A: ์—ฌ๋Ÿฌ ์›์ธ์ด ์žˆ์„ ์ˆ˜ ์žˆ์–ด์š”:
โ€ข ๋ฌธ์„œ๊ฐ€ ๋„ˆ๋ฌด ๋งŽ๊ฑฐ๋‚˜ ์ฒญํฌ ํฌ๊ธฐ๊ฐ€ ํผ โ†’ ์ฒญํฌ ํฌ๊ธฐ๋ฅผ ์ค„์ด๊ณ  ๊ฒ€์ƒ‰ ๊ฐœ์ˆ˜(k) ์กฐ์ •
โ€ข ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ์ด ๋А๋ฆผ โ†’ ๋” ๋น ๋ฅธ ๋ชจ๋ธ๋กœ ๋ณ€๊ฒฝ
โ€ข ๋„คํŠธ์›Œํฌ ์ง€์—ฐ โ†’ ๋กœ์ปฌ ๋ชจ๋ธ ์‚ฌ์šฉ ๊ณ ๋ ค

Q2: "๋‹ต๋ณ€์ด ๋ถ€์ •ํ™•ํ•ด์š”!"
A: ๋‹ค์Œ์„ ์ฒดํฌํ•ด๋ณด์„ธ์š”:
โ€ข ๋ฌธ์„œ ํ’ˆ์งˆ์ด ๋‚ฎ์Œ โ†’ ๋ฌธ์„œ๋ฅผ ๋” ๋ช…ํ™•ํ•˜๊ฒŒ ์ž‘์„ฑ
โ€ข ์ฒญํฌ ํฌ๊ธฐ๊ฐ€ ๋ถ€์ ์ ˆ โ†’ 500-1000์ž๋กœ ์กฐ์ •
โ€ข ๊ฒ€์ƒ‰ ๊ฐœ์ˆ˜๊ฐ€ ๋ถ€์กฑ โ†’ k ๊ฐ’์„ 3-5๋กœ ์„ค์ •
โ€ข ํ”„๋กฌํ”„ํŠธ๊ฐ€ ๋ถˆ๋ช…ํ™• โ†’ ๋” ๊ตฌ์ฒด์ ์ธ ์ง€์‹œ์‚ฌํ•ญ ์ถ”๊ฐ€

Q3: "๋น„์šฉ์ด ๋„ˆ๋ฌด ๋งŽ์ด ๋‚˜์™€์š”!"
A: ๋น„์šฉ ์ ˆ๊ฐ ๋ฐฉ๋ฒ•:
โ€ข ์บ์‹ฑ ํ™œ์šฉ์œผ๋กœ ์ค‘๋ณต ์š”์ฒญ ๋ฐฉ์ง€
โ€ข ๋” ์ €๋ ดํ•œ ๋ชจ๋ธ ์‚ฌ์šฉ (gpt-3.5-turbo)
โ€ข ํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์ œํ•œ
โ€ข ๋ถˆํ•„์š”ํ•œ ๋ฌธ์„œ ์ œ๊ฑฐ

Q4: "ํ•œ๊ธ€ ๋ฌธ์„œ๊ฐ€ ์ œ๋Œ€๋กœ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์•„์š”!"
A: ์ธ์ฝ”๋”ฉ ๋ฌธ์ œ์ผ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์•„์š”:
โ€ข TextLoader์— encoding='utf-8' ๋ช…์‹œ
โ€ข PDF์˜ ๊ฒฝ์šฐ ํ•œ๊ธ€ ํฐํŠธ๊ฐ€ ์ž„๋ฒ ๋”ฉ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ
โ€ข ํ•„์š”์‹œ OCR ๋„๊ตฌ ์‚ฌ์šฉ ๊ณ ๋ ค

๐Ÿ› ๋””๋ฒ„๊น… ํŒ

# ๋””๋ฒ„๊น… ๋ชจ๋“œ ํ™œ์„ฑํ™”
import logging

logging.basicConfig(level=logging.DEBUG)

# ๊ฒ€์ƒ‰๋œ ๋ฌธ์„œ ํ™•์ธํ•˜๊ธฐ
def debug_retrieval(vectorstore, query, k=3):
    """๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ธํžˆ ํ™•์ธ"""
    retriever = vectorstore.as_retriever(search_kwargs={"k": k})
    docs = retriever.get_relevant_documents(query)
    
    print(f"\n์งˆ๋ฌธ: {query}")
    print(f"๊ฒ€์ƒ‰๋œ ๋ฌธ์„œ ์ˆ˜: {len(docs)}\n")
    
    for i, doc in enumerate(docs, 1):
        print(f"--- ๋ฌธ์„œ {i} ---")
        print(f"์ถœ์ฒ˜: {doc.metadata.get('source', '์•Œ ์ˆ˜ ์—†์Œ')}")
        print(f"๋‚ด์šฉ ๋ฏธ๋ฆฌ๋ณด๊ธฐ: {doc.page_content[:200]}...")
        print()

# ์‚ฌ์šฉ ์˜ˆ์‹œ
debug_retrieval(vectorstore, "ํ™˜๋ถˆ ์ •์ฑ…์ด ์–ด๋–ป๊ฒŒ ๋˜๋‚˜์š”?")

๐Ÿš€ ๋ฐฐํฌ ๋ฐ ์šด์˜

์ฑ—๋ด‡์„ ๋งŒ๋“ค์—ˆ์œผ๋ฉด ์ด์ œ ์‹ค์ œ๋กœ ์‚ฌ์šฉ์ž๋“ค์—๊ฒŒ ์ œ๊ณตํ•ด์•ผ๊ฒ ์ฃ ? ๋ฐฐํฌ ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊ฒŒ์š”! ๐ŸŒ

โ˜๏ธ ํด๋ผ์šฐ๋“œ ๋ฐฐํฌ ์˜ต์…˜

ํ”Œ๋žซํผ ์žฅ์  ๋‹จ์  ์ถ”์ฒœ ๋Œ€์ƒ
Streamlit Cloud ๋ฌด๋ฃŒ, ์‰ฌ์šด ๋ฐฐํฌ ์ œํ•œ๋œ ๋ฆฌ์†Œ์Šค ํ”„๋กœํ† ํƒ€์ž…, ์†Œ๊ทœ๋ชจ
Heroku ๊ฐ„๋‹จํ•œ ์„ค์ • ์œ ๋ฃŒ, ์†๋„ ์ œํ•œ ์ค‘์†Œ๊ทœ๋ชจ ์„œ๋น„์Šค
AWS EC2 ์™„์ „ํ•œ ์ œ์–ด ๋ณต์žกํ•œ ์„ค์ • ๋Œ€๊ทœ๋ชจ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ
Google Cloud Run ์ž๋™ ์Šค์ผ€์ผ๋ง ๋น„์šฉ ๊ด€๋ฆฌ ํ•„์š” ํŠธ๋ž˜ํ”ฝ ๋ณ€๋™ ํฐ ์„œ๋น„์Šค
Azure App Service MS ์ƒํƒœ๊ณ„ ํ†ตํ•ฉ ํ•™์Šต ๊ณก์„  ๊ธฐ์—… ํ™˜๊ฒฝ

๐Ÿณ Docker๋กœ ์ปจํ…Œ์ด๋„ˆํ™”ํ•˜๊ธฐ

Docker๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์–ด๋””์„œ๋“  ๋™์ผํ•œ ํ™˜๊ฒฝ์—์„œ ์ฑ—๋ด‡์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด์š”:

# Dockerfile
FROM python:3.9-slim

WORKDIR /app

# ์˜์กด์„ฑ ์„ค์น˜
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ ๋ณต์‚ฌ
COPY . .

# ํฌํŠธ ๋…ธ์ถœ
EXPOSE 8501

# Streamlit ์‹คํ–‰
CMD ["streamlit", "run", "streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
# docker-compose.yml
version: '3.8'

services:
  chatbot:
    build: .
    ports:
      - "8501:8501"
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
    volumes:
      - ./documents:/app/documents
      - ./chroma_db:/app/chroma_db
    restart: unless-stopped

๐Ÿ‹ Docker ์‚ฌ์šฉ๋ฒ•:

1. docker-compose up -d๋กœ ์‹คํ–‰
2. http://localhost:8501์—์„œ ์ ‘์†
3. docker-compose down์œผ๋กœ ์ข…๋ฃŒ

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๊ฐœ๋ฐœ ํ™˜๊ฒฝ๊ณผ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์„ ๋™์ผํ•˜๊ฒŒ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์–ด์š”!

๐Ÿ“Š ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ์™„์ „ํ•œ FAQ ์‹œ์Šคํ…œ ๊ตฌ์ถ•

์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๋ชจ๋“  ๋‚ด์šฉ์„ ์ข…ํ•ฉํ•ด์„œ ์‹ค์ œ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์™„์ „ํ•œ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ์š”! ๐ŸŽฏ

๐Ÿ“ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

faq-chatbot/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ chatbot.py          # ์ฑ—๋ด‡ ํ•ต์‹ฌ ๋กœ์ง
โ”‚   โ”œโ”€โ”€ config.py           # ์„ค์ • ๊ด€๋ฆฌ
โ”‚   โ””โ”€โ”€ utils.py            # ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜
โ”œโ”€โ”€ documents/              # ๋ฌธ์„œ ์ €์žฅ ํด๋”
โ”‚   โ”œโ”€โ”€ faq.pdf
โ”‚   โ”œโ”€โ”€ manual.pdf
โ”‚   โ””โ”€โ”€ policy.txt
โ”œโ”€โ”€ chroma_db/             # ๋ฒกํ„ฐ DB (์ž๋™ ์ƒ์„ฑ)
โ”œโ”€โ”€ logs/                  # ๋กœ๊ทธ ํŒŒ์ผ
โ”œโ”€โ”€ tests/                 # ํ…Œ์ŠคํŠธ ์ฝ”๋“œ
โ”‚   โ””โ”€โ”€ test_chatbot.py
โ”œโ”€โ”€ streamlit_app.py       # ์›น UI
โ”œโ”€โ”€ requirements.txt       # ์˜์กด์„ฑ ๋ชฉ๋ก
โ”œโ”€โ”€ .env                   # ํ™˜๊ฒฝ ๋ณ€์ˆ˜
โ”œโ”€โ”€ .gitignore
โ”œโ”€โ”€ Dockerfile
โ”œโ”€โ”€ docker-compose.yml
โ””โ”€โ”€ README.md

โš™๏ธ config.py - ์„ค์ • ๊ด€๋ฆฌ

import os
from dotenv import load_dotenv

load_dotenv()

class Config:
    """์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ค์ •"""
    
    # OpenAI ์„ค์ •
    OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
    OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-3.5-turbo")
    EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "text-embedding-ada-002")
    
    # ๋ฌธ์„œ ์ฒ˜๋ฆฌ ์„ค์ •
    DOCUMENTS_PATH = os.getenv("DOCUMENTS_PATH", "./documents")
    CHUNK_SIZE = int(os.getenv("CHUNK_SIZE", "1000"))
    CHUNK_OVERLAP = int(os.getenv("CHUNK_OVERLAP", "200"))
    
    # ๋ฒกํ„ฐ DB ์„ค์ •
    VECTOR_DB_PATH = os.getenv("VECTOR_DB_PATH", "./chroma_db")
    SEARCH_K = int(os.getenv("SEARCH_K", "3"))
    
    # ์ฑ—๋ด‡ ์„ค์ •
    TEMPERATURE = float(os.getenv("TEMPERATURE", "0.3"))
    MAX_TOKENS = int(os.getenv("MAX_TOKENS", "500"))
    
    # ๋ณด์•ˆ ์„ค์ •
    MAX_QUERY_LENGTH = int(os.getenv("MAX_QUERY_LENGTH", "500"))
    MAX_QUERIES_PER_SESSION = int(os.getenv("MAX_QUERIES_PER_SESSION", "100"))
    
    # ๋กœ๊น… ์„ค์ •
    LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
    LOG_FILE = os.getenv("LOG_FILE", "./logs/chatbot.log")

๐Ÿงช ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ

์•ˆ์ •์ ์ธ ์„œ๋น„์Šค๋ฅผ ์œ„ํ•ด์„œ๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ํ•„์ˆ˜์˜ˆ์š”! ๊ฐ„๋‹จํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณผ๊ฒŒ์š”:

import unittest
from app.chatbot import DocumentFAQChatbot

class TestChatbot(unittest.TestCase):
    """์ฑ—๋ด‡ ํ…Œ์ŠคํŠธ"""
    
    @classmethod
    def setUpClass(cls):
        """ํ…Œ์ŠคํŠธ ์‹œ์ž‘ ์ „ ์ฑ—๋ด‡ ์ดˆ๊ธฐํ™”"""
        cls.chatbot = DocumentFAQChatbot("./test_documents")
        cls.chatbot.setup()
    
    def test_basic_query(self):
        """๊ธฐ๋ณธ ์งˆ๋ฌธ ํ…Œ์ŠคํŠธ"""
        answer, sources = self.chatbot.ask("ํ™˜๋ถˆ ์ •์ฑ…์ด ๋ญ”๊ฐ€์š”?")
        self.assertIsNotNone(answer)
        self.assertGreater(len(sources), 0)
    
    def test_empty_query(self):
        """๋นˆ ์งˆ๋ฌธ ํ…Œ์ŠคํŠธ"""
        with self.assertRaises(ValueError):
            self.chatbot.ask("")
    
    def test_long_query(self):
        """๊ธด ์งˆ๋ฌธ ํ…Œ์ŠคํŠธ"""
        long_query = "ํ…Œ์ŠคํŠธ " * 200
        with self.assertRaises(ValueError):
            self.chatbot.ask(long_query)
    
    def test_cache(self):
        """์บ์‹ฑ ํ…Œ์ŠคํŠธ"""
        question = "๋ฐฐ์†ก์€ ์–ผ๋งˆ๋‚˜ ๊ฑธ๋ฆฌ๋‚˜์š”?"
        answer1, _ = self.chatbot.ask(question)
        answer2, _ = self.chatbot.ask(question)
        self.assertEqual(answer1, answer2)

if __name__ == '__main__':
    unittest.main()

๐ŸŽจ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„ ํ•˜๊ธฐ

๊ธฐ์ˆ ์ ์œผ๋กœ ์™„๋ฒฝํ•ด๋„ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ๋‚˜์˜๋ฉด ์•„๋ฌด๋„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„์š”. UX๋ฅผ ๊ฐœ์„ ํ•˜๋Š” ๋ช‡ ๊ฐ€์ง€ ํŒ์„ ๊ณต์œ ํ• ๊ฒŒ์š”! โœจ

๐Ÿ’ก UX ๊ฐœ์„  ์ฒดํฌ๋ฆฌ์ŠคํŠธ

โœ… ๋น ๋ฅธ ์‘๋‹ต: 3์ดˆ ์ด๋‚ด ๋‹ต๋ณ€ ์ œ๊ณต (๋˜๋Š” ๋กœ๋”ฉ ํ‘œ์‹œ)
โœ… ๋ช…ํ™•ํ•œ ํ”ผ๋“œ๋ฐฑ: ์ฒ˜๋ฆฌ ์ค‘์ž„์„ ์‹œ๊ฐ์ ์œผ๋กœ ํ‘œ์‹œ
โœ… ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€: ์นœ์ ˆํ•˜๊ณ  ๊ตฌ์ฒด์ ์ธ ์•ˆ๋‚ด
โœ… ๋Œ€ํ™” ๋งฅ๋ฝ: ์ด์ „ ๋Œ€ํ™” ๋‚ด์šฉ ์œ ์ง€
โœ… ์ œ์•ˆ ์งˆ๋ฌธ: ์ž์ฃผ ๋ฌป๋Š” ์งˆ๋ฌธ ๋ฒ„ํŠผ ์ œ๊ณต
โœ… ์ถœ์ฒ˜ ํ‘œ์‹œ: ๋‹ต๋ณ€์˜ ๊ทผ๊ฑฐ ๋ฌธ์„œ ๋ณด์—ฌ์ฃผ๊ธฐ
โœ… ํ”ผ๋“œ๋ฐฑ ์ˆ˜์ง‘: ๋‹ต๋ณ€์ด ๋„์›€๋˜์—ˆ๋Š”์ง€ ๋ฌผ์–ด๋ณด๊ธฐ
โœ… ๋‹ค๊ตญ์–ด ์ง€์›: ํ•„์š”์‹œ ์—ฌ๋Ÿฌ ์–ธ์–ด๋กœ ์ œ๊ณต
โœ… ๋ชจ๋ฐ”์ผ ์ตœ์ ํ™”: ์ž‘์€ ํ™”๋ฉด์—์„œ๋„ ์ž˜ ๋ณด์ด๊ฒŒ
โœ… ์ ‘๊ทผ์„ฑ: ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ๋“ฑ ๋ณด์กฐ ๊ธฐ์ˆ  ์ง€์›

๐ŸŽฏ ์ œ์•ˆ ์งˆ๋ฌธ ๊ธฐ๋Šฅ ์ถ”๊ฐ€

# streamlit_app.py์— ์ถ”๊ฐ€
SUGGESTED_QUESTIONS = [
    "ํ™˜๋ถˆ ์ •์ฑ…์ด ์–ด๋–ป๊ฒŒ ๋˜๋‚˜์š”?",
    "๋ฐฐ์†ก์€ ์–ผ๋งˆ๋‚˜ ๊ฑธ๋ฆฌ๋‚˜์š”?",
    "ํšŒ์› ๊ฐ€์ž…์€ ์–ด๋–ป๊ฒŒ ํ•˜๋‚˜์š”?",
    "๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์žŠ์–ด๋ฒ„๋ ธ์–ด์š”",
    "๊ฒฐ์ œ ๋ฐฉ๋ฒ•์€ ๋ฌด์—‡์ด ์žˆ๋‚˜์š”?"
]

st.markdown("### ๐Ÿ’ฌ ์ž์ฃผ ๋ฌป๋Š” ์งˆ๋ฌธ")
cols = st.columns(3)
for i, question in enumerate(SUGGESTED_QUESTIONS):
    col_idx = i % 3
    with cols[col_idx]:
        if st.button(question, key=f"suggested_{i}"):
            # ์งˆ๋ฌธ ์ž๋™ ์ž…๋ ฅ
            st.session_state.current_question = question

๐Ÿ‘ ํ”ผ๋“œ๋ฐฑ ์ˆ˜์ง‘ํ•˜๊ธฐ

# ๋‹ต๋ณ€ ํ›„ ํ”ผ๋“œ๋ฐฑ ๋ฒ„ํŠผ ์ถ”๊ฐ€
col1, col2 = st.columns(2)
with col1:
    if st.button("๐Ÿ‘ ๋„์›€์ด ๋˜์—ˆ์–ด์š”"):
        save_feedback(prompt, answer, positive=True)
        st.success("ํ”ผ๋“œ๋ฐฑ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!")

with col2:
    if st.button("๐Ÿ‘Ž ๋„์›€์ด ์•ˆ ๋˜์—ˆ์–ด์š”"):
        save_feedback(prompt, answer, positive=False)
        st.info("๋” ๋‚˜์€ ๋‹ต๋ณ€์„ ์ œ๊ณตํ•˜๋„๋ก ๊ฐœ์„ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.")

def save_feedback(question, answer, positive):
    """ํ”ผ๋“œ๋ฐฑ์„ ํŒŒ์ผ์— ์ €์žฅ"""
    import json
    from datetime import datetime
    
    feedback = {
        "timestamp": datetime.now().isoformat(),
        "question": question,
        "answer": answer,
        "positive": positive
    }
    
    with open("feedback.jsonl", "a", encoding="utf-8") as f:
        f.write(json.dumps(feedback, ensure_ascii=False) + "\n")

๐ŸŒŸ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ: RAG ์ตœ์ ํ™”

RAG(Retrieval-Augmented Generation)๋Š” ๋ฌธ์„œ ๊ธฐ๋ฐ˜ ์ฑ—๋ด‡์˜ ํ•ต์‹ฌ์ด์—์š”. ๋” ์ •๊ตํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผ๊ฒŒ์š”! ๐Ÿš€

๐Ÿ”„ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒ€์ƒ‰

๋ฒกํ„ฐ ๊ฒ€์ƒ‰๋งŒ ์‚ฌ์šฉํ•˜๋ฉด ํ‚ค์›Œ๋“œ๊ฐ€ ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋Š” ๋ฌธ์„œ๋ฅผ ๋†“์น  ์ˆ˜ ์žˆ์–ด์š”. ๋ฒกํ„ฐ ๊ฒ€์ƒ‰๊ณผ ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰์„ ๊ฒฐํ•ฉํ•˜๋ฉด ๋” ์ข‹์€ ๊ฒฐ๊ณผ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์ฃ :

from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever

def create_hybrid_retriever(vectorstore, documents):
    """ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒ€์ƒ‰๊ธฐ ์ƒ์„ฑ"""
    # ๋ฒกํ„ฐ ๊ฒ€์ƒ‰๊ธฐ
    vector_retriever = vectorstore.as_retriever(
        search_kwargs={"k": 5}
    )
    
    # ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰๊ธฐ (BM25)
    bm25_retriever = BM25Retriever.from_documents(documents)
    bm25_retriever.k = 5
    
    # ์•™์ƒ๋ธ” ๊ฒ€์ƒ‰๊ธฐ (๊ฐ€์ค‘์น˜ ์กฐํ•ฉ)
    ensemble_retriever = EnsembleRetriever(
        retrievers=[vector_retriever, bm25_retriever],
        weights=[0.7, 0.3]  # ๋ฒกํ„ฐ 70%, ํ‚ค์›Œ๋“œ 30%
    )
    
    return ensemble_retriever

๐ŸŽฏ ์žฌ์ˆœ์œ„ํ™” (Re-ranking)

๊ฒ€์ƒ‰๋œ ๋ฌธ์„œ๋“ค์„ ๋‹ค์‹œ ํ•œ๋ฒˆ ์ •๋ ฌํ•ด์„œ ๊ฐ€์žฅ ๊ด€๋ จ์„ฑ ๋†’์€ ๊ฒƒ๋งŒ ์‚ฌ์šฉํ•˜๋ฉด ๋‹ต๋ณ€ ํ’ˆ์งˆ์ด ์˜ฌ๋ผ๊ฐ€์š”:

from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor

def create_compressed_retriever(base_retriever, llm):
    """์••์ถ• ๊ฒ€์ƒ‰๊ธฐ ์ƒ์„ฑ (๊ด€๋ จ ๋ถ€๋ถ„๋งŒ ์ถ”์ถœ)"""
    compressor = LLMChainExtractor.from_llm(llm)
    
    compression_retriever = ContextualCompressionRetriever(
        base_compressor=compressor,
        base_retriever=base_retriever
    )
    
    return compression_retriever

๐Ÿ’ฐ ๋น„์šฉ ์ตœ์ ํ™” ์ „๋žต

OpenAI API๋Š” ์‚ฌ์šฉ๋Ÿ‰์— ๋”ฐ๋ผ ๋น„์šฉ์ด ๋ฐœ์ƒํ•ด์š”. ๋˜‘๋˜‘ํ•˜๊ฒŒ ๋น„์šฉ์„ ์ ˆ๊ฐํ•˜๋Š” ๋ฐฉ๋ฒ•๋“ค์„ ์•Œ์•„๋ณผ๊ฒŒ์š”! ๐Ÿ’ธ

๐Ÿ’ต ๋น„์šฉ ๋ฐœ์ƒ ์š”์†Œ

1. ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ: ๋ฌธ์„œ๋ฅผ ๋ฒกํ„ฐ๋กœ ๋ณ€ํ™˜ํ•  ๋•Œ
2. LLM ํ˜ธ์ถœ: ๋‹ต๋ณ€์„ ์ƒ์„ฑํ•  ๋•Œ
3. ํ† ํฐ ์‚ฌ์šฉ: ์ž…๋ ฅ๊ณผ ์ถœ๋ ฅ ํ† ํฐ ๋ชจ๋‘ ๊ณผ๊ธˆ

์˜ˆ์ƒ ๋น„์šฉ (gpt-3.5-turbo ๊ธฐ์ค€):
โ€ข ์ž„๋ฒ ๋”ฉ: $0.0001 / 1K ํ† ํฐ
โ€ข ์ž…๋ ฅ: $0.0015 / 1K ํ† ํฐ
โ€ข ์ถœ๋ ฅ: $0.002 / 1K ํ† ํฐ

1000๋ช…์ด ํ•˜๋ฃจ 10๋ฒˆ์”ฉ ์งˆ๋ฌธํ•˜๋ฉด ์›” ์•ฝ $300~500 ์ •๋„ ์˜ˆ์ƒ๋ผ์š”.

๐Ÿ’ก ๋น„์šฉ ์ ˆ๊ฐ ํŒ

1. ์บ์‹ฑ ์ ๊ทน ํ™œ์šฉ: ๋™์ผ ์งˆ๋ฌธ์€ ์žฌ๊ณ„์‚ฐํ•˜์ง€ ์•Š๊ธฐ
2. ์ฒญํฌ ํฌ๊ธฐ ์ตœ์ ํ™”: ๋ถˆํ•„์š”ํ•˜๊ฒŒ ํฌ์ง€ ์•Š๊ฒŒ
3. ๊ฒ€์ƒ‰ ๊ฐœ์ˆ˜ ์ œํ•œ: k=3 ์ •๋„๋ฉด ์ถฉ๋ถ„
4. ์งง์€ ํ”„๋กฌํ”„ํŠธ: ๋ถˆํ•„์š”ํ•œ ์„ค๋ช… ์ œ๊ฑฐ
5. ๋กœ์ปฌ ์ž„๋ฒ ๋”ฉ: ๊ฐ€๋Šฅํ•˜๋ฉด ์˜คํ”ˆ์†Œ์Šค ๋ชจ๋ธ ์‚ฌ์šฉ
6. ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ: ์—ฌ๋Ÿฌ ์š”์ฒญ์„ ๋ชจ์•„์„œ ์ฒ˜๋ฆฌ
7. ์‚ฌ์šฉ๋Ÿ‰ ๋ชจ๋‹ˆํ„ฐ๋ง: ์ด์ƒ ํŒจํ„ด ๊ฐ์ง€ ๋ฐ ์ œํ•œ
8. ์ €๋ ดํ•œ ๋ชจ๋ธ: gpt-4 ๋Œ€์‹  gpt-3.5-turbo ์‚ฌ์šฉ

๐Ÿ” ํ”„๋กœ๋•์…˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

์‹ค์ œ ์„œ๋น„์Šค์— ๋ฐฐํฌํ•˜๊ธฐ ์ „์— ๋ฐ˜๋“œ์‹œ ํ™•์ธํ•ด์•ผ ํ•  ์‚ฌํ•ญ๋“ค์ด์—์š”! โœ…

๐Ÿ“‹ ๋ฐฐํฌ ์ „ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

๋ณด์•ˆ
โ–ก API ํ‚ค๊ฐ€ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉ๋˜์ง€ ์•Š์•˜๋Š”๊ฐ€?
โ–ก ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌ๋˜๋Š”๊ฐ€?
โ–ก ์ž…๋ ฅ ๊ฒ€์ฆ์ด ์ œ๋Œ€๋กœ ๋˜๋Š”๊ฐ€?
โ–ก HTTPS๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๊ฐ€?
โ–ก ์ธ์ฆ/์ธ๊ฐ€๊ฐ€ ๊ตฌํ˜„๋˜์—ˆ๋Š”๊ฐ€?

์„ฑ๋Šฅ
โ–ก ์‘๋‹ต ์‹œ๊ฐ„์ด 3์ดˆ ์ด๋‚ด์ธ๊ฐ€?
โ–ก ์บ์‹ฑ์ด ์ ์šฉ๋˜์—ˆ๋Š”๊ฐ€?
โ–ก ๋™์‹œ ์‚ฌ์šฉ์ž๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š”๊ฐ€?
โ–ก ์—๋Ÿฌ ์ฒ˜๋ฆฌ๊ฐ€ ์ ์ ˆํ•œ๊ฐ€?

๋ชจ๋‹ˆํ„ฐ๋ง
โ–ก ๋กœ๊น…์ด ์„ค์ •๋˜์—ˆ๋Š”๊ฐ€?
โ–ก ์„ฑ๋Šฅ ์ง€ํ‘œ๋ฅผ ์ถ”์ ํ•˜๋Š”๊ฐ€?
โ–ก ์•Œ๋ฆผ ์‹œ์Šคํ…œ์ด ์žˆ๋Š”๊ฐ€?
โ–ก ๋ฐฑ์—… ๊ณ„ํš์ด ์žˆ๋Š”๊ฐ€?

์‚ฌ์šฉ์ž ๊ฒฝํ—˜
โ–ก ๋ชจ๋ฐ”์ผ์—์„œ ์ž˜ ์ž‘๋™ํ•˜๋Š”๊ฐ€?
โ–ก ๋กœ๋”ฉ ์ƒํƒœ๊ฐ€ ํ‘œ์‹œ๋˜๋Š”๊ฐ€?
โ–ก ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ์นœ์ ˆํ•œ๊ฐ€?
โ–ก ํ”ผ๋“œ๋ฐฑ์„ ์ˆ˜์ง‘ํ•˜๋Š”๊ฐ€?

๋ฌธ์„œ
โ–ก README๊ฐ€ ์ž‘์„ฑ๋˜์—ˆ๋Š”๊ฐ€?
โ–ก API ๋ฌธ์„œ๊ฐ€ ์žˆ๋Š”๊ฐ€?
โ–ก ์„ค์น˜ ๊ฐ€์ด๋“œ๊ฐ€ ๋ช…ํ™•ํ•œ๊ฐ€?
โ–ก ๋ฌธ์ œ ํ•ด๊ฒฐ ๊ฐ€์ด๋“œ๊ฐ€ ์žˆ๋Š”๊ฐ€?

๐ŸŽ“ ํ•™์Šต ๋ฆฌ์†Œ์Šค ๋ฐ ๋‹ค์Œ ๋‹จ๊ณ„

์—ฌ๊ธฐ๊นŒ์ง€ ๋”ฐ๋ผ์˜ค์‹œ๋А๋ผ ์ˆ˜๊ณ  ๋งŽ์œผ์…จ์–ด์š”! ๐ŸŽ‰ ๋” ๊นŠ์ด ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ด๋Ÿฐ ๋ฆฌ์†Œ์Šค๋“ค์„ ์ถ”์ฒœํ•ด์š”:

๐Ÿ“š ์ถ”์ฒœ ํ•™์Šต ์ž๋ฃŒ

๊ณต์‹ ๋ฌธ์„œ:
โ€ข LangChain ๊ณต์‹ ๋ฌธ์„œ: python.langchain.com
โ€ข OpenAI API ๋ฌธ์„œ: platform.openai.com/docs
โ€ข Streamlit ๋ฌธ์„œ: docs.streamlit.io

์˜จ๋ผ์ธ ๊ฐ•์˜:
โ€ข DeepLearning.AI - LangChain for LLM Application Development
โ€ข Udemy - Building AI Applications with LangChain
โ€ข YouTube - LangChain ํŠœํ† ๋ฆฌ์–ผ ์‹œ๋ฆฌ์ฆˆ

์ปค๋ฎค๋‹ˆํ‹ฐ:
โ€ข LangChain Discord ์„œ๋ฒ„
โ€ข Reddit r/LangChain
โ€ข Stack Overflow LangChain ํƒœ๊ทธ

๋ธ”๋กœ๊ทธ & ์•„ํ‹ฐํด:
โ€ข Towards Data Science
โ€ข Medium AI ์„น์…˜
โ€ข LangChain ๊ณต์‹ ๋ธ”๋กœ๊ทธ

๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋‚˜์•„๊ฐ€๊ธฐ

๊ธฐ๋ณธ์„ ๋งˆ์Šคํ„ฐํ–ˆ๋‹ค๋ฉด ์ด๋Ÿฐ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๋“ค์— ๋„์ „ํ•ด๋ณด์„ธ์š”:

1. ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ์ง€์›: ์ด๋ฏธ์ง€, ์˜ค๋””์˜ค๋„ ์ฒ˜๋ฆฌํ•˜๋Š” ์ฑ—๋ด‡
2. ์—์ด์ „ํŠธ ๊ตฌํ˜„: ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ž์œจ AI
3. ํŒŒ์ธํŠœ๋‹: ํŠน์ • ๋„๋ฉ”์ธ์— ์ตœ์ ํ™”๋œ ๋ชจ๋ธ
4. ์Œ์„ฑ ์ธํ„ฐํŽ˜์ด์Šค: ๋ง๋กœ ์งˆ๋ฌธํ•˜๊ณ  ๋‹ต๋ณ€ ๋“ฃ๊ธฐ
5. ๋‹ค๊ตญ์–ด ์ง€์›: ์—ฌ๋Ÿฌ ์–ธ์–ด๋กœ ์„œ๋น„์Šค ์ œ๊ณต
6. ์‹ค์‹œ๊ฐ„ ํ•™์Šต: ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ์œผ๋กœ ์ง€์† ๊ฐœ์„ 
7. ํ†ตํ•ฉ ์‹œ์Šคํ…œ: CRM, ํ‹ฐ์ผ“ํŒ… ์‹œ์Šคํ…œ๊ณผ ์—ฐ๋™
8. ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ: ์‚ฌ์šฉ ํŒจํ„ด ์‹œ๊ฐํ™”

๐ŸŽฌ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€! ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์ฃ ? ๐Ÿ˜… ํ•˜์ง€๋งŒ ์ด์ œ ์—ฌ๋Ÿฌ๋ถ„์€ LangChain์„ ์‚ฌ์šฉํ•ด์„œ ์‹ค์šฉ์ ์ธ ๋ฌธ์„œ ๊ธฐ๋ฐ˜ FAQ ์ฑ—๋ด‡์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ๋Šฅ๋ ฅ์„ ๊ฐ–์ถ”์…จ์–ด์š”! ๐ŸŽ‰

์ฒ˜์Œ์—๋Š” ๋ณต์žกํ•ด ๋ณด์˜€๋˜ ๊ฐœ๋…๋“ค์ด ์ด์ œ๋Š” ์ข€ ๋” ๋ช…ํ™•ํ•ด์กŒ์„ ๊ฑฐ์˜ˆ์š”. ๋ฌธ์„œ ๋กœ๋”ฉ๋ถ€ํ„ฐ ๋ฒกํ„ฐ ์ €์žฅ, ๊ฒ€์ƒ‰, ๋‹ต๋ณ€ ์ƒ์„ฑ๊นŒ์ง€์˜ ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ์„ ์ดํ•ดํ•˜๊ณ  ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์œผ๋‹ˆ๊นŒ์š”.

๊ธฐ์–ตํ•˜์„ธ์š”: ์™„๋ฒฝํ•œ ์ฑ—๋ด‡์€ ํ•œ ๋ฒˆ์— ๋งŒ๋“ค์–ด์ง€์ง€ ์•Š์•„์š”. ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์•„๊ฐ€๋ฉฐ ์ง€์†์ ์œผ๋กœ ๊ฐœ์„ ํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด์š”. ์ž‘๊ฒŒ ์‹œ์ž‘ํ•ด์„œ ์ ์ง„์ ์œผ๋กœ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ์‹์„ ์ถ”์ฒœ๋“œ๋ ค์š”! ๐ŸŒฑ

์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ „๋ฌธ ์ง€์‹์„ ๊ณต์œ ํ•  ๋•Œ๋„ ์ด๋Ÿฐ ์ฑ—๋ด‡์„ ํ™œ์šฉํ•˜๋ฉด ๋” ๋งŽ์€ ์‚ฌ๋žŒ๋“ค์—๊ฒŒ ํšจ์œจ์ ์œผ๋กœ ๋„์›€์„ ์ค„ ์ˆ˜ ์žˆ์„ ๊ฑฐ์˜ˆ์š”. ์—ฌ๋Ÿฌ๋ถ„์˜ ์ง€์‹์ด AI๋ฅผ ํ†ตํ•ด 24์‹œ๊ฐ„ ์ผํ•˜๋Š” ๋˜‘๋˜‘ํ•œ ๋น„์„œ๊ฐ€ ๋˜๋Š” ๊ฑฐ์ฃ ! ๐Ÿค–โœจ

๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๊ฑฐ๋‚˜ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด ์ฃผ์ €ํ•˜์ง€ ๋ง๊ณ  ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•˜์„ธ์š”. ์šฐ๋ฆฌ ๋ชจ๋‘ ํ•จ๊ป˜ ๋ฐฐ์›Œ๊ฐ€๋Š” ๊ฑฐ๋‹ˆ๊นŒ์š”! ๐Ÿ’ช

๊ทธ๋Ÿผ ์—ฌ๋Ÿฌ๋ถ„์˜ ๋ฉ‹์ง„ ์ฑ—๋ด‡ ํ”„๋กœ์ ํŠธ๋ฅผ ์‘์›ํ•ฉ๋‹ˆ๋‹ค! ํ™”์ดํŒ…! ๐Ÿš€๐ŸŽŠ

๐ŸŽ‰ ์ถ•ํ•˜ํ•ฉ๋‹ˆ๋‹ค!

LangChain ๋ฌธ์„œ ๊ธฐ๋ฐ˜ FAQ ์ฑ—๋ด‡ ๋งˆ์Šคํ„ฐ ๊ณผ์ •์„ ์™„๋ฃŒํ•˜์…จ์Šต๋‹ˆ๋‹ค!
์ด์ œ ์—ฌ๋Ÿฌ๋ถ„๋งŒ์˜ AI ์ฑ—๋ด‡์„ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”! ๐Ÿ’ชโœจ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0