본문으로 건너뛰기
채택됨중급AI 에이전트

tool-calling 에이전트가 존재하지 않는 함수를 반복 호출하며 같은 실패를 되풀이합니다. 무한 재시도를 막는 루프는 어떻게 설계해야 할까요?

tool-calling 방식으로 에이전트를 만들고 있는데, 모델이 정의하지 않은 함수명을 호출해 실패하면 그다음 턴에서도 똑같은 함수를 또 부릅니다. 단순히 예외를 잡아 재시도(try/except로 다시 호출)만 걸어놨더니 같은 호출을 max_retries까지 그대로 반복하다 끝나버립니다. 백오프도 넣어봤지만 없는 함수라 몇 초 기다린다고 달라지는 게 없더군요.

실패했을 때 모델이 스스로 다른 접근을 하도록 유도하려면 루프를 어떻게 구성해야 할까요? 그리고 어떤 실패는 재시도하고 어떤 실패는 하지 말아야 하는지 기준도 궁금합니다.

답변 1

  • 채택된 답변

    무한 반복의 원인은 재시도 자체가 아니라 같은 컨텍스트로 같은 호출을 다시 시킨 데 있습니다. 실패를 삼키고 그대로 다시 부르면 모델 입력이 안 바뀌고, 입력이 같으면 출력도 같습니다. 순서대로 설계하면 잡힙니다.

    1. 실패를 구조화해 관측: 예외를 삼키지 말고 {error, message, available} 형태로 만들어 tool_result로 되돌립니다. 자연어 "에러남"으로 던졌을 때보다 파싱 가능한 신호로 던졌을 때 다음 턴의 교정률이 눈에 띄게 높았습니다.
    2. 에러를 컨텍스트에 되먹임: 그 결과를 messages에 push해야 다음 턴이 실패를 봅니다. 이게 빠지면 반성 프롬프트를 넣어도 소용없습니다.
    3. 반복 시그니처 감지로 무한 루프 차단: hash(name+args) 카운트가 임계(예: 3)를 넘으면 실행하지 말고 강제 중단 신호를 되먹입니다. 전체 step 상한만 두면 같은 호출로 상한을 다 태웁니다.
    4. 툴 화이트리스트: 레지스트리에 없는 이름은 실행 전 거부합니다. tools 스키마를 줘도 모델은 없는 함수명을 환각합니다.
    for (let s = 0; s < MAX; s++) {
      const c = await llm(messages, { tools });
      if (!c.tool) return c.text;
    
      if (!reg.has(c.tool.name)) {                    // 환각 차단
        push(err(c.id, "unknown_tool", [...reg.keys()]));
        continue;
      }
    
      const sig = hash(c.tool.name + JSON.stringify(c.tool.args));
      seen[sig] = (seen[sig] ?? 0) + 1;
      if (seen[sig] >= 3) {                           // 같은 호출 3회=실행 없이 중단
        push(err(c.id, "repeated_failure", "다른 인자나 다른 도구로 접근하세요"));
        continue;
      }
    
      try {
        push(ok(c.id, await reg.get(c.tool.name)(c.tool.args)));
        delete seen[sig];                             // 성공하면 카운터 리셋
      } catch (e) {
        if (transient(e)) { await sleep(backoff(seen[sig])); continue; }
        push(err(c.id, e.code, e.message));           // 결정적 실패는 백오프 X
      }
    }
    

    핵심은 3번 분기가 임계 도달 시 tool 실행을 건너뛰고(continue) 실패 신호만 되먹이는 부분입니다. 여기서 continue를 빼면 "repeated_failure"를 알려주고도 같은 호출을 그대로 실행해 버려서 차단이 무의미해집니다.

    when-NOT: 백오프 재시도는 429·5xx·timeout 같은 일시적 실패 전용입니다. 없는 함수·400·401 같은 결정적 실패는 입력을 안 바꾸면 몇 번을 돌려도 결과가 같으니, 재시도가 아니라 반성·에스컬레이트 대상입니다. 반성 단계는 실패했을 때만 켜서 토큰과 지연을 아끼고, args-hash는 의미가 같고 문자열만 다른 반복은 못 잡으니 step 상한을 안전망으로 병행하세요.