18장. Project 03. 내 서버에서 일하는 AI Agent
쓰기 작업은 확인을 거친다
비서가 휴가 신청서를 써도 도장은 본인이 찍습니다. 큰 금액의 이체는 은행 앱이 "정말 보내시겠습니까?"라고 다시 묻습니다. 에이전트의 쓰기 도구도 바로 실행하지 않고 작업을 "확인 대기"로 만들어 두고, 사람이 확인하면 그때 실행합니다.
1번에서 에이전트의 쓰기 도구는 조건을 검사한 뒤 작업 번호를 붙인 제안만 만듭니다. 2번에서 사용자(또는 결재자)가 그 작업을 확인합니다. 3번에서 서버가 작업을 실행하고 상태를 바꿉니다. 확인하는 사람은 그 작업의 주인이어야 합니다. 여기서 휴가 신청서가 제안이고, 도장이 확인입니다.
쓰기 작업의 상태
| 상태 | 뜻 | 다음 |
|---|---|---|
| 확인 대기 | 에이전트가 만든 제안 | 주인이 확인하면 실행, 아니면 그대로 |
| 접수됨 | 확인을 받아 실행했다 | 바꿀 수 없다(취소는 따로) |
| 결재 대기 | 휴가처럼 다른 사람(팀장)의 결재가 필요하다 | 결재 시스템으로 넘어간다 |
확인 대기 흉내 내기
import itertools
ids = itertools.count(1)
actions = {}
def propose(owner, kind, detail):
"""쓰기 도구는 실행하지 않고 제안만 만든다"""
aid = f"A-{next(ids):03d}"
actions[aid] = {"주인": owner, "종류": kind, "내용": detail, "상태": "확인 대기"}
return aid
def confirm(aid, who):
"""주인만 확인할 수 있고, 한 번 실행한 작업은 다시 실행하지 않는다"""
act = actions.get(aid)
if act is None or act["주인"] != who:
return 403, "확인할 수 없는 작업입니다"
if act["상태"] != "확인 대기":
return 409, f"이미 {act['상태']}"
act["상태"] = "접수됨"
return 200, act
aid = propose("C01", "반품 접수", {"주문번호": "O-1001"})
print(confirm(aid, "C02")) # 남이 확인하려 하면
print(confirm(aid, "C01")) # 주인이 확인하면
print(confirm(aid, "C01")) # 같은 확인을 두 번 보내면차례로 (403, …), (200, {… '상태': '접수됨'}), (409, '이미 접수됨') 이 나옵니다. 409 Conflict 는 "지금 상태와 맞지 않는 요청"이라는 뜻입니다. 화면에서 확인 단추를 두 번 눌러도 반품이 두 번 접수되지 않습니다.
코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
itertools.count(1) | 1 부터 하나씩 커지는 번호를 끝없이 내주는 번호표 기계를 만든다 |
next(ids) | 번호표 기계에서 다음 번호 하나를 뽑는다 |
f"A-{…:03d}" | 번호를 세 자리 정수(d = decimal, 10진수)로 맞추고 빈 앞자리를 0 으로 채운다. 1 은 A-001 이 된다 |
actions[aid] = {…, "상태": "확인 대기"} | 작업을 실행하지 않고 장부에 "확인 대기"로만 적는다 |
actions.get(aid) | 작업 번호로 작업을 찾는다. 없으면 None 이다 |
return 403, … | 403 Forbidden(금지됨)은 "권한이 없는 요청"을 뜻하는 HTTP 상태 코드다 |
return 200, act | 200 OK 는 "요청을 잘 처리했다"는 HTTP 상태 코드다 |
이 코드가 뜻하는 것
미용실 예약을 떠올려 봅니다. 직원이 장부에 예약을 연필로 적고 예약 번호를 알려 줍니다. 손님이 확인 문자에 답하면 그때 펜으로 확정합니다. 다른 사람이 대신 답할 수 없고, 같은 답을 두 번 보내도 예약은 하나입니다. 여기서 연필 예약이 propose, 손님의 확인 답장이 confirm, 예약 번호가 aid 입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
aid = propose("C01", "반품 접수", …) | C01 의 반품 작업을 확인 대기로 만들고 첫 번호 A-001 을 받는다 | 연필로 예약을 적고 예약 번호를 준다 |
confirm(aid, "C02") | 주인이 아니라서 403 을 돌려준다 | 다른 사람의 답장은 받아 주지 않는다 |
confirm(aid, "C01") | 상태를 "접수됨"으로 바꾸고 200 을 돌려준다 | 손님의 답장을 받고 펜으로 확정한다 |
confirm(aid, "C01") 한 번 더 | 이미 접수됨이라 409 를 돌려준다 | 같은 답장이 또 와도 예약은 하나다 |
결과에서 상태가 바뀐 것은 둘째 줄뿐입니다. 남의 확인과 겹친 확인은 장부를 건드리지 못합니다. 비유와 다른 점은, 미용실은 답이 없으면 예약을 저절로 취소하기도 하지만 이 코드의 작업은 확인할 때까지 "확인 대기"로 남는다는 것입니다.
작게 줄여서 눈으로 보기
작업 번호를 만드는 두 부품만 떼어 봅니다.
import itertools
ids = itertools.count(1)
print(next(ids), next(ids), next(ids)) # 부를 때마다 1씩 커진다
print(f"A-{7:03d}", f"A-{42:03d}") # 세 자리로 맞추고 빈 앞자리는 01 2 3
A-007 A-042| 결과 | 읽는 법 |
|---|---|
1 2 3 | 같은 번호표 기계에서 뽑을 때마다 다음 번호가 나온다. 번호가 겹치지 않는다 |
A-007 A-042 | 자릿수가 달라도 세 자리로 맞춰져, 글자 차례로 줄 세워도 번호 차례와 같다 |
주의할 점
- 조건 검사(기한, 잔여, 권한)는 제안을 만들 때 한 번, 실행할 때 한 번 더 합니다. 제안한 뒤 확인하기까지 그사이에 상태가 바뀔 수 있습니다.
- 확인 단계 없이 바로 실행해도 되는 쓰기 도구는 되돌리기 쉬운 것(장바구니 담기 정도)만으로 좁힙니다.