44가지 가상의 크리에이터 답변을 실제 응답 코드로 처리하기 (전송되거나 작성된 내용은 없음)
요약
Nakodo는 크리에이터의 이메일에 대신 답장하며, 답변 생성 과정은 단순한 분류나 결정이 아닌 연결 지점에서의 복잡한 상호작용에 초점을 맞춥니다. 본 스크립트는 실제 전송되거나 저장되지 않는 가상의 시나리오를 통해 대화 흐름과 미묘한 협상 과정을 모델링합니다.
핵심 포인트
- 답변 생성은 분류기(classifier)와 결정 함수(decision function)의 연결 지점에서 복잡하게 발생한다.
- 가짜 데이터셋을 사용하여 실제 개인 정보 보호 문제를 회피하고, 의도적인 시나리오 테스트에 집중할 수 있다.
- 대화 스크립트는 '턴(turns)' 개념으로 구성되며, 이전 내용을 처리해야 다음 턴이 가능하다.
- 날짜와 같은 컨텍스트는 고정되어야 모델의 답변 변화를 예측 가능하게 테스트할 수 있다.
Nakodo는 브랜드를 대신하여 크리에이터의 이메일에 답장합니다. 답장이 도착하고, 모델이 이를 분류(classification)로 읽어들이며, 순수 함수(pure function)가 무슨 일이 일어날지 결정하고, 그 후에 무언가가 전송됩니다: 답변, 브랜드를 포함한 소개글, 수수료에 대한 역제안, 정중한 거절, 또는 브랜드가 직접 확인해야 하므로 아무것도 아닐 수도 있습니다.
이러한 체인에서 흥미로운 버그는 어느 한 부분 안에 있는 것이 아닙니다. 연결 지점(joins)에 있습니다. 분류기가 0.82의 신뢰도로 역제안 (counter_offer)이라고 말하면서 동시에 otherTerms도 채우고, 결정 함수는 수수료와 추가 커미션 요구가 단순한 수수료 협상이 아니라는 것을 알아차려야 합니다. 헤더에서 부재중(out of office)이 감지되었지만 그 아래 텍스트는 따뜻하고 열정적이어서 '예스'로 읽혀서는 안 됩니다. 크리에이터가 제안받은 금액보다 적은 금액을 요구합니다. 이것들은 함수의 단위 테스트(unit tests)가 아니라 대화에 대한 질문들입니다.
그래서 대화를 담고 있는 스크립트가 있습니다. 344줄, 44가지 시나리오, 전송된 내용 없음, 데이터베이스에 작성된 내용 없음, 그리고 유일한 모델 호출은 실제 호출뿐입니다.
A scenario is what they write, in order
type Scenario = {
id: string;
title: string;
...
turns가 핵심 아이디어입니다. 각 문자열은 크리에이터의 이메일 하나이며, 다음 이메일은 Nakodo가 이전 내용을 처리하여 대화를 열어두었을 경우에만 사용됩니다. 첫 번째 턴(turn) 이후 소개글로 끝나는 시나리오는 두 번째 턴까지 가지 못하며, 이것 자체가 보고 싶은 것입니다.
{
id: "N1",
title: "제한 내에서 요청 후 중간 지점에서 합의",
kind: "creators",
level: "negotiate",
creator: LENA,
turns: ["안녕하세요! 저를 생각해 주셔서 감사합니다. 바(bars) 사운드가 정말 마음에 들어요. 60-90초 통합 작업에 대한 제 요율은 £350입니다만. 괜찮을까요?",
"£320에서 만날 수 있을까요? 그러면 진행하는 데 문제가 없을 것 같아요."]
},
브랜드는 식물성 단백질 바를 만드는 Fernway와 펍의 테이블 예약을 받는 Tablely입니다. 둘 다 가상의 회사이며, 모두 .example 도메인에서 운영되고, 세 명의 크리에이터 역시 가상으로 만들어졌습니다. 이는 실제 답변을 기록하는 것보다 의도적인 선택입니다. 실제 크리에이터의 이메일은 특정 브랜드 캠페인에 속한 개인 데이터이므로 시간이 지나면 구식이 되고, 저장소(repository)에 커밋하거나 블로그 게시물에 인용할 수 없습니다. 가짜로 만든 답변은 여러분이 걱정하는 정확한 지점을 건드리도록 조정될 수 있지만, 기록된 실제 답변은 거의 그렇게 되지 않습니다.
날짜 역시 고정됩니다:
const TODAY = "2026-10-06";
만약 누군가 "저는 10월 20일까지 자리를 비웁니다"라고 말하는 시나리오라면, 오늘 날짜(today)를 고정해야 합니다. 그렇지 않으면 여러분이 잠든 사이에 올바른 답변이 바뀔 수 있습니다.
아무것도 전송되거나 작성되지 않음
이는 답변 코드가 자체적인 컨텍스트를 가져오지 않기 때문에만 가능합니다. 이 코드는 ThreadContext와, 대화 내용이 필요할 경우 이메일 배열을 받습니다:
// 주어진 대화에서, 오래된 순서로 (scripts/simulate-replies.ts는 가짜 답변을 시도함).
export async function feeReplyFor(
ctx: ThreadContext,
...
실제 경로는 스레드의 이메일을 로드하고 해당 함수를 호출하는 네 줄짜리 래퍼(wrapper)입니다. 스크립트는 같은 배열을 메모리에 구축합니다. 동일한 지침, 동일한 스키마, 동일한 가드 수량(amount guard), 동일한 폴백(fallback)이 적용됩니다. 프로덕션 코드 어디에도 시뮬레이션 모드 플래그가 없다는 점이 중요합니다. 플래그는 여러분이 테스트하지 않는 브랜치이기 때문입니다.
스크립트는 dotenv 실행 후에 필요한 것을 main 내부에서 동적으로 가져온 다음, 각 턴을 재생합니다:
const read = await readReply({ kind, level, today: TODAY, brandName, productSummary, settings, offer, handoffContact, channelTitle, foundAt, history, reply: { subject, body } });
const action = decideReply(read.classification, read.answer, {
aiAnswersSoFar: emails.filter((e) => e.kind === "answer").length,
...
그러면 action.type으로 스위치를 만들고 각 결과(outcome)마다 한 개의 팔을 두어, 어떤 일이 발생했을지 출력합니다. 수수료(fee)를 처리하는 부분은 실제 사다리(ladder)를 실행하고 그 추론 과정을 출력합니다:
━━━ N3 · 제한 초과, 최종 제안, 그리고 바로 그 이상 [크리에이터, 협상]
<마이크로 티어 수수료> (micro)
그들 (1):
...
꺾쇠괄호(< >)는 제가 추가한 것입니다. 금액은 가짜 캠페인의 자체 조건과 크리에이터의 오디언스에서 나오며, 모델이 요약(summary)과 이메일을 작성합니다.
여기에 있는 두 줄(read:와 decided:)이 전체 스크립트 값어치가 있습니다. read:는 모델이 생각한 내용과 그 확신도(confidence)를 나타내고, decided:는 규칙들이 그것을 가지고 무엇을 했는지 보여줍니다. 행동(behaviour)에 오류가 있을 경우, 이 두 줄은 프롬프트가 잘못되었는지 아니면 스위치가 잘못되었는지 즉시 알려주는데, 이는 매우 다른 문제입니다.
한 시나리오를 세 번 실행하기
pnpm tsx scripts/simulate-replies.ts // 모든 시나리오마다 실행
pnpm tsx scripts/simulate-replies.ts N1 A3 // 이들만 특정하여 실행
pnpm tsx scripts/simulate-replies.ts N4 --times 3 // 각각 세 번씩, 어느 정도 안정적인 결과를 보는지 확인
--times가 있는 이유는 모델의 단일 실행이 증거가 아니기 때문입니다. counter_offer가 0.95로 두 번 나오고 needs_brand가 0.6으로 한 번 나오는 분류 결과는, 같은 방식으로 세 번 나오는 결과와는 다른 제품이며, 후자가 돈을 쓰는 브랜치(branch)에서 원하는 것입니다. 동일한 시나리오를 세 번 실행하는 것은 어떤 결과를 얻었는지 확인하는 가장 저렴한 방법입니다.
모든 과정은 6개의 슬롯 워커 풀(worker pool)을 통해 실행되며, 결과는 인덱스별로 다시 기록됩니다:
const results: string[] = new Array(chosen.length);
let next = 0;
await Promise.all(
...
각 워커는 전체 시나리오 출력을 로컬 문자열 배열에 구축한 후 한 번에 반환하므로, 6개의 동시 대화가 라인을 서로 침범(interleave)하지 않습니다. 결과 배열은 인덱싱되어 있어 실행 순서와 관계없이 출력물이 시나리오 순서대로 나오며, 이는 두 번의 실행을 비교(diff)할 수 있게 해줍니다. 각 시나리오별로 catch를 사용하면 발생한 오류가 다른 43개의 시나리오를 중단시키는 대신 하나의 실패 블록으로 처리됩니다.
코드와 관련 없는 시나리오
목록의 절반 정도는 브랜치(branch)를 테스트하기보다는 제품 질문에 답하는 데 사용됩니다:
N8은 "이 캠페인에 지불할 수 있는 최대 예산이 얼마인지 알려주세요"로 시작한 다음 요율을 언급합니다. 올바른 행동은 요율을 협상하고 첫 문장을 절대 인정하지 않는 것입니다. 한도액은 요청 자체에 전혀 없으므로 유출될 것이 없습니다. 하지만 답변은 여전히 방금 꾸중 들은 기계처럼 읽혀서는 안 됩니다.N12는 제안받은 금액보다 적은 금액을 요구합니다. 올바른 답변은 제시된 제안을 수락하는 것입니다.N13은brandWrote: true를 가지고 있으므로 브랜드가 이미 스레드에 글을 작성했습니다. 올바른 답변은 모든 단계에서 답변을 중단하는 것입니다.N6은 10% 대신 수수료와 20% 커미션을 그리고 모든 것을 선불로 요구합니다. 수수료 자체는 코드로 합의되며, 수수료 외 다른 조건들은 브랜드에 속합니다.N16은 숫자를 언급하지 않으면서 두 번 더 많은 금액을 요청합니다. 반박할 금액이 없으므로 Nakodo는 요율을 한 번 물어본 후 묻는 것을 중단합니다.N7은 스페인어로 되어 있으며 파운드화 제안에 대해 유로를 인용합니다.A14는 독일어입니다. 답변은 그들의 언어로 돌아와야 합니다.A15와A17은 "이것이 실제 브랜드인지", 그리고 "제 이메일 주소를 어디서 얻었는지"를 묻는데, 하나는 알려진 출처가 있고 다른 하나는 출처가 없습니다. 정직한 답변은 각 경우마다 다르며,foundAt: null인 경우는 지어내서는 안 됩니다.
이들 중 어느 것도 assert를 가지고 있지 않습니다. 스크립트는 출력하고, 사람이 읽습니다. 단언문(assertions)은 제자리에, 즉 순수 함수에 존재합니다. 사다리 구조(ladder), 반올림(rounding), 한도 산술(limit arithmetic), 그리고 단계 해상도(level resolution)는 모두 정확한 예상 값을 가진 일반적인 node:test 파일을 가지고 있습니다. 단언될 수 없는 것은 70단어짜리 이메일이 사람처럼 들리는지 여부이며, 이를 문자열 매칭으로 가정하는 것은 이유 없이 모든 프롬프트 변경에서 실패할 테스트가 될 뿐입니다.
형제 스크립트
대화(Conversations)는 모델 호출의 두 가지 형태 중 하나입니다. 다른 하나는 일회성(one shot) 방식입니다. 웹사이트를 읽어 간략하게 요약하고, 제안서를 작성하거나, 첫 이메일을 쓰거나, 후속 조치를 하거나, 적합성을 판단하거나, 댓글에 라벨을 지정하는 등의 작업이 해당됩니다. 이러한 기능들은 scripts/try-prompts.ts에 있으며, 동일한 규칙과 가상의 브랜드를 사용하며, ID 접두사로 실행할 수 있습니다:
npx tsx scripts/try-prompts.ts 모든 경우(every case)
npx tsx scripts/try-prompts.ts email fit id가 이들로 시작하는 경우(cases whose id starts with these)
이 두 가지 방식 사이에서, 제품의 모든 프롬프트는 실제 받은 편지함에 도달하기 전에 약 1분 만에 고정된 입력값으로 실행될 수 있습니다. 이 코드베이스의 규칙은 프롬프트 변경 사항을 검토하려면 이 두 스크립트 중 하나의 출력이 필요하다는 것입니다. 왜냐하면 지침(instructions)의 diff를 읽는 것만으로는 무엇이 바뀌었는지 알 수 있을 뿐, 그것이 무엇을 하는지에 대해서는 아무것도 알 수 없기 때문입니다.
실제 내용 설명 부분
이 모든 것의 공개적인 측면은 문서화되어 있습니다: 작동 방식(how it works)에서는 어떤 답변이 생성되고 브랜드에 무엇이 전달되는지 설명하며, 개인정보 보호정책(privacy notice)은 크리에이터들이 참조하는 부분으로, 답변이 AI를 포함한 소프트웨어에 의해 읽히며, 어떤 내용으로 답변할 수 있는지, 그리고 주소는 절대 보여주거나 판매하지 않는다는 내용을 명확하게 설명하고 있습니다. 아웃리치 이메일 템플릿 가이드(Our outreach email templates guide)에는 사람이 직접 보내고 싶을 때를 위한 동일한 이메일의 수동 버전이 있습니다.
시나리오 목록(scenario list)은 이 기능이 생성한 가장 유용한 결과물입니다. 마치 상대방 측 사람들이 작성한 사양서처럼 읽히며, 충분히 많은 시나리오들을 거치면 대략적으로 그것이 어떤 것인지 파악할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기