AI 에이전트 — 모델이 아니라 그래프 · 중단점과 사람의 개입 · 실습
고액 환불이 승인 목록에 뜨지도 않았다
목표
되돌릴 수 없는 일 앞에서 그래프를 멈추고, 사람이 값을 고치거나 거절한 뒤 이어서 돌린다. 멈추는 방법 두 가지를 모두 만들어 보고 둘의 차이를 직접 센 숫자로 확인한다.
왜 중요한가
LangGraph 에서 멈춤은 저장 위에 세워져 있다. 멈춘다는 것은 "다음 단계를 지금 하지 않고 남겨 둔 상태에서 나중에 이어서 한다" 는 뜻이라, 남겨 둘 곳(체크포인터)이 없으면 이어서 할 수도 없다. 그런데 체크포인터 없이 중단점만 주어도 오류가 나지 않는다 — 그래프는 조용히 멈추고 절반짜리 상태를 돌려준다. 이 실습에서 그것을 먼저 자기 눈으로 봅니다.
멈춘 뒤 사람이 하는 일은 승인만이 아니다. 금액을 깎기도 하고 거절하기도 한다. update_state 로 고친 값은 그 열쇠의 리듀서를 거쳐 들어가므로, 덮어쓰는 열쇠와 이어 붙이는 열쇠가 다르게 동작한다.
멈추는 방법은 두 가지다. interrupt_before 는 노드에 들어가기 전에 멈추고, 노드 안의 interrupt() 는 부른 그 자리에서 멈춘다. 후자는 재개할 때 그 노드를 처음부터 다시 돈다 — 앞에 둔 부수 효과가 두 번 일어난다는 뜻이다. 이 실습에서 몇 번 들어오는지 직접 셉니다.
채점기는 여러분이 적어 둔 설명을 믿지 않는다. 여러분의 모듈을 실제로 불러 임의의 금액과 사유로 돌려 보고, 멈춘 자리와 이어 돌린 결과를 채점기가 따로 계산한 값과 대조한다.
단계
1. /root/work/aghitl/approve.py 에 AUTO_LIMIT = 100000·RISKY·needs_approval(amount, reason)·State·노드 셋(intake·auto_settle·settle)·triage·build_graph() 를 만드세요. 아직 멈추지 않습니다.
2. build_forgotten() 과 run_forgotten(amount, reason) 을 더하세요. 체크포인터 없이 interrupt_before=["settle"] 만 준 판을 돌려 무슨 일이 생기는지 적습니다.
3. SAVER = MemorySaver()·build_pausing()·thread(name)·start_case(amount, reason, name) 를 더해 진짜로 멈추게 하세요. start_case 는 {"next": ["settle"], "outcome": "", "paid": -1} 를 돌려줍니다.
4. resume_case(name) 을 더해 invoke(None, config) 로 이어서 돌리게 하세요.
5. resume_case(name, amount=..., decision=...) 로 사람이 금액을 고치거나 거절한 뒤 이어서 돌리게 하세요. 거절이면 한 푼도 나가지 않습니다.
6. confirm 노드와 build_asking()·ask_and_answer(amount, reason, answer, name) 을 더하세요. 노드 안에서 interrupt() 로 멈추고 Command(resume=답) 으로 이어서 돌립니다. 노드에 몇 번 들어왔는지 SIDE_EFFECTS 로 셉니다.
7. handle(...) 을 더해 한 건을 끝까지 처리하고 요청값과 집행값, 승인 여부를 한 줄로 남기게 하세요.
8. /root/work/aghitl/approve_report.json 과 /root/work/aghitl/approve_report.md 에 기록을 남기세요.
참고
- 실행 계약: 채점기는
/root/work/aghitl/approve.py를 파이썬 모듈로 불러 위에 적은 이름들을 직접 씁니다. 스크립트로 실행하지 않습니다. needs_approval(amount, reason)은 금액이AUTO_LIMIT을 넘거나 사유가RISKY에 있으면 참입니다.RISKY = ("파손", "오배송")입니다.- 상태 열쇠:
amount·reason·decision·paid·outcome·trace.trace는 이어 붙이는 리듀서를 씁니다. 노드 이름과 상태 열쇠는 겹치면 안 됩니다 — 겹치면 컴파일할 때ValueError: 'x' is already being used as a state key가 납니다. - 노드가 남기는 값:
auto_settle은outcome="auto",settle은 승인이면outcome="settled", 상태의decision이"reject"이면outcome="rejected"이고paid는 0 입니다. 승인이면paid는 그 시점의amount입니다. trace에 쓰는 이름은 노드 이름과 같게(intake·auto_settle·settle·confirm), 사람이 고친 흔적은human으로 씁니다.run_forgotten의 답:{"raised": 참거짓, "trace": [...], "outcome": 문자열, "resumable": 참거짓}. 직접 돌려 보고 본 대로 채우세요.start_case의 답:{"next": [...], "outcome": 문자열, "paid": 정수}.next는get_state(config).next를 리스트로 바꾼 것입니다.resume_case의 답:{"outcome": 문자열, "paid": 정수, "trace": [...]}.ask_and_answer의 답:{"next": [...], "asked": [...], "entered_before": 정수, "entered_after": 정수, "outcome": 문자열, "paid": 정수, "decision": 문자열}.asked는get_state(config).tasks의interrupts에 담긴 값들입니다. 답이"yes"면 승인, 아니면 거절입니다.handle의 답:{"approval": "not_required"|"approved"|"rejected", "outcome": 문자열, "paid": 정수, "requested": 정수, "changed_by_human": 참거짓}.- 같은
thread_id를 다시 쓰면 앞의 기록이 남아 있습니다. 채점기는 매번 다른 이름을 넘깁니다. update_state로 고친 값은 갈래를 다시 태웁니다. 그 값은 마지막으로 돈 노드가 쓴 것으로 기록되어 그 노드의 조건부 엣지가 다시 평가됩니다. 그래서 승인 대기 중인 건의 금액을AUTO_LIMIT밑으로 깎으면 그 건은 승인 경로를 벗어나auto_settle로 빠집니다. 채점기는 한도 위에서만 깎으므로 이 실습에서는 그 재배선을 다루지 않지만, 알고 있어야 합니다.- 이 파드에는 인터넷이 없습니다. langgraph 0.2.60 이 이미 들어 있습니다.
- 공식 문서: [Interrupts](https://docs.langchain.com/oss/python/langgraph/interrupts) · [Persistence](https://docs.langchain.com/oss/python/langgraph/persistence) · [Types 레퍼런스](https://reference.langchain.com/python/langgraph/types/)
- 흔한 실수: 체크포인터 없이 중단점만 주기,
thread_id를 안 주기, 이어서 돌릴 때 원래 입력을 다시 넣기(그러면 처음부터 다시 돕니다 —None을 주세요),interrupt()앞에 되돌릴 수 없는 일을 두기.
단계 8개
- 사람이 봐야 하는 일을 가른다
- 체크포인터를 빠뜨리면 조용히 사라진다
- 진짜로 멈춘다
- 남겨 둔 데서 이어서 돌린다
- 승인자가 금액을 고치거나 거절한다
- 노드 안에서 멈추면 그 노드는 두 번 돈다
- 요청한 값과 나간 값을 함께 남긴다
- 무엇을 보았는지 기록한다