AI 에이전트수정 2026-08-08

LangGraph 실행 모델 Q&A — State·도구 루프·체크포인터·승인 게이트 26문답

리듀서를 왜 채널마다 정해야 하는지, tool_calls가 찍힌 시점에 무엇이 아직 일어나지 않았는지, 기억이 어디에 붙어 있는지를 결론부터 정리한다.

그래프를 짜기 시작하면 질문의 성격이 바뀐다. "무엇을 쓸 것인가"에서 "왜 값이 사라지나", "왜 도구를 안 부르나", "왜 재시작하면 대화가 없어지나"로 옮겨 간다. 이 글은 그 질문들을 다룬다.

세 가지가 반복해서 사고를 만든다. 리듀서를 안 붙여 이력이 매 턴 초기화되는 것, tool_calls를 실행 결과로 착각하는 것, thread_id를 공유해 다른 사용자의 대화가 섞이는 것이다. 셋 다 문법 오류가 아니라 정상 동작이라 빌드도 테스트도 잡아 주지 않는다.

프레임워크 선택과 RAG 기본은 기본기 Q&A, 멀티에이전트와 제품 구현은 멀티에이전트 Q&A, 신뢰성·비용·안전은 운영 Q&A에 있다.

리듀서 카탈로그

채널마다 병합 규칙이 다르다. 어떤 리듀서를 고르느냐가 곧 팬아웃 설계다.

리듀서동작쓰는 곳주의
없음(기본)덮어쓰기(last write wins)현재 단계·플래그·최종 답변병렬 노드가 같은 키를 쓰면 충돌
operator.add리스트 연결 / 숫자 합산로그 누적, 분석 결과 팬인중복 제거 없음, 무한 누적 위험
add_messages누적 + id 기반 upsert + 타입 정규화대화 이력LangChain 메시지 객체 전용
커스텀 merge_dicts{**old, **new}키가 서로 다른 병렬 결과키 충돌 시 덮어씀

Q. 에이전트 상태 설계에서 가장 중요한 결정은 무엇인가

필드 목록을 정하는 일이 아니라 필드별 병합 정책을 정하는 일이다.

기본 동작이 덮어쓰기라 대화 이력에 리듀서를 안 붙이면 매 턴 1개로 초기화된다. 반대로 현재 단계나 최종 답변처럼 최신값만 의미 있는 필드는 덮어쓰기가 맞다. 병렬 노드를 쓰는 순간 이 결정이 곧 정합성을 좌우한다.

자세한 것은 State와 Reducer가 정하는 것에 있다.

Q. 왜 Annotated를 쓰나

채널별 리듀서를 매다는 자리이기 때문이다.

타입 힌트를 예쁘게 쓰려는 것이 아니라, 그 자리가 아니면 "이 채널은 누적한다"를 선언할 곳이 없다. Annotated[list, add_messages]를 빼면 이력이 매 턴 1개로 초기화된다.

Q. add_messages가 단순 연결과 다른 점은

누적에 더해 id 기반 upsert와 타입 정규화를 한다.

동작단순 연결add_messages
새 메시지뒤에 붙임뒤에 붙임
같은 id 재전송중복 누적갱신
dict/문자열 입력그대로메시지 객체로 정규화

id 갱신이 되기 때문에 스트리밍 중 부분 갱신과 메시지 수정이 성립한다.

Q. 병렬 노드가 같은 키에 쓰면 어떻게 되나

리듀서가 없으면 쓰기 충돌 에러다.

dict면 병합 함수를, list면 operator.add를 선언한다. 리듀서의 본질이 병렬 쓰기 충돌 해소라, 단일 쓰기 채널에는 애초에 필요 없다.

Q. dict 병합과 list 누적 중 무엇을 쓰나

키가 고정이면 dict, 분기 수가 가변이면 list다.

상황선택이유
서브테마명처럼 키가 도메인상 정해짐dict + 병합 함수결과를 이름으로 찾을 수 있다
경쟁사·검색결과처럼 개수가 입력에 따라 변함list + operator.add인덱스가 아니라 원소 안의 type 메타데이터로 구분

Q. 노드는 상태 전체를 반환해야 하나

아니다. 바꾼 칸만 반환한다.

반환하지 않은 키는 그대로 유지된다. 그래서 여러 노드가 서로 다른 채널을 동시에 갱신해도 충돌 없이 병합된다. 상태 전체를 복사해 수정하고 돌려주는 습관이 남아 있으면 이 이점이 사라진다.

Q. 리듀서 없이도 카운터가 늘던데

리듀서 덕이 아니다. 노드가 직전 값을 읽어 +1 했을 뿐이다.

누적 규칙이 아니라 읽고-쓰기의 결과다. 리스트 채널에서 같은 방식을 쓰면 덮어쓰기가 그대로 드러난다. 이 차이를 모르면 병렬 노드를 붙이는 순간 값이 사라진다.

Q. tool_calls가 생기면 도구가 실행된 것인가

아니다. 그 시점에는 요청만 만들어졌다.

