AI 기초

GUIDE / 18번째 글

거절도 설계한다: 안 된다고 말하는 법

거절은 안전 장치의 마지막 동작이면서 사용자가 제품을 평가하는 순간입니다. 켜고 끄는 스위치 대신 다섯 칸 사다리를 쓰는 법, 좋은 거절 문구의 조건, 과잉 거절을 재는 방법을 정리합니다.

PALDYN Team41 MIN READ

지난 글에서 무엇을 막을지 정하는 방법을 다뤘다. 이번 글은 그 뒤다. 막기로 정했을 때, 그것을 사용자에게 어떻게 전달할 것인가.

이 부분이 대개 아무의 일도 아니다. 안전 팀은 판정까지가 자기 일이라고 보고, 제품 팀은 안전 문구는 안전 팀 것이라고 본다. 그래서 거절 문구는 처음 만든 사람이 급하게 쓴 한 문장이 몇 년씩 그대로 나간다. 화면에 나가는 문장 중에 담당자가 없는 문장은 대개 이것 하나뿐이다.

그런데 사용자 입장에서 거절은 제품의 가장 강한 인상이다. 잘 되던 것 백 번보다 안 된 것 한 번이 기억에 남는다. 거절은 사용자가 "이 도구가 나를 어떻게 취급하는가"를 처음으로 확인하는 자리이기도 하다. 잘 된 요청은 결과만 보고 지나가지만, 막힌 요청 앞에서는 사용자가 문장을 끝까지 읽는다.

과잉 거절

가드레일을 다루는 글은 대개 놓친 것만 실패로 센다. 위험한 출력이 나갔다, 개인정보가 새어 나갔다, 이런 것들이다. 실무에서는 반대 방향의 비용이 더 크게 나오는 경우가 많다. 과잉 거절은 정상적인 요청을 위험한 것으로 잘못 판정해 막아 버리는 것을 말한다.

보이지 않는 실패

두 실패의 결정적인 차이는 크기가 아니라 관측 가능성이다.

놓친 것은 반드시 드러난다. 사고가 나고, 스크린샷이 돌고, 법무가 움직이고, 회고가 열린다. 조직 전체가 그 실패를 안다.

과잉 거절은 어디에도 안 남는다. 거절 응답은 HTTP 200이다. 응답 시간도 정상이고 에러율도 0이다. 대시보드의 "요청 처리 성공률 99.8%" 안에 막힌 요청이 성공으로 들어가 있다. 그리고 정상 요청이 막힌 사용자는 대개 항의하지 않는다. 자기가 뭘 잘못 물었나 잠깐 생각하고, 그 기능을 다시 안 쓴다. 항의하는 사람은 이미 그 제품에 애착이 있는 소수다.

이 비대칭이 시간이 지나면서 임계값을 한 방향으로만 민다. 사고가 나면 조인다. 과잉 거절이 늘어도 아무도 모르니 푸는 압력은 생기지 않는다. 그래서 아무도 결정하지 않았는데 반년 뒤에 보면 서비스가 훨씬 답답해져 있다. 이 자리에서 필요한 것은 조심성이 아니라 반대 방향을 재는 지표다. 없으면 조심성은 늘 한쪽으로만 작동한다.

우회의 학습

두 번째 비용은 사용자 쪽에서 생긴다. 정직하게 물으면 막히고 돌려 물으면 되는 것을 한 번 알게 되면, 그 사용자는 앞으로 늘 돌려 묻는다.

여기서 손해가 두 겹으로 온다. 하나는 사용자 경험이고, 다른 하나는 판정기 자체다. 우회를 배운 사용자가 늘면 트래픽에서 의도가 겉으로 드러나는 비율이 줄어든다. 판정기는 이제 의도를 감춘 요청을 보게 되고, 그만큼 판정이 어려워진다. 가드가 자기 입력을 오염시킨다.

로그의 뜻도 같이 흐려진다. "이 라벨의 요청이 지난달보다 줄었다"가 실제로 줄어서인지, 사용자들이 표현을 바꿔서인지 구별할 수 없게 된다. 그리고 정상 사용자가 시행착오로 찾아낸 우회 문구는 커뮤니티에 공유되는 순간 악의적인 사용자에게도 그대로 쓸모가 있다. 우리가 정상 사용자에게 시킨 탐색이 공격자에게 무료로 넘어간다.

