당신의 에이전트는 모든 것을 확인했습니다. 하지만 여전히 틀렸습니다.
요약
멀티 에이전트 워크플로우 운영 중 발생한 실패 사례를 통해 에이전트 시스템의 검증 한계를 분석합니다. 재시도 루프의 무용지물화와 데이터 불일치 문제를 통해 단순한 로직 구현을 넘어선 선제적 대응과 정밀한 검증의 중요성을 강조합니다.
핵심 포인트
- 반응형 복구 로직(Retry loop)이 해결하지 못하는 예외 상황 존재
- API 오류 코드 없이 실패하는 시나리오에 대한 선제적 세션 관리 필요
- 컴파일 에러가 없는 논리적 데이터 불일치(좌표 매핑 오류) 위험성
- 멀티 에이전트 파이프라인 내 검증(Verification) 단계의 고도화 필요
저는 몇 달 동안 멀티 에이전트 (multi-agent) 개발 워크플로우를 운영해 왔습니다. 하나의 모델은 설계를 작성하고, 다른 모델은 코드를 생성하며, 세 번째 모델은 구현 내용을 검토하고, 제가 최종 결과를 승인하는 방식입니다. 대부분의 경우 잘 작동합니다. 하지만 최근 세 건의 실패 사례가 이 파이프라인을 통해 감지되지 않은 채 통과되었으며, 이들은 세 사례가 모두 드러나기 전까지는 제가 명확히 설명할 수 없었던 공통된 패턴을 공유하고 있습니다.
이것들은 대단한 버그는 아닙니다. 일단 발견하고 나면 개별적인 근본 원인은 단순합니다. 이 글을 쓸 가치가 있는 이유는 이러한 종류의 실패 구조에 대해 무엇을 드러내는지, 그리고 프로덕션 (production)에 도달하기 전에 어떤 종류의 검증 (verification)이 이를 잡아낼 수 있었는지에 있습니다.
사례 1: 결코 실행되지 않은 재시도 루프 (retry loop)
하나의 ETL 파이프라인이 제3자 ERP API로부터 PostgreSQL 데이터 웨어하우스 (warehouse)로 데이터를 동기화했습니다. 5분에서 15분마다 세션 기반 인증 메커니즘을 사용하여 증분 변경 사항을 가져왔습니다. 코드에는 표준적인 재시도 루프 (retry loop)가 있었습니다. 3번의 시도, 지수 백오프 (exponential backoff), 그리고 실패 시 다음 시도 전에 재인증을 수행하기 위한 resetLogin() 호출이 포함되어 있었습니다. 이 방식은 몇 주 동안 잘 작동했습니다.
그러던 어느 오후, 모든 동기화 작업이 동일한 오류로 실패하기 시작했습니다: COPY from stdin failed: expected N values, got 1. 파이프라인은 오류를 보고하고 재시도했습니다. 그리고 또 재시도했습니다. 그리고 누군가 알아차릴 때까지 몇 시간 동안 계속해서 실패했습니다.
코드에는 재시도 루프가 존재했습니다. resetLogin()도 코드에 존재했습니다. 하지만 둘 중 어느 것도 실행된 적이 없었습니다. 원인을 파악하기 위해 조사가 필요했습니다. ERP 세션이 약 8.5시간 후에 만료되었을 때, API는 401이나 403 또는 그 어떤 HTTP 오류도 반환하지 않았습니다. 대신 `[[
해결책은 재시도 로직 (retry logic)을 개선하는 것이 아니었습니다. 해결책은 4시간 주기의 티커 (ticker)를 통해 선제적으로 세션을 갱신하는 백그라운드 고루틴 (goroutine)이었습니다. 즉, 반응형 복구 경로 (reactive recovery path)를 무의미하게 만드는 키프라이브 (keepalive) 방식이었습니다. 정확히 이 시나리오를 처리해야 했던 시스템의 구성 요소인 재시도 루프 (retry loop)는 시작부터 무용지물이었습니다.
사례 2: 존재하지 않았던 좌표들
데스크톱 도구의 코드 생성기 (code generator)가 임베디드 마이크로컨트롤러 (embedded microcontroller)용 C 소스 파일을 생성했습니다. 시각적인 UI 디자인이 주어지면, 이 도구는 위젯 매크로 정의가 포함된 헤더 파일 (header files), 이벤트 디스패치 (event dispatch) 함수가 포함된 컨텍스트 파일 (context file), 그리고 외부 플래시 메모리 (external flash memory)의 이미지 주소를 매핑하는 리소스 인덱스 (resource indices)를 출력했습니다. 개발자들은 이 출력물을 펌웨어 (firmware)로 컴파일하고 칩에 플래싱 (flashing)했지만, 이상한 현상을 목격했습니다. 동적 텍스트 위젯들이 디자인에서 배치된 위치와 상관없이 항상 (0, 0) 위치에서 렌더링되는 것이었습니다.
생성된 코드는 경고 없이 컴파일되었습니다. 링커 (linker)는 참조된 모든 심볼 (symbols)을 찾아냈습니다. 어떤 함수도 에러를 반환하지 않았습니다. 모든 것이 완벽해 보였습니다. 하지만 각 위젯의 ID를 x, y, 너비, 높이에 매핑하는 별도의 생성 파일인 런타임 위젯 테이블 (runtime widget table)이 전혀 작성되지 않았습니다. 해당 테이블을 생성하는 메서드 (method)를 호출한 사람이 아무도 없었던 것입니다. 헤더 파일에는 x/y 필드를 가진 위젯 구조체 (struct) 타입이 선언되어 있었습니다. 리소스 매니페스트 (resource manifest)에는 좌표 정보가 담겨 있었습니다. 설계 문서에는 위젯 테이블 형식이 기술되어 있었습니다. 하지만 코드 생성기는 단순히 그 파일을 출력하지 않았을 뿐입니다.
생성된 출력물은 내용의 정확성에 대해 검토되었습니다. 검토자는 헤더 매크로 (header macros)가 올바른 리소스 주소를 가리키고 있는지, 이벤트 라우팅 (event routing)이 정확한지, 페이지 정의가 설계와 일치하는지를 확인했습니다. 그 모든 것은 사실이었습니다. 하지만 아무도 질문하지 않았습니다. "우리가 생성해야 할 모든 파일들을 생성하고 있는가?"
사례 3: 주석이 아니라 지침을 읽으세요
새로운 마이크로컨트롤러(microcontroller)를 위한 SDK를 통합하고 있었습니다. 벤더(vendor)는 칩의 SDK가 포함된 zip 파일을 제공했고, 디렉토리 구조는 올바르게 보였습니다: 스타트업 파일(startup file), 특정 칩 모델을 위한 링커 스크립트(linker scripts), SPI, DMA, GPIO를 위한 주변 장치 드라이버(peripheral drivers) — 제가 예상했던 모든 것이 있었습니다.
startup.S 파일은 다음과 같은 주석 헤더로 시작되었습니다:
; GCC for CSKY Embedded Processors
CSKY는 이 칩이 사용하는 것이 아닌 다른 임베디드 CPU 아키텍처(architecture)입니다. 데이터시트(datasheet)에는 이 칩이 RISC-V라고 명확히 명시되어 있었습니다. 하지만 SDK 디렉토리 이름은 이 특정 칩 제품군(family)의 이름으로 되어 있었고, 하위 디렉토리들도 일치했으며, 스타트업 파일은 있어야 할 위치에 정확히 놓여 있었습니다. 주석은 한 가지를 말하고 있었고, 디렉토리 이름은 다른 것을 말하고 있었습니다. 무엇이 맞을까요?
두 가지 사실이 이 문제를 해결해 주었습니다. 첫째, 벤더가 실수로 동일한 제품군 내의 다른 칩(CSKY를 사용하는 칩)용 SDK를 전달한 것이었습니다. 디렉토리 이름이 오해를 불러일으켰던 것입니다. 둘째, 올바른 SDK가 도착했을 때, 그 startup.S 파일의 헤더에도 동일한 CSKY 주석이 들어 있었습니다. 벤더가 제3자 툴체인(third-party toolchain)에서 템플릿을 복사한 후 한 번도 정리하지 않았기 때문입니다. 실제 명령어(instructions)는 다른 이야기를 하고 있었습니다:
csrw mtvec, a0 ; RISC-V: write machine trap vector
la sp, g_top_irqstack
jal main ; RISC-V: jump-and-link to main
가장 간단한 검증 방법은 grep을 사용하는 것이었습니다: grep -c "csrw\|mret" startup.S를 실행하자 3이 반환되었습니다. 그것은 RISC-V였습니다. 주석은 화석(fossil)에 불과했습니다.
지능의 문제가 아니다
제 워크플로(workflow)에 있는 세 에이전트(agent) — 설계자(designer), 구현자(implementer), 검토자(reviewer) — 모두 개별적으로는 옳은 행동을 했습니다. 설계자는 작업에 부합하는 사양(spec)을 작성했습니다. 구현자는 사양에 부합하는 코드를 생성했습니다. 검토자는 그 일치 여부를 확인했습니다. 모든 검증 단계가 통과되었습니다. 시스템은 일관되었습니다. 일관되게 틀렸을 뿐입니다.
이것은 지능의 문제가 아닙니다. 이것은 경계(boundary)의 문제입니다. 에이전트는 주어진 컨텍스트(context) 내에서 요청받은 일을 정확히 수행합니다. 아무도 그 컨텍스트가 어디서 끝나는지 알려주지 않았습니다.
사례 1(Case 1)에서 컨텍스트(context)는 "HTTP 호출이 에러를 반환했는지 확인하라"였습니다. 아무도 "성공적인 응답이 실제로 데이터를 포함하고 있는지도 확인하라"고 말하지 않았습니다.
사례 2(Case 2)에서 컨텍스트는 "생성된 파일들이 올바른지 확인하라"였습니다. 아무도 "모든 필수 출력 파일이 존재하는지도 확인하라"고 말하지 않았습니다.
사례 3(Case 3)에서 컨텍스트는 "SDK 디렉토리 구조와 문서가 정확하다고 신뢰하라"였습니다. 아무도 "실제 명령어 스트림(instruction stream)과 비교하여 아키텍처 주장을 검증하라"고 말하지 않았습니다.
모든 사례에서 에이전트는 자신이 작업 범위(scope)라고 이해한 것에 대해 완전한 검증을 수행했습니다. 하지만 그 범위가 틀렸습니다. 그리고 에이전트가 확인하는 것과 실제로 참이어야 하는 것 사이의 그 간극 — 이것이 바로 AI 보조 워크플로우(AI-assisted workflow)가 구조적으로 인지하지 못하는 실패 모드(failure mode)입니다.
이것은 에이전트의 버그가 아닙니다. 워크플로우에 누락된 레이어(layer)입니다. 에이전트는 당신이 검증하라고 지시한 것들을 검증할 것입니다. 검증해야 할 새로운 범주의 항목들을 독립적으로 찾아내지는 않을 것입니다. 그러한 발견은 여전히 엔지니어링(engineering)의 영역입니다.
체크리스트에 포함되어야 할 사항
그 내용은 다음과 같습니다.
1. 의미론적 출력 검증 (Semantic output validation): 단순히 성공을 반환했는지가 아니라, 성공이 무엇을 의미하는지를 확인하십시오.
API 호출, 함수 반환, 시스템 호출 — 각각은 성공 경로(success path)와 실패 경로(failure path)를 가집니다. 하지만 어떤 시스템들은 성공 내부에 실패를 인코딩(encode)하기도 합니다. 모든 외부 호출 후에 자문하십시오: 반환된 데이터가 데이터처럼 보이는가, 아니면 오류를 가장한 것처럼 보이는가? 행(row)의 개수를 확인하십시오. 구조를 확인하십시오. 에러 키워드를 스캔하십시오. 만약 호출자가 예상되는 컬럼(column) 수보다 훨씬 적은 1개의 행을 받았다면, err가 무엇이라고 말하든 상관없이 그 데이터는 이미 의심스러운 상태입니다.
2. 출력 완결성 (Output completeness): 존재하는 것들이 올바른지뿐만 아니라, 모든 필수 결과물(artifacts)이 존재하는지 검증하십시오.
코드 생성 (Code generation)은 여러 개의 파일을 생성합니다. 어떤 파일들은 출력 디렉토리에 보입니다. 어떤 파일들은 있어야 하지만 보이지 않습니다. 생성 결과를 수락하기 전에, 예상되는 출력 파일들을 열거하고 각 파일이 비어 있지 않은 파일로 존재하는지 확인하십시오. "생성된 파일들이 올바른 것 같습니다"라는 말을 "모든 생성된 파일이 존재합니다"라는 말의 대체제로 수락하지 마십시오.
3. 정답 검증 (Ground-truth verification): 기계에 의해 테스트될 수 있는 모든 기술적 주장(technical claim)에 대해서는, 기계로 테스트하십시오.
주석은 거짓말을 할 수 있습니다. 파일 이름은 거짓말을 할 수 있습니다. 디렉토리 구조도 거짓말을 할 수 있습니다. .S 파일의 CPU 아키텍처는 헤더 주석에 적힌 내용이 아니라, 어셈블러(assembler)가 방출할 명령어 그 자체입니다. 바이너리 파일의 형식은 README가 주장하는 내용이 아니라, 특정 오프셋(offset)에 있는 바이트가 포함하고 있는 내용 그 자체입니다. 어떤 에이전트 워크플로(agent workflow)가 기술적 가정에 기반하여 설계 결정을 내리기 전에, 해당 아티팩트(artifact)를 직접 읽는 명령어를 통해 그 가정을 검증하십시오. 확인되지 않은 주장을 그대로 전달하지 마십시오.
이 세 가지 체크포인트 중 어느 것도 극적인 것은 아닙니다. 각각은 워크플로를 실질적으로 변경하지 않고도 작업 정의(task definition)나 검토 단계(review stage)에 추가할 수 있을 만큼 충분히 작습니다. 하지만 이들을 결합하면 제가 현재 여러 번 목격한 세 가지 실패 모드(failure modes)를 방어할 수 있습니다: 성공적인 응답 내부의 감지되지 않은 실패, 코드 생성기(code generator)로부터의 누락된 출력, 그리고 툴체인 아티팩트(toolchain artifact)에 의해 반복되는 잘못된 주장입니다.
에이전트는 스스로에게 "내가 무엇을 더 확인해야 할까?"라고 묻지 않을 것입니다. "내가 확인한 모든 것이 맞더라도 무엇이 더 잘못될 수 있을까?"라는 질문은 에이전트가 자신의 컨텍스트(context) 내부에서 스스로 생성할 수 있는 것이 아닙니다. 그것은 외부에서 와야 합니다. 설계되어야 합니다. 멀티 에이전트 워크플로(multi-agent workflow)를 설정할 때, 당신은 단순히 코드를 위한 파이프라인을 설계하는 것이 아닙니다. 당신은 각 에이전트가 무엇을 정답(ground truth)으로 취급할지에 대한 경계를 설계하는 것입니다. 만약 당신이 그 경계에 검증 체크포인트를 배치하지 않는다면, 아무도 배치하지 않을 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기