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

8장. 최종 프로젝트

프로젝트 3 — 구름다리여행 종합 상담 챗봇

1장부터 7장까지 만든 구름다리여행의 부품을 모두 묶어 종합 상담 챗봇을 만듭니다. 손님은 한 대화 안에서 상품을 찾고("12월에 제주 가는 40만 원 이하 상품 있어요?"), 앞에서 고른 상품의 요금을 묻고("그거 어른 둘에 아이 하나면 얼마예요?"), 약관을 묻습니다("출발 5일 전에 취소하면 수수료는요?"). 약관·FAQ 검색 도구, 상품 찾기 도구, 요금 계산 도구를 에이전트에게 쥐여 주고, 대화 기억과 웹 화면을 붙인 뒤 시연 점검표로 확인해 보세요. 구름다리여행과 약관, 상품, FAQ 는 모두 연습용으로 지어낸 것입니다. 에이전트가 진짜 모델로 도구를 고르므로 과목 폴더의 .env 에 API 키가 있어야 돌아갑니다. 이 절의 출력은 API 키를 받기 전에 실행하지 않고 유추한 예상 출력입니다.

항목내용
약관travel_terms.pdf(6장). 4쪽, 조항 16개
FAQfaq.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(시연 점검표)

요구사항은 다섯 가지입니다.

  1. 약관을 조항 단위로 나누고 FAQ 10개를 더해 한 색인에 넣고, 그 색인을 찾는 search_terms 도구를 만듭니다. 찾은 글마다 "[약관 제5조(2쪽)]", "[FAQ 환불]" 꼴의 출처를 붙입니다.
  2. 5장의 find_products, calc_price 와 함께 세 도구를 create_agent 에 넘깁니다. 시스템 프롬프트에 "규정은 search_terms 로 찾고 출처를 댄다", "요금은 calc_price 로 계산한다", "자료에 없으면 상담 시간을 안내한다"를 넣습니다.
  3. 대화 기록을 함께 넘겨 이어지는 질문 셋을 묻고, 차례마다 어느 도구를 불렀는지 확인합니다.
  4. 부품 파일과 화면 파일로 나누고, 답 아래에 부른 도구를 작게 보입니다.
  5. AppTest 로 시연 질문 넷(상품, 요금, 약관, 자료 밖)과 키 노출을 점검해 저장합니다.

단계별 힌트입니다.

  1. FAQ 한 줄은 Document(page_content=f"{질문} {답}", metadata={"source": f"FAQ {의도}"}) 로 만들면 약관 조각과 같은 꼴이 됩니다.
  2. 도구는 색인 db 를 써야 하므로 build_agent 함수 안에서 @tool 로 만들면 바깥 변수 없이 db 를 씁니다.
  3. 에이전트의 결과 out["messages"] 에서 m.type == "tool" 인 메시지의 m.name 이 부른 도구의 이름입니다.
  4. 화면에 그릴 기록({"role", "content"} 사전)을 그대로 에이전트에 넘기면 7장의 채팅 화면과 같은 꼴로 기억합니다.

확인할 것: 다 풀었으면 항목마다 스스로 확인해 체크해 보세요.

코드의 흐름

예시 풀이의 코드가 어떤 차례로 흘러가는지 한 장으로 그렸습니다. 풀기 전에 큰 줄기를 먼저 보고, 풀다가 막히면 지금 어느 상자에 있는지 확인해 보세요.

부품 파일, 한 번 묻기, 이어 묻기, 화면과 시연의 네 단계를 이은 코드 흐름도

상자 하나가 예시 풀이의 단계 하나이고, 회청 화살표를 따라가면 실행 차례입니다. 1단계의 부품 파일 하나에 6장의 검색, 5장의 도구, 에이전트가 모두 들어가고, 2·3단계는 그 파일을 불러 보통 파이썬으로 시험하며, 4단계는 같은 파일을 화면 파일이 불러 씁니다.

실제 데이터로 해 보려면

실제 여행사나 가게의 상담 챗봇을 만들려면 약관, FAQ, 상품 표를 그 회사에서 받아야 합니다. 상품 표는 날마다 바뀌므로 실제 서비스에서는 파일 대신 회사의 예약 시스템을 부르는 도구로 바꿉니다(5장의 MCP 서버처럼).

자료구하는 곳쓰는 법
이용 약관회사 누리집의 약관 쪽에서 내려받기이용 조건을 확인하고 조항 단위로 나눈다
자주 묻는 질문회사 누리집의 고객센터질문과 답을 한 줄씩 csv 로 옮긴다
상품과 남은 자리회사의 예약 시스템공식 API 가 있으면 도구 함수에서 부른다. 개인정보(손님 이름, 연락처)는 도구가 돌려주지 않게 한다
결과 보기 (예시 풀이)

1단계. 부품 파일 쓰기

Python
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단계. 부품을 불러 한 번 묻기

Python
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단계. 대화 기록으로 이어 묻기

Python
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단계. 화면 파일 쓰고 시연 점검표 만들기

Python
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장의 배포 폴더처럼 꾸리면 웹 주소로 시연할 수 있습니다.

← 이전 절8장 · 프로젝트 2 — 전공 지식 챗봇: 정보통신 학습 도우미다음 절 →8장 · 정리