
도메인 하나가 고장 났을 때, 나머지 다섯 개를 다시 실행하기를 거부했다
요약
TerraformAgent의 효율적인 오류 복구를 위해 실패한 특정 도메인만 재실행하는 최적화 기법을 소개합니다. 전체 파이프라인을 재실행하는 대신, 에러 메시지 내 파일명을 분석하여 문제가 발생한 서브에이전트만 다시 호출함으로써 LLM 비용과 시간을 절감합니다.
핵심 포인트
- TerraformAgent는 6개의 노드로 구성된 멀티 에이전트 시스템임
- 전체 재실행 대신 실패한 도메인만 선별적으로 재시도하여 비용 최적화
- 에러 로그 내 파일명 매칭을 통해 문제의 원인이 된 도메인을 식별
- 조건부 엣지를 활용한 그래프 기반의 효율적인 워크플로우 제어
TerraformAgent는 일반적인 영어로 된 인프라 요청을 검증된 Terraform으로 변환합니다. 6개의 노드로 구성됩니다: 도메인을 계획하는 오케스트레이터 (orchestrator), AWS 문서를 가져오는 리서처 (researcher), 병렬로 코드를 작성하는 도메인 서브에이전트 (domain subagents) 세트, 이를 하나로 엮는 어그리게이터 (aggregator), 여러 LLM으로 이를 검사하는 리뷰어 (reviewer), 그리고 무엇인가가 배포되기 전에 실제 terraform fmt 및 terraform validate를 실행하는 이밸류에이터 (evaluator)입니다.
서브에이전트들은 도메인당 하나씩 병렬로 실행됩니다. 네트워킹 (Networking)은 network.tf를 작성하고, 컴퓨팅 (compute)은 compute.tf를 작성하며, 보안 (security)은 iam.tf와 kms.tf를 작성하는 식입니다. 일반적인 요청에서는 이 중 4개 또는 5개가 동시에 실행됩니다.
그런 다음 이밸류에이터가 정확히 그중 하나에서 문제를 발견합니다. 와일드카드 IAM 액션, network.tf 내의 잘못된 참조, 혹은 단일 파일을 지목하는 검증 실패 등이 발생할 수 있습니다.
가장 쉬운 방법은 전체 파이프라인을 처음부터 다시 루프를 돌려 모든 것을 재생성하는 것입니다. 그것도 작동은 합니다. 하지만 이는 하나의 고장 난 도메인을 고치기 위해 통과한 나머지 4개의 도메인을 버리는 일이며, 각 도메인은 별도의 LLM 호출입니다. 5개 도메인 요청의 경우, 단순한 전체 재실행은 단 한 번의 호출로 해결할 수 있는 문제를 위해 5번의 호출 비용을 발생시키며, 이는 재시도할 때마다 반복됩니다.
저는 그 비용을 지불하고 싶지 않았습니다. 그래서 재시도 시 실제로 실패한 도메인들만 다시 실행하도록 했습니다.
파이프라인 요약
그래프는 이밸류에이터까지 선형적으로 진행되다가 그 이후에 분기됩니다. 이밸류에이터가 실행된 후, 하나의 조건부 엣지 (conditional edge)가 작업을 종료할지 아니면 도메인 서브에이전트로 다시 라우팅할지를 결정합니다. 다시 라우팅될 때, 서브에이전트들은 모든 것을 다시 실행하지 않습니다. 오직 플래그(flag)가 지정된 도메인들만 다시 실행합니다.
핵심 결정: 실패를 특정 도메인의 탓으로 돌리기
이 모든 것은 한 가지 질문에 달려 있습니다. 이밸류에이터나 리뷰어가 문제를 보고했을 때, 어떤 도메인이 그 문제의 소유인가?
저는 파일 이름의 문자열 매칭 (string-matching)을 통해 그 답을 구합니다. 각 도메인은 알려진 파일 세트를 소유합니다:
DOMAIN_FILES: Dict[str, List[str]] = {
"networking": ["network.tf"],
"compute": ["compute.tf"],
...
무언가 실패했을 때, compute_domains_to_retry는 리뷰 피드백(review feedback), fmt 출력 결과, 그리고 validate 출력 결과의 결합된 텍스트를 스캔하여 해당 파일 이름들이 있는지 확인합니다. 발견된 모든 파일 이름은 해당 파일을 소유한 도메인(domain)으로 매핑되며, 오직 그 도메인들만 다시 실행됩니다.
def compute_domains_to_retry(
review_feedback: str, eval_results: Dict[str, Any], active_domains: List[str]
) -> List[str]:
...
만약 validate가 Error in network.tf: invalid reference와 같은 오류로 실패하면, ["networking"]을 반환하며 그 외의 다른 도메인은 다시 실행되지 않습니다. 만약 리뷰어가 BLOCKING_ERROR: iam.tf has a wildcard action이라고 작성하면, ["security"]를 반환합니다. 만약 두 파일이 모두 나타나면, 두 도메인이 모두 반환됩니다.
마지막 세 줄에는 의도적인 폴백(fallback) 로직이 있습니다. 무언가 실패했지만 파일 이름을 특정할 수 없는 경우, 재시도를 건너뛰는 대신 모든 활성 도메인(active domain)을 다시 실행합니다. 저는 책임 소재를 파싱할 수 없다는 이유로 고장 난 코드를 조용히 통과시키기보다는, 차라리 전체 재실행 비용을 지불하는 쪽을 택하겠습니다.
그래프에 연결하기 (Wiring it into the graph)
LangGraph는 조건부 엣지(conditional edge)를 통해 라우팅(routing)을 수행합니다. 평가기(evaluator)가 상태(state)에 domains_to_retry를 기록하면, 라우터 함수가 이를 읽습니다:
def should_retry_or_finish(state: dict) -> str:
if state.get("domains_to_retry"):
return "domain_subagents"
...
재시도 시 서브에이전트(subagents) 노드가 실행되면, 동일한 리스트를 읽어 플래그(flag)가 지정된 도메인에 대해서만 워커(worker)를 가동합니다. 그 외의 모든 것은 이전 출력을 그대로 유지합니다:
active_domains = state.get("active_domains", [])
domains_to_retry = state.get("domains_to_retry", [])
domains_this_pass = domains_to_retry if domains_to_retry else active_domains
...
existing_outputs는 이전 패스(pass)의 복사본으로 시작하므로, 통과한 도메인들은 그대로 유지되고 플래그(flag)가 지정된 도메인들만 덮어씌워집니다. 첫 번째 패스는 모든 것을 실행하고, 재시도 패스(retry pass)는 그 하위 집합(subset)만 실행합니다.
상한선(cap), 그리고 그것이 존재하는 이유
상한선이 없는 재시도 루프(retry loop)는 모델이 해결할 수 없는 도메인에 돈을 낭비하는 방법입니다. 평가기(evaluator)는 카운터를 증가시키며 3회 이후에는 다시 라우팅하는 것을 중단합니다:
retry_count = state.get("retry_count", 0)
if domains_to_retry and retry_count < 3:
next_status = AgentStatus.EVALUATING
...
3이라는 숫자는 튜닝된 값이 아닌 추측치입니다. 이는 모델이 잘못된 참조(reference)나 피드백에서 확인할 수 있는 와일드카드(wildcard)로부터 회복하기에 충분한 횟수이며, 동시에 모델이 근본적으로 해결할 수 없는 도메인이 영원히 맴돌지 않도록 할 만큼 낮은 수치입니다.
내가 다르게 했을 부분
속성 지정(attribution)은 문자열 매칭(string matching) 방식이며, 이것이 약점입니다. 이는 실패 원인이 도메인과 매핑되는 파일명을 명시할 때만 작동합니다. 파일 간 오류(cross-file error), 파일명을 언급하지 않는 유효성 검사(validate) 메시지, 또는 파일 정보가 없는 프로바이더(provider) 수준의 오류는 모두 "모두 다시 실행" 분기로 빠지게 됩니다. 이런 일이 발생하면 선택적 재시도는 조용히 전체 재실행으로 변질되며, 저는 비용 절감 효과를 얻지 못한 채 라우팅 로직에 대한 비용만 지불하게 됩니다.
저는 해피 패스(happy paths)에 대해 실제 테스트 커버리지를 확보하고 있습니다. test_validate_failure_with_file_in_output는 network.tf 오류가 ["networking"]만을 반환함을 확인해주고, test_blocking_with_no_attributable_file_retries_all_active는 폴백(fallback) 시 모든 것을 다시 실행함을 확인해줍니다. 제가 갖지 못한 것은 파일명을 명시하지 않는 실패를 속성 지정할 수 있는 방법입니다. 정직한 다음 단계는 하위 에이전트(subagents)가 자신의 도메인이 태그된 구조화된 결과(structured findings)를 출력하도록 만드는 것입니다. 그렇게 되면 속성 지정 시 문자열을 그레핑(grepping)하는 대신 필드를 읽을 수 있게 됩니다. 이는 이 설계가 아무런 이득을 얻지 못하는 유일한 사례인 "전체 재실행으로의 폴백" 케이스를 줄여줄 것입니다.
요점 (Takeaway)
주변의 모든 것을 재시도하지 말고 실패한 것만 재시도하세요. 그리고 전체적인 비용 절감은 무엇이 실패했는지 이름을 붙일 수 있는지 여부에 달려 있다는 점을 받아들여야 합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기