Issue 01중국 AI
AC POST
중국 AI 목록
掘金2026년 9월 19일 14:05중국어 → 한국어

DeepSeek Harness 해부 3편: 답변 완료 후에도 Agent가 도는 이유

답변이 다 표시됐는데도 Agent가 계속 실행되거나 완료를 말한 뒤 다시 모델을 호출하는 현상을 무한 루프로 단정하지 않고 원인을 살펴본다.

중국어 원문을 AI로 번역했습니다. 고유명사와 수치는 원문 표기를 우선하며, 중요한 판단에는 아래 출처 원문을 함께 확인하세요.

이런 화면을 본 적이 있을 것이다. 답변은 이미 완전히 표시되었는데 Agent는 여전히 실행 중이거나, 분명히 "완료했다"고 말했는데 바로 이어서 다시 모델 요청을 보내는 경우다.

섣불리 무한 루프에 빠졌다고 단정하지 말자. 모델 출력의 종료와 작업의 종료는 애초에 같은 판단이 아니다.

이 글은 계속해서 DeepSeek Harness 0.1.6-alpha.2, 커밋 ddefc45fbc7f8e46dd73185e68295696d1297887 을 읽는다. agent.ts 의 turn() 과 step() 이 각각 실행 주기와 단계를 처리하며, 화면 상태는 이 두 함수의 반환 조건과 대조해 봐야 한다.

먼저 세 단위를 정리하자.

단위는 여기서 무엇을 뜻하는가 request / attempt 한 번의 모델 호출 시도로, 성공할 수도 있고 실패 후 재시도될 수도 있다 step 이미 진입한 한 번의 루프 단계로, 요청 시도와 그에 대응하는 도구 실행을 포함한다 turn turn/start 에서 turn/end 까지의 한 번의 실행 주기로, 여러 step 을 포함할 수 있다

따라서 요청 하나가 반환되었다고 해서 이 step 의 동작이 모두 끝난 것이 아니며, step 하나가 끝났다고 해서 turn 이 끝날 수 있는 것도 아니다. 또한 같은 step 이 복구가 허용된 요청 오류를 만나면 모델 호출을 재시도할 수도 있다.

도구 결과는 보통 다음 모델 요청으로 들어간다. 도구가 스스로 종료하거나, 취소되거나, 예외가 발생하면 다른 분기를 탄다.

가장 흔한 도구 경로부터 보자. 모델이 도구 호출 하나를 생성하고, 스트림이 끝난 뒤 Harness 가 assistant 메시지를 제출하고, 도구를 실행하고, 결과를 기록한다. step() 이 반환하는 것은 일반적인 completed 가 아니라, 현재 turn 이 계속되어야 함을 나타내는 null 이다. 도구 실행이 명확한 종료 신호를 주지 않는 한 그렇다.

보통 도구가 실행된 뒤에는 그 결과를 처리하기 위한 모델 요청이 한 번 더 필요하다. 첫 번째 글의 읽기·수정·테스트 흐름이 그렇다. 세 번의 도구 요청 뒤에 최종 설명이 한 번 더 있어, 총 네 번의 요청이 된다.

다만 이는 기본 경로일 뿐이며, 매번 도구 호출 뒤의 필연적 동작은 아니다. 소스에는 도구가 스스로 turn 을 conclude 하는 분기가 있고, 취소와 예외도 과정을 종료시킬 수 있다. 스케줄러를 구현할 때는 구두 설명을 따라 무조건적인 continue 를 작성해서는 안 된다.

이제 도구 호출이 없는 응답을 보자. 모델이 완전한 텍스트 한 단락을 바로 내놓았다고 가정하면, step() 은 실제로 completed 를 반환할 수 있다. 그러나 turn() 은 수신함도 확인한다. 다음 단계에 속하는 입력이 있는가?

