← Start here · Module 3A · 43 slides

Module 3A · Tools & Structure

LLM의 답을 자유 텍스트가 아니라 프로그램이 바로 쓸 수 있는 구조로 받아내는 4단계(Level 1→4). 느슨한 규칙에서 출발해, 검증 → 도구 호출 → 표준 프로토콜(MCP)로 올라갑니다.

📊 원본 슬라이드: ksetp.netlify.app/tools · 칩(슬라이드 N)이 덱의 번호와 같습니다.
🔌 42 연결: "출력을 구조로 강제 → 검증 → 실패 시 재시도"는 파서/프로토콜을 다뤄 본 사람에겐 익숙한 그림이에요. LLM을 믿을 수 없는 입력원으로 보고 경계에서 검증한다는 점만 새롭습니다.

3A.1 · Live demo: SQL Code Generation for BigQuery Slide 3–5

"자연어 → SQL" 데모 하나로, 진짜 앱이 풀어야 했던 6가지 고민을 끌어냅니다.

What that app had to solve (Slide 6–11)

Structure · Context · Prompting · Defense · Architecture · Cost
  • Structure — 출력이 항상 같은 모양이어야 기계가 받아 쓴다
  • Context — 어떤 테이블·컬럼이 있는지 모델에 알려줘야 한다
  • Prompting — 규칙(코드펜스 금지 등)을 명확히 지시
  • Defense — 모델이 규칙을 어겼을 때 후처리로 방어
  • Architecture — 단발 호출이냐, 도구 루프냐
  • Cost — 토큰·재시도 비용 관리

3A.2 · What you'll learn: 4 Levels Slide 12–17

출력을 "구조화"하는 강도가 점점 세집니다. 필요에 맞는 가장 낮은 레벨을 고르세요.

flowchart LR L1["Level 1
Parseable text"] --> L2["Level 2
Structured (JSON schema)"] L2 --> L3["Level 3
Tool use (agent loop)"] L3 --> L4["Level 4
MCP"]

Pick the lowest level that does the job

flowchart TD Q{"what do you need?"} Q -->|"one fixed shape, single call"| L2b["Level 2
structured output + schema"] Q -->|"just a formatting rule"| L1b["Level 1
parseable text + post-process"] Q -->|"model must act: call tools, loop"| L3b["Level 3
tool use (agent loop)"] Q -->|"share tools across apps/clients"| L4b["Level 4
MCP server"]
🎚️ 가장 낮은 레벨 우선: 레벨이 높을수록 강력하지만 복잡·비용도 같이 올라가요. 한 가지 모양만 뽑으면 Level 2로 끝, 굳이 에이전트 루프(3)나 MCP(4)를 세우지 마세요. 필요가 커질 때만 한 칸씩 올립니다.

"이메일에서 이름·금액 하나만 뽑아 DB에 넣기" — 가장 알맞은 레벨은?

정해진 한 가지 모양을 단발로 뽑는 일엔 Level 2가 딱 맞아요. 도구 루프(3)나 MCP(4)는 더 복잡한 상황을 위한 것 — 가장 낮은 레벨로 충분하면 그걸 쓰는 게 정석입니다.

3A.3 · Level 1 · Parseable output Slide 14, 18–21

포맷 규칙이 있는 단순 텍스트. system prompt로 규칙을 강제하고, 방어적으로 후처리.

✉️ 비유: "답장은 한 줄로, 인사말 빼고, 코드블록 쓰지 말고"라고 미리 일러두는 것. 상대가 대체로 지키지만, 가끔 어기니 받는 쪽에서 한 번 더 다듬습니다.

예: SQL 생성기에서 "``` 코드펜스 금지, SELECT로 시작" 같은 규칙을 system prompt에 넣고, 응답에서 혹시 붙은 코드펜스를 잘라내는 후처리를 둡니다. 싸고 빠르지만, 기계가 검증해 주는 보장은 없어요.

3A.4 · Level 2 · True structured output Slide 15, 22–27

JSON schema로 출력 모양을 강제하고 검증. 실패하면 그 에러를 모델에 되먹여 스스로 고치게 합니다(자기 교정 재시도).

📋 비유: 빈칸이 정해진 양식을 주는 것. "이름/금액/날짜 칸을 채워" → 틀린 칸이 있으면 "여기 형식 틀렸어요, 다시"라고 돌려보냅니다. 통과하면 깨끗한 데이터 보장.
검증 루프:
  1) tool use로 JSON 강제 → 2) Pydantic 등으로 검증
  3) 실패 시 에러 메시지를 모델에 피드백 → 4) 자기 교정 재시도
  → 검증된 데이터 또는 구조화된 422 보장

chat-app의 "결함 찾기"에서 본 경계 검증과 같은 정신이에요 — 모델 출력을 믿지 말고, 쓰기 전에 형식을 확인.

3A.5 · Level 3 · Tool use (the agent loop) Slide 16, 28–33

