4장. 의도 분류기 - 구조화 출력
왜 구조화 출력이 필요한가 — 코드는 문장을 읽지 못한다
한 줄 요약
3장까지는 모델의 답변을 사람이 읽고 이해했습니다. 이제는 코드가 모델의 답변을 받아 다음에 할 일을 결정해야 합니다. 이를 위해서는 자유로운 문장보다 항목과 값이 정해진 데이터 형식이 필요합니다. 이번 장에서는 모델이 정해진 형식으로 답하도록 하고, 그 결과를 활용해 고객 문의의 의도를 분류하는 기능을 만듭니다.
1. 다음 단계는 "어떤 문의인지"에 달려 있다
1장에서 그린 처리 흐름의 첫 단계는 [의도 파악] 이었습니다. 배송을 묻는 문의는 주문 데이터를 조회해야 하고, 환불을 묻는 문의는 정책 문서를 찾아야 하고, 사람을 찾는 문의는 상담원에게 넘겨야 합니다. 문의의 종류에 따라 다음에 실행할 코드가 달라집니다.
"어떤 종류인지 판단하는 일"은 LLM이 잘합니다. 문제는 그 판단 결과를 코드에서 받아 처리해야 한다는 데 있습니다.
if 문의의_종류 == "환불교환":
정책_문서를_찾는다()
elif 문의의_종류 == "주문배송조회":
주문_데이터를_조회한다()
이 if가 동작하려면 문의의_종류에는 미리 약속한 몇 개의 값 중 하나가 글자 그대로 들어 있어야 합니다.
2. 그냥 물으면 문장이 온다
"다음 문의가 어떤 유형인지 알려줘: 지난주에 산 운동화 아직도 안 왔는데 환불되나요?"라고 물으면 모델은 이렇게 답합니다. (이 교재를 준비하며 실행한 결과입니다. 문장은 실행할 때마다 달라집니다.)
문의하신 내용은 **[배송 및 환불/취소 문의]** 유형에 해당합니다.
구체적으로는 다음과 같은 두 가지 성격을 모두 가지고 있습니다.
1. **배송 문의:** "운동화 아직도 안 왔는데" (상품이 예상보다 늦게 오거나 아직 도착하지 않은 상태 확인)
2. **환불 문의:** "환불되나요?" (상품 미수령 또는 단순 변심 등에 따른 환불 가능 여부 및 절차 문의)
(이하 생략)
사람이 읽기에는 좋은 답입니다. 그런데 이 답을 받은 코드는 할 수 있는 일이 없습니다.
| 코드가 해 볼 만한 것 | 왜 안 되는가 |
|---|---|
if r.text == "환불교환": |
답은 문장 전체입니다. 라벨 하나와 같을 수 없습니다 |
if "환불" in r.text: |
"환불 문의가 아닙니다"라는 답에도 참이 됩니다 |
| 유형 이름을 잘라 낸다 | 같은 질문을 세 번 했더니 유형 이름이 "배송 및 환불/취소 문의", "배송/환불(취소) 문의", "배송 지연 및 환불/취소 문의"로 세 번 다 달랐습니다 |
3. "JSON으로 답해"라는 부탁으로는 부족하다
프로그램끼리 데이터를 주고받을 때 가장 널리 쓰는 형식이 JSON(JavaScript Object Notation) 입니다. 이름(키)과 값을 짝지어 적는 텍스트 형식입니다.
{
"intent": "환불교환",
"urgency": "보통"
}
파이썬에서는 json.loads()로 이 텍스트를 딕셔너리로 바꿔 result["intent"]처럼 꺼내 씁니다.
프롬프트에 "JSON으로 답해. 키: intent, urgency."라고 적을 수도 있습니다. 그런데 이것은 부탁입니다. 답을 어떤 기호로 감쌀지, intent에 어떤 이름을 넣을지는 여전히 모델이 그때그때 정합니다. 뒤의 if가 기다리는 것은 미리 약속한 값이므로, 형식과 값을 프롬프트의 글이 아니라 설정으로 정해 주는 방법이 필요합니다. 다음 절에서 봅니다.
부탁은 형식을 보장하지 않습니다. 필요한 것은 부탁이 아니라 강제입니다.
4. 이걸 모르면 무엇을 판단하지 못하는가
- 모델의 답으로 코드를 분기시켜도 되는지 가려내지 못합니다. 문장으로 분기하는 코드는 시험할 때는 되다가 어느 날 조용히 틀립니다.
- 답이 이상할 때 그것이 형식의 문제인지, 값의 문제인지, 판단의 문제인지 구분하지 못합니다. 셋은 고치는 곳이 다릅니다.
- 분류기가 얼마나 맞는지 숫자로 말하지 못합니다. 값이 정해져 있어야 정답과 비교할 수 있습니다.
5. 이번 장에서 만드는 것
모델의 출력을 자유 문장이 아니라 약속한 구조로 받아 내는 것을 구조화 출력(structured output) 이라고 합니다. 이번 장의 산출물은 문의 한 문장을 받아 네 칸을 채워 돌려주는 함수 classify() 입니다.
| 칸 | 들어갈 값 | 누가 쓰나 |
|---|---|---|
intent |
일곱 가지 라벨 중 하나 | 다음 단계를 고르는 코드 |
urgency |
낮음 / 보통 / 높음 | 처리 순서를 정하는 코드 |
order_id |
문의에 주문번호가 있으면 그 번호, 없으면 비움 | 주문을 조회하는 코드 |
summary |
한 줄 요약 | 넘겨받은 상담원 |
이 함수는 뒤에서 문의를 갈래별로 나눠 보내는 워크플로우(18장, 19장)가 그대로 가져다 씁니다.
핵심 정리
- 답을 받는 쪽이 코드가 되면, 문장은 쓸 수 없습니다.
- 코드가 분기하려면 미리 약속한 값 중 하나가 글자 그대로 와야 합니다.
- "JSON으로 답해"라는 부탁은 형식도 값도 보장하지 않습니다. 형식과 값은 설정으로 정해 줍니다.
- 출력을 약속한 구조로 받아 내는 것이 구조화 출력입니다.
- 이번 장의 산출물은 문의를 네 칸으로 정리하는
classify()입니다.