Vercel AI SDK は、LLM とのストリーミング対話を TypeScript/JavaScript で扱うためのライブラリ群です。React / Vue / Svelte 向けの useChat、サーバー側の streamText、多数のプロバイダ抽象化が揃っており、チャット UI と薄い API サーバーを短いコードで繋げるのに向いています。
本記事では、SDK の機能と基本的な使い方を整理し、その上で SVG を生成・プレビューできるチャットアプリを作例として紹介します。SVG 部分は SDK の機能ではなくフロント側の拡張ですが、「テキスト応答の中に埋め込んだ構造化データを、チャット内でリッチに描画する」パターンの具体例として読んでもらえると思います。ちなみに Open WebUI の Artifacts を使えば簡単に同じことができますので、開発に興味のない方はそちらをお勧めします。
Vercel AI SDK とは
SDK は大きくサーバー側(ai)とクライアント側(@ai-sdk/vue 等)に分かれます。
| パッケージ | 役割 |
|---|---|
ai |
streamText、toDataStreamResponse、プロバイダ抽象化 |
@ai-sdk/vue |
Vue 3 向け useChat
|
ollama-ai-provider |
ローカル Ollama 接続 |
@ai-sdk/openai |
OpenAI API 接続 |
ポイントは、フロントが期待する Data Stream Protocol に合わせてバックエンドが応答を返すことです。自前で SSE をパースするコードを書く代わりに、useChat がメッセージ一覧・入力・ストリーミング受信・ローディング状態を面倒見てくれます。
サーバー側で使う機能
| 機能 | 用途 |
|---|---|
streamText |
LLM からトークンを逐次受け取る |
toDataStreamResponse |
useChat 互換の HTTP レスポンスに変換 |
system |
システムプロンプト注入 |
| Provider 切替 | 同じ API 形状で Ollama / OpenAI を替える |
最小構成のバックエンドは次のイメージです。
import { streamText } from 'ai'
import { createOllama } from 'ollama-ai-provider'
const ollama = createOllama({ baseURL: 'http://localhost:11434/api' })
const result = streamText({
model: ollama('qwen3.6:27b'),
system: 'あなたは親切なアシスタントです。',
messages: validation.messages,
})
return result.toDataStreamResponse()
streamText の戻り値をそのまま toDataStreamResponse() で返すのが、useChat との接続における定石です。
クライアント側で使う機能
Vue 3 では @ai-sdk/vue の useChat を使います。
import { useChat } from '@ai-sdk/vue'
const { messages, input, handleSubmit, isLoading, append, error } = useChat({
api: '/api/chat',
body: computed(() => ({
modelId: selectedModelId.value,
})),
onFinish: (message, { usage }) => {
// 応答完了時の処理(トークン数記録など)
},
})
| API | 用途 |
|---|---|
messages |
会話履歴(ストリーミング中も更新される) |
handleSubmit |
フォーム送信 |
append |
任意のユーザーメッセージを手動追加 |
body |
モデル ID など追加パラメータ |
isLoading / error
|
UI 状態 |
追加パラメータは body に computed で渡すのが重要です。modelId を plain なオブジェクトで渡すと、モデル切替がリクエストに反映されません。
本記事の作例で使っていない機能
SDK には Tool Calling、Structured Output、Embeddings、RAG などもありますが、SVG Chat では使っていません。リッチ UI は応答テキストをクライアントでパースする方式で実現しています(後述)。
開発時の構成
作例プロジェクト svgchat1 では yarn workspaces でフロント(Vite + Vue)とバックエンド(H3)を分け、Vite が /api を localhost:3001 へプロキシします。ブラウザから Ollama へ直接接続せず、API キーもバックエンドの環境変数だけに置く構成です。
yarn install
cp .env.example .env
ollama serve # 別ターミナル
yarn dev
環境変数の例です。
| 変数 | 用途 |
|---|---|
OLLAMA_BASE_URL |
Ollama 接続先 |
OLLAMA_MODEL |
デフォルトモデル |
OPENAI_API_KEY |
設定時のみ OpenAI モデルを一覧に追加 |
作例: SVG Chat
上記の SDK 構成をそのまま使い、ローカル Ollama(および任意で OpenAI)と対話して SVG を扱う Web チャットを作りました。
何ができるか
- チャットで SVG の生成・編集指示を出す
- 応答中の SVG をプレビューし、
.svgファイルとしてダウンロード - ベクトル描画キャンバスでラフを描き、SVG として LLM に送信
- 既存
.svgをキャンバスに読み込み、編集を依頼 - ヘッダーで Ollama / OpenAI モデルを切り替え
- アシスタント応答の Markdown 表示、トークン使用量などのメタ情報表示
チャットの芯は SDK 標準の useChat + streamText です。SVG 固有の処理はフロント側の拡張に閉じています。
エンドポイント
| メソッド | パス | 役割 |
|---|---|---|
POST |
/api/chat |
Data Stream でチャット応答 |
GET |
/api/models |
利用可能モデル一覧 |
モデル ID は {provider}:{modelName} 形式です(例: ollama:qwen3.6:27b、openai:gpt-4o)。
POST /api/chat リクエスト
{
"messages": [
{ "role": "user", "content": "赤い円の SVG を作って" }
],
"modelId": "ollama:qwen3.6:27b"
}
バックエンドは modelId を解析し、Ollama または OpenAI の LanguageModel を streamText に渡します。応答終了時には StreamData.appendMessageAnnotation でモデル名やトークン数を付与し、フロントの onFinish と message.annotations の両方からメタ情報を表示できます。
データの流れ
- ユーザーがメッセージ送信(必要なら SVG 付き)
-
useChatがPOST /api/chatを呼ぶ - バックエンドが
streamTextで LLM へ中継 - Data Stream を
useChatが受信しmessagesを更新 -
MessageItemがテキストを Markdown と SVG に分割して描画
SDK が担うのは 2〜4 です。5 以降はアプリ固有の UI レイヤです。
操作の要点
| 操作 | 方法 |
|---|---|
| モデル選択 | ヘッダーのプルダウン |
| メッセージ送信 | Ctrl+Enter |
| キャンバス付き送信 | Shift+Ctrl+Enter |
| SVG アップロード | 描画キャンバス → ツールバー |
SVG レンダラー(カスタムコンテンツレンダラー)
SVG Chat の見どころの一つが、LLM 応答の ```svg ブロックをチャット内で画像としてプレビューする仕組みです。SDK の Tool Calling ではなく、テキスト応答 + クライアント側パース + 専用 Vue コンポーネントという構成です。
なぜこの方式か
| 方式 | 特徴 |
|---|---|
| Tool Calling | サーバーが関数を実行。型付きだが API 実装が増える |
| Structured Output | JSON スキーマで返却。パースは楽だが UI 柔軟性に限界 |
| テキストフェンス + クライアントパース | プロンプトで形式を約束。useChat のままリッチ UI を載せられる |
SVG のように「説明文 + 長いソースコード」を返す用途では、Markdown のコードフェンスが LLM にとっても自然です。ストリーミング完了後にクライアントが抽出すれば、Tool Calling を導入せずにプレビュー・ダウンロード UI を載せられます。
AI への指示
backend/src/prompts/system.ts で出力形式を固定します。
- SVG を出力する場合は必ず ```svg コードブロックで返してください
- viewBox、width、height を明示してください
- SVG 以外の説明はコードブロックの外に書いてください
- 有効な XML として well-formed な SVG を生成してください
ユーザー側も同じ形式に揃えます。キャンバスやアップロードから LLM へ渡す SVG は wrapSvgInFence() で ```svg フェンスに包み、append または handleSubmit で送信します。
フロントエンド実装
抽出 — frontend/src/lib/svg.ts
export function extractSvgs(text: string): string[] {
// ```svg ... ``` を優先
// なければ <svg>...</svg> を拾う
}
export function extractTextWithoutSvgs(text: string): string {
// 説明文だけ Markdown 用に残す
}
描画 — MessageItem.vue が会話の各メッセージで分割表示します。
-
MarkdownContent— アシスタントの説明文 -
SvgContentRenderer— 抽出した SVG のプレビューとダウンロード - フォールバック — パースできない場合は生テキスト
レンダラー本体 — SvgContentRenderer.vue
- props で SVG 文字列を受け取る
-
v-htmlで SVG を描画 -
SvgDownloadButtonで.svg保存
<SvgContentRenderer
v-for="(svg, index) in svgs"
:key="index"
:svg="svg"
:index="index"
/>
新しいリッチ表示(表、グラフなど)を足す場合も、同じパターンでフェンスタグ → 抽出関数 → レンダラーコンポーネント → MessageItem と拡張できます。
注意点
- ストリーミング途中は SVG が不完全で、一時的にプレビュー失敗することがある(完了後に再描画)
-
v-html利用のため、信頼できない LLM 出力を公開する用途ではサニタイズが必要 - SVG 品質はモデル依存。プロンプトで形式を補正するしかない
成果物
まとめ
Vercel AI SDK は、streamText + toDataStreamResponse(サーバー)と useChat(クライアント)を繋ぐだけで、ストリーミングチャットの骨格が完成します。プロバイダを差し替えるだけで Ollama と OpenAI を同じ UI から使えるのも便利です。
SVG Chat は、その SDK 構成の上に ```svg フェンスとカスタムコンテンツレンダラーを載せた作例です。Tool Calling を使わなくても、プロンプトで出力形式を約束し、クライアントでパースすれば、チャット内にドメイン固有のリッチ UI を載せられます。
「まずローカル LLM でチャットを動かしたい」「その上で LLM の出力をテキスト以外でも見せたい」という段階なら、本記事の構成はそのまま転用しやすいと思います。
おまけ
LLMにどの程度の絵心があるか、比較してみました。「家の屋根に座ってこっちを見ている猫の絵を描いてください。」というプロンプトで描いてもらった例です。
llama3.2:3b(ローカルLLM/ファイルサイズ2㎇)
llama3.2:3bは、何を描いているのか分かりません。
qwen3.6:27b(ローカルLLM/ファイルサイズ17GB)
qwen3.6:27bは、屋根瓦の線や猫の耳がずれています。gpt-5.5と同じ元ネタがありそうです。
gpt-5.5(クラウドLLM/ファイルサイズ不明)
gpt-5.5は、指示通りに描けています。



