무색
기술블로그
에세이
연구
소개

무색

소프트웨어로 비즈니스의 가능성을 만듭니다. 웹·앱 개발, 음성 AI, 자동화 콘텐츠 제작까지 — 기술이 필요한 곳에 무색이 있습니다.

연락처

contact@museck.com

사업자 정보

상호: 무색

대표: 배성재

사업자등록번호: 577-58-00836

인천광역시 연수구 인천타워대로 323, 에이동 8층 801-802호 AB-132 (송도동, 송도 센트로드)

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관·연락처
INCHEON, KR
Mastra + vLLM 로컬 LLM Playground — bauhaus 키 비주얼
🤖 LLM 서버 운영기
2026. 2. 8.

Mastra + vLLM으로 로컬 LLM Playground 만들기

vLLM 서버를 띄우고 curl로 대화하는 건 되는데, 매번 터미널에서 JSON을 조립하는 건 너무 불편했다. 브라우저에서 ChatGPT처럼 쓸 수 있는 Playground를 만들기로 했다.

왜 Mastra인가

Mastra는 TypeScript 기반 AI 에이전트 프레임워크다. OpenAI-compatible API를 지원하는 모델이라면 뭐든 연결할 수 있고, 내장 Playground UI를 제공한다. vLLM이 OpenAI-compatible 엔드포인트를 지원하니까, 이 둘을 연결하면 별도 프론트엔드 없이 바로 브라우저에서 대화할 수 있다.

목표는 세 가지였다.

  • 브라우저에서 로컬 LLM과 멀티턴 대화
  • 대화 이력 저장 (세션 간 유지)
  • GPU 서버의 vLLM 자동 시작/관리

전체 구조

모델 연결: 공식 문서에 없는 방법

첫 번째 삽질은 모델 연결이었다. Mastra 문서에는 createOpenAI()로 커스텀 엔드포인트를 연결하라고 되어 있다. 하지만 이렇게 생성한 모델 인스턴스는 Mastra Agent의 model 필드와 호환되지 않았다.

정답은 model 필드에 { id, url } 객체를 직접 전달하는 것이었다.

import { Agent } from '@mastra/core/agent';

export const chatAgent = new Agent({
  name: 'Local LLM Chat',
  model: {
    id: 'openai/my_model',  // openai/ prefix 필수
    url: 'http://localhost:8000/v1',
  },
  memory,  // LibSQLStore 기반 멀티턴
});

openai/ prefix가 중요하다. 이걸 빼면 Mastra가 어떤 프로바이더를 쓸지 판단하지 못한다.

Memory 설정: 인스턴스 분리가 핵심

Mastra의 Memory는 LibSQLStore를 storage로 사용한다. 여기서 두 번째 삽질이 발생했다. Mastra 인스턴스 자체도 storage를 가지는데, Memory의 storage와 같은 인스턴스를 쓰면 테이블 충돌이 난다.

또 하나, DB 경로는 반드시 절대경로를 써야 한다. 상대경로는 실행 위치에 따라 다른 파일을 참조해서 대화 이력이 사라지는 것처럼 보인다.

import { Memory } from '@mastra/memory';
import { LibSQLStore } from '@mastra/libsql';

// Memory 전용 storage (별도 인스턴스)
const memoryStorage = new LibSQLStore({
  url: 'file:///absolute/path/to/memory.db',  // 절대경로 필수
});

export const memory = new Memory({
  storage: memoryStorage,
});

GPU 자동 관리: JupyterLab WebSocket

GPU 서버는 항상 켜져 있지 않다. Playground에서 대화를 시작하면 vLLM 서버가 자동으로 올라와야 한다. 이 부분의 플로우는 이렇다.

  • GET /v1/models로 vLLM 상태 확인. 이미 실행 중이면 바로 대화 시작.
  • GPU API의 /status 엔드포인트로 GPU가 idle 상태인지 확인.
  • JupyterLab의 WebSocket 터미널 API로 vllm serve 명령 원격 실행.
  • 5초 간격으로 /v1/models 폴링. 모델이 로드되면 대화 시작.

이 패턴은 나중에 다른 프로젝트(paper-translate)의 VLLMManager에서도 동일하게 재사용했다. GPU를 여러 프로젝트가 공유하는 환경에서는 이런 자동 관리가 필수다.

Thinking 태그 처리: 서버 측이 정답

Qwen3 같은 모델은 응답에 <think> 태그로 reasoning을 포함한다. Mastra에는 extractReasoningMiddleware라는 미들웨어가 있어서 이걸 파싱해준다. 처음에는 이걸 썼다.