오탐률 5%

오탐(false positive)은 정상인 것을 위험하다고 판정한 경우를 말한다. 오탐률 5%는 팀 회의에서 그렇게 나쁘게 들리지 않는다. 이 숫자를 사용자 쪽에서 다시 읽어 보면 인상이 달라진다.

건 단위로는 20건에 1건이다. 하루 10만 건이 오가는 서비스면 매일 5,000건이 잘못 막힌다. 사람 단위로 옮기면 이렇다. 어떤 사용자가 하루에 20건을 쓴다면, 각 건이 독립이라고 볼 때 그 사용자가 하루 안에 한 번 이상 막힐 확률은 1에서 0.95의 20제곱을 뺀 값, 즉 64%다. 주 5일을 쓰는 사용자라면 거의 매일 한 번은 막힌다는 뜻이다.

오탐률 5%는 건으로 셀 때의 이야기다

그래서 지표를 건 단위로만 보면 안 된다. 오탐률 옆에 한 번 이상 거절을 받은 사용자 비율을 같이 둔다. 이 둘은 헤비 유저가 많을수록 크게 벌어진다. 그리고 헤비 유저는 대개 그 제품을 남에게 소개하는 사람들이다.

거절 설계의 목표

그래서 거절 설계의 목표는 "안전하게 막기"가 아니라 "막아야 할 것만 막고, 막을 때 사용자를 길 위에 남겨 두기"다.

목표 문장 하나를 바꾸면 네 가지 결정이 따라 움직인다. 임계값을 하나가 아니라 두 개 두게 된다. 거절 문구에 사람과 번역과 실험 예산을 배정하게 된다. 릴리스 전에 통과해야 하는 검사에 "위험한 것을 막았는가"뿐 아니라 "정상적인 것이 통과하는가"가 들어간다. 사고 회고에서도 "왜 못 막았나"만이 아니라 "이번 조치로 무엇이 같이 막히게 되나"를 함께 묻게 된다.

거절 사다리

거절을 이분법으로 다루면 회색 지대가 전부 한쪽으로 몰린다. 실제로 쓸 수 있는 선택지는 다섯이다.

거절은 켜고 끄는 스위치가 아니라 다섯 칸짜리 사다리다

다섯 칸

  • 그대로 수행 — 위험이 없다고 판정된 경우
  • 줄여서 수행 — 요청의 대부분을 수행하되 민감한 부분만 뺀다. 문서 요약에서 개인정보가 든 문단만 빼고 요약하는 식이다
  • 대안 제시 — 요청한 그것은 못 하지만 인접한 안전한 것을 한다. 특정 약물의 치사량은 답하지 않되 상담 창구를 안내하는 식이다
  • 확인 요청 — 판정이 애매할 때 의도를 되묻는다. "어떤 용도인지 알려 주시면 맞게 도와드릴게요" 한 문장이 회색 지대의 상당 부분을 해결한다
  • 거절 — 이유와 다음 걸음을 남기고 거절한다

두 번째와 네 번째 칸이 없는 제품이 많다. 이 둘이 없으면 조금이라도 위험한 것은 전부 다섯 번째로 가고, 그래서 거절이 갑작스럽고 과하게 느껴진다. 사다리가 다섯 칸이라고 말해 놓고 실제로는 첫 칸과 끝 칸만 쓰는 구현이 흔한데, 그건 사다리가 아니라 여전히 스위치다.

가운데 띠

판정기가 0에서 1 사이의 점수를 낸다고 하자. 지난 글에서 임계값을 하나가 아니라 둘 두라고 했다. 0.3 아래는 그대로 수행, 0.7 위는 거절, 그 사이가 가운데 띠다. 사다리의 가운데 세 칸이 존재하는 이유가 바로 이 띠를 받기 위해서다.

