Upgrade to Pro — share decks privately, control downloads, hide ads and more …

AI SDKで作るチャットボット開発 / Chatbot Development with...

Avatar for shuntaka shuntaka
October 10, 2025
730

AI SDKで作るチャットボット開発 / Chatbot Development with AI SDK

Avatar for shuntaka

shuntaka

October 10, 2025
Tweet

More Decks by shuntaka

Transcript

  1. 自己紹介
 https://shuntaka.dev/who 髙橋俊⼀ (shuntaka) • 2016年 ⾦融情報ベンダー⼊社 バックエンド ◦ 株価配信Web API開発

    • 2019年 クラスメソッド⼊社 ◦ CX/IoT事業部にてIoT案件を複数 • 2024年 製造ビジネステクノロジー部担当 ◦ R&D業務/サーバーサイド/AIチャットボット開発 🔰 最近は⽣成AI/AIDD関連で情報発信をしています! 2
  2. AI SDKとは? 
 6 メジャー リリース時期 ※1 v1 2023-06-15 v2

    2023年後半 ~ 2024年初頭 v3 2024-03-01 v4 2024-11-18 v5 2025-07-31 ‧TypeScript向けAIツールキット ‧Next.jsの開発元のVercelが提供 ‧AIプロダクト構築に必要な機能を揃えたOSS SDK ‧⼤体9ヶ⽉毎にv1→v3 v3→v4 v4→v5と約9ヶ⽉ごと にメジャーが進み、機能とAPI設計が継続的に最適化 MastraやVoltAgentはAI SDKをベースにより簡単に AIエージェント構築可能。 本スライドはAI SDKのみにフォーカスします! ※ 公開告知を参考としており、Alpha/Beta段階は含みません
  3. チャットボットサーバーサイドのベースのコード(Hono想定) 
 8 const { messages } = await c.req.json();

    const response = createUIMessageStreamResponse({ status: 200, stream: createUIMessageStream({ async execute({ writer }) { const streamResult = streamText({ model: bedrock('us.anthropic.claude-sonnet-4-20250514-v1:0'), messages: convertToModelMessages(messages), system: 'あなたは親切なアシスタントです。ユーザーの質問に日本語で答えてください。', }); writer.merge(streamResult.toUIMessageStream()); } }), });
  4. クライアント(FEなど)からメッセージ受取 
 9 const { messages } = await c.req.json();

    const response = createUIMessageStreamResponse({ status: 200, stream: createUIMessageStream({ async execute({ writer }) { const streamResult = streamText({ model: bedrock('us.anthropic.claude-sonnet-4-20250514-v1:0'), messages: convertToModelMessages(messages), system: 'あなたは親切なアシスタントです。ユーザーの質問に日本語で答えてください。', }); writer.merge(streamResult.toUIMessageStream()); } }), });
  5. const { messages } = await c.req.json(); const response =

    createUIMessageStreamResponse({ status: 200, stream: createUIMessageStream({ async execute({ writer }) { const streamResult = streamText({ model: bedrock('us.anthropic.claude-sonnet-4-20250514-v1:0'), messages: convertToModelMessages(messages), system: 'あなたは親切なアシスタントです。ユーザーの質問に日本語で答えてください。', }); writer.merge(streamResult.toUIMessageStream()); } }), }); 多段でLLM呼び出しする拡張性を考慮しラップ 
 10
  6. const { messages } = await c.req.json(); const response =

    createUIMessageStreamResponse({ status: 200, stream: createUIMessageStream({ async execute({ writer }) { const streamResult = streamText({ model: bedrock('us.anthropic.claude-sonnet-4-20250514-v1:0'), messages: convertToModelMessages(messages), system: 'あなたは親切なアシスタントです。ユーザーの質問に日本語で答えてください。', }); writer.merge(streamResult.toUIMessageStream()); } }), }); HTTPレスポンスストリームへの書き込みインターフェース 
 11
  7. const { messages } = await c.req.json(); const response =

    createUIMessageStreamResponse({ status: 200, stream: createUIMessageStream({ async execute({ writer }) { const streamResult = streamText({ model: bedrock('us.anthropic.claude-sonnet-4-20250514-v1:0'), messages: convertToModelMessages(messages), system: 'あなたは親切なアシスタントです。ユーザーの質問に日本語で答えてください。', }); writer.merge(streamResult.toUIMessageStream()); } }), }); streamメッセージをストリームに書き込み、返却 
 12
  8. const { messages } = await c.req.json(); const response =

    createUIMessageStreamResponse({ status: 200, stream: createUIMessageStream({ async execute({ writer }) { const streamResult = streamText({ model: bedrock('us.anthropic.claude-sonnet-4-20250514-v1:0'), messages: convertToModelMessages(messages), system: 'あなたは親切なアシスタントです。ユーザーの質問に日本語で答えてください。', }); writer.merge(streamResult.toUIMessageStream()); } }), }); AIモデル指定 
 13
  9. v5はuseChatのメッセージ構造から変換が必要 
 14 const { messages } = await c.req.json();

    const response = createUIMessageStreamResponse({ status: 200, stream: createUIMessageStream({ async execute({ writer }) { const streamResult = streamText({ model: bedrock('us.anthropic.claude-sonnet-4-20250514-v1:0'), messages: convertToModelMessages(messages), system: 'あなたは親切なアシスタントです。ユーザーの質問に日本語で答えてください。', }); writer.merge(streamResult.toUIMessageStream()); } }), });
  10. StreamTextの内容 
 16 data: {"type":"start","messageId":"gc99f"} data: {"type":"start-step"} data: {"type":"text-start","id":"0"} data:

    {"type":"text-delta","id":"0","delta":"は"} (中略) data: {"type":"text-delta","id":"0","delta":"あります か?"} data: {"type":"text-end","id":"0"} data: {"type":"finish-step"} data: {"type":"finish"} typeを⾒てクライアント側 のuseChatがmessage構造 を作ってくれる
  11. writer.write 
 17 writer.write({ type: 'start', messageId: messageId, }); writer.write({

    type: 'start-step', }); writer.write({ type: 'text-start', id: '0', }); const textChunks = [ 'はじめまして!私は C', 'laude(クロード)です ', ]; for (const chunk of textChunks) { writer.write({ type: 'text-delta', id: '0', delta: chunk, }); await new Promise((resolve) => setTimeout(resolve, 1000)); } writer.write({ type: 'text-end', id: '0', }); writer.write({ type: 'finish-step', }); writer.write({ type: 'finish', }); writer経由でHTTPレスポンス ストリームへの書き込んで、 ⾃前でStreamTextっぽいこと も可能
  12. ワークフローを組む 
 18 タスクを明確なステップに分解ためにLLMに 複数回問い合わせるケースがある 参考: https://ai-sdk.dev/docs/agents/workflows 「AIエージェント実践⼊⾨」で Plan-and-Execute型エージェントの紹介があ り、実践的で分かりやすい

    https://www.kspub.co.jp/book/detail/5401408.html ざっくり流れは ①計画⽴案→②サブタスク回答→③最終回答作 成という流れで①-③全てでLLM呼び出しがあ る。②ではツール検索、リフレクションで指定 回ループで回答精度を上げる処理をする。
  13. カスタムデータのストリーミング 
 19 WorkflowでLLMに複数回問い合わせをすると、 インタラクションがなく体験が悪化 😭 → typeにdata prefixをつけて細かくインタラク ションを返却可能

    🤩 参考: https://ai-sdk.dev/docs/ai-sdk-ui/streaming-data writer.write({ type: 'data-notify-plan-started', id: 'plan-start', data: { message: '計画を作成中 ==.', }, transient: true, }); const plan = await planNode({ question, pastMessages, }); writer.write({ type: 'data-notify-subtasks-started', id: 'subtasks-start', data: { totalTasks: plan.subTasks.length, message: '検索中==.', }, transient: true, }); const subTaskResults = await execSubTaskListNode({ plan, question, pastMessages, }); writer.write({ type: 'data-notify-answer-started', id: 'answer-start', data: { message: '最終回答を作成中 ==.', }, transient: true, }); const streamResult = await finalAnswerNode({ subTaskResults, question, pastMessages, }); writer.merge(streamResult.toUIMessageStream()); ※ 通知目的なら会話履歴に含めない設定 が必要!
  14. LangSmith 
 21 +import * as ai from 'ai'; import

    { convertToModelMessages, - generateObject, type ModelMessage, - streamText, type UIMessage, type UIMessageStreamWriter, } from 'ai'; +import { wrapAISDK } from 'langsmith/experimental/vercel'; +const { streamText, generateObject } = wrapAISDK(ai); import差し替えとAPIキーの環境変数登録でで対応完了 😆
  15. LangSmithの実行のグループ化 
 23 const planNode = async (params: { question:

    string; pastMessages: ModelMessage[]; }): Promise<Plan> => { const nodeName = 'PlanNode'; logger.info({ params }, `Start${nodeName}`); try { =/ 処理実行 const result = await generateObject({ =* ==. =/ }); return result.object; } catch (error) { logger.error({ error }, `FailedIn${nodeName}`); throw error; } }; import { traceable } from 'langsmith/traceable'; const planNode = traceable( async (params: { question: string; pastMessages: ModelMessage[]; }): Promise<Plan> => { const nodeName = 'PlanNode'; logger.info({ params }, `Start${nodeName}`); try { =/ 処理実行(中身は同じ) const result = await generateObject({ =* ==. =/ }); return result.object; } catch (error) { logger.error({ error }, `FailedIn${nodeName}`); throw error; } }, { name: 'Plan Generation', =/ トレース時の表示名 } ); 実装前 実装後 関数をラップする実装が必要 → 公式の実装例を元にAIに書かせれば⽐較 的正確かつ早い https://ai-sdk.dev/providers/observability/langsmith#with-traceable
  16. モデル毎の違い 
 29 AI SDKで共通化されていると はいえ、モデルごとの差異は ⼤きい。 const result =

    await generateText({ model: model("us.anthropic.claude-sonnet-4-20250514-v1:0"), stopWhen: [stepCountIs(2)], tools: { searchManual: createSearchManualTool() }, prepareStep: ({ model, stepNumber, messages }) => { if (stepNumber === 1) { return { model, messages, activeTools: [], }; } else { return { model, }; } }, messages, }); console.log(JSON.stringify(result.response.messages, null, 2)); AI SDK Warning: The "toolContent" setting is not supported by this model - Tool calls and results removed from conversation because Bedrock does not support tool content without active tools. Bedrock はアクティブなツールがない場合、ツール関 連の呼び出しと結果を会話から除外される