실행은 별도 노드가 하고 tool_call_id가 요청과 결과를 잇는다. 이 분리가 중요한 이유는 결정과 실행 사이에 사람을 세울 자리가 생기기 때문이다. 한 몸이면 승인 게이트를 놓을 곳이 없다.

Q. bind_toolsToolNode를 왜 둘 다 쓰나

전자는 LLM에게 무엇을 부를지 결정시키고, 후자가 실제로 실행한다.

단계담당산출
결정bind_tools로 묶인 LLMtool_calls (구조화된 호출 요청)
실행ToolNodeToolMessage (결과)
연결tool_call_id

같은 도구 리스트가 서로 다른 두 곳에 들어간다. 상세는 bind_tools·ToolNode·ReAct 루프에 있다.

Q. 순환 그래프는 어떻게 만드나

배선 두 줄이면 ReAct 루프가 완성된다.

조건부 엣지로 chatbot → tools, 고정 엣지로 tools → chatbot. 되돌림 엣지 하나가 "도구를 몇 번 부를지 런타임에 정해진다"는 성질을 만든다.

Q. 도구를 만들었는데 LLM이 안 부른다

프롬프트가 아니라 docstring과 파라미터 이름을 고친다.

LLM이 보는 것은 함수명·시그니처·docstring 세 가지뿐이다. docstring은 주석이 아니라 라우팅 신호다. 도구 선택 정확도 문제의 상당수가 여기서 해결된다.

지점왜 중요한가
docstringLLM이 보는 설명 전부. 스펙처럼 쓴다
타입 힌트function calling 스키마를 만든다. 없으면 인자 형식을 추측한다
호출 예시docstring에 실제 호출·응답 형태를 넣으면 인자 오류가 크게 준다
반환값 압축원본 대신 필요한 지표만 정제해 반환. 툴 설계는 곧 컨텍스트 예산 관리

설계 원칙 전체는 에이전트 Tool 설계에 있다.

Q. 도구가 늘수록 좋은가

아니다. 도구 스키마가 매 호출 컨텍스트에 실린다.

개수가 늘면 토큰이 늘고 선택 정확도는 떨어진다. 도구를 추가하기 전에 기존 도구의 설명을 고쳐 해결되는지를 먼저 본다.

Q. 도구를 두 번 이상 이어 부르려면

tools → chatbot 되돌림 엣지가 그 일을 한다.

LLM이 결과를 보고 다음 도구를 정한다. 몇 바퀴 돌지는 사전에 정해지지 않는다 — 이것이 DAG로 표현되지 않는 이유이자 종료 조건을 따로 설계해야 하는 이유다.

Q. 라우팅을 LLM에 맡길지 UI에 맡길지 어떻게 정하나

사용자가 이미 정답을 아는 결정이면 UI 선택값이 낫다.

LLM 라우팅UI 선택값
결정론없음있음
비용호출 1회0
재현성낮음높음
대가소스 혼합 불가

제품 화면의 선택지를 그래프 분기 키로 옮긴 사례는 Focus 라우팅과 인용 정합성에 있다.

Q. 에이전트의 기억은 어디에 저장되나

모델이나 그래프가 아니라 체크포인터에 저장되고, 조회 키는 thread_id다.

그래서 매 호출에 이력 전체를 다시 넣지 않는다. 새 발화 하나만 넣으면 체크포인터가 이전 상태를 복원하고 add_messages가 이어붙인다.

멀티유저에서 thread_id를 사용자·세션 단위로 발급하면 그것만으로 대화 격리가 완성된다. 반대로 공유하거나 하드코딩하면 다른 사용자의 대화가 섞여 나오는 사고로 직결된다.

Q. MemorySaver를 프로덕션에 써도 되나

안 된다. 프로세스 메모리라 재시작 시 소실되고 다중 인스턴스 공유가 불가능하다.

구현저장소용도
MemorySaver프로세스 메모리학습·노트북
SQLite 세이버로컬 파일단일 노드 데모·개인 도구
PostgreSQL 세이버RDB다중 인스턴스 서비스 운영

교체 비용은 작다 — compile 인자 한 줄이다. 그래서 미루다가 그대로 배포되는 일이 잦다.

Q. 메시지를 잘라내면 기억이 지워지나

LLM 입력만 잘린다. 체크포인터에는 전량 남는다.

컨텍스트 관리와 데이터 보존은 별개 층이다. 자르면 비용만 줄고 데이터는 그대로다 — 반대로 말하면, 잘랐다고 개인정보가 지워진 것도 아니다.

전략방식트레이드오프
윈도우 슬라이싱최근 N개만 전달구현 1줄, 오래된 사실 유실
요약 압축오래된 구간을 요약 1개로 치환맥락 보존, 요약 호출 비용·왜곡
선별 검색이력을 벡터 검색해 관련 대목만 주입장기 기억에 강함, 인프라 필요
결과 압축워커 산출물 중 마지막 메시지 1개만 상위로컨텍스트 오염 차단, 중간 발견 소실

Q. 체크포인트에 무엇이 남나

상태 값, 다음 실행 노드, thread_id와 타임스탬프, 그리고 어느 노드가 무엇을 썼는지(writes)가 남는다.

