하나의 에이전트를 세 개로 분리하다. 계획은 실행자에게 도달하지 못했다.
요약
본 글은 LangGraph 서브그래프 경계에서 발생하는 '핸드오프 손실(handoff loss)' 문제를 분석합니다. 플래닝, 실행자, 검토자의 3단계 파이프라인을 구축했음에도 계획 정보가 다음 단계에 온전히 전달되지 않는 현상을 관찰했습니다. 그럼에도 불구하고 이 세 단계 구조는 단일 호출 대비 높은 정확도를 유지하며 성능 우위를 입증했습니다.
핵심 포인트
- LangGraph 경계에서 플래너의 계획이 손실되는 '핸드오프 손실' 문제가 발생함.
- 3단계 파이프라인은 단일 호출보다 정확도는 높으나, 토큰 및 시간 비용이 크게 증가함.
- 성능 향상은 단순히 플래너의 힌트 덕분이 아니라, 각 단계별 기여도를 분리 분석한 결과임.
- 멀티 에이전트 시스템 설계 시 상태 스키마 불일치와 비용 문제를 고려해야 함.
LangGraph 서브그래프 경계가 계획을 떨어뜨렸고, 얻은 이득은 계획 자체가 아니라 소스를 다시 읽는 것에서 왔다.
나는 로컬의 8B 모델에 실제 작업을 수행하기 전에 플래닝(planning) 단계를 부여했고, 핸드오프(handoff)를 LangGraph 서브그래프 자체 상태 스키마가 실제로 작동하는 방식대로 모델링했다. 즉, 부모 단계가 생성하지만 자식의 스키마에서 절대로 선언하지 않는 키는 반대편에서는 단순히 사라진다. 계획은 다음 단계에 온전하게 도달하지 못했다. 모든 작업에서 경계를 넘나든 부분은 플래너(planner)가 실제로 작성한 내용 중 60자짜리 조각이었다. 나는 이것이 손해를 입힐 것이라고 예상했다. 하지만 이 세 단계 파이프라인은 정확도 면에서 단일 호출 기준선보다 상당한 격차로 여전히 우세했다. 이 두 가지 사실이 합쳐져서 이 글을 읽을 가치가 있는 전체 이유이다. 왜냐하면 두 번째 사실은 보이는 것만큼 의미하는 바가 아니기 때문이다.
이 글은 AI의 도움을 받아 작성했으며, 모든 주장(claim)에 대해서는 내가 직접 사실 확인과 편집을 거쳤다.
증명 블록. 로컬에서 실행되는 Ollama의 llama3.1:8b, API 비용 0, 온도 0, 고정 시드. 12개의 합성 사고 보고서 추출 작업, 조건별로 하나의 결정론적 실행(실제 명시된 한계이며 숨겨진 한계가 아니다). 조건 A: 작업당 한 번의 호출. 조건 B: 실제 LangGraph 그래프를 실행하는 대신, LangGraph 서브그래프의 상태-스키마 경계를 모델링한 순수 Python 세 단계 파이프라인 (플래너 → 실행자 → 검토자), 총 36회 호출. 아래의 모든 주장은 공개적이고 재실행 가능한 저장소에서 추적 가능하다: github.com/priyansh18/agent-handoff-loss-benchmark.
요약 (TL;DR)
설명에 앞서 언급할 가치가 있는 네 가지 점이 있었다:
- 세 단계 파이프라인은 단일 호출의 83.3%에 비해 91.7%의 필드 정확도를 기록했지만, 토큰 수는 4.4배, 경과 시간(wall-clock time)은 5.7배였습니다.
- 총 12개 작업 중 12개 작업에서 플래너가 작성한 자체 메모(각 368~584자)는 실행자가 확인하기 전에 60자로 잘렸습니다. 이는 프롬프트 버그가 아니라 실제 상태 스키마 불일치(state-schema mismatch) 문제였습니다.
- 세 가지 개별 필드가 세 개의 다른 작업에 걸쳐 오답에서 정답으로 바뀌었습니다. 저는 실행자의 중간 답변과 리뷰어의 최종 답변을 분리하여 기록했습니다. 이는 단순히 가정하는 대신 어떤 단계가 기여했는지 확인하기 위함이었습니다. 실행자만 보고서 전체와 생존한 힌트 조각을 읽었음에도 이미 88.9%(36개 중 32개)에 도달했으며, 리뷰어가 추가로 단 하나의 필드를 더 추가했습니다.
- 세 가지 개선 사항 모두 플래너의 힌트가 경계를 넘어 유용한 정보를 전달했기 때문은 아니었습니다. 두 가지는 작업별 내용이 전혀 없는 순수한 보일러플레이트(boilerplate) 힌트로 인해 발생했고, 세 번째는 플래너의 문장 끊김 지점 덕분에 우연히 정말 유용한 단어가 포함된 힌트로 인해 발생했습니다.
**멀티 에이전트 LangGraph 핸드오프 손실(multi-agent LangGraph handoff loss)**은 이 실행에서 미니어처 형태로 재현되는 특정한 실패 사례이며, 이는 Anthropic이 자체 멀티 에이전트 시스템 엔지니어링 문서에서 언급하는 문제와 형태가 같습니다: 에이전트는 일반적으로 단일 채팅 턴보다 약 4배 더 많은 토큰을 사용하며, 전체 멀티 에이전트 시스템은 약 15배에 달합니다. 이 비용 중 일부는 한 에이전트의 출력이 다음 에이전트로 전달되는 과정에서 요약되고 재요약되면서 발생하는 일종의
무언가를 실행하기 전에, 즉 결과가 실제로 실패할 수 있도록: 단일 추출 호출을 플래너(planner)-실행자(executor)-리뷰어(reviewer) 파이프라인으로 분리하는 경우, 실행자의 상태 스키마(state schema)가 플래너가 생성하는 것보다 좁으면 핸드오프(handoff) 과정에서 측정 가능한 정보 손실이 발생하며, 이 손실은 개선된 성능이 아니라 단일 호출 기준선과 같거나 나쁜 정확도로 나타날 것입니다._ 그 절반은 맞았습니다. 나머지 절반은 틀렸고, 이는 충분히 논의할 가치가 있는 이유가 있습니다.
방법: 서브그래프 핸드오프 (subgraph handoff)
테스트 피처(test fixtures)로 작성된 12개의 짧고 허구적인 사고 보고서들은 각각 하나의 버그를 설명합니다. 예를 들어, 기능 플래그 롤아웃으로 인해 고갈된 Redis 연결 풀, 캐시된 파일을 덮어쓰는 오래된 모델 아티팩트, URL 단축기로 잘린 비밀번호 재설정 링크 등이 있으며, 같은 형태의 보고서가 9개 더 있습니다. 각 보고서는 세 필드(root_cause, affected_component, fix_action)에 필요한 키워드의 정답(ground truth)을 가지고 있습니다. 이 필드는 모델의 답변이 해당 필드의 필수 키워드 중 최소 하나를 포함하면 정확하다고 간주하며, 점수가 재현 가능하고 저의 주관적인 판단이 아닌 기계적인 방식으로 채점됩니다.
조건 A는 모든 세 필드를 JSON 형식으로 llama3.1:8b 모델에 작업당 한 번 요청합니다. 조건 B는 작업을 위해 세 번 호출을 실행합니다:
# 코드로 작성된 서브그래프 경계(subgraph boundary)입니다.
# 플래너가 전체 노트를 작성하면, 실행자는 오직 이것만 보게 됩니다:
focus_hint = plan_notes.split(".")[0][:60] if plan_notes else ""
해당 라인은 전체 실험의 핵심 구조를 이루는 부분이며, LangGraph의 StateGraph 호출이 아닌 순수한 Python 코드입니다. 제가 의도적으로 이렇게 작성한 이유는, LangGraph 자체 서브그래프 문서에서 설명하는 개념을 프레임워크의 전체 런타임 없이 모델링하기 위함이었습니다. 즉, 서브그래프의 상태 스키마가 실제로 부모/자식 경계를 넘나드는 것이며, 부모 단계가 생성했지만 자식의 스키마에 명시되지 않은 키는 다른 쪽에서는 단순히 누락된다는 것입니다. 예외(Exception)도, 경고(Warning)도 없습니다. 저는 의도적으로 스키마를 좁게 만들었는데, 이는 마감 기한에 쫓기는 팀이 실제로 하는 방식과 같습니다. 즉, 어떤 필드 이름을 연결할지 고민하기 전에, 무엇이 실제로 인계 과정에서 살아남아야 할지를 생각하며 임시로 지정하는 방식을 취했습니다.
그 후 실행자(executor)는 이 60자 길이의 힌트와 함께 세 개의 필드를 추출하고, 검토자(reviewer)는 원래 보고서에 실행자의 JSON을 추가하여 이를 수정할 수 있습니다. 총 세 번의 호출, 무언가를 올바르게 얻기 위한 세 번의 기회, 그리고 매번 공유되는 하나의 보고서가 만들어집니다.
설명에 앞선 결과

