페이지 목차
이 페이지에서 검색
멀티 에이전트
이 페이지에 묻기
멀티 에이전트 · 현재 차시와 목차 본문에서만 찾아 답합니다. 답을 지어내지 않고 해당하는 대목을 그대로 보여 주며, 여기에 없으면 없다고 알려 드립니다.
생성형 AI가 아니라 이 페이지 본문을 찾아 그대로 보여 주는 방식입니다. 외부로 나가는 요청이 없습니다.
MULTI · AGENT
멀티 에이전트
생성 수정
도구 실행·검색·기억·그래프 흐름·전문 에이전트의 협업을 구성하고 권한·평가·사람의 확인으로 통제합니다.
26개 문서 · 1일차 · 5 / 26
4차시 · 의도 분류기 — 구조화 출력
지도 위치 : 1일차 · README 4장 ① 호출 층의 마지막 — 모델의 글을 코드가 쓸 수 있는 데이터 로 | 이전: 3차시 프롬프트 설계 | 다음: 5차시 Function Calling 원리 키워드 : 구조화 출력, 스키마 검증, 예외 처리
이 차시를 다 읽으면
- 구조화 출력을 유와 종차로 정의하고, 프롬프트로 "JSON으로" 부탁 하는 것과 스키마로 강제 하는 것의 차이를 말할 수 있다.
- Pydantic과
Literal로 분류값을 고정한 스키마를 읽고,messages.parse코드의 핵심 줄을 짚을 수 있다. - ⭐ 스키마는 형식을 보장할 뿐, 분류가 맞는지는 보장하지 않는다 (축 ⑧)를 예로 설명할 수 있다.
- 낮은 확신 · 기타, 파싱 실패 · 거절 · 잘림을 각각 어떻게 처리할지 정할 수 있다.
- 평가셋 혼동표 를 읽고, 분류를 가벼운 모델(Haiku)에 맡길지 비용과 정확도로 판단할 수 있다.
0. 이 차시의 판별 축
| # | 축 | 왼쪽 (0) | 오른쪽 (1) | 이 축으로 갈리는 것 | 큰 축 (README 5장) |
|---|---|---|---|---|---|
| 1 | ⭐ 형식인가 뜻인가 | 구문 — JSON이 스키마에 맞는가 | 의미 — 분류가 맞는가 | 스키마 검증 ↔ 평가셋 채점 | ⑧ 구문론 ↔ 의미론 |
| 2 | ⭐ 부탁 ↔ 강제 | 프롬프트에 "JSON으로 답해" | 스키마로 형식을 강제 | 형식이 깨질 수 있는가 | 5.4 파생 축 (부모 ②) |
| 3 | 누가 흐름을 정하는가 | 코드 — 분류 결과로 분기 | 모델 — 스스로 다음 행동 결정 | 분류기 뒤의 처리 순서 | 5.2 중심 축 |
| 4 | 한 건의 비용 ↔ 전체 비용 | 분류 한 번의 비용 | 모든 질문 × 분류 비용 | 분류에 어떤 모델을 쓸까 | 5.4 파생 축 (부모 ⑤) |
이 차시의 질문은 "모델의 자유로운 글을, 코드가 if로 분기할 수 있는 값으로 바꾸려면?" 입니다. 앞에서 프롬프트로 형식을 부탁하고 판단 근거를 남기게 했습니다(3차시). 이번에는 형식을 강제 합니다. 그리고 강제로도 해결되지 않는 것(뜻이 맞는가)을 평가셋으로 잽니다.
1. 구조화 출력 — 부탁에서 강제로
1.1 정의
의도 분류 — [텍스트 분류]인데, [ 사용자의 말이 무엇을 원하는지 미리 정한 범주 중 하나로 고른다 ]는 점에서 말의 감정(긍정 · 부정)을 고르는 감정 분류와 구별된다.
구조화 출력 (Structured Output) — [LLM 출력 방식]인데, [ 정해진 스키마(필드 이름 · 타입 · 허용 값)에 맞는 데이터로만 답하게 강제한다 ]는 점에서 자유로운 글로 답하는 일반 응답과 구별된다.
스키마 — [데이터 약속]인데, [ 어떤 필드가 있고 각 필드에 어떤 값이 올 수 있는지 기계가 검사할 수 있게 적은 것 ]이라는 점에서 사람이 읽는 설명문과 구별된다.
1.2 부탁 ↔ 강제
| 프롬프트로 부탁 | 스키마로 강제 | |
|---|---|---|
| 방법 | 시스템 프롬프트에 "JSON으로, intent 필드에 답해" | output_format=Intent (2절) |
| 형식 깨짐 | 가끔 — 앞에 "네, 분류 결과입니다:"를 붙이거나, 필드 이름을 바꾸거나, 없는 분류값을 만듦 | 스키마에 맞는 JSON만 나오도록 생성 단계에서 제한 |
| 코드 | 파싱 실패 처리를 여기저기 | 검증된 객체를 바로 사용 |
⚠️ 최신 Claude 모델은 답의 앞부분을 미리 채워 두는 방식(assistant prefill)으로 형식을 유도할 수 없습니다(400 에러). 형식은 구조화 출력 으로 맞춥니다.
2. 스키마 정의 — Pydantic
2.1 Pydantic과 Literal
Pydantic — [파이썬 라이브러리]인데, [ 클래스로 데이터 모양을 선언하면 들어온 값이 그 모양에 맞는지 자동으로 검사한다 ]는 점에서 아무 값이나 담기는 일반 딕셔너리와 구별된다.
from pydantic import BaseModel
from typing import Literal
class Intent(BaseModel):
intent: Literal["휴가조회", "규정질문", "장비신청", "IT장애", "기타"]
confidence: Literal["high", "low"]
reason: str
🔍 읽는 포인트 : 필드 세 개만 보세요.
intent—Literal[...]안의 다섯 값 외에는 올 수 없습니다. "휴가 조회"(띄어쓰기)나 "연차문의" 같은 변형이 원천 차단됩니다.confidence— 모델이 스스로 매기는 확신. 3.2에서 씁니다.reason— 앞에서 배운 판단 근거 필드 (3차시). 틀린 분류의 원인을 찾는 단서입니다.
2.2 호출 — messages.parse
response = client.messages.parse(
model="claude-opus-5-5",
max_tokens=1024,
system=CLASSIFIER_PROMPT,
messages=[{"role": "user", "content": user_text}],
output_format=Intent,
)
result = response.parsed_output # 검증된 Intent 객체
print(result.intent, result.confidence, result.reason)
🔍 읽는 포인트 : create 대신 parse, 그리고 output_format=Intent 한 줄. 돌아온 parsed_output은 글이 아니라 .intent로 꺼낼 수 있는 객체 입니다.
💡 Pydantic을 쓰지 않는 방법도 있습니다 — client.messages.create(..., output_config={"format": {"type": "json_schema", "schema": {...}}})로 JSON 스키마를 직접 주면 첫 text 블록이 스키마에 맞는 JSON입니다. 이때 스키마에 "additionalProperties": False와 required를 꼭 넣습니다. 스키마에서 지원하지 않는 제약도 있으니 공식 문서를 확인하세요.
3. ⭐ 스키마가 보장하는 것과 보장하지 않는 것
3.1 축 ⑧ — 형식은 맞고 뜻은 틀린 답
{"intent": "장비신청", "confidence": "high", "reason": "노트북 관련 요청"}
질문은 "노트북 화면이 깨져서 안 보여요" 였습니다. 기대 의도는 IT장애(고장 신고)입니다.
| 검사 | 결과 | 누가 잡나 |
|---|---|---|
구문 — 필드가 다 있는가, intent가 다섯 값 중 하나인가 | ⭕ 통과 | 스키마 (자동) |
의미 — 이 질문의 의도가 정말 장비신청인가 | ❌ 틀림 | 평가셋 (6절) |
⭐ 스키마는 "말이 되는 모양"을 보장하고, 평가셋은 "맞는 말"인지를 잽니다. 둘은 다른 축의 검사라서 하나가 다른 하나를 대신하지 못합니다. AI가 "구조화 출력을 적용해서 분류가 정확해졌습니다"라고 보고하면, 형식 오류가 사라진 것 과 정확도가 오른 것 을 나눠서 증거를 요구하세요.
3.2 낮은 확신과 "기타"
| 결과 | 처리 | 이유 |
|---|---|---|
confidence="high" + 다섯 의도 중 하나 | 해당 처리로 진행 | 정상 |
confidence="low" | 되묻기 — "휴가 잔여를 확인하시려는 건가요, 휴가 신청을 하시려는 건가요?" | 틀린 처리로 가는 것보다 한 번 묻는 것이 쌈 |
intent="기타" | 범위 밖 안내 또는 담당자 연결 | 1차시 "사람만" 칸의 요청이 여기로 옴 |
⚠️ confidence도 모델의 자기 평가 입니다. "high"인데 틀리는 경우가 있습니다(3.1의 예). 확신 값이 실제로 쓸모 있는지는 평가셋에서 "low인 문항의 오답률이 high보다 높은가" 로 확인합니다.
💡 "기타"를 빼면 모델은 범위 밖 질문도 다섯 의도 중 하나에 억지로 넣습니다. 탈출구 값은 꼭 둡니다.
4. 예외 처리
| 상황 | 알아보는 법 | 처리 |
|---|---|---|
| 거절 | stop_reason == "refusal" | 결과를 읽지 않음. "답할 수 없는 요청입니다" + 로그 |
| 잘림 | stop_reason == "max_tokens" | JSON이 중간에 끊겼을 수 있음. max_tokens를 늘리거나 reason을 짧게 하라고 지시 |
| 파싱 실패 | parsed_output이 비어 있거나 검증 예외 | 한 번 재시도 → 그래도 실패면 기타로 보고 사람에게 |
| API 오류 | anthropic. 에러 클래스 (2차시) | 일시 오류는 재시도, 400은 요청 수정 |
def classify(user_text: str) -> Intent:
try:
response = client.messages.parse(..., output_format=Intent)
except anthropic.APIError as e:
log.error("분류 API 오류: %s", e)
return FALLBACK # Intent(intent="기타", confidence="low", reason="분류 실패")
if response.stop_reason in ("refusal", "max_tokens") or response.parsed_output is None:
log.warning("분류 비정상 종료: %s", response.stop_reason)
return FALLBACK
return response.parsed_output
🔍 읽는 포인트 : 어떤 실패든 FALLBACK(기타 + low)으로 모입니다. 분류기가 실패해도 서비스는 죽지 않고 "사람에게 연결" 쪽으로 안전하게 넘어갑니다. ⚠️ 이 코드는 구조 예시입니다. 거절 · 잘림 때 parse가 예외를 내는지 빈 값을 주는지는 SDK 버전에 따라 다를 수 있으니 공식 문서로 확인하게 하세요.
5. 분류 결과로 분기 — 워크플로우
intent = classify(user_text)
if intent.confidence == "low":
return ask_clarifying_question(user_text)
match intent.intent:
case "휴가조회": return handle_leave(user) # 6차시: 휴가 조회 도구
case "IT장애": return handle_it_ticket(user) # 6차시: 티켓 조회 도구
case "규정질문": return handle_policy(user_text) # 12~15차시: 문서 검색
case "장비신청": return draft_equipment_request() # 23차시: 사람 승인
case _: return hand_over_to_human()
🔍 읽는 포인트 : match 아래 다섯 줄이 갈림길 입니다. 어느 길로 갈지는 모델이 고른 값으로 정하지만, 갈림길 자체와 각 길의 처리 순서는 코드가 정했습니다. 1차시 사다리의 2칸 워크플로우 입니다.
| 이 분류기 (워크플로우) | 9차시 에이전트 | |
|---|---|---|
| 모델이 하는 일 | 다섯 값 중 하나를 고름 | 다음에 무엇을 할지 스스로 계속 정함 |
| 흐름 | 코드의 match | 모델의 반복 |
| 시험 | 분기마다 따로 시험 가능 | 경로가 매번 다를 수 있음 |
앞으로 다시 만납니다 — 7차시에서 이 분류 결과로 쓸 도구 묶음을 좁히고 , 19차시에서 같은 분기를 그래프의 조건부 엣지로 다시 만듭니다.
6. 평가셋과 혼동표
6.1 혼동표 읽기
혼동표 (Confusion Matrix) — [평가 표]인데, [ 기대 범주(행)와 실제 예측(열)을 교차해 어떤 범주를 어떤 범주로 착각했는지 센다 ]는 점에서 맞은 비율 하나만 보여 주는 정확도와 구별된다.
| 기대 \ 예측 | 휴가조회 | 규정질문 | 장비신청 | IT장애 | 기타 | 합계 |
|---|---|---|---|---|---|---|
| 휴가조회 | ___ | ___ | ___ | ___ | ___ | ___ |
| 규정질문 | ___ | ___ | ___ | ___ | ___ | ___ |
| 장비신청 | ___ | ___ | ___ | ___ | ___ | ___ |
| IT장애 | ___ | ___ | ___ | ___ | ___ | ___ |
| 기타 | ___ | ___ | ___ | ___ | ___ | ___ |
(빈칸 양식입니다.) 대각선 이 맞힌 수, 대각선 밖이 착각입니다. 정확도 = 대각선 합 ÷ 전체.
⭐ 정확도 하나보다 어디서 틀리는가 가 더 중요합니다.
IT장애행의장비신청칸이 크다 → 고장 신고와 새 장비 요청의 경계가 흐림 → 프롬프트에 경계 기준과 예시(3차시)를 추가기타행이 다른 칸으로 새어 나간다 → 범위 밖 질문을 억지로 분류 → 위험 (1차시 "사람만" 칸이 엉뚱한 처리로 감)
6.2 가벼운 모델로 분류할까
분류는 모든 질문마다 한 번씩 일어납니다. 답을 길게 쓰지도 않습니다. 앞에서 본 한 건 비용 계산(2차시)을 다시 쓰면:
| 모델 | 분류 정확도 | 기타 오분류 | 한 건 비용 (계산) | 하루 건수 × 비용 |
|---|---|---|---|---|
claude-haiku-5-5 | ___ % | ___ 건 | ___ | ___ |
claude-sonnet-5-5 | ___ % | ___ 건 | ___ | ___ |
claude-opus-5-5 | ___ % | ___ 건 | ___ | ___ |
(빈칸 양식입니다. 가격은 공식 가격 페이지 확인.)
판단 순서 — ① 가장 싼 모델부터 평가셋을 돌린다 → ② 완료 기준(1차시)을 넘으면 거기서 멈춘다 → ③ 못 넘으면 프롬프트 개선 → 그래도 안 되면 한 단계 위 모델. ⚠️ 정확도가 비슷해도 기타 행의 오분류 (위험한 요청을 놓침)가 많은 모델은 탈락입니다.
7. 실습 — 맡기고 검수하기
지시
오피스봇 의도 분류기를 구조화 출력으로 만들고 평가해줘.
참고 공식 문서: Anthropic Structured Outputs(messages.parse, output_format), Pydantic 공식 문서
목표:
1. classifier.py: Intent 모델(intent Literal 5개, confidence high/low, reason)과 classify() 함수
- refusal, max_tokens, 파싱 실패, API 오류 → 모두 FALLBACK(기타, low)으로, 원인은 로그에
2. router.py: 분류 결과로 분기하는 match 문 (각 처리는 지금은 "TODO: 처리 이름"만 반환)
3. eval_classifier.py: eval/eval_set_v0.csv로 혼동표(5×5), 정확도, confidence별 오답률 출력
4. 같은 평가를 claude-haiku-5-5와 claude-opus-5-5로 각각 실행, usage로 한 건 평균 비용 계산
5. 틀린 문항은 [질문 | 기대 | 예측 | reason] 표로
제약: 프롬프트로 "JSON으로 답해"라고 쓰는 방식은 쓰지 말 것. assistant prefill 금지. temperature 금지.
추측하지 말고 실행 명령과 실행 출력 원문을 붙여.
보고 형식: 모델별 [정확도 | 기타 오분류 | high 오답률 | low 오답률 | 평균 비용] 표 + 혼동표 원문
받을 증거
- 모델별 혼동표 출력 원문 과 틀린 문항 표
- 일부러
max_tokens=20으로 줄여 실행했을 때 FALLBACK으로 떨어진 로그
내가 직접 확인할 것
| 볼 것 | 이걸로 아는 것 |
|---|---|
| 혼동표의 대각선 합 ÷ 전체가 보고된 정확도와 같은가 (손계산) | 6.1 — 숫자를 믿을 수 있는가 |
| "노트북 화면이 깨졌어요", "노트북 하나 더 필요해요"를 직접 넣어 두 의도가 갈리는가 | 3.1 — 형식이 아니라 뜻을 맞히는가 |
"팀장님한테 폭언을 들었어요"가 기타로 가는가 | 3.2 — 사람이 맡을 요청을 놓치지 않는가 |
low인 문항의 오답률이 high보다 높은가 | 3.2 — confidence 값이 쓸모 있는가 |
| Haiku와 Opus의 정확도 차이 vs 비용 차이 | 6.2 — 분류에 어떤 모델이면 충분한가 |
부록 A. 스스로 확인하기
| # | 질문 | 답이 있는 곳 |
|---|---|---|
| 1 | 구조화 출력을 유와 종차로 정의하면? | 1.1 |
| 2 | 프롬프트로 "JSON으로"와 스키마 강제의 차이는? | 1.2 |
| 3 | Literal이 막아 주는 것은? | 2.1 |
| 4 | messages.parse에서 볼 두 줄은? | 2.2 |
| 5 | 스키마를 통과했는데 틀린 답의 예와, 그것을 잡는 수단은? | 3.1 |
| 6 | confidence="low"와 기타를 각각 어떻게 처리하나? | 3.2 |
| 7 | 거절 · 잘림 · 파싱 실패를 한곳으로 모으는 이유는? | 4 |
| 8 | 분류 결과로 match 분기하는 구조가 워크플로우인 이유는? | 5 |
| 9 | 혼동표에서 정확도 하나보다 더 봐야 하는 것은? | 6.1 |
| 10 | 분류 모델을 고르는 판단 순서는? | 6.2 |
부록 B. 자주 하는 실수
| 실수 | 바로잡기 |
|---|---|
| 프롬프트에 "반드시 JSON으로"만 쓰고 끝 | ❌ 부탁. 스키마로 강제 (1.2) |
| 응답 앞부분을 미리 채워 형식 유도 | ❌ 최신 모델은 400. 구조화 출력으로 (1.2) |
| "구조화 출력을 쓰니 분류가 정확하다" | ❌ 형식 보장 ≠ 의미 보장. 평가셋으로 (3.1) |
| 분류값에 "기타"가 없음 | ⚠️ 범위 밖 질문이 억지로 분류됨 (3.2) |
stop_reason 확인 없이 parsed_output 사용 | ❌ 거절 · 잘림 먼저 확인 (4) |
| 정확도 하나만 보고 판단 | ⚠️ 혼동표에서 어디로 틀리는지 (6.1) |
| 처음부터 가장 비싼 모델로 분류 | ⚠️ 가장 싼 모델부터, 기준을 넘으면 멈춤 (6.2) |
부록 C. NotebookLM 복습 프롬프트
이 4차시 노트만 근거로 6문제를 내줘. 비전공자 기준으로.
"부탁과 강제", "스키마는 형식만 보장하고 뜻은 평가셋으로", "낮은 확신과 기타 처리", "혼동표 읽기", "분류 모델 고르기"는 반드시 포함해줘.
답은 내가 답한 다음에 채점하고, 틀린 이유를 노트의 절 번호를 근거로 설명해줘.
LangTailor