はじめに
この記事は 株式会社TRAILBLAZER Advent Calendar 2025 の記事です。
TRAILBLAZERでフロントエンドエンジニアをしている田原です。
TanStackファミリーに新たな仲間が加わったのでご紹介です。
その名も✨ TanStack AI ✨
Reactだけでなく様々なフロントエンドフレームワーク・言語でのアプリケーションにAI機能を簡単に統合できるフレームワークです。
今回はGeminiのAPIと統合したアシスタントチャットボットの実装方法を紹介します。
結果
TanStack AIについてもう少し詳しく
TanStackファミリーの新しいライブラリで、以下の特徴があります。
- 複数のAI Provider対応: OpenAI、Gemini、Anthropicなど様々なAI APIをサポート
- ストリーミング対応: リアルタイムでAIの応答を表示
-
React統合:
useChatフックで簡単にチャット機能を実装 - 型安全: TypeScriptで完全に型付けされている
- Tools対応: Function callingでAIに外部ツールを使わせることができる
Vercel AIやMastraなどと似たポジションのライブラリです。
構成について
今回作成するプロジェクトの主要な技術スタック:
{
"dependencies": {
"@tanstack/ai": "^0.0.3",
"@tanstack/ai-gemini": "^0.0.3",
"@tanstack/ai-react": "^0.0.3",
"@tanstack/react-router": "^1.132.0",
"@tanstack/react-start": "^1.132.0",
"react": "^19.2.0",
"react-markdown": "^10.1.0",
"tailwindcss": "^4.0.6"
}
}
実装手順
1. プロジェクトのセットアップ
まず、必要なパッケージをインストールします:
npm install @tanstack/ai @tanstack/ai-gemini @tanstack/ai-react
Gemini APIキーを取得して、.envに設定します:
# .env
GOOGLE_GEMINI_API_KEY=your_api_key_here
APIキーはGoogle AI Studioから取得できます。
2. APIエンドポイントの作成
TanStack Routerを使用してAPIエンドポイントを作成します。src/routes/api/chat.tsを作成:
import { chat, toStreamResponse } from '@tanstack/ai'
import { gemini } from '@tanstack/ai-gemini'
import { createFileRoute } from '@tanstack/react-router'
import { SYSTEM_PROMPT } from '@/lib/prompts/system'
import { searchWebTool } from '@/lib/tools/searchWeb'
export const Route = createFileRoute('/api/chat')({
server: { handlers: { POST } },
})
export async function POST({ request }: { request: Request }) {
// APIキーの確認
if (!process.env.GOOGLE_GEMINI_API_KEY) {
return new Response(
JSON.stringify({
error: 'GOOGLE_GEMINI_API_KEY not configured',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
},
)
}
const { messages, conversationId } = await request.json()
try {
// TanStack AIのchat関数を使用
const stream = chat({
adapter: gemini(),
messages,
model: 'gemini-2.0-flash-exp',
systemPrompts: [SYSTEM_PROMPT],
conversationId,
tools: [searchWebTool], // ツールを追加
})
return toStreamResponse(stream)
} catch (error) {
console.error(error)
return new Response(
JSON.stringify({
error: 'エラーが発生しました。',
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
},
)
}
}
TanStack AIの核心: chat()関数
TanStack AIの中核となるchat()関数について詳しく見ていきましょう。
const stream = chat({
adapter: gemini(), // AI Providerの指定
messages, // 会話履歴
model: 'gemini-2.0-flash-exp', // 使用するモデル
systemPrompts: [SYSTEM_PROMPT], // システムプロンプト
conversationId, // 会話ID
tools: [searchWebTool], // 使用可能なツール
})
主要なパラメータ
| パラメータ | 説明 | 必須 |
|---|---|---|
adapter |
使用するAI Provider(gemini、openai、anthropicなど) | ✅ |
messages |
ユーザーとアシスタントの会話履歴 | ✅ |
model |
使用するモデル名 | ✅ |
systemPrompts |
AIの振る舞いを定義するプロンプト | ❌ |
conversationId |
会話を識別するID | ❌ |
tools |
AIが使用できるツール(Function calling) | ❌ |
アダプターの切り替え
TanStack AIの大きな利点は、アダプターを変更するだけで別のAI Providerに切り替えられることです:
// Gemini
const stream = chat({ adapter: gemini(), ... })
// OpenAI
const stream = chat({ adapter: openai(), ... })
// Anthropic
const stream = chat({ adapter: anthropic(), ... })
ストリーミングレスポンス
chat()関数はAsyncIterableを返し、toStreamResponse()でHTTPレスポンスに変換します:
const stream = chat({ ... })
return toStreamResponse(stream) // Server-Sent Events形式で返す
これにより、AIの応答がリアルタイムでクライアントに送信されます。
3. TanStack AIの高度な機能: Tools
TanStack AIの強力な機能の一つがToolsです。これはFunction callingとも呼ばれ、AIに外部ツールを使わせることができます。
ツールの定義
src/lib/tools/searchWeb.tsでWeb検索ツールを定義します:
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'
// ツールの定義
export const searchWebDef = toolDefinition({
name: 'searchWeb',
description: 'インターネット上の情報を検索します。最新のニュース、観光情報、一般的な知識など、あらゆる情報を検索できます。',
inputSchema: z.object({
query: z.string().describe('検索キーワード'),
}),
outputSchema: z.object({
results: z.array(
z.object({
title: z.string().describe('検索結果のタイトル'),
snippet: z.string().describe('検索結果の要約'),
url: z.string().describe('検索結果のURL'),
})
),
summary: z.string().describe('検索結果の要約'),
}),
})
// ツールの実装
export const searchWebTool = searchWebDef.server(async ({ query }) => {
// DuckDuckGo HTML版を使用して検索
const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`
const response = await fetch(searchUrl, {
headers: {
'User-Agent': 'Mozilla/5.0...',
},
})
const html = await response.text()
// HTMLから検索結果を抽出
const results = extractSearchResults(html)
return {
results,
summary: `「${query}」について${results.length}件の検索結果を見つけました。`,
}
})
ツールの仕組み
-
定義 (
toolDefinition): ツールの名前、説明、入出力スキーマを定義 -
実装 (
server): ツールの実際の処理を実装 - スキーマ (Zod): 入出力の型を定義し、AIに構造を伝える
AIがツールを使う流れ
AIは自動的に:
- ユーザーの質問からツールを使うべきか判断
- 適切なパラメータでツールを呼び出し
- ツールの結果を元に回答を生成
します。これがTanStack AIのFunction callingの魅力です。
4. システムプロンプトの定義
AIの振る舞いを定義します。src/lib/prompts/system.ts:
export const SYSTEM_PROMPT = `
あなたは親切で知識豊富なアシスタントです。
## 回答ルール
1. 丁寧でフレンドリーな対応を心がける
2. 不明な点は推測せず、正直に分からないと伝える
3. 最新の情報が必要な場合はWeb検索ツールを使用する
4. 回答は簡潔に、必要に応じて箇条書きを使用
## 利用可能なツール
- searchWeb: インターネット上の最新情報を検索できます
`;
5. フロントエンド: useChatフック
TanStack AIのuseChatフックを使用してチャットUIを実装します:
'use client'
import { useState } from 'react'
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
import ReactMarkdown from 'react-markdown'
import { markdownComponents } from './markdown-components'
export function Chat() {
const [input, setInput] = useState('')
// useChat フックでチャット機能を実装
const { messages, sendMessage, isLoading } = useChat({
connection: fetchServerSentEvents('/api/chat'),
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (input.trim() && !isLoading) {
sendMessage(input)
setInput('')
}
}
return (
<div className="flex flex-col h-full">
{/* メッセージ表示エリア */}
<div className="flex-1 overflow-y-auto p-6 space-y-4">
{messages.map((message) => (
<div key={message.id}>
{message.parts.map((part, idx) => (
<div key={idx}>
{part.type === 'text' && (
<ReactMarkdown components={markdownComponents}>
{part.content}
</ReactMarkdown>
)}
</div>
))}
</div>
))}
{/* ローディング表示 */}
{isLoading && (
<div className="flex items-center gap-2">
<span>考え中</span>
<div className="flex gap-1">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" />
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"
style={{ animationDelay: '150ms' }} />
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"
style={{ animationDelay: '300ms' }} />
</div>
</div>
)}
</div>
{/* 入力エリア */}
<form onSubmit={handleSubmit}>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="質問を入力..."
disabled={isLoading}
/>
<button type="submit" disabled={!input.trim() || isLoading}>
送信
</button>
</form>
</div>
)
}
useChatフックの詳細
const { messages, sendMessage, isLoading } = useChat({
connection: fetchServerSentEvents('/api/chat'),
})
返り値
| 値 | 型 | 説明 |
|---|---|---|
messages |
Message[] |
会話履歴の配列 |
sendMessage |
(content: string) => void |
メッセージ送信関数 |
isLoading |
boolean |
ロード中かどうか |
メッセージの構造
interface Message {
id: string
role: 'user' | 'assistant'
parts: Array<{
type: 'text' | 'thinking' | 'tool-call' | 'tool-result'
content: string
}>
}
ストリーミングの仕組み
fetchServerSentEventsを使用することで、Server-Sent Events (SSE)を介してリアルタイムでメッセージを受信します。
User → sendMessage()
→ POST /api/chat
→ SSE Stream
→ messages 更新
→ UI 再レンダリング
AIの応答が生成されるたびに、messagesが更新され、UIに反映されます。
6. Markdown対応
AIの応答をMarkdownとしてレンダリングするため、react-markdownを使用します:
npm install react-markdown remark-gfm rehype-highlight
import type { Components } from 'react-markdown'
export const markdownComponents: Components = {
// 太字
strong: ({ children, ...props }) => (
<strong className="font-bold" {...props}>
{children}
</strong>
),
// コード
code: ({ inline, children, ...props }) => {
return inline ? (
<code className="bg-gray-100 px-2 py-1 rounded" {...props}>
{children}
</code>
) : (
<code {...props}>{children}</code>
)
},
// その他のコンポーネント...
}
使用方法:
<ReactMarkdown components={markdownComponents}>
{part.content}
</ReactMarkdown>
7. ルーティング設定
TanStack Routerを使用してルーティングを設定します:
import { ClientOnly, createFileRoute } from '@tanstack/react-router'
import { Chat } from '@/components/chat'
export const Route = createFileRoute('/')({ component: App })
function App() {
return (
<div className="h-screen flex flex-col">
<header className="bg-blue-600 text-white p-4">
<h1 className="text-2xl font-bold">AIアシスタント</h1>
</header>
<main className="flex-1 overflow-hidden">
<ClientOnly>
<Chat />
</ClientOnly>
</main>
</div>
)
}
ClientOnlyを使用することで、SSR時のハイドレーションエラーを防ぎます。
TanStack AIの利点
1. シンプルで一貫性のあるAPI
TanStackファミリー共通のAPIデザインにより、学習コストが低く、直感的に使えます。
// わずか数行でAI機能を実装
const stream = chat({
adapter: gemini(),
messages,
model: 'gemini-2.0-flash-exp',
})
2. プロバイダー非依存
アダプターを変更するだけで、別のAI Providerに切り替え可能です。ベンダーロックインを避けられます。
3. 強力なTools(Function calling)
ツールを簡単に定義・実装でき、AIに外部機能を使わせることができます。
const searchWebTool = toolDefinition({
name: 'searchWeb',
description: '...',
inputSchema: z.object({ ... }),
outputSchema: z.object({ ... }),
}).server(async ({ query }) => {
// 実装
})
4. 型安全
TypeScriptで完全に型付けされており、開発時のミスを防げます。Zodスキーマによる入出力の型定義も明確です。
5. ストリーミング対応
リアルタイムで応答を表示できるため、優れたユーザーエクスペリエンスを提供できます。
6. React統合
useChatフックにより、Reactアプリケーションに簡単に統合できます。状態管理やローディング処理が自動化されます。
TanStack AIと他のライブラリの比較
| 機能 | TanStack AI | Vercel AI SDK | Langchain.js |
|---|---|---|---|
| 複数Provider対応 | ✅ | ✅ | ✅ |
| ストリーミング | ✅ | ✅ | ✅ |
| React統合 | ✅ | ✅ | ❌ |
| Function calling | ✅ | ✅ | ✅ |
| 型安全 | ✅ | ✅ | ⚠️ |
| 学習コスト | 低 | 低 | 高 |
| RAG対応 | ❌ | ⚠️ | ✅ |
TanStack AIは、TanStackファミリーとの統合や一貫性のあるAPIが強みです。
まとめ
TanStack AIを使用することで、以下のような利点があります:
- シンプルな実装: わずか数行のコードでAI機能を追加
- 柔軟性: 複数のAI Providerに対応、簡単に切り替え可能
- 強力なTools: Function callingで外部機能との統合が簡単
- 優れたDX: TypeScriptによる型安全性とReact統合
- リアルタイム: ストリーミング対応で優れたUX
TanStack AIはまだ新しいライブラリですが、TanStackファミリーの一貫したAPIデザインにより、学習コストが低く、すぐに使い始められます。
AI機能を持つアプリケーションを構築する際は、ぜひTanStack AIを検討してみてください!
最後に
本記事を最後まで読んで頂きありがとうございます![]()
TRAILBLAZERでは一緒に働くメンバーを募集中です!!
皆さまからのご連絡お待ちしております![]()
参考リンク