이 확인은 모델 생성 중에 도착한 새로운 사실을 받아내기 위한 것이다. 예를 들어 파일 감시 플러그인이 의존성이 방금 변경된 것을 발견하거나, 사용자가 "겸사겸사 빈 배열도 고려해 줘"라고 한마디 덧붙이는 경우다. 이런 입력이 현재 작업에 영향을 주어야 한다면, 런타임은 모델이 방금 한 finish 라는 말만으로 전체 turn 을 종료해서는 안 된다.

소스에는 모습은 비슷하지만 의미가 다른 세 개의 입구가 있다.

followup(message) → next-turn, 그리고 깨운다 steer(message) → next-step, 그리고 깨운다 inject(message) → next-step, 능동적으로 깨우지 않음

followup() 은 이후 turn 으로 밀어 넣는 입력을 표현하고, steer() 는 다음 한 단계에 영향을 줄 입력을 표현한다. inject() 는 플러그인이 컨텍스트를 보충하기에 더 적합하다. 실행 중에는 이어서 소비될 수 있고, 유휴 상태에서는 먼저 남겨 두었다가 다음 번 깨울 때 처리한다. 배경에 변화가 있을 때마다 사용자를 대신해 작업을 시작하는 것이 아니다.

이 세 입구는 입력이 루프를 능동적으로 깨울지 여부도 결정한다. 파일 감시자가 변화를 발견할 때마다 모델을 깨우면, Agent 가 스스로 파일을 고치는 것이 다시 감시자를 발동시켜 호출이 한 바퀴 더 늘어나거나 심지어 연쇄적으로 자기 자극이 일어날 수 있다. inject 를 선택하면 모든 감시 이벤트를 새로운 깨우기 요청으로 만드는 것을 피할 수 있지만, 플러그인 스스로도 중복 정보를 무한히 주입하지 않도록 주의해야 한다.

step 이 이미 종료 조건을 갖추었고 다음 단계의 수신함이 비어 있을 때, 루프는 직렬 확장점 하나를 더 내보낸다. agent/turn-stopping 이다.

이것은 "이미 끝났다"는 알림이 아니다. 그것은 종료 전의 창에 위치하며, 리스너는 여전히 다음 단계 입력을 추가할 수 있다. 리스너가 완료될 때까지 기다린 뒤, 루프는 수신함과 취소 신호를 한 번 더 확인하고 나서야 종료 여부를 결정한다.

저장소에는 통제된 테스트가 하나 있다. 모델 스크립트가 step 1, step 2, step 3 을 차례로 답하고, 세 번의 응답 모두 순수 텍스트이며 도구 호출이 없다. 리스너는 stopping 창에서 완료된 단계 수를 확인하고, 세 단계에 미치지 못하면 steer('continue') 한다.

최종 단언은 같은 turn 안에서 세 단계가 돌고 세 번의 요청이 발생한다는 것이다. 이 테스트는 우리가 실제로 실행해 통과시켰다. 그것이 증명하는 것은 런타임이 종료 전에 작업을 덧붙이는 것을 허용한다는 것이지, 모델이 갑자기 스스로 두 라운드를 더 생각하기로 결정했다는 것이 아니다.

이 확장점은 루프를 멈추지 못하게 만들 수도 있다. 어떤 플러그인이 stopping 마다 새 메시지를 밀어 넣는다면, 모델이 "완료"라고 몇 번을 말하든 turn 이 반드시 끝난다고 할 수 없다. 이때는 프롬프트만 고쳐서 모델이 "답이 끝나면 멈추도록" 하는 것으로는 문제를 해결할 수 없다. 진짜 구동 조건은 여기에 있지 않다.

점검해야 할 것은 이것이다. 입력을 누가 추가했는지, 언제 추가했는지, 종료 조건이 있는지, 그리고 외부에 예산과 사용자 취소 메커니즘이 있는지. 확장점이 제공하는 것은 "계속 실행" 능력이며, 계속 조건과 정지 조건은 비즈니스 로직이 스스로 설정해야 한다.

