Docker 클라우드 샌드박스에서 자율 A/B 테스트 에이전트 구축하기
요약
본 포스트는 Docker Cloud Sandboxes를 활용하여 AI 에이전트가 자율적으로 A/B 테스트를 수행하는 방법을 안내합니다. 가상의 온라인 상점을 예시로, 여러 개선 버전을 안전한 샌드박스에 배포하고, 다른 에이전트들이 이를 평가하며 최적의 버전을 찾아내는 과정을 구현합니다.
핵심 포인트
- Docker Cloud Sandbox는 AI 에이전트가 코드를 실행할 때 필요한 안전성과 분리성을 제공합니다.
- A/B 테스트를 위해 여러 버전의 웹사이트를 독립적인 샌드박스에 배포하고 비교하는 방법을 보여줍니다.
- OpenAI API 키 등을 안전하게 관리하며 외부 호출을 수행할 수 있습니다.
- TypeScript와 `@docker/sandboxes` SDK를 사용하여 전체 과정을 제어합니다.
AI 에이전트가 스스로 A/B 테스트를 실행할 수 있을지 궁금해한 적이 있나요?
이 포스트에서는 함께 하나를 만들어 볼 것입니다. Docker Cloud Sandboxes 사용 경험이 전혀 없는 분들도 따라 할 수 있도록, 한 파일씩 천천히 진행하겠습니다.
전체 코드: github.com/ajeetraina/sbx-ab-agent
무엇을 만들고 있나요?
작은 온라인 상점을 생각해 보세요. 어떤 사람들은 이 상점에서 물건을 찾기 어렵다고 말합니다. 우리는 이를 개선하고 싶지만, 어떤 수정이 가장 좋을지 확신하지 못합니다.
그래서 다음과 같이 합니다:
- 상점을 안전한 클라우드 박스(샌드박스)에 온라인으로 올립니다.
- 가짜 쇼핑객들(AI 에이전트)을 보내 상점 사용이 얼마나 쉬웠는지 평가해 달라고 요청합니다.
- 분석가(다른 AI 에이전트)에게 몇 가지 개선 사항을 제안하도록 요청합니다.
- 각 개선 사항을 코드를 작성하는 빌더(또 다른 AI 에이전트)에게 전달하고, 각각의 샌드박스에 넣습니다.
- 동일한 가짜 쇼핑객들을 보내 각 새로운 버전을 테스트하게 합니다.
- 가장 높은 점수를 받은 버전을 선택하여 그 코드를 노트북으로 복사합니다.
이 아이디어는 그림과 같습니다:
┌──────────── Docker Cloud Sandboxes ────────────┐
npm start ──► │ A 원본 상점 → 쇼핑객 테스트 → 분석가 │
│ B 새 샌드박스 → 빌더가 수정 B 추가 → 재테스트 │
...
"A"는 원본 상점입니다. "B"와 "C"는 새로운 버전입니다. A/B 테스트가 실제로 의미하는 것은 이것입니다: 몇 가지 버전을 시도해 보고 어떤 것이 더 잘 작동하는지 확인하는 것입니다.
아예 샌드박스를 사용해야 하는 이유?
우리의 빌더 에이전트는 아무도 검사하지 않은 코드를 작성하고 실행할 것입니다. 그런 것을 여러분의 노트북에서 실행하고 싶지는 않을 겁니다.
Docker Cloud Sandbox는 클라우드의 작고 일회용 컴퓨터(마이크로VM)입니다. 이것은 우리에게 필요한 네 가지를 제공합니다:
- 안전성 (Safety). 에이전트가 엉뚱한 행동을 하더라도, 영향을 받는 것은 오직 샌드박스뿐입니다.
- 분리성 (Separation). Fix B와 Fix C는 각각 자신만의 샌드박스를 가지므로 절대 서로 섞이지 않습니다.
- 실제 연결 (A real link). 각 샌드박스는 공개 HTTPS URL을 공유할 수 있어, 모든 버전을 브라우저에서 열어볼 수 있습니다.
- 안전한 키 관리 (Safe keys). OpenAI API 키는 비밀(secret)로 저장됩니다. 샌드박스가 외부 호출에 이 키를 사용하지만, 키 자체가 샌드박스 내부에 노출되지는 않습니다.
이 모든 것을 @docker/sandboxes SDK를 사용하는 작은 TypeScript 프로그램으로 제어합니다.
필요한 것 (What you need)
- Node.js 20 이상
- Docker Cloud Sandboxes에 접근할 수 있는 Docker 계정
- OpenAI API 키 (에이전트는 OpenAI의 Codex를 사용합니다)
단계 1: 코드 가져오기 및 둘러보기 (Get the code and look around)
git clone https://github.com/ajeetraina/sbx-ab-agent
cd sbx-ab-agent
npm install
프로젝트가 작습니다. 지도를 참고하세요:
app/ 데모 상점 (the demo shop)
server.mjs 작은 웹 서버
index.html 페이지
...
좋은 읽기 순서는 app/ 먼저, 그다음 prompts.ts, 그 다음 sbx.ts, 마지막으로 ab.ts입니다. 이 순서를 따를 것입니다.
단계 2: 상점 살펴보기 (app/) (Meet the shop (app/))
상점은 긴 제품 목록을 보여줍니다. 그것이 전부입니다. 검색, 필터링, 정렬 기능은 없습니다. 이것은 의도된 것입니다. 이것이 바로 우리가 에이전트가 찾아내고 고치기를 원하는 문제입니다.
제품을 그리는 코드는 app/app.js에서 가져왔습니다:
async function main() {
const products = await (await fetch('/products.json')).json();
render(products);
...
모든 제품을 로드하여 페이지에 배치합니다. 그 이상은 없습니다.
이제 app/server.mjs의 서버를 보세요. 핵심 코드는 이 부분입니다:
const body = await readFile(file);
서버는 누군가 요청할 때마다 디스크에서 파일을 읽습니다. 오래된 사본을 절대 유지하지 않습니다. 이것은 나중에 중요합니다: 빌더 에이전트가 app.js를 변경하면, 라이브 사이트는 즉시 바뀝니다. 재시작이 필요 없습니다.
자신의 기기에서 상점을 테스트해 보세요:
npm run shop
# http://localhost:3000 에서 열기
Step 3: 세 가지 AI 도우미를 만나다 (src/prompts.ts)
저희는 세 가지 역할을 가지고 있습니다. 이들은 모두 동일한 AI(Codex)를 사용합니다. 유일한 차이점은 각 도우미에게 제공하는 지침(프롬프트)입니다.
도우미 1: 가상의 쇼핑객 (the pretend shopper)
저희는 각각 간단한 목표를 가진 세 명의 쇼핑객을 가지고 있습니다.
| 쇼핑객 | 역할 | 원하는 것 |
|---|---|---|
budget-student | 약 $20를 가진 학생 | 개발자 친구를 위한 20달러 미만의 선물 |
| ... |
코드 상에서, 쇼핑객은 단지 작은 객체일 뿐입니다:
{
id: 'hoodie-hunter',
who: '이미 L 사이즈의 후디를 갖고 싶다는 것을 아는 바쁜 엔지니어',
...
쇼핑객에게는 "당신은 개발자가 아닌 실제 사람이다. 어떤 파일도 수정하지 마라."라는 지침이 주어집니다. 쇼핑객은 curl을 사용하여 상점을 살펴보고, 실제 사람이 필요로 할 클릭과 스크롤을 상상합니다.
가장 중요한 점: 쇼핑객은 단락이 아닌 JSON 형식으로 답변해야 합니다. 그래야 저희 프로그램이 답변을 쉽게 읽을 수 있습니다.
{
"persona": "hoodie-hunter",
"completed": true,
...
score는 1부터 10까지입니다. 10점은 작업이 매우 쉬웠다는 것을 의미합니다.
도우미 2: 분석가 (the analyst)
분석가는 모든 쇼핑객 보고서를 읽고 몇 가지 수정 사항을 제안합니다. 저희가 부여하는 규칙은 다음과 같습니다:
- 각 수정 사항은 작아야 합니다.
index.html,app.js,styles.css만 변경해야 합니다.- 새 라이브러리 추가 금지, 백엔드 변경 금지.
- 각 수정 사항은 서로 다른 아이디어여야 합니다.
이 분석가 역시 JSON 형식으로 답변합니다:
{
"summary": "쇼핑객들이 전체 목록을 스크롤했습니다. 범위를 좁힐 방법이 없습니다.",
"variants": [
...
도우미 3: 빌더 (the builder)
빌더는 하나의 수정 사항(예: "가격순 정렬")을 받아 코드를 작성합니다. 이 역시 엄격한 규칙을 가지고 있습니다:
index.html,app.js,styles.css만 수정해야 합니다.- 순수 HTML, CSS 및 JavaScript를 사용합니다. 프레임워크는 사용하지 않습니다.
- 서버를 재시작하지 마십시오.
- 완료되면, 페이지가 여전히
curl로 로드되는지 확인하십시오.
이러한 규칙들은 모든 버전을 공정하게 유지합니다. 각 버전은 동일한 상점에 작은 변화 하나만 더한 것입니다.
Step 4: Docker에 연결하고 키 저장하기 (src/sbx.ts)
이제 Docker Cloud Sandboxes에 대해 이야기해 보겠습니다. 이 파일의 모든 내용은 나중에 호출할 작은 헬퍼입니다.
로그인 (Signing in)
const auth = oauth({
store: fileOAuthCredentialStore({ path: '.docker-sandboxes/oauth.json' }),
onVerification: ({ verificationUriComplete, verificationUri, userCode }) => {
...
쉽게 말해 설명하면:
- 처음에는 링크와 코드를 출력합니다. 이 링크를 열고 로그인합니다.
- 로그인 정보를 로컬 파일에 저장하므로 다음번에는 다시 요청하지 않습니다.
new Sandboxes({ auth })는 클라이언트를 제공합니다. 우리는 이 클라이언트를 다른 모든 작업에 사용합니다.
브라우저 단계를 원치 않는다면, DOCKER_USERNAME과 DOCKER_PAT(개인 액세스 토큰)을 설정하세요. 그러면 코드가 대신 pat()를 사용합니다.
OpenAI 키를 비밀(Secret)로 저장하기
const secret = await client.secrets.create({
displayName: 'sbx-ab-agent-openai',
serviceType: 'openai',
...
이것은 사용자의 OpenAI 키를 Docker 내에, 어떤 샌드박스 내부가 아닌 곳에 저장합니다. 나중에 우리는 이 비밀을 각 샌드박스에 연결(attach)합니다. Codex가 OpenAI를 호출할 때, 샌드박스는 외부로 나가는 요청에 키를 추가해 줍니다. Codex는 절대 키 자체를 볼 수 없습니다.
기억해야 할 작은 점이 있습니다: SDK가 사용자의 OpenAI 비밀을 자동으로 연결해주지 않습니다. 샌드박스를 생성할 때 직접 전달해야 합니다. 다음 단계에서 그렇게 하겠습니다.
5단계: 샌드박스를 시작하고 상점(shop)을 안에 넣기 (launchShop)
이 함수는 다섯 가지 작은 작업을 수행합니다. 하나씩 살펴보겠습니다.
1. 샌드박스 생성 (Create the sandbox)
const sandbox = await client.kits.launchAndWait('codex', {
displayName,
resources: 'small',
...
'codex'는 키트(kit)입니다. 즉, Codex가 이미 설치된 준비된 샌드박스입니다. launchAndWait은 이를 생성하고 준비될 때까지 기다립니다. 여기서 비밀을 전달하는 것을 주목하세요.
2. 상점 파일 복사하기 (Copy the shop files in)
await sandbox.files.mkdir(APP_DIR, { mode: 0o755, parents: true });
for (const f of await readdir(localAppDir)) {
await sandbox.files.write(`${APP_DIR}/${f}`, await readFile(join(localAppDir, f), 'utf8'));
...
샌드박스에 폴더를 만들고, app/ 폴더의 모든 파일을 그 안에 복사합니다.
3. 웹 서버 시작하기
await sandbox.processes.start({
args: ['node', 'server.mjs'],
workingDir: APP_DIR,
...
processes.start는 백그라운드에서 명령을 실행합니다. 서버가 계속 실행되는 동안 우리는 다른 작업을 할 수 있습니다.
4. 서버 응답 대기하기
waitForHttp 헬퍼 함수는 sandbox.run(...)을 사용하여 최대 30번, 초당 한 번씩 서버를 시도합니다. 좋은 응답을 받으면 다음 단계로 넘어갑니다.
5. 공개 링크 얻기
const port = await sandbox.ports.create({ number: 3000, protocol: 'tcp' });
return { sandbox, url: port.url };
이 코드는 포트 3000을 인터넷에 열고 실제 HTTPS URL을 반환합니다. 브라우저에서 열어볼 수 있습니다.
이것으로 끝입니다. 하나의 함수만으로 클라우드에서 작동하는 라이브 상점을 갖게 됩니다.
단계 6: Codex에게 작업 요청하기 (runAgent)
모든 AI 도우미(쇼퍼, 분석가, 빌더)는 이 하나의 함수를 거칩니다. 채팅 창 없이 샌드박스 내부에서 Codex를 실행하며, 프롬프트만 입력하고 답변을 받습니다.
const script =
`printf '%s' "$1" | codex exec -C ${APP_DIR} --skip-git-repo-check --ephemeral ` +
`--dangerously-bypass-approvals-and-sandbox -o ${outFile} -`;
...
보기에는 무섭게 생겼으니, 하나씩 분석해 보겠습니다:
codex exec는 질문을 하지 않고 Codex를 한 번 실행합니다.printf ... | ... -은 프롬프트를 Codex로 보냅니다. 끝의-는
AI가 생성한 답변은 항상 깔끔하지 않습니다. 때로는 JSON이 코드 블록 안에 갇혀 있거나, 앞에 문장이 붙어 나오는 경우도 있습니다. extractJson 헬퍼는 첫 번째 {와 마지막 }를 찾아 그 사이에 있는 내용을 읽습니다. 간단하며 잘 작동합니다.
Step 7: 모든 것을 통합하기 (src/ab.ts)
이제 메인 프로그램입니다. 위에서 정의한 헬퍼들을 순서대로 호출할 뿐입니다. main()을 위에서 아래로 읽어보면 다음과 같은 단계들을 볼 수 있습니다:
Step A: 원본 상점 시작하기
const base = await launchShop(client, 'ab-A-baseline', secret, 'app');
Step B: 쇼퍼들 투입하기
const baseSessions = await simulate(base.sandbox, 'A', PERSONAS);
simulate는 Promise.all을 사용하여 세 명의 쇼퍼를 동시에 실행합니다. 각각은 JSON 보고서를 반환합니다.
Step C: 분석가에게 수정 요청하기
const plan = extractJson(
await runAgent(base.sandbox, analystPrompt(GOAL, baseSessions, N_VARIANTS)),
);
Step D: 모든 수정을 한 번에 구축하고 테스트하기
const settled = await Promise.allSettled(
variants.map((v) => runVariant(client, secret, v)),
);
각 수정 사항에 대해 runVariant는 세 가지 작업을 수행합니다:
- 상점의 최신 사본으로 새로운 샌드박스를 시작합니다.
- 빌더에게 수정을 추가하도록 요청합니다.
- 동일한 세 명의 쇼퍼를 투입하여 테스트합니다.
우리는 Promise.all 대신 Promise.allSettled을 사용합니다. 차이점은 다음과 같습니다: 만약 한 수정 사항이 실패하더라도 다른 것들은 계속 완료된다는 것입니다. 하나의 나쁜 버전이 전체 실행을 망치지 않습니다.
Step E: 승자 선택하기
const ranked = [...arms].sort((a, b) =>
b.avgScore - a.avgScore || b.completion - a.completion || a.avgSteps - b.avgSteps);
쉽게 말해: 평균 점수가 가장 높은 것이 승자입니다. 만약 두 개가 동점이라면, 쇼퍼가 더 많이 완료된 쪽이 승자입니다. 여전히 동점이라면, 단계 수가 적은 쪽이 승자입니다.
Step F: 승리한 코드를 가져오기
const body = await winner.sandbox.files.read(`${APP_DIR}/${f}`, { encoding: 'utf8' });
await writeFile(`results/winner/${f}`, body);
승리한 파일들은 샌드박스에서 노트북의 results/winner/로 복사됩니다. 또한 프로그램은 모든 점수와 쇼퍼 인용문이 담긴 results/report.md 및 results/report.json을 작성합니다.
Step 8: 실행하기
키를 설정하세요:
cp .env.example .env
# open .env and add: OPENAI_API_KEY=sk-...
set -a; source .env; set +a
그런 다음 실행을 시작합니다:
npm start
처음 실행할 때는 Docker 로그인 링크가 표시됩니다. 이를 열고 로그인하면 실행이 계속됩니다.
일반적인 실행(수정 2회, 쇼퍼 3명)은 작은 샌드박스 3개와 약 12개의 Codex 호출을 사용합니다. 소요 시간은 약 5분에서 10분입니다.
유용한 옵션들:
npm start -- --variants 3 --personas 2
npm start -- --goal "사용자는 어떤 품목이 할인 중인지 알 수 없다"
npm start -- --cleanup # 끝날 때 샌드박스를 삭제합니다
...
Step 9: 실시간으로 확인하고 결과 읽기
실행되는 동안 http://localhost:4000을 여세요. 이것이 라이브 대시보드(src/dashboard.ts)입니다. 각 단계가 활성화되는 것을 볼 수 있으며, A, B, C를 보여주는 상자에 점수가 들어오는 대로 표시됩니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기