LabHub
배우기 러닝패스 코스

AI Agents — A Graph, Not a Model

Only One Footprint Is Left Behind

LabHub 에서 이어서 보기

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

목표

에이전트 상태를 열쇠 목록이 아니라 합치는 규칙으로 설계한다. 리듀서가 없을 때 무슨 일이 생기는지 직접 재현한 뒤, 이어 붙이기·중복 제거·최댓값·창 자르기 네 가지 리듀서를 만들고, 입력과 출력 스키마를 나눠 바깥에 나갈 열쇠를 좁힌다.

왜 중요한가

LangGraph 의 상태는 열쇠마다 채널이 하나씩 있는 구조다. 노드는 바꾸고 싶은 열쇠만 담은 딕셔너리를 돌려주고, 그래프가 채널별로 적용한다. 이때 리듀서를 안 적은 열쇠는 덮어쓴다 — 세 노드가 차례로 쓴 trace 가 마지막 하나만 남는 것이 그래서다. 오류가 나지 않아서 "로그가 안 남는다" 로 보이고, 로그 코드를 의심하다 시간을 버린다. 반대쪽 문제도 있다. 리듀서를 붙이면 그 열쇠는 끝없이 자라는데, 체크포인터는 상태를 통째로 저장한다. 노드가 서른 번 돌면 체크포인트도 서른 개고 하나하나가 그 시점의 상태 전체다. 그래서 자라는 열쇠에는 상한을 리듀서 안에 적어 둔다 — 노드마다 흩어 두면 한 곳만 빠뜨려도 조용히 새어 나간다. 마지막으로 상태에는 바깥에 보이면 안 되는 것이 섞인다. 출력 스키마로 나갈 열쇠를 화이트리스트로 정하면, 새 열쇠가 늘어도 저절로 막힌다. 채점기는 여러분이 적어 둔 설명을 믿지 않는다. 여러분의 모듈을 실제로 불러서 리듀서를 매번 다른 값으로 직접 두드려 보고, 그래프를 돌려 나온 상태를 채점기가 따로 계산한 값과 대조한다. 노드 이름과 숫자는 실행마다 바뀐다.