더 은밀한 "한 번 더 도는" 경우도 있다. 모델 요청 실패 후의 재시도다. step() 내부에는 자체적으로 한 겹의 시도 루프가 더 있다. firstAttempt 는 이 step 의 사용자 메시지가 한 번만 인정되게 하여, 재시도마다 반복 추가되지 않게 한다.

같은 단계 입력, 같은 전처리가 유효한 출력을 얻기까지 여러 호출 시도를 거칠 수 있다. 그래서 비용을 계산할 때는 request 를 세고, 업무 진척을 볼 때는 step 을 세며, 사용자의 이번 한 라운드가 어떻게 수습되는지를 볼 때는 turn 을 센다. 세 숫자를 뭉쳐 "Agent 가 N번 호출했다"는 한마디로 만들면, 가장 설명이 필요한 정보가 사라진다.

로그도 유효한 assistant 메시지와 모델 이력에 성공적으로 남지 못한 attempt 를 구분한다. 재시도에서 실패한 조각은 진단 자료로 남길 수 있지만, 함부로 "이미 모델에게 말해 준" 이력으로 삼아서는 안 된다. 네 번째 글에서 이어서 이 일을 뜯어볼 것이다.

소스에는 max-tokens 도 기록되어 있다. 이 종료 사실은 turn 안에서 점착성을 갖는다. 어느 step 이 상한에 도달하면, 이후에 정상적으로 완료되더라도 전체 turn 의 결과를 조용히 일반 completed 로 되돌릴 수 없다.

최종 답변이 완전하다고 해서 이전 단계에 잘림이 없었던 것은 아니므로, 이 상태는 보존되어야 한다. 호출자는 실행이 무엇을 겪었는지 알아야 사용자에게 알릴지, 보완 작업을 요구할지, 진단을 기록할지 결정할 수 있다. 이는 token 상한에 도달한 작업이 모두 실패했다는 뜻이 아니라, 중요한 실행 사실을 숨겨서는 안 된다는 뜻이다.

제품 차원에서 보면, 하나의 "생각 중" 상태로 모든 단계를 덮지 않는 것이 좋다. 모델이 생성 중인 것, 도구가 아직 실행 중인 것, 종료 전 확장이 아직 처리되지 않은 것은 세 가지 서로 다른 대기다. 사용자가 완전한 텍스트를 본 뒤에도 기다리고 있다면, 적어도 시스템이 무엇을 기다리는지는 알 기회가 있어야 한다.

다만 이런 상태를 표시하려면 런타임의 사실을 구독해야 하며, 텍스트 내용으로 추측해서는 안 된다. 답변에 "완료됨"이 나타나는 것은 상태 전환이 되지 않으며, "확인해 보겠습니다"가 나타난다고 해서 뒤에서 정말 도구가 실행되었다고 보장되지도 않는다.

이 글과 관련된 loop.spec.ts 는 총 65개 테스트, inbox.spec.ts 는 총 7개 테스트로, 모두 고정 커밋에서 통과했다. 핵심은 정지 창에서 다음 단계를 추가하는 것, 유휴 상태의 주입이 turn 을 시작하지 않는 것, 그리고 도구 실행 중의 주입 순서다. 이들은 통제된 응답으로 스케줄링 의미를 검증하며, 실제 모델의 작업 성공률을 측정하지는 않는다.

turn 이 끝나는지는 모델 동작, 도구 종료 신호, 수신함, 종료 전 확장과 취소 상태에 달려 있다. 지속 실행을 조사할 때는 이 조건들을 항목별로 점검해야 하며, 모델의 종료 어구만 바꾸는 것으로는 런타임 조건을 바꿀 수 없다.

소스와 검증:

- 세 개의 입력 입구와 그 대상 큐

- turn 의 종료 확인

- step 내의 요청 시도와 도구 분기

- 정지 창에서 세 단계를 추가하는 테스트