ãã®èšäºã¯ every Tech Blog Advent Calendar 2024 9 æ¥ç®ã®èšäºã§ãã ã¯ããã« ããã«ã¡ã¯ãDELISH KITCHENéçºéšã®æäžã§ãã DELISH KITCHENã§ã¯ããããŸã§ã®ãã¬ã·ãåç»ã¢ããªããããAIæçã¢ã·ã¹ã¿ã³ãããç®æãã¹ãããããŸã§ä»¥äžã«AIé åã«åãå
¥ããŠããŸãã詳ããã¯ãã¡ãã«ãèšèŒãããã®ã§ããã²ã芧ãã ããã AI/LLMでtoC向けサービスはどう変わるのか?『DELISH KITCHEN』は、「レシピ動画アプリ」から「AI料理アシスタント」へ ãã®AI掻çšã¯ç€Ÿå
ã§ã®æ¥åæ¹åã«ãé²ãã§ãããçŽè¿ã§OpenAI APIãçšãã瀟å
ã·ã¹ãã ã®éçºãããæ©äŒããããŸããããã®äžã§ä»åã¯Vercelã® AI SDK ãäœ¿ãæ©äŒããã£ãã®ã§AI SDKãçšããã¹ããªãŒãã³ã°å¯èœãªUIãwebã¢ããªã±ãŒã·ã§ã³å
ã§å®çŸããæ¹æ³ã玹ä»ããŸãã AI SDKãšã¯ Vercelã®AI SDKã¯AI/LLMãçšããwebã¢ããªã±ãŒã·ã§ã³éçºãæ¯æŽããããã®ããŒã«ã§ããAI/LLMãçšããéçºã§ã¯OpenAI, Claudeãªã©å€éšAPIãžã®ç¹ããã¿ããã£ããUIã®å®è£
ããã£ããå±¥æŽã®ä¿åãã¹ããªãŒãã³ã°æ©èœãRAGã®å©çšãªã©ã®æ©èœãæ±ãããããããŸããããããå
šãŠèªåã§éçºããããšæããšãããšãwebãã¬ãŒã ã¯ãŒã¯ã䜿ã£ãŠããŠãããªãæéãããã£ãŠããŸããŸããAI SDKãå©çšããããšã§ããããå®è£
å·¥æ°ãåæžããããåšèŸºã®æ©èœéçºã«æéãå²ãããšãã§ããŸãã çŸç¶AI SDKã¯ä»¥äžã®3ã€ããæ§æãããŠããŸãã AI SDK Core ããã¹ãçæãæ§é åãªããžã§ã¯ãã®çæãLLMïŒå€§èŠæš¡èšèªã¢ãã«ïŒã䜿çšããããŒã«åŒã³åºããè¡ãããã®ãããã€ããŒã«äŸåããªãçµ±äžAPIã®æäŸ AI SDK UI ãã£ããããã®ä»ãŠãŒã¶ãŒã€ã³ã¿ãŒãã§ãŒã¹ãæ§ç¯ããããã®ããŒã«ã®æäŸ AI SDK RSC React Server Components (RSC) ã䜿çšããŠãŠãŒã¶ãŒã€ã³ã¿ãŒãã§ãŒã¹ãã¹ããªãŒãã³ã°ããæ©èœãâ»çŸåšã¯å®éšçãªéçºæ®µéã AI SDKã¯åãéçºå
ããåºãŠããNext.jsã¯ãã¡ããNuxtãSvelteãªã©ä»ç°å¢ãžã®å¯Ÿå¿ãããŠããŸãããAI SDK RSCã¯Next.jsã®App Routerã ãããµããŒãããŠãããããã®ã§ãç°å¢å¥ã§äœã䜿ãããã¯å
¬åŒãåç
§ããã®ãããããã§ãã https://sdk.Vercel.ai/docs/getting-started/navigating-the-library#environment-compatibility AI/LLMãçšããã¢ããªã±ãŒã·ã§ã³ã«ãããã¹ããªãŒãã³ã°æ©èœ ç¹ã«LLMãçšããwebã¢ããªã±ãŒã·ã§ã³éçºãããŠããå Žåãéçºäžã±ã¢ããŠãããããã€ã³ãã¯ããã€ããããŸããããã®äžã€ã«ã¹ããªãŒãã³ã°æ©èœããããŸãã ãŸããåçŽãªäžåäžççãªãã®ãå®çŸããããšæã£ãŠã質åå
容ãåçã«ãã£ãŠã¯APIã§LLMãçšããŠåçãçæããæ®µéã§ãã®åºåãŸã§ãŠãŒã¶ãŒã«åŸ
ã¡æéãçºçããŠããŸããŸããããã«åçŽãª1åã®APIåŒã³åºãã ãã§çµããã°ããã§ãããå€ãã®å Žåã§ã¯è€æ°ã®APIåŒã³åºããåŠçã®ã¹ããããçµãŠãåºåçµæãäœã£ãŠããã®ã§ãã®åŸ
ã¡æéã¯ãŠãŒã¶ãŒäœéšãšããŠç¡èŠã§ããªããã®ã«ãªã£ãŠããŸãã ããã§èº«è¿ãªãšããã§ããã°ãChatGPTã§ãå
šãŠã®åçãçæãçµããåã«åçã®åºåãæ®µéçã«è¡ããããŠãŒã¶ãŒãäœæããåŸ
ã¡æéã軜æžããŠãããšæããŸãããåºåãåŠçã®éäžã§ããŠãŒã¶ãŒã«ãã£ãŒãããã¯ã§ãããããªã¹ããªãŒãã³ã°æ©èœãæ±ããããŸãã AI SDKã§ã¯AI SDK RSCã®äžã§ãã®æ©èœããµããŒãããŠããã®ã§ä»¥éã§ã¯ããã€ãã®çš®é¡ã«åããŠæ©èœã玹ä»ããŠãããŸãã ããã¹ãã®åºåçµæãã¹ããªãŒãã³ã°ã§è¡šç€ºãã Server ServeråŽã§ã¯ãŸã createStreamableValue ã§ServerããClientã«ã¹ããªãŒãã³ã°ã§éãããã®ããŒã¿ã®æ ŒçŽå
ãæºåãã streamText ã䜿ã£ãŠOpenAI APIãªã©Providerããã¹ããªãŒãã³ã°ãããåºåçµæã§æŽæ°ããŸãã 'use server' ; import { streamText } from 'ai' ; import { openai } from '@ai-sdk/openai' ; import { createStreamableValue } from 'ai/rsc' ; export async function generate ( input : string ) { const stream = createStreamableValue( '' ); ( async () => { const { textStream } = streamText( { model : openai( 'gpt-4o-mini' ), prompt : input, } ); for await ( const delta of textStream) { stream.update(delta); } stream.done(); } )(); return { output : stream.value } ; } Client ClientåŽã§ã¯ServeråŽã createStreamableValue ã§çæãããããŒã¿ã readStreamableValue ãçšããããšã§ç°¡åã«èªã¿åãããšãã§ããã®ã§åãåã£ããã®ãåŠçããhooksãå®çŸ©ããŸãã import { StreamableValue, readStreamableValue } from 'ai/rsc' import { useEffect, useState } from 'react' export const useStreamableText = ( content : string | StreamableValue < string > ) => { const [ rawContent , setRawContent ] = useState( typeof content === 'string' ? content : '' ) useEffect(() => { ;( async () => { if ( typeof content === 'object' ) { let value = '' for await ( const delta of readStreamableValue(content)) { if ( typeof delta === 'string' ) { setRawContent((value = value + delta)) } } } } )() } , [ content ] ) return rawContent } 衚瀺ããã³ã³ããŒãã³ãåŽã§ã¯Serverããã®çµæãäžèšã§å®çŸ©ãã useStreambleText ã䜿ã£ãŠè¡šç€ºããã ãã§ç°¡åã«å®çŸã§ããŸãã 'use client' ; import { useState } from 'react' ; import { generate } from '@/lib/actions' ; import { useStreamableText } from '@/lib/hooks' ; import { StreamableValue } from 'ai/rsc' ; export default function QuestionAnswer () { const [ answer , setAnswer ] = useState< string | StreamableValue < string >>( '' ); return ( < div > < button onClick = { async () => { const { output } = await generate( 'ç°¡åã«äœãããåŒåœã¬ã·ããæããŠãã ããã' ); setAnswer(output); } } > Ask </ button > { answer && < AssistantMessage answer = { answer } /> } </ div > ); } export function AssistantMessage ( { answer , } : { answer : string | StreamableValue < string > ; } ) { const text = useStreamableText(content); return ( < div > { text } </ div > ); } ãªããžã§ã¯ãã®åºåçµæãã¹ããªãŒãã³ã°ã§è¡šç€ºãã Server ServeråŽã§ã¯ãåãããã« createStreamableValue ãå©çšãããšããã¯åãã§ãããããã§ã¯ãªããžã§ã¯ã圢åŒã®åºåã«å¯Ÿå¿ãã streamObject ãå©çšããŠããããã€ããŒãã鿬¡éä¿¡ãããæ§é åãããããŒã¿ã§æŽæ°ããŸããAI SDKã§ã¯OpenAI APIã® Structured Outputs ã«ã察å¿ããŠããã®ã§ã structuredOutputs ãã©ã¡ãŒã¿ãŒã§æå®ããŸãã 'use server' ; import { streamObject } from 'ai' ; import { openai } from '@ai-sdk/openai' ; import { createStreamableValue } from 'ai/rsc' ; import { z } from 'zod' ; export async function generateObject ( input : string ) { const stream = createStreamableValue( { answer : '' , quotation_links : [] } ); ( async () => { const { objectStream } = streamObject( { model : openai( 'gpt-4o-mini' , { structuredOutputs : true , } ), schema : z.object( { answer : z.string(), quotation_links : z.array( z.object( { title : z.string(), link : z.string(), } ) ), } ), prompt : input, } ); for await ( const delta of objectStream) { stream.update(delta); } stream.done(); } )(); return { output : stream.value } ; } äžèšã®äŸã§ã¯ã objectStream ã«ã¯åžžã« { answer: '', quotation_links: [] } ã®åœ¢åŒãä¿ããã圢ã§éæãã®ããã¹ãæ
å ±ãé
åèŠçŽ ã远å ãããŠããã®ã§ãç¹ã«è€éãªå å·¥åŠçãããããšãªããClientåŽã§å©çšå¯èœãªç¶æ
ã«ãªããŸãã Client ClientåŽã§ã¯ãããã¹ããã¹ããªãŒãã³ã°ããæãšåãããã« readStreamableValue ã䜿çšããŠã¹ããªãŒãã³ã°ããããªããžã§ã¯ããåãåããåçã«æŽæ°ããŸãã import { StreamableValue, readStreamableValue } from 'ai/rsc' ; import { useEffect, useState } from 'react' ; type AnswerObject = { answer : string ; quotation_links : { title : string ; link : string } [] ; } ; export const useStreamableObject = ( content : AnswerObject | StreamableValue < AnswerObject > ) => { const [ rawContent , setRawContent ] = useState< AnswerObject | null >( typeof content === 'object' && !( 'subscribe' in content) ? content : { answer : '' , quotation_links : [] } ); useEffect(() => { ( async () => { if ( typeof content === 'object' && 'subscribe' in content) { let value: AnswerObject | null = null ; for await ( const delta of readStreamableValue(content)) { if ( typeof delta === 'object' ) { setRawContent((value = { ...value, ...delta } )); } } } } )(); } , [ content ] ); return rawContent; } ; å®çŸ©ãã useStreamableObject ã䜿çšããŠãã¹ããªãŒãã³ã°ã§åãåã£ããªããžã§ã¯ãããŒã¿ã衚瀺ããŸãã 'use client' ; import { useState } from 'react' ; import { generateObject } from '@/lib/actions' ; import { useStreamableObject } from '@/lib/hooks' ; import { StreamableValue } from 'ai/rsc' ; type AnswerObject = { answer : string ; quotation_links : { title : string ; link : string } [] ; } ; export default function ObjectDisplay () { const [ answer , setAnswer ] = useState< AnswerObject | StreamableValue < AnswerObject > | null >( null ); return ( < div > < button onClick = { async () => { const { output } = await generateObject( 'ç°å¢åé¡ã«é¢ããææ°ã®ã¬ããŒããæããŠãã ããã' ); setAnswer(output); } } > Ask </ button > { answer && < AssistantObjectMessage answer = { answer } /> } </ div > ); } export function AssistantObjectMessage ( { answer , } : { answer : AnswerObject | StreamableValue < AnswerObject > ; } ) { const data = useStreamableObject(answer); return ( < div > { data ? ( < div > < p > { data.answer } </ p > < ul > { data.quotation_links. map (( item , index ) => ( < li key = { index } > < a href = { item. link } target = "_blank" rel = "noopener noreferrer" > { item. title } </ a > </ li > )) } </ ul > </ div > ) : ( 'Loading...' ) } </ div > ); } ãªããžã§ã¯ã圢åŒã®ã¹ããªãŒãã³ã°ã®ã¡ãªããã¯äžèšã®ããã«ããããç°ãªãèŠçŽ ã«å¯ŸããŠåå¥ã®ã¹ã¿ã€ãªã³ã°ãåŠçãè¡ãããšãã§ããç¹ã§ããä»ãŸã§ã®ããã¹ãã§ã®ã¹ããªãŒãã³ã°ã¯è¡šçŸæ¹æ³ãéå®çã«ãªãããããããšæã£ãŠãããã¹ãæ
å ±ã倿ãããããªè€éãªåŠçãããªããšãããäžå®å®ã«ãªã£ãŠããŸããŸããããªããžã§ã¯ã圢åŒã§åãåããããšã«ãã£ãŠãã¢ããªã±ãŒã·ã§ã³ã«ãã£ãŠç¬èªã®èŠãæ¹ãå¯èœã«ãªããèªç±åºŠãäžãããŸããã åŠçã«åãããŠUIèªäœãã¹ããªãŒãã³ã°ã§è¡šç€ºãã Vercelã§ã¯ããã¹ãããªããžã§ã¯ãã ãã§ã¯ãªããUIèªäœãã¹ããªãŒãã³ã°ããããšãã§ããŸãããã®æ©èœã䜿ãããšã§ããã¹ãããªããžã§ã¯ãã ãã§ã¯ãªããLLMã®åççµæãUIã§è¡šç€ºããããšãå¯èœã«ãªããŸãã ä»åã¯ãAIã¢ããªã±ãŒã·ã§ã³ã§ãããã¡ãªåçãåºåãããŸã§é²æã衚瀺ããUIãäŸã«ç޹ä»ããŸãã äžçªæåã«å©çšãã AssistantMessage ãšä»¥äžã® WorkflowProgress ã³ã³ããŒãã³ããServerãšClientã§ãããšãããŸãã 鲿ã衚瀺ããWorkflowProgressã³ã³ããŒãã³ã export function WorkflowProgress ({ workflowSteps }) { const completedSteps = workflowSteps . filter (( step ) => step . status === 'completed' ) . length ; const totalSteps = workflowSteps . length ; const progressValue = ( completedSteps / totalSteps ) * 100 ; return ( < div className = "p-4 bg-gray-100 rounded-lg shadow" > < h3 className = "font-semibold text-lg" > Progress </ h3 > < progress value = { progressValue } max = "100" className = "w-full mb-2" ></ progress > < ul > { workflowSteps . map (( step ) => ( < li key = { step . id } className = "mb-2" > < strong > { step . name } : </ strong > { step . status } </ li > ))} </ ul > </ div > ) ; } Server ãµãŒããŒåŽã§ã¯ createStreamableUI ãå©çšããŠãã¹ããªãŒãã³ã°ããã³ã³ããŒãã³ãã远å ãæŽæ°ããããšãã§ããŸããä»åã¯AI SDKã®æ©èœç޹ä»ãã¡ã€ã³ã®ãããstepæ¯ã®å
·äœçãªåŠçã«ã€ããŠã¯çç¥ããŸãã 'use server' ; import { streamText } from 'ai' ; import { openai } from '@ai-sdk/openai' ; import { createStreamableUI } from 'ai/rsc' ; import { WorkflowProgress } from '@/components/WorkflowProgress' ; import { AssistantMessage } from '@/components/AssistantMessage' ; export async function generateWithSteps ( input : string ) { const workflowSteps = [ { id : 'step1' , name : '質åãè§£æäž' , status : 'in-progress' , tasks : [] } , { id : 'step2' , name : 'æ¢çŽ¢æ¹æ³ãæ€èš' , status : 'pending' , tasks : [] } , { id : 'step3' , name : 'é¢é£ããŒã¿ãååŸ' , status : 'pending' , tasks : [] } , ] ; const displayUI = createStreamableUI( < WorkflowProgress workflowSteps = {workflowSteps} /> ); ( async () => { // Step 1: 質åãè§£æ await new Promise (( resolve ) => setTimeout (resolve, 2000 )); workflowSteps[ 0 ]. status = 'completed' ; workflowSteps[ 1 ]. status = 'in-progress' ; displayUI.update( < WorkflowProgress workflowSteps = {workflowSteps} /> ); // Step 2: æ¢çŽ¢æ¹æ³ãæ±ºå® await new Promise (( resolve ) => setTimeout (resolve, 2000 )); workflowSteps[ 1 ]. status = 'completed' ; workflowSteps[ 2 ]. status = 'in-progress' ; displayUI.update( < WorkflowProgress workflowSteps = {workflowSteps} /> ); // Step 3: ããŒã¿ãååŸ await new Promise (( resolve ) => setTimeout (resolve, 2000 )); workflowSteps[ 2 ]. status = 'completed' ; displayUI.update( < WorkflowProgress workflowSteps = {workflowSteps} /> ); // çµæãçæ const { textStream } = streamText( { model : openai( 'gpt-4o-mini' ), prompt : input, } ); let generatedText = '' ; for await ( const delta of textStream) { generatedText += delta; // é²æç¶æ³ã®ã³ã³ããŒãã³ãããåççµæã®ã³ã³ããŒãã³ãã«å€ãã displayUI.update( < AssistantMessage answer = {generatedText} /> ); } displayUI.done(); } )(); return { display : displayUI.value } ; } Client ã¯ã©ã€ã¢ã³ãåŽã§ã¯ãå®çŸ©ããã¢ã¯ã·ã§ã³ãåŒã³åºãããã®çµæããã®ãŸãŸè¡šç€ºããããšã§ç°¡åã«åçãªUIãäœããŸããå®éã®æåã¯ãŸãæåã«Progressã衚瀺ããåççµæãLLMããåºåããå§ãããšProgressã¯é衚瀺ã«ãªããåçãçæãããŠãããããªåœ¢ã«ãªããŸãã 'use client' ; import { useState } from 'react' ; import { generateWithSteps } from '@/lib/actions' ; export default function DynamicProgressDisplay () { const [ display , setDisplay ] = useState< React.ReactNode | null >( null ); return ( < div > < button onClick = { async () => { const { display } = await generateWithSteps( 'ç°å¢åé¡ã«é¢ããææ°ã®ã¬ããŒããæããŠãã ããã' ); setDisplay(display); } } className = "mb-4 px-4 py-2 bg-blue-500 text-white rounded" > Ask </ button > { display } </ div > ); } æåŸã« ä»åã¯Vercelã®AI SDKã䜿ã£ãŠãããã€ãã®ææ³ã§ã¹ããªãŒãã³ã°å¯èœãªUIãã¢ããªã±ãŒã·ã§ã³äžã§å®è£
ããæ¹æ³ã玹ä»ããŸããããã®ä»ã«ãäŒè©±å±¥æŽã®ä¿åããã®ç¶æ
ã®ç®¡çãç°¡åã«æ±ããããã«è±å¯ãªæ©èœãæäŸãããŠããŸããAI SDK RSCã¯ãŸã ããŒã¿çãªäœçœ®ä»ãã§ãããwebã§LLMã䜿ã£ãã¢ããªã±ãŒã·ã§ã³ãããéã«ã¯æ¯èŒçç°¡åã«ãªãããªã¢ããªã±ãŒã·ã§ã³ãäœããã®ã§ããã²ç€Ÿå
ããŒã«ãç°¡åã«åããã®ãäœãããå Žåã«ã¯Vercelã®AI SDKã䜿ã£ãŠã¿ãŠãã ããã åé ã§ã玹ä»ããããã«DELISH KITCHENã§ã¯ãããŸã§ã®ãã¬ã·ãåç»ã¢ããªããããAIæçã¢ã·ã¹ã¿ã³ãããžã®å€åãèµ·ããããšããŠããŸãããã²ããã®åãçµã¿ã«èå³ãæã£ãæ¹ã¯äžåºŠã«ãžã¥ã¢ã«é¢è«ã§ã話ããŸãããïŒ corp.every.tv