← 목차 · 케이스 6
챗봇 인시던트 대응 에이전트 — 제보 한 줄이 PR이 되기까지
2026.04 ~ 2026.08 · Claude Code · Python · pytest · Langfuse · Cloud Run · Supabase
팀 구성 — 개발 1(본인)
봇의 사고 대응을 확인 → 분석 → 수정 → 검증 네 단계 워크플로우로 고정해, 상담사 제보가 올라오면 에이전트가 받아서 돌리게 했습니다. 제가 다른 일을 하는 동안 1차 대응이 먼저 돕니다.
고쳤다는 증명은 고객이 받을 문장으로, 과거에도 샜는지는 소급 스캔으로 하고, 머지와 배포는 사람이 합니다.
이 문서는 배경 → 문제정의 → 해결방안 → 결과 → 정직하게 순서입니다. 케이스 3의 해결방안 ③이 이 케이스의 요약이고, 여기서는 그 워크플로우를 어떻게 만들었고 무엇을 버렸는지를 다룹니다.
배경 — 제보 한 줄에서 시작되는 일
봇의 오답은 상담사 제보로 알게 됩니다. 제보는 대개 Slack 스레드에 캡처 한 장과 한 줄입니다 — "취소 문의에 유효 접수일이 전부 같은 날로 안내돼요." 예약번호도 세션 식별자도 없습니다. 상담사는 그런 것을 모릅니다.
거기서부터 제가 하던 일은 정해져 있었습니다. 처리 기록에서 그 대화를 찾고, 서버 로그와 대화 로그를 열어 대조하고, 원인을 가르고, 인접한 곳에 같은 버그가 없는지 보고, 고치고, 봇이 실제로 보낼 문장으로 확인하고, 과거에도 같은 일이 있었는지 봅니다. 톡톡 봇과 스마트스토어 봇(케이스 3) 모두 같은 절차를 탑니다. 2026년 4월까지는 이 전부를 제가 손에 익은 대로 했습니다 — 매번 조금씩 다르게.
문제정의 — 왜 어려운가
사고 대응은 매번 같은 절차인데, 사람이 하면 순서가 달라지고 단계를 빠뜨립니다. 그렇다고 에이전트에게 끝까지 맡기면 틀린 수정이 그대로 운영에 나갑니다. 어디까지 맡기고 어디에 사람이 설지를 정하는 일이었고, 셋으로 나눴습니다.
① 제보로 사건을 특정하고 원인을 어떻게 가를까
- 제보에는 내부 식별자가 없습니다. 되물으면 대응은 거기서 멈춥니다 — 상담사가 고객에게 다시 묻고, 고객이 답하고, 그동안 봇은 같은 답을 계속 보냅니다.
- 같은 증상도 원인이 갈립니다. 봇이 없는 이름을 말했을 때, 처리 기록에 그 이름이 있었으면 누출이고 없었으면 환각입니다. 시간대를 정반대로 안내했을 때 도구 호출이 한 건도 없었다면 데이터가 아니라 프롬프트 문제입니다. 어디를 보느냐로 원인이 갈리는데, 급하면 한 곳만 보고 적게 됩니다.
② 어디까지 고칠까
- 이번 사고만 막으면 인접에서 다시 납니다. 필드 필터를 하나 고치면 옆 필드가 새고, 프롬프트 한 줄을 고치면 같은 전칭 표현("항상", "모두")이 다른 절에 남아 있습니다.
- 그렇다고 매번 넓게 고치면 라이브 중인 봇의 회귀 위험이 커집니다. 넓힐지 좁힐지를 근거를 남기고 정해야 합니다.
③ 고쳤다는 것을 무엇으로 증명하고, 과거에도 샜는지 어떻게 알까
- 단위 테스트로는 안 끝납니다. 계산 함수 테스트는 통과하는데 고객이 받는 문장은 그대로인 경우가 실제로 있었습니다. 문장은 핸들러 체인 → 에이전트 → 도구 → 프롬프트를 다 지나야 나옵니다.
- 업무시간·마감 같은 시각 의존 버그는 지금 시계로는 재현되지 않습니다. 외부 시스템이 간헐적으로 빈 응답을 준 사건은 외부가 고쳐지면 재현할 길이 없습니다.
- 사건을 닫아도 과거 대화에 같은 패턴이 남아 있으면 운영은 여전히 새는 것입니다. 그 고객들에게는 다시 연락해야 합니다.
해결방안 — 무엇을 했나
전체 그림부터 봅니다. 아래는 사고가 났을 때 도는 흐름입니다(서비스가 도는 동안의 그림은 케이스 3에). 사람이 서는 자리는 둘 — 제보와 리뷰·머지 — 이고, 리뷰나 소급 스캔에서 걸리면 수정으로 되돌아갑니다.
- 접수에 사람이 없습니다. 제보 멘션을 감지해 어느 봇의 일인지 가리고 백그라운드에서 워크플로우를 기동하는 것까지 자동입니다 — 제가 다른 일을 하는 동안 1차 대응이 먼저 돕니다(접수 자동화는 별도의 개인 인프라 → 정직하게).
- 에이전트가 하는 일은 PR 생성까지입니다. 머지·배포·운영 서버 재기동·외부로 나가는 메시지는 권한표에서 사람 쪽에 있습니다(→ 버린 것/산 것).
- 되돌아가는 화살표가 셋입니다. 전/후 입증 실패, 소급 스캔에서 못 막는 사례 발견, 리뷰 불통과 — 전부 수정 단계로 갑니다(→ ②③).
- 중간에 끊겨도 이어서 합니다. 단계마다 상태 파일을 갱신해 세션이 끊기거나 다른 에이전트가 이어받아도 같은 지점에서 재개합니다(→ ①).
- 스마트스토어 봇은 2026-06에 같은 구조를 복제했습니다. 공용은 로그를 읽는 프로브뿐이고, 무엇을 볼지는 봇마다 따로 정의합니다.
① 상담사에게 내부 식별자를 되묻지 않고 발화 조각으로 처리 기록을 거슬러 사건을 특정하게 했고, 로그 세 곳을 전부 대조한 뒤에야 원인을 적게 했습니다
되묻지 않는 것을 규약으로 박았습니다. 1단계는 제보 원문을 그대로 보존하고 채널·시각·식별자를 뽑되, 예약번호·세션 식별자 같은 내부 식별자는 묻지 않습니다. 대신 제보 안에서 처리 기록을 검색할 발화 조각을 최소 하나 확보하는 것이 1단계의 통과 조건입니다. 2단계는 사건일 앞뒤 하루의 처리 기록을 병렬로 전부 내려받아(30페이지 기준 60초 → 약 13초) 그 조각으로 대화를 찾고, 식별자를 역으로 얻어 채웁니다.
세 곳을 다 보기 전에는 원인을 적지 못합니다. 처리 기록·서버 로그·대화 로그가 각각 필수 산출물이고, 원인 문서는 그 셋을 인용해야 합니다. 이유는 위 문제정의 ①의 두 사건입니다 — 하나는 처리 기록에 그 값이 찍혀 있어서 환각이 아니라 누출로, 하나는 도구 호출이 한 건도 없어서 데이터가 아니라 프롬프트 문제로 판명됐습니다. 급할 때 한 곳만 보면 둘 다 "환각"으로 적고 프롬프트를 고쳤을 것이고, 누출은 그대로 남았을 것입니다.
대신 포기한 것 — 속도의 상한입니다. 식별자가 있는 제보도 같은 경로를 탑니다.
② 고치기 전에 이번 버그에 이름을 붙이고 인접한 곳에 같은 버그가 샐 수 있는지 세게 해서, 수정의 기본값을 이번 사고 하나가 아니라 그 인접 지점까지로 두었습니다
확산 가설이 분석 단계의 마지막 필수 항목입니다. 이번 버그의 "추상 클래스"에 이름을 붙이고(예: 상담 시점용 시간대 규칙이 상품 일정의 시간대 의미로 새는 과잉 일반화), 인접 지점 후보를 셋 이상 세고, 각각 가드가 필요한지를 코드에서 찾은 근거를 인용해 판정합니다. 추측으로 채우지 못하게 했고, "과거에 이렇게 고쳤으니 이번에도"라는 선례 추정도 금지했습니다.
수정 범위는 명시적으로 고릅니다. 기본값은 넓은 수정 — 확산 가설에서 "가드 필요"로 판정된 인접 지점까지 한 PR에서 막습니다. 좁게 갈 때는 사유를 남깁니다. 시간대 사건에서는 새 규칙 절을 추가하는 것으로 끝내지 않고 문제의 원래 줄도 함께 고쳤습니다 — 그대로 두면 두 규칙이 충돌한 채 남기 때문입니다.
프롬프트를 고칠 때는 규칙 한 줄을 더하는 데서 멈추지 않습니다. 값이 어디서 오는지 애매한 자리 표시자는 이름 자체를 출처 경로로 바꿔 둡니다 — 규칙을 읽지 않아도 형태가 출처를 강제하게. 그리고 워크플로우 정의에는 과거 사건의 정답을 일부러 넣지 않았습니다. 정답을 미리 쥐여 주면 에이전트가 증거보다 선례에 끌립니다.
대신 포기한 것 — 가장 작은 변경입니다. 라이브 중인 봇에 인접 지점까지 손대는 것은 회귀 면적을 넓힙니다. 그 면적을 ③의 재생과 소급 스캔이 덮습니다.
③ 고쳤다는 증명은 웹훅 페이로드를 그대로 재생해 고객이 받을 문장으로 전후를 입증하게 했고, 과거 기록을 소급 스캔해 못 막는 사례가 나오면 수정으로 되돌아가게 했습니다
단언 대상은 고객이 받는 문장입니다. 사건 당시의 웹훅 페이로드를 앱에 그대로 넣어 핸들러 체인부터 프롬프트까지 다 태우고, 봇이 발신하는 문장을 가로채 확인합니다. 외부 연결 없이 프로세스 안에서 돕니다 — 시계를 사건 시각에 고정하고, 외부 시스템 응답을 사건 당시 값으로 주입하고, 실제 발신만 막습니다. 재생에 쓰는 고객 식별자는 합성값만 허용합니다 — 실제 식별자를 쓰면 분석이 오염되고, 차단이 풀리는 순간 실제 고객에게 발송될 수 있습니다. 그리고 고치기 전 코드로 버그가 재현되는지까지 확인합니다. 재현이 안 되면 고쳐졌다는 것도 증명이 아닙니다. 모델을 실제로 부르므로 결과가 흔들릴 수 있어 3~5회 반복 통과를 기준으로 삼습니다.
재현이 불가능한 클래스는 먼저 가릅니다. 외부 시스템이 간헐적으로 빈 응답을 준 사건은 외부가 고쳐진 뒤에는 실환경에서 다시 만들 수 없습니다. 이 클래스는 처리 기록에서 그때의 응답을 그대로 떼어 고정 응답으로 두고 검증합니다. 실채널 검증은 사람이 원할 때만 합니다.
소급 스캔이 PR 직전의 필수 게이트입니다. 이번 버그 클래스가 과거 30~60일 처리 기록에 있었는지 전수로 셉니다. 0건이면 PR, 잡혔는데 이번 수정이 덮으면 PR 본문에 소급 영향 표(상담사가 다시 연락할 대상), 잡혔는데 못 덮으면 수정 단계로 되돌아가 범위를 넓힙니다. 사건이 닫힌 것처럼 보이는데 운영은 여전히 새는 상태를 막으려는 규칙입니다. 완료의 정의는 "PR 생성 + 소급 스캔 클린 또는 커버"입니다. 제 작업 환경에는 봇 코드를 고친 브랜치가 검증 증거 없이 PR을 만들려 하면 그 자리에서 차단하는 훅도 걸려 있습니다.
| 소급 스캔 | 범위 | 결과 |
|---|---|---|
| 담당자 이름 누출 | 60일 4,304건 | 잔존 0 |
| 시간대 오안내 | 30일 2,093건 | 3건 — 본 사건 1 + 과거 2, 수정으로 커버 |
| 집결 시각 단정 | 30일 2,180건 | 시각을 말한 295건 중 정상 안내 276건은 두고 버그 클래스 19건만 차단 |
| 취소 접수일 음수 | 30일 2,086건 | 58건 전부 수정으로 커버 |
세 번째 줄이 이 게이트의 요점입니다 — "시각을 말한 대화"를 전부 막았으면 멀쩡한 안내 276건이 죽습니다. 버그 클래스를 정확히 정의해야 소급 스캔이 쓸모가 있습니다(→ ②). 사건마다 재생 테스트와 스캐너를 레포에 남깁니다 — 재생 테스트 톡톡 12개·스마트스토어 9개, 스캐너 톡톡 1개·스마트스토어 13개.
대신 포기한 것 — 빠른 테스트입니다. 재생 테스트는 모델을 부르므로 느리고 흔들려서 기본 테스트 런에서 빼고 수동으로 돌립니다.
무엇을 버리고 무엇을 샀나
| 어디 | 버린 것 | 산 것 | 왜 |
|---|---|---|---|
| ① | 속도의 상한 — 식별자가 있어도 같은 경로 | 빠뜨리지 않음 — 세 곳을 다 보기 전엔 원인을 못 적는다 | 한 곳만 보면 누출도 환각으로 적는다. 실제로 그렇게 갈린 사건이 둘이다 |
| ② | 가장 작은 변경 | 인접 지점까지 — 같은 클래스가 옆에서 다시 나지 않는다 | 좁게 고친 뒤 옆에서 다시 나면 사건이 둘이 된다. 넓힌 회귀 면적은 ③이 덮는다 |
| ③ | 빠른 테스트 — 재생은 느리고 흔들린다 | 고객이 받는 문장으로 증명 + 과거까지 확인 | 계산 함수는 통과하는데 문장은 그대로였던 적이 있다 |
| 사람의 자리 | 완전 자동화 — 머지·배포는 에이전트가 안 한다 | 되돌릴 수 있는 실패 — 틀린 수정은 PR에서 멈춘다 | 리뷰 없는 머지는 사고 대응이 사고가 되는 길이다 |
| 스마트스토어 | 공유 추상 — 두 봇을 한 벌로 | 복제 두 벌 — 봇마다 좌표·검증 경로가 다르다 | 공유안은 독립 검증에서 기각됐다. 봇 둘에 추상 계층은 과설계 |
결과 — 실적과 지금 상태
- 두 봇 모두 이 워크플로우로 사고를 처리합니다(톡톡 2026-04부터, 스마트스토어 06부터). 사건마다 남는 것 — 사건 기록(작업 로그 50건), 재생 테스트 톡톡 12개·스마트스토어 9개, 소급 스캐너 톡톡 1개·스마트스토어 13개.
- 소급 스캔 실적 4건(위 표) — 그중 하나는 과거 2건을 찾아 상담사가 다시 연락할 대상이 됐습니다.
- 사람의 자리 — 접수·기동·조사·수정·검증에는 사람이 없고, PR 리뷰·머지·배포·운영 서버 재기동·외부 메시지 발송이 사람 몫입니다. 실채널 검증도 사람이 원할 때만.
- 에이전트로 인한 외부 영향 사고는 없었습니다(증언 · 2026-04~08). 되돌릴 수 없는 행동이 권한 밖이라는 구조가 그 뒤에 있습니다.
- 지금 누가 고치나 — 지금도 제가 고칩니다. 최근 변경은 2026-08입니다.
정직하게 — 한계와 다음 문제
- 시간을 얼마나 줄였는지는 재지 못했습니다. 제보 시각이 기록으로 남지 않아 전후 비교가 안 됩니다. 기록으로 남은 한 건이 25분이었고, 그 전에 손으로 할 때 얼마나 걸렸는지는 기록이 없습니다.
- 시간이 줄었다는 것은 체감뿐입니다. 단건 대응에 들이던 제 시간이 크게 줄었다고 느끼지만 정성적이라 정확하지 않습니다 — 그래서 위 결과에 시간 수치를 쓰지 않았습니다.
- 처음 발견은 여전히 사람입니다. 워크플로우는 제보 뒤부터 돕니다. 자동 탐지는 없고, 스캐너는 알려진 패턴의 사후 스캔입니다.
- 접수 자동화는 제 개인 작업 환경의 상주 프로세스입니다. 제보 감지와 백그라운드 기동은 제 장비에서 돌고, 팀 표준 시스템은 아닙니다. 워크플로우 자체(절차·테스트·스캐너)는 레포에 있어 팀이 같이 씁니다.
- 워크플로우 폴더 산출물은 일부만 남았습니다. 사건 기록 자체는 작업 로그로 전부 남습니다(네이버 봇 사건 기록 50건). 다만 워크플로우가 사건 폴더로 남긴 것은 스마트스토어 12건·톡톡 1건이고, 톡톡의 나머지 사건은 PR 본문·재생 테스트·작업 로그로 남았습니다.
- 재생 테스트는 결정적이지 않습니다. 모델을 실제로 부르므로 같은 입력에 다른 문장이 나올 수 있어 반복 통과를 기준으로 삼았고, 기본 테스트 런에서 빼 두었습니다. 회귀를 자동으로 잡는 장치는 아닙니다.
- 재현 불가 클래스의 검증은 실제 재현이 아닙니다. 고정 응답으로 대체한 검증이라 외부 시스템이 다른 모양으로 실패하면 다시 놓칠 수 있습니다.
- 소급 스캔은 패턴 매칭입니다. 못 잡는 것이 있을 수 있고, 반대로 예외 문맥(공급사 귀책 환불 조항 등)에서 위양성이 나와 예외 규칙을 따로 둡니다.
- 스마트스토어는 복제라 두 벌을 유지합니다. 한쪽을 고치면 다른 쪽도 봐야 합니다. 실제로 톡톡 쪽 정의가 코드 변경을 따라가지 못해 검증 레시피가 죽어 있던 것을, 스마트스토어판을 만들며 발견해 고쳤습니다(2026-06).
- 2026-04 이전 사고는 손으로 처리했습니다. 이 케이스의 소급 실적 4건은 전부 그 뒤의 것입니다.