
자율형 AI 에이전트에서 가장 어려운 부분은 언해피 패스(unhappy path)입니다
요약
자율형 AI 에이전트 구축 시 예외 상황(unhappy path)을 처리하는 엔지니어링의 중요성을 다룹니다. LangGraph를 활용해 유전체 분석 에이전트인 BioAgent를 구현하며, 상태 머신과 유계된 사이클을 통해 안정성을 확보하는 방법을 설명합니다.
핵심 포인트
- 에이전트의 핵심은 성공 경로가 아닌 예외 상황(unhappy path) 처리 능력임
- LangGraph를 사용하여 사이클과 조건부 라우팅이 포함된 상태 머신 모델링
- 무한 루프 방지를 위해 모든 사이클에 엄격한 재시도 제한(retry limit) 설정
- BioAgent는 유전체 데이터 분석부터 문헌 검색, 보고서 작성까지 수행
대부분의 AI 에이전트 데모는 해피 패스(happy path)를 보여줍니다. 깔끔한 질문, 정돈된 답변, 그리고 모두의 박수갈채 말이죠. 하지만 흥미로운 엔지니어링은 그 외의 모든 곳에 존재합니다. 에이전트가 의존하는 API가 다운되면 어떻게 할까요? 모델이 즐겁게 루프(looping)를 반복하는 동안, 매 단계마다 당신의 신용카드가 연결되어 있다면 어떨까요? 데이터가 전혀 없는데도, 데이터처럼 보이는 무언가를 완벽하게 써 내려갈 능력이 있다면 어떻게 해야 할까요?
저는 이러한 질문들이 학술적인 수준에 머물지 않는 도메인을 위해 자율형 에이전트를 구축했으며, 언해피 패스(unhappy path)를 제대로 처리하는 것이 작업의 대부분이라는 것을 깨달았습니다. 제가 배운 점은 다음과 같습니다.
프로젝트는 github.com/gbadedata/bioagent입니다.
기능
BioAgent는 유전체 파이프라인(genomics pipeline)을 위한 자율 품질 관리 분석가입니다. 샘플 ID를 제공하면 나머지는 스스로 수행합니다. 일련의 도구들을 통해 라이브 파이프라인 API에서 일치도(concordance) 및 재현성(reproducibility) 지표를 가져오고, 벤치마크 임계값(benchmark thresholds)에 따라 숫자의 의미를 파악하며, 실제 발견 사항을 바탕으로 타겟팅된 PubMed 쿼리를 생성하고, 문헌을 검색하며, 구조화된 임상 등급의 품질 보고서를 작성합니다. 추론하는 동안 이 모든 과정을 Streamlit 채팅으로 스트리밍하며, 스케줄러가 호출할 수 있는 FastAPI 엔드포인트를 노출합니다.
이 에이전트는 LangGraph와 Claude로 구축되었습니다. 왜 단순한 "도구 목록이 여기 있습니다" 식의 에이전트가 아닌 LangGraph를 사용했는지가 이 포스트의 핵심입니다.
왜 그래프(graph)인가, 그리고 왜 제한(bounded)되어야 하는가
단순한 에이전트는 질문을 받고, 아마도 몇 가지 도구를 호출한 뒤 답변을 내놓습니다. 하지만 BioAgent는 순차적으로 결정을 내려야 합니다: 데이터를 가져온 다음, 돌아온 결과값을 바탕으로 문헌을 검색할 가치가 있는지 결정해야 합니다. 만약 검색 결과가 비어 있다면, 검색 범위를 넓혀 재시도해야 합니다. 만약 파이프라인에 접속할 수 없다면, 중단하고 이를 명확하게 알려야 합니다. 이것은 사이클(cycles)과 조건부 라우팅(conditional routing)이 있는 상태 머신(state machine)이며, 이것이 바로 LangGraph가 모델링하는 방식입니다.
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#eef2f7','primaryBorderColor':'#1b2a4a','primaryTextColor':'#1b2a4a','lineColor':'#4c78a8','fontFamily':'Segoe UI, sans-serif'}}}%%
flowchart TD
START([sample_id]) --> FETCH["fetch_data<br/>call 5 pipeline API tools"]
...
가장 중요한 속성은 그래프가 **유계(bounded)**되어 있다는 점입니다. 모든 사이클(cycle)에는 엄격한 재시도 제한(retry limit)이 있으며, 에이전트는 물리적으로 영원히 루프를 돌 수 없습니다. 에이전트가 매 단계마다 유료 API를 호출하는 경우, "영원히 루프를 돌 수 없다"는 것은 있으면 좋은 기능(nice-to-have)이 아니라 안전 요구 사항(safety requirement)입니다.
다음은 처음부터 끝까지의 단일 실행 과정입니다:
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#eef2f7','actorBkg':'#eef2f7','actorBorder':'#1b2a4a','actorTextColor':'#1b2a4a','signalColor':'#4c78a8','signalTextColor':'#1b2a4a','noteBkgColor':'#f4f7fb','noteBorderColor':'#4c78a8'}}}%%
sequenceDiagram
participant U as User / API
...
레슨 1: 루프의 유계화(bounding a loop)는 미묘하게 틀리기 쉽습니다
다음은 데이터 가져오기(data-fetch) 단계 이후의 라우팅(routing)입니다:
def route_after_fetch(state):
critical = {"get_concordance_summary", "get_pipeline_runs"}
critical_failed = critical.intersection(set(state["failed_tools"]))
...
아이디어는 간단합니다. 만약 핵심 도구(critical tools)가 실패했고 재시도 예산(retry budget)이 남아 있다면 다시 시도하고, 예산을 모두 소진했다면 우아하게 포기(give up gracefully)하며, 그렇지 않으면 계속 진행하는 것입니다.
미묘한 차이는 카운터(counter)가 실제로 움직여야만 유계(bound)가 유계로서 작동한다는 점입니다. 작업을 수행하는 노드(node)가 재시도 횟수(retry count)를 증가시키는 것을 잊어버리면, 라우터는 영원히 "예산이 남아 있음"을 확인하게 되고, 우아한 종료(graceful exit) 단계에 절대 도달하지 못합니다. 에이전트는 프레임워크의 재귀 제한(recursion limit)이 걸려 예외를 던질 때까지 루프를 돌게 되는데, 이는 안전하게 실패(failing safely)하는 것과 정반대되는 상황입니다. 해결책은 fetch 노드에 단 한 줄을 추가하는 것입니다:
return {
...
"fetch_retries": state.get("fetch_retries", 0) + 1, # 이 값이 움직여야만 유계가 작동합니다
...
이러한 종류의 버그는 개발 중에는 절대 나타나지 않습니다. 왜냐하면 여러분은 항상 API가 정상 작동하는 해피 패스 (happy path)만을 테스트하기 때문입니다. 이 버그는 의존성 (dependency)이 깨졌을 때만 나타납니다. 따라서 진정한 해결책은 단순히 한 줄을 추가하여 값을 증가시키는 것이 아니라, API를 강제로 다운시킨 상태에서 에이전트를 실행하고, 에이전트가 무한 루프에 빠지는 대신 성능 저하 (degrade) 상태로 전환되는지를 확인하는 테스트입니다:
def test_full_run_degrades_when_api_down(mock_pipeline_api_down):
result = run_agent("HG001")
assert result["status"] == "degraded"
해당 '루프 방지 및 성능 저하' 테스트를 포함하여 CI에서 통과(green)하고 있는 전체 테스트 스위트는 다음과 같습니다:
[

언해피 패스 (unhappy path)를 테스트하지 않는다면, 가장 중요한 부분을 테스트하지 않은 것입니다.
레슨 2: 데이터가 없는 에이전트는 보고서를 작성해서는 안 된다
이러한 유형의 시스템에서 가장 위험한 실패는 크래시 (crash)가 아닙니다. 아무런 근거 없이 생성된, 자신감 넘치고 정교해 보이는 보고서입니다. 따라서 핵심 도구 (tools)에 접근할 수 없을 때, 그래프 (graph)는 어떤 도구가 실패했는지, 무엇을 검색할 수 없었는지, API를 어떻게 시작해야 하는지를 정확히 보고하고 멈추는 전용 노드 (dedicated node)로 경로를 지정해야 합니다. 절대로 그 공백을 그럴듯한 숫자로 채워서는 안 됩니다.
이는 단순히 프롬프트 (prompt) 지침에 그치는 것이 아니라, 성능 저하된 보고서에 조작된 지표 (fabricated metrics)가 포함되어 있지 않음을 확인하는 테스트를 통해 강제됩니다:
def test_report_does_not_hallucinate_metrics():
result = graceful_degradation(state_with_failed_tools)
assert "0.99" not in result["report"]
모델에게 따르라고 요청하는 규칙은 희망일 뿐입니다. 테스트가 강제하는 규칙은 보증입니다.
레슨 3: 모델이 실제로 검색한 내용에 근거를 두게 하라
에이전트가 보고서에 PubMed 논문을 인용하는 것은 관련성을 지어내라는 조용한 유혹입니다. 초기에는 초록(abstract)을 가져온 뒤 이를 버리고 PMID(PubMed ID)만 하위 단계로 전달했기 때문에, 모델은 해당 논문이 무엇을 말하는지 전혀 보지 못한 채 논문이 어떻게 결과(findings)를 뒷받침하는지 설명해야 했습니다. 이것이 바로 확신에 찬 헛소리(confident nonsense)를 만들어내는 전형적인 지름길입니다.
해결책은 검색된 초록 텍스트를 보고서 단계까지 그대로 전달하고, 모델에게 제공된 초록에만 근거하여 문헌 섹션을 작성하도록 하며, 검색된 내용이 없다면 이를 명확하게 말하도록 지시하는 것이었습니다. 검색(Retrieval)은 검색된 텍스트가 실제로 글을 쓰는 곳까지 도달해야만 비로소 근거 설정(grounding)이 됩니다.
핵심 요약 (Takeaways)
- 모든 루프에 제한을 두고, 이를 증명하라. 자율형 에이전트(autonomous agent)에서 제한 없는 재시도(unbounded retry)는 걷잡을 수 없는 비용 청구로 이어집니다. 제한(bound)은 카운터가 실제로 증가할 때만 의미가 있으므로, 실제로 증가하는지 테스트하십시오.
- 언해피 패스(unhappy path)를 테스트하라. 해피 패스(happy path)는 항상 작동할 수밖에 없는 부분입니다. 의존성(dependency)을 강제로 차단하여 에이전트가 안전하게 실패(fail safely)하는지 확인하십시오.
- 데이터가 없으면 보고서도 없다. "지어내지 마시오"를 프롬프트의 정중한 요청이 아닌, 테스트된 보증(guarantee)으로 만드십시오.
- 근거 설정(Grounding)이란 검색된 텍스트가 작성자에게 도달하는 것을 의미한다. 초록을 가져온 뒤 무시하는 것은 아예 가져오지 않는 것보다 더 나쁩니다.
코드, 전체 LangGraph 상태 머신(state machine), 테스트, 그리고 아키텍처 다이어그램은 모두 리포지토리에 있습니다: github.com/gbadedata/bioagent. 동일한 아이디어에 대한 MCP-server-plus-tool-using-agent 관점의 구현을 원하신다면, github.com/gbadedata/mcp-research-agent에 별도로 작성해 두었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기