4장. 대화형 챗봇과 메모리 전략
JSON 과 JsonOutputParser
택배를 보낼 때 송장에는 받는 사람, 주소, 전화번호를 적는 칸이 정해져 있습니다. 어느 택배 회사든 이 칸을 읽을 줄 알아서 물건이 제대로 갑니다. 프로그램끼리 데이터를 주고받을 때 쓰는 송장 양식이 JSON 입니다. 모델에게 "JSON 으로 답하라"고 하면 모델은 JSON 꼴의 글을 쓰고, 파서가 그 글을 파이썬 사전으로 바꿉니다.
세 상자는 왼쪽에서 오른쪽으로 일어나는 차례입니다. 프롬프트에 "JSON 으로 답하라"는 형식 지시를 붙이고, 모델이 JSON 꼴의 글을 쓰면, JsonOutputParser 가 그 글을 사전으로 바꿉니다. 여기서 송장 양식이 JSON, 송장을 읽어 물건을 분류하는 직원이 파서입니다. 비유와 다른 점은, 모델이 쓴 것은 여전히 "글"이라서 가끔 양식을 어기고 설명을 덧붙이거나 괄호를 빠뜨린다는 것입니다. 그러면 파서가 오류를 냅니다.
JSON 과 파이썬 견주기
| JSON | 파이썬 | 예 |
|---|---|---|
객체 { } | 사전 dict | {"목적지": "제주도"} |
배열 [ ] | 목록 list | ["바다", "맛집"] |
"글자" | str | 글자는 반드시 큰따옴표 |
2, 150.5 | int, float | 숫자는 따옴표 없이 |
true, false | True, False | 소문자로 쓴다 |
null | None | 값이 없음 |
짧은 예시
모델 대신 AIMessage 에 JSON 글을 직접 넣어 파서를 시험합니다. 3장의 StrOutputParser 자리에 JsonOutputParser 를 끼운 것과 같습니다.
from langchain_core.messages import AIMessage
from langchain_core.output_parsers import JsonOutputParser
parser = JsonOutputParser()
# get_format_instructions: 프롬프트에 붙일 "형식 지시" 글을 돌려준다
print("형식 지시:", parser.get_format_instructions())
# 모델이 ```json ... ``` 으로 감싸서 답해도 파서가 안쪽만 꺼낸다
reply = AIMessage('```json\n{"목적지": "제주도", "박수": 2, "관심사": ["바다", "맛집"]}\n```')
data = parser.invoke(reply)
print(type(data).__name__, data)
print("관심사 첫째:", data["관심사"][0])형식 지시: Return a JSON object.
dict {'목적지': '제주도', '박수': 2, '관심사': ['바다', '맛집']}
관심사 첫째: 바다모델이 JSON 을 코드 블록 표시( ```json )로 감싸서 답하는 일이 흔한데, 파서가 그 표시를 떼고 안쪽만 읽었습니다. 결과는 사전이라 data["관심사"][0] 처럼 바로 꺼내 씁니다.
코드가 의미하는 것
이 코드는 송장을 읽는 직원(파서)에게 모델의 답 대신 손으로 쓴 송장을 건네 보는 일입니다. 파서를 만들고, 형식 지시를 보고, JSON 글을 사전으로 바꿉니다.
| 코드 | 하는 일 | 챗봇에서 뜻하는 것 |
|---|---|---|
JsonOutputParser() | JSON 글을 사전으로 바꾸는 파서를 만든다 | 체인 끝의 StrOutputParser() 자리에 끼우는 부품입니다. 모델의 답을 글 대신 사전으로 받게 됩니다 |
parser.get_format_instructions() | 프롬프트에 붙일 형식 지시 글을 돌려준다 | 모델에게 "이 모양으로 답하라"고 알리는 글입니다. 출력처럼 한 줄뿐이라 열쇠 이름은 알려 주지 않습니다 |
AIMessage(...) | 모델의 답을 손으로 만든다 | 모델을 부르지 않고 파서만 시험하려는 대역입니다. 모델이 코드 블록 표시로 감싸 답하는 흔한 경우를 흉내 냈습니다 |
parser.invoke(reply) | 답에서 JSON 을 읽어 사전으로 바꾼다 | 체인 안에서는 모델의 답이 저절로 이 자리에 들어옵니다 |
data["관심사"][0] | 사전에서 값을 꺼낸다 | 받은 뒤에는 보통 사전처럼 쓴다는 뜻입니다 |
결과에서 읽을 것은 두 가지입니다.
- 형식 지시는 한 줄.
Return a JSON object.뿐이라 어떤 열쇠를 쓸지는 우리가 프롬프트에 따로 알려야 합니다. - 감싼 표시는 떼어 냈다. 둘째 줄의
dict {…}에는 코드 블록 표시가 없습니다. 관심사도 파이썬 목록이 되어바다를 바로 꺼냈습니다.
양식을 어긴 답을 넣으면 어떻게 되는지도 봅니다.
from langchain_core.exceptions import OutputParserException
try:
# JSON 이 아닌 줄글을 넣는다
parser.invoke(AIMessage("제주도로 2박 3일 다녀오시면 좋겠어요."))
except OutputParserException as e:
# 오류 글의 첫 줄만 본다
print("파서 오류:", str(e).splitlines()[0])파서 오류: Invalid json output: 제주도로 2박 3일 다녀오시면 좋겠어요.코드가 의미하는 것: 양식을 어긴 답
이 코드는 모델이 송장 양식을 어기고 줄글로 답한 경우를 흉내 냅니다. 파서가 읽지 못할 때 무엇이 일어나는지 봅니다.
| 코드 | 하는 일 | 챗봇에서 뜻하는 것 |
|---|---|---|
parser.invoke(AIMessage("제주도로 …")) | JSON 이 아닌 글을 파서에 넣는다 | 모델이 형식 지시를 어기고 설명만 쓴 경우입니다 |
except OutputParserException as e | 파서가 낸 오류를 받는다 | 챗봇에서는 이 자리에서 오류를 잡아야 화면이 멈추지 않습니다 |
str(e).splitlines()[0] | 오류 글의 첫 줄만 꺼낸다 | 오류 글에 모델이 쓴 답이 함께 들어 있어서 무엇이 잘못 왔는지 알 수 있습니다 |
결과에서 읽을 것은 두 가지입니다.
- 파서는 고쳐 주지 않는다. 파서는 줄글을 사전으로 바꾸려다 실패를 알릴 뿐, 알아서 칸을 채우지 않습니다.
- 오류에 모델의 답이 남는다.
Invalid json output:뒤에 모델이 쓴 줄글이 그대로 붙었습니다. 어떤 답이 왔는지 알아야 프롬프트를 고칠 수 있습니다.
주의할 점
JsonOutputParser()의 형식 지시는 "JSON 객체로 답하라"는 한 줄뿐입니다. 어떤 열쇠를 써야 하는지는 알려 주지 않으므로, 열쇠 이름은 프롬프트에 따로 적거나 다음 절의 Pydantic 틀을 씁니다.- 파서는 글을 사전으로 바꿀 뿐 값이 맞는지는 보지 않습니다.
"박수": -1도 그대로 통과합니다. 값 검사는 다음 절에서 합니다.