띠를 어느 칸으로 보낼지는 라벨 종류가 정한다. 되돌릴 수 없는 피해로 이어지는 라벨(무기 제조, 자해)은 가운데 띠도 거절이나 대안 제시로 보낸다. 되돌릴 수 있는 것(저작권 우려, 민감한 소재의 창작)은 축소 수행이나 확인 요청으로 보낸다. 이 대응표를 안 만들면 구현하는 사람이 매번 임의로 정하고, 그 임의가 대개 "일단 막자"로 수렴한다.

띠의 크기도 재 둔다. 가운데 띠에 들어오는 요청이 전체의 3%라면, 그 3%를 어디로 보내느냐가 거절률을 3퍼센트포인트만큼 움직인다. 이 숫자를 모르면 임계값을 조정할 때마다 결과를 짐작으로만 예상하게 된다.

축소 수행

줄여서 수행할 때 가장 중요한 것은 무엇을 뺐는지 보이는 것이다. 조용히 빼면 사용자는 결과가 완전하다고 믿는다.

100행짜리 표에서 개인정보가 든 3행을 빼고 요약했는데 그 사실을 말하지 않으면, 사용자는 97행으로 결론을 내고 그 결론을 회의에 가져간다. 이건 거절보다 나쁘다. 거절은 최소한 무언가 안 됐다는 것을 알려 주지만, 조용한 축소는 틀린 것을 맞는 것처럼 넘긴다.

표시는 결과 옆에 한 줄이면 된다. 자리와 개수를 적는다. "개인정보가 포함된 3개 행은 제외했습니다" 정도다. 개인정보 마스킹을 하는 경로에서는 이 한 줄이 특히 중요하다. 지운 자리가 본문 안이라 눈에 안 띄기 때문이다.

축소가 안 되는 경우도 분명히 해 둔다. 민감한 부분을 뺀 뒤 남은 것이 요청의 목적을 못 채우면 그건 축소가 아니라 거절이다. 반쪽짜리 결과를 완성품처럼 내놓는 것이 축소 수행의 유일한 실패 방식이고, 그래서 "뺀 뒤에도 쓸모가 있는가"를 코드가 판단할 수 있어야 이 칸을 쓸 수 있다.

확인 요청의 경계

되묻기는 회색 지대를 크게 줄여 주지만, 위험이 명백한 요청에 쓰면 안 된다. 되묻는 순간 "어떻게 대답하면 통과하는지"를 알려 주는 셈이기 때문이다.

여기에 한 가지 규칙을 더 둔다. 되묻고 받은 답을 그 자체로 판정 근거로 쓰지 않는다. "연구 목적입니다"라는 한 줄이 통과의 열쇠가 되면 그 문장은 곧바로 만능 열쇠가 된다. 되묻기의 목적은 사용자의 자기 신고를 믿는 것이 아니라 요청을 더 구체적으로 만들어 다시 판정하는 것이다. 답을 받은 뒤 원래 요청과 합쳐 판정을 한 번 더 돌린다. 답이 요청의 내용을 실제로 바꾸지 않았다면 판정도 바뀌지 않는다.

횟수도 한 번으로 제한한다. 두 번 되묻는 것은 도와주는 것이 아니라 사용자를 시험하는 것이고, 세 번이면 사용자는 이미 떠났다.

거절 문구

네 요소

거절 문구는 네 가지를 담아야 한다.

  1. 무엇이 걸렸는지 — 요청 전체가 아니라 걸린 부분을 특정한다
  2. 왜 안 되는지 — 한 문장. 정책 조항 낭독이 아니라 사람 말로
  3. 무엇은 되는지 — 인접한 가능한 것
  4. 다음에 무엇을 하면 되는지 — 사용자가 지금 취할 수 있는 행동

같은 거절, 다른 결과

왼쪽 문구는 문법적으로 정중하지만 사용자에게 남기는 정보가 0이다. 무엇이 걸렸는지 모르니 고칠 수도 없고, 다시 시도할 방법도 없다. 남는 선택지는 서비스를 떠나거나 우회를 시도하는 것뿐이다.

오른쪽은 같은 것을 거절하면서 사용자를 길 위에 남긴다. 걸린 대상이 특정되어 있고("주민등록번호가 있는 열"), 다음 행동이 문장 안에 있다("그 열을 지운 파일을 올려 주시면").

