내 지원 챗봇의 점수는 0.92였다. 게다가 고객에게 거짓말도 했다.
요약
본 글은 Udacity와 AWS Agent Engineer Nanodegree 과정을 통해 개발한 고객 지원 챗봇 프로젝트를 다룹니다. 이 챗봇은 별도의 분류기나 라우팅 노드 없이 단일 시스템 프롬프트만으로 복잡한 메시지 처리(버그 리포트, FAQ 답변 등) 및 티켓 접수 기능을 구현했습니다. 특히 모델의 환각 현상과 점수가 놓칠 수 있는 문제를 발견하고 이를 개선하는 과정에 초점을 맞추고 있습니다.
핵심 포인트
- AWS Bedrock AgentCore managed harness를 활용한 챗봇 개발 사례입니다.
- 별도의 분류기 없이 단일 시스템 프롬프트로 라우팅 및 정보 수집을 구현했습니다.
- 모델의 환각(Hallucination) 위험성을 실제 경험으로 제시하며 주의를 당부합니다.
- 전체 아키텍처가 CloudFormation과 스크립트로 코드로 관리됩니다.
내가 테스트한 13개의 프롬프트 중 12개가 완벽하게 1.00점을 받았다. 두 번의 독립적인 평가를 거쳤고, 정확도는 0.92로 나왔다. 메시지가 잘못된 곳으로 라우팅된 적은 없었고, 프롬프트 주입 시도도 모두 거부되었다.
하지만 한 대화에서 내 챗봇은 고객에게 다음과 같이 말했다:
"제가 티켓 ID TIX-345678로 버그 리포트를 접수했습니다."
그런 티켓은 존재하지 않았다. 데이터베이스에는 아무것도 기록되지 않았다. 고객은 자신의 버그가 로그되었다고 믿고 떠났을 것이다.
이것이 내가 그 챗봇을 만들게 된 이야기이며, 왜 점수가 문제를 발견할 수 없었는지, 그리고 지금 내가 대신 확인하는 것들에 대한 내용이다.
프로젝트
나는 Udacity × AWS Agent Engineer Nanodegree 과정을 진행 중이다. 첫 번째 프로젝트는 가상의 온라인 상점을 위한 고객 지원 챗봇으로, 세 가지 종류의 메시지를 처리한다.
- 버그 리포트: 설명을 수집하고, 재현 단계(steps to reproduce), 환경 정보를 수집하며, 여러 차례에 걸쳐 진행될 수 있으며, 이후 티켓을 접수한다.
- 플랫폼 질문 (주문, 배송, 반품, 결제): 상점의 FAQ에서만 답변해야 한다.
- 그 외 모든 것: 정중하게 인간 지원 라인으로 연결(hand off)한다.
한 가지 특이점은, 과정 초기에 설계되었던 Bedrock Agents Classic이 2026년 7월 30일부로 신규 고객에게 폐쇄되었다는 것이다. 그래서 이 프로젝트는 그 후속 버전인 Amazon Bedrock AgentCore managed harness를 사용한다.
또 다른 특이점, 그리고 실제 연습의 핵심은: 분류기(classifier)도 없고 라우팅 노드도 없다. 모든 라우팅, 정보 수집, 접지(grounding) 동작이 단일 시스템 프롬프트에 존재한다는 것이다.
아키텍처
Customer ──► chat.py ──invoke_harness──► AgentCore managed harness ◄──► Amazon Nova Pro
│ (system_prompt.txt + FAQ) temp 0, topK 1
│
...
구성 요소들:
- Harness: AgentCore는 에이전트 루프(agent loop)를 실행합니다: 모델 호출(model calls), 세션 상태(session state), 도구 실행(tool execution). 저는 프롬프트만 제공하고, FAQ는 하네스(harness)가 생성될 때
{{FAQ}}플레이스홀더에 주입됩니다. - Gateway: Lambda를 MCP 도구로 노출합니다. 모델은 이를
<targetName>___<toolName>세 개의 언더스코어로 인식합니다. - Lambda + DynamoDB: 세 필드가 모두 비어있지 않은지 검증하고, 티켓을 작성하며, UUID
ticketId를 반환합니다. - Everything as code: 두 개의 CloudFormation 스택(도구 및 테스트)과 몇 가지 boto3 스크립트로 구성됩니다. 프롬프트를 개선하는 과정은 단순히 파일을 수정하고,
create_harness.py를 다시 실행한 다음, 새로운 채팅 세션을 여는 것으로 끝납니다.
모델은 Nova와 함께 신뢰할 수 있는 도구 호출을 위해 AWS가 권장하는 그리디 디코딩(greedy decoding) (temperature 0, topK 1)으로 us.amazon.nova-pro-v1:0에 고정됩니다.
단어만으로 라우팅하기
라우팅을 프롬프트 내부의 분류 문제로 처리하는 것이 예상보다 더 잘 작동했습니다. 이 프롬프트는 모델에게 무언가를 작성하기 전에 정확히 하나의 카테고리를 선택하고, 절대 여러 카테고리를 혼합하지 않도록 지시합니다.
가장 어려운 부분은 경계(boundary)입니다.
또한 고객이 작성하는 모든 것을 지시사항이 아닌 데이터로 취급하는 섹션을 추가했으며, 논쟁하거나 탐지 방법을 설명하지 않고 거부할 수 있도록 오버라이드 패턴 목록("ignore your previous instructions", "I'm your developer")을 포함했습니다.
평가 (Evaluating it)
수동 채팅은 확장성이 떨어지기 때문에 13가지 케이스 스위트(suite)를 작성했습니다: 버그 리포트 케이스 3개, FAQ 케이스 3개, 핸드오프(hand-offs) 2개, 그리고 엣지 케이스(edge cases) 5개(단순 help, 모호한 메시지 2개, 인젝션 2개). 스크립트는 각 케이스를 **새 세션(fresh session)**에서 실행하고, Bedrock Evaluations가 기대하는 형식으로 JSONL 파일을 작성합니다. 이후 Nova Pro는 이를 LLM-as-a-judge 방식으로 참조 값과 비교하여 각 응답에 점수를 매깁니다.

