AI 에이전트 — 모델이 아니라 그래프 · 체크포인트·재개·시간여행 · 실습
저장해 두었으니 그 자리로 돌아간다
목표
체크포인터를 붙인 그래프에서 무엇이 어디에 남는지를 직접 세어 본다. 같은 thread_id 가 이어지는 것을 확인하고, 체크포인트를 목록으로 읽고, 과거 좌표로 되감고, 값을 고쳐 갈래를 치고, 상태를 직렬화해 바이트를 센다. 마지막으로 장부를 새로 만들면 무엇이 사라지는지 재현한다.
왜 중요한가
체크포인터는 단계마다 그 시점의 상태를 통째로 남긴다. 이 한 문장이 좋은 일과 나쁜 일을 동시에 설명한다.
좋은 쪽은 이어서 돌릴 수 있다는 것이다. 여덟 번째 단계에서 실패해도 처음부터 다시 할 필요가 없고, "세 번째에서 다른 자료를 썼으면 어땠을까" 라는 질문에 같은 조건으로 답할 수 있다. 과거 스냅샷의 config 로 invoke(None, ...) 하면 그 자리에서 다시 돌고, update_state 로 값을 고쳐 넣으면 거기서 갈래가 생긴다. 이때 원래 갈래는 지워지지 않지만 스레드의 "지금" 은 새 갈래로 옮겨 가므로, 원래 결과를 다시 보려면 그 시점의 config 를 손에 들고 있어야 한다.
나쁜 쪽은 저장되는 것이 상태 전체라는 것이다. 상태에 큰 값을 하나 담으면 그 값이 체크포인트 수만큼 되풀이 저장된다. 이 실습에서는 그것을 시간이 아니라 바이트로 잰다 — 시간은 기계와 부하에 따라 달라지지만 크기는 같은 상태면 늘 같기 때문이다.
채점기는 여러분이 적어 둔 설명을 믿지 않습니다. 여러분의 모듈을 실제로 불러 그래프를 돌리고, 체크포인트를 스스로 세어 여러분이 돌려준 값과 대조합니다. 주제와 담는 값의 크기는 실행마다 바뀝니다.
단계
1. /root/work/agckpt/ckpt.py 에 ANGLES·State·노드 셋(plan·write·review)·build_graph(checkpointer=None)·new_saver()·cfg(thread_id) 를 만드세요. 같은 thread_id 는 이어지고 다른 것은 따로 남아야 합니다.
2. history(app, config)·history_len(app, config)·pending(app, config) 를 더해 체크포인트를 오래된 것부터 읽고 세게 하세요.
3. snapshot_before(app, config, node) 를 더해 그 노드를 아직 돌지 않은 가장 최근 체크포인트를 찾게 하세요.
4. replay(app, config, node) 를 더해 그 좌표에서 invoke(None, snapshot.config) 로 다시 돌게 하세요.
5. fork(app, config, node, values) 를 더해 update_state 로 갈래를 치고, 원래 갈래의 끝도 함께 돌려주게 하세요.
6. state_bytes(values)·history_bytes(app, config)·weigh(topic, payload) 와 상태 열쇠 bulk 를 더해 체크포인트의 크기를 바이트로 재게 하세요.
7. lost_demo(topic) 을 더해 장부를 새로 만들면 같은 thread_id 로도 아무것도 남지 않는 것을 재현하세요.
8. /root/work/agckpt/ckpt_report.json 과 /root/work/agckpt/ckpt_report.md 에 재어 본 값을 기록하세요.
참고
- 실행 계약: 채점기는
/root/work/agckpt/ckpt.py를 파이썬 모듈로 불러ANGLES·State·build_graph·new_saver·cfg·history·history_len·pending·snapshot_before·replay·fork·state_bytes·history_bytes·weigh·lost_demo를 직접 씁니다. 스크립트로 실행하지 않으므로if __name__ == "__main__"은 없어도 됩니다. ANGLES = {"기본": 1, "요약": 2, "비교": 3}입니다. 갈래 이름마다 초안을 몇 번 되풀이할지를 담습니다.- 노드가 하는 일은 정확히 이렇습니다.
plan은{"stage": "plan", "angle": 지금 angle 또는 "기본", "notes": ["plan:<topic>"]}를,write는"<topic>/<angle> "를ANGLES[angle]번 이어 붙인 뒤 양끝 공백을 없앤 것을draft에 담고{"stage": "write", "notes": ["write:<angle>"]}를,review는{"stage": "review", "score": len(draft), "notes": ["review:<score>"]}를 돌려줍니다. notes에는operator.add리듀서를 붙입니다. 나머지 열쇠는 덮어쓰기입니다.build_graph(checkpointer=None)는graph.compile(checkpointer=checkpointer)로 끝냅니다. 체크포인터를 안 주면 아무것도 저장되지 않습니다.cfg(thread_id)는{"configurable": {"thread_id": thread_id}}를 돌려줍니다.get_state_history(config)는 최신 것부터 돌려줍니다.history()는 그것을 뒤집어 오래된 것부터 돌려주세요.pending()은 각 스냅샷의next첫 원소를, 비어 있으면 빈 문자열을 담은 목록입니다.replay()는{"from": node, "result": 결과, "added": 늘어난 체크포인트 수}를 돌려줍니다.fork()는{"forked_config": ..., "result": ..., "original": 분기 전 끝의 값 딕셔너리, "total": 지금 체크포인트 수}를 돌려줍니다.state_bytes(values)는json.dumps(values, ensure_ascii=False, sort_keys=True, default=str)한 문자열을 UTF-8 로 인코딩한 길이입니다. 채점기가 똑같이 계산해 대조하므로 이 세 인자를 그대로 쓰세요.weigh(topic, payload)는{"checkpoints": ..., "thin_total": ..., "fat_total": ..., "payload_bytes": ..., "carrying": ...}를 돌려줍니다.carrying은 같은 자리끼리 견주어 크기 차이가payload_bytes이상인 체크포인트의 개수입니다.lost_demo(topic)은{"kept": ..., "after_restart": ..., "values_after_restart": ...}를 돌려줍니다. 스레드 이름은run-1을 쓰세요.- 이 파드에는 인터넷이 없습니다.
pip install은 되지 않습니다. langgraph 0.2.60 이 이미 들어 있습니다(python3 -c "import langgraph"). - 공식 문서: [Persistence](https://docs.langchain.com/oss/python/langgraph/persistence) · [Use time-travel](https://docs.langchain.com/oss/python/langgraph/use-time-travel) · [Graph API overview](https://docs.langchain.com/oss/python/langgraph/graph-api)
- 흔한 실수:
compile()에 체크포인터를 안 주고get_state를 부르기(ValueError: No checkpointer set), 되감을 때 입력에None대신 상태를 주기(새로 시작합니다),update_state가 돌려준 config 를 버리고 원래 config 로 이어 돌리기,get_state_history의 순서를 뒤집지 않기.
단계 8개
- 같은 스레드는 이어지고 다른 스레드는 따로 남는다
- 체크포인트는 단계마다 남는다
- 되감을 좌표를 찾는다
- 그 자리에서 다시 돈다
- 값을 고쳐 다른 갈래로 간다
- 크기로 잰다
- 장부를 새로 만들면 사라진다
- 재어 본 값으로 기록한다