EMQX + Volcano Engine RTCでリアルタイム音声エージェントを構築する
本ドキュメントでは、Docker Composeを使ってAIエージェントのデモをデプロイする方法を説明します。本デモはブラウザ上の知能人形をスマートデバイスのシミュレーションとして使用し、Volcano Engine RTCを活用して低レイテンシの音声対話を実現します。さらに、MCPを介したMQTTプロトコルによるデバイス側機能(写真撮影、表情切替、音量制御など)の呼び出しや、Volcano EngineのCustomLLMモードを利用したカスタムAIエージェントサービスとの統合による多ターン対話やツール呼び出しを紹介します。音声対話からデバイス制御までの一連のワークフローを体験できます。
デモ動画もぜひご覧ください。
アーキテクチャ概要
コンポーネント
システムは以下の3つのコアコンポーネントで構成されています。
| コンポーネント | 役割 | ポート | 主な責務 |
|---|---|---|---|
| volc-server | Volcano Engineプロキシ | 3002 | RTCルーム・トークン管理、CustomLLMのコールバック先設定 |
| web | MCPサーバー | 8080 | フロントエンドUI、カメラ・表情・音量などのハードウェア制御ツールを公開 |
| app | MCPクライアント+AIエージェント | 8081 | /chat-streamエンドポイント提供、LLM/VLM推論およびMCPツール呼び出し処理 |
通信フロー
コア機能:
- MCP over MQTT:EMQXブローカーを介したネットワーク越えのツール呼び出し。AIエージェントがカメラ、表情、音量などのデバイス機能を制御
- マルチモーダル理解:VLMを統合し、「今何を持っている?」などの視覚的ユースケースに対応
- リアルタイム音声対話:Volcano Engine RTC+ASR/TTSによる低レイテンシの音声認識と合成をエンドツーエンドで実現
- 並列処理アーキテクチャ:ツール呼び出しと音声合成を非同期で処理し、滑らかなユーザー体験を提供
前提条件
1. Docker環境
Docker 24以上(docker --versionで確認可能)。
2. MQTTブローカー
本プロジェクトでは、webサービス(MCPサーバー)とappサービス(MCPクライアント+AIエージェント)コンテナが接続可能なEMQXブローカーが必要です。
デプロイ方法(いずれかを選択):
- 自己ホスト型:EMQXインストールガイドを参照
- マネージドサービス:EMQX Cloudを利用
設定例:
MQTT_BROKER_HOST=localhost # EMQXブローカーのホスト
MQTT_BROKER_PORT=1883 # MQTTポート
MQTT_USERNAME=your_username # 認証有効時のユーザー名
MQTT_PASSWORD=your_password # 認証有効時のパスワード3. LLM APIキー
本プロジェクトはVolcano Engine CustomLLMモードを通じてカスタムAIエージェントを統合します。デフォルトではAlibaba Cloud Bailianのqwen-flashモデルを使用します。
Alibaba Cloud Bailianの有効化
- Alibaba Cloud Bailianコンソールにアクセス
- 上部に有効化の案内があればクリックしてサービスを有効化(無料枠内は無料、有料枠超過時のみ課金)
- 必要に応じて本人確認を完了
APIキーの作成
- API-KEY管理画面にアクセス
- API-Keyタブで「API-KEY作成」をクリック
- アカウントとワークスペース(通常はデフォルト)を選択し、説明を入力して作成
- APIキー横のコピーアイコンでシークレットを取得
app/.envのDASHSCOPE_API_KEYに設定
他のモデルサービスの利用(任意)
OpenAI互換のモデルサービスを利用する場合はapp/.envを以下のように更新:
LLM_API_BASE=https://your-model-service.com/v1 # モデルサービスのベースURL
LLM_API_KEY=your_api_key # モデルサービスのAPIキー
LLM_MODEL=your_model_name # モデル名代表的なモデルサービスのエンドポイント:
- OpenAI:
https://api.openai.com/v1 - DeepSeek:
https://api.deepseek.com/v1 - その他互換サービス:各プロバイダーのドキュメントを参照
LLMサービスによってレイテンシやコストは大きく異なります。要件に応じて選択してください。低レイテンシを重視する場合はデフォルトのAlibaba Cloud Bailian qwen-flashがおすすめです。
4. Volcano Engine認証情報
本プロジェクトは複数のVolcano Engineサービスを利用します。Volcano Engineコンソールで登録・ログインしてください。
有効化が必要なサービス:
- RTCサービス — 有効化ガイド
- 有効化後、
VOLC_RTC_APP_IDとVOLC_RTC_APP_KEYを取得 - 取得場所:RTCコンソール
- 有効化後、
- ASR/TTS音声サービス — Doubao Speechコンソール
- アプリ作成時に以下を選択:
- ASR:ストリーミング音声認識
- TTS:音声合成
- 以下の認証情報を取得:
VOLC_ASR_APP_ID- ASRアプリIDVOLC_TTS_APP_ID- TTSアプリIDVOLC_TTS_APP_TOKEN- TTSアプリトークンVOLC_TTS_RESOURCE_ID- TTSリソースID(選択した音声による)
- アプリ作成時に以下を選択:
- アカウント認証情報 — キー管理
VOLC_ACCESS_KEY_ID- アクセスキーIDVOLC_SECRET_KEY- シークレットアクセスキー
権限設定
必須:RTCコンソールでクロスサービス認可を設定しないと、エージェントがASR/TTS/LLMサービスを正しく呼び出せません。
メインアカウント呼び出し(推奨、簡単):
- メインアカウントでRTCコンソールにログイン
- クロスサービス認可へ移動
- 「ワンクリックでクロスサービス認可を有効化」をクリックし、
VoiceChatRoleForRTCロールを設定 - メインアカウントのAK/SKを使ってサービスを呼び出す
サブアカウント呼び出し(任意、追加設定が必要):
詳細ガイド:リアルタイム会話AIの前提条件
LLM設定
本プロジェクトはCustomLLMモードを使用し、Volcano EngineがアプリのカスタムAIエージェントサービスにコールバックしてLLM応答を取得します。
主要設定:
VOLC_LLM_URL- アプリの/chat-streamエンドポイントを指す- ローカル環境:
http://app:8081/chat-stream(コンテナネットワーク内) - 本番環境:
https://your-domain.com/chat-stream(公開アクセス可能であること)
- ローカル環境:
VOLC_LLM_API_KEY- カスタム認証キー。アプリのCUSTOM_LLM_API_KEYと完全一致させる必要あり(後述の「ステップ2:環境変数設定」参照)
オプションのモデルソース:
- Volcano Ark:Arkコンソールで推論エンドポイントやアプリを作成
- Cozeプラットフォーム:Cozeでエージェントを作成 — ガイド
- サードパーティモデル:OpenAI互換サービスURLを準備 — 要件
注意:本プロジェクトのアプリサービスはすでにCustomLLMプロトコルを実装済みです。DASHSCOPE_API_KEYなどのAPIキーを設定するだけで、追加のモデルサービスデプロイは不要です。
パラメータの迅速取得
推奨:公式Volcano Engineデモで設定を素早く検証可能。
- リアルタイム会話AIデモを開く
- デモ実行後、右上の「APIアクセス」ボタンをクリック
- パラメータ設定スニペットをコピーし、必要な認証情報を抽出
5. ネットワーク要件
開放すべきポート(デフォルト、Composeファイルで調整可能):
8080- Web UI8081- Appバックエンド(SSEエンドポイント)3002- volc-serverプロキシ(Volcano Engineサービス設定)
アクセス要件:
重要:本プロジェクトでMCP over MQTTを完全に体験するには、appサービスの/chat-streamエンドポイントを公開可能なHTTPS環境にデプロイし、Volcano Engineがコールバックできるようにする必要があります。
- 本番環境(推奨):appを公開HTTPS URL(例:
https://your-domain.com/chat-stream)にデプロイし、SSEストリームがdata: [DONE]で正しく終了することを確認 - ローカルテスト:非公開環境ではLLM推論やMCPツール呼び出しAPIのみテスト可能。Volcano Engineの音声対話は完全には体験できません
クイックチュートリアル:10分で音声対話+デバイス制御デモを動かす
前提条件をすべて満たしたら、以下の手順で音声対話とデバイス制御を備えたAIエージェントデモを素早くセットアップできます(「デバイス」はWeb UI上でシミュレーションされます)。
ステップ1:コードを取得
git clone -b volcengine/rtc https://github.com/emqx/mcp-ai-companion-demo.git
cd mcp-ai-companion-demoステップ2:環境変数を設定
最も重要なステップです。前提条件で取得した認証情報を3つのサービスの設定ファイルに正しく入力してください。各項目の説明と取得元をよく確認してください。
2.1 appサービス(AIエージェントバックエンド)の設定
設定ファイルを作成:
cp app/.env.example app/.envapp/.envを編集し、以下を入力:
# ===== LLM設定 =====
# 出典:前提条件「3. LLM APIキー」
# 目的:AIエージェントがLLMを呼び出して対話推論を行う
DASHSCOPE_API_KEY=sk-xxxxxxxxxxxxx # Alibaba Cloud BailianのAPIキーに置き換え
# 他のモデルサービスを使う場合は以下も設定:
# LLM_API_BASE=https://api.openai.com/v1
# LLM_MODEL=gpt-4
# ===== CustomLLM認証キー =====
# 出典:自分で生成(強力なランダム文字列を推奨)
# 目的:Volcano Engineがコールバックリクエストを検証するために使用
# 要件:volc-serverのVOLC_LLM_API_KEYと完全一致させること
CUSTOM_LLM_API_KEY=your-strong-random-secret-key-here
# 生成例(ターミナルで実行):
# openssl rand -base64 32
# またはオンラインツール:https://www.random.org/strings/
# ===== MQTTブローカー設定 =====
# 出典:前提条件「2. MQTTブローカー」
# 目的:MCP over MQTT通信のためEMQXブローカーに接続
MQTT_BROKER_HOST=localhost # EMQXブローカーのホスト
MQTT_BROKER_PORT=1883 # MQTTポート
# EMQX認証が有効な場合:
MQTT_USERNAME=your_mqtt_username # EMQXユーザー名(任意)
MQTT_PASSWORD=your_mqtt_password # EMQXパスワード(任意)
# ===== 任意設定 =====
MCP_TOOLS_WAIT_SECONDS=5 # MCPツール登録待機秒数
PHOTO_UPLOAD_DIR=uploads # 写真アップロードディレクトリ
# APP_SSL_CERTFILE=/path/to/cert # HTTPS証明書(本番用)
# APP_SSL_KEYFILE=/path/to/key # HTTPS秘密鍵(本番用)補足:
DASHSCOPE_API_KEYとCUSTOM_LLM_API_KEYの違い:DASHSCOPE_API_KEY:アプリがAlibaba Cloud BailianなどのLLMサービスを能動的に呼び出す際に使用CUSTOM_LLM_API_KEY:Volcano Engineからのコールバックを認証するために使用(APIゲートウェイトークンのような役割)
CUSTOM_LLM_API_KEYの生成例(いずれかを選択):bash# オプション1:opensslで生成(推奨) openssl rand -base64 32 # オプション2:Pythonで生成 python3 -c "import secrets; print(secrets.token_urlsafe(32))" # オプション3:オンラインツール # https://www.random.org/strings/ (長さ32、英数字)
2.2 volc-serverサービス(Volcano Engineプロキシ)の設定
設定ファイルを作成:
cp volc-server/.env.example volc-server/.envvolc-server/.envを編集し、Volcano Engine認証情報を入力:
# ===== Volcano Engineアカウント認証情報 =====
# 出典:前提条件「4. Volcano Engine認証情報 > アカウント認証情報」
# 取得場所:https://console.volcengine.com/iam/keymanage/
VOLC_ACCESS_KEY_ID=AKLT*********************
VOLC_SECRET_KEY=************************************
# ===== RTCサービス認証情報 =====
# 出典:前提条件「4. Volcano Engine認証情報 > RTCサービス」
# 取得場所:https://console.volcengine.com/rtc/aigc/listRTC
VOLC_RTC_APP_ID=your_rtc_app_id
VOLC_RTC_APP_KEY=your_rtc_app_key
# ===== ASR/TTS音声サービス認証情報 =====
# 出典:前提条件「4. Volcano Engine認証情報 > ASR/TTS音声サービス」
# 取得場所:https://console.volcengine.com/speech/app
VOLC_ASR_APP_ID=your_asr_app_id
VOLC_TTS_APP_ID=your_tts_app_id
VOLC_TTS_APP_TOKEN=your_tts_app_token
VOLC_TTS_RESOURCE_ID=your_tts_resource_id
# ===== CustomLLM設定 =====
# 目的:Volcano EngineにLLM応答取得先を通知
# VOLC_LLM_URL - appの/chat-streamエンドポイント
# ローカルテスト時:Dockerコンテナネットワークを利用
# VOLC_LLM_URL=http://app:8081/chat-stream
# 本番環境:公開HTTPS URLである必要あり(Volcano Engineコールバック用)
VOLC_LLM_URL=https://your-domain.com/chat-stream
# VOLC_LLM_API_KEY - CustomLLM認証キー
# 要件:app/.envのCUSTOM_LLM_API_KEYと完全一致させること
VOLC_LLM_API_KEY=your-strong-random-secret-key-here # appと一致させる設定チェックリスト:
| 項目 | 設定項目 | 取得元 |
|---|---|---|
| Volcano Engine認証情報 | VOLC_ACCESS_KEY_ID、VOLC_SECRET_KEY | Volcano Engineコンソール |
| RTCアプリ設定 | VOLC_RTC_APP_ID、VOLC_RTC_APP_KEY | RTCコンソール |
| 音声サービス設定 | VOLC_ASR_APP_ID、VOLC_TTS_APP_ID、VOLC_TTS_APP_TOKEN、VOLC_TTS_RESOURCE_ID | Doubao Speechコンソール |
| LLMキー整合性 | VOLC_LLM_API_KEY | app/.envのCUSTOM_LLM_API_KEYと完全一致させること |
| 権限設定 | クロスサービス認可 | 「権限設定」前提条件を完了済み |
2.3 webサービス(フロントエンドUI)の設定
webサービスはビルド時環境変数を使用します。ローカル開発のデフォルト設定で通常は十分です:
VITE_AIGC_PROXY_HOST=http://localhost:3002 # volc-serverプロキシのアドレスカスタマイズが必要なのは:
- volc-serverをリモートホストにデプロイした場合
- volc-serverが3002以外のポートを使う場合
起動前に環境変数をエクスポートして設定:
export VITE_AIGC_PROXY_HOST=http://your-remote-host:3002設定マッピングまとめ
前提条件 設定ファイルの場所
├─ 3. LLM APIキー ──► app/.env (DASHSCOPE_API_KEY)
├─ 4. Volcano Engine認証情報
│ ├─ アカウント認証情報 ──► volc-server/.env (VOLC_ACCESS_KEY_ID/SECRET_KEY)
│ ├─ RTCサービス ──► volc-server/.env (VOLC_RTC_APP_ID/APP_KEY)
│ ├─ ASR/TTSサービス ──► volc-server/.env (VOLC_ASR_*/VOLC_TTS_*)
│ └─ LLM設定 ──► volc-server/.env (VOLC_LLM_URL/API_KEY)
└─ 2. MQTTブローカー ──► app/.env (MQTT_BROKER_HOST/PORT/USERNAME/PASSWORD)
自分で生成
└─ CUSTOM_LLM_API_KEY ──► app/.env + volc-server/.env(両方で一致必須)ポイント:
CUSTOM_LLM_API_KEYは唯一自分で生成し、app/.envとvolc-server/.envで完全に一致させる必要がありますDASHSCOPE_API_KEYはLLM呼び出し用、CUSTOM_LLM_API_KEYはVolcano Engineコールバック認証用です- 本番環境では
VOLC_LLM_URLを公開HTTPS URLに変更しないと、Volcano Engineがアプリにコールバックできません
ステップ3:サービスを起動
Docker Composeで全サービスを起動:
docker compose -f docker/docker-compose.web-volc.yml up --build起動処理:
- イメージビルド:
mcp-app、mcp-volc-server、mcp-web - コンテナ起動、以下ポートで待機:
8080- Web UI8081- AIエージェントバックエンド3002- Volcano Engineプロキシ
初回起動は依存関係のダウンロードやイメージビルドに数分かかる場合があります。
ログ確認(任意):
# 全サービスのログをフォロー
docker compose -f docker/docker-compose.web-volc.yml logs -f
# 特定サービスのログをフォロー
docker compose -f docker/docker-compose.web-volc.yml logs -f appステップ4:動作確認
4.1 Web UIを開く
ブラウザで http://localhost:8080 を開きます。
チャットボットのアバター、マイク、カメラボタンなどを備えた仮想デバイス画面が表示されます。
4.2 MQTT接続の設定(初回のみ)
- 画面右上の設定アイコンをクリック
- 設定パネルでEMQXブローカー情報を入力:
- ブローカー:
ws://localhost:8083/mqtt(MQTTポート1883ではなくWebSocketポート8083を使用) - ユーザー名:EMQX認証が有効なら入力
- パスワード:EMQX認証が有効なら入力
- ブローカー:
- 「保存」をクリック
- 確認ダイアログで「確認」をクリックするとページが自動リロードされ、新設定が反映されてMQTT接続が自動的に確立されます
補足:
- デバイスIDは自動生成されます(形式:
web-ui-hardware-controller/{randomID})、手動設定は不要です - MQTT接続成功後、MCPツールが自動登録され、AIエージェントから呼び出し可能になります
- 接続失敗時はEMQXのWebSocketリスナーが有効か(デフォルト8083)を確認してください
4.3 音声対話を開始
画面下部のマイクボタンをクリックし、マイク使用許可を与えます。システムが自動的にRTC接続を確立します。接続成功するとマイクボタンが紫色になり、話しかけられる状態になります。
推奨テスト例:
- 「こんにちは」や「物語を聞かせて」と話しかけて基本対話を確認
- 「今何を持っている?」と言って写真撮影+視覚認識をトリガー
- 「音量を80%にして」や「笑顔の表情にして」と言ってデバイス制御を確認
4.4 成功の目安
- 音声対話:ASRの文字起こしが正確、LLMがストリーミング応答、TTS再生が正常
- MCPツール呼び出し:写真撮影、表情切替、音量調整がすべて反映される
- ログにエラーなし:app、volc-server、ブラウザコンソールにエラーが表示されない
4.5 部分機能テスト
カスタムAIエージェントを使わず、UIとVolcano Engine設定のみ検証したい場合:
docker compose -f docker/docker-compose.web-volc.yml up --build volc-server webモードの特徴:
- 利用可能:ASR、TTS、基本的な対話
- 利用不可:MCPツール呼び出し(カメラ、表情、音量制御など)
Volcano ArkプラットフォームのLLMを使う場合:
Arkコンソールで推論エンドポイントまたはAgentアプリを作成
EndpointId(推論エンドポイント)またはBotId(Agentアプリ)を取得volc-server/src/config.tsでLLM設定を変更:typescriptllm: { mode: 'ArkV3', // ArkプラットフォームLLMを使用 endpointId: 'ep-xxx', // オプション1:推論エンドポイントID(どちらか一方) // botId: 'bot-xxx', // オプション2:AgentアプリID(どちらか一方) systemMessages: [ { role: 'system', content: 'You are a friendly voice assistant' } ], historyLength: 5, // コンテキスト履歴ターン数 }volc-serverサービスを再起動し、ArkプラットフォームLLMを利用開始
ヒント:よりスムーズな対話には、Doubao-proシリーズなどの軽量モデルを推奨します。詳細な設定パラメータはVolcano Engineドキュメントを参照してください。
ステップ5:サービス停止
docker compose -f docker/docker-compose.web-volc.yml downFAQおよびトラブルシューティング
設定調整
ポート競合
ポートが使用中の場合、docker/docker-compose.web-volc.ymlのポートマッピングを変更してください:
services:
web:
ports:
- "8888:8080" # Web UIポートを変更
app:
ports:
- "8082:8081" # appポートを変更
volc-server:
ports:
- "3003:3002" # volc-serverポートを変更注意:volc-serverのポートを変えた場合は、VITE_AIGC_PROXY_HOST環境変数も更新してください。
HTTPS有効化(本番環境)
証明書ファイル(
fullchain.pem、privkey.pem)を用意重要:単一証明書ファイルではなくフルチェーン(完全な証明書チェーン)を使用してください。Volcano Engineのコールバックはフルチェーン検証を行うため、これがないとSSLハンドシェイクに失敗します。
- Let’s Encryptの場合:
fullchain.pem(証明書+中間証明書を含む)を使用 - その他CAの場合:サーバー証明書+中間証明書を含むフルチェーンファイルを用意
- Let’s Encryptの場合:
プロジェクトディレクトリ(例:
certs/)に配置app/.envに証明書パスを設定:bashAPP_SSL_CERTFILE=./certs/fullchain.pem # フルチェーンであること APP_SSL_KEYFILE=./certs/privkey.pemvolc-server/.envのVOLC_LLM_URLをHTTPSアドレス(例:https://your-domain.com:8081)に更新
イメージを個別にビルド
特定サービスのイメージをビルドする場合:
docker build -t mcp-web:local ./web
docker build -t mcp-app:local ./app
docker build -t volc-server:local ./volc-serverよくある問題
サービス起動問題
| 問題 | 原因候補 | 対処方法 |
|---|---|---|
| コンテナが起動しない | ポートが既に使用中 | 1) lsof -i :8080でプロセス確認 2) Composeのポート変更 3) 再度docker compose up --build実行 |
| 環境変数が反映されない | .envファイルが読み込まれていない | 1) .envが正しいディレクトリにあるか確認 2) ファイル権限確認 3) イメージ再ビルド |
Volcano Engineサービス問題
| 問題 | 原因候補 | 対処方法 |
|---|---|---|
| 「AI準備中」状態で停止 | クロスサービス認可未設定 | 1) 「権限設定」前提条件を完了 2) サービス有効化と残高確認 3) パラメータの大文字小文字確認 |
| 401/403エラー | AK/SKまたはトークン誤り | 1) VOLC_ACCESS_KEY_ID/VOLC_SECRET_KEY確認 2) トークン有効期限確認 3) クロスサービス認可確認 |
| サブアカウントのクォータ制限 | デフォルトクォータ不足 | クォータセンターで増枠申請 |
LLMリクエスト問題
| 問題 | 原因候補 | 対処方法 |
|---|---|---|
| LLMリクエスト失敗 | APIキー誤り | 1) DASHSCOPE_API_KEYが正しいか確認 2) ネットワーク接続確認 3) ログ確認:docker compose logs app |
| CustomLLMコールバック失敗 | 認証キー不一致 | 1) CUSTOM_LLM_API_KEYが両者で一致しているか確認 2) VOLC_LLM_URL確認 3) volc-serverからappへの到達性確認 |
| HTTPSコールバック失敗 | 証明書チェーン不完全 | フルチェーン証明書を使用:APP_SSL_CERTFILEはfullchain.pemを指すこと(単一のcert.pem不可)。Volcano Engineコールバックはフルチェーン検証が必須。 |
MCPツール呼び出し問題
| 問題 | 原因候補 | 対処方法 |
|---|---|---|
| ツールが利用できない | MQTT接続またはdevice_id問題 | 1) ブラウザコンソールでMQTT状態確認 2) デバイスIDが一致しているか確認 3) MCP_TOOLS_WAIT_SECONDS=10に増やす |
| カメラ写真撮影が失敗 | 権限が許可されていない | 1) ブラウザのカメラ権限を確認 2) 許可をクリック 3) ページをリロード |
MQTT接続問題
| 問題 | 原因候補 | 対処方法 |
|---|---|---|
| MQTT接続失敗 | ブローカー設定誤り | 1) EMQXブローカーが起動しているか確認 2) MQTT_BROKER_HOST/PORT確認 3) 認証情報確認 4) ネットワーク接続テスト |
| Web UIが接続できない | WebSocketポートが開いていない | 1) WebSocketリスナーが有効か(デフォルト8083)確認 2) ws://スキームを使用(例:ws://localhost:8083/mqtt) |
ログの確認
# 全サービスのログをフォロー
docker compose -f docker/docker-compose.web-volc.yml logs -f
# 特定サービスのログをフォロー
docker compose -f docker/docker-compose.web-volc.yml logs -f app
# 最新100行を表示
docker compose -f docker/docker-compose.web-volc.yml logs --tail=100 appパフォーマンス最適化
- LLMレイテンシ:低レイテンシモデルを使用(推奨:Alibaba Cloud Bailian
qwen-flash) - 音声品質:
volc-server/src/config.tsでASRのVAD閾値やTTS音声選択を調整 - ツール呼び出しレイテンシ:appとweb間のネットワーク接続を最適化し、MQTTレイテンシを減らす(同一LANや低レイテンシ環境推奨)
ローカル開発(非Docker環境):
- web:
pnpm dev - app:
uv run ... - volc-server:
bun run dev