계획 에이전트가 60% 실패한다
이날은 같은 원인으로 세 가지 버그를 잡은 날이었다.
싸고 빠른 모델이 큰 스키마를 처리하지 못했다.
계획 에이전트가 60% 실패했다.
상태 추출이 전부 빈 값이 나왔다.
프로세스가 종료되지 않았다.
세 가지가 전부 다른 증상인데 원인은 같은 방향에 있었다.
소설 생성 에이전트를 테스트했다.
5개 에피소드 중 3개에서 계획 에이전트가 실패했다.
JSON 파싱이 실패한다는 에러가 났다.
원인을 추적해보니 계획 에이전트에 도구가 달려 있었다.
RAG 검색 도구다.
모델이 도구 호출 단계에서 턴을 끝내버렸다.
최종 JSON 텍스트를 내놓지 않은 거다.
근데 RAG 컨텍스트는 이미 확정적으로 주입되고 있었다.
도구로 또 검색할 필요가 없었다.
도구가 중복이자 해로운 거였다.
해결: 도구를 빼고 단순하게
도구를 뺐다.
그리고 온도를 0.7에서 0.2로 낮췄다.
일관성을 높이는 거다.
조기 종료 조건도 제거했다.
그러니까 계획 에이전트 실패가 3/5에서 0/5로 떨어졌다.
전체 종단 간 테스트도 2/5에서 5/5가 됐다.
상태 추출이 전부 빈 값이다
두 번째 버그는 상태 추출이었다.
에피소드가 끝난 후 상태를 추출하는데 종료 상태가 전부 빈 값이 나왔다.
엔티티랑 관계는 추출되는데 종료 상태만 날아갔다.
원인은 스키마가 너무 컸다.
상태 추출 스키마가 18개 필드였다.
엔티티, 관계, 줄거리, 종료 상태가 전부 하나의 큰 스키마에 들어 있었다.
모델이 큰 스키마의 끝부분을 잘라버렸다.
종료 상태가 스키마 끝에 있었으니까 잘린 거다.
해결: 스키마를 쪼개서 따로 호출
종료 상태를 별도의 작은 호출로 뺐다.
11개 필드짜리 새 스키마를 만들었다.
큰 스키마에서 종료 상태를 제거했다.
그러니까 종료 상태 추출이 0/5에서 5/5가 됐다.
같은 원인, 다른 증상
이 두 버그는 같은 원인에서 왔다.
DeepSeek V4 Flash 모델이 큰 스키마랑 도구 호출을 동시에 잘 못 처리한다.
이 모델은 싸고 빠르다.
2840억 매개변수 모델인데 초당 1222 토큰을 생성한다.
출력 100만 토큰당 28센트다.
가성비가 엄청나다.
근데 약점이 있다.
도구랑 JSON을 동시에 처리하는 게 약하다.
그리고 큰 단일 스키마를 처리하는 것도 약하다.
스키마 끝부분을 잘라버린다.
해결 패턴: 호출을 작고 단순하게
이 약점에 대한 해결 패턴은 일관됐다.
각 LLM 호출을 작고 단순하게 만드는 거다.
도구를 빼라. 온도를 낮춰라.
큰 스키마를 작은 스키마로 쪼개라. 구조화된 출력 대신 일반 텍스트로 받고 직접 파싱해라.
이 패턴을 계획 에이전트에도 적용하고 상태 추출에도 적용했다.
두 경우 모두 같은 패턴으로 해결됐다.
“호출을 작게 쪼개면 된다.”
프로세스가 종료되지 않는다
세 번째 버그는 프로세스가 종료되지 않는 거였다.
배치가 끝났는데 프로세스가 안 끝났다.
1920초 이상 돌고 있었다.
원인은 단순했다.
메인 함수에 프로세스 종료 함수가 없었다.
데이터베이스 연결, HTTP 클라이언트, 타이머 같은 핸들이 이벤트 루프를 잡고 있었다.
명시적으로 종료하지 않으니까 이벤트 루프가 계속 살아 있었다.
해결은 끝에 프로세스 종료 함수(0)를 넣는 거였다.
한 줄이다.
근데 이게 없으면 클라우드 런 타임아웃에 걸린다.
성공인지 실패인지도 ambiguous 해진다.
마무리
이날의 교훈은 “싼 모델의 약점을 알고 그에 맞게 호출을 설계하라”는 거다.
DeepSeek V4 Flash는 싸고 빠르다.
근데 큰 스키마를 못 처리한다.
도구랑 JSON을 동시에 못한다.
이걸 모르고 큰 스키마에 도구를 달면 60% 실패한다.
약점을 알면 우회할 수 있다.
스키마를 쪼개고 도구를 빼고 일반 텍스트로 받고 직접 파싱한다.
이러면 싼 모델의 장점을 살리면서 약점을 피할 수 있다.
비싼 모델을 쓰면 이런 약점이 없다.
근데 비싼 모델은 비용이 든다.
싼 모델의 약점을 알고 우회하는 게 더 경제적이다.
호출을 작게 쪼개는 건 비용도 줄이고 안정성도 높인다.