문제는 Memory와 함께 사용할 때 터졌다. extractReasoningMiddleware가 분리한 reasoning이 대화 이력에 그대로 저장된다. 다음 턴에서 모델이 이전 reasoning까지 입력으로 받으면서 컨텍스트가 폭발하고, 결국 모델이 hang되었다.

해결책은 간단했다. 클라이언트 측 미들웨어를 버리고 vLLM 서버에서 직접 처리하면 된다.

# vLLM 서버 측에서 thinking 태그 처리
vllm serve "model-name" \
    --reasoning-parser qwen3

--reasoning-parser 옵션을 주면 vLLM이 응답에서 thinking 태그를 분리해서 별도 필드로 보내준다. 클라이언트에서는 아무 처리도 필요 없다.

기타 삽질 포인트

  • Playground 에이전트 이름 캐싱: Mastra Playground는 에이전트 이름을 브라우저의 session storage에 캐싱한다. 에이전트 이름을 바꾸면 기존 캐시 때문에 에러가 난다. 브라우저 캐시를 지워야 한다.
  • model id의 openai/ prefix: 이 prefix가 없으면 Mastra가 프로바이더를 인식 못 한다. vLLM이 OpenAI-compatible이므로 openai/를 붙여야 한다.
  • Memory DB 경로: 절대경로를 쓰지 않으면 mastra dev와 mastra build에서 서로 다른 DB를 참조해서 대화가 사라진 것처럼 보인다.

결과와 배운 것

최종적으로 브라우저에서 로컬 LLM과 멀티턴 대화가 가능한 Playground가 완성되었다. GPU가 꺼져 있어도 대화를 시작하면 자동으로 올라온다. 대화 이력은 SQLite에 저장되어 세션 간에도 유지된다.

Mastra는 아직 초기 프로젝트라 문서가 부실한 부분이 많다. 특히 커스텀 모델 연결과 Memory 설정은 소스 코드를 직접 읽어야 정확한 사용법을 알 수 있었다. 하지만 한번 설정해두면 TypeScript 생태계에서 가장 편하게 LLM Agent를 만들 수 있는 프레임워크라는 인상을 받았다.

GPU 자동 관리 패턴(상태 확인 -> 원격 시작 -> 폴링 대기)은 GPU를 여러 프로젝트가 나눠 쓰는 환경에서 범용적으로 적용할 수 있다. 이 패턴을 별도 모듈로 분리해두면 다른 프로젝트에서도 그대로 가져다 쓸 수 있다.

자주 묻는 질문

Mastra에서 vLLM을 연결할 때 createOpenAI()를 쓰면 안 되는 이유는?

createOpenAI()로 생성한 모델 인스턴스는 Mastra Agent의 model 필드와 호환되지 않는다. Agent 생성 시 model에 { id: 'openai/모델명', url: 'http://...' } 객체를 직접 전달해야 한다.

Mastra Memory의 storage와 Mastra 인스턴스의 storage가 같으면 안 되는 이유는?

같은 LibSQLStore 인스턴스를 공유하면 내부적으로 테이블 충돌이 발생한다. Memory용 storage와 Mastra 인스턴스용 storage는 반드시 별도 인스턴스로 생성해야 한다.

vLLM의 Thinking 태그를 Mastra에서 어떻게 처리하나?

extractReasoningMiddleware를 사용하면 Memory와 함께 사용 시 reasoning이 대화 이력에 저장되어 모델이 hang된다. vLLM 서버 측에서 --reasoning-parser 옵션으로 처리하는 것이 안정적이다.

자주 묻는 질문

Mastra에서 vLLM을 연결할 때 createOpenAI()를 쓰면 안 되는 이유는?
createOpenAI()로 생성한 모델 인스턴스는 Mastra Agent의 model 필드와 호환되지 않는다. Agent 생성 시 model에 { id: 'openai/모델명', url: 'http://...' } 객체를 직접 전달해야 한다.
Mastra Memory의 storage와 Mastra 인스턴스의 storage가 같으면 안 되는 이유는?
같은 LibSQLStore 인스턴스를 공유하면 내부적으로 테이블 충돌이 발생한다. Memory용 storage와 Mastra 인스턴스용 storage는 반드시 별도 인스턴스로 생성해야 한다.
vLLM의 Thinking 태그를 Mastra에서 어떻게 처리하나?
extractReasoningMiddleware를 사용하면 Memory와 함께 사용 시 reasoning이 대화 이력에 저장되어 모델이 hang된다. vLLM 서버 측에서 --reasoning-parser 옵션으로 처리하는 것이 안정적이다.
🤖 LLM 서버 운영기(2/3)
Prev

RTX 5090에서 vLLM 돌리기: cu130 의존성 지옥 탈출기

Next

vLLM 서빙 속도 2배: NVFP4 양자화와 CUDA Graph 최적화