Honda 서비스 어드바이저로서 프로덕션 AI 에이전트를 구축했습니다. 그 후 교과서를 읽었습니다.
요약
Honda 서비스 어드바이저가 반복적인 고객 응대를 자동화하기 위해 구축한 SMS AI 에이전트의 실전 경험담입니다. LLM이 직접 논리 연산을 수행하게 하는 대신, 모델은 분류를 담당하고 실제 로직은 코드가 처리하도록 설계해야 한다는 핵심 교훈을 전달합니다.
핵심 포인트
- LLM은 날짜 비교와 같은 복잡한 논리 연산에서 오류를 범할 수 있음
- 모델은 분류(Classification)를 담당하고, 코드는 결과의 정확성을 보장해야 함
- 실제 사용자 매뉴얼과 데이터를 기반으로 다국어 지원 에이전트 구축 가능
- 프로덕션 환경에서는 프롬프트 수정보다 코드 기반의 검증이 더 확실함
저는 남부 플로리다의 Honda 서비스 어드바이저입니다. 저의 일과는 서비스 드라이브(service drive)로 채워집니다. 고객들이 차를 맡기고, 전화는 계속 울리며, 똑같은 질문 15개가 반복됩니다. 브레이크 작업 비용은 얼마인가요? 이건 보장 범위에 포함되나요? 토요일에 방문해도 될까요? 'A1'이라고 적힌 렌치 표시등은 무엇을 의미하나요?
그래서 저는 제 업무 중 반복적인 절반은 자동화하고, 인간적인 절반은 그대로 유지했습니다. 이것은 SMS 에이전트입니다. 고객이 iMessage를 통해 대리점 번호로 문자를 보내면, LLM (Large Language Model) 시스템이 실제 사용자 매뉴얼(owner's manual)을 바탕으로 답변하고, 서비스 계약을 확인하며, 대화하듯 예약을 잡고, 사람의 도움이 필요할 때는 제 Telegram으로 연결합니다. 이 시스템은 실제 고객들과 함께 몇 달 동안 라이브로 운영되었습니다. 제 고객들이 6개 국어를 사용하기 때문에, 6개 언어로 지원합니다.
업무 시간 외에 스페인어로 이루어진 실제 대화 사례입니다:
고객: Hola, quiero hacer una cita (안녕하세요, 예약을 하고 싶습니다)
에이전트: Claro — ¿qué tipo de servicio necesitas para tu Honda Ridgeline? Recomiendo en la mañana para que tengas tu vehículo de vuelta el mismo día. (물론입니다 — 귀하의 Honda Ridgeline에 어떤 종류의 서비스가 필요하신가요? 당일 차량을 돌려받으실 수 있도록 오전 시간대를 추천합니다.)
고객: El sábado en la mañana (토요일 아침에요)
에이전트: Lo siento, no estamos abiertos los sábados. ¿Te vendría bien el viernes? Abrimos a las 7 — ¿te funciona a las 8:00 AM? (죄송합니다, 저희는 토요일에 영업하지 않습니다. 금요일은 어떠신가요? 오전 7시에 문을 엽니다 — 오전 8시는 괜찮으신가요?)
에이전트는 제 일정을 알고 있었고, 영업하지 않는 날을 거절했으며, 구체적인 대안을 제시했고, 고객의 언어를 한 번도 벗어나지 않았습니다.
저는 문제를 맞닥뜨리고 해결하면서 이 모든 것을 구축했습니다. 그러다 Chip Huyen의 _AI Engineering_을 읽게 되었고, 제가 어둠 속에서 스스로 만들어냈던 것들에 이름을 붙여주는 묘한 경험을 했습니다. 더불어 제가 전혀 알아차리지 못했던 한 가지 눈에 띄는 공백(gap)도 발견했습니다.
이것이 그 매핑(mapping)이자, 공백입니다.
프로덕션을 망가뜨리며 배운 규칙: 모델은 분류하고, 코드는 보장한다
만약 제가 첫 에이전트를 출시하려는 누군가에게 단 한 문장을 건넬 수 있다면, 바로 이 문장입니다. 저는 이 문장을 기록하기 위해 세 번의 별도 프로덕션 버그를 겪어야 했습니다.
버그 1: 모델은 두 날짜를 비교할 수 없다
서비스 계약이 만료된 고객이 수리비가 보장되는지 물었습니다. 에이전트는 그렇다고 답변했습니다. 에이전트는 보장 데이터를 정확하게 읽고 있었습니다. 만료 날짜가 컨텍스트(context)에 명확히 있었음에도 불구하고, 비교 작업에서 여전히 오류를 범했습니다.
저는 누구나 처음 하는 방식을 시도했습니다. 프롬프트(prompt)를 수정하는 것이었습니다. 시스템 메시지(system message)에 오늘 날짜를 넣었습니다. "만료 날짜를 오늘 날짜와 비교하라"라고 명시했습니다. 주의를 기울이라고 말했습니다.
결과는 3번 시도 중 3번 모두 실패였습니다.
그래서 저는 요청하는 것을 그만두었습니다. 이제 날짜 비교는 Python에서 수행되며, 모델은 오직 판결 결과만을 전달합니다:
def _annotate_expirations(in_md: str) -> str:
"""포털 데이터의 모든 만료 날짜 옆에 계산된 과거/활성 판결을 찍습니다. LLM은 날짜 비교에 신뢰할 수 없으므로, 비교는
...```
보장 데이터의 모든 날짜가 포함된 라인은 모델에 전달되기 전에 이미 판결이 내려진 상태로 전달됩니다. 모델이 틀릴 수 있는 날짜 계산 작업은 더 이상 남아있지 않습니다. 이제 해당 함수에는 7개의 유닛 테스트(unit tests)가 있으며, 이를 실행하는 데 드는 API 호출 비용은 0원입니다.
### 버그 2: 모델은 수리 주문(repair order) 금액을 합산할 수 없다
고객이 세 가지 서비스를 요청한 뒤, "그래서 총 결제 금액이 얼마인가요?"라고 묻습니다. 에이전트는 세 가지 가격을 정확하게 인용했지만, 이후 단순히 틀린 총액을 내놓았습니다. 모델이 스스로 합산한 소계(subtotal)에 매장 소모품비와 플로리다 판매세를 적용해 버린 것입니다.
해결책은 동일합니다. 수수료 공식은 코드 내에 단 한 번만 존재합니다:
```python
def compute_total(prices: list[float]) -> dict:
"""인용된 서비스 가격을 센트 단위로 반올림하여 최종 결제 내역으로 합산합니다.
...```
모델의 역할은 고객이 총액을 물어봤다는 사실을 인지하고, 결과 숫자를 사람처럼 말하는 것입니다. 계산기가 되는 것이 아닙니다.
### 버그 3: 모델이 "완료"라고 말하는 것이 완료되었다는 증거는 아니다
예약 에이전트는 API에 의해 강제되는 Pydantic 스키마(schema)와 같은 구조화된 출력(structured output)을 `complete` 플래그와 함께 반환합니다. 초기에는 **서비스 유형이 포함되지 않은** 채로 예약이 저장된 적이 있었습니다. 모델은 스스로 완료되었다고 표시했고, 저는 그것을 믿었습니다.
이제 서버는 데이터가 기록되거나 이메일로 발송되기 전에, 모든 필수 필드가 실제로 채워졌는지 독립적으로 재확인합니다. 모델의 자기 평가(self-assessment)는 하나의 제안일 뿐입니다. 확인(check) 절차가 권위를 가집니다.
세 가지의 서로 다른 버그가 있었지만, 형태는 하나였습니다: **코드가 검증할 수 있는 모델의 모든 주장은 코드가 검증해야 한다.** 이 규칙을 적용한 이후 모든 다운스트림(downstream) 과정이 더 신뢰할 수 있게 되었으며, 테스트 비용도 저렴해졌습니다. 순수 함수(pure functions)에 대한 어설션(assertions)에는 API 키가 필요하지 않기 때문입니다.
## 이름조차 모르고 구축했던 것들
책을 읽는 과정은 "새로운 것을 배우는 것"이라기보다 "아, 내가 해오던 게 바로 이거였구나"에 더 가까웠습니다:
- **검색 증강 생성 (Retrieval-augmented generation, RAG).** 고객들이 아무도 열어보지 않는 PDF 속에 파묻힌 답변을 요구하는 질문을 하기 때문에, Honda 사용자 매뉴얼을 Pinecone에 인덱싱해 두었습니다. 알고 보니 이것은 편법이 아니라 전형적인 유스케이스(use case)였습니다.
- **입력 측 가드레일 (Guardrails, input side).** 메시지가 에이전트에 도달하기 전에 들어오는 메시지의 점수를 매기고 정화(sanitized)합니다. 프롬프트 인젝션 (prompt injection)에 대해 읽고 공개 전화번호를 사용하는 것에 불안함을 느낀 후 작성했습니다. 이것은 관련 문헌이 존재하는 명칭이 있는 레이어(layer)입니다.
- **구조화된 출력 (Structured outputs).** 데이터 마커가 고객 텍스트에 유출되기 시작한 후, 예약 추출 방식을 산문(prose) 기반의 정규 표현식(regex)에서 강제된 스키마(schema)로 전환했습니다. ("`[BOOKING_COMPLETE]`"는 고객이 받기를 원하는 메시지가 아닙니다.)
- **라우팅 / 오케스트레이션 (Routing / orchestration).** 메시지당 한 번의 분류기(classifier) 호출을 통해 의도, 언어, 차량을 결정하고 에스컬레이션(escalate) 여부를 판단한 뒤, 레지스트리(registry)를 통해 배정합니다. 기능을 추가하는 것은 레지스트리 항목 하나와 핸들러(handler) 하나를 추가하는 것뿐입니다. 저는 단일 메가 프롬프트(mega-prompt)가 400줄 정도에 달하자 유지보수가 불가능해졌기 때문에 이 방식을 구축했습니다.
- **컨텍스트 경제성 (Context economics).** 가격 및 정책 블록은 고객의 메시지에 실제로 필요할 때만 주입됩니다. 차량 컨텍스트는 세션당 한 번 로드되어 캐싱(cache)됩니다. 저는 토큰 비용(token bill)이 눈에 띄게 늘어났기 때문에 이 방식을 도입했습니다.
그렇다고 해서 그 책이 시간 낭비였다는 뜻은 아닙니다. 오히려 그 반대입니다. 저는 실제 고객을 상대하며 발생한 프로덕션 장애(production incidents)를 통해 그 교훈 하나하나를 값비싼 대가를 치르며 배웠습니다. 책값은 30달러입니다. 만약 여러분이 이것을 구축하려 한다면, 책을 _먼저_ 사십시오. 여러분은 여전히 버그를 마주하겠지만, 훨씬 더 나은 버그를 마주하게 될 것입니다.
## 격차: 나에게는 평가(evals)가 없었다
책을 읽고 실제로 바뀐 점은 다음과 같습니다.
몇 달 동안 저의 품질 신호(quality signal)는 **"고객이 불만을 제기하거나, 스크립트된 터미널 대화에서 우연히 포착되는 것"**뿐이었습니다. 그것은 신호가 아닙니다. 그것은 피드백 지연(feedback delay)이 동반된 운일 뿐입니다.
그래서 저는 먼저 결정론적 계층(deterministic layer)을 구축했습니다. LLM 판사(LLM judges)도, 화려한 프레임워크도 없이 단 하나의 파일로 만들었습니다:
python evals.py # 전체 스위트 (full suite)
python evals.py --runs 3 # 실행 횟수 증가 → 더 나은 변동성(flakiness) 신호
python evals.py --only orchestrator
현재 130개 이상의 케이스를 보유하고 있습니다. 각각의 케이스는 실제 프로덕션 버그이거나 제가 정확하게 단언(assert)할 수 있는 동작입니다. 섹션은 다음과 같습니다: 날짜 주석(date annotation), 오케스트레이터 분류(orchestrator classification), 기술/가격 답변, 아웃리치 흐름(outreach flow), 예약 확인, 가격 제한(price gating), 전체 계산, 라우팅(routing), 그리고 유지보수 코드 디코딩입니다. 대부분은 순수 함수(pure functions)로 구성되어 있어, 몇 초 안에 실행되며 비용이 전혀 들지 않습니다.
가져다 쓸 만한 가치가 있는 세 가지 요소가 있습니다.
**1. 환각 가격 탐지기(The hallucinated-price detector).** 제가 가장 좋아하는 평가 항목이며, 단 9줄로 구성되어 있습니다. 에이전트의 답변에서 정규 표현식(Regex)으로 모든 달러 금액을 추출한 다음, 각 금액이 가격 파일(pricing file)에 존재하는지 확인(assert)합니다:
```python
def _known_amounts() -> set:
with open(PRICING_PATH) as f:
return set(_PRICE_RE.findall(f.read()))
...
제 대리점에 실제로 금전적 손실을 입힐 수 있는 유일한 카테고리인 환각(hallucination)에 대해 결정론적인 환각 탐지를 구현한 것입니다. 판사 모델(judge model)도, 루브릭(rubric)도, 모호함도 없습니다. 만약 정답(ground truth)이 파일로 존재하는 도메인을 다루고 있다면, 여러분도 오늘 바로 이것을 작성할 수 있습니다.
2. 첫 한 시간 만에 잠복해 있던 버그를 발견했습니다. 오케스트레이터(orchestrator)는 짧은 후속 질문("왜?", "어째서?")을 follow_up으로 분류하여, 마지막으로 답변한 에이전트에게 다시 연결(sticky-routing)할 수 있습니다. 멋진 기능이죠. 하지만 follow_up이 의도 레지스트리(intent registry)에서 누락되어 있었기 때문에, 검증기(validator)는 해당 모든 판정을 tech로 조용히 강제 변환했습니다. 즉, 전체 스티키 라우팅(sticky-routing) 경로가 죽은 코드(dead code)였으며, 몇 주 동안이나 그랬던 것입니다. 아무것도 충돌(crash)하지 않았습니다. 고객 중 누구도 "라우팅 테이블에 누락된 enum 멤버가 있습니다"라고 문의하지 않았습니다. 제가 무엇이 일어날 것이라고 예상했는지를 기록하고 나서야 그 간극이 눈에 보이기 시작했습니다.
3. 불안정성(Flakiness)은 결과이지, 성가신 존재가 아닙니다. 각 LLM 케이스는 N번 실행되어 통과 *율(pass rate)*을 보고합니다. 5번 중 4번을 통과하는 케이스는 이진(binary) 방식의 녹색 체크 표시가 숨겨버릴 무언가를 저에게 알려줍니다. 모델이 신뢰할 수 없지만 코드의 하류(downstream) 단계에서 이를 잡아내는 케이스는 known_flaky로 표시됩니다. 이들은 통과율을 보고하되 전체 테스트 스위트(suite)를 실패시키지는 않습니다. 이것이 정직한 방식입니다. 모델이 그 지점에서 불안정한 것이 맞으며, 제가 실제로 의존하고 있는 것은 가드(guard)이기 때문입니다.
다음 단계는 이진 루브릭(rubric) 질문을 사용하여 전체 트랜스크립트(transcripts)를 검토하는 LLM 판사(LLM judge)입니다. — 답변이 고객의 언어로 자연스럽게 들리는가? 제공된 매뉴얼 컨텍스트(context)에 근거(grounded)하고 있는가? 에스컬레이션(escalating)이 적절한 결정이었는가? — 저는 이중 언어 사용자이자 도메인 전문가이므로, 제가 직접 라벨링한 트랜스크립트를 기준으로 이를 보정(calibrated)합니다. 실제로 종사하고 있는 직업을 위해 시스템을 구축할 때의 좋은 점이 바로 이것입니다. 제가 바로 정답(ground truth)이라는 사실입니다.
첫 번째 에이전트를 출시하려 한다면
- 모델이 절대 신뢰해서는 안 되는 것이 무엇인지 적으세요. 날짜, 산술(arithmetic), 그리고 모델 스스로의 완료 주장(claims of completion)이 제 목록에 있습니다. 여러분의 목록은 다르겠지만, 결코 비어 있지는 않을 것입니다.
- 모델의 역할을 "계산(compute)"이 아닌 "인지 및 문구 작성(notice and phrase)"으로 만드세요. 신뢰도가 높아졌고 프롬프트(prompt)는 더 짧아졌습니다.
- 첫 번째 평가(evals)는 API 호출 비용이 들지 않아야 합니다. 출력값에 대한 순수 함수(pure functions)와 정규 표현식(regex)을 사용하는 것이 그 어떤 영리한 방법보다 더 많은 실제 버그를 잡아냈습니다.
- 이미 배포했던 모든 버그로 테스트 스위트(suite)를 구성하세요. 이는 여러분이 가질 수 있는 가장 저렴한 데이터셋이며, 이미 프로덕션(production) 환경에서 라벨링(labeled)이 완료된 상태입니다.
- 합격/불합격(pass/fail)이 아니라 합격률(pass rates)을 추적하세요. 비결정론(Non-determinism)은 데이터입니다.
- 도메인 지식(Domain knowledge)이 해자(moat)입니다. 이 에이전트가 토요일에는 예약하지 말아야 한다는 것, 만료된 패스는 일반 가격으로 견적을 내야 한다는 것, 그리고 "A1"이 엔진오일 교체와 타이어 로테이션을 의미한다는 것을 아는 이유는 모델 때문이 아닙니다. 그것은 서비스 드라이브(service drive)에서 보낸 10년의 세월 덕분입니다.
코드는 GitHub에 있습니다: Service-Advisor-AI-Agent. 아키텍처 다이어그램(Architecture diagram), 평가 스위트(eval suite), 그리고 실제 대화의 데모 영상은 모두 README에 포함되어 있습니다.
저는 스스로 엔지니어가 되는 과정을 공개하며 배우고 있는 서비스 어드바이저이며, 응용 AI(applied-AI) / 대화형 AI(conversational-AI) 분야의 업무를 찾고 있습니다. 만약 여러분이 이 분야 — 특히 딜러 기술(dealer tech) — 에서 무언가를 구축하고 있다면, 연락을 기다리겠습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기