4장. 대화형 챗봇과 메모리 전략
실습 1 — Pydantic 틀로 여행 조건 확인하기
구름다리여행 상담 창구에 들어온 여행 요청 글 10개(trip_requests.csv)가 있습니다. 표에는 손님이 쓴 요청 글과, 사람이 요청 글을 읽고 정리한 정답 조건(목적지, 박수, 어른, 아이, 예산, 관심사)이 함께 들어 있습니다. 요청과 조건은 모두 연습용으로 지어낸 가상 데이터입니다. 정답 조건을 Pydantic 틀로 검사하고, 일부러 틀린 값을 넣어 틀이 걸러 내는지 봅니다. 모델은 부르지 않으므로 API 키가 없어도 됩니다.
1단계. 여행 요청 표 읽기
import pandas as pd
# 요청 글 10개와 사람이 정리한 정답 조건을 읽는다
trips = pd.read_csv("trip_requests.csv")
print(trips.shape)
# 예산은 일행 전체의 예산(만 원), 관심사는 ; 로 이어 적었다
print(trips[["번호", "목적지", "박수", "어른", "아이", "예산", "관심사"]].to_string(index=False))(10, 8)
번호 목적지 박수 어른 아이 예산 관심사
1 제주도 2 2 2 150 바다;맛집
2 부산 1 1 0 30 카페
3 강릉 3 4 0 200 온천;산책
4 경주 2 2 1 80 역사
5 여수 1 2 0 50 야경
6 전주 1 3 0 60 맛집;한옥
7 속초 2 2 3 120 바다;시장
8 제주도 4 2 0 300 바다;드라이브;맛집
9 통영 6 1 0 100 섬
10 가평 1 2 2 50 캠핑;물놀이코드가 의미하는 것 pd.read_csv 로 읽은 표에는 손님이 쓴 요청 글과 사람이 정리한 정답 조건이 한 행에 나란히 있습니다. 정답이 있는 요청 묶음이어야 실습 2에서 모델이 뽑은 조건을 칸마다 채점할 수 있습니다. 예산은 일행 전체 기준, 관심사는 ; 로 잇는다는 이 표의 약속을 뒤의 틀과 채점 코드가 그대로 따릅니다.
2단계. 여행 조건의 틀 정하기
from pydantic import BaseModel, Field
class TripRequest(BaseModel):
"""손님의 여행 조건. 칸 이름은 영어로, 뜻은 description 에 한국어로 적는다"""
destination: str = Field(description="여행 목적지. 도시나 섬 이름")
nights: int = Field(description="몇 박인지. 2박 3일이면 2", ge=1, le=30)
adults: int = Field(description="어른 수", ge=1)
children: int = Field(default=0, description="아이 수. 없으면 0", ge=0)
budget: int = Field(description="일행 전체 예산(만 원). 150만 원이면 150", ge=0)
interests: list[str] = Field(description="관심사 낱말 목록. 예: 바다, 맛집")
def row_to_trip(row):
"""표의 한 행(한국어 열 이름)을 틀(영어 칸 이름)에 맞춰 넣는다"""
return TripRequest(
destination=row["목적지"],
nights=row["박수"],
adults=row["어른"],
children=row["아이"],
budget=row["예산"],
interests=row["관심사"].split(";"), # "바다;맛집" → ["바다", "맛집"]
)
first = row_to_trip(trips.iloc[0]) # iloc[0]: 첫째 행
print(repr(first))
# model_dump: 틀의 객체를 보통 사전으로 바꾼다. dump 는 "쏟아 내다"라는 뜻
print(first.model_dump())TripRequest(destination='제주도', nights=2, adults=2, children=2, budget=150, interests=['바다', '맛집'])
{'destination': '제주도', 'nights': 2, 'adults': 2, 'children': 2, 'budget': 150, 'interests': ['바다', '맛집']}코드가 의미하는 것 TripRequest 는 상담 카드 양식이고, Field 의 description 은 칸 옆에 적어 둔 작성 요령입니다. "일행 전체 예산(만 원)"처럼 헷갈리기 쉬운 단위를 적어 두는 것은 실습 2에서 모델이 이 글을 읽고 칸을 채우기 때문입니다. row_to_trip 은 사람이 정리한 표를 모델이 채울 카드와 같은 양식으로 옮기고, model_dump() 는 그 카드를 다시 사전으로 펼쳐 표나 파일에 넣기 좋게 합니다.
3단계. 맞는 값과 틀린 값 검사하기
from pydantic import ValidationError
# 정답 조건 10행이 모두 틀을 통과하는지 본다
passed = [row_to_trip(row) for _, row in trips.iterrows()]
print("통과한 행:", len(passed), "/", len(trips))
# 일부러 틀린 값을 넣은 네 가지 경우
good = first.model_dump()
bad_cases = {
"박수가 음수": {**good, "nights": -1},
"어른이 0명": {**good, "adults": 0},
"예산이 글자": {**good, "budget": "많이"},
"목적지 빠짐": {k: v for k, v in good.items() if k != "destination"},
}
for name, values in bad_cases.items():
try:
TripRequest(**values) # ** 는 사전을 이름 붙은 인자로 풀어 넘긴다
print(name, "→ 통과")
except ValidationError as e:
err = e.errors()[0]
# loc: 어느 칸, type: 어긴 조건의 이름
print(name, "→ 걸림:", err["loc"][0], err["type"])통과한 행: 10 / 10
박수가 음수 → 걸림: nights greater_than_equal
어른이 0명 → 걸림: adults greater_than_equal
예산이 글자 → 걸림: budget int_parsing
목적지 빠짐 → 걸림: destination missing코드가 의미하는 것 정답 10행을 모두 통과시키는 것은 틀이 너무 깐깐해서 맞는 값까지 막지 않는지 보는 일이고, 틀린 값 네 가지는 막아야 할 것을 막는지 보는 일입니다. {**good, "nights": -1} 처럼 한 칸만 바꿔 넣으면 어느 조건이 무엇을 잡는지 하나씩 가려 볼 수 있습니다. 챗봇에서는 모델이 채운 답이 이 검사를 거쳐야 예약 같은 다음 단계로 넘어갑니다.
4단계. 모델에게 갈 스키마 저장하기
import json
from pathlib import Path
# 틀을 JSON 스키마로 바꾼다. 실습 2에서 모델에게 건네지는 설계도가 이것이다
schema = TripRequest.model_json_schema()
for name, spec in schema["properties"].items():
print(f"{name:12} {spec['type']:8} {spec['description']}")
print("꼭 채울 칸:", schema["required"])
Path("out_ch04_trip_schema.json").write_text(json.dumps(schema, ensure_ascii=False, indent=2), encoding="utf-8")
print("저장: out_ch04_trip_schema.json")destination string 여행 목적지. 도시나 섬 이름
nights integer 몇 박인지. 2박 3일이면 2
adults integer 어른 수
children integer 아이 수. 없으면 0
budget integer 일행 전체 예산(만 원). 150만 원이면 150
interests array 관심사 낱말 목록. 예: 바다, 맛집
꼭 채울 칸: ['destination', 'nights', 'adults', 'budget', 'interests']
저장: out_ch04_trip_schema.json코드가 의미하는 것 model_json_schema() 가 만든 사전은 모델이 실제로 받아 보는 빈 카드입니다. 모델은 파이썬 클래스를 읽지 못하므로 칸 이름, 자료형, 설명, 꼭 채울 칸이 이 스키마로 옮겨져야 틀대로 답할 수 있습니다. 파일로 남겨 두면 모델에게 건네지는 설계도를 나중에 열어 확인할 수 있습니다.
이 실습이 뜻하는 것
이 실습은 은행 창구 직원이 새 서식을 만들고, 이미 받아 둔 서식 열 장과 일부러 잘못 쓴 서식 네 장으로 시험해 보는 일과 같습니다. 받아 둔 서식을 펼치고(1단계), 칸과 조건을 정하고(2단계), 맞는 서식과 틀린 서식을 넣어 보고(3단계), 서식의 설계도를 남깁니다(4단계). 여기서 서식이 TripRequest, 칸마다의 조건이 Field(...), 돌려보내는 일이 ValidationError 입니다. 비유와 다른 점은, 이 직원은 "칸에 맞는 값인가"만 보고 "손님의 말과 같은 값인가"는 보지 않는다는 것입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 표 읽기 | (10, 8) | 요청 10개, 열 여덟(번호, 요청, 정답 조건 여섯)짜리 표입니다 |
| 2. 틀 정하기 | TripRequest(destination='제주도', …) | 첫 행이 틀을 통과해 객체가 됐고, "바다;맛집" 은 목록 ['바다', '맛집'] 이 됐습니다 |
| 3. 맞는 값 | 통과한 행 10 / 10 | 사람이 정리한 정답 조건은 모두 틀에 맞습니다 |
| 3. 틀린 값 | 네 가지 모두 걸림 | 음수 박수와 0명 어른은 greater_than_equal, 글자 예산은 int_parsing, 빠진 목적지는 missing 으로 걸렸습니다 |
| 4. 스키마 | 칸 여섯, 꼭 채울 칸 다섯 | 기본값을 준 children 만 꼭 채울 칸에서 빠졌습니다 |
한 줄로 줄이면, Pydantic 틀이 "칸에 맞지 않는 값"을 정확히 걸러 낸다는 것을 확인한 실습입니다. 틀린 값 네 가지가 서로 다른 이유(greater_than_equal 은 "이상이어야 함", int_parsing 은 "정수로 읽을 수 없음", missing 은 "칸이 빠짐")로 걸렸고, 오류에 어느 칸인지도 함께 나왔습니다. 다만 "제주"와 "제주도"처럼 틀에 맞는 값끼리의 차이는 틀이 가려 주지 않습니다. 실습 2에서는 이 틀을 모델에게 건네 요청 글에서 조건을 뽑게 합니다.
코드 설명
class TripRequest(BaseModel)는 칸 여섯 개의 틀입니다. 칸마다 자료형(int,str,list[str])과Field(description=…, ge=…)로 뜻과 조건을 적었습니다.row_to_trip은 표의 한국어 열 이름을 틀의 영어 칸 이름에 하나씩 짝지어 넣습니다."바다;맛집".split(";")은 글을;에서 잘라 목록으로 만듭니다.TripRequest(**values)는 사전을 풀어 이름 붙은 인자로 넘깁니다.{**good, "nights": -1}은 좋은 값을 그대로 복사하되nights칸만 바꾼 새 사전입니다.e.errors()는 틀린 칸마다 위치(loc), 조건의 이름(type), 설명(msg)을 담은 사전의 목록입니다. 어긴 칸이 여럿이면 목록도 길어집니다.model_json_schema()는 틀을 JSON 스키마로 바꿉니다.out_ch04_trip_schema.json을 열어 보면description과minimum같은 조건이 모두 들어 있습니다.