챗봇의 오해를 문법 회귀 테스트(Grammar Regression Tests)로 전환하기
요약
챗봇의 문법 오류를 단순한 수정 대상이 아닌 회귀 테스트로 활용하는 워크플로우를 제안합니다. AI의 추측을 실행 단계와 분리하여 결정론적인 명령 실행을 유지하면서도 자연어 처리 능력을 개선하는 방법을 다룹니다.
핵심 포인트
- 해석(Interpretation)과 실행(Execution)의 엄격한 분리 필요
- AI의 추측이 직접 실행되지 않도록 결정론적 파서 유지
- 대화 실패 사례를 문법 개선을 위한 회귀 테스트로 전환
- 사용자에게 상세한 파서 에러를 노출하지 않는 설계 권장
커스텀 문법 (Custom-grammar) 봇은 문법 작성자가 예상하지 못한 지극히 합리적인 문장을 누군가 입력하기 전까지는 신뢰할 수 있습니다.
보통 불편한 선택지는 다음과 같이 제시됩니다:
- 문법을 결정론적 (Deterministic)으로 유지하되 답답할 정도로 범위를 좁게 가져가거나,
- AI 모델이 모든 것을 해석하도록 하여 예측 불가능한 동작을 수용하거나.
이는 잘못된 경계 설정입니다. 대화 실패 사례를 사용하여 문법을 개선하는 동안 명령 실행은 결정론적으로 유지할 수 있으며, 선택적으로 인간이 확인해야 하는 해석을 제안하는 데 AI를 사용할 수 있습니다.
핵심은 모든 오해를 파서를 즉석에서 더 허용적으로 만들라는 초대장이 아니라, 잠재적인 회귀 테스트 (Regression test)로 취급하는 것입니다.
이 튜토리얼은 다음과 같은 명령을 이해하는 작은 JavaScript 봇을 위한 워크플로우를 구축합니다:
feed Luna 20g
remind me to feed Miso at 19:30
우리는 AI가 생성한 추측이 명령을 실행하도록 허용하지 않으면서 자연스러운 변형에 대한 지원을 추가할 것입니다.
불변 법칙: 해석과 실행은 분리되어야 한다
우리 봇은 세 가지 결과 중 하나를 생성합니다:
// 성공적으로 파싱되었으며 도메인 검증을 통과함.
{ status: "accepted", command: { ... }, grammarVersion: "2026-08-02" }
...
accepted 상태인 명령만이 실행될 자격이 있습니다. 모델의 제안, 지원 답변, 또는 부분적으로 파싱된 명령은 결코 accepted와 동일하지 않습니다.
이러한 구분이 중요한 이유는 사용자의 실제 관심사가 AI가 그럴듯한 해석을 만들어낼 수 있는지 여부가 아니기 때문입니다. AI는 종종 그럴듯한 해석을 만들어낼 수 있습니다. 진짜 관심사는 그럴듯하지만 틀린 해석이 조용히 실제 동작으로 이어질 수 있는지 여부입니다.
가장 작은 재현 가능한 파서 구축하기
프로젝트를 생성합니다:
mkdir grammar-repair-loop
cd grammar-repair-loop
npm init -y
...
package.json을 업데이트합니다:
{
"type": "module",
"scripts": {
...
src/commands.peggy를 생성합니다:
{
function feed(cat, amount) {
return { intent: "feed", cat, amountGrams: Number(amount) };
...
이 문법(grammar)은 공손한 접두사(polite prefixes), 선택적 문장 부호(optional punctuation), 그리고 따옴표로 묶인 다중 단어 이름(quoted multiword names)을 처리합니다. 하지만 99:99가 유효한 시간인지, 혹은 5,000g을 급여하는 것이 합리적인지 여부는 여전히 결정하지 않습니다. 이는 도메인 결정(domain decisions) 사항이므로 문법 외부에서 다루어야 합니다.
src/parse.js를 생성합니다:
import { parse } from "./generated-parser.js";
export const GRAMMAR_VERSION = "2026-08-02";
...
파서 예외(parser exception) 텍스트를 사용자에게 직접 보내지 마세요. 상세한 파서 에러는 개발자에게는 유용하지만, “offset 17에서 Unit이 필요합니다”와 같은 메시지는 지원 응답으로서 유용할 때가 거의 없으며 문법 내부 구조(grammar internals)를 노출할 위험이 있습니다.
예시를 영구적인 기록으로 만들기
채팅 기록(chat transcript)은 발생한 일을 단 한 번 설명할 뿐입니다. 하지만 픽스처(fixture)는 같은 일이 다시 발생하는 것을 방지합니다.
data/grammar-cases.json을 생성합니다:
[
{
"id": "basic-feed",
...
그 다음 test/grammar.test.js를 추가합니다:
import { describe, expect, it } from "vitest";
import cases from "../data/grammar-cases.json" with { type: "json" };
import { parseCommand } from "../src/parse.js";
...
코퍼스(corpus)를 실행합니다:
npm test
새로운 표현 방식(wording)이 실패할 경우, 문법을 수정하기 전에 이를 코퍼스에 먼저 추가하세요. 권장되는 순서는 다음과 같습니다:
- 픽스처(fixture)를 사용하여 오해 상황을 재현합니다.
- 사용자 또는 도메인 소유자에게 의도된 명령을 확인합니다.
- 테스트를 실행하고 실패하는 것을 관찰합니다.
- 해당 표현을 지원할 수 있는 가장 최소한의 문법 변경을 수행합니다.
- 거부된 입력(rejected inputs)을 포함하여 전체 코퍼스를 실행합니다.
- 기존의 발화(utterance)가 이제 다른 명령을 생성하지 않는지 검토합니다.
마지막 단계는 단순히 테스트 스위트를 초록색(pass)으로 만드는 것보다 훨씬 중요합니다.
채팅을 정답(ground truth)으로 취급하지 않고 수정 사례 기록하기
유용한 수정 기록(repair record)에는 원래의 결과(original outcome)와 확인된 의도(confirmed intent)가 모두 필요합니다. 첫 번째 개발자나 모델이 올바르게 추측했을 것이라고 가정해서는 안 됩니다.
const repairCase = {
id: crypto.randomUUID(),
rawUtterance: "could you give Luna twenty grams?",
...
실용적인 상태 시퀀스(state sequence)는 다음과 같습니다:
needs_clarification
-> intent_confirmed
-> fixture_added
...
모든 실패한 발화(utterance)를 자동으로 유지하지 마세요. 명령에는 이름, 일정, 계정 정보 또는 잘못된 상자에 붙여넣은 텍스트가 포함될 수 있습니다. 발화를 장기적인 테스트 픽스처(test fixture)로 보존하기 전에 권한을 요청하거나, 식별 가능한 값을 대표적인 플레이스홀더(placeholder)로 교체하세요.
예를 들어, 실제 반려동물의 이름이 포함된 확인된 보고서는 다음과 같이 될 수 있습니다:
{
"utterance": "could you give ExampleCat twenty grams?",
"expected": {
...
AI가 도움을 줄 수 있는 부분과 멈춰야 하는 부분
언어 모델(Language Model)은 no_match 발생 이후 유용할 수 있는데, “could you give Luna twenty grams?”와 같은 의역(paraphrase)에 대해 가능성 있는 구조화된 해석을 제안할 수 있기 때문입니다. 또한 유지 관리자를 위해 후보 픽스처(candidate fixture)를 초안하거나 유사한 실패 사례들을 그룹화할 수도 있습니다.
하지만 그러한 능력이 입증되었다고 해서 그것이 신뢰할 수 있는 명령 실행과 동일한 것은 아닙니다.
모델은 다음과 같은 행동을 할 수 있습니다:
- 존재하지 않는 엔티티(entity)를 만들어냄
- 모호한 문장에서 잘못된 의도(intent)를 선택함
- 도메인 제한(domain limits)을 무시함
- 동일한 입력에 대해 서로 다른 답변을 생성함
- 사용자 텍스트에 포함된 지시 사항을 따름
- 명확한 확인이 필요한 상황에서 확신에 찬 추측을 함
모델의 출력을 제안(proposal)으로 취급하고 그 형태(shape)를 검증하세요:
import { z } from "zod";
const candidateSchema = z.discriminatedUnion("intent", [
...
usable한 제안이라 할지라도 바로 실행 가능한 것은 아닙니다. 이는 다음과 같은 확인(clarification) 과정을 지원할 수 있습니다:
“Luna에게 20g을 먹여달라는 뜻인가요?”
사용자의 확인을 통해 새로운 결정론적(deterministic) 명령을 제출할 수 있습니다. 모델의 응답이 소급적으로 권한 부여(authorization)로 전환되어서는 안 됩니다.
간단한 의사결정 프레임워크는 다음과 같습니다:
| 상황 | AI의 역할 | 인간의 통제 |
|---|---|---|
| 회귀 픽스처(regression fixture) 초안 작성 | 문구 및 예상 구조 제안 | 유지 관리자가 픽스처 승인 |
| ... |
유용한 경계선은 모델이 얼마나 유창하게 들리는가가 아니라, 그 결과로 발생하는 영향(consequence)에 따라 결정됩니다.
출시 전 의미론적 차이(semantic diffs) 검토
문법(Grammar) 변경은 코드 변경과 같지만, 가장 중요한 차이점(diff)은 문법 파일에 나타나지 않을 수도 있습니다. PEG 대안(alternative)의 순서가 바뀌면, 모든 새로운 테스트를 통과하더라도 기존 입력값이 해석되는 방식이 달라질 수 있습니다.
출시 전, 승인된 코퍼스(corpus)를 통해 이전 파서(parser)와 제안된 파서를 비교하십시오. 다음과 같은 전환(transition)이 발생하면 사람이 검토할 수 있도록 표시해야 합니다:
no_match -> accepted expected expansion (예상된 확장)
accepted -> no_match likely regression (회귀 가능성 높음)
accepted -> accepted inspect if the command AST changed (명령 AST가 변경되었는지 검사)
...
모든 accepted -> accepted 전환에 대해, 의도(intent) 이름만 비교하지 말고 전체 명령 객체(command object)를 비교하십시오. amountGrams가 20에서 200으로 변경되는 것은 의도가 feed로 유지되더라도 의미론적 회귀(semantic regression)입니다.
유지보수자가 보고 시점의 동작을 재현할 수 있도록 문법 버전을 지원 기록(support records)에 보관하십시오. 파서 버전이 없는 픽스처(fixture)는 여러 번의 출시 이후에는 진단이 불가능해질 수 있습니다.
실행해 볼 가치가 있는 실패 훈련(Failure drills)
픽스처는 개발자의 가정을 인코딩한다
사용자가 “feed Luna at seven”이라고 작성합니다. 유지보수자는 이를 7그램(seven grams)으로 가정하지만, 사용자는 7시(seven o’clock)를 의미했을 수 있습니다.
의도된 의미가 확인될 때까지 픽스처를 추가하지 마십시오. 일부 발화는 모호한 상태로 남겨두어 영구적으로 확인(clarification)을 요청하도록 해야 합니다.
더 넓은 규칙이 더 좁은 규칙의 입력을 가로챈다
PEG 파서는 순서가 있는 선택(ordered choices)을 사용합니다. 특정 규칙 위에 허용 범위가 넓은 규칙을 추가하면, 어떤 분기(branch)가 승리할지가 조용히 바뀔 수 있습니다.
중복되는 형태에 대한 픽스처를 포함하고, 대안(alternatives)의 순서가 바뀔 때마다 의미론적 차이(semantic diffs)를 검토하십시오.
모델의 제안이 UI 문구를 통해 동작이 된다
Continue라고 표시된 버튼은 어떤 일이 일어날지 숨길 수 있습니다. 제안된 명령을 도메인 언어로 표시하십시오:
Feed Luna 20 grams now
별도의 확인 이벤트(confirmation event)를 요구한 다음, 확인된 구조화된 명령(structured command)을 일반적인 검증 경로를 통해 제출하십시오.
거절된 예시들이 스위트(suite)에서 사라진다
유효한 명령(valid commands)만 포함된 코퍼스(corpus)는 점점 더 허용적인 파싱(parsing)을 보상하게 됩니다. 관련 없는 대화, 잘못된 형식의 시간(malformed times), 과도한 값(excessive values), 그리고 지원되지 않는 동작(unsupported actions)에 대해서는 부정적인 픽스처(negative fixtures)를 유지하십시오.
지원 보고서(support report)를 재현할 수 없는 경우
보고서에 문법 버전(grammar version), 원래의 파서 결과(original parser outcome), 또는 정확한 승인된 문구(approved wording)가 누락된 경우, 유지 관리자(maintainer)는 서로 다른 동작을 대상으로 테스트하게 될 수 있습니다. 사용자가 보존된 예시를 편집하거나 거부할 수 있도록 허용하면서, 해당 필드들을 자동으로 캡처하십시오.
수정 루프(repair loop)를 정의한 후 대화 전송 수단(conversation transport)을 선택하십시오
워크플로우는 특정 지원 도구에 의존하지 않습니다. GitHub 이슈 템플릿, 이메일 별칭(email alias), 또는 앱 내 채팅(in-app chat)을 통해 모두 실패한 발화(utterance)를 수집하고 사용자에게 의도한 명령을 확인하도록 요청할 수 있습니다.
규모가 작은 제품의 경우, 창업자가 직접 주도하는 채팅은 명확한 확인 과정을 단축할 수 있는데, 문법을 변경하는 사람이 정확한 후속 질문을 던질 수 있기 때문입니다. 트레이드오프(trade-off)는 채팅은 대화 전송 수단일 뿐, 귀하의 회귀 코퍼스(regression corpus)나 릴리스 기록(release record)이 아니라는 점입니다. 확인된 사례들은 여전히 버전 관리되는 픽스처(version-controlled fixtures)로 이동해야 합니다.
한 가지 구현 옵션은 Knocket으로, 이는 공유 가능한 연락처 페이지, 임베드 가능한 웹 라이브 채팅 위젯(web live-chat widget), 모바일 WebView SDK, 그리고 통합 인박스(unified inbox)를 제공합니다. 웹사이트 위젯은 커스텀 백엔드를 구축하지 않고도 스크립트 태그(script tag)를 통해 설치할 수 있습니다. 방문자는 채팅을 시작하기 위해 계정이 필요하지 않으며, 메시지는 Telegram으로 라우팅될 수 있습니다. 인용된 Telegram 답장은 웹사이트 방문자에게 다시 전달될 수 있습니다.
이것이 명확화(clarification)를 위한 접점을 제공할 수는 있지만, 파서 결과(parser result), 동의 결정(consent decision), 확인된 의도(confirmed intent), 픽스처(fixture), 그리고 릴리스 검증(release verification)은 귀하가 테스트하고 감사(audit)할 수 있는 시스템에 남아 있어야 합니다.
릴리스 체크리스트(Release checklist)
문법 수정 사항을 배포하기 전에 다음을 확인하십시오:
- 기존의 실패 사례에 대해 승인되고 개인정보 보호 검토(privacy-reviewed)를 마친 픽스처(fixture)가 있는가.
- 모호한 문구에 대해 추측하는 대신 명확하게 정리하였는가.
- 문법 변경 전에는 해당 픽스처가 실패했는가.
- 기존에 수락된 명령들이 여전히 동일한 구조화된 출력(structured output)을 생성하는가.
- 부정적 픽스처(Negative fixtures)가 여전히 거부되거나 유효하지 않은 상태로 유지되는가.
- 도메인 검증(Domain validation)이 문법 외부에서 여전히 실행되는가.
- AI가 생성한 후보군(candidates)이 직접적으로 실행을 호출할 수 없는가.
- 상태 유지 동작(Stateful actions)이 명시적인 확인 절차를 표시하는가.
- 수정 기록(Repair records)에 문법 버전이 포함되어 있는가.
- 지원 응답(Support response)이 사용자에게 무엇이 변경되었는지, 또는 명확한 설명이 계속 필요하다는 점을 알려주는가.
문법 버그는 단 한 문장이 파싱(parsing)되기 시작한다고 해서 완전히 해결된 것이 아닙니다. 의도된 의미가 문서화되고, 이전 동작을 재현할 수 있으며, 관련 없는 입력값들이 안전하게 유지되고, 향후의 변경 사항이 수정을 조용히 되돌릴 수 없을 때 비로소 해결된 것입니다.
고지(Disclosure): 저는 Knocket에서 일하고 있으므로, 이를 중립적인 추천이 아닌 하나의 구현 사례로 취급해 주시기 바랍니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기