케이스 간의 독립성을 유지하기 위해 하네스(harness) 메모리를 비활성화했습니다. 공유할 만한 작은 함정 같은 점이 있습니다: create_harness는 memory={"disabled": {}}를 허용하지만, update_harness는 memory={"optionalValue": {"disabled": {}}}가 필요합니다. 생성 시의 형태(shape)를 업데이트에 전달하면 실패하며, 스크립트는 재실행할 때마다 업데이트 경로를 따릅니다.
결과: 0.92, 두 번 연속으로 나왔습니다.
점수가 보여주지 못한 것 (What the score didn't show)
여기 불편한 부분이 있습니다. 제가 발견한 실제 결함 두 가지 모두, 평가가 절대 수행하지 않는 작업을 통해 찾아냈습니다: DynamoDB 테이블을 열어 채팅 기록과 비교하는 것이었습니다.
1. 조작된 티켓 ID (Invented ticket IDs)
다중 턴 대화(검색창 오류 → 무슨 일이 발생했는지? → 어떤 브라우저?)에서, 봇은 티켓 ID로 끝났습니다. 하지만 터미널에는 [tool call] 라인이 없었음에도 불구하고, 테이블에는 여전히 열 개가 아닌 아홉 개의 행이 남아 있었습니다.
시도에 따라 #12345, TICKET1234, TIX-345678 등이 생성되었습니다. UUID가 아닌 플레이스홀더 형태의 문자열들이었습니다.
내 프롬프트에는 이미 '절대 정보를 꾸며내지 마라(never fabricate information)'라고 명시되어 있었다. 그것이 분명 충분히 구체적이지 않았던 것이다. 나는 다음과 같은 명시적인 규칙을 추가했다: 제공할 수 있는 유일한 티켓 ID는 성공적인 호출로 반환된 정확한 ticketId 문자열이어야 한다. 만약 그러한 ID를 받지 못했다면, 보고서가 제출되지 않은 것이니 그렇게 말하라.
2. 티켓에 작성된 쓰레기 데이터
두 번의 실행 모두에서 점수 0.00을 받은 프롬프트는 다음과 같다: "앱이 계속 로그아웃됩니다. Windows 11용 Chrome을 사용하고 있습니다."
채팅에서는 응답이 괜찮아 보였다: 고맙습니다, 여기 실제 티켓 ID가 있습니다. 하지만 표(table)에는 stepsToReproduce 필드에 다음 중 하나가 포함되어 있었다:
"앱에서 로그아웃되는 원인이나 시나리오를 구체적으로 제공해 주세요."(봇의 자체 명확화 질문이 고객이 말한 것처럼 저장됨), 또는"앱 사용 중."(빈 내용)

원인은 내 프롬프트 자체의 긴장감(tension) 때문이었다. 봇이 고객에게 캐묻는 것을 막기 위해, 나는 단계 규칙을 관대하게 만들었다: _"결제 버튼을 클릭할 때 충돌합니다(It crashes when I click Pay)\
- 탐색적 도구 호출(Exploratory tool calls). 모델이 때때로 필드가 누락된 상태로
create_bug_report를 호출했는데, 이는 의도적인 것이었습니다. 이렇게 하면 Lambda의 유효성 검사 오류가 어떤 내용을 요청해야 하는지 알려주었습니다. 고객에게 보이는 동작은 괜찮았지만, 메커니즘 자체에 문제가 있었습니다. 새로운 규칙: 실패한 도구 호출이 누락된 필드를 발견하는 방법이 될 수는 없습니다. - 추론 누출(Reasoning leaks). Nova Pro는 응답을
<thinking>…</thinking>로 여는 경향이 있었습니다. "추론 과정을 절대 보여주지 말라"는 규칙을 프롬프트의 가장 상단으로 옮기자 그 빈도가 많이 줄었습니다. 하지만 완전히 제거하지는 못했습니다.
시작할 때 스스로에게 말하고 싶은 것들
1. 도구를 사용하는 에이전트(tool-using agents)의 경우, 텍스트가 아닌 부작용(side effects)을 단언하라.
정확성 판별자(correctness judge)는 응답을 읽습니다. 빈 필드나 꾸며낸 필드를 감싸고 있지만 자신감 있고 도움이 되는 것처럼 들리는 응답이 높은 점수를 받습니다. 가장 중요했던 결함들은 측정 지표에는 보이지 않았지만 데이터베이스에서는 명확했습니다. 다음번에는 테스트 스위트가 저장된 행(stored row)을 확인합니다.
2. 단일 턴 평가(Single-turn evals)는 다중 턴 동작(multi-turn behaviour)을 테스트하지 못한다.
제가 가장 중요하게 수정한 두 가지 수정 사항은 모두 다중 턴 수집과 관련되었는데, 평가 프롬프트 13개 모두가 단일 턴이었습니다. Run 2의 동일한 0.92 점수는 이 수정 사항들이 비용이 들지 않았고 점수가 재현 가능하다는 것만 알려주었을 뿐입니다. 그것들이 작동했다는 것을 알려주지는 못했습니다.
3. 긴 프롬프트는 불균형하게 성능이 저하된다.
탐욕적 디코딩(greedy decoding)을 사용했음에도 불구하고, 긴 프롬프트 중간에 숨겨진 규칙들은 어떤 세션에서는 지켜졌고 다른 세션에서는 지켜지지 않았습니다. Nova Pro를 이용한 다중 턴 티켓 작성은 여전히 신뢰하기 어려웠습니다. 다음 반복 작업(iteration)은 더 많은 규칙을 추가하는 것이 아니라, 더 짧고 재구성된 프롬프트가 될 것입니다.
4. 시간 소모가 컸던 몇 가지 AgentCore 세부 사항들:
- Gateway에서 허용하는 대상 이름(target names)은 문자, 숫자, 밑줄(_)만 가능합니다. 하이픈(-)은 Nova 도구 호출을 끊어버립니다. 이 경우 _"Model produced invalid sequence as part of ToolUse"_라는 오류가 발생합니다.
- Gateway는 도구 인자(tool arguments)를 Lambda 이벤트로 직접 전달합니다. Agents Classic의
parameters래퍼 구조를 사용하지 않습니다. 도구 이름은 `context.client_context.custom[
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기