멀티 에이전트 시스템을 실제로 망가뜨리는 것은 무엇인가: 실제 운영 실패 사례 보고서
요약
멀티 에이전트 시스템 운영 중 발생하는 '조용한 실패(Silent Failure)' 사례를 분석합니다. LangChain의 체크포인터 버그처럼 에러 로그 없이 도구 호출을 건너뛰는 현상을 탐지하기 위한 도구 개선 과정과 실제 운영 패턴을 다룹니다.
핵심 포인트
- 에러 로그가 남지 않는 '조용한 도구 건너뛰기' 현상이 에이전트 운영의 핵심 위험 요소임
- LangChain의 체크포인터가 이전 도구 출력을 컨텍스트로 오인해 새 도구 호출을 생략하는 버그 사례 제시
- 단순 에러 키워드 탐지를 넘어 'skipped' 상태 및 빈 콘텐츠 반환을 포착하는 정교한 모니터링 필요
다음은 LangChain에서 확인되어 보고된 버그입니다. 체크포인터 (checkpointer)가 연결된 에이전트에서 다음과 같은 대화가 발생합니다:
Turn 1: "Company A는 언제 설립되었나요?"
→ 도구 (tool) 실행, "2020" 반환
→ 에이전트: "Company A는 2020년에 설립되었습니다." ✅ 정답
Turn 2 (동일한 대화): "Company B는 언제 설립되었나요?"
→ 도구가 실행되지 않음
→ 에이전트: "Company B에 대한 정보가 없습니다." ❌ 오답, 그리고 조용히 틀림
예외도 없고, 에러 로그도 없으며, 타임아웃도 없습니다. 도구가 단순히 호출되지 않았을 뿐입니다. 메커니즘은 다음과 같습니다: 체크포인터가 Turn 1의 도구 결과를 메시지 기록 (message history)에 저장합니다. Turn 2가 들어오면, LLM은 컨텍스트 (context)에 이미 놓여 있는 이전 도구 출력을 보고, 필요한 정보를 이미 가지고 있다고 가정하여 새로운 도구 호출을 발행하지 않습니다.
저는 에이전트의 실행 추적 (execution trace)을 입력받아 — SDK나 계측 (instrumentation) 없이 직접 붙여넣기만 하면 — 신뢰도 점수와 발견된 모든 실패의 근본 원인을 반환하는 도구를 만들었습니다. 그래서 당연하게도, 이 정확한 추적을 다시 재현하여 제가 만든 탐지기로 잡아낼 수 있는지 확인해 보았습니다.
결과는 100점 만점에 100점이었습니다. 깨끗했습니다. 감지된 실패가 없었습니다.
이러한 유형의 문제를 잡기 위해 특별히 제작된 저의 도구가 첫 번째 시도에서 실제 문제를 놓친 것입니다. 그 순간, 이것은 사이드 프로젝트를 넘어 이러한 실패가 어떤 모습인지, 그리고 왜 그렇게 잘 숨어 있는지에 대한 실제 조사로 변했습니다.
왜 나의 도구가 이를 놓쳤는가
저의 탐지 로직은 도구 출력에서 "failed", "error", "not found"와 같은 명시적인 에러 키워드를 확인하고 있었습니다. 이 추적에서는 도구가 단순히 아무것도 반환하지 않았습니다. 일치하는 에러 단어가 없었으며, 단지 빈 결과와 제 코드에서 실제로 읽지 않는 "skipped"로 표시된 상태 (status) 필드만 있었을 뿐입니다.
해결을 위해 두 가지 변경이 필요했습니다: 상태가 명시적으로 "skipped"인 모든 도구 단계를 포착하는 것과, 상태와 관계없이 빈 콘텐츠를 반환하는 모든 도구 단계를 별도로 포착하는 것입니다. 이전에는 둘 다 존재하지 않았습니다. 수정 후, 동일한 추적은 100점 만점에 65점을 기록했으며, 도구가 침묵한 바로 그 단계에서 "MISSING MANDATORY TOOL CALL (필수 도구 호출 누락)"이라고 정확하게 표시했습니다.
그 간극 — 즉, 요란한 실패만 포착하고 조용한 실패는 놓치는 것 — 이 전체 이야기의 핵심임이 드러났습니다.
실제 실패 사례를 찾아 나서기
그 후 이틀 동안 저는 간단한 일을 수행했습니다. GitHub 이슈를 검색하고, Twitter와 LinkedIn에서 에이전트 문제를 설명하는 개발자들에게 답장을 보냈으며, 추측하는 대신 직접적인 질문을 던졌습니다. 그 과정에서 나타난 패턴은 일관적이었고 다소 불안했습니다. 이러한 실패 중 거의 대부분은 에러를 발생시키지 않는다는 점입니다. 그것들은 마치 성공한 것처럼 보입니다.
다음은 실제 운영 시스템에서 반복적으로 나타난 패턴들이며, 모두 익명 처리되었습니다.
조용한 도구 건너뛰기 (The Silent Tool Skip)
위에서 언급한 LangChain 버그가 가장 깔끔한 예시입니다. 실행되어야 할 도구가 실행되지 않음에도 불구하고, 에이전트는 마치 실행된 것처럼 계속 진행하며, 종종 그럴싸하게 들리는 틀린 답을 내놓습니다. 도구가 호출되었는지 여부를 구체적으로 확인하지 않는 한, 트레이스 (trace)는 성공적인 실행과 동일해 보입니다.
진동 루프 (The Oscillation Loop)
자동 질문 생성기를 구축하던 한 개발자는 자신의 검증/수정 (validator/repair) 파이프라인을 다음과 같이 설명했습니다.
1라운드: 검증기가 문제 A를 표시함 → 수정 단계에서 이를 "수정"함
2라운드: 검증기가 다시 문제 A를 표시함 → 수정 단계에서 다시 "수정"함
3라운드: 검증기가 다시 문제 A를 표시함 → 동일한 수정이 다시 적용됨
...종료 조건 없이 4라운드 이상 지속됨
타임아웃도 없고, 에러도 없습니다. 그저 수렴 (converge)하지 못한 채 토큰 (token)만 소모하며 무한히 앞뒤로 반복될 뿐입니다. 그들은 모델이 반복을 멈추기를 바라며 수정 노드 (repair node)에 이미 시도했던 내역의 히스토리를 제공해 보았습니다. 어느 정도 도움이 되었지만, 루프가 여전히 해결되지 않는 경우가 있었고, 결국 그들이 선택한 해결책은 기술적인 방법보다는 경제적인 방법이었습니다. 수렴하려고 노력하는 것보다 깨진 시도를 폐기하고 처음부터 다시 생성하는 것이 토큰 비용 면에서 더 저렴했기 때문입니다.
수정 간섭 및 연쇄적 패치 실패 (Fix Interference and Cascading Patch Failure)
더 미묘한 변형 사례: 한 번의 수정 주기에서 문제 B와 C를 해결하면, 이전 라운드에서 이미 해결되었던 문제 A가 다시 나타나는 경우입니다. 각각의 개별적인 수정은 국소적으로는 타당해 보이지만, 시스템은 자신의 변경 사항이 서로 어떻게 상호작용하는지에 대한 모델을 가지고 있지 않기 때문에 결국 여러 라운드에 걸쳐 제자리걸음만 반복하게 됩니다.
한 개발자의 실제 근본 원인은 예상보다 훨씬 더 좁고 기이한 것으로 밝혀졌습니다. 네 번의 별도 수정 시도 동안, 모델은 표기법의 드리프트 (notation drift)가 수정되었다고 매번 자신 있게 주장하면서도, 정확히 동일한 잘못된 수학 기호(재귀 공식에서 문자 u 대신 그리스 문자 nu를 작성함)를 계속해서 다시 생성했습니다. 수정 노드 (repair node)는 주변 문구와 서식을 변경했지만, 실제로 고장 난 그 한 줄은 결코 건드리지 않았습니다. 애초에 수정은 모델의 손에 달려 있지 않았습니다.
멀티 스텝 태스크 (multi-step task)가 올바르게 시작되고, 여러 번의 도구 호출 (tool calls)이 성공한 뒤, 에이전트가 "이제 다음 단계로 진행하겠습니다"와 같은 말을 남기며 트레이스 (trace)가 그냥 끝나버리는 경우가 있습니다. 그 뒤로는 아무 일도 일어나지 않습니다. 에러도 없고, 타임아웃 (timeout)도 없으며, 그저 아무런 문제가 없다고 보고되는 미완료된 태스크일 뿐입니다.
생산성으로 위장한 무한 루프 (Infinite Loops)
밀접하게 연관되어 있지만 서로 다른 사례입니다. 동일한 개발자가 1,500개 이상의 세션을 실행한 결과, 콘텐츠 에이전트가 내내 기술적으로는 "작동"하고 있었지만 측정 가능한 진전은 전혀 없었던 13개 세션의 구간을 발견했습니다. 에이전트가 처리했어야 할 실제 큐 (queue)가 계속 가득 차 있었기 때문입니다. 외부에서 보기에는 모든 세션이 활발하고 성공적인 것처럼 보였습니다.
패턴 뒤에 숨겨진 패턴
여기에 구체적이고 테스트 가능한 주장이 있습니다. 위에서 언급한 모든 실패 사례에서, 시스템이 실제로 고장 난 바로 그 순간에 트레이스의 자체 상태 필드 (status field)는 "성공 (success)", "완료 (completed)", "수정됨 (fixed)"와 같이 긍정적인 내용을 나타내고 있었습니다. 이러한 실패 사례 중 단 하나도 에러 플래그 (error flag)를 설정하거나, 예외 (exception)를 발생시키거나, 알림 (alert)을 트리거하지 않았습니다. 이들은 모두 "상태: 성공 (status: success)" 형태의 실패였습니다.
이는 에러, 예외, 또는 200 이외의 상태 코드 (non-200 status codes)를 포착하도록 구축된 모든 모니터링 시스템이 이러한 사례들을 모두 정상적인 실행으로 간주할 것임을 의미합니다. 이를 포착할 수 있는 유일한 방법은 "충돌(crash)이 발생했는가"보다 더 구체적인 것을 확인하는 것입니다. 즉, 도구가 실제로 호출되었는지, 연속된 두 개의 출력이 의심스러울 정도로 동일하게 나왔는지, 최종 상태가 트레이스가 주장하는 발생 내용과 일치하는지 등을 확인해야 합니다. 이는 대부분의 로깅 (logging) 및 모니터링 (monitoring) 설정이 기본적으로 수행하도록 설계된 것과는 다른 범주의 점검입니다. 이것이 바로 이러한 실패들이 데모나 소수의 수동 테스트가 아닌, 수백 번의 실행을 거친 운영 환경 (production)에서 나타나는 경향이 있는 정확한 이유입니다.
내가 이것으로 하고 있는 것
저는 에이전트 실행 트레이스 (execution trace)를 가져와서 — 별도의 계측 (instrumentation)이나 SDK 설치 없이 직접 붙여넣기만 하면 — 위에서 언급한 것과 같은 패턴을 찾는 결정론적 체크 (deterministic checks) 세트와, 더 포착하기 어려운 사례를 위한 의미론적 검사 (semantic pass)를 수행하는 도구를 만들고 있습니다. 직접 트레이스를 붙여넣어 무엇을 찾아내는지 확인해보고 싶다면 무료로 체험해 보세요: https://6jovkucbyygcamzbeksa67.streamlit.app
하지만 솔직히 말해서, 지난 이틀 동안 얻은 더 흥미로운 결과는 도구가 아니었습니다. 그것은 수학 문제 생성기, 광고 운영 에이전트, 고객 지원 봇, 콘텐츠 파이프라인 등 완전히 다른 시스템 전반에 걸쳐 이러한 실패 형태 (failure shapes)가 얼마나 일관되게 나타나는가 하는 점이었습니다. 도메인은 다르지만, 근본 원인 (root causes)은 몇 가지로 동일했습니다.
여러분의 에이전트에서도 이와 유사한 문제를 겪으셨다면, 진심으로 그 이야기를 듣고 싶습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기