Slide 1

Slide 1 text

クラスメソッド 製造ビジネステクノロジー部 髙橋俊一 
 AI SDKで作るチャットボット開発 
 1

Slide 2

Slide 2 text

自己紹介
 https://shuntaka.dev/who 髙橋俊⼀ (shuntaka) ● 2016年 ⾦融情報ベンダー⼊社 バックエンド ○ 株価配信Web API開発 ● 2019年 クラスメソッド⼊社 ○ CX/IoT事業部にてIoT案件を複数 ● 2024年 製造ビジネステクノロジー部担当 ○ R&D業務/サーバーサイド/AIチャットボット開発 🔰 最近は⽣成AI/AIDD関連で情報発信をしています! 2

Slide 3

Slide 3 text

前提
 3 ‧AI SDKを使ってチャットボットを構築される⽅向け 🔰 ‧AI SDK v5(最新)前提 ‧サーバサイドの話がメイン

Slide 4

Slide 4 text

目次
 4 ‧AI SDKとは ‧AI SDKで作るチャットボット ‧AI SDKを使う上での学び ‧まとめ

Slide 5

Slide 5 text

目次
 5 ‧AI SDKとは 👈 ‧AI SDKで作るチャットボット ‧AI SDKを使う上での学び ‧まとめ

Slide 6

Slide 6 text

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段階は含みません

Slide 7

Slide 7 text

目次
 7 ‧AI SDKとは ‧AI SDKで作るチャットボット 👈 ‧AI SDKを使う上での学び ‧まとめ

Slide 8

Slide 8 text

チャットボットサーバーサイドのベースのコード(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()); } }), });

Slide 9

Slide 9 text

クライアント(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()); } }), });

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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()); } }), });

Slide 15

Slide 15 text

実行の様子(gif) 
 15 Server-Sent Eventで ストリームで送られて いる

Slide 16

Slide 16 text

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構造 を作ってくれる

Slide 17

Slide 17 text

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っぽいこと も可能

Slide 18

Slide 18 text

ワークフローを組む 
 18 タスクを明確なステップに分解ためにLLMに 複数回問い合わせるケースがある 参考: https://ai-sdk.dev/docs/agents/workflows 「AIエージェント実践⼊⾨」で Plan-and-Execute型エージェントの紹介があ り、実践的で分かりやすい https://www.kspub.co.jp/book/detail/5401408.html ざっくり流れは ①計画⽴案→②サブタスク回答→③最終回答作 成という流れで①-③全てでLLM呼び出しがあ る。②ではツール検索、リフレクションで指定 回ループで回答精度を上げる処理をする。

Slide 19

Slide 19 text

カスタムデータのストリーミング 
 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()); ※ 通知目的なら会話履歴に含めない設定 が必要!

Slide 20

Slide 20 text

Observability 
 20 AI SDKだけでもツール連携可 能。LangSmithの例を紹介。 https://ai-sdk.dev/providers/observability 使われている例が多いと思わ れるLangfuseでも記法は違え ど同様のことが可能。

Slide 21

Slide 21 text

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キーの環境変数登録でで対応完了 😆

Slide 22

Slide 22 text

LangSmith 
 22 この実装だけだとフラット構造に...🥺

Slide 23

Slide 23 text

LangSmithの実行のグループ化 
 23 const planNode = async (params: { question: string; pastMessages: ModelMessage[]; }): Promise => { 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 => { 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

Slide 24

Slide 24 text

LangSmithのグループ化可視化結果 
 24 先ほどのフラット構造より圧倒的に⾒や すい😆 実⾏結果が1つのグループ に集約 ⭐

Slide 25

Slide 25 text

チャットボットのインフラ構成の観点(AWS) 
 25 ‧応答時間の制約  ‧15分あれば⼿堅い  ‧120秒だと厳しそう...(AppRunner 🙅) ‧WebSocketが必要(AppRunner、Lambda 🙅)  ‧AI SDKはSSEだが、UI FW含め総合的に考える必要がある ‧RDBやOpenSearchなどとの接続性

Slide 26

Slide 26 text

CloudFrontのオリジンに設定している場合 
 26 CloudFrontのレスポンスタイムアウトはデフォルト30s、120s以上 は上限緩和申請が必要。SSE応答間隔が⻑くなりすぎないように注 意が必要!

Slide 27

Slide 27 text

目次
 27 ‧AI SDKとは ‧AI SDKで作るチャットボット ‧AI SDKを使う上での学び 👈 ‧まとめ

Slide 28

Slide 28 text

ストリーミングAPI仕様の共有 
 28 FE/BEでチームが分かれていてAI SDKを利⽤する場合、REST API仕 様ような硬い仕様を作るのは難しい ‧BEを早めに作ってFEに繋げてもらってmessagesの構造を決める ‧今後はそのmessages構造でassertして、意図しないレスポンスに変 わらないように注意する onFinish: ({ messages }) => { logger.info({ messages }, 'EndTestChatResponse'); },

Slide 29

Slide 29 text

モデル毎の違い 
 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 はアクティブなツールがない場合、ツール関 連の呼び出しと結果を会話から除外される

Slide 30

Slide 30 text

目次
 30 ‧AI SDKとは ‧AI SDKで作るチャットボット ‧AI SDKを使う上での学び ‧まとめ 👈

Slide 31

Slide 31 text

まとめ
 31 ‧AI SDKだけでも⾊々出来る(workflow, o11yなど)。AI SDKラッパーを使う際に 切り分けで使える場合がありそう ‧インフラ構成は応答時間やストリーミング間隔、他のミドルウェアとの相性 も考慮して設計しよう ‧AI SDKにしか出来ないこと、まだ未対応の機能などトレードオフを考慮してAI エージェントの技術選定が必要 以上、ありがとうございました!