LLM은 서술하고, 코어가 결정한다: AI 결제 프로세스의 구조 분석
요약
AI 에이전트의 결제 프로세스 구축 시 LLM은 대화를 주도하되, 실제 결정은 결정론적 코어(deterministic core)가 담당하는 구조를 분석합니다. 텍스트, 음성, 전화 등 다양한 채널에서 일관된 결제 경험을 제공하기 위한 도구 호출 및 상태 관리 설계 방식을 설명합니다.
핵심 포인트
- LLM은 대화 흐름을 관리하고, 결정론적 코어가 최종 결정을 내리는 구조 설계
- 도구 호출(shopping_cart, collect_user_info)을 통한 정형화된 데이터 처리
- 프롬프트가 아닌 서버 측 출처(provenance)를 기반으로 한 데이터 신뢰성 확보
- 다양한 인터페이스(채팅, 음성, 전화)에서 동일한 프로토콜 유지
채팅창, 음성 세션, 실시간 전화 통화에서 동일한 프로토콜을 실행하는 결제 시스템을 구축한 방법.
한 단어, 다양한 움직임
제가 작업하고 있는 AI 스토어프론트 (storefront) 플랫폼에서는 고객이 채팅창에 "결제"라고 입력하거나, 음성 에이전트 또는 전화 통화로 말하면, 짧은 대화 후에 주문 번호를 받게 됩니다. 대화 기록(transcript)은 평범해 보입니다. 특별 요청 사항에 대한 질문, 아마도 연락처 양식, 그리고 간단한 "모든 것이 정확한가요?"라는 확인 절차로 끝납니다.
하지만 그 짧은 교환 이면에는 시스템에서 가장 정교하게 설계된 경로가 숨겨져 있습니다. 결제(Checkout)는 언어 모델 (Language Model), 여러 도구 (tools), 주문 엔진 (order engine), 그리고 길을 걷고 있을지도 모르는 사람이 방금 무슨 일이 일어났는지에 대해 모두 동의해야 하는 지점입니다. 여기서 발생하는 어떠한 불일치도 실제 비용 손실로 이어집니다.
이 포스트에서는 우리의 접근 방식, 즉 그것이 무엇인지, 왜 제약 조건들이 이러한 특정한 형태를 강제하는지, 그리고 텍스트와 음성에서 각 요소가 어떻게 함께 움직이는지를 설명합니다. 그런 다음, 설계는 대안과 비교했을 때 비로소 의미를 갖기 때문에, 일반적으로 이 기능이 구축되는 다른 두 가지 방식과 비교해 보겠습니다.
파트 I: 접근 방식
전체 설계는 한 문장으로 요약됩니다: 언어 모델 (Language Model)은 대화를 진행하지만, 결정론적 코어 (deterministic core)가 모든 결정을 내린다.
구체적으로, 결제 과정에는 모델이 호출할 수 있는 두 가지 도구가 포함됩니다:
shopping_cart— 장바구니 작업이며,submit이 결과적인 동작입니다.collect_user_info— 연락처 세부 정보를 위한 작은 하위 프로토콜로, 네 가지 동작이 있습니다:get(현재 상태를 읽되 아무것도 렌더링하지 않음),show(양식을 열며, 미리 채워져 있을 수 있음),update(한 필드를 채움), 그리고confirm(저장 및 닫기).
모델이 shopping_cart(action="submit")을 호출하면, 해당 요청은 모든 채널에서 공유되는 단일 서버 측 함수로 전달됩니다. 이 코어는 누가 구매하고 있는지 확인하고, 여전히 물어봐야 할 것이 있는지 결정하며, 산문(prose)이 아닌 **타입화된 결과 (typed outcome)**로 응답합니다. 다섯 가지 결과가 모든 케이스를 커버합니다:
NEED_CUSTOMER_INFO— 사용 가능한 연락처 정보가 없음; 양식을 엽니다.CONFIRM_CUSTOMER_INFO— 이전에 기록된 연락처가 있음; 이를 다시 읽어주고 확인을 받습니다.CART_EMPTY— 결제할 항목이 없음.SUBMITTED— 주문이 존재함; 주문 번호는 다음과 같습니다.- 실패 — 모델이 전달할 수 있는 이유 코드 (reason code)를 포함합니다. 가장 흔한 것은
NEED_ORDER_DETAILS입니다: 주문 질문이 던져지기 전에 주문이 제출되었으므로, 주문 엔진 (order engine)이 거부하며 모델은 다시 돌아가 질문을 던집니다.
결제 결정 (checkout decision)을 유도하는 고객 데이터는 모델의 프롬프트 (prompt)에 절대 존재하지 않습니다. 주문을 위해 사용된 연락처는 도구 결과 (tool results)에 실려 전달되며, 코어 (core)는 프롬프트가 말하는 내용이 아니라 출처 (provenance)를 통해 서버 측에서 이를 해결합니다. 그 외에 프롬프트는 각 신호에 반응하기 위한 프로토콜 (protocol)만을 보유합니다. 그리고 모든 재제출 (resubmission)에는 **누적된 플래그 (accumulated flags)**가 포함됩니다: 주문 질문이 완료되면 orderDetailsConfirmed=true, 고객이 기록된 세부 정보를 승인하면 contactConfirmed=true가 됩니다. 이 플래그들은 멀티턴 협상 (multi-turn negotiation)이 맴도는 것을 방지하는 방법입니다. 이미 결정된 모든 사항은 앞으로 전달되므로, 어떤 단계도 반복되지 않으며, 코어가 플래그 없이는 진행하지 않기 때문에 어떤 단계도 건너뛸 수 없습니다.
그 "프롬프트에 절대 존재하지 않는다"는 말에는 한 가지 전제 조건이 필요합니다. 텍스트 및 SMS의 경우, 프롬프트는 주변 인지 (ambient awareness)를 위해 고객 프로필, 장바구니, 위치 정보가 포함된 세션 컨텍스트 (session-context) 블록을 포함합니다. 공급자 설정이 고정되어 있는 음성 (voice)의 경우, 그마저도 없습니다. 어느 쪽이든 코어는 이를 무시합니다. 프롬프트에 데이터가 존재한다고 해서 그것을 사용하는 권한이 주어지는 것은 결코 아닙니다.
이것이 접근 방식입니다. 모델은 서술하고, 코어가 결정합니다. 이 포스트의 나머지 내용은 이러한 분할의 이면에 있는 추론과 이를 작동하게 만드는 메커니즘에 관한 것입니다.
제2부: 왜 이러한 형태인가
네 가지 제약 조건이 우리를 이 방향으로 이끌었으며, 그중 적어도 세 가지는 거래를 수행하는 에이전트 (agent)를 구축하는 누구에게나 적용된다고 생각합니다.
1. 채널은 당신이 포크(fork)하는 모든 것을 증폭시킵니다. 동일한 상점이 웹 채팅, 임베디드 위젯(embeddable widget), 모바일 앱, 앱 내 음성, 실제 전화 통화, 그리고 SMS를 통해 판매를 진행합니다. 초기에는 각 채널마다 모델과 비즈니스 로직(business logic) 사이를 연결하는 고유의 접착제(glue)가 있었으며, 이 접착제들은 서로 어긋나기 시작했습니다. 한 감사 결과에 따르면, 이러한 어긋남(drift)이 초래하는 버그의 유형들이 발견되었습니다. 예를 들어, 특정 채널에서는 소유권 확인을 건너뛰지만 다른 채널에서는 확인하는 주문 조회 기능, 텍스트로는 구매가 가능하지만 음성으로는 거절당하는 전화 전용 고객, 그리고 실패를 성공한 것처럼 보고하는 특정 채널 등이 있었습니다. 이 중 어느 것도 설계상의 실수는 아니었습니다. 그것은 바로 동기화(synchronization) 실수였습니다. 해결책은 더 엄격한 검토 규율을 세우는 것이 아니었습니다. 결정 사항이 정확히 단 한 곳에만 존재하도록 만들어, 동기화할 대상 자체가 없도록 만드는 것이었습니다.
2. 음성 프롬프트는 고정되어 있습니다. 우리의 음성 에이전트(voice agents)는 제3자 대화형 AI(conversational AI) 공급업체 위에서 작동하며, 이들의 시스템 프롬프트(system prompts)는 공급업체의 시스템에 지속적으로 유지되는 구성(configurations)으로 컴파일됩니다. 통화 시점에 프롬프트에 "이 고객의 이름은 Pat입니다"와 같은 내용을 주입할 수 없습니다. 이는 LLM 앱 설계에서 가장 흔히 쓰이는 기술인 '프롬프트 개인화(personalizing the prompt)'를 불가능하게 만들며, 대신 더 깔끔한 방식을 강제합니다. 즉, 프롬프트는 정적으로 유지되며 오직 프로토콜(protocol)만을 설명하고, 고객별 정보는 모두 도구 결과(tool results)에 실려 전달되어야 합니다. 몇 주 전에 작성된 프롬프트가 오늘의 통화자를 처리할 수 있는 이유는, 오늘의 통화자 정보가 지시 사항(instructions)이 아닌 데이터(data)로서 전달되기 때문입니다.
3. 전화 통화에는 화면이 없습니다. 텍스트 채팅에서 "고객의 정보를 수집하세요"라는 말은 양식(form)을 렌더링하는 것을 의미할 수 있습니다. 하지만 전화 통화에서는 양식도, 버튼도, 탭할 수 있는 그 무엇도 없습니다. 따라서 프로토콜의 모든 단계는 순수하게 대화로 가능한 대응 수단이 필요하며, 이는 결과적으로 좋은 설계를 위한 강제 함수(forcing function)가 됩니다. 만약 어떤 단계가 말로 내뱉었을 때 성립되지 않는다면, 그것은 아마도 모호함을 가리기 위해 UI에 의존하고 있었을 가능성이 높습니다.
4. 모델에게 돈을 맡길 수는 없습니다. 이는 비난이 아니라 — 엔지니어링 측면의 사실입니다. 언어 모델 (Language Model)은 대화 초반에 본 내용을 바탕으로 고객의 이메일 주소를 채워 넣거나, 아무 일도 일어나기 전에 주문이 완료되었다고 발표하거나, 타임아웃(time-out)된 제출을 재시도하여 주문을 두 번 생성할 수도 있습니다. 이것들은 프롬프트 (Prompt)로 해결할 수 있는 예외 케이스 (Edge case)가 아닙니다. 모델이 모델답게 행동하는 것입니다. 누구의 연락처를 사용할지, 주문을 넣을지 말지, 이 제출이 이미 이루어졌는지와 같이 비용이 발생하는 결정은 결정론적 (Deterministic)으로 내려질 수 있고 테스트가 가능한 곳에 존재해야 합니다.
제3부: 작동 방식
텍스트에서의 안무 (Choreography)
다음은 실제 일어나는 일을 주석으로 달아놓은 처음 방문한 게스트의 결제 과정입니다:
고객: checkout
모델: "주문에 대한 특별 요청 사항이 있으신가요?" ← 프로토콜 (Protocol): 항상 먼저 질문할 것
...
두 가지 주목할 점이 있습니다. 모델은 자유 형식의 채팅 (Free-form chat)으로 고객의 이메일을 묻지 않았습니다. 구조화된 인터페이스 (Structured surface)가 사용 가능했기에, _양식 (Form)_이 이를 수집하고, 검증하고, 정규화 (Normalize)했습니다. (전화 통화에서는 이러한 인터페이스가 존재하지 않으며 말로 묻는 것이 전부입니다 — 그 상황에서도 무엇이 구조화된 상태로 유지되는지는 나중에 다시 다루겠습니다.) 그리고 두 번째 submit이 통과되었을 때도 여전히 orderDetailsConfirmed=true를 유지했습니다. 확정된 사항은 그대로 유지됩니다.
이제 다음 주에 다시 돌아온 동일한 고객의 경우입니다:
모델 → shopping_cart(submit, orderDetailsConfirmed=true)
코어 (Core): 이전 방문 시 파일에 저장된(ON FILE) 연락처 발견
→ 아직 신뢰할 수 있는 재구매자는 아님
...
파일에 저장된 값들은 _도구 결과 (Tool result) 내부_로 전달되었습니다. 모델은 이를 기억하는 대신 다시 읽어 들인 것입니다. 텍스트에서는 이 디테일이 작아 보일 수 있습니다. 하지만 음성 (Voice) 환경에서는 이것이 승패를 가르는 핵심입니다.
음성: 같은 악보, 다른 연주
음성이라고 해서 프로토콜이 변하는 것은 없습니다 — 그것이 핵심입니다. 신호는 동일하며, 차이점은 각 측면이 자신의 역할을 수행하는 방식뿐입니다.
코어가 CONFIRM_CUSTOMER_INFO에 응답할 때, 그 결과에는 에이전트가 말할 수 있도록 이름, 이메일, 전화번호가 포함됩니다. 고객이 변경을 원할 때 키보드는 없으므로, 양식은 음성으로 채워집니다. 즉, 에이전트는 고객이 말하는 각 필드에 대해 collect_user_info(update)를 호출한 다음, confirm을 호출하여 저장하고 종료합니다. 우리의 웹 및 앱 인터페이스에서는 고객이 대화하는 동안 화면상의 양식이 채워지는 것을 실제로 지켜봅니다. 이는 텍스트 흐름(text flow)이 사용하는 것과 동일한 양식이지만, 이를 다루는 주체(hands)만 다를 뿐입니다.
이를 통해 하나의 프로토콜에 대한 세 가지 체제(regimes)가 도출됩니다:
- 텍스트 (Text) — 고객이 양식에 직접 입력하고 확인(Confirm)을 누릅니다.
- 화면이 있는 음성 (Voice with a screen) — 고객이 지켜보는 동안 에이전트가
update호출을 통해 양식을 채웁니다. - 전화 통화 (A phone call) — 화면이 존재하지 않습니다.
show는 아무것도 렌더링하지 않으며, 전체 교환 과정은 말로 읽어주고 수정하는 방식으로 이루어집니다.
세 번째 체제는 주의 사항이 필요합니다. 왜냐하면 이곳은 "모델이 자유 텍스트(free text)로 질문하지 않는다"라는 말이 문자 그대로의 의미에서 벗어나는 지점이기 때문입니다. 전화 통화에서는 정보 수집이 진정으로 자유 형식(free-form)으로 이루어집니다. 에이전트가 이메일을 소리 내어 묻고, 고객이 이를 말합니다. 구조화된 상태로 유지되는 것은 바로 캡처(capture) 과정입니다. 각 음성 답변은 즉시 collect_user_info(update)를 통해 한 번에 하나의 필드씩 전달되며, 그 후 수정을 위해 다시 읽어줍니다. 이메일은 가장 정밀한 검토를 거칩니다. 잘못된 주소는 나중에 복구할 수 없기 때문에, 캡처되는 즉시 수정되고(
신뢰는 존재 여부가 아니라 출처(provenance)에 의해 제어됩니다. 코어(core)가 연락처 정보를 해결할 때, 단순히 _'내가 이것을 가지고 있는가?'_라고 묻지 않습니다. 대신 _'이것이 어디에서 왔는가?'_를 묻습니다. 명시적 인자(explicit argument)는 모델이 이번 턴(turn)에 이 정보를 전달받았음을 의미합니다. 대화 세션(conversation session)은 고객이 바로 이 채팅에서 몇 분 전에 해당 정보를 입력했음을 의미하며, 다시 물어봄으로써 고객을 모욕하지 마십시오. 파일에 기록된 레코드(record on file)는 이전 방문 시의 정보임을 의미합니다. 이를 사용하기 전에 다시 읽어주고 확인하십시오. 동일한 세 개의 필드이지만, 세 가지 서로 다른 동의 수준을 가집니다. 데이터의 존재가 곧 사용 권한을 의미하지는 않습니다.
획득된 신뢰에는 수치가 있습니다. 모든 주문마다 연락처를 재확인하는 것은 단골 고객에게 마찰(friction)이라는 벌을 주는 것과 같습니다. 따라서 명시적인 기준이 존재하며, 다음 세 가지가 동시에 충족되어야 합니다: 고객의 가장 최근 주문 3건이 모두 동일한 연락처 정보를 포함하고 있어야 하며, 그 세 건의 정보가 현재 파일에 있는 연락처와 일치해야 하고, 이름, 이메일, 전화번호라는 세 가지 필드가 모두 존재해야 합니다. 모든 비교는 정규화(normalization) 후에 이루어집니다. 왜냐하면 PAT@x.com과 (555) 010-3985는 각각 pat@x.com 및 5550103985와 일치해야 하기 때문입니다. 이 중 하나라도 누락되면 — 즉, 세 개의 주문 중 하나라도 다르거나, 전화번호가 수집된 적이 없는 경우 — 우리는 다시 질문합니다. 이 임계값(threshold)은 영리한 것이 아니라, 핵심은 그것이 _명시적(explicit)_이며 단위 테스트(unit test)를 통해 유지될 수 있다는 점입니다.
증거(Evidence)는 소비됩니다. 하나의 미묘한 문제가 큰 교훈을 남겼습니다. "이 대화에 참여했습니다"라는 세션은 _증거 토큰(evidence token)_입니다. 이는 고객이 방금 자신을 누구인지 우리에게 말했음을 보증합니다. 우리는 이 토큰이 그것이 보증하는 주문보다 더 오래 지속되는 버전을 출시했었고, 그 결과 동일한 대화 내의 두 번째 주문이 첫 번째 주문의 증거에 올라타 고객과의 접촉 상호작용 없이 그대로 통과되는 일이 발생했습니다. 해결책은 다음과 같습니다: 성공적인 주문은 토큰을 _소비(spends)_합니다 (실패한 주문은 토큰을 유지하여, 재시도 시 다시 묻지 않도록 합니다). 토큰을 소비한다고 해서 고객을 다시 빈 양식으로 돌려보내지는 않는다는 점에 유의하십시오. 그 시점에는 이미 고객의 세부 정보가 파일에 저장되어 있으므로, 동일한 채팅 내의 다음 주문은 재입력이 아닌 한 줄의 읽기(read-back)를 받게 됩니다. 단 10줄의 코드와 두 개의 테스트로 해결되었으며, 정확히 찾아봐야 할 곳이 단 한 군데뿐이었기에 단 한 번의 디버깅 세션만으로 문제를 찾아낼 수 있었습니다.
주문은 단순히 생성되는 것이 아니라, 점유(claimed)됩니다. 제출은 장바구니에 대한 원자적 점유(atomic claim)로 시작됩니다. 따라서 타임아웃이 발생하여 재시도되는 요청은 동일한 주문을 재현할 뿐, 쌍둥이 주문을 생성하지 않습니다. 모델이 아무리 재시도를 많이 하더라도, 코어는 멱등성(idempotent)을 유지합니다.
제4부: 비교를 위한 두 가지 대안
대안 1: 워크플로우 위저드 (workflow wizard)
전통적인 해답은 고정된 상태 머신(state machine)입니다. 즉, 장바구니 → 세부 정보 → 연락처 → 확인 → 결제 순으로 진행되며, LLM은 각 단계를 서술하는 역할로 축소됩니다. 이는 결정론적(deterministic)이고 안전하며, 지난 20년 동안 대부분의 결제 흐름이 구축된 방식입니다.
하지만 이는 대화와 접촉하는 순간 무너집니다. 결제 도중에 "잠시만요, 더 큰 사이즈도 있나요?"라고 묻는 고객은 정해진 대본을 벗어난 것이며, 위저드에는 이를 위한 상태(state)가 없습니다. "대신 제 업무용 이메일을 사용해 주세요"라는 요청은 흐름이 예상하지 못한 수정 사항입니다. 또한 위저드는 그 매체에 종속됩니다. 양식 기반의 상태 머신은 전화 통화로 렌더링될 자연스러운 방식이 없으므로, 각 채널은 자신만의 변형을 키워나가게 되고, 결국 당신은 서로 달라지는 N개의 복사본을 관리하는 상황으로 되돌아가게 됩니다.
대안 2: 자유 형식 에이전트 (free-form agent)
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기