넷을 다 담을 수 없는 경우도 있다. 그때 지킬 것은 길이를 정보량에 비례시키는 것이다. 담을 정보가 한 줄이면 한 줄로 쓴다. 세 번째와 네 번째를 못 채웠는데 문장 수를 유지하려고 사과를 늘리면, 읽는 사람은 문장이 길다는 것과 정보가 없다는 것을 동시에 확인하게 된다. 그게 가장 나쁜 조합이다.

대상과 규칙

여기서 조심할 선이 하나 있다. 걸린 대상을 말하는 것과 판정 규칙을 말하는 것은 다르다.

"주민등록번호가 있어서 처리할 수 없습니다"는 대상이다. "숫자 13자리가 하이픈 없이 연속으로 나오면 차단합니다"는 규칙이고, 이건 곧 우회 설명서다. 대상은 말하되 임계값·패턴·모델 이름·라벨 목록은 말하지 않는다.

선이 애매할 때 쓸 수 있는 질문이 하나 있다. 이 문장만 읽고 통과하는 입력을 만들 수 있는가. 만들 수 있으면 규칙을 말한 것이다. "주민등록번호가 있는 열"만 보고는 다음에 무엇을 지워야 할지는 알아도 어떻게 숨겨야 할지는 모른다. "13자리 연속 숫자"를 알려 주면 하이픈을 넣으면 된다는 것까지 함께 알려 준 것이다.

모든 규칙 노출이 같은 무게는 아니다. "요청이 너무 길어 처리할 수 없습니다"는 규칙에 가깝지만 우회의 가치가 없다. 판정 임계값, 어떤 모델이 판정하는지, 라벨이 몇 개이고 무엇인지, 어느 층에서 걸렸는지 — 이 넷은 우회 가치가 높으므로 어떤 표현으로도 내보내지 않는다.

나쁜 거절 다섯 유형

유형 실제로 나오는 문장 문제
설교형 "그런 요청은 타인에게 피해를 줄 수 있으므로 삼가시기 바랍니다" 정상 요청이 걸렸을 때 사용자를 모욕한다
모호형 "해당 요청은 처리할 수 없습니다" 무엇이 걸렸는지 없다
사과 반복형 "죄송합니다. 정말 죄송하지만…" 길어질수록 내용이 줄어든다
가짜 무능형 "저는 그런 기능이 없습니다" 거짓말이다. 못 하는 게 아니라 안 하는 것이고, 들키면 신뢰가 통째로 깎인다
정책 낭독형 이용약관 3조 2항을 그대로 붙여 넣음 사용자는 조항 번호를 모른다

가짜 무능형이 특히 나쁘다. 안 하기로 정한 것을 못 한다고 말하면, 사용자가 조금만 우회해서 그 일이 되는 것을 확인하는 순간 제품이 거짓말을 했다는 사실만 남는다. 그리고 그 확인은 반드시 일어난다 — 같은 모델이 다른 서비스에서 그 일을 하고 있기 때문이다. "안 합니다"와 "못 합니다"를 정확히 구별해서 쓴다. 이 구별의 대가는 문장 하나이고, 안 지켰을 때의 대가는 그 사용자가 제품의 다른 모든 설명도 의심하기 시작하는 것이다.

설교형은 판정이 틀렸을 때 최악이 된다. 정당한 요청을 한 사용자가 훈계를 듣게 된다. 앞에서 계산한 대로 오탐률이 5%라면 그런 경험을 하는 건이 20건 중 1건이고, 하루 20건을 쓰는 사용자로 세면 대부분이 언젠가 한 번은 훈계를 듣는다.

서비스별 톤

거절 문구를 하나로 통일해 두는 팀이 많은데, 서비스 성격에 따라 맞는 톤이 다르다.

  • 업무용 도구 — 짧고 사무적으로. 사과보다 상태와 다음 행동
  • 일반 소비자 서비스 — 부드럽게, 그러나 여전히 구체적으로
  • 아동 대상 — 쉬운 말과 안내. 왜 안 되는지를 이해할 수 있게
  • 전문 영역(의료·법률·금융) — 왜 이 판단을 못 하는지의 근거와, 사람 전문가로 가는 경로

