ちゃんと動いているのに止まって見える
한국어 원문으로 표시합니다.
목표
같은 그래프를 다섯 가지 방식으로 흘려보내며 무엇이 언제 나오는지 직접 센다. 갱신만 모아 마지막 상태를 다시 세워 보고, 흘려보낸 마지막 상태가 invoke 의 답과 같은지 대조한다.
왜 중요한가
에이전트에서 흘려보낼 것은 토큰만이 아니다. 모델을 마지막에 한 번 부르는 그래프라면 토큰 스트리밍을 붙여도 앞의 침묵은 그대로다. 사용자에게 필요한 것은 "지금 어느 단계인지" 와 "무엇이 새로 정해졌는지" 이고, 그것은 모델이 아니라 그래프가 알고 있다.
모드마다 단위가 다르다. updates 는 노드 단위라 나란히 도는 노드 둘이 같은 수퍼스텝에 있어도 이벤트가 따로 나오고, values 는 수퍼스텝 단위라 그 둘의 결과가 합쳐진 뒤 한 번 나온다. debug 는 수퍼스텝 번호까지 알려 준다. 이 차이를 모르면 "왜 두 번 나오지" 를 버그로 오해한다.
갱신만 모아 상태를 다시 세우려면 리듀서를 알아야 한다. 그래프 안에서는 리듀서가 해 주던 일을 바깥에서는 직접 해야 하고, 이어 붙이는 열쇠를 덮어쓰면 발자국이 한 칸만 남는다.
그리고 상태에 남기고 싶지 않은 진행 표시는 custom 으로 내보낸다. 상태에 넣으면 체크포인트에 실리고 재개할 때 옛 표시가 되살아난다.
채점기는 여러분이 적어 둔 설명을 믿지 않는다. 여러분의 모듈을 실제로 불러 매번 다른 회의록으로 돌려 보고, 이벤트의 개수·순서·모양을 채점기가 따로 센 값과 대조한다. 시간은 재지 않는다.
단계
- /root/work/agstream/stream.py 에
State·노드 넷(split·keypoints·actions·compose)·build_graph()·start(text)·run_values(text)를 만드세요.split다음에keypoints와actions가 나란히 돌고, 둘 다compose로 모입니다. run_updates(text)를 더해stream_mode="updates"의 이벤트를 그대로 목록으로 돌려주게 하세요.REDUCERS·rebuild(text)·rebuild_report(text)를 더해 갱신만 모아 마지막 상태를 다시 세우고, 그것이values의 마지막과 같은지 대조하게 하세요.run_debug(text)를 더해{수퍼스텝번호: [노드이름...정렬됨]}을 돌려주게 하세요.run_multi(text)와mode_sequence(text)를 더해stream_mode=["updates", "values"]의 결과를 다루게 하세요.compose가writer: StreamWriter를 받아 조각 두 개를 내보내게 하고,run_custom(text)를 더하세요.compare_invoke(text)를 더해 흘려보낸 마지막 상태와invoke의 답이 같은지, 가장 먼저 도착하는 노드가 무엇인지, 이벤트가 몇 개인지를 한 번에 돌려주게 하세요.- /root/work/agstream/minutes.txt 에 잴 회의록을 저장하고, 그것으로 재어 /root/work/agstream/stream_report.json 과 /root/work/agstream/stream_report.md 에 기록하세요.
참고
- 실행 계약: 채점기는
/root/work/agstream/stream.py를 파이썬 모듈로 불러 위에 적은 이름들을 직접 씁니다. 스크립트로 실행하지 않습니다. - 상태 열쇠:
text·lines·points·todos·summary·trace.trace만 이어 붙이는 리듀서를 쓰고 나머지는 덮어씁니다. 노드 이름과 상태 열쇠는 겹치면 안 됩니다 — 겹치면 컴파일할 때ValueError: 'x' is already being used as a state key가 납니다. - 노드가 하는 일:
split은text를 줄로 나눠 공백 줄을 버리고lines에 담습니다.keypoints는"결정"이 든 줄을points에,actions는"하기로"가 든 줄을todos에 담습니다.compose는summary에"결정 N건 · 할 일 M건"을 담습니다. 네 노드 모두trace에 자기 이름을 씁니다. start(text)는{"text": text, "trace": []}를 돌려줍니다.run_values·run_updates·run_multi·run_custom은stream(...)이 준 것을 가공하지 말고 그대로 목록으로 돌려줍니다.rebuild는start(text)에서 시작해updates이벤트를 차례로 적용한 딕셔너리를 돌려줍니다.REDUCERS는{"trace": "append"}처럼 이어 붙이는 열쇠를 적어 둔 표입니다.rebuild_report(text)의 답:{"same": 참거짓, "stream_order": [...], "merged_order": [...]}.stream_order는 재구성한 상태의trace,merged_order는values마지막 이벤트의trace입니다. 둘이 같지 않습니다 — 왜 다른지는 직접 보고 적으세요.run_debug는event["type"] == "task"인 것만 보고event["step"]과event["payload"]["name"]을 모읍니다. 값은 정렬합니다.mode_sequence(text)는run_multi의 튜플에서 모드 이름만 순서대로 뽑습니다.compare_invoke(text)의 답:{"same": 참거짓, "first_node": 문자열, "values_events": 정수, "updates_events": 정수}.- 8단계의 /root/work/agstream/minutes.txt 는 보고서를 만든 회의록입니다. 채점기가 그 파일을 읽어 같은 입력으로 다시 재므로, 보고서를 만든 뒤에 내용을 바꾸면 숫자가 어긋납니다.
- 이 파드에는 인터넷이 없습니다. langgraph 0.2.60 이 이미 들어 있습니다.
- 공식 문서: Streaming · Graph API overview · Types 레퍼런스
- 흔한 실수:
updates가 수퍼스텝 단위라고 믿기, 재구성할 때 이어 붙이는 열쇠를 덮어쓰기, 진행 표시를 상태에 넣기,stream()의 결과를 가공해 돌려주기(채점기는 원래 모양을 봅니다).
상태 전체가 단계마다 나온다
/root/work/agstream/stream.py 에 State·노드 넷·build_graph()·start(text)·run_values(text) 를 만드세요. split 다음에 keypoints 와 actions 가 나란히 돌고 둘 다 compose 로 모입니다.
stream(입력, stream_mode="values") 가 주는 것을 그대로 목록으로 돌려주면 됩니다. 맨 앞에 입력 상태가 한 번 나온다는 점을 눈으로 확인하세요. 나란히 도는 두 노드의 결과는 합쳐진 뒤 한 이벤트로 나옵니다.
갱신은 노드마다 하나씩
run_updates(text) 를 더해 stream_mode="updates" 의 이벤트를 가공하지 말고 그대로 목록으로 돌려주게 하세요.
이벤트 하나가 {노드이름: 갱신} 입니다. 나란히 도는 노드 둘이 같은 수퍼스텝에 있어도 이벤트는 따로 나옵니다 — 그래서 values 보다 이벤트 수가 많습니다. 몇 개가 나오는지 직접 세어 보세요.
갱신만으로 상태를 다시 세운다
REDUCERS = {"trace": "append"} 와 rebuild(text) 를 더하세요. start(text) 에서 시작해 updates 이벤트를 차례로 적용한 딕셔너리를 돌려줍니다.
그래프 안에서는 리듀서가 해 주던 일을 바깥에서는 직접 해야 합니다. 전부 덮어쓰면 이어 붙이는 열쇠에서 마지막 한 칸만 남습니다. 그런데 리듀서를 제대로 흉내 내도 완전히 같아지지는 않습니다 — 두 trace 를 나란히 찍어 보고 어디가 다른지, 왜 그런지 직접 확인하세요. 둘 다 매번 같은 답을 주지만 서로 다릅니다.
몇 번째 단계에서 무엇이 함께 도는가
run_debug(text) 를 더해 {수퍼스텝번호: [노드이름...정렬됨]} 을 돌려주게 하세요. event["type"] == "task" 인 것만 봅니다.
debug 모드는 노드마다 task 와 task_result 두 이벤트를 냅니다. task 쪽의 step 이 수퍼스텝 번호이고 payload["name"] 이 노드 이름입니다. 나란히 도는 노드들이 같은 번호를 갖는지 확인하세요 — 그것이 이 모드를 쓰는 이유입니다.
여러 모드를 함께 듣는다
run_multi(text) 와 mode_sequence(text) 를 더하세요. stream_mode=["updates", "values"] 를 주면 (모드이름, 값) 튜플이 나옵니다.
목록으로 모드를 주면 이벤트마다 어느 모드의 것인지가 앞에 붙습니다. 두 모드의 이벤트가 어떤 순서로 섞여 나오는지 직접 보세요 — 한 모드를 다 준 뒤 다른 모드를 주는 것이 아닙니다. mode_sequence 는 그 순서를 이름만 뽑아 보여 줍니다.
상태에 남기지 않고 보여 준다
compose 가 writer: StreamWriter 를 두 번째 인자로 받아 {"stage": "compose", "points": 개수} 와 {"stage": "compose", "todos": 개수} 를 차례로 내보내게 하고, run_custom(text) 를 더하세요.
노드 함수의 두 번째 인자 이름을 writer 로 두고 StreamWriter 로 주석하면 LangGraph 가 채워 줍니다. writer(...) 로 넣은 것은 상태에 남지 않고 듣는 쪽으로만 갑니다 — 진행 표시를 상태에 넣으면 체크포인트에 실리고 재개할 때 되살아납니다.
흘려보낸 마지막과 한 번에 받은 답
compare_invoke(text) 를 더해 {"same": 참거짓, "first_node": 문자열, "values_events": 정수, "updates_events": 정수} 를 돌려주게 하세요.
values 의 마지막 이벤트가 invoke() 의 답과 같다면, 화면은 스트리밍으로 그리고 기록은 따로 invoke 로 남기는 식으로 두 번 돌릴 이유가 없습니다. first_node 는 updates 의 첫 이벤트에 담긴 노드 이름입니다 — 사용자에게 가장 먼저 보여 줄 수 있는 것이 무엇인지 알려 줍니다.
무엇을 언제 보여 줄지 정리한다
/root/work/agstream/minutes.txt 에 잴 회의록을 저장하세요(결정 사항과 할 일이 각각 한 줄 이상, 빈 줄도 하나). 그 회의록으로 재어 /root/work/agstream/stream_report.json 에 values_events·updates_events·first_node·supersteps·mode_sequence·custom_chunks·rebuild_same·stream_order·merged_order 를, /root/work/agstream/stream_report.md 에 ## 다섯 가지 방식이 각각 무엇을 주나 ## 갱신만으로 상태를 다시 세우려면 ## 화면에 먼저 보여 줄 수 있는 것 ## 현장에서 무엇을 고르나 네 절로 쓰세요.
전부 아래 회의록으로 돌려 얻은 값입니다. supersteps 는 run_debug 의 답을 문자열 열쇠로 바꿔 담고, rebuild_same·stream_order·merged_order 는 rebuild_report 의 답 그대로입니다. 숫자를 지어내지 말고 세어서 넣으세요.