保存してあるから、その場所に戻れる
한국어 원문으로 표시합니다.
목표
체크포인터를 붙인 그래프에서 무엇이 어디에 남는지를 직접 세어 본다. 같은 thread_id 가 이어지는 것을 확인하고, 체크포인트를 목록으로 읽고, 과거 좌표로 되감고, 값을 고쳐 갈래를 치고, 상태를 직렬화해 바이트를 센다. 마지막으로 장부를 새로 만들면 무엇이 사라지는지 재현한다.
왜 중요한가
체크포인터는 단계마다 그 시점의 상태를 통째로 남긴다. 이 한 문장이 좋은 일과 나쁜 일을 동시에 설명한다.
좋은 쪽은 이어서 돌릴 수 있다는 것이다. 여덟 번째 단계에서 실패해도 처음부터 다시 할 필요가 없고, "세 번째에서 다른 자료를 썼으면 어땠을까" 라는 질문에 같은 조건으로 답할 수 있다. 과거 스냅샷의 config 로 invoke(None, ...) 하면 그 자리에서 다시 돌고, update_state 로 값을 고쳐 넣으면 거기서 갈래가 생긴다. 이때 원래 갈래는 지워지지 않지만 스레드의 "지금" 은 새 갈래로 옮겨 가므로, 원래 결과를 다시 보려면 그 시점의 config 를 손에 들고 있어야 한다.
나쁜 쪽은 저장되는 것이 상태 전체라는 것이다. 상태에 큰 값을 하나 담으면 그 값이 체크포인트 수만큼 되풀이 저장된다. 이 실습에서는 그것을 시간이 아니라 바이트로 잰다 — 시간은 기계와 부하에 따라 달라지지만 크기는 같은 상태면 늘 같기 때문이다.
채점기는 여러분이 적어 둔 설명을 믿지 않습니다. 여러분의 모듈을 실제로 불러 그래프를 돌리고, 체크포인트를 스스로 세어 여러분이 돌려준 값과 대조합니다. 주제와 담는 값의 크기는 실행마다 바뀝니다.
단계
- /root/work/agckpt/ckpt.py 에
ANGLES·State·노드 셋(plan·write·review)·build_graph(checkpointer=None)·new_saver()·cfg(thread_id)를 만드세요. 같은thread_id는 이어지고 다른 것은 따로 남아야 합니다. history(app, config)·history_len(app, config)·pending(app, config)를 더해 체크포인트를 오래된 것부터 읽고 세게 하세요.snapshot_before(app, config, node)를 더해 그 노드를 아직 돌지 않은 가장 최근 체크포인트를 찾게 하세요.replay(app, config, node)를 더해 그 좌표에서invoke(None, snapshot.config)로 다시 돌게 하세요.fork(app, config, node, values)를 더해update_state로 갈래를 치고, 원래 갈래의 끝도 함께 돌려주게 하세요.state_bytes(values)·history_bytes(app, config)·weigh(topic, payload)와 상태 열쇠bulk를 더해 체크포인트의 크기를 바이트로 재게 하세요.lost_demo(topic)을 더해 장부를 새로 만들면 같은thread_id로도 아무것도 남지 않는 것을 재현하세요.- /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 · Use time-travel · Graph API overview
- 흔한 실수:
compile()에 체크포인터를 안 주고get_state를 부르기(ValueError: No checkpointer set), 되감을 때 입력에None대신 상태를 주기(새로 시작합니다),update_state가 돌려준 config 를 버리고 원래 config 로 이어 돌리기,get_state_history의 순서를 뒤집지 않기.
같은 스레드는 이어지고 다른 스레드는 따로 남는다
/root/work/agckpt/ckpt.py 에 ANGLES·State·노드 셋(plan·write·review)·build_graph(checkpointer=None)·new_saver()·cfg(thread_id) 를 만드세요. 체크포인터를 준 그래프는 같은 thread_id 로 다시 돌리면 앞의 notes 위에 쌓이고, 다른 thread_id 는 빈 데서 시작해야 합니다.
graph.compile(checkpointer=checkpointer) 한 줄이 전부입니다. cfg(thread_id) 는 {"configurable": {"thread_id": thread_id}} 를 돌려주는 작은 함수로 두면 뒤 단계가 편합니다. notes 에 operator.add 를 붙여야 이어지는 것이 눈에 보입니다 — 붙이지 않으면 저장은 되는데 값이 덮어써져서 이어진 줄 모릅니다.
체크포인트는 단계마다 남는다
history(app, config)·history_len(app, config)·pending(app, config) 를 더하세요. history() 는 체크포인트를 오래된 것부터 늘어놓고, pending() 은 스냅샷마다 다음에 돌 노드 이름을(다 끝난 자리는 빈 문자열) 담은 목록을 돌려줍니다.
app.get_state_history(config) 는 제너레이터이고 최신 것부터 나옵니다. list() 로 받아 reversed() 하세요. 스냅샷의 next 는 튜플입니다 — 비어 있으면 더 돌 것이 없다는 뜻입니다. 세어 보면 노드 수보다 둘 많은데, 입력을 받은 자리와 다 끝난 자리가 하나씩 더 있기 때문입니다.
되감을 좌표를 찾는다
snapshot_before(app, config, node) 를 더하세요. 그 노드를 아직 돌지 않은 가장 최근 체크포인트를 돌려주고, 그런 자리가 없으면 None 을 돌려줍니다.
찾는 조건은 스냅샷의 next 가 (node,) 인 것입니다. 오래된 것부터 늘어놓은 목록을 뒤에서부터 훑으면 '가장 최근' 을 먼저 만납니다. 돌려주는 것은 스냅샷 자체입니다 — 뒤 단계가 쓰는 것은 그 안의 config 이고, 거기에는 thread_id 말고 checkpoint_id 가 더 들어 있습니다.
그 자리에서 다시 돈다
replay(app, config, node) 를 더하세요. snapshot_before 로 좌표를 찾아 app.invoke(None, snapshot.config) 로 이어 돌리고, {"from": node, "result": 결과, "added": 늘어난 체크포인트 수} 를 돌려줍니다. 좌표가 없으면 ValueError 를 올리세요.
입력을 None 으로 주는 것이 핵심입니다. 상태를 주면 새로 시작하는 것이고, None 은 '저장된 그 시점부터 이어서 돌라' 는 뜻이 됩니다. added 는 되감기 전후로 history_len 을 재어 뺀 값입니다 — 다시 돈 노드 수만큼 늘어납니다.
값을 고쳐 다른 갈래로 간다
fork(app, config, node, values) 를 더하세요. 분기 전 스레드의 끝을 먼저 잡아 두고, app.update_state(snapshot.config, values) 로 갈래를 만든 뒤 그 config 로 이어 돌립니다. {"forked_config": ..., "result": ..., "original": 분기 전 끝의 값 딕셔너리, "total": 지금 체크포인트 수} 를 돌려주세요.
update_state 는 새 checkpoint_id 가 든 config 를 돌려줍니다. 그것을 버리고 원래 config 로 이어 돌리면 분기가 되지 않습니다. 그리고 분기한 뒤에는 thread_id 만으로 물어본 '지금' 이 새 갈래의 끝을 가리키므로, 원래 결과는 분기 전에 잡아 둔 스냅샷의 config 로 따로 읽어야 합니다.
크기로 잰다
상태 열쇠 bulk 와 state_bytes(values)·history_bytes(app, config)·weigh(topic, payload) 를 더하세요. state_bytes 는 json.dumps(values, ensure_ascii=False, sort_keys=True, default=str) 한 문자열의 UTF-8 길이입니다. weigh 는 같은 그래프를 bulk 없이 한 번, bulk 에 payload 를 담고 한 번 돌려 {"checkpoints": ..., "thin_total": ..., "fat_total": ..., "payload_bytes": ..., "carrying": ...} 를 돌려줍니다.
carrying 은 두 목록을 같은 자리끼리 견주어 크기 차이가 payload_bytes 이상인 체크포인트의 개수입니다 — 그 큰 값을 실제로 들고 있는 자리의 수라는 뜻입니다. 두 번 돌릴 때는 장부를 따로 쓰세요(new_saver() 를 두 번). 시간을 재지 마세요 — 기계와 부하에 따라 달라집니다. 바이트는 같은 상태면 늘 같습니다.
장부를 새로 만들면 사라진다
lost_demo(topic) 을 더하세요. 새 장부로 그래프를 만들어 run-1 스레드를 한 번 돌리고 체크포인트 수를 잰 뒤, 또 다른 새 장부로 그래프를 하나 더 만들어 같은 thread_id 로 물어봅니다. {"kept": ..., "after_restart": ..., "values_after_restart": ...} 를 돌려주세요.
MemorySaver 는 프로세스 안 메모리에 담습니다. 새 MemorySaver 를 만드는 것이 프로세스가 다시 뜬 것을 흉내 내는 가장 정직한 방법입니다. 새 장부에 물어보면 오류가 나지 않고 빈 값이 돌아옵니다 — 그래서 이 문제는 조용합니다. 실습과 시험에는 알맞지만 운영에는 맞지 않는 이유가 이것입니다.
재어 본 값으로 기록한다
/root/work/agckpt/ckpt_report.json 에 topic·checkpoints_per_run·pending·base_score·fork_angle·fork_score·checkpoints_after_fork·payload_bytes·carrying·survives_restart 를, /root/work/agckpt/ckpt_report.md 에 ## 무엇이 저장되는가 ## 되감기와 분기는 어떻게 다른가 ## 크기로 잰 것 ## 사라지는 것 네 절로 쓰세요.
숫자는 손으로 적지 말고 여러분의 모듈을 실제로 돌려 얻은 값으로 채우세요. topic 은 여러분이 쓴 주제 문자열 그대로, fork_angle 은 기본 이 아닌 갈래 이름입니다 — 채점기가 그 둘로 점수를 다시 계산해 대조합니다. 분기는 write 앞에서 치세요. payload_bytes 는 300 이상이어야 차이가 눈에 보입니다. survives_restart 는 참·거짓입니다.