에이전트 그래프 엔지니어링 (Agent Graph Engineering), 파트 0: 이름 붙여지기 전에 필요했던 것
요약
본 글은 단순 챗봇을 넘어, 추적 및 테스트가 가능한 복잡한 AI 시스템 구축의 어려움을 다룹니다. 기존 방식으로는 해결할 수 없었던 문제들을 '그래프' 기반의 제어 흐름(control flow)으로 모델링하는 방법론의 필요성을 강조합니다.
핵심 포인트
- 복잡한 AI 시스템은 단순 챗봇을 넘어선 추적 및 테스트가 필수입니다.
- 기존 코드 기반의 흐름 대신, 그래프로 선언적인 제어 흐름을 사용하는 것이 핵심 해결책입니다.
- 에이전트 분할, 안전한 도구 호출, 전체 흐름 가시성 확보 등이 주요 과제입니다.
요약 챗봇을 만드는 것은 쉽다. 하지만 추적하고, 테스트하고, 두려움 없이 변경할 수 있는 AI 시스템을 구축하는 것은 아니다. 나는 의도 감지(intent detection)에 중첩된 if/else 문을 더한 LLM 기능을 출시하는 데 몇 년을 보냈지만, 아무리 많은 관측 가능성(observability) 도구를 사용해도 해결되지 않았다. 왜냐하면 문제는 흐름이 코드 안에만 존재하고, 내 머릿속과 오래된 다이어그램에만 존재했기 때문이다. 해결책은 흐름을 제어 흐름(control flow)으로 작성하는 것을 멈추고, 그래프로 선언하기 시작하는 것이었다. 이 글은 내가 어떻게 그 지점에 도달하게 되었는지, 이 방법론이 이름 붙여지기 몇 년 전의 이야기이며, 왜 나는 그래프가 트렌드라기보다는 지속 가능한 해답이라고 생각하는지에 대한 이야기이다. 만약 팀에 가져갈 스택과 논리만 알고 싶다면 Part 1로 건너뛰라.
- 문서 처리와 웹 검색이 동일한 프롬프트 지침의 복잡한 엉킴이 되지 않도록 작업을 전담 에이전트(dedicated agents)들로 어떻게 분할하시나요?
- 이메일 발송과 같은 되돌릴 수 없는 작업에 대해서는 인간의 승인을 위해 도구 호출(tool call)을 일시 중지하면서도, 공개 프로필 읽기처럼 안전한 호출은 방해받지 않고 실행되게 하려면 어떻게 해야 하나요?
- 누구나 에이전트 흐름(agent flow) 전체 그림을 볼 수 있나요? 의도 라우팅(Intent routing)은 조건문(conditionals) 안에 존재합니다. 디자인 검토 시 가리킬 아티팩트가 없습니다.
- 사용자 메시지 하나를 도착부터 최종 답변까지 추적하려면 어떻게 해야 할까요? 관측 가능성 플랫폼(Observability platforms)은 LLM 스팬(LLM spans) 목록을 제공할 뿐입니다. 흐름을 재구성한다는 것은 코드를 역으로 읽거나 트레이스(traces)를 사람이 읽기 쉬운 형태로 재구성하는 스크립트를 작성해야 함을 의미합니다.
- 프론트엔드는 백엔드가 어떤 메시지를 보낼 수 있는지 어떻게 알게 되나요? 흐름이 변경될 때, 한쪽에서 스키마가 조용히 구식이 되는 것을 막는 것은 무엇인가요?
- 40초 동안 침묵하다가 모든 것을 한 번에 쏟아내는 대신, 예측 가능한 방식으로 중간 진행 상황을 스트리밍하려면 어떻게 해야 하나요?
- 이 모든 것들을 어떻게 테스트하나요? 빠른 피드백을 위한 목업 응답(Mocked responses)과 모델을 교체하거나 프롬프트를 수정할 때 회귀(regressions)를 포착하기 위한 실제 모델 실행입니다.
- 에이전트가 잘못된 도구를 호출하거나, 두 개의 도구를 호출하거나, 아무것도 호출하지 않았을 때, 실제로 무엇을 보아야 할까요?
만약 이 중 어떤 것도 당신을 괴롭히지 않았다면, 아마도 시스템이라기보다는 챗봇(chatbot)을 가지고 있을 가능성이 높습니다. 저도 그랬으니까요.
이야기를 시작하기 전에 한 가지 말씀드릴 것이 있습니다. 제가 알고 싶어서입니다. 다음에 나오는 내용은 복합적이거나 가상의 것이 아닙니다. 실제 금융 도메인에서, 실제 사용자들과 팀이 있었고, 제가 실수했던 경험을 바탕으로 합니다. 요즘 생성 에이전트 관련 글들이 많이 나오고 있어서, 저는 이 포스트를 훑어보며 핵심 내용을 찾으려는 반사적인 행동(reflex)을 이해합니다. 그러니 그냥 훑어보세요. 여기에 있는 세부 사항들은 실제로 일어났기 때문에 구체적이며, 이는 꾸며낼 수 없는 것입니다.
이 시리즈와 함께 사용되는 코드는 같은 이야기를 담고 있습니다. 이 코드는 실시간 인프라 및 에이전트 시스템에 약 5년간 매달린 경험 중, 한 에이전트 제품에 3년, 다른 제품에 2년을 투입한 프로덕션 시스템 다섯 가지 서비스에서 추출되었습니다. 계승되는 것은 도메인이 아닙니다. 이 부분은 의도적으로 제거되었습니다. 계승되는 것은 실패 모드입니다. 지나치게 신경 쓴 것처럼 보이는 부분이 사실은 다른 곳에서 먼저 고장 났던 부분들입니다.
제가 처했던 상황
몇 년 전, ChatGPT가 등장했을 무렵 제 팀과 저는 금융 도메인에서 AI 제품을 개발하고 있었습니다. 저는 완전히 처음부터 시작한 것은 아니었습니다. GPT-2 모델을 학습시킨 경험이 있었고, Django Channels를 사용해 실시간 시스템을 구축했으며, 챗봇도 출시한 적이 있습니다.
제품은 작동했습니다. 사용자들도 사용했습니다. 하지만 이는 당시 누구나 알던 방식대로만 만들어졌습니다. '에이전트(agent)'라는 용어는 아직 존재하지 않았습니다. 저희는 함수 호출(function calls)을 가진 비서들을 만들었고, 이것이 나중에 도구 호출(tool calls)로 발전했으며, 생각의 사슬(chain of thought)이나 생각의 나무(tree of thought) 같은 프롬프팅 기법도 사용했습니다. 저희 도메인은 사용자가 원하는 것을 분류하고 적절한 핸들러로 라우팅해야 했기 때문에, 시스템의 핵심은 의도 분류기(intent classifier)가 조건문 트리(tree of conditionals)에 데이터를 공급하는 구조였습니다.
저희는 이 부분에 소홀하지 않았습니다. 리팩토링을 했고, 알고 있는 모든 모범 사례를 적용했으며, 코드는 일반적인 백엔드 표준으로 볼 때 합리적이었습니다. 하지만 무언가 잘못되었다는 느낌이 들었고, 그것을 명명하는 데 시간이 걸렸습니다.
실제로 부족했던 것은 다음과 같았습니다:
시스템 뷰(system view)의 부재. 흐름의 형태를 보여주는 것이 아무것도 없었습니다. 이해하려면 진입점부터 종료점까지 읽고, 모든 분기점을 머릿속에 담아 draw.io 같은 곳에 손으로 그려야 했습니다. 그러다 누군가 변경 사항을 배포했고, 그 다이어그램은 거짓이 되었습니다.
종단 간 추적(end-to-end trace)의 부재. 개별 모델 호출은 볼 수 있었습니다. 하지만 하나의 사용자 메시지가 라우팅, 도구 호출, 중간 결과, 그리고 최종 답변까지 단일 연결된 흐름으로 이어지는 것을 따라갈 수는 없었습니다.
생성된 계약(generated contract)이 없었습니다. 저희의 REST API에는 OpenAPI가 있었습니다. 직렬화기(serializer)를 변경하고 재생성하면 프론트엔드가 알 수 있었습니다. 하지만 저희 AI 레이어에는 그에 상응하는 것이 아무것도 없었습니다. WebSocket 메시지 형태는 두 곳에서 수동으로 유지 관리되었고, 시간이 지나면서 어긋나갔습니다.
불안정한 스트리밍(Flaky streaming). 작동은 했지만, 아무도 재현할 수 없는 방식으로 지연되거나 끊기는 일이 있었습니다.
새로운 팀원에게 그 시스템을 온보딩하는 데는 몇 주가 걸렸습니다. 프로덕션 보고서를 디버깅한다는 것은 코드를 읽어 내려가는 원정대와 같았습니다. 그것은 더 나은 로거(logger)로 해결할 수 있는 확장성 문제가 아니었습니다.
상태 기계의 필요성 (The state machine itch)
어느 순간 그 형태가 명확해졌습니다. 각 단계는 하나의 상태를 가집니다. 모델은 그 상태를 처리합니다. 결과에 따라 다른 상태로 전환됩니다. 그것이 바로 상태 기계(state machine)입니다. 저는 대학에서 배웠고 몇 년 동안 생각하지 않았던 것이었습니다.
이렇게 보니, 조건문(conditionals)들이 코드로 작성되기보다는 선언(declared)되기를 원하는 무언가의 구현 세부 사항처럼 보였습니다. 그리고 선언된 상태 기계는 구조 자체가 제어 흐름(control flow)이 아닌 데이터로 존재하기 때문에 자동으로 그려지고, 검사되고, 추적될 수 있습니다.
당시 프레임워크 환경은 기본적으로 LangChain이었고, 초기 LangChain은 거칠었습니다. 그 자리에 있었다면 아실 겁니다. 무언가 잘못되었을 때 그러한 추상화(abstractions)를 통해 디버깅하는 것은 자체 코드를 디버깅하는 것보다 더 힘들었습니다.
그래서 저는 에이전트를 위한 그래프 라이브러리를 원했습니다. 상태에 따라 라우팅할 수 있고, 스스로 렌더링할 수 있으며, 팀원이 천 줄의 코드를 읽지 않고도 시스템을 이해할 수 있게 해주는 것이요. 그래서 제가 작은 패키지를 만들기 시작했지만 포기했고, 제대로 하려면 많은 작업이 필요했고 배포해야 할 제품이 있었기 때문입니다.
조각들을 찾다 (Finding the pieces)
시간이 흘렀고, 이 분야는 발전했으며 라이브러리들도 훨씬 좋아졌습니다. CrewAI가 등장했습니다. Pydantic AI도 등장했습니다. 심지어 LangChain조차 저를 지치게 했던 버전과는 상당히 다른, 진정으로 유용한 무언가로 성장했습니다.
Pydantic AI는 저를 즉시 사로잡았습니다. Pydantic 모델로 검증되는 툴 호출(Tool calls), 타입이 지정된 출력(typed outputs), 타입이 지정된 의존성(typed dependencies), 제공업체 독립성(provider independence) 그리고 사용자가 불편함을 느끼지 않는 API가 있습니다. 이 라이브러리는 에이전트를 선언된 출력 유형과 선언된 도구 세트를 가진 '개체'로 취급하는데, 이것이 정확합니다.
저는 여전히 그래프 부분이 필요했습니다. 그러다 Reddit 스레드에서 LangGraph를 접하게 되었습니다. 그것은 제가 스케치하던 바로 그 것이었습니다: 상태(state), 노드(nodes), 조건부 엣지(conditional edges), 그리고 다이어그램으로 내보낼 수 있는 흐름입니다. 저는 Andrew Ng의 강좌를 통해 이를 학습했고, 이미 다른 방향에서 같은 모델에 도달했기 때문에 개념들이 빠르게 이해되었습니다.
한 가지가 저를 신경 쓰이게 했습니다. LangGraph는 LangChain 생태계 라이브러리이고, 저는 에이전트 계층에는 Pydantic AI를 원했습니다. 그래서 이 둘을 결합한 사람들을 찾아보았고, 많이 발견했습니다. 이 조합은 계속해서 등장했고, 강력한 지지 의견들과 함께 일부 팀들은 이미 프로덕션 환경에서 이를 운영하고 있었습니다.
저는 사이드 프로젝트에 적용해 보았는데 예상보다 훨씬 잘 작동했습니다. 두 가지의 미흡한 부분이 남아있었습니다. Pydantic AI와 LangGraph 간의 인터럽트(Interrupts)는 주의 깊은 처리가 필요했고, 스트리밍(streaming)도 어색했습니다. 왜냐하면 LangGraph는 LangChain 외부의 모든 것에 대해 일반적인 커스텀-스트림 채널만 제공하기 때문입니다.
이 스트리밍 문제는 제가 이미 다른 곳에서 해결한 적이 있습니다. chanx는 타입이 지정된 메시지(typed messages)와 생성된 AsyncAPI 문서를 위해 제가 만든 WebSocket 라이브러리이며, 호출 그래프를 통해 스트림 객체를 전달할 필요 없이 프로세스의 어느 곳에서든 방송(broadcast)할 수 있습니다. 이를 도입함으로써 서브그래프 깊숙한 곳에 있는 그래프 노드가 그래프 흐름이 알거나 신경 쓰지 않아도 사용자에게 진행 상황 이벤트를 내보낼 수 있게 되었습니다.
LangGraph, Pydantic AI, 그리고 chanx. 이것이 제가 몇 년 동안 원했던 조합이며, 여전히 제가 가장 먼저 찾는 조합입니다.
프레임워크를 뛰어넘는 이유
저는 수많은 AI 트렌드가 나타났다 사라지는 것을 지켜보았습니다. 대부분은 견고하지 않았습니다. 하지만 그래프는 그렇지 않으며, 저는 그 이유가 이 아이디어가 관련 과대광고보다 더 오래되었기 때문이라고 생각합니다. 이는 빌드 시스템(build systems), 데이터플로우 엔진(dataflow engines), 상태 머신(state machines) 등에서 나타나는 구조와 같으며, 새로운 실행 기질(execution substrate)에 적용된 것입니다.
신호들 또한 정렬되고 있습니다. Google의 ADK 자료는 이제 그래프 엔지니어링을 직접 가르치고 있습니다. 제가 인터넷에서 활동하는 코너에서의 논의는 프롬프트 트릭(prompt tricks)에서 흐름 구조(flow structure) 쪽으로 이동했습니다.
어휘력도 따라잡았습니다. 그래프 엔지니어링에 대한 가이드가 생겼고, 점점 쌓이는 논문들이 있으며, 거의 정착된 정의가 있습니다: 에이전트 시스템이 실행되는 그래프를 설계하는 것, 작업을 수행하는 노드(nodes), 그 사이를 라우팅하는 엣지(edges), 그리고 그 위를 이동하는 상태(state)입니다. 이전에 가지고 있었으면 좋았을 구분이 하나 있습니다. 루프 엔지니어링은 단일 에이전트가 완료할 때까지 반복하는 주기를 설계합니다. 그래프 엔지니어링은 그러한 주기들 간의 조정(coordination)을 설계합니다. 제가 겪었던 고통 대부분은 첫 번째 문제에 대한 도구로 두 번째 문제를 해결하려고 시도하면서 발생했습니다.
여전히 부족한 것은 프로덕션 자료입니다. 쓰인 거의 모든 것이 튜토리얼입니다: 여기 상태 그래프가 있고, 여기에 노드와 엣지가 있으며, 질문에 답하는 두 노드 예시가 있습니다. 실제 시스템 내에서 그래프가 작동해야 할 때 무슨 일이 일어나는지에 대해서는 거의 다루지 않습니다. 즉, 프론트엔드에 대한 타입이 지정된 계약(typed contract), 되돌릴 수 없는 행동을 승인하는 사람, 사고 발생 후 누군가에게 건네줄 수 있는 추적 기록(traces), 모델을 변경할 때 손가락으로 기대는 것이 아니라 검증할 수 있는 평가(evals), 그리고 마지막 배포(deployment) 같은 것들입니다. 이 격차를 채우는 것이 바로 이 시리즈의 목적입니다.
명확히 할 것이 한 가지 더 있습니다. 바로 이것이 이 주제를 검색하기 어렵게 만드는 이유입니다. 검색창에 'graph'와 'AI'를 넣으면 주로 지식 그래프(knowledge graphs)나 그래프 RAG(Graph RAG)가 나옵니다. 이는 그래프가 쿼리하는 데이터인 검색 기법을 말합니다. 하지만 이것은 에이전트 자체의 실행 그래프(execution graph)입니다. Graph RAG는 여기에 있는 여러 도구 중 하나로 하나의 노드 뒤에 위치할 수 있습니다. 같은 단어지만 관련 없는 개념입니다.
이 시리즈에서 다룰 내용
스택(stack)부터 추론(reasoning)을 거쳐 실제 구동 시스템까지 12개의 포스트를 통해 다룹니다. 우리는 의도적으로 노트북 형태가 아닌 프로덕션 환경에 맞는 레퍼런스 구현체를 구축할 것입니다. 여기에는 React 프론트엔드, 비즈니스 데이터를 소유하는 Django 백엔드, 그리고 그래프를 실행하고 타입이 지정된 WebSocket 계약을 통해 통신하며, 인간의 개입(human-in-the-loop) 승인, 진행 상황 스트리밍, 엔드투엔드 추적(end-to-end tracing), 목업 테스트(mocked tests), 실제 모델 평가(real-model evals) 기능을 갖춘 별도의 FastAPI 에이전트 서비스가 포함됩니다.
Part 1에서는 스택과 각 구성 요소의 필요성을 다루며, 이는 기술 리드에게 전달할 수 있는 부분입니다. 그 이후는 모두 코드에 관한 내용입니다.
이 시리즈에서 사용되는 프레임워크들은 시간이 지나면서 구식이 될 것입니다. 일부는 대체될 것이고, 여러분은 직접 구축해야 할 수도 있습니다. 하지만 에이전트의 흐름을 제어 흐름(control flow)으로 작성하는 대신 그래프로 선언해야 한다는 근본적인 구조적 아이디어는 도구(tooling)를 초월하여 지속되기를 기대합니다.
이 이야기가 유용하기를 바랍니다. 특히 본문의 전반부에서 자신을 발견한다면 더욱 그럴 것입니다.
저는 이러한 심층 분석 글을 작성하고 몇 가지 Python 라이브러리를 유지 관리하고 있으니, 이런 종류의 콘텐츠에 관심이 있다면 GitHub와 LinkedIn에서 계속 지켜봐 주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기