톤을 고를 때 기준으로 삼을 상황은 판정이 맞았을 때가 아니라 틀렸을 때다. 판정이 맞았다면 어떤 톤이든 큰 문제가 안 된다. 나쁜 조합은 오판에서 나온다. 설교형과 소비자 서비스가 만나면 정당한 사용자가 도덕적 훈계를 듣는다. 사과 반복형과 업무용 도구가 만나면 급한 사람이 사과문을 세 문장 읽고 나서야 아무 정보도 없다는 것을 안다. 아동 서비스에서 모호형이 나오면 아이는 자기가 나쁜 짓을 했다고 이해한다.

사유 코드

기록에 남길 칸

사유 코드는 사용자에게 보이는 문구와 별개로 내부 기록에 남기는 짧은 식별자다. 어느 라벨이 걸렸는지, 어느 층에서 걸렸는지, 점수가 얼마였는지를 코드 하나와 몇 개의 값으로 남긴다.

최소한 이만큼은 남긴다. 걸린 라벨, 걸린 층(입력 필터인지 출력 검증인지 도구 호출 단계인지), 판정 점수, 그때의 임계값, 정책 버전, 사다리의 어느 칸으로 갔는지, 어떤 문구 템플릿이 나갔는지, 그리고 요청 식별자.

임계값과 정책 버전까지 남기는 이유가 있다. 어느 날 거절이 갑자기 늘었을 때, 임계값을 0.7에서 0.6으로 내려서인지 라벨 정의가 넓어져서인지 트래픽 성격이 바뀌어서인지를 구별해야 한다. 이 세 값이 로그에 없으면 배포 이력과 대시보드를 눈으로 맞춰 보는 수밖에 없고, 그 작업은 하루가 지나면 거의 불가능해진다. 사유 코드가 없으면 "거절이 늘었다"는 사실만 알고 어디를 고칠지는 모른다.

프롬프트 바깥

거절 문구를 모델 프롬프트 안에 문장으로 적어 두면 안 된다. 이유가 넷이다.

  • 문구를 한 글자 바꾸려면 프롬프트를 배포해야 한다
  • 언어별로 다르게 쓰기 어렵다
  • 모델이 상황에 따라 문구를 조금씩 바꿔서 낸다 — 그러면 무엇이 나갔는지 기록이 안 남는다
  • 모델이 없는 정책을 지어낸다 — "회사 정책 7조에 따라"라는 문장을 모델이 만들어 내면 그것도 그대로 대외 커뮤니케이션이다

네 번째가 가장 늦게 발견된다. 프롬프트에 "정책에 따라 거절하세요"라고만 적어 두면 모델은 그럴듯한 근거를 만들어 붙이는 쪽으로 움직인다. 그리고 존재하지 않는 조항 번호는 문의가 들어오기 전까지 아무도 모른다.

템플릿과 값

사유 코드는 시스템이 정하고, 문구는 그 코드에 대응하는 템플릿에서 꺼낸다. 모델은 판정과 근거 추출까지만 하고, 사용자에게 보이는 문장은 애플리케이션 층에서 만든다. 그래야 문구를 비교 실험할 수 있고, 언어별로 다르게 쓸 수 있고, 무엇이 나갔는지 정확히 기록된다.

문구를 프롬프트가 쥐면 무엇이 나갔는지 남지 않는다

예외가 하나 있다. 거절 문구에 요청의 구체적인 내용이 들어가야 좋은 경우다("올려 주신 파일의 세 번째 열"). 이때는 템플릿에 채울 값만 모델이 뽑고 문장 틀은 여전히 코드가 쥔다. 다만 그 값도 그대로 쓰면 안 된다. 모델이 뽑아 온 문자열을 검증 없이 화면에 넣으면 원문의 민감한 내용이 거절 문구를 타고 그대로 나갈 수 있다. 길이를 제한하고, 가능하면 자유 문자열이 아니라 열 번호나 라벨 같은 정해진 형태만 받는다.

