벙개벙개의 대화방에 AI 봇을 붙였습니다. 모임 이야기를 거들고, 물으면 답하고, 필요하면 검색도 합니다.

만들면서 가장 많이 본 증상은 봇이 아무 말도 안 하는 것이었습니다. 오류도 없고 로그도 깨끗한데 그냥 조용합니다. 원인이 둘이었고 둘 다 성격이 달랐습니다.

하나 — 생각하느라 답을 못 했습니다

답변 길이 상한을 낮게 잡으면 봇이 침묵하거나 이상한 토막만 뱉었습니다. ` `json{ ` 같은 것 몇 글자요.

재 봤습니다. 출력 상한을 120토큰으로 두고 부르면 사고 토큰이 117개, 실제 답변은 빈 문자열, 종료 사유는 "상한 도달"이었습니다. 모델이 답하기 전에 생각을 하는데, 그 생각이 같은 예산에서 나갑니다. 120을 주면 117을 생각에 쓰고 3이 남습니다.

사고를 끄니 같은 120으로 완결된 답이 48토큰에 나왔습니다.

채팅 봇에는 긴 사고가 필요 없습니다. 껐습니다. 답변 예산이 전부 답변으로 가고, 사고 토큰에 붙던 요금도 없어집니다.

둘 — 답이 JSON이 아니었습니다

검색을 켠 뒤로 봇이 전부 침묵했습니다. 로그를 보니 호출은 되는데 전부 "응답 파싱 실패"로 기록되고 있었습니다.

원인은 검색과 JSON 응답 형식이 서로 배타라는 것이었습니다. 검색을 켜면 모델이 JSON을 못 냅니다. 산문으로 옵니다. 그런데 받는 쪽은 JSON만 기다리고 있었으니, 멀쩡한 답이 도착해도 파싱에 실패해서 버려졌습니다.

답은 있는데 못 읽어서 없는 것이 됐습니다.

검색 모드에서 파싱에 실패하면 원문을 그대로 답으로 씁니다. 다만 이걸 무조건 하면 안 됩니다. 모델이 "이 질문에는 답하지 않겠다"고 제대로 JSON으로 거절한 경우까지 원문을 뱉게 되니까요. 파싱 실패일 때만 폴백합니다.

검색 호출 자체가 실패하면(무료 키 한도 같은 것) 검색 없이 다시 부릅니다. 라이브 정보만 못 줄 뿐 답은 합니다. 기능 하나가 막혔다고 전체가 침묵하는 것이 이 봇의 기본 실패 모양이었는데, 그걸 뒤집었습니다.

돈이 새는 곳에 천장을 뒀습니다

봇은 부르지 않아도 가끔 말합니다. 그러면 요금이 조용히 쌓입니다.

하루 답변 횟수에 상한을 뒀습니다. 중요한 건 이 상한이 명시적으로 불러도 안 넘어간다는 점입니다. 확률이나 간격 조절은 "덜 말하게" 하는 장치지만, 이건 예산 천장이라 누가 부르든 넘지 않습니다. 둘은 다른 물건입니다.

답변 하나의 최대 길이도 정할 수 있게 했습니다.

프롬프트에도 울타리를 쳤습니다

한도만으로는 부족합니다. 누가 봇에게 코드를 짜 달라거나 과제를 대신해 달라고 하면 한 번에 예산을 크게 씁니다.

그래서 항상 들어가는 규칙을 뒀습니다. "범용 AI 비서가 아니라 이 방의 대화 참가자"이고, 장문 코드·에세이·번역 대행 같은 역할 밖 요청은 한두 문장으로 정중히 거절하고 본래 역할로 돌아옵니다.

한도가 돈을 막고, 이 규칙이 애초에 그런 요청이 오래 이어지지 않게 합니다. 겹쳐 두는 편이 낫습니다 — 한도만 있으면 하루치를 한 사람이 다 쓰고, 규칙만 있으면 규칙을 우회하는 방법이 늘 있습니다.

부르는 법도 바꿨습니다

처음에는 #으로 불렀는데, 앱에 이미 #으로 시작하는 명령어가 있었습니다. 이름이나 @로 부르게 바꿨습니다.

익명방에서 참여자 목록에 봇이 별칭으로 뜨던 것도 고쳤습니다. 사람은 익명이어야 하지만 봇은 익명일 이유가 없습니다. 오히려 누가 봇인지 모르면 대화가 이상해집니다.

남는 것

두 침묵의 공통점은 실패가 실패처럼 안 보였다는 것입니다. 하나는 모델이 정상 종료했고(예산을 다 썼을 뿐), 하나는 정상 응답을 받았습니다(읽는 쪽이 못 읽었을 뿐).

봇을 붙일 때 제가 준비한 것은 "오류가 나면 알린다"였는데, 정작 필요했던 것은 "답이 없으면 왜 없는지 기록한다"였습니다. 로그에 남은 "응답 파싱 실패" 한 줄이 두 번째 문제를 풀어 줬습니다.