두 AI가 서로를 탓했다 — 프로젝트마다 에이전트를 붙였을 때 생기는 일
프로젝트마다 붙인 에이전트가 각자 자기 로그만 보고 결백을 주장해서, 양쪽 로그를 겹쳐 놓고 결론을 낸 기록
하려던 것
공시 요약 배치에서 동시 50건을 던졌더니 애플리케이션 API가 뻗었다. 원인을 찾아야 했다.
여기까지는 평범한 장애다. 특이했던 건 디버깅 구조였다.
어떻게 시켰나
나는 두 프로젝트에 Claude Code 세션을 각각 띄워 놓고 쓴다. 하나는 모델 서버를 보고, 하나는 요약 애플리케이션을 본다. 장애가 나서 각자에게 물었다.
그리고 각자 자기 로그만 보고 각자 결백을 주장했다.
공시 쪽에 물으니 모델 서버가 못 버틴 거라고 했다. 그래서 서버 쪽에 가져갔더니 그쪽은 요청을 다 처리했다고 했다. 다시 공시 쪽으로 갔다.
“서버 쪽 로그 확인해 볼 수 있어? 걔는 네 잘못이래. 전혀 문제 없었다던데.”
나는 두 AI 사이에서 메신저를 하고 있었다. 각자 자기 컨테이너 로그, 자기 코드베이스, 자기 대화 히스토리만 갖고 있고, 상대 시스템은 “저쪽에서 그렇게 말한다더라”라는 전언으로만 안다.
그리고 전언은 각색된다. 내가 A의 말을 B에게 옮기면 B는 그걸 자기 맥락에서 해석해 반박을 만들고, 그 반박을 다시 A에게 옮기면 A가 또 자기 맥락에서 해석한다. 두 번 왕복하니 원래 무슨 말이었는지 나도 헷갈렸다. 사람 조직에서 부서 간에 벌어지는 일이랑 똑같은데, 속도만 훨씬 빠르다.
어디서 막혔나
원인 찾기부터 막혔다. 그라파나에는 500이 잔뜩 찍혀 있는데 애플리케이션 로그에는 해당 시간대가 비어 있었다. 시간대를 몇 번 바꿔 물어봐도 “그 시각 로그가 없다”는 답만 돌아왔다.
로그는 유실되거나 레벨에 걸려 안 남을 수 있지만, 카운터가 500을 셌으면 500이 난 거다. 그러면 지표를 믿어야 한다. 그런데 에이전트는 자기가 확인할 수 있는 쪽(로그 없음)을 근거로 삼으려 했다.
그리고 자꾸 선을 넘었다. 공시 쪽 에이전트가 장애를 풀겠다고 다른 프로젝트의 서버 설정(max_model_len)을 건드리려 했다. 그쪽 사정은 모르는 채로. 그 값은 이미 한 번 조정된 상태였고, 짧게 잡았다가 문서가 길어서 에러 났던 이력이 붙어 있다.
에이전트는 자기 프로젝트 안에서는 놀랄 만큼 유능한데, 경계 밖 사정을 모른다는 걸 스스로 모른다.
어떻게 풀었나
양쪽 로그를 겹쳐 놓고 나서 결론이 나왔다.
“나는 전체 처리량을 늘리고 싶은 거지, 타임아웃을 늘리자는 게 아니야. 서버에선 에러가 없거든. 요청한 거 다 처리해 줬어. 네가 못 받아온 거지.”
모델 서버 로그에는 에러가 없다. 요청은 다 처리됐다. 받는 쪽이 못 받은 거다. 응답 하나에 수십 초 걸리는 요청을 50개 동시에 HTTP로 물고 있으니 커넥션이 먼저 마른다.
해결책을 찾는 데서도 한참 돌았다. 처음 나온 안은 타임아웃을 늘리자는 것이었는데, 그건 증상만 미루는 거다. 그다음은 비동기 콜백. 그러면 콜백이 유실될 때를 대비해야 하고, 그러려면 상태를 저장할 DB가 필요하고, 그럼 이 프로젝트에 DB가 들어온다. gRPC 얘기까지 나왔다. 구조물이 계속 늘었다.
그러다 제일 단순한 데서 멈췄다. 요청을 스트리밍으로 받으면 커넥션이 놀지 않으니 타임아웃에 안 걸린다. 바이트가 계속 흐르니까 죽은 커넥션으로 오해받을 일이 없다. 큐도 DB도 필요 없다.
한 시간 동안 설계한 것 중에 남은 게 없다. 이런 식으로 흘러가는 게 처음이 아니다.
배운 점
- 장애가 두 시스템에 걸치면 한쪽 에이전트에게 양쪽 로그 접근을 줘라. 전언으로 옮기지 말 것. 전언은 각색된다.
- 에이전트의 “저쪽 문제입니다”는 결론이 아니라 자기가 볼 수 있는 범위의 끝이다.
- 지표와 로그가 다른 말을 하면 지표를 믿어라. 로그는 안 남을 수 있어도 카운터는 안 틀린다.
- 에이전트가 남의 프로젝트 설정에 손대려 하면 막아라. 그 값에는 대개 여기서는 안 보이는 이력이 붙어 있다.
- 커넥션이 마르는 문제와 처리량이 부족한 문제는 별개다. 전자는 스트리밍으로 풀리고 후자는 아직 안 건드렸다.