단일 호출 대 세 단계 파이프라인 비교 (동일한 12개 작업, 동일 모델, 동일 장비). 이미지 출처: 저자, 해당 실행 요약(summary.json)에서 생성.
나란히 놓인 수치들을 보면 다음과 같습니다:
- 필드 정확도: 단일 호출의 경우 30/36 (83.3%) 대 세 단계 파이프라인의 경우 33/36 (91.7%).
- 세 필드 모두 올바른 작업 수: 12개 중 7개 vs. 12개 중 9개.
- LLM 호출 횟수: 12회 vs. 36회.
- 총 토큰 수: 1,659개 vs. 7,323개.
- 실제 경과 시간(Wall-clock time): 72.7초 vs. 411.1초.
그리고 위의 비교에서는 보여주지 않는 인계(handoff) 횟수입니다: 12개의 작업 모두에서 전체 플래너 출력은 368자에서 584자가 나왔으며, 실제로 실행자에게 도달한 focus_hint는 60자로 제한되었습니다. 실제 로그 텍스트를 사용하여 한 작업의 사례를 보여드리겠습니다:

Image by author, generated programmatically from the actual run's raw_results.jsonl.
Planner가 전체적으로 수행한 retry-storm 작업에서: "다음은 간략한 조사 메모입니다: '시스템 로그를 검토하여 다운스트림 장애의 시점과 지속 시간 및 청구 워커 큐에 미친 영향을 파악하십시오. 시스템이 처음에 지수 백오프(exponential backoff)를 구현하지 않은 이유를 알아보기 위해 재시도 로직을 조사하고, 백오프를 구현하기 위해 수행된 구성 또는 코드 변경 사항을 검토하십시오. 또한, 재시도 폭풍(retry storm)이 데이터 불일치나 기타 문제를 일으켰는지 평가하십시오.'" 반면, 실행자(executor)가 받은 내용은: "다음은 간략한 조사 메모입니다: '시스템 로그를 검토'였습니다. 이는 'retry storm'이나 'backoff'라는 단어가 나타나기 훨씬 전에 잘린 60자 분량의 내용이었습니다.
실패 분석: 실제 이득이 발생한 곳
세 가지 필드가 세 개의 다른 작업에 걸쳐 조건 간에 잘못된 값에서 올바른 값으로 바뀌었습니다. 저는 실행자의 중간 답변을 검토자의 최종 답변과 별도로 기록했는데, 이는 가정을 하는 대신 어떤 단계가 실제로 작업을 수행했는지 확인할 수 있게 하기 위함이었고, 이 세 가지는 모두 동일한 방식으로 해결되지는 않았습니다.
feature-flag-leak의 경우, 플래너(planner)의 힌트는 순수한 상투어였습니다: "다음은 간략한 조사 메모입니다: 조사 메모"라는 전제 문구와 60자 분량의 서문만 있었고, 작업 관련 내용은 전혀 없었습니다. 이는 전체 실행 과정에서 가장 쓸모없는 힌트 버전이었습니다. 검토자가 손대기 전에 실행자 스스로 작성한 답변은 여전히 "공유 캐시 엔트리 재사용으로 인한 기능 플래그 타겟팅 규칙 누수"로 정확했습니다. 이 답변은 힌트가 아무것도 담고 있지 않았음에도 불구하고, 파이프라인의 모든 단계가 플래너의 힌트 내용 외에 받는 전체 원본 보고서를 읽었기 때문에 도달할 수 있었습니다.
retry-storm 케이스에서는 힌트가 '시스템 로그를 검토하세요'와 같이 반(半)일반적이었고, 순수한 상투문은 아니었지만 구체적이지도 않았습니다. 실행자(executor)의 답변은 여전히 틀렸으며, 단일 호출 때와 같은 간극을 보였습니다. 즉, '다운스트림 장애(downstream outage)'라는 언급만 있을 뿐, 재시도 폭풍(retry storm)에 대한 언급은 없었습니다. 검토자(reviewer) 단계에서는 전체 보고서를 두 번째로 받은 후 이를 '다운스트림 장애가 재시도 폭풍을 유발했다'고 수정하여 통과했습니다. 이것이 이번 실행 전체에서 오직 실행자의 자체적인 분석만으로는 부족했고, 검토자가 명확하게 필요했던 유일한 수정 사항입니다.
세 번째 redis-pool 케이스는 가장 솔직한 복잡성을 보여줍니다. 단일 호출에서는 'redis' 또는 '연결 풀(connection pool)'을 놓쳤지만, 실행자(executor)가 이를 수정했고, 이번에는 60자로 제한된 힌트가 실제로 'Review the Redis connection pool confi...'라는 내용을 포함하고 있었습니다. 플래너(planner)가 우연히 잘린 지점 직전에 정확히 올바른 단어들로 시작한 덕분에 가능했습니다. 여기서 힌트의 도움을 배제할 수는 없으며, 이 특정 작업에 대해 '쓸모없었다'고 말하는 것은 반대 방향으로 과장하는 것입니다. 제가 확실하게 말씀드릴 수 있는 것은 세 가지 수정 사항 중 두 가지는 아무런 기여를 하지 않은 힌트로 발생했고, 힌트가 도움이 되었을 수도 있는 한 경우는 파이프라인의 스키마가 보장하도록 설계된 것이 아니라 문장이 어디에서 끊기게 되었는지에 달린 문제였다는 것입니다.
종합하자면: 계획기 단계는 정직하게 측정되었지만, 이번 실행에서는 신뢰할 만한 결과를 얻지 못했습니다. 그 유일하고 그럴듯한 기여는 아키텍처가 아닌 우연한 표현에 의존했습니다. 실행자(executor)의 자체 보고서 읽기는 힌트와 독립적으로 이미 88.9%(32/36)를 기록했으며, 이는 추론된 것이 아니라 저장된 중간 답변을 통해 직접 측정되었습니다. 검토자(reviewer)는 그 위에 정확히 한 필드를 추가하여 33/36을 달성했습니다. 만약 제가 계획기와 검토자를 완전히 제거하고 보고서를 하나의 실행자 형태의 호출로 바로 보낸다면, 단일 호출 기준선인 83.3%가 아니라 이 88.9%에 가까운 수치를 기대할 것입니다. 왜냐하면 실행자 프롬프트 자체가 순수한 단일 호출 프롬프트보다 더 구조화된 출력을 요청하기 때문입니다. 저는 정확히 그러한 제거 실험(ablation)을 수행하지 않았기 때문에, 그 특정 비교는 측정된 결과가 아니라 합리적인 예상으로 남겨집니다. 이 섹션의 나머지 모든 것은 측정된 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기