AI가 답변 문장을 작성하지 못하게 하고, 자신 있을 때만 응답하는 안내 시스템 구현기
요약
본 글은 AI가 임의로 문장을 생성하는 대신, 미리 정의된 답변 중 하나를 '선택'하여 응답하는 안내 시스템 구현 과정을 다룹니다. 이 시스템은 답변에 자신이 없을 경우 침묵함으로써 사용자에게 신뢰도를 높이는 것이 핵심입니다.
핵심 포인트
- AI의 역할을 '문장 생성'에서 '옵션 선택'으로 제한하여 통제력을 확보했습니다.
- 초기 임베딩 방식과 Llama 3.1을 사용한 모델 비교를 통해 정확도 문제를 분석했습니다.
- 최종적으로 양쪽 모두에게 선택하게 하고, 두 답변이 일치할 때만 응답하는 방식으로 개선했습니다.
내 사이트의 왼쪽 아래에 작은 말풍선 버튼을 배치했다. 이 버튼을 누르면 입력창이 열리고, 사이트에 대해 질문할 수 있다.
공개한 날, 본편에서 처음 받은 질문은 '이 사이트는 무엇인가요?'였다. 안내 시스템은 이에 답변하지 않았다. 대신 '그 질문에 대한 답은 아직 준비되지 않았습니다'라고 응답하고 질문만 저장했다.

