AI Agents — A Graph, Not a Model
It Is Working Fine but Looks Frozen
한국어 원문으로 표시합니다.
목표
같은 그래프를 다섯 가지 방식으로 흘려보내며 무엇이 언제 나오는지 직접 센다. 갱신만 모아 마지막 상태를 다시 세워 보고, 흘려보낸 마지막 상태가 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 의 답 그대로입니다. 숫자를 지어내지 말고 세어서 넣으세요.