언어별 문구는 번역이 아니라 다시 쓴다. 사과 표현이 갖는 무게가 언어마다 다르기 때문이다. 한국어에서 자연스러운 정중함을 영어로 직역하면 과하게 굽신거리거나 반대로 사무적으로 읽힌다. 템플릿을 코드가 쥐고 있으면 언어마다 다른 문장을 두는 것이 자연스럽게 가능해지지만, 프롬프트에 문구가 박혀 있으면 번역기를 한 번 통과시키는 것 말고는 방법이 없다.

사용자 노출 여부

사유 코드 자체는 사용자에게 보여 주지 않는다. 코드 이름은 대개 라벨 체계를 그대로 드러내고, 사용자에게는 아무 뜻도 없다.

대신 참조 번호를 보여 준다. 요청 식별자를 짧게 줄인 문자열 하나면 된다. 이게 있으면 문의가 "제 요청 번호 7K2M이 막혔습니다"로 들어오고, 담당자는 그 번호로 로그를 찾아 어느 라벨이 어느 점수로 걸렸는지 곧바로 본다. 없으면 문의는 "어제 뭔가 안 됐어요"로 들어오고, 그 문의는 대부분 재현되지 않은 채로 닫힌다. 참조 번호 한 줄이 이의 제기 절차 전체를 가능하게 만드는 최소 조건이다.

과잉 거절 측정

거절 설계에서 가장 어려운 부분은 문구가 아니라 측정이다. 앞에서 본 대로 놓친 것은 사고로 드러나지만 과하게 막은 것은 아무 데도 안 나타난다. 그래서 재는 장치를 따로 만들어야 한다.

정상 요청 세트

우리 서비스에서 반드시 되어야 하는 요청 200~500건을 모아 둔 것이 정상 요청 세트다. 이 세트의 통과율이 거절률의 반대 지표다.

만드는 절차가 중요하다. 팀이 회의실에서 상상해 만든 세트는 팀이 이미 아는 실패만 담는다. 실제 트래픽에서 뽑는다. 거절된 요청을 무작위로 표본 추출하고, 사람이 하나씩 정상인지 아닌지 판정하고, 정상인데 거절된 것을 세트에 넣는다. 이 과정은 세트를 만드는 일이면서 동시에 지금 오탐률이 얼마인지를 처음으로 재는 일이기도 하다.

경계에 가까운 것을 일부러 채운다. 의료 서비스라면 약물 부작용 질문, 창작 서비스라면 갈등이 있는 장면 묘사처럼 정당하지만 판정기가 헷갈릴 만한 것들이다. 이런 요청이 세트의 3분의 1은 되어야 한다. 전부 쉬운 요청이면 통과율이 늘 100%로 나와서 지표가 움직이지 않고, 움직이지 않는 지표는 아무 결정도 못 바꾼다.

세트는 늙는다. 정책이 바뀌면 어제의 정답이 오늘 오답이 된다. 분기마다 한 번은 세트를 다시 열어 여전히 "되어야 하는 것"이 맞는지 확인한다.

거절 이후 행동

거절을 받은 사용자가 다음에 무엇을 했는지를 셋으로 가른다. 같은 요청을 다시 물었는가, 표현이나 내용을 바꿔 다시 시도했는가, 그냥 떠났는가.

로그에서는 거절 응답 뒤 같은 세션 안에서 일정 시간 안에 다음 요청이 있었는지로 가른다. 없으면 이탈이다. 세 번째 비율이 높은 거절 문구는 정보가 부족한 문구다.

이 지표는 반드시 문구 템플릿 단위로 낸다. 전체 이탈률은 평균이라 아무것도 안 알려 준다. 템플릿별로 갈라 보면 어떤 문구가 사람을 막다른 곳에 세우는지가 한 번에 보인다. 그리고 이것이 문구를 프롬프트가 아니라 코드가 쥐어야 하는 가장 실용적인 이유다 — 무엇이 나갔는지 기록에 남아 있지 않으면 이 표를 만들 수 없다.

재시도의 두 얼굴

