본문 바로가기
C.W.K.
Stream
Lesson 04 of 05 · published

Side-effect 경계

~22 min · read-tool, write-tool, idempotency, human-in-the-loop

Level 0호기심 많은 독자
0 XP0/48 lessons0/14 achievements
0/100 XP to next level100 XP to go0% complete

모든 tool 이 같은 무게는 아니야. Read tool — 주문 목록, 고객 조회, 가격 확인 — 은 모델이 실수로 열 번을 불러도 아무 일 없어. Write tool — 환불, 이메일 발송, 배포 예약 — 은 모델이 한 번 흥분하면 누군가의 하루가 날아가. Protocol 이 이 구분을 대신 지켜주지 않아. 네가 직접 만들어 넣어야 해.

첫 번째 습관은 이름으로 갈라놓기. Read tool 은 질문처럼 들려야 해: search_orders, get_customer, list_invoices. Write tool 은 동사에 대상을 붙여: refund_order, send_email, schedule_deploy. 이름은 schema 바깥에서 모델이 보는 몇 안 되는 단서라서, write 에 동사를 쓰는 것만으로도 정확도가 조용히 올라가.

두 번째 습관은 write tool 에 idempotency key 붙이기. 네트워크가 끊겨서 같은 write 가 두 번 들어오면, 시스템이 그걸 알아채고 중복을 걷어내야 해. 네가 부르는 API 는 보통 이미 이걸 지원하고 (Stripe 의 Idempotency-Key 같은 거), 네 tool 은 대화 맥락에서 만든 key 를 받아서 그대로 넘겨주면 돼.

세 번째 습관은 위험한 가장자리에 사람을 세우는 것. 되돌릴 수 없는 write — 돈이 나가고, 메시지가 나가고, infra 가 바뀌는 것 — 에는 두 단계 tool 패턴을 써. propose_refund 가 정돈된 제안서를 돌려주고, agent 가 그걸 사람한테 보여주고, 명시적으로 승인을 받은 다음에야 proposal ID 를 들고 execute_refund 를 불러. Protocol 은 제안서의 모양까지만 책임져. 승인을 받아내는 건 네가 user 한테 진 빚이야.

MCP 도 이 관점을 명시적으로 가져왔어. 세상에 영향을 줄 수 있는 tool 은 표시해두고, client 가 그걸 user 한테 보여주라고 spec 이 요구하거든. 관점 자체는 좋은 API 설계자들이 이미 하던 거야. MCP 는 그걸 contract 안으로 들여놨을 뿐이지.

Code

두 단계 write — 제안하고, 그다음 실행·python
{
  "name": "propose_refund",
  "description": "Build a refund proposal for human review. Does NOT execute.",
  "input_schema": {
    "type": "object",
    "properties": {
      "order_id": {"type": "string"},
      "amount_cents": {"type": "integer"},
      "reason": {"type": "string"}
    },
    "required": ["order_id", "amount_cents", "reason"]
  }
},
{
  "name": "execute_refund",
  "description": (
    "Execute a previously-approved refund. Requires a proposal_id from "
    "propose_refund AND the human's explicit approval. Do not call without "
    "approval — the user-facing client will reject it."
  ),
  "input_schema": {
    "type": "object",
    "properties": {
      "proposal_id": {"type": "string"},
      "approved_by_user": {"type": "boolean"}
    },
    "required": ["proposal_id", "approved_by_user"]
  }
}
경계에서 idempotency·python
import hashlib, requests

def execute_refund(proposal_id, approved_by_user, conversation_id):
    if not approved_by_user:
        return {"error": "Refund requires explicit user approval."}
    idem_key = hashlib.sha256(f"{conversation_id}:{proposal_id}".encode()).hexdigest()
    r = requests.post(
        "https://api.stripe.com/v1/refunds",
        data={"payment_intent": proposal_id_to_intent(proposal_id)},
        headers={"Idempotency-Key": idem_key, "Authorization": "Bearer ..."},
    )
    return r.json()

External links

Exercise

네가 관리하는 (실제든 가상이든) 프로젝트의 tool 을 전부 적어. 각각에 글자 하나씩 붙여 — 순수 read 면 R, write 면 W, 되돌릴 수 없는 파괴면 D. 모든 D 에 propose/execute 분리를 설계해봐. 두 단계가 없는 D 의 개수가 네가 남겨둔 숙제야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.