LabHub
배우기 러닝패스 코스

AIエージェント — モデルではなくグラフ

高額返金が承認待ちにすら現れなかった

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

되돌릴 수 없는 일 앞에서 그래프를 멈추고, 사람이 값을 고치거나 거절한 뒤 이어서 돌린다. 멈추는 방법 두 가지를 모두 만들어 보고 둘의 차이를 직접 센 숫자로 확인한다.

왜 중요한가

LangGraph 에서 멈춤은 저장 위에 세워져 있다. 멈춘다는 것은 "다음 단계를 지금 하지 않고 남겨 둔 상태에서 나중에 이어서 한다" 는 뜻이라, 남겨 둘 곳(체크포인터)이 없으면 이어서 할 수도 없다. 그런데 체크포인터 없이 중단점만 주어도 오류가 나지 않는다 — 그래프는 조용히 멈추고 절반짜리 상태를 돌려준다. 이 실습에서 그것을 먼저 자기 눈으로 봅니다. 멈춘 뒤 사람이 하는 일은 승인만이 아니다. 금액을 깎기도 하고 거절하기도 한다. update_state 로 고친 값은 그 열쇠의 리듀서를 거쳐 들어가므로, 덮어쓰는 열쇠와 이어 붙이는 열쇠가 다르게 동작한다. 멈추는 방법은 두 가지다. interrupt_before 는 노드에 들어가기 전에 멈추고, 노드 안의 interrupt() 는 부른 그 자리에서 멈춘다. 후자는 재개할 때 그 노드를 처음부터 다시 돈다 — 앞에 둔 부수 효과가 두 번 일어난다는 뜻이다. 이 실습에서 몇 번 들어오는지 직접 셉니다. 채점기는 여러분이 적어 둔 설명을 믿지 않는다. 여러분의 모듈을 실제로 불러 임의의 금액과 사유로 돌려 보고, 멈춘 자리와 이어 돌린 결과를 채점기가 따로 계산한 값과 대조한다.

단계

  1. /root/work/aghitl/approve.pyAUTO_LIMIT = 100000·RISKY·needs_approval(amount, reason)·State·노드 셋(intake·auto_settle·settletriage·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.pyAUTO_LIMIT = 100000·RISKY·needs_approval·State·노드 셋·triage·build_graph() 를 만드세요. 소액이고 단순한 사유면 auto_settle, 아니면 settle 로 갑니다. 아직 멈추지 않습니다.

기준을 needs_approval 한 곳에 적어 두면 정책을 바꾸는 일이 그 함수를 고치는 일이 됩니다. triage 는 갈래 이름(auto·hold)을 돌려주고, 어느 노드로 가는지는 경로 지도가 정합니다. 노드 이름을 상태 열쇠와 다르게 지으세요.

체크포인터를 빠뜨리면 조용히 사라진다

build_forgotten()run_forgotten(amount, reason) 을 더하세요. 체크포인터 없이 interrupt_before=["settle"] 만 준 판을 돌리고, 예외가 났는지·어디까지 갔는지·이어서 돌릴 수 있는지를 {"raised": ..., "trace": [...], "outcome": ..., "resumable": ...} 로 적습니다.

직접 돌려 보고 본 대로 적으세요. 예상과 다를 것입니다 — 그 놀람이 이 단계의 전부입니다. 저장된 것이 없으니 invoke(None, ...) 로 이어서 돌릴 수도 없습니다. 이것이 왜 위험한지는 로그에 오류가 하나도 없다는 데 있습니다.

진짜로 멈춘다

SAVER = MemorySaver()·build_pausing()·thread(name)·start_case(amount, reason, name) 를 더하세요. 승인이 필요한 건은 settle 앞에서 멈추고, start_case{"next": ["settle"], "outcome": "", "paid": -1} 를 돌려줍니다.

세 가지가 함께 있어야 합니다 — 체크포인터, thread_id, 중단점. get_state(config).next 가 비어 있지 않다는 것이 곧 "아직 안 끝났다" 는 뜻입니다. 소액 건은 멈추지 않고 그대로 끝나야 하니, 중단점을 둔 노드를 지나지 않는 갈래가 있다는 점이 중요합니다.

남겨 둔 데서 이어서 돌린다

resume_case(name) 을 더해 invoke(None, config) 로 이어서 돌리게 하세요. 답은 {"outcome": "settled", "paid": 금액, "trace": [...]} 입니다.

입력 자리에 None 을 주는 것이 "새 입력은 없다. 저장된 데서 이어서 하라" 는 뜻입니다. 원래 입력을 다시 넣으면 처음부터 다시 도는데, 그러면 발자국이 두 배가 되어 티가 납니다. 같은 thread_id 를 써야 이어집니다.

승인자가 금액을 고치거나 거절한다

resume_case(name, amount=..., decision=...)update_state 로 상태를 먼저 고치고 이어서 돌리게 하세요. decision="reject" 이면 outcomerejected 이고 paid 는 0 입니다. 고친 흔적은 tracehuman 으로 남깁니다.

update_state 가 넣는 값도 그 열쇠의 리듀서를 거칩니다. amount 처럼 덮어쓰는 열쇠는 덮어쓰지만 trace 처럼 이어 붙이는 열쇠는 쌓입니다 — 그래서 흔적 한 줄을 남기기에 알맞습니다. 거절도 결과여야 합니다. 승인자가 "그냥 안 누르는" 것으로 거절하게 두면 그 건은 영원히 대기열에 남습니다.

노드 안에서 멈추면 그 노드는 두 번 돈다

confirm 노드와 build_asking()·ask_and_answer(amount, reason, answer, name) 을 더하세요. interrupt() 로 멈추고 Command(resume=답) 으로 이어서 돌리며, SIDE_EFFECTS 로 노드에 몇 번 들어왔는지 셉니다.

interrupt() 가 돌려주는 값이 곧 사람이 준 답입니다. 재개하면 그 노드가 처음부터 다시 도니, interrupt() 앞에 둔 일은 두 번 일어납니다 — 직접 세어 확인하세요. get_state(config).tasks 안의 interrupts 에 멈출 때 넘긴 값이 들어 있습니다.

요청한 값과 나간 값을 함께 남긴다

handle(amount, reason, name, amount_override=None, decision=None) 을 더해 한 건을 끝까지 처리하고 {"approval": ..., "outcome": ..., "paid": ..., "requested": ..., "changed_by_human": ...} 를 돌려주게 하세요.

승인 기록의 핵심은 사람 이름이 아니라 바뀐 값입니다. 요청된 금액과 실제로 나간 금액이 다르면 그 사실이 기록에 있어야 나중에 설명할 수 있습니다. 승인이 필요 없는 건은 not_required 로 적고 멈추지 않습니다.

무엇을 보았는지 기록한다

/root/work/aghitl/approve_report.jsonauto_limit·forgotten·small·large·trimmed·rejected·confirm_entered 를, /root/work/aghitl/approve_report.md## 어디서 멈추기로 했나 ## 체크포인터를 빠뜨리면 ## 사람이 고친 값은 어디로 들어가나 ## 노드 밖에서 멈추기와 노드 안에서 멈추기 네 절로 쓰세요.

forgottenrun_forgotten 의 답 그대로, small·large·trimmed·rejectedhandle 의 답에서 approval·outcome·paid 세 열쇠만 담습니다. confirm_entered{"before": 정수, "after": 정수}ask_and_answer 가 센 값입니다. 지어내지 말고 돌려서 얻으세요.