마지막 항목이 사후 추적의 근거다. "왜 이 답이 나왔나"를 되짚을 때 필요한 것은 최종 상태가 아니라 각 스텝의 기록이다.

Q. Human-in-the-Loop은 어떻게 구현하나

compile(interrupt_before=["tools"]) 한 줄이면 지정한 노드 직전에 그래프가 멈춘다.

전제 조건은 체크포인터다. 멈춘 지점을 저장할 곳이 있어야 재개가 되기 때문이다.

멈춘 뒤에는 get_state로 다음 노드와 호출 인자를 검토하고, 그대로 재개하거나 update_state로 고쳐서 재개하거나 중단한다. 상세는 체크포인터·HITL 편에 있다.

Q. 체크포인터 없이 인터럽트만 걸면 어떻게 되나

성립하지 않는다. 멈춘 지점을 저장할 곳이 있어야 재개가 된다.

두 옵션이 별개로 보이지만 하나가 다른 하나의 전제다. 개발 중에도 MemorySaver를 지정해 두는 편이 낫다.

Q. Human-in-the-Loop의 조직적 의미는 무엇인가

정책 문서가 아니라 코드로 강제되는 게이트라는 점이다.

인터럽트 지점의 스냅샷이 그대로 승인 로그가 된다. "위험한 작업은 확인받는다"가 문서에만 있으면 지켜지는지 확인할 방법이 없지만, 그래프에 있으면 통과 기록이 남는다.

Q. 자동화 범위를 어떻게 넓히나

처음엔 전 도구를 승인 대상으로 두고, 신뢰가 쌓인 도구부터 게이트에서 뺀다.

점진 자동화가 통제를 잃지 않는 유일한 방법이다. 반대 순서 — 전부 자동으로 열어 두고 사고가 나면 닫는 방식 — 은 사고가 나야만 경계를 알게 된다.

Q. 사람이 고친 내용은 버리나

아니다. update_state 이력이 곧 모델 개선용 라벨 데이터다.

무엇을 어떻게 고쳤는지가 쌓이면 프롬프트를 어디서 손봐야 하는지가 드러난다. 승인 UI를 만들 때 수정 전후를 함께 저장하지 않으면 이 자산이 사라진다.

Q. 진행 상황은 어떻게 보여주나

노드 경계 스트리밍은 노드가 끝나야 값이 나온다.

토큰과 도구 시작·종료까지 보여주려면 노드 내부 이벤트 스트림이 필요하다. 두 층을 구분하지 않으면 "왜 한참 멈춰 있다가 한 번에 나오나"를 설명할 수 없다. 구현은 노트북을 서비스로에 있다.

Q. 진행 표시와 스트리밍은 같은 것인가

다르다. 단계 문구를 찍는 것그래프 이벤트를 구독하는 것은 별개다.

그리고 단계별 UX는 검색과 생성이 노드로 분리돼야 성립한다. 한 노드가 전부 하면 보여줄 중간 지점이 없다.

Q. UI에서 대화 상태는 어디에 두나

렌더링용 히스토리는 세션 저장소, 그래프 실행 상태는 **체크포인터 + thread_id**로 분리한다.

그래프 객체 자체는 캐싱해 재컴파일을 막는다. 셋을 한 곳에 두면 화면을 새로 그릴 때마다 그래프가 다시 만들어지거나, 반대로 대화가 남아 있는데 상태가 사라지는 일이 생긴다.

용어 정리

용어
StateGraph상태 스키마를 받아 노드·엣지를 조립하는 그래프 빌더
Node / Edge상태를 받아 부분 업데이트를 반환하는 함수 / 다음 노드로의 연결
Conditional Edge함수 반환값으로 다음 노드를 고르는 분기. 사이클의 출발점
Reducer기존 채널 값과 노드 반환값을 어떻게 합칠지 정하는 채널별 병합 규칙
add_messages메시지 전용 리듀서. 누적 + id 기반 upsert + 타입 정규화
Tool / bind_toolsLLM이 호출할 외부 기능 / LLM에게 도구 목록을 인지시키는 바인딩(결정)
ToolNodetool_calls를 실제로 실행해 결과 메시지를 반환하는 노드(실행)
tool_call_id도구 호출 요청과 그 결과를 잇는 식별자
Checkpointer스텝별 상태를 저장·복원하는 지속성 계층. HITL의 전제 조건
thread_id대화 세션을 가르는 키. 잘못 공유하면 대화가 섞이는 사고
HITL / interrupt_before실행 도중 사람이 승인·수정하는 구조 / 지정 노드 직전에 정지시키는 옵션
Structured Output출력을 스키마로 강제해 파싱 실패를 없애는 기능. 그래프 안정성의 전제

여기까지가 에이전트 하나를 제대로 돌리는 문제다. 다음 질문은 "언제 쪼개는가"로 넘어간다. 분할 경계, 토폴로지, 팬아웃, 그리고 실제 제품을 뜯어 만든 클론들은 멀티에이전트 Q&A에서 다룬다.