재시도율이 높은 것은 좋은 신호일 수도 나쁜 신호일 수도 있다. 사용자가 요청을 정당하게 고쳐서 다시 오는 것은 좋고, 같은 요청을 표현만 바꿔 계속 시도하는 것은 탈옥 탐색에 가깝다.

둘을 가르는 것은 원본과의 차이다. 요청의 내용이 바뀌었는가, 포장만 바뀌었는가. 개인정보가 든 열을 지우고 다시 올린 것은 내용이 바뀐 것이다. 같은 문장을 존댓말로 바꾸거나 "가상의 시나리오에서"를 앞에 붙인 것은 포장만 바뀐 것이다. 기계적으로는 의미 표현끼리의 유사도는 아주 높은데 표면 문자열은 매번 다른 패턴을 찾으면 된다.

연속 거절 횟수도 함께 본다. 한 세션에서 세 번 이상 연속으로 거절이 나면 그때부터는 같은 문구를 반복해 보여 주는 것이 의미가 없다. 정당한 사용자라면 이미 문구가 도움이 안 된다는 뜻이고, 탐색 중이라면 반복이 곧 탐색의 재료다.

사유 코드 분포

지표 좋을 때 나쁠 때 무엇을 의심하는가
정상 세트 통과율 높다 임계값이 너무 낮거나 라벨 정의가 넓다
거절 후 재시도율 적당히 높다 0에 가까우면 문구가 길을 안 남긴 것
거절 후 이탈률 낮다 높으면 막다른 거절이다
사유 코드 분포 고르다 한 코드가 압도적이면 그 라벨이 과하게 넓다

마지막 줄이 가장 빨리 결론을 준다. 한 달 거절이 4,000건인데 그중 3,100건이 한 코드에서 나왔다면, 그 코드가 붙은 요청 100건을 뽑아 사람이 읽어 본다. 절반 이상이 정상이면 고칠 곳은 문구가 아니라 그 라벨의 정의다.

분포가 고르다면 라벨 정의는 대체로 맞다는 뜻이고, 그때는 임계값이나 문구를 손볼 차례다. 분포는 시계열로도 본다. 특정 날짜부터 한 코드가 튀어 오르면 그날 나간 배포를 먼저 확인한다.

이의 제기

측정 장치를 다 만들어도 개별 오판은 계속 난다. 그 오판을 사용자가 알려 줄 수 있는 경로와, 알려 준 것을 되돌리는 절차가 마지막 조각이다.

신고 경로

거절 화면에 "이건 정상 요청입니다" 한 줄을 둔다. 여기서 중요한 것은 버튼이 아니라 그 신고가 어디로 가느냐다.

신고가 고객 문의 큐로만 가면 담당자가 한 건씩 사과하고 닫는 것으로 끝난다. 같은 오판은 다음 주에도 그대로 난다. 신고는 문의 큐가 아니라 정상 요청 세트의 후보 큐로 흘러가야 한다. 신고가 들어오면 사람이 정상인지 아닌지 판정하고, 정상이면 세트에 넣고, 그 세트가 다음 임계값 조정과 라벨 정의 수정의 근거가 된다. 이 경로가 있으면 오판 하나가 그 부류 전체를 고치는 재료가 된다.

신고 버튼에는 부작용이 하나 있다. 악의적인 사용자도 누른다. 그래서 규칙은 명확하다 — 신고는 데이터이지 판정이 아니다. 신고했다고 그 요청이 자동으로 통과되지 않는다. 통과 여부는 사람이 보거나 정책이 바뀐 뒤에 정해진다.

되돌리기 범위

오판으로 확인됐을 때 어디까지 되돌릴지를 미리 정해 둔다. 되돌리기 비용이 조치마다 완전히 다르기 때문이다.

싸게 되돌릴 수 있는 것이 있다. 거절된 요청을 다시 처리해 주는 것, 실패한 요청에 차감된 사용량을 복구하는 것 정도다.

되돌리기 어려운 것도 있다. 계정 정지, 신고 이력, 관리자에게 이미 나간 알림, 외부 기관에 보고된 기록이다. 이런 것들은 취소해도 흔적이 남고, 사용자 입장에서는 취소됐다는 사실이 겪은 일을 지워 주지 않는다.