단계

  1. /root/work/agstate/state.pyCATALOG·State·노드 셋(intake·lookup·finishbuild_graph() 를 만드세요. 노드는 바꾸는 열쇠만 돌려주고, 손대지 않은 열쇠는 그대로 남아야 합니다.
  2. overwrite_demo(names) 를 더해 리듀서 없는 열쇠가 덮어써지는 것을 재현하게 하세요. 이름마다 노드를 하나씩 만들어 줄줄이 잇고, 노드마다 seen 에 자기 이름 하나를 씁니다.
  3. StatetraceAnnotated[list, operator.add] 를 붙여 세 노드의 발자국이 순서대로 쌓이게 하세요.
  4. 리듀서 merge_sources(old, new) 를 만들고 sources 열쇠에 붙이세요. 같은 출처는 한 번만 남고 순서는 처음 본 순서를 지킵니다.
  5. used_callsoperator.add 를, peak_ms 에 직접 만든 keep_max(old, new) 를 붙이세요.
  6. RECENT_KEEP = 3 과 리듀서 keep_recent(old, new) 를 만들어 recent 열쇠가 뒤에서 세 개만 남게 하세요. finish 는 한 번에 두 개를 씁니다.
  7. InputState·OutputState 를 만들고 build_public()StateGraph(State, input=InputState, output=OutputState) 로 컴파일한 그래프를 돌려주게 하세요. 결과에는 answer 만 나와야 합니다.
  8. /root/work/agstate/state_report.json/root/work/agstate/state_report.md 에 상태 설계를 기록하세요.

참고

노드는 바꾸는 열쇠만 돌려준다

/root/work/agstate/state.pyCATALOG·State·노드 셋(intake·lookup·finishbuild_graph() 를 만드세요. build_graph() 는 compile() 된 그래프를 돌려주고, 노드가 손대지 않은 열쇠는 그대로 남아야 합니다.

TypedDict 로 상태를 정의하고 StateGraph(State) 에 노드를 더한 뒤 START 에서 END 까지 잇습니다. 노드는 상태 전체가 아니라 바꾸고 싶은 열쇠만 담은 딕셔너리를 돌려줍니다 — 그래야 다른 노드가 넣어 둔 값이 지워지지 않습니다. total=False 를 붙이면 열쇠가 다 채워지지 않아도 됩니다.

리듀서가 없으면 마지막 하나만 남는다

overwrite_demo(names) 를 더하세요. names 의 이름마다 노드를 하나씩 만들어 줄줄이 잇고, 노드마다 리듀서 없는 열쇠 seen 에 자기 이름 하나를 씁니다. 돌려주는 값은 {"writes": [...], "state_after": [...]} 입니다.

노드를 반복문으로 만들 때 람다가 마지막 이름만 붙잡는 고전적인 함정을 조심하세요 — 기본 인자나 감싸는 함수로 이름을 묶어야 합니다. writes 에는 여러분이 만든 노드 이름을 순서대로, state_after 에는 그래프를 돌린 뒤 실제로 남은 seen 값을 담습니다. 이 단계는 일부러 잘못된 설계를 재현하는 것입니다.

발자국을 쌓는 리듀서

StatetraceAnnotated[list, operator.add] 로 바꿔 세 노드의 발자국이 ["intake", "lookup", "finish"] 순서로 쌓이게 하세요.

from typing import Annotatedimport operator 가 필요합니다. Annotated 의 두 번째 자리에 놓인 함수가 그 열쇠의 리듀서가 됩니다. 노드 쪽 코드는 그대로 두세요 — 바뀌는 것은 상태 정의 한 줄뿐입니다. 그게 이 설계의 요점입니다.

같은 출처를 두 번 적지 않는다

리듀서 merge_sources(old, new) 를 만들어 sources 열쇠에 붙이세요. 같은 값은 한 번만 남고 순서는 처음 본 순서를 지킵니다. 세 노드는 각각 ["질문"]·["질문", "재고목록"]·["재고목록"] 을 씁니다.

리듀서는 (옛값, 새값) 두 인자를 받아 새 값을 돌려주는 보통 함수입니다. set 으로 중복을 지우면 순서가 무너지니 순서를 지키면서 걸러야 합니다. 옛값이 아직 없을 수도 있으니 old or [] 처럼 빈 값을 견디게 쓰세요.

더할 것과 가장 큰 것만 남길 것

used_callsoperator.add 를, peak_ms 에 직접 만든 keep_max(old, new) 를 붙이세요. 노드마다 used_calls 는 1 을, peak_msintake 4 · lookup 17 · finish 9 를 씁니다.

숫자에도 리듀서를 붙일 수 있습니다. 세는 규칙을 노드마다 흩어 두지 말고 상태 정의 한 줄로 모으는 것이 요점입니다. 내장 max 를 리듀서로 그대로 주면 안 됩니다 — LangGraph 가 리듀서의 서명을 들여다보는데 내장 함수에는 서명이 없어 컴파일에서 죽습니다. 한 겹 감싸세요.

자라는 열쇠에 상한을 건다

RECENT_KEEP = 3 과 리듀서 keep_recent(old, new) 를 만들어 recent 열쇠가 뒤에서 세 개만 남게 하세요. intake["intake"], lookup["lookup"], finish["finish:작성", "finish:검토"] 를 씁니다.

네 개가 들어오고 세 개만 남아야 하니, 합친 뒤에 자르는 순서가 중요합니다. 상한을 노드에 넣지 말고 리듀서에 넣으세요 — 노드는 앞으로도 늘어날 텐데, 한 곳만 빠뜨려도 조용히 새어 나갑니다. 체크포인터가 상태를 통째로 저장한다는 점이 이 상한의 이유입니다.

나갈 것만 내보낸다

InputState(질문만)·OutputState(답만)를 만들고 build_public()StateGraph(State, input=InputState, output=OutputState) 로 컴파일한 그래프를 돌려주게 하세요. {"question": ...} 만 넣어 돌린 결과에는 answer 열쇠 하나만 있어야 합니다.

노드는 그대로 전체 상태를 보고, 좁아지는 것은 입구와 출구뿐입니다. 노드를 잇는 코드를 함수로 빼 두면 build_graph()build_public() 이 같은 배선을 나눠 쓸 수 있습니다. 나가는 것을 지우는 코드로 막으면 새 열쇠가 늘 때마다 뒤처집니다 — 화이트리스트가 저절로 막습니다.

왜 그렇게 정했는지 남긴다

/root/work/agstate/state_report.jsonreducers·trace·sources·used_calls·peak_ms·recent_len·public_keys 를, /root/work/agstate/state_report.md## 무엇을 상태에 두었나 ## 어떤 리듀서를 왜 붙였나 ## 자라지 않게 막은 곳 ## 바깥에 내보내지 않는 것 네 절로 쓰세요.

숫자는 손으로 적지 말고 여러분의 그래프를 실제로 돌려 얻은 값으로 채우세요. reducers 는 열쇠 이름을 열쇠로, 붙인 리듀서 이름을 값으로 두는 객체입니다(예: operator.add). public_keysbuild_public() 결과에 실제로 나온 열쇠 목록입니다. 채점기는 같은 것을 따로 계산해 대조합니다.