
AI 채팅 봇에 '근거가 없으면 대답하지 않기'를 구현하다. 할루시네이션 (Hallucination)을 포기하고 멈추기
요약
BYOK(Bring Your Own Key) 방식의 AI 채팅 봇 개발 시 발생할 수 있는 보안 및 비용 문제를 다룹니다. API 키 암호화 저장 방식과 사용자의 과도한 API 호출로 인한 비용 폭탄을 방지하기 위한 설계 전략을 공유합니다.
핵심 포인트
- API 키 유출 방지를 위해 AES-256-GCM 암호화 및 salt 활용 권장
- 단순 Rate Limit을 넘어선 일일 사용량 상한제 도입 필요
- 동시성 문제를 방지하기 위한 DB 측 원자적(Atomic) 카운팅 구현
- 배포 시 사용자의 비용 손실을 방지하는 설계의 중요성
BYOK(사용자가 자신의 API 키를 입력하여 사용하는 방식)로 AI 채팅 봇을 만들고 있을 때, 구현 도중 문득 손이 멈췄습니다.
만약 누군가 악의를 품고 자동화된 요청을 밤새도록 계속 보낸다면 어떻게 될까. 그 API 청구서는 내가 아니라, 이 플러그인을 설치해 준 사이트 운영자에게 전달됩니다. 내가 작성한 코드 때문에 타인이 자신도 모르는 사이에 과금된다. 상상만 해도 소름이 끼쳤습니다.
그때 깨달았습니다. AI를 자신의 프로덕트에 실어서 배포한다는 것은, 단순히 기능을 추가하는 문제가 아니라 타인의 손실에 책임을 지는 문제라는 것을.
직접 AI를 사용할 때는 실패하더라도 나만 손해를 봅니다. 토큰 (Token)을 낭비하든, 이상한 답변이 돌아오든 피해자는 나뿐입니다. 하지만 배포하는 입장이 되면 손해를 보는 쪽은 나를 믿고 설치해 준 사람이 됩니다. 이 비대칭성이 설계의 무게를 바꾸었습니다.
만들면서 알게 된 것은 피할 수 없는 세 가지가 있다는 사실이었습니다. 사용자의 키를 어떻게 보관할 것인가. 사용자의 지갑에 어떻게 천장을 만들 것인가. AI의 출력을 어디까지 신뢰하지 않을 것인가. 이 모두는 '나중에 추가할 기능'이 아니라 '처음에 결정해야 할 설계'이며, 게다가 소개 페이지에는 적히지 않는 종류의 것들입니다. 차례대로 무엇을 선택했고 어디에서 타협했는지 적어보겠습니다.
AI를 구동하려면 API 키가 필요합니다. 여기서 첫 번째 분기점이 있었습니다. 내가 일괄적으로 지불할 것인가, 아니면 사용자에게 자신의 키를 입력하게 할 것인가.
BYOK 방식을 택한 이유는, 내가 대리 지불을 하면 사용량이 늘어날수록 내가 적자가 나기 때문입니다. 개인 개발자가 타인의 사용량을 자신의 지갑으로 감당하는 것은 무리가 있습니다.
대신 키를 보관해야 하는 책임이 생겼습니다. update_option에 평문(Plaintext)으로 넣는 것은 그만두었습니다. DB만 유출되는 경로는 현실적으로 수없이 많습니다. 다른 플러그인의 SQL 인젝션 (SQL Injection), 백업 파일 방치, 공유 서버에서의 노출. 그때 평문 키가 있다면 그대로 사용됩니다.
그래서 wp-config의 salt로부터 키를 유도하여, AES-256-GCM으로 암호화하여 저장하고 있습니다. 키를 코드에 직접 작성하면 누구나 복호화할 수 있고, DB에 두면 함께 유출됩니다. 파일 쪽에 있는 salt로부터 유도하는 것이 타협점이었습니다.
다만 이것이 만능은 아닙니다. 서버 파일까지 읽힌다면 salt도 읽을 수 있으므로 복호화됩니다. 완전히 침입당했을 경우에는 효과가 없습니다. 그럼에도 이 방식을 도입한 이유는, 실제 사고가 'DB만' 유출되는 형태로 발생하는 경우가 많기 때문입니다. 전부를 지킬 수는 없습니다. 일어나기 쉬운 형태만 막는 것입니다.
앞서 소름 끼쳤던 바로 그 부분입니다.
처음에는 속도 제한 (Rate Limit)을 생각했습니다. 하지만 이것만으로는 부족합니다. 연타는 막을 수 있지만, 30초에 한 번씩 하루 종일 예의 바르게 요청을 보낸다면 그냥 통과하게 됩니다. 내가 지키고 싶은 것은 '연타를 당하지 않는 것'이 아니라 '청구 금액이 상정치를 넘지 않는 것'이었습니다. 봐야 할 것은 빈도가 아니라 총량입니다.
그래서 하루 이용 상한을 서버 측에서 관리하고, 상한을 초과하면 AI를 호출하지 않고 정형 메시지를 반환하도록 했습니다. 호출한 뒤에 차단해 봤자 돈은 이미 나갔으므로, 호출하기 전에 판정하는 것이 핵심입니다. 카운트는 DB 측에서 원자적 (Atomic)으로 가산합니다. 읽고 더해서 다시 쓰는 구현 방식이라면 동시 접속 시 숫자를 놓칠 수 있습니다. 공격을 받고 있을 때일수록 요청이 겹치기 때문에, 가장 숫자를 정확히 세어야 할 순간에 실패하게 됩니다. 이 부분은 제가 실제로 겪어보고 수정했습니다.
이것도 완벽하지는 않습니다. 익명의 방문자는 IP로 셀 수밖에 없어서, 같은 직장에서 접속하면 여러 명이 한 명으로 보일 수 있고, IP를 바꾸면 회피됩니다. 다만 목적은 개인 식별이 아니라, 최악의 경우의 청구 금액에 천장을 만드는 것이었습니다. 상한에 도달하면 AI를 호출하지 않는다. 그 한 가지만 지켜진다면 무엇을 당하더라도 청구 금액은 정해진 선에서 멈춥니다. '최악의 경우 얼마'라고 말할 수 있게 되자, AI 채팅 봇을 훨씬 쉽게 배치할 수 있게 되었습니다.
세 번째는 AI의 답변 그 자체입니다.
채팅 봇은 모르는 질문을 받으면 그럴듯한 거짓말을 만들어냅니다. 존재하지 않는 반품 조건을 안내하거나, 제공하지 않는 서비스를 '있습니다'라고 답합니다. 방문자는 그것을 사이트의 공식 답변으로 받아들입니다. 즉, AI의 거짓말이 곧 사이트 운영자의 거짓말이 됩니다.
그래서 '근거 있는 답변만' 하는 모드를 만들었습니다. 질문에 대해 사이트 내의 콘텐츠를 검색하고, 충분한 근거를 찾지 못하면 AI가 대답하게 하지 않고 '사이트 내에 해당 정보를 찾을 수 없습니다'라고 반환합니다. 그것뿐입니다.
이것은 똑똑해 보이게 만드는 기능이 아닙니다. 똑똑해 보이는 것을 포기하는 기능입니다. 만들면서 조금 묘한 기분이 들었습니다. 어렵게 AI를 탑재해 놓고, 대답하지 않는 방향으로 구현을 하고 있으니까요. 하지만 고객 지원 창구에 필요한 것은 박학다식한 거짓말쟁이가 아니라, 정직한 안내원이라고 생각합니다.
이와 더불어, LLM의 응답은 외부 입력 (External Input)으로 취급하고 있습니다. 사용자 입력은 의심하면서, 그것을 LLM에 통과시킨 출력물은 그대로 믿어버립니다. 저는 이것을 이중 신뢰 (Double Trust) 문제라고 부르는데, 실제로 이것 때문에 한 번 당한 적이 있습니다. 응답을 그대로 화면에 출력하고 있어서, 구문상으로는 아무런 문제가 없는데도 위험한 상황이었습니다.
여기서의 타협점은 대답할 수 있는 범위가 좁아진다는 것입니다. "모르겠습니다"라고 말하는 횟수가 늘어나고, 화려함은 떨어집니다. 그럼에도 불구하고, 틀린 내용을 자신만만하게 말하는 것보다는 낫다고 판단했습니다. "모른다"라고 말할 수 있는 봇은, 대답했을 때의 신뢰도가 올라간다는 부수적인 효과도 있었습니다.
글을 다 쓰고 나서 깨달은 것이지만, 이 세 가지 판단은 모두 같은 형태를 띠고 있었습니다.
열쇠는 최악의 경우 DB가 유출되어도 평문 (Plaintext)으로는 나오지 않는다. 지갑은 최악의 경우 공격을 당해도 상한선에서 멈춘다. 출력은 최악의 경우 모르더라도 거짓말은 하지 않는다. 모두 "잘 되었을 때 멋진 것"이 아니라, "문제가 생겼을 때 어디까지에서 멈출 것인가"를 설계하고 있습니다. 최악의 케이스를 이용자가 가늠할 수 있도록 한다는 한 가지 점에서 이 세 가지는 연결되어 있었습니다.
흥미롭게도, 이런 방식의 설계는 제품 소개 페이지에서는 잘 다뤄지지 않습니다. 나열되는 것은 고정밀, 다기능, 간편한 도입입니다. 공포에 관한 이야기는 셀링 포인트 (Selling Point)가 되지 않기 때문이라고 생각합니다. 하지만 도입을 망설이는 사람의 머릿속에 있는 것은 기능의 개수가 아니라, 바로 이런 종류의 불안함일 것입니다.
- 사용하는 쪽과 탑재하는 쪽은 다르다. 탑재하는 쪽의 실패는 신뢰해 준 타인에게 손해를 입힌다.
- 열쇠를 맡는다면 평문으로 두지 않는다. 단, 전부를 지킬 수는 없다. 일어나기 쉬운 사고의 형태만 막는다.
- 지갑은 빈도가 아니라 총량에 천장을 둔다. 호출하기 전에 판정한다. 카운트는 원자적 (Atomic)으로 처리한다.
- 출력은 외부 입력으로 취급한다. 근거가 없다면 대답하게 하지 않는다. 똑똑해 보이는 것을 포기하는 용기.
- 세 가지의 공통점은 "최악의 케이스를 이용자가 가늠할 수 있다는 것"이다.
AI를 탑재한다는 것은 기능을 추가하는 것보다, 타인의 손실에 책임을 지는 것이었습니다. 화려함 없는 설계뿐이지만, 이 세 가지를 처음에 결정해 둔 덕분에 밤에 잠을 잘 잘 수 있습니다.
구현의 세부 사항(열쇠의 암호화, 하루 이용 상한 카운트, 근거 있는 답변만 하는 모드)은 별도의 기사에 작성했습니다.
평소에는 raplsworks.com에서 WordPress 플러그인 개발이나 Claude Code 관련 내용을 쓰고 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기