에이전트가 도구를 호출하지 않고 텍스트로 뱉는 현상의 원인 추적

26년 06월 15일

도구 호출이 텍스트로 나온다

이날은 에이전트가 도구를 안 부르는 현상을 추적한 날이다.

도구 호출을 텍스트로 출력하기만 하고 실제로 실행하지 않았다.

원인을 찾은 줄 알았는데, 가설이 깨졌다.

근데 그 과정 자체가 중요했다.

증상이 이상했다.

에이전트가 도구 호출을 텍스트로 출력했다.

“이 파일을 편집하겠습니다”라고 하면서 실제 편집 도구를 호출하지 않는다.

도구 호출 XML을 텍스트로 적어서 보여주기만 한다.

그리고 턴을 끝낸다.

“계속해”라고 하면 같은 텍스트를 또 출력한다.

무한 루프다.

재미있는 건 Claude에서도 GLM에서도 같은 현상이 나왔다는 거다.

모델이 바뀌어도 같다.

그러니까 모델 품질 문제가 아니다.

인프라 문제다.

첫 번째 가설: TODO 연속 훅

첫 번째 의심은 TODO 연속 훅이었다.

프레임워크가 에이전트한테 “TODO를 끝내라”고 계속 압박하는 훅이 있다.

이게 매 턴마다 발사해서 에이전트가 압박을 받고 제대로 호출을 못 하는 게 아닌가. 측정해봤다.

142개 메시지 중에 3번만 발사했다.

7.5%다.

“매 턴마다”가 아니었다.

가설이 틀렸다.

이건 증상을 악화시키는 요인일 수는 있어도 근본 원인은 아니다.

두 번째 가설: prefill 에러

진짜 원인을 찾은 줄 알았다.

assistant message prefill 에러였다.

대화가 assistant 메시지로 끝나면, 일부 모델이 거부한다.

“대화는 user 메시지로 끝나야 합니다”라는 에러를 낸다.

근데 프레임워크가 대화를 assistant 메시지로 끝나게 만들고 있었다.

이건 명확했다.

프레임워크가 assistant 메시지를 끝에 붙이는 훅을 가지고 있었다.

이 훅이 메시지 구조를 깨서 모델이 도구 호출을 제대로 반환하지 못하는 거다.

왜 프레임워크가 안 잡았나

프레임워크에 prefill을 막는 가드가 있었다.

근데 이 가드의 허용 목록이 너무 좁았다.

첫째, 프로바이더 확인이 좁았다.

사내 게이트웨이가 목록에 없었다.

그래서 게이트웨이를 통하는 요청은 가드를 통과하지 못했다.

둘째, 모델 접두사 확인도 좁았다.

특정 모델 접두사만 목록에 있었다.

그래서 다른 Claude 모델도 가드를 못 탔다.

이건 구조적 결함이었다.

특정 프로바이더만의 문제가 아니라, 더 넓은 범위의 문제였다.

가설이 깨졌다

그런데 같은 날 저녁에 가설이 깨졌다.

같은 현상이 GLM-5.2에서도 재현됐다.

GLM은 Anthropic API를 안 쓴다.

prefill 에러가 날 수 없는 환경이다.

근데 같은 증상이 나왔다.

이러면 prefill이 단일 원인일 수 없다.

prefill 에러는 진짜지만 Anthropic 환경에서만 발생하는 거다.

더 넓은 범위의 현상은 다른 원인이 있다.

다중 원인 모델

그래서 원인 모델을 수정했다.

세 가지 원인이 겹쳐 있다.

첫째, 모델의 도구 호출 직렬화 결함이다.

짧은 호출은 성공하는데, 길고 복잡한 호출은 형식을 잃는다.

둘째, 런타임 파서 불일치다.

모델은 호출을 생성하는데, 런타임이 텍스트로 해석한다.

셋째, prefill 에러다.

Anthropic 환경에서만 발생하는 추가 원인이다.

모델의 인식과 인프라의 제어

이 사건에서 가장 중요한 통찰은 “모델의 인식과 인프라의 제어는 다르다”는 거다.

실패는 4단계로 일어난다.

첫째, 의도는 모델에 있다.

둘째, 메시지 구조는 프레임워크가 만든다.

셋째, API 전송은 프로바이더가 처리한다.

넷째, 도구 실행은 런타임이 한다.

모델의 인식은 첫 번째 단계에만 있다.

근데 파손은 둘째, 셋째, 넷째 단계에서 일어난다.

모델이 완벽하게 도구 호출을 알아도, 인프라가 끊기면 호출이 안 된다.

전화에 비유하면 이렇다.

전화 거는 법을 완벽히 알아도, 전화선이 끊기면 통화가 안 된다.

모델은 “전화 거는 법”을 아는데, “전화선”이 끊긴 거다.

마무리

내가 이날 배운 핵심은 다음과 같다.

이날의 핵심은 “가설을 세우되, 그 가설을 깨보는 데이터도 찾아라”는 거다.

prefill 에러 가설은 명확했다.

증거도 있었다.

프레임워크 소스에서 원인을 찾았다.

근데 GLM에서 같은 현상이 재현되면서 가설이 깨졌다.

prefill이 단일 원인일 수 없다는 증거가 나온 거다.

가설이 깨지면 당황할 수 있다.

근데 깨진 가설은 더 정확한 이해로 이끈다.

단일 원인이라고 생각했던 게, 사실은 다중 원인이었다.

prefill은 진짜 원인이지만 전체 그림의 일부일 뿐이었다.

과학이 이렇게 발전한다.

가설을 세우고 검증하고 깨지면 더 나은 가설로 간다.

이 과정을 건너뛰면, 틀린 가설을 가진 채로 끝난다.

깨지는 가설이 무서운 게 아니라, 깨지지 않는 가설을 확인하지 않는 게 무섭다.