모델이 반복 루프를 주도합니다. 정보를 모으거나 행동하려고 도구를 호출하고, 그 결과를 보고 다음 도구를 부르길 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).

3A.6 · Level 4 · MCP — Model Context Protocol Slide 17, 34–42

벤더가 도구 스키마를 한 번 publish하면, MCP 호환 클라이언트(Claude Desktop·Cursor·IDE 에이전트)가 손으로 배선하지 않고 그 도구를 발견해 씁니다.

🔌 비유 — USB: 기기마다 전용 케이블을 만들던 세상(N×M)에서, 표준 포트 하나로 통일(N+M)된 것. N개 앱 × M개 도구를 일일이 잇던 문제가 "각자 표준만 따르면 끝"으로 바뀝니다.
Before MCP: 모든 통합을 손으로 배선 → N × M 개의 연결
After  MCP: 도구가 표준 스키마를 publish → 클라이언트가 자동 발견 → N + M

15줄짜리 파이썬 서버로 도구를 publish하고(슬라이드 38), 클라이언트가 그걸 발견해(39) LLM에 꽂는(40) 흐름을 봅니다. 언제 MCP가 이득이고 아닌지(42)도 함께 — 작은 단일 앱엔 과하고, 여러 클라이언트가 같은 도구를 쓸 때 빛납니다.

MCP가 푸는 핵심 문제는?

MCP는 "도구↔클라이언트"를 표준화해, 매 조합마다 따로 배선하던 비용을 없앱니다. USB가 케이블 난립을 끝낸 것과 같은 아이디어예요.

3A.7 · Project: single-turn structured extractor Slide 43

제공 문서에서 정보를 뽑는 단발(single-turn) 구조화 추출기를 만듭니다(= Level 2 실습). 자유 형식 글 → 타입이 정해진 객체.

📋 비유: 사람이 자유롭게 쓴 글을 받아 정해진 양식의 빈칸(제목·회사·연봉…)에 옮겨 적는 일입니다. 빈칸의 모양(타입)을 미리 정해두고, 모델이 그 칸에만 답을 채우도록 강제하는 게 이 프로젝트의 전부예요.

What's in the starter zip

다운로드한 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 참고

Key pattern: forced 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 블록인가" 걱정도 없습니다.

You only fix three places (they move as one)

extract.py 상단의 세 블록은 같은 필드를 세 가지 시선으로 적은 것입니다. 하나를 바꾸면 셋 다 맞춰야 해요:

three blocks
  • EXTRACT_TOOL — 모델에게 주는 JSON schema(어떤 칸이 있고 무엇이 필수인지)
  • Job(BaseModel) — 받은 dict를 Pydantic 타입으로 재검증(2차 안전망)
  • SYSTEM_PROMPT — 모호한 경우의 판단 규칙(모르면 null, 통화기호 제거 등)
🛡️ 두 겹 검증: schema는 모델 출력 단계의 1차 방어, Pydantic은 내 코드가 객체로 받는 입력 단계의 2차 방어입니다. schema가 "salary_min": ["number","null"]을 허용해도, Pydantic float | None이 다시 한 번 타입을 보증해 줍니다.

Intentionally broken samples — that's the lesson

스타터는 구인공고 전용입니다. 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 요약을 출력합니다.

to do
  • 대상 문서 타입 하나 고르기(비-구인 샘플 또는 직접 작성)
  • 세 블록(EXTRACT_TOOL · Job · SYSTEM_PROMPT)을 그 타입에 맞게 교체
  • 관찰한 실패 하나를 막는 명시적 규칙을 system prompt에 추가
  • "깨끗이 되는 1건 + 깔끔히 깨지는 1건"을 만들 때까지 반복

A real extension: instructor-extractor

이 패턴을 실제 웹앱으로 키운 예제가 레포의 Projects/Module_03_extractor/webapp/입니다. 강사 구인 글(이메일·카톡 복붙)을 붙여넣으면 구조화 필드로 추출하고, 빈 필드는 ⚠️ 누락으로 표시하며, 누락 정보를 되묻는 follow-up 이메일 초안까지 Claude가 작성합니다.

instructor-extractor 데모 — 추출 결과와 누락 필드, 생성된 follow-up 이메일

🔁 두 방향의 LLM 사용: 추출(글→데이터)은 출력이 기계가 읽을 구조라 tool_choice로 양식을 강제하고, 이메일(데이터→글)은 출력이 사람이 읽을 문장이라 tool 없이 자유 텍스트로 받습니다. 출력이 무엇이냐가 tool 강제 여부를 가릅니다.

코드·실행: Projects/Module_03_extractor/webapp/

tool_choice={"type":"tool", ...}로 도구를 강제하면 무엇이 좋아지나요?

강제 도구 호출은 모델 출력을 schema에 맞추도록 API 단에서 보장합니다. 그래서 block.input이 바로 dict로 와서, 정규식/JSON 블록 파싱이 필요 없어집니다. (속도·요금과는 무관합니다.)