응답하지 않았을 때의 표시. 입력창 아래 주의사항에 명시된 대로, 질문만 저장함
고장 난 것은 아니다. 답변에 자신이 없을 때는 침묵하는 것, 이것이 이 안내 시스템의 가장 중요한 사양이며, 그날은 그 사양대로 작동했다. 본 글에서는 AI가 답변 문장을 단 한 글자도 작성하지 못하게 하면서 안내 시스템을 만든 과정과, 이를 개인 사이트 규모에서 무료로 운영하고 있는 이야기를 적는다. 중간에 판별 메커니즘 자체를 완전히 교체했기 때문에, 그 비교 내용도 포함한다.
AI에게 '무엇을 반환할지'만 선택하게 하기
안내 시스템이 반환하는 문장은 모두 내가 직접 작성한 것이다. 현재는 11가지로, '어떤 일을 하는 사람인가요?', '어떤 그림에 능숙한가요?', '의뢰 가능한가요?', '연락처는?' 등 자주 들어올 법한 질문들에 대한 답변과 관련 페이지 링크 조합이다. 답변 하나는 다음과 같은 형태를 한다.
{
"id": "request",
"answer": {
...
AI의 역할은 도착한 질문이 이 11가지 중 어느 것에 해당하는지만 선택하는 것이다. 해당되면 그 답변을 그대로 반환하고, 아무것도 해당되지 않으면 응답하지 않는다.

응답했을 때의 표시. 반환되는 문장과 링크 모두, 답변 파일에 작성된 것을 그대로 사용
문장을 생성하지 못하게 하는 이유는 간단하다. 사이트에 올라가는 문장은 내 언어로 하고 싶기 때문이다. AI가 생성하게 하면, 지어낸 이야기를 하거나, 능숙하게 유도되어 말하게 되거나, 지시를 수정당하는 등의 문제가 필연적으로 따른다. 선택만 하게 한다면, 아무리 벗어나도 화면에 표시되는 것은 내가 작성한 문장 중 하나일 것이고, 답변의 질 상한선은 내 글의 질로 결정된다.
이로써 AI의 역할은 상당히 작아졌다. 다만, '무엇을 반환할지'를 정확하게 선택하는 것이 의외로 어려웠다.
첫 번째 형태: 두 개가 같은 답변을 가리킬 때만 응답하기
처음에 시도한 것은 임베딩(embedding)이었다. 답변마다 질문 예시를 준비해 두고, 도착한 질문과 의미상 가장 가까운 예를 찾는 방식이다. 시험 삼아 테스트 세트를 만들었다.
- 맞았으면 하는 의역 49건: '자기소개해', 'Do you take commissions?'와 같이, 어떤 답변으로 응답받고 싶은 질문 -
맞지 말아야 할 질문 24건: '오늘 날씨는?', '이 그림을 그려줘', 'Hello' 등
Cloudflare Workers AI의 임베딩 모델 4개를 사용했을 때, 가장 가까운 답변이 맞았던 비율은 36~42/49였다. 문제는 의역과 무관한 질문들의 근접성 영역이 겹쳐 있다는 것이었다. 예를 들어 '이 그림을 그려줘'는 의뢰 답변에 0.739로 접근했고, 이는 올바른 의역 중 가장 유사하지 않은 것보다 높았다. 임계치를 어디에 두든, 놓치거나 너무 많이 통과하는 둘 중 하나가 발생했다.
작은 언어 모델(Llama 3.1 8B)에게 답변 ID를 하나 선택하게 하자, 의역에는 45/49가 맞았다. 다만, 무관한 질문을 6/24나 통과시켰다. 'Hello'나 '아'가 자기소개 답변으로 가버렸다. 선택지를 보여주면 아무리 그래도 일단 하나를 고르려고 한다.
어긋나는 방식이 다르다면, 양쪽에 선택하게 하고 같은 답변을 가리킬 때만 응답하면 된다.

첫 번째 형태. 두 개의 판별이 일치할 때만 응답
| 판별 (초기 시제품) | 정확한 경우 | 다른 답변 | 응답 안 함 | 무관한 질문 통과 |
|---|---|---|---|---|
| 임베딩 (bge-m3) × 언어 모델 (Llama 3.1 8B) | 39/49 | 0 | 10 | 3/24 |
정확한 수는 언어 모델 하나만 쓸 때보다 줄었지만, 다른 답변을 반환하는 오류가 0이 되었다. 틀릴 때는 반드시 '응답 안 함'으로 떨어졌다. 안내 시스템이 가장 피해야 할 것은, 의뢰 질문에 연락처 답변을 반환하는 식의 자신감 넘치는 오판이다. 응답하지 않는 것이 그것보다 훨씬 해롭다. 정확도를 높이는 것보다, 잘못되었을 때 어느 쪽으로 떨어질지 결정하는 것이 더 효과적이었다.
이 형태로 공개했다.
생성하지 않는 '결정 모델'을 놓치고 있었다
공개하고 며칠 후, 문장을 생성하지 않는 판별형 모델이라면 애초에 지어낸 이야기 걱정이 필요 없는 것 아닌가 싶어 다시 조사해 보았다. Cloudflare가 딱 2주 전에 Workers AI에 Clef라는 결정 모델을 출시했었다.
Clef는 문장을 생성하지 않습니다. 결정된 선택지 중 어느 것에 해당하는지를 확률과 함께 반환하는 모델이며, 문장 생성 경로 자체가 없습니다. 안내 역할을 하려던 것 자체만으로도, 비교 후보군에서 빠져있었던 것은 단순한 간과였습니다.
사용 방법은 이렇습니다. 각 선택지에 설명을 작성하여 전달하면, 선택된 항목과 각 선택지별 확률이 반환됩니다.
const out = await env.AI.run("@cf/cloudflare/clef-flash", {
model: "clef-flash",
state: question, // 방문자의 질문
...
확률이 함께 오기 때문에, '두 개 일치' 방식에서 '확률 하한선(minimum probability)' 방식으로 바꿀 수 있습니다. 선택된 답변의 확률이 하한선에 도달하지 못하면 응답하지 않습니다.
export const MIN_PROBABILITY = 0.65;
/** 답을 선택하고, 확률이 하한 이상일 때만 해당 id를 반환 */
export function decide(choice: ClefChoice): string | null {
...

Clef로 변경 후. 확률이 하한에 도달했을 때만 답변함
같은 시험 그룹으로 비교해 보기
처음 시제품 스크립트는 남아있지 않아, 시험 그룹을 다시 만들었습니다. 이번에는 2개를 만들어 역할을 분담했습니다.
첫 번째 그룹(의역 49건・무관 24건): 임계값(threshold)을 선택하기 위한 그룹 -
두 번째 그룹(의역 50건・무관 30건): 선택된 임계값을 확인하기 위한 그룹. 무관 항목 중 14개는 '추천 화재는요?', '다음 작품은 언제요?', '본명은요?'와 같이 작가나 그림에 대한 이야기지만 답변을 준비하지 않은 애매한 질문으로 만들었습니다.
임계값을 같은 그룹에서 선택하고, 같은 그룹에서 평가하면, 그 그룹에 맞춰진 숫자만 나오게 됩니다. 확인용 그룹을 별도로 두는 것이 목적입니다.
Clef-flash를 확률로 자르지 않고 사용했을 때는, 두 번째 그룹의 의역 항목 중 49/50건이 맞았고, 무관 항목은 24/30건 통과했습니다. 어떤 것을 선택하려는 습성은 언어 모델과 같습니다. 다른 점은, 무관한 질문에는 확률이 낮게 나온다는 것이었습니다('졸림' 0.18, 'test' 0.26).
첫 번째 그룹으로 하한을 0.65로 정하고, 두 번째 그룹에 적용한 결과가 이것입니다.
| 판정 (두 번째 그룹) | 정확함 | 다른 답변 | 응답하지 않음 | 무관 통과 | neuron/회 |
|---|---|---|---|---|---|
| 초기 형태 (bge-m3 × Llama 일치) | 35/50 | 1 | 14 | 4/30 | 약 10 |
| Clef-flash, 확률 0.65 이상 | 43/50 | 0 | 7 | 2/30 | 약 3.5 |
첫 번째 그룹에서도 정확한 수는 같았습니다(43/49). 무관 통과 수는 3개에서 0개로 줄었습니다.
하한을 움직여 보면 이렇게 됩니다 (두 번째 그룹).
| 하한 | 0.5 | 0.6 | 0.65 | 0.7 | 0.8 |
|---|---|---|---|---|---|
| 정확함 | 46/50 | 44/50 | 43/50 | 40/50 | 31/50 |
| 무관 통과 | 3/30 | 2/30 | 2/30 | 2/30 | 1/30 |
눈에 띄었던 점은, 초기 형태가 다른 답변을 1건 반환했다는 것입니다. '최근 그린 그림은?'이라는 질문에, 작품 목록이 아닌 화풍의 답변을 반환했습니다. 임베딩과 언어 모델이 함께 같은 방향으로 벗어나 있었습니다. 초기 시제품에서 '다른 답변 0'이었던 것은, 그 그룹에서는 우연히 두 개가 동시에 벗어나지 않았을 뿐, 일치시키면 막을 수 있다는 보장은 아니었습니다. Clef-flash는 이 질문을 작품 목록에 0.73으로 맞추고 있습니다.
Clef-flash가 0.65 이상으로 통과시킨 2건은, '이 사이트의 소스 코드를 주세요'(사이트 제작 설명을 반환)와 '제 아이콘을 지금 바로 만들어 주세요'(‘우선 상담부터’를 반환)였습니다. 확률이 0.84와 0.76으로 높기 때문에 하한으로는 자르지 못했습니다. 다만 반환되는 것은 직접 작성한 무난한 문장이었고, 후자는 오히려 맞았습니다. 초기 형태는 '이 작품의 캐릭터 이름은?', '게임 추천 빌드는?' 등도 통과시키고 있었습니다.
그 외 확인한 사항:
흔들림 없음: 경계에 가까운 8개 질문을 3번씩 다시 호출하여, 선택된 답변과 확률 모두 소수점 4자리까지 동일했습니다 -
속도: 1회 중앙값 0.46초, 느릴 때 1.2초 (손에서 불러온 값) -
무료 플랜으로 호출 가능: 무료 플랜 계정에서 실제로 호출할 수 있었습니다. Workers AI 중에는 무료 플랜으로 호출할 수 없는 모델도 있습니다.
2개를 구동한다면, 무엇을 조합할까
처음 형태는 '2개의 일치'로 유지했다. Clef도 2개를 구동하면 더 좋을지 비교해 보았다.
우선, 같은 Clef를 2개 구동해도 의미가 없다. 같은 질문에는 소수점 4자리까지 동일한 답을 반환하기 때문에, 2개는 항상 일치하여 1개와 다르지 않다. 조합하려면, 제외하는 방식이 다른 것을 해야 한다.
두 가지 조합(154문항)으로 비교했다.
| 조합 | neuron/회 | 첫 번째 정답/통과 | 두 번째 정답/통과 |
|---|---|---|---|
| Clef-flash만 (0.65) | 약 3.5 | 43/49・0/24 | 43/50・2/30 |
| ... |
'2문항을 한 번에 질문하기'는 Clef가 1회 호출로 여러 질문에 답할 수 있다는 점을 활용한 형태다. '어떤 답인지' 외에도 '이 질문은 사이트의 작가에 대해 묻고 있는가(예/아니오)'를 물어, 둘 다 충족했을 때만 반환한다. 언어 모델과 조합하면 무관하게 통과하는 수는 0이 되지만, 답변 가능한 수가 줄어들고 비용은 약 4배가 된다. 임베딩이나 reranker와 조합하면 오히려 나빠졌다.
2문항 형태가 가장 좋아 보인다. 다만 차이는 154문항 중 정답이 5건, 통과가 1건이라는 점이다. 게다가 이 형태의 임계값(답변 0.5・'작가에 대해' 0.3)은 두 가지 조합을 보고 선택했기 때문에, 확인하는 조합이 남아있지 않다. 또 하나 알아낸 것은, 질문을 1개 늘렸을 뿐인데도 답변 확률 자체가 변했다는 점이다 (1문항일 때와의 차이는 중앙값 0.05). 조정할 다이얼이 2개가 되고, 게다가 서로 영향을 주고받는다.
그래서 지금은 Clef-flash 1개로 했다. 2문항 형태는 실제 운영에서 쌓인 질문을 세 번째 조합으로 넣어 다시 시도하고, 차이가 남으면 교체한다.
답변하지 못한 질문이 다음 답이 되다
틀릴 경우 반드시 '답변 안 함'으로 떨어지도록 설계한 데에는 또 다른 이유가 있다. 답변하지 못한 질문은 그대로 다음 답변의 재료가 된다.
답변할 수 없었던 질문은, 질문의 문・언어・Clef가 가장 확률을 매긴 답과 그 확률・날짜만 저장한다 (IP 주소는 저장하지 않는다). 이를 주기적으로 읽어 3가지로 분배한다.
기존 답변에 질문 예시를 추가하면 맞는 것: 예시 추가 -
새로운 답변이 필요한 것: 답변 문장을 작성하여 추가 -
답변할 필요가 없는 것: 무관・장난. 아무것도 하지 않음
답변은 리포지토리의 JSON 파일에 두고 있다. 추가할 때는 브랜치를 쳐서 PR을 내기 때문에, 차이점이 남고 CI 테스트도 통과한다.
실제로, 공개 전 프리뷰에서 처음 쌓인 질문은 '그림 보고 싶다'였다. 애초에 작품 목록으로 안내하는 답변을 준비하지 않았었기 때문에, 그것을 추가하니 맞게 되었다. 이번 비교에서 놓쳤던 의역(‘Discord 있어?’, ‘팔로우하고 싶어’, ‘pixiv 하고 있어?’)도 예시를 1개 추가하면 확률이 높아지는 종류다.
서두의 '이 사이트는 뭐야?' 역시 이 흐름으로 분배했다. Clef는 자기소개 답변에 0.49를 매겼지만, 하한에 미달하여 답변하지 못한 질문이었다. 자기소개로 반환할지, 이 사이트 제작에 대한 답변으로 반환할지, 새로운 답변을 쓸지. 여기는 AI가 아니라 내가 결정함으로써, '그림을 중심으로 여러 가지 만드는 사람의 사이트'라는 답이 가능한 자기소개 답변에 질문 예시를 추가했다. 추가한 후에는 '이 사이트는 뭐야?', '여기는 무슨 사이트인가요?', 'What is this website?'도 자기소개로 해당했고, '이 사이트 제작 방법은?'은 제작 답변으로 그대로였다.
안내 역할은, 답변하지 못한 수만큼, 어디를 채워 넣어야 할지 알려준다.
무료로 구동하는 조건
마지막으로 비용에 대해 이야기한다. 제목의 '무료'에는 조건이 있다.
방문자 수가 어느 정도 읽을 만한 개인 사이트 규모에 한정된 이야기다. 방문자가 적지 않은 규모, 예를 들어 화제가 되거나 서비스 문의 창구로 사용할 경우에는 종량제 무료 범위를 초과한다. 이 기사의 형태를 그대로 따라 해도, 무료로는 끝나지 않는다.
이 사이트는 Cloudflare Workers의 무료 플랜으로 구동하고 있으며, 판정은 Workers AI, 답변하지 못한 질문 저장에 D1, 횟수 계산에는 KV를 사용하고 있다. 모두 무료 범위 내에 포함된다.
가장 효과적인 것은 Workers AI의 범위다. Clef-flash는 입력 100만 토큰당 $0.038이고, 출력에는 과금되지 않는다. 답변 설명 11개를 전달하면 입력은 1회 약 1,000 토큰이 되고, neuron(Workers AI의 과금 단위)으로 환산하면 1회 약 3.5 neuron이 된다. 처음 형태(Llama 3.1 8B)는 약 10이었다.
Workers AI의 무료 할당량은 하루 10,000 neuron이다. 이 할당량은 같은 계정에서 작동하는 다른 Bot과 공유된다. 다 사용하면 그 Bot의 게시물도 멈추기 때문에, 안내 역할은 하루 최대 100회로 정했다. 100회라도 약 350 neuron으로, 할당량의 3.5%에 머문다. 게다가 사람당 시간당 20회의 제한도 두었고, '자주 묻는 질문'으로 나열한 3개는 API를 호출하지 않고 파일에서 직접 출력한다.
100회를 초과하는 날에는 AI를 호출하지 않고 '오늘은 여기까지 했습니다'라고 응답하며 질문만 모은다. 따라서 방문자가 갑자기 늘어나도 청구액이 늘지 않고 안내 역할만 멈춘다. 여기에서도, 오작동했을 때 문제가 없는 방향으로 설계했다. 반대로, 멈추지 않고 모두에게 답변하고 싶다면 유료 플랜의 종량제(従量課金)를 사용해야 한다.
요약 (まとめ)
- AI가 답변 문장을 작성하게 하는 대신, 준비된 답변 중 무엇을 돌려줄지만 선택하도록 했다.
- 처음에는 임베딩과 언어 모델이 일치할 때만 반환하는 형태로 만들었다. 불일치할 때는 '답변하지 않음'으로 처리했지만, 둘 다 맞지 않아 질문이 있는 경우가 있었다.
- 문장 생성을 하지 않는 결정 모델 Clef-flash로 교체하고, 확률 0.65 이상일 때만 반환하도록 했다. 별도로 만든 조합으로 확인해 보니, 정확하게 답변하는 수가 늘고 관련 없는 것을 통과시키는 수가 줄었으며, 비용은 약 3분의 1이 되었다.
- 임계값(しきい値)은 선택하는 조합과 확인하는 조합을 나누어 정했다.
- 답변하지 못한 질문은 모아서 다음 답변의 재료로 사용한다.
- 개인 사이트 규모라면, 하루 상한선을 두어 무료 할당량 내에서 운영할 수 있다.
정확도(正解率)를 높이지 않아도, 오작동해도 문제가 없는 방향으로만 오작동하도록 하면 작은 모델로 충분했다. 멈추는 메커니즘은 둘의 일치에서 확률 하한선으로 바뀌었지만, 안내 역할이 침묵한 질문들이 여전히 조금씩 쌓여가는 것은 변함없다.
토론 (Discussion)

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기