4장. 대화형 챗봇과 메모리 전략
with_structured_output 으로 틀째 받기
상담원에게 손님의 말을 줄글로 받아 적게 한 뒤 내가 카드에 옮겨 적을 수도 있고, 처음부터 빈 상담 카드를 건네며 "이 칸을 채워 주세요"라고 할 수도 있습니다. 뒤의 방법이 덜 틀립니다. 칸의 이름과 뜻이 상담원 눈앞에 있기 때문입니다. with_structured_output 은 모델에게 빈 카드(Pydantic 틀)를 건네는 방법입니다.
첫 상자에서 model.with_structured_output(TripRequest) 로 틀을 붙인 모델을 만듭니다. 이 모델을 부르면 LangChain 이 틀을 JSON 스키마(칸의 이름, 자료형, 설명, 조건을 적은 설계도)로 바꿔 함께 보내고, 모델은 그 칸에 맞춰 답을 쓰고, 돌아온 답은 검사를 거쳐 TripRequest 객체가 됩니다. 여기서 빈 상담 카드가 Pydantic 틀, 카드를 채워 돌려주는 상담원이 틀을 붙인 모델입니다. 비유와 다른 점은, 모델이 칸은 꼬박꼬박 채워도 그 안의 값(예: 1인당 예산과 전체 예산)을 잘못 읽을 수는 있다는 것입니다.
구조화된 출력을 받는 세 방법
| 방법 | 쓰는 모양 | 돌려받는 것 | 값 검사 | 어울리는 때 |
|---|---|---|---|---|
JsonOutputParser | prompt | model | JsonOutputParser() | 사전 | 하지 않는다 | 칸이 자주 바뀌는 가벼운 일 |
PydanticOutputParser | prompt | model | PydanticOutputParser(pydantic_object=틀) | 틀의 객체 | 한다 | 구조화된 출력을 지원하지 않는 모델 |
with_structured_output | prompt | model.with_structured_output(틀) | 틀의 객체 | 한다 | OpenAI 처럼 지원하는 모델(이 장의 기본) |
with_structured_output 의 method 인자는 기본값이 "json_schema"(스키마를 모델에 직접 건네는 방식)입니다. 이 교안의 langchain-openai 1.7 판에서 확인한 값이며, 따로 바꾸지 않습니다.
짧은 예시
모델을 부르지 않고, 모델에게 건네지는 스키마를 들여다봅니다. 그리고 틀을 붙인 모델이 돌려줄 결과를 PydanticOutputParser 로 흉내 냅니다.
import json
from langchain_core.messages import AIMessage
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
class TripRequest(BaseModel):
"""손님의 여행 조건"""
destination: str = Field(description="여행 목적지")
nights: int = Field(description="몇 박인지. 2박 3일이면 2", ge=1)
# model_json_schema: 틀을 JSON 스키마(사전)로 바꾼다. 모델에게는 이 설계도가 간다
schema = TripRequest.model_json_schema()
print(json.dumps(schema["properties"], ensure_ascii=False))
print("꼭 채울 칸:", schema["required"])
# 모델이 스키마대로 JSON 을 썼다고 보고, 틀의 객체로 바꾼다
parser = PydanticOutputParser(pydantic_object=TripRequest)
trip = parser.invoke(AIMessage('{"destination": "부산", "nights": 1}'))
print(repr(trip))
print("점(.)으로 꺼내기:", trip.destination, trip.nights){"destination": {"description": "여행 목적지", "title": "Destination", "type": "string"}, "nights": {"description": "몇 박인지. 2박 3일이면 2", "minimum": 1, "title": "Nights", "type": "integer"}}
꼭 채울 칸: ['destination', 'nights']
TripRequest(destination='부산', nights=1)
점(.)으로 꺼내기: 부산 1스키마에는 칸마다 자료형(type), 설명(description), 조건(minimum)이 들어 있습니다. ge=1 이 JSON 스키마에서는 minimum: 1 로 적힙니다. with_structured_output 으로 부른 모델도 마지막 줄처럼 TripRequest 객체를 돌려주므로, 사전의 ["destination"] 대신 trip.destination 처럼 점으로 꺼냅니다.
코드가 의미하는 것
이 코드는 상담원에게 건넬 빈 카드가 어떻게 생겼는지 펼쳐 보고, 상담원이 채워 돌려준 카드를 받는 일을 흉내 냅니다. 진짜 모델 대신 PydanticOutputParser 가 돌려받는 쪽을 맡습니다.
| 코드 | 하는 일 | 챗봇에서 뜻하는 것 |
|---|---|---|
TripRequest.model_json_schema() | 틀을 JSON 스키마 사전으로 바꾼다 | with_structured_output 이 모델에게 함께 보내는 설계도가 이것입니다. 모델은 파이썬 클래스가 아니라 이 설계도를 읽습니다 |
schema["properties"], schema["required"] | 칸마다의 설명과 꼭 채울 칸을 꺼낸다 | 모델이 받는 것이 칸 이름, 자료형, 설명, 조건뿐이라는 것을 눈으로 확인합니다 |
PydanticOutputParser(pydantic_object=TripRequest) | JSON 글을 틀의 객체로 바꾸는 파서를 만든다 | 틀을 붙인 모델이 마지막에 하는 일을 대신합니다. 구조화된 출력을 지원하지 않는 모델에서는 이 파서를 직접 씁니다 |
trip.destination | 객체에서 점으로 값을 꺼낸다 | 돌려받은 것이 사전이 아니라 틀의 객체라는 뜻입니다 |
결과에서 읽을 것은 두 가지입니다.
- `ge=1` 이 `minimum: 1` 로 바뀌었다. 파이썬에서 적은 조건이 모델이 읽는 스키마의 말로 옮겨졌습니다.
description도 한국어 그대로 들어 있습니다. - 꼭 채울 칸은 둘. 기본값이 없는
destination과nights가required에 들었습니다. 모델은 이 두 칸을 반드시 채워야 합니다.
주의할 점
with_structured_output은 모델마다 지원 여부가 다릅니다. 1장의 가짜 모델(FakeListChatModel)에서 부르면 "지원하지 않는다"는 오류(NotImplementedError)가 납니다. 그래서 이 절의 예시는 파서로 흉내 냈고, 진짜 모델로는 실습 2에서 부릅니다.- 틀을 붙인 모델은 줄글 답을 쓰지 않습니다. 손님에게 보여 줄 말과 조건을 함께 받고 싶으면, 틀 안에
reply: str같은 칸을 하나 더 둡니다.