← Start here · Module 3A · 43 slides
LLM의 답을 자유 텍스트가 아니라 프로그램이 바로 쓸 수 있는 구조로 받아내는 4단계(Level 1→4). 느슨한 규칙에서 출발해, 검증 → 도구 호출 → 표준 프로토콜(MCP)로 올라갑니다.
"자연어 → SQL" 데모 하나로, 진짜 앱이 풀어야 했던 6가지 고민을 끌어냅니다.
What that app had to solve (Slide 6–11)
출력을 "구조화"하는 강도가 점점 세집니다. 필요에 맞는 가장 낮은 레벨을 고르세요.
Pick the lowest level that does the job
"이메일에서 이름·금액 하나만 뽑아 DB에 넣기" — 가장 알맞은 레벨은?
정해진 한 가지 모양을 단발로 뽑는 일엔 Level 2가 딱 맞아요. 도구 루프(3)나 MCP(4)는 더 복잡한 상황을 위한 것 — 가장 낮은 레벨로 충분하면 그걸 쓰는 게 정석입니다.
포맷 규칙이 있는 단순 텍스트. system prompt로 규칙을 강제하고, 방어적으로 후처리.
예: SQL 생성기에서 "``` 코드펜스 금지, SELECT로 시작" 같은 규칙을
system prompt에 넣고, 응답에서 혹시 붙은 코드펜스를 잘라내는 후처리를 둡니다.
싸고 빠르지만, 기계가 검증해 주는 보장은 없어요.
JSON schema로 출력 모양을 강제하고 검증. 실패하면 그 에러를 모델에 되먹여 스스로 고치게 합니다(자기 교정 재시도).
검증 루프:
1) tool use로 JSON 강제 → 2) Pydantic 등으로 검증
3) 실패 시 에러 메시지를 모델에 피드백 → 4) 자기 교정 재시도
→ 검증된 데이터 또는 구조화된 422 보장
chat-app의 "결함 찾기"에서 본 경계 검증과 같은 정신이에요 — 모델 출력을 믿지 말고, 쓰기 전에 형식을 확인.
모델이 반복 루프를 주도합니다. 정보를 모으거나 행동하려고 도구를 호출하고,
그 결과를 보고 다음 도구를 부르길 end_turn까지 반복.
# 에이전트 루프의 뼈대 (~20줄)
while True:
resp = client.messages.create(model=..., messages=messages, tools=tools)
if resp.stop_reason == "end_turn":
break
# 모델이 부른 도구 실행 → 결과를 messages에 append → 루프
messages.append({"role": "assistant", "content": resp.content})
messages.append({"role": "user", "content": tool_results})
이 ~20줄이 Claude Code·Cursor를 포함한 거의 모든 에이전트의 엔진입니다 (Module 5에서 더 깊이). "모델이 규칙을 벗어날 때(off-script)" 대비도 함께 다뤄요(슬라이드 33).
벤더가 도구 스키마를 한 번 publish하면, MCP 호환 클라이언트(Claude Desktop·Cursor·IDE 에이전트)가 손으로 배선하지 않고 그 도구를 발견해 씁니다.
N개 앱 × M개 도구를 일일이 잇던 문제가
"각자 표준만 따르면 끝"으로 바뀝니다.
Before MCP: 모든 통합을 손으로 배선 → N × M 개의 연결
After MCP: 도구가 표준 스키마를 publish → 클라이언트가 자동 발견 → N + M
15줄짜리 파이썬 서버로 도구를 publish하고(슬라이드 38), 클라이언트가 그걸 발견해(39) LLM에 꽂는(40) 흐름을 봅니다. 언제 MCP가 이득이고 아닌지(42)도 함께 — 작은 단일 앱엔 과하고, 여러 클라이언트가 같은 도구를 쓸 때 빛납니다.
MCP가 푸는 핵심 문제는?
MCP는 "도구↔클라이언트"를 표준화해, 매 조합마다 따로 배선하던 비용을 없앱니다. USB가 케이블 난립을 끝낸 것과 같은 아이디어예요.
제공 문서에서 정보를 뽑는 단발(single-turn) 구조화 추출기를 만듭니다(= Level 2 실습). 자유 형식 글 → 타입이 정해진 객체.
다운로드한 extract-starter.zip을 풀면 이 구조가 나옵니다(레포의 Projects/Module_03_extractor/starter/와 동일):
Projects/Module_03_extractor/starter/
├── extract.py # 추출 로직 — 여기만 고치면 됨
├── requirements.txt # anthropic, pydantic, python-dotenv
├── .env.example # ANTHROPIC_API_KEY=... 양식
└── documents/ # 입력 샘플들
├── job_posting.txt ← 스타터가 깨끗하게 통과
├── job_posting_2.txt ← 통과
├── meeting_notes.txt ← 일부러 깨짐
├── recipe.txt ← 일부러 깨짐
└── email.txt ← 일부러 깨짐
스타터: extract-starter.zip —
다운로드
· 레포에선 Projects/Module_03_extractor/starter/README.md 참고
tool_use"JSON으로 답해줘"라고 부탁만 하면 모델이 가끔 설명을 덧붙이거나 형식을 어겨서, 응답 텍스트를 직접 파싱해야 합니다. 이 스타터는 대신 도구 호출을 강제합니다:
resp = client.messages.create(
model="claude-sonnet-4-6",
system=SYSTEM_PROMPT,
messages=[{"role": "user", "content": text}],
tools=[EXTRACT_TOOL],
tool_choice={"type": "tool", "name": "record_job"}, # ← 반드시 이 도구를 부르게
)
block = next(b for b in resp.content if b.type == "tool_use")
return block.input # 이미 input_schema를 통과한 dict
tool_choice로 도구를 못 박으면 모델의 출력이
input_schema를 따르도록 API 단에서 보장됩니다. 그래서 block.input은
파싱이 필요 없는 dict로 바로 옵니다 — 정규식도, "혹시 ```json 블록인가" 걱정도 없습니다.
extract.py 상단의 세 블록은 같은 필드를 세 가지 시선으로 적은 것입니다. 하나를 바꾸면 셋 다 맞춰야 해요:
EXTRACT_TOOL — 모델에게 주는 JSON schema(어떤 칸이 있고 무엇이 필수인지)Job(BaseModel) — 받은 dict를 Pydantic 타입으로 재검증(2차 안전망)SYSTEM_PROMPT — 모호한 경우의 판단 규칙(모르면 null, 통화기호 제거 등)"salary_min": ["number","null"]을
허용해도, Pydantic float | None이 다시 한 번 타입을 보증해 줍니다.
스타터는 구인공고 전용입니다. job_posting*.txt엔 title·company·salary가 있어 깔끔히 통과하지만,
recipe.txt(재료·조리시간)나 email.txt엔 그런 필드가 없습니다. 그래서 모델이 빈 값을 지어내거나
(→ 부정확), 필수 필드가 비어 ValidationError로 떨어집니다. 이 실패가 패턴을 이해시키는 핵심이에요.
documents/*.txt 한 장씩 →
extract()가 Claude에 보내 강제 도구 호출 →
block.input(dict) →
validate()가 Job(**raw)로 검증 →
성공하면 JSON 출력, 실패하면 VALIDATION FAILED를 stderr로 찍고 실패 카운트 +1.
마지막에 N / M extracted successfully 요약을 출력합니다.
EXTRACT_TOOL · Job · SYSTEM_PROMPT)을 그 타입에 맞게 교체instructor-extractor
이 패턴을 실제 웹앱으로 키운 예제가 레포의 Projects/Module_03_extractor/webapp/입니다.
강사 구인 글(이메일·카톡 복붙)을 붙여넣으면 구조화 필드로 추출하고, 빈 필드는 ⚠️ 누락으로
표시하며, 누락 정보를 되묻는 follow-up 이메일 초안까지 Claude가 작성합니다.

tool_choice로 양식을 강제하고, 이메일(데이터→글)은 출력이 사람이 읽을 문장이라
tool 없이 자유 텍스트로 받습니다. 출력이 무엇이냐가 tool 강제 여부를 가릅니다.
코드·실행: Projects/Module_03_extractor/webapp/
tool_choice={"type":"tool", ...}로 도구를 강제하면 무엇이 좋아지나요?
강제 도구 호출은 모델 출력을 schema에 맞추도록 API 단에서 보장합니다. 그래서 block.input이
바로 dict로 와서, 정규식/JSON 블록 파싱이 필요 없어집니다. (속도·요금과는 무관합니다.)