5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TanStackファミリーに新しい仲間が!Tanstack AI を使ってAIアシスタントを作る

Last updated at Posted at 2025-12-17

はじめに

この記事は 株式会社TRAILBLAZER Advent Calendar 2025 の記事です。

TRAILBLAZERでフロントエンドエンジニアをしている田原です。

TanStackファミリーに新たな仲間が加わったのでご紹介です。
その名も✨ TanStack AI
Reactだけでなく様々なフロントエンドフレームワーク・言語でのアプリケーションにAI機能を簡単に統合できるフレームワークです。

今回はGeminiのAPIと統合したアシスタントチャットボットの実装方法を紹介します。

結果

旅行エージェント.gif

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を作成:

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検索ツールを定義します:

src/lib/tools/searchWeb.ts
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}件の検索結果を見つけました。`,
  }
})

ツールの仕組み

  1. 定義 (toolDefinition): ツールの名前、説明、入出力スキーマを定義
  2. 実装 (server): ツールの実際の処理を実装
  3. スキーマ (Zod): 入出力の型を定義し、AIに構造を伝える

AIがツールを使う流れ

AIは自動的に:

  1. ユーザーの質問からツールを使うべきか判断
  2. 適切なパラメータでツールを呼び出し
  3. ツールの結果を元に回答を生成

します。これがTanStack AIのFunction callingの魅力です。

4. システムプロンプトの定義

AIの振る舞いを定義します。src/lib/prompts/system.ts

src/lib/prompts/system.ts
export const SYSTEM_PROMPT = `
あなたは親切で知識豊富なアシスタントです。

## 回答ルール
1. 丁寧でフレンドリーな対応を心がける
2. 不明な点は推測せず、正直に分からないと伝える
3. 最新の情報が必要な場合はWeb検索ツールを使用する
4. 回答は簡潔に、必要に応じて箇条書きを使用

## 利用可能なツール
- searchWeb: インターネット上の最新情報を検索できます
`;

5. フロントエンド: useChatフック

TanStack AIのuseChatフックを使用してチャットUIを実装します:

src/components/chat.tsx
'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
src/components/markdown-components.tsx
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を使用してルーティングを設定します:

src/routes/index.tsx
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を使用することで、以下のような利点があります:

  1. シンプルな実装: わずか数行のコードでAI機能を追加
  2. 柔軟性: 複数のAI Providerに対応、簡単に切り替え可能
  3. 強力なTools: Function callingで外部機能との統合が簡単
  4. 優れたDX: TypeScriptによる型安全性とReact統合
  5. リアルタイム: ストリーミング対応で優れたUX

TanStack AIはまだ新しいライブラリですが、TanStackファミリーの一貫したAPIデザインにより、学習コストが低く、すぐに使い始められます。

AI機能を持つアプリケーションを構築する際は、ぜひTanStack AIを検討してみてください!


最後に

本記事を最後まで読んで頂きありがとうございます:bow:

TRAILBLAZERでは一緒に働くメンバーを募集中です!!
皆さまからのご連絡お待ちしております:train:


参考リンク

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?