~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()
네가 관리하는 (실제든 가상이든) 프로젝트의 tool 을 전부 적어. 각각에 글자 하나씩 붙여 — 순수 read 면 R, write 면 W, 되돌릴 수 없는 파괴면 D. 모든 D 에 propose/execute 분리를 설계해봐. 두 단계가 없는 D 의 개수가 네가 남겨둔 숙제야.
Progress
Progress is local-only — sign in to sync across devices.