그래서 조치의 강도를 정할 때 위험도만 보지 말고 되돌리기 비용을 함께 본다. 한 번의 자동 판정으로 계정을 정지하지 않는다는 규칙은 여기서 나온다. 판정기는 5%쯤 틀리고, 계정 정지는 0%만큼만 틀려야 하는 조치다.

사람 검토

그래서 되돌리기 어려운 조치에는 사람 검토를 끼운다. 요청 거절, 축소 수행, 확인 요청까지는 자동으로 한다. 계정에 손을 대는 것부터는 사람이 본 뒤에 한다.

되돌리기 비용이 사람 검토의 경계를 정한다

검토 큐에는 처리 시한을 붙인다. 며칠씩 걸리는 검토는 없는 검토와 같다. 그 사이에 사용자는 이미 떠났거나 다른 도구로 갔다.

검토 자체도 지표를 갖는다. 사람이 본 건 중 자동 판정이 뒤집힌 비율이다. 이 비율이 높으면 자동 판정이 과하다는 뜻이고, 반대로 0에 가까우면 검토를 붙일 필요가 없었던 자리이거나 검토가 형식적으로 돌고 있다는 뜻이다. 어느 쪽인지는 뒤집힌 건들을 직접 읽어 보면 금방 갈린다.

정리

  • 놓친 것만 실패가 아니다. 과하게 막은 것도 실패이고, 이쪽은 지표에 안 나타난다
  • 오탐률은 건 단위와 사람 단위로 함께 읽는다. 건 기준 5%가 사람 기준으로는 훨씬 크게 보인다
  • 거절은 다섯 칸 사다리다. 줄여서 수행과 확인 요청 칸을 비워 두지 않는다
  • 축소 수행은 무엇을 뺐는지 반드시 알린다. 조용한 축소는 거절보다 나쁘다
  • 좋은 거절 문구는 걸린 대상·이유·가능한 것·다음 행동 넷을 담고, 길이가 정보량에 비례한다
  • 대상은 말하되 판정 규칙은 말하지 않는다. 그 문장만 보고 통과하는 입력을 만들 수 있으면 규칙을 말한 것이다
  • "못 합니다"와 "안 합니다"를 구별한다
  • 정상 요청 세트와 사유 코드가 없으면 과잉 거절은 영원히 안 보인다
  • 문구는 프롬프트가 아니라 코드가 쥔다. 값만 모델이 채운다
  • 신고는 문의 큐가 아니라 정상 요청 세트로 흘러가야 하고, 되돌리기 어려운 조치에는 사람이 낀다

여기까지가 가드를 세우고 판정하고 전달하는 이야기였다. 다음 글은 이 전부가 실제로 작동하는지 확인하는 방법을 다룬다. 가드레일은 코드처럼 테스트할 수 있지만, 보통의 단위 테스트와는 다른 성질을 갖는다.


읽어주셔서 감사합니다. 😊

LATEST

AI 기초의 최신 글

AI 기초2026.08.19

탈옥 방어는 기능이 아니라 운영이다

탈옥 방어를 한 번 세우는 기능으로 다루면 반드시 뒤처집니다. 뚫린 것을 관측하고 재현하고 고쳐서 회귀 테스트에 고정하는 고리를 어떻게 돌리는지, 그리고 무엇을 지표로 삼아야 하는지 정리합니다.

40 MIN
AI 기초2026.08.18

개인정보를 지우는 자리는 출력이 아니다

출력에서 마스킹하면 이미 늦습니다. 원문이 어디에 복사되는지, 자리표로 치환하고 되돌리는 방식이 왜 실무의 기본값인지, 그리고 지운 것을 어떻게 검증하는지를 정리합니다.

45 MIN
AI 기초2026.08.18

출력 가드: 답이 사용자에게 닿기 전에

출력 검사는 답이 다 만들어진 뒤에야 돌 수 있습니다. 무엇을 어떤 순서로 검사할지, 걸렸을 때 재생성·수정·차단 중 무엇을 고를지, 스트리밍과 어떻게 함께 쓸지를 정리합니다.

37 MIN