8장. 최종 프로젝트
프로젝트 3 — 구름다리여행 종합 상담 챗봇
1장부터 7장까지 만든 구름다리여행의 부품을 모두 묶어 종합 상담 챗봇을 만듭니다. 손님은 한 대화 안에서 상품을 찾고("12월에 제주 가는 40만 원 이하 상품 있어요?"), 앞에서 고른 상품의 요금을 묻고("그거 어른 둘에 아이 하나면 얼마예요?"), 약관을 묻습니다("출발 5일 전에 취소하면 수수료는요?"). 약관·FAQ 검색 도구, 상품 찾기 도구, 요금 계산 도구를 에이전트에게 쥐여 주고, 대화 기억과 웹 화면을 붙인 뒤 시연 점검표로 확인해 보세요. 구름다리여행과 약관, 상품, FAQ 는 모두 연습용으로 지어낸 것입니다. 에이전트가 진짜 모델로 도구를 고르므로 과목 폴더의 .env 에 API 키가 있어야 돌아갑니다. 이 절의 출력은 API 키를 받기 전에 실행하지 않고 유추한 예상 출력입니다.
| 항목 | 내용 |
|---|---|
| 약관 | travel_terms.pdf(6장). 4쪽, 조항 16개 |
| FAQ | faq.csv(1장). 10행(열: 의도, 질문, 답) |
| 상품 | trip_products.csv(5장). 12행(열: 상품코드, 상품명, 지역, 박수, 가격, 남은자리, 출발일) |
| 도구 | search_terms(약관·FAQ 검색), find_products, calc_price(5장 실습 1) |
| 만들 파일 | out_ch08_gureum_bot.py(부품), out_ch08_gureum_app.py(화면), out_ch08_gureum_demo.csv(시연 점검표) |
요구사항은 다섯 가지입니다.
- 약관을 조항 단위로 나누고 FAQ 10개를 더해 한 색인에 넣고, 그 색인을 찾는
search_terms도구를 만듭니다. 찾은 글마다 "[약관 제5조(2쪽)]", "[FAQ 환불]" 꼴의 출처를 붙입니다. - 5장의
find_products,calc_price와 함께 세 도구를create_agent에 넘깁니다. 시스템 프롬프트에 "규정은 search_terms 로 찾고 출처를 댄다", "요금은 calc_price 로 계산한다", "자료에 없으면 상담 시간을 안내한다"를 넣습니다. - 대화 기록을 함께 넘겨 이어지는 질문 셋을 묻고, 차례마다 어느 도구를 불렀는지 확인합니다.
- 부품 파일과 화면 파일로 나누고, 답 아래에 부른 도구를 작게 보입니다.
AppTest로 시연 질문 넷(상품, 요금, 약관, 자료 밖)과 키 노출을 점검해 저장합니다.
단계별 힌트입니다.
- FAQ 한 줄은
Document(page_content=f"{질문} {답}", metadata={"source": f"FAQ {의도}"})로 만들면 약관 조각과 같은 꼴이 됩니다. - 도구는 색인
db를 써야 하므로build_agent함수 안에서@tool로 만들면 바깥 변수 없이db를 씁니다. - 에이전트의 결과
out["messages"]에서m.type == "tool"인 메시지의m.name이 부른 도구의 이름입니다. - 화면에 그릴 기록(
{"role", "content"}사전)을 그대로 에이전트에 넘기면 7장의 채팅 화면과 같은 꼴로 기억합니다.
확인할 것: 다 풀었으면 항목마다 스스로 확인해 체크해 보세요.
코드의 흐름
예시 풀이의 코드가 어떤 차례로 흘러가는지 한 장으로 그렸습니다. 풀기 전에 큰 줄기를 먼저 보고, 풀다가 막히면 지금 어느 상자에 있는지 확인해 보세요.
상자 하나가 예시 풀이의 단계 하나이고, 회청 화살표를 따라가면 실행 차례입니다. 1단계의 부품 파일 하나에 6장의 검색, 5장의 도구, 에이전트가 모두 들어가고, 2·3단계는 그 파일을 불러 보통 파이썬으로 시험하며, 4단계는 같은 파일을 화면 파일이 불러 씁니다.
실제 데이터로 해 보려면
실제 여행사나 가게의 상담 챗봇을 만들려면 약관, FAQ, 상품 표를 그 회사에서 받아야 합니다. 상품 표는 날마다 바뀌므로 실제 서비스에서는 파일 대신 회사의 예약 시스템을 부르는 도구로 바꿉니다(5장의 MCP 서버처럼).
| 자료 | 구하는 곳 | 쓰는 법 |
|---|---|---|
| 이용 약관 | 회사 누리집의 약관 쪽에서 내려받기 | 이용 조건을 확인하고 조항 단위로 나눈다 |
| 자주 묻는 질문 | 회사 누리집의 고객센터 | 질문과 답을 한 줄씩 csv 로 옮긴다 |
| 상품과 남은 자리 | 회사의 예약 시스템 | 공식 API 가 있으면 도구 함수에서 부른다. 개인정보(손님 이름, 연락처)는 도구가 돌려주지 않게 한다 |
결과 보기 (예시 풀이)
1단계. 부품 파일 쓰기
from pathlib import Path
BOT_CODE = r'''
"""구름다리여행 종합 상담 챗봇의 부품: 약관·FAQ 검색 도구, 상품 도구 둘, 에이전트"""
import os
import re
from pathlib import Path
import pandas as pd
from langchain.agents import create_agent
from langchain_community.document_loaders import PyPDFLoader
from langchain_community.vectorstores import FAISS
from langchain_core.documents import Document
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
HERE = Path(__file__).parent
HEADER = "구름다리여행 여행 약관 (연습용 가상 문서)"
SYSTEM = (
"너는 구름다리여행의 상담 챗봇이다. 취소, 환불, 짐, 아동 요금, 반려동물, 보험, 여권 같은 규정은 "
"search_terms 로 찾아 답하고 끝에 출처를 괄호로 적는다. 상품은 find_products 로 찾고, "
"요금은 calc_price 로 계산한다. 숫자를 직접 셈하지 않는다. "
"도구로 찾은 자료에 없으면 '그 내용은 제가 가진 자료에 없습니다. 상담 시간(평일 오전 9시~오후 6시)에 "
"상담원에게 물어 주세요.'라고 답한다. 답은 두세 문장으로 짧게 쓴다."
)
def load_docs():
"""약관을 조항 단위로 나누고 FAQ 를 더해 Document 목록을 만든다"""
docs = []
for p in PyPDFLoader(str(HERE / "travel_terms.pdf")).load():
text = re.sub(r"\s+", " ", p.page_content.replace(HEADER, "")).strip()
for part in re.split(r"(?=제\d+조 )", text)[1:]:
number = re.match(r"제\d+조", part).group()
docs.append(Document(page_content=part.strip(), metadata={"source": f"약관 {number}({p.metadata['page'] + 1}쪽)"}))
for r in pd.read_csv(HERE / "faq.csv").itertuples():
docs.append(Document(page_content=f"{r.질문} {r.답}", metadata={"source": f"FAQ {r.의도}"}))
return docs
def build_agent(api_key):
"""키를 받아 색인과 도구 셋, 에이전트를 만든다. (에이전트, 색인 조각 수)를 돌려준다"""
emb = OpenAIEmbeddings(model=os.getenv("OPENAI_EMBED_MODEL", "text-embedding-3-small"), api_key=api_key)
db = FAISS.from_documents(load_docs(), emb)
products = pd.read_csv(HERE / "trip_products.csv")
@tool
def search_terms(query: str) -> str:
"""구름다리여행의 약관과 자주 묻는 질문(FAQ)에서 query 와 가까운 글 3개를 출처와 함께 찾는다."""
return "\n".join(f"[{d.metadata['source']}] {d.page_content}" for d in db.similarity_search(query, k=3))
@tool(parse_docstring=True)
def find_products(region: str = "", max_price: int = 0) -> str:
"""구름다리여행 상품을 지역과 가격으로 찾는다.
Args:
region: 지역 이름. 예: 제주, 부산. 비우면 모든 지역
max_price: 어른 1명 가격의 상한(만 원). 0 이면 가격을 따지지 않는다
"""
df = products
if region:
df = df[df["지역"] == region]
if max_price > 0:
df = df[df["가격"] <= max_price]
if df.empty:
return "조건에 맞는 상품이 없습니다."
return "\n".join(
f"{r.상품코드} {r.상품명} | {r.가격}만 원 | 남은 자리 {r.남은자리} | 출발 {r.출발일}" for r in df.itertuples()
)
@tool(parse_docstring=True)
def calc_price(code: str, adults: int, children: int = 0) -> str:
"""상품코드와 인원으로 총 여행 요금(만 원)을 계산한다. 아동 요금은 어른의 80% 이다.
Args:
code: 상품코드. 예: P01
adults: 어른 수
children: 아동(만 2~12세 미만) 수
"""
rows = products[products["상품코드"] == code]
if rows.empty:
return f"없는 상품코드입니다: {code}"
row = rows.iloc[0]
total = round(row["가격"] * adults + row["가격"] * 0.8 * children, 1)
return f"{code} {row['상품명']} · 어른 {adults}명, 아동 {children}명 · {float(total):g}만 원"
model = ChatOpenAI(model=os.getenv("OPENAI_MODEL", "gpt-4.1-mini"), api_key=api_key, temperature=0)
agent = create_agent(model, tools=[search_terms, find_products, calc_price], system_prompt=SYSTEM)
return agent, db.index.ntotal
def reply(agent, history, question):
"""화면의 기록(history)에 새 질문을 더해 에이전트를 부르고, 답과 이번에 부른 도구 이름을 돌려준다"""
out = agent.invoke({"messages": history + [{"role": "user", "content": question}]})
new = out["messages"][len(history) + 1:] # 이번 질문 뒤에 생긴 메시지
return out["messages"][-1].content, [m.name for m in new if m.type == "tool"]
'''
Path("out_ch08_gureum_bot.py").write_text(BOT_CODE.lstrip(), encoding="utf-8")
print("저장: out_ch08_gureum_bot.py,", len(BOT_CODE.strip().splitlines()), "줄")저장: out_ch08_gureum_bot.py, 93 줄코드가 의미하는 것 부품 파일 하나에 6장의 문서 검색, 5장의 도구, 에이전트가 함께 들어갑니다. load_docs 는 약관 조항과 FAQ 줄을 같은 Document 꼴로 만들어 한 색인에 넣고, 출처를 metadata["source"] 에 "약관 제5조(2쪽)", "FAQ 환불" 꼴로 적어 search_terms 가 돌려주는 글 앞에 붙게 합니다. 세 도구를 build_agent 안에서 @tool 로 만든 것은 그 안에서 만든 db 와 products 를 도구가 바로 쓰게 하려는 것이고, create_agent(model, tools=[…], system_prompt=SYSTEM) 는 어느 도구를 언제 부를지를 모델에게 맡깁니다.
2단계. 부품을 불러 한 번 묻기
import os
import sys
from dotenv import find_dotenv, load_dotenv
# 작업 폴더(data)의 부품 파일을 불러온다
sys.path.insert(0, ".")
import out_ch08_gureum_bot as bot
load_dotenv(find_dotenv(usecwd=True))
agent, n_docs = bot.build_agent(os.getenv("OPENAI_API_KEY"))
print("검색 조각:", n_docs, "개(약관 16 + FAQ 10)")
answer, used = bot.reply(agent, [], "강아지랑 같이 갈 수 있어요?")
print("부른 도구:", used)
print("답:", answer)검색 조각: 26 개(약관 16 + FAQ 10)
부른 도구: ['search_terms']
답: 네, 반려동물은 '함께 여행' 메뉴의 동반 상품에서만 데려가실 수 있습니다. 몸무게 7kg 이하의 개와 고양이만 되고, 이동장에 넣어야 합니다. (출처: 약관 제12조(3쪽), FAQ 반려동물)코드가 의미하는 것 bot.build_agent(os.getenv("OPENAI_API_KEY")) 는 키를 받아 색인과 에이전트를 한 번 만들고, bot.reply(agent, [], …) 는 빈 기록으로 첫 질문을 보냅니다. reply 가 돌려주는 도구 이름 목록은 에이전트의 메시지 가운데 type == "tool" 인 것의 name 을 모은 것으로, 모델이 도구를 골랐는지 혼자 답했는지를 확인하는 창입니다. 예상 출력에서는 반려동물 질문에 search_terms 를 불렀다고 보았습니다.
3단계. 대화 기록으로 이어 묻기
history = []
for q in ["12월에 제주 가는 40만 원 이하 상품 있어요?", "그거 어른 둘에 아이 하나면 얼마예요?",
"출발 5일 전에 취소하면 수수료는요?"]:
answer, used = bot.reply(agent, history, q)
# 화면에 그릴 기록과 같은 꼴로 쌓는다. 다음 차례에 함께 넘어가 "그거"를 알아듣게 한다
history += [{"role": "user", "content": q}, {"role": "assistant", "content": answer}]
print(f"Q. {q}\n 부른 도구: {used}\nA. {answer}")
print("기록:", len(history), "개")Q. 12월에 제주 가는 40만 원 이하 상품 있어요?
부른 도구: ['find_products']
A. 12월에 출발하는 제주 상품으로는 P01 제주 동쪽 바다 2박 3일(39만 원, 12월 5일 출발, 남은 자리 6)이 있습니다. 40만 원 이하인 P12 제주 감귤 체험 1박 2일은 1월 23일 출발입니다.
Q. 그거 어른 둘에 아이 하나면 얼마예요?
부른 도구: ['calc_price']
A. P01 제주 동쪽 바다 2박 3일을 어른 2명, 아동 1명으로 예약하면 모두 109.2만 원입니다. 아동은 어른 요금의 80%로 계산했습니다.
Q. 출발 5일 전에 취소하면 수수료는요?
부른 도구: ['search_terms']
A. 출발 3일 전부터 6일 전까지 취소하면 상품 금액의 30%를 취소 수수료로 뗍니다. 5일 전이면 30%입니다. (출처: 약관 제5조(2쪽))
기록: 6 개코드가 의미하는 것 history 에 {"role", "content"} 사전을 차례마다 두 개씩 쌓아 다음 reply 에 함께 넘기는 것이 4장의 대화 기억입니다. reply 는 넘겨받은 기록만 보고 답하므로, "그거"가 어느 상품인지는 이 기록에 적힌 앞 차례의 답을 읽어야 알 수 있습니다. 차례마다 used 를 찍어 질문이 상품·요금·약관에 맞는 도구로 갔는지 확인하는 것이 이 단계에서 볼 곳입니다.
4단계. 화면 파일 쓰고 시연 점검표 만들기
import re
import pandas as pd
import streamlit as st
from streamlit.testing.v1 import AppTest
APP_CODE = r'''
import os
import sys
from pathlib import Path
import streamlit as st
from dotenv import find_dotenv, load_dotenv
HERE = Path(__file__).parent
sys.path.insert(0, str(HERE))
import out_ch08_gureum_bot as bot # noqa: E402
PLACEHOLDER = "발급받은_인증키"
def get_api_key():
"""클라우드에서는 Secrets 칸, 내 PC 에서는 .env 에서 키를 읽는다(7차시 실습 3)"""
try:
return st.secrets["OPENAI_API_KEY"]
except (FileNotFoundError, KeyError):
load_dotenv(find_dotenv(usecwd=True))
return os.getenv("OPENAI_API_KEY")
@st.cache_resource(show_spinner="약관과 FAQ 로 색인을 만드는 중입니다...")
def load_agent(api_key):
return bot.build_agent(api_key)
st.set_page_config(page_title="구름다리여행 상담 챗봇")
st.title("구름다리여행 상담 챗봇")
api_key = get_api_key()
if not api_key or api_key == PLACEHOLDER:
# 이 앱은 진짜 모델이 도구를 고르므로 키가 없으면 멈춘다
st.warning("API 키가 없어 상담 챗봇을 열 수 없습니다. .env 나 Secrets 에 키를 넣어 주세요.")
st.stop()
agent, n_docs = load_agent(api_key)
with st.sidebar:
st.header("자료")
st.caption(f"약관·FAQ 조각: {n_docs}개")
st.caption("도구: search_terms, find_products, calc_price")
st.caption("연습용으로 지어낸 가상 여행사의 자료입니다.")
if "gureum_messages" not in st.session_state:
st.session_state.gureum_messages = []
for m in st.session_state.gureum_messages:
with st.chat_message(m["role"]):
st.markdown(m["content"])
if question := st.chat_input("상품, 요금, 약관을 물어보세요"):
with st.chat_message("user"):
st.markdown(question)
with st.spinner("자료를 찾는 중입니다..."):
# 그리기 전의 기록을 넘긴다(7차시: 질문은 부른 뒤에 기록에 넣는다)
answer, used = bot.reply(agent, st.session_state.gureum_messages, question)
with st.chat_message("assistant"):
st.markdown(answer)
st.caption("부른 도구: " + (", ".join(used) if used else "없음"))
st.session_state.gureum_messages += [{"role": "user", "content": question},
{"role": "assistant", "content": answer}]
'''
Path("out_ch08_gureum_app.py").write_text(APP_CODE.lstrip(), encoding="utf-8")
print("저장: out_ch08_gureum_app.py,", len(APP_CODE.strip().splitlines()), "줄")
checks = []
st.cache_resource.clear()
# 진짜 모델이 도구를 여러 번 부르므로 기다리는 시간을 넉넉히 준다
at = AppTest.from_file("out_ch08_gureum_app.py", default_timeout=90).run()
checks.append({"점검": "첫 화면", "결과": "통과" if not at.exception and not at.warning else "고칠 것",
"까닭": " / ".join(c.value for c in at.sidebar.caption[:2])})
# (질문, 불러야 할 도구, 답에 있어야 할 글)
DEMO = [("12월에 제주 가는 40만 원 이하 상품 있어요?", "find_products", "P01"),
("그거 어른 둘에 아이 하나면 얼마예요?", "calc_price", "109.2"),
("출발 5일 전에 취소하면 수수료는요?", "search_terms", "30%"),
("엔화 환율이 얼마예요?", None, "자료에 없습니다")]
for q, tool_name, must in DEMO:
at.chat_input[0].set_value(q).run()
reply = at.chat_message[-1]
said, used = reply.markdown[0].value, reply.caption[0].value
ok = must in said and (tool_name is None or tool_name in used)
checks.append({"점검": f"질문: {q}", "결과": "통과" if ok else "고칠 것", "까닭": used})
checks.append({"점검": "기록", "결과": "통과" if len(at.session_state["gureum_messages"]) == 8 else "고칠 것",
"까닭": f"말풍선 {len(at.chat_message)}개"})
leaks = [f for f in ["out_ch08_gureum_bot.py", "out_ch08_gureum_app.py"]
if re.search(r"sk-[A-Za-z0-9_-]{20,}", Path(f).read_text(encoding="utf-8"))]
checks.append({"점검": "코드에 적힌 키", "결과": "고칠 것" if leaks else "통과",
"까닭": ", ".join(leaks) if leaks else "키 꼴의 글 없음"})
demo = pd.DataFrame(checks)
print(demo.to_string(index=False))
demo.to_csv("out_ch08_gureum_demo.csv", index=False, encoding="utf-8-sig")
print("저장: out_ch08_gureum_demo.csv,", len(demo), "행")저장: out_ch08_gureum_app.py, 59 줄
점검 결과 까닭
첫 화면 통과 약관·FAQ 조각: 26개 / 도구: search_terms, find_products, calc_price
질문: 12월에 제주 가는 40만 원 이하 상품 있어요? 통과 부른 도구: find_products
질문: 그거 어른 둘에 아이 하나면 얼마예요? 통과 부른 도구: calc_price
질문: 출발 5일 전에 취소하면 수수료는요? 통과 부른 도구: search_terms
질문: 엔화 환율이 얼마예요? 통과 부른 도구: search_terms
기록 통과 말풍선 8개
코드에 적힌 키 통과 키 꼴의 글 없음
저장: out_ch08_gureum_demo.csv, 7 행예상 출력에서 에이전트는 질문마다 맞는 도구 하나를 골랐다고 보았습니다. 상품 질문에는 find_products(지역 제주, 가격 상한 40)를, 요금 질문에는 calc_price(P01, 어른 2, 아동 1)를, 약관 질문에는 search_terms 를 불렀습니다. find_products 에는 날짜 칸이 없어서 "12월에"는 모델이 출발일을 읽고 골라냈습니다. 둘째 질문의 "그거"는 앞 차례의 답에 적힌 P01 을 대화 기록에서 읽어 알아들었고, 요금 109.2만 원은 39 × 2 + 39 × 0.8 = 109.2 로 도구가 계산한 값입니다. 환율 질문에는 search_terms 로 찾아본 뒤 자료에 없다고 답하고 상담 시간을 안내했다고 보았습니다. 모델이 쓰는 답의 글과 고르는 도구는 실행할 때마다, 모델마다 조금씩 달라지므로 여러분의 결과는 다를 수 있습니다. 특히 둘째 질문에서 모델이 find_products 를 다시 부르거나, 셋째 질문에서 앞의 109.2만 원에 30% 를 직접 곱해 답할 수도 있습니다. 시스템 프롬프트의 "숫자를 직접 셈하지 않는다"는 그런 일을 줄이려고 넣은 규칙입니다.
코드가 의미하는 것 화면 파일은 프로젝트 1과 짜임이 같지만 한 가지가 다릅니다. 이 앱은 진짜 모델이 도구를 골라야 돌아가므로, 키가 없거나 자리표시면 st.warning 을 띄우고 st.stop() 으로 멈춥니다(프로젝트 1은 가짜 모델로 연습 모드를 돌았습니다). 시연 점검은 DEMO 의 질문마다 "불러야 할 도구"와 "답에 있어야 할 글"을 함께 보고, 답 아래 st.caption 에 적은 "부른 도구"를 reply.caption[0].value 로 읽어 확인하며, default_timeout=90 은 도구를 여러 번 부르는 동안 시험이 끊기지 않게 넉넉히 둔 값입니다.
결론은, 1~7장의 부품(대화 기억, 도구, 문서 검색, 웹 화면)을 에이전트 하나에 묶으면 손님이 한 대화 안에서 상품, 요금, 약관을 오가며 물어도 답할 수 있다는 것입니다. 다만 도구를 고르는 일을 모델에게 맡겼으므로, 차례마다 "부른 도구"를 찍어 두고 시연 점검표로 확인해야 무엇이 잘못되었는지 찾을 수 있습니다.
| 받은 피드백(지어낸 것) | 고친 것 | 다시 잰 결과(예상) |
|---|---|---|
| 답만 보고는 약관을 본 것인지 지어낸 것인지 모르겠다 | 답 아래에 부른 도구를 보이고, 프롬프트에 "출처를 괄호로 적는다" | 약관 질문의 답 끝에 (출처: 약관 제5조(2쪽)) |
| 수수료를 금액으로도 알고 싶다 | 아직 고치지 않음(계획: 상품코드와 출발까지 남은 날로 수수료를 계산하는 calc_fee 도구) | 지금은 30% 비율만 답함 |
| 매진 상품(남은 자리 0)도 목록에 나온다 | 아직 고치지 않음(계획: find_products 가 매진을 빼거나 "매진"이라 적게) | 제주 상품에서는 매진인 P02 가 40만 원을 넘어 나오지 않음 |
이 프로젝트가 뜻하는 것
이 프로젝트는 여행사 창구에 노련한 상담원 한 명을 앉히고, 약관집, 상품 목록, 계산기를 책상에 놓아 주는 일과 같습니다. 책상을 차리고(1단계), 첫 손님의 질문 하나를 받아 보고(2단계), 한 손님이 이것저것 이어 묻는 상담을 하고(3단계), 창구를 열기 전에 점검표로 확인합니다(4단계). 여기서 약관집이 search_terms, 상품 목록이 find_products, 계산기가 calc_price, 상담원이 에이전트, 상담 메모가 대화 기록입니다. 비유와 다른 점은, 사람 상담원은 계산기를 두고도 암산하지 않을 줄 알지만 모델은 규칙으로 일러 주어도 가끔 직접 셈하므로 점검표로 확인해야 한다는 것입니다.
| 단계 | 출력(예상) | 뜻 |
|---|---|---|
| 1. 부품 파일 | 93줄 | 검색 도구, 상품 도구 둘, 에이전트가 한 파일에 들어갔습니다 |
| 2. 한 번 묻기 | 조각 26개, search_terms | 약관과 FAQ 가 한 색인에서 함께 찾아졌습니다 |
| 3. 이어 묻기 | find_products → calc_price → search_terms | 질문마다 다른 도구를 골랐고, "그거"를 기록으로 알아들었습니다 |
| 4. 시연 점검 | 일곱 가지 통과 | 첫 화면, 질문 넷, 기록, 키 노출을 같은 기준으로 확인했습니다 |
한 줄로 줄이면, 종합 상담 챗봇은 "도구마다 맡은 일 + 도구를 고르는 에이전트 + 함께 넘기는 대화 기록 + 무엇을 불렀는지 보이는 화면"으로 만든다는 것을 본 프로젝트입니다. 이 앱을 7장의 배포 폴더처럼 꾸리면 웹 주소로 시연할 수 있습니다.