AI 에이전트 기술: Worldcup RAG 앱 마이그레이션 (Gemini 및 GCP에서 Qwen 및 Alibaba로)
요약
Gemini와 Google Cloud 기반의 RAG 애플리케이션을 Qwen 및 Alibaba Cloud 스택으로 마이그레이션한 기술 사례를 소개합니다. 모델 교체 과정에서 발생하는 실질적인 기술적 함정과 해결 방법을 CloudPort Agent라는 에이전트 워크플로우를 통해 공유합니다.
핵심 포인트
- 단일 클라우드/모델 의존성을 탈피하기 위한 멀티 클라우드 전략의 중요성
- Gemini에서 Qwen으로의 모델 및 임베딩 스택 전환 과정 기술
- 마이그레이션 경험을 재사용 가능한 에이전트 스킬로 자산화
- RAG 기반 다국어 영상 검색 앱 SoccerScope를 통한 실증
공개 (Disclosure)
Qwen Cloud와 함께하는 Global AI Hackathon Series 블로그 포스트 상을 위한 제출물입니다.
프로젝트: CloudPort Agent
우리가 이 작업을 수행한 이유
오늘날 대부분의 AI 애플리케이션은 하나의 클라우드, 하나의 모델 제공업체, 그리고 하나의 SDK 가정에 의존합니다. 이는 편리하지만, 그렇지 않은 상황이 닥치면 문제가 됩니다.
만약 미국의 모델 API가 갑자기 사용 불가능해지거나, 속도 제한(rate-limited)이 걸리거나, 너무 비싸지거나, 특정 지역에서 사용하기 어려워진다면, 작동 가능한 백업이 중요합니다. 단순히 이론적인 백업이 아니라, 실제로 실행될 수 있는 두 번째 배포(deployment)가 필요합니다.
그것이 이 프로젝트의 동기였습니다. Qwen Cloud 해커톤을 위해, 우리는 기존의 Gemini + Google Cloud 애플리케이션을 Qwen + Alibaba Cloud로 마이그레이션했습니다. 그런 다음 마이그레이션 지식을 재사용 가능한 Qwen 코드 기술(skills)로 전환했습니다.
그 결과물은 CloudPort Agent입니다. 이는 우리가 겪은 실질적인 마이그레이션 함정(traps)을 포착하는 작은 에이전트 워크플로우(workflow)로, 다음 마이그레이션이 빈 페이지에서 시작하지 않도록 돕습니다. 마이그레이션된 앱은 SoccerScope로, 여러 국가의 인기 있는 월드컵 YouTube 영상을 찾는 다국어 RAG(Retrieval-Augmented Generation) 앱입니다. 사용자는 여러 언어에 걸쳐 바이럴 축구 영상을 검색할 수 있으며, 에이전트에게 검색 결과를 보고서, 소셜 포스트 또는 웹사이트 스타일의 요약으로 변환하도록 요청할 수 있습니다.
Qwen 버전은 마이그레이션 데모이며, 프로덕션 규모의 배포는 아닙니다. 이는 애플리케이션이 Qwen + Alibaba Cloud 스택에서 실행될 수 있음을 증명하고, 그 과정에서 발견한 함정들을 기록하기 위한 것입니다.
우리가 마이그레이션한 앱
기존 SoccerScope 스택은 다음과 같았습니다:
- 에이전트 프레임워크 (Agent framework): Google ADK
- LLM: Gemini
- RAG: MongoDB Atlas Vector Search
- 임베딩 (Embeddings):
gemini-embedding-001 - 도구 (Tools): 공식 MongoDB MCP 서버
- 런타임 (Runtime): Google Cloud Run 기반 FastAPI
- 배포 (Deployment): Docker 컨테이너
대상 스택은 다음과 같았습니다:
- LLM: Alibaba Cloud Model Studio / DashScope를 통한 Qwen
- 임베딩 (Embeddings):
text-embedding-v4 - 런타임 (Runtime): Alibaba Cloud Function Compute 3.0
- 배포 (Deployment): Function Compute 커스텀 런타임
- 에이전트 어시스턴트 (Agent assistant): 커스텀 스킬을 갖춘 Qwen Code
언뜻 보기에는 비교적 쉬운 모델 교체처럼 보였습니다. 앱이 Google ADK를 사용했기 때문에, 모델 교체는 대부분 설정 및 API 매핑 작업일 것이라고 예상했습니다. 실제로 마이그레이션은 앱 전체를 다시 작성하는 것보다 쉬웠지만, 여전히 몇 가지 명확하지 않은 함정들이 있었습니다. 그중 중요한 6가지를 소개합니다.
함정 1: 계정 유형
대회가 Qwen Cloud에서 개최되었기 때문에, 처음에는 그곳에 계정을 생성했습니다. Qwen Cloud는 이메일 기반 인증을 사용하는 반면, Alibaba Cloud는 비밀번호 기반 인증에 의존하므로 Alibaba Cloud에서 계정을 다시 설정했습니다. 기업용 계정을 생성하라는 권장 사항을 따랐지만, 기업 계정과 개인 계정 간에 인증 절차 및 무료 크레딧 허용량이 다를 수 있다는 점을 유의하시기 바랍니다.
기존 애플리케이션이 Cloud Run (Docker)용으로 설계되었기 때문에 App Engine Service가 적절한 선택으로 보였습니다. 하지만 우리는 검증된 실적이 있는 Function Compute를 선택했습니다. Alibaba Cloud Function Compute가 컨테이너 실행을 지원하기는 하지만, 문제는 우리의 특정 계정 유형이 체험 기간을 허용하지 않아 사실상 프로덕션 환경에 적합한 설정을 강제한다는 점이었습니다. 결과적으로 우리는 컨테이너 (Docker) 대신 Function Compute 3.0 커스텀 런타임을 활용하는 방식으로 전환했습니다.
즉, 다음과 같은 의미입니다:
- Python 앱을 zip 파일로 패키징 (Package)
- 코드 패키지 업로드
python3 main.py와 같은 시작 명령 (startup command) 구성- 예상되는 포트 (port) 노출
- 타임아웃 (timeout) 조정
- 런타임 의존성 (runtime dependencies)을 직접 관리
이것이 첫 번째 중요한 교훈이었습니다: 클라우드 마이그레이션 에이전트(cloud migration agent)는 "서버리스 앱 (serverless app)"이 곧 "컨테이너 앱 (container app)"을 의미한다고 맹목적으로 가정해서는 안 됩니다. 올바른 배포 경로는 계정, 리전 (region), 과금 설정, 그리고 체험판 제약 조건에 따라 달라집니다. 이제 CloudPort Agent는 Docker를 기본값으로 하드코딩하는 대신, 배포 전략을 하나의 결정 지점 (decision point)으로 취급합니다.
함정 2: 리전 선택이 커스텀 도메인에 영향을 미침
우리는 처음에 중국 본토 리전 (mainland China region)을 시도했습니다. 그러자 즉시 커스텀 도메인 (custom-domain) 문제가 발생했습니다. 중국 본토 리전에서는 커스텀 도메인을 바인딩할 때 ICP 비안 (ICP filing)이 필요할 수 있습니다. 많은 해외 개발자들에게 이는 현실적인 빠른 시작 (quick-start) 경로가 아닙니다. 또한 임시 Function Compute URL은 브라우저 기반 웹 앱을 위한 좋은 공개 데모 URL이 아니었습니다. 우리의 경우, 기본 URL 동작은 일반적인 웹사이트 데모를 위해 우리가 원했던 방식이 아니었습니다.
해결책은 도쿄 리전 (Tokyo region)에 배포를 다시 생성하고 일반적인 커스텀 도메인 설정을 사용하는 것이었습니다:
- Cloudflare에서 DNS 호스팅
- 서브도메인 (subdomain)을 Function Compute 엔드포인트 (endpoint)로 지정
- Function Compute 측에서 커스텀 도메인 및 HTTPS 인증서 (certificate) 구성
마이그레이션 교훈은 간단합니다: 다른 모든 것을 구축하기 전에 리전을 먼저 선택하십시오. 에이전트의 경우, 이를 사전 점검 질문 (preflight question)으로 인코딩해야 합니다:
이 배포가 커스텀 도메인을 가진 공개 웹 앱을 위한 것입니까?
만약 그렇다면, 배포 파일을 작성하기 전에 리전 및 도메인 제약 조건을 확인해야 합니다.
함정 3: MCP는 Node에 의존하며, 커스텀 런타임에는 Node가 없었음
SoccerScope는 공식 MongoDB MCP 서버를 사용합니다. 기존 Docker 환경에서는 컨테이너 내에 Node.js가 이미 설치되어 있었기 때문에 이것이 쉬웠습니다. 앱은 다음과 같이 호출할 수 있었습니다:
npx mongodb-mcp-server
Function Compute Custom Runtime에서는 그 가정이 실패했습니다.
예상된 PATH에 Node.js 바이너리가 없었습니다. 설상가상으로, 콜드 스타트 (Cold start) 시에 npx를 사용하는 것은 런타임이 시작 중에 네트워크를 통해 패키지를 해결(resolve)하려고 시도할 수 있음을 의미했습니다. 이는 서버리스 (Serverless) 환경에서 매우 취약한 방식입니다.
한동안 우리는 MCP를 완전히 제거하고 직접적인 pymongo 호출로 되돌아갔습니다. 그것은 작동했지만, 원래 아키텍처의 진정한 마이그레이션은 아니었습니다.
최종 해결책은 다음과 같았습니다:
- Function Compute Layers를 통해 Node.js를 추가합니다.
- 빌드 타임 (Build time) 중에
node_modules를 번들링 (Bundle) 합니다. npx mongodb-mcp-server를 직접적인node호출로 교체합니다.- 콜드 스타트 (Cold start) 중에 네트워크 패키지 해결을 피합니다.
이것은 문서에서 한곳에 모아 가르쳐주는 경우가 드문 종류의 함정입니다. 각 구성 요소는 어딘가에 문서화되어 있습니다:
- Function Compute Custom Runtime
- Function Compute Layers
- Node.js
- MCP
- MongoDB MCP server
- Serverless cold starts
하지만 실패는 이 모든 것을 결합했을 때에만 나타납니다.
그 결합이야말로 에이전트 기술 (Agent skill)이 반드시 기억해야 할 내용입니다.
함정 4: Gemini 구조화된 출력 (Structured output)과 Qwen 구조화된 출력은 동일하지 않음
Gemini는 구조화된 출력 (Structured output)을 매우 단순하게 느껴지게 만들었습니다. 원래 앱에서는 Pydantic 스타일의 응답 스키마 (Response schema)를 전달하면 API가 이를 밀접하게 따를 것이라고 기대할 수 있었습니다.
OpenAI 호환 API를 통해 Qwen으로 이동했을 때, 동작은 달랐습니다. 가장 중요한 교훈은 "Qwen은 구조화된 출력을 할 수 없다"가 아니었습니다. 할 수 있습니다. 교훈은 다음과 같습니다:
Gemini 스타일의 스키마 동작이 Qwen과 일대일로 매핑될 것이라고 가정하지 마십시오. 우리의 마이그레이션 경로에서는 JSON 출력 모드를 사용한 다음 Pydantic으로 로컬에서 검증했습니다.
이 과정에서 몇 가지 실질적인 문제들이 드러났습니다:
- 프롬프트에 모델이 JSON을 출력해야 한다고 명시적으로 명시해야 합니다.
- 사고 모드 (Thinking mode)와 JSON 출력 설정은 주의 깊게 처리해야 합니다.
- 출력이 잘리면 (Truncated output) JSON 형식이 깨질 수 있으므로, 토큰 제한 (Token limits)에 각별히 주의해야 합니다.
- 기대하는 스키마 (Schema)를 프롬프트에 직접 작성해야 합니다.
- 애플리케이션은 결과를 로컬에서 검증하고, 불일치할 경우 재시도 (Retry)해야 합니다.
따라서 다음과 같은 교체 패턴이 만들어졌습니다:
- 프롬프트에 JSON 스키마 요구 사항을 넣습니다.
- JSON 출력을 명시적으로 요청합니다.
- 구조화된 출력 (Structured output)과 충돌하는 경우 사고 모드 (Thinking mode)를 비활성화합니다.
- 응답을 파싱 (Parse)합니다.
- Pydantic으로 검증합니다.
- 검증에 실패하면 재시도합니다.
지루한 과정이지만, 이는 견고 (Robust)합니다. CloudPort Agent는 이를 다음 개발자가 흩어진 예시들을 읽으며 새로 발견하게 두는 대신, API 매핑 규칙으로 기록합니다.
함정 5: 임베딩 (Embedding) 마이그레이션은 단순히 모델 이름만 바꾸는 것이 아닙니다
RAG를 위해 LLM을 변경하는 것은 마이그레이션의 절반에 불과합니다. 임베딩 모델 (Embedding model) 또한 그만큼 중요합니다. 기존 앱은 gemini-embedding-001을 사용했습니다. Qwen 버전은 text-embedding-v4를 사용합니다. 이 벡터 (Vectors)들은 서로 호환되지 않습니다.
즉, 마이그레이션에는 다음 사항들이 포함되어야 합니다:
- 코퍼스 (Corpus) 재임베딩
- 벡터 차원 (Vector dimensions) 확인
- MongoDB Atlas Vector Search 인덱스 설정 확인
- 입력 순서 유지
- 정규화 (Normalization) 동작 유지
- 변경 후 다국어 검색 (Multilingual retrieval) 품질 검증
우리의 경우, 768차원 임베딩을 사용했습니다. 하지만 중요한 규칙은 다음과 같습니다: 차원을 가정하지 말고 반드시 확인하십시오.
또한 작지만 매우 실질적인 API 차이가 있었습니다.
기존 Gemini 임베딩 흐름은 더 큰 청크 크기 (Chunk size)를 사용했습니다. Qwen text-embedding-v4의 경우, 공식적인 배치 크기 (Batch size) 제한은 요청당 텍스트 10개입니다. 따라서 마이그레이션 과정에서 임베딩 청크 크기를 10으로 변경해야 했습니다.
그것은 어려운 변경 사항이 아닙니다. 문제는 누군가가 그것을 알아차려야 한다는 점입니다. 이것이 바로 CloudPort Agent가 skills/gemini-to-qwen-api-mapping/examples.md와 같은 예시를 포함하는 이유입니다.
그 목표는 인간이 모든 API 차이점을 암기하게 만드는 것이 아닙니다. 목표는 그러한 작은 차이점들을 스킬 (skill) 파일에 넣어 에이전트 (agent)가 이를 일관되게 적용할 수 있도록 하는 것입니다.
함정 6: 마이그레이션한 앱이 여전히 Gemini를 호출하고 있을 수 있음
이것은 가장 웃기면서도 가장 당혹스러운 버그였습니다.
Alibaba Cloud 배포는 성공했습니다. 웹 앱이 로드되었습니다. 백엔드 (backend)도 실행 중이었습니다. 그런데 검색을 실행하자 다음과 같은 오류가 나타났습니다:
Generation failed: agent error: No API key was provided.
Please pass a valid API key.
Learn how to create an API key at https://ai.google.dev/...
앱은 Alibaba Cloud에 배포되었지만, 남아 있는 하나의 코드 경로 (code path)가 여전히 Gemini를 호출하려고 시도하고 있었습니다. 이것이 바로 "마이그레이션 (migration)"이 위험한 단어가 되는 지점입니다.
마이그레이션은 단순히 다음과 같은 상황이라고 해서 완료된 것이 아닙니다:
- 앱이 시작됨
- UI가 로드됨
- 새로운 API 키가 설정됨
- 대부분의 파일이 수정됨
실제 런타임 경로 (runtime path)를 확인하는 엔드 투 엔드 스모크 테스트 (end-to-end smoke test)가 필요합니다. 여기서 한 가지 중요한 뉘앙스가 있습니다.
앱이 Google ADK를 사용하기 때문에, Google과 관련된 모든 의존성 (dependency)이 사라졌다고 말할 수는 없습니다.
Google ADK 자체는 내부적으로 Google의 최신 google-genai SDK를 사용합니다. 이번 마이그레이션의 목표는 이름에 "google"이 포함된 모든 패키지를 삭제하는 것이 아니었습니다. 목표는 런타임 LLM 호출과 임베딩 (embedding) 호출이 의도한 대로 Qwen / DashScope로 라우팅 (routing)되도록 보장하는 것이었습니다.
CloudPort Agent는 이제 이러한 종류의 숨겨진 결합 (coupling)에 대한 검증 체크를 포함합니다. 마이그레이션된 앱이 실제 엔드 투 엔드 요청을 실행할 수 있을 때까지 성공을 선언하지 않습니다.
3일에서 5분으로
첫 번째 수동 마이그레이션은 약 3일이 걸렸습니다. 교훈들을 Qwen Code 스킬 (skills)로 인코딩한 후, 데모 마이그레이션 흐름은 훨씬 짧아졌습니다.
이 프로젝트를 기준으로 측정하면 다음과 같습니다:
| 항목 | 수동 마이그레이션 (Manual migration) | CloudPort Agent 데모 |
|---|---|---|
| 시간 | 약 3일 | 약 5분 |
| ... |
핵심은 세상의 모든 클라우드 마이그레이션이 이제 5분 만에 끝난다는 것이 아닙니다. 핵심은 반복되는 마이그레이션 지식을 캡처(capture)할 수 있다는 점입니다.
여전히 사람은 중요한 변경 사항을 승인합니다. 에이전트(Agent)는 기계적인 작업을 수행하고, 알려진 API 매핑을 적용하며, 일반적인 실패 패턴을 확인합니다. 바로 이 부분이 제가 흥미롭다고 느끼는 지점입니다.
Qwen Code 스킬이 적합했던 이유
CloudPort Agent는 Qwen Code 커스텀 스킬 (custom skills)로 구현되었습니다.
스킬은 마법이 아닙니다. 실제로 스킬은 대부분 구조화된 텍스트, 즉 코딩 에이전트 (coding agent)가 사용할 수 있는 방식으로 작성된 지침 (instructions), 규칙 (rules), 예시 (examples), 그리고 알려진 함정 (known traps)들입니다. 이러한 단순함이 유용합니다.
마이그레이션 지식은 비공개 서비스나 복잡한 프레임워크 안에 갇혀 있지 않습니다. 이는 Markdown 파일 안에 존재합니다. 이론적으로는 동일한 지식을 Claude Code나 다른 코딩 에이전트 (coding agents)에도 적용할 수 있겠지만, 아직 테스트해보지는 않았습니다.
이 프로젝트에서 유용했던 부분은 단순히 "에이전트를 사용한다"는 것만이 아니었습니다. 유용했던 핵심은 다음과 같습니다:
마이그레이션 과정에서 얻은 경험적 지식(scar tissue)을 에이전트가 읽을 수 있는 파일로 만드는 것입니다.
시도해보기
CloudPort Agent는 여기서 확인할 수 있습니다:
https://github.com/webbigdata-jp/cloudport-agent
데모 프롬프트 (demo prompt)는 여기 있습니다:
https://github.com/webbigdata-jp/cloudport-agent/blob/main/docs/demo-prompt.md
YouTube 데모는 여기 있습니다:
macOS에서 Qwen Code로 시도해보고 싶다면, 기본적인 설정 방법은 다음과 같습니다.
mkdir qwen-dir
cd qwen-dir
node -v
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기