本篇為系列文章的第二部分,第一部分請看這裡 。本次內容會說明如何透過 Vercel AI SDK 進行文字的總結和從文字中萃取資訊並轉成結構化的資料。
啟動 dev server 並到 /summarization
頁面。
你應該會看到一個評論列表。這個列表中的資料是從 /app/(3-summarization)/summarization
來的,包含來自團隊成員的 20 條消息,討論了項目更新、客戶反饋、時間表以及即將到來的客戶電話會議的準備工作。
如果能夠獲得所有評論的摘要,這樣我們就不必閱讀所有內容了,這不是很棒嗎?讓我們使用 generateObject
來構建這個功能。
在 /app/(3-summarization)/summarization
目錄中創建一個名為 actions.ts
的新檔案。這將是我們的伺服器端環境,在這裡我們將與模型進行交互。這個動作將接受一個包含評論的 Array 作為參數。
app/(3-summarization)/summarization/actions.ts "use server" ;
import { generateObject } from "ai" ;
export const generateSummary = async ( comments : any []) => {
const result = await generateObject ();
return result.object;
};
我們希望模型能夠總結這些評論,但由於模型只看得懂文字資料,所以我們需要先將評論從 Json 格式轉換為純文字並與 prompt 一起傳遞。
app/(3-summarization)/summarization/actions.ts "use server" ;
import { generateObject } from "ai" ;
import { openai } from "@ai-sdk/openai" ;
import { z } from "zod" ;
export const generateSummary = async ( comments : any []) => {
const result = await generateObject ({
model: openai ( "gpt-4o" ),
prompt: `Please summarise the following comments.
---
Comments:
${ JSON . stringify ( comments ) }` ,
});
return result.object;
};
我們可以使用 Zod 定義一個 schema 來驗證模型生成的資訊是否符合我們的需求。
app/(3-summarization)/summarization/actions.ts "use server" ;
import { generateObject } from "ai" ;
import { openai } from "@ai-sdk/openai" ;
import { z } from "zod" ;
export const generateSummary = async ( comments : any []) => {
const result = await generateObject ({
model: openai ( "gpt-4o" ),
prompt: `Please summarise the following comments.
---
Comments:
${ JSON . stringify ( comments ) }` ,
schema: z. object ({
headline: z. string (),
context: z. string (),
discussionPoints: z. string (),
takeaways: z. string (),
}),
});
return result.object;
};
你可以將剛剛創建的 Server Action
import 到 page.tsx
,並創建一個新的 state 來儲存評論摘要。以下是更新後的 page.tsx 範例代碼:
app/(3-summarization)/summarization/actions.ts "use client" ;
import { MessageList } from "./message-list" ;
import { Button } from "@/components/ui/button" ;
import messages from "./messages.json" ;
import { generateSummary } from "./actions" ;
import { useState } from "react" ;
export default function Home () {
const [ summary , setSummary ] = useState < Awaited <
ReturnType <typeof generateSummary >
> | null > ( null );
const [ loading , setLoading ] = useState ( false );
return (
< main className = "mx-auto max-w-2xl pt-8" >
< div className = "flex space-x-4 items-center mb-2" >
< h3 className = "font-bold" > Comments </ h3 >
< Button
variant = { "secondary" }
disabled = {loading}
onClick = { async () => {
setLoading ( true );
// generate summary
setLoading ( false );
}}
>
Summar{loading ? "izing..." : "ize" }
</ Button >
</ div >
< MessageList messages = {messages} />
</ main >
);
}
呼叫 generateSummary
函數並將結果設置到 summary
狀態變量中
app/(3-summarization)/summarization/actions.ts "use client" ;
import { MessageList } from "./message-list" ;
import { Button } from "@/components/ui/button" ;
import messages from "./messages.json" ;
import { generateSummary } from "./actions" ;
import { useState } from "react" ;
export default function Home () {
const [ summary , setSummary ] = useState < Awaited <
ReturnType <typeof generateSummary >
> | null > ( null );
const [ loading , setLoading ] = useState ( false );
return (
< main className = "mx-auto max-w-2xl pt-8" >
< div className = "flex space-x-4 items-center mb-2" >
< h3 className = "font-bold" > Comments </ h3 >
< Button
variant = { "secondary" }
disabled = {loading}
onClick = { async () => {
setLoading ( true );
// generate summary
setSummary ( await generateSummary ( messages ));
setLoading ( false );
}}
>
Summar{loading ? "izing..." : "ize" }
</ Button >
</ div >
< MessageList messages = {messages} />
</ main >
);
}
匯入 SummaryCard
元件並渲染摘要。
app/(3-summarization)/summarization/actions.ts "use client" ;
import { MessageList } from "./message-list" ;
import { Button } from "@/components/ui/button" ;
import messages from "./messages.json" ;
import { generateSummary } from "./actions" ;
import { useState } from "react" ;
import { SummaryCard } from "./summary-card" ;
export default function Home () {
const [ summary , setSummary ] = useState < Awaited <
ReturnType <typeof generateSummary >
> | null > ( null );
const [ loading , setLoading ] = useState ( false );
return (
< main className = "mx-auto max-w-2xl pt-8" >
< div className = "flex space-x-4 items-center mb-2" >
< h3 className = "font-bold" > Comments </ h3 >
< Button
variant = { "secondary" }
disabled = {loading}
onClick = { async () => {
setLoading ( true );
// generate summary
setSummary ( await generateSummary ( messages ));
setLoading ( false );
}}
>
Summar{loading ? "izing..." : "ize" }
</ Button >
</ div >
{ summary && < SummaryCard { ... summary } />}
< MessageList messages = {messages} />
</ main >
);
}
回到瀏覽器並點擊生成!你應該會看到評論的摘要。但文本的格式不太理想。讓我們使用 describe
來改進生成的內容。
Update the actions.ts
file with the following code:
app/(3-summarization)/summarization/actions.ts "use server" ;
import { generateObject } from "ai" ;
import { openai } from "@ai-sdk/openai" ;
import { z } from "zod" ;
export const generateSummary = async ( comments : any []) => {
const result = await generateObject ({
model: openai ( "gpt-4o" ),
prompt: `Please summarise the following comments.
---
Comments:
${ JSON . stringify ( comments ) }
` ,
schema: z. object ({
headline: z
. string ()
. describe ( "The headline of the summary. Max 5 words." ),
context: z
. string ()
. describe (
"What is the relevant context that prompted discussion. Max 2 sentences." ,
),
discussionPoints: z
. string ()
. describe ( "What are the key discussion points? Max 2 sentences." ),
takeaways: z
. string ()
. describe (
"What are the key takeaways / next steps? Include names. Max 2 sentences." ,
),
}),
});
return result.object;
};
在這個更新的代碼中,我們使用了 describe
來為模型提供更多的上下文,讓模型了解期望的輸出。這將幫助模型生成更準確的摘要。我們還將輸出限制在一定的字元數範圍內,以確保輸出簡潔且相關。
回到瀏覽器並點擊生成!你應該會看到一個更有結構的評論摘要。
啟動 dev server 並到 /extraction
頁面。
你應該會看到一個輸入欄位,可以在其中輸入關於預約的非結構化文本。讓我們構建一個系統,使用 generateObject
從這些輸入中提取結構化資訊。
在 extraction
目錄中創建一個名為 actions.ts
的新檔案。在其中定義一個新的伺服器動作 extractAppointment
,這個動作將接受一個參數 input
,它是一個字串。匯入並呼叫 generateObject
,並返回生成的物件。
app/(4-extraction)/extraction/actions.ts "use server" ;
import { generateObject } from "ai" ;
export const extractAppointment = async ( input : string ) => {
const result = await generateObject ();
return result.object;
};
我們希望模型從輸入文本中提取預約資訊。這邊我們使用 gpt-4o-mini
模型,並透過 prompt 告訴模型我們的需求。
app/(4-extraction)/extraction/actions.ts "use server" ;
import { generateObject } from "ai" ;
import { openai } from "@ai-sdk/openai" ;
import { z } from "zod" ;
export const extractAppointment = async ( input : string ) => {
const result = await generateObject ({
model: openai ( "gpt-4o-mini" ),
prompt: "Extract appointment info for the following input: " + input,
});
return result.object;
};
現在,讓我們定義一個模式來指定我們想要提取的準確資訊。這個模式將確保我們能夠提取到正確的預約資料,像是日期、時間、地點等。
app/(4-extraction)/extraction/actions.ts "use server" ;
import { generateObject } from "ai" ;
import { openai } from "@ai-sdk/openai" ;
import { z } from "zod" ;
export const extractAppointment = async ( input : string ) => {
const result = await generateObject ({
model: openai ( "gpt-4o-mini" ),
prompt: "Extract appointment info for the following input: " + input,
schema: z. object ({
title: z. string (),
startTime: z. string (). nullable (),
endTime: z. string (). nullable (),
attendees: z. array (z. string ()). nullable (),
location: z. string (). nullable (),
date: z. string (),
}),
});
return result.object;
};
匯入 AppointmentDetails
型別並創建一個新的狀態變量來儲存提取的預約詳細資料。
匯入 CalendarAppointment
元件來顯示提取的預約資訊。
app/(4-extraction)/extraction/page.tsx "use client" ;
import { useState } from "react" ;
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" ;
import { Input } from "@/components/ui/input" ;
import { Button } from "@/components/ui/button" ;
import {
type AppointmentDetails,
CalendarAppointment,
} from "./calendar-appointment" ;
export default function Page () {
const [ loading , setLoading ] = useState ( false );
const [ appointment , setAppointment ] = useState < AppointmentDetails | null >(
null ,
);
const handleSubmit = async ( e : React . FormEvent < HTMLFormElement >) => {
e. preventDefault ();
setLoading ( true );
// extract appointment
setLoading ( false );
};
return (
< div className = "max-w-lg mx-auto px-4 py-8" >
< div className = "flex flex-col gap-6" >
< Card >
< CardHeader >
< CardTitle >Extract Appointment</ CardTitle >
</ CardHeader >
< CardContent >
< form onSubmit = {handleSubmit} className = "space-y-4" >
< Input
name = "appointment"
placeholder = "Enter appointment details..."
className = "w-full"
/>
< Button type = "submit" className = "w-full" disabled = {loading}>
{loading ? "Extracting..." : "Extract Appointment" }
</ Button >
</ form >
</ CardContent >
</ Card >
< CalendarAppointment appointment = { null } />
</ div >
</ div >
);
}
匯入新創建的 extractAppointment
Action 並呼叫它。將表單中的輸入值傳遞給它,並使用等待的結果更新提取的預約狀態。
app/(4-extraction)/extraction/page.tsx "use client" ;
import { useState } from "react" ;
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" ;
import { Input } from "@/components/ui/input" ;
import { Button } from "@/components/ui/button" ;
import {
AppointmentDetails,
CalendarAppointment,
} from "./calendar-appointment" ;
import { extractAppointment } from "./actions" ;
export default function Page () {
const [ loading , setLoading ] = useState ( false );
const [ appointment , setAppointment ] = useState < AppointmentDetails | null >(
null ,
);
const handleSubmit = async ( e : React . FormEvent < HTMLFormElement >) => {
e. preventDefault ();
setLoading ( true );
const formData = new FormData (e.target as HTMLFormElement );
const input = formData. get ( "appointment" ) as string ;
setAppointment ( await extractAppointment (input));
setLoading ( false );
};
return (
< div className = "max-w-lg mx-auto px-4 py-8" >
< div className = "flex flex-col gap-6" >
< Card >
< CardHeader >
< CardTitle >Extract Appointment</ CardTitle >
</ CardHeader >
< CardContent >
< form onSubmit = {handleSubmit} className = "space-y-4" >
< Input
name = "appointment"
placeholder = "Enter appointment details..."
className = "w-full"
/>
< Button type = "submit" className = "w-full" disabled = {loading}>
{loading ? "Extracting..." : "Extract Appointment" }
</ Button >
</ form >
</ CardContent >
</ Card >
< CalendarAppointment appointment = { null } />
</ div >
</ div >
);
}
將 appointment 傳遞給 CalendarAppointment 元件作為 props。
app/(4-extraction)/extraction/page.tsx "use client" ;
import { useState } from "react" ;
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" ;
import { Input } from "@/components/ui/input" ;
import { Button } from "@/components/ui/button" ;
import {
AppointmentDetails,
CalendarAppointment,
} from "./calendar-appointment" ;
import { extractAppointment } from "./actions" ;
export default function Page () {
const [ loading , setLoading ] = useState ( false );
const [ appointment , setAppointment ] = useState < AppointmentDetails | null >(
null ,
);
const handleSubmit = async ( e : React . FormEvent < HTMLFormElement >) => {
e. preventDefault ();
setLoading ( true );
const formData = new FormData (e.target as HTMLFormElement );
const input = formData. get ( "appointment" ) as string ;
setAppointment ( await extractAppointment (input));
setLoading ( false );
};
return (
< div className = "max-w-lg mx-auto px-4 py-8" >
< div className = "flex flex-col gap-6" >
< Card >
< CardHeader >
< CardTitle >Extract Appointment</ CardTitle >
</ CardHeader >
< CardContent >
< form onSubmit = {handleSubmit} className = "space-y-4" >
< Input
name = "appointment"
placeholder = "Enter appointment details..."
className = "w-full"
/>
< Button type = "submit" className = "w-full" disabled = {loading}>
{loading ? "Extracting..." : "Extract Appointment" }
</ Button >
</ form >
</ CardContent >
</ Card >
< CalendarAppointment appointment = {appointment} />
</ div >
</ div >
);
}
測試提取功能:
如果 dev server 尚未運行,請執行以下命令:
進入 /extraction 頁面。
在輸入欄位中輸入一段未結構化的預約文本,例如:
Meeting with Guillermo Rauch about Next Conf Keynote Practice tomorrow at 2pm at Vercel HQ
點擊 "Submit" 按鈕並觀察顯示結構化的預約詳細資訊。
現在你應該能看到使用 CalendarAppointment
元件呈現的提取預約資訊。
但請注意,日期和時間格式並不理想,預約名稱也不完美。我們來添加一些 Zod 描述來澄清提取欄位的預期格式。
更新 action.ts
檔案,為提取的欄位添加描述。以下是修改後的範例代碼:
app/(4-extraction)/extraction/actions.ts "use server" ;
import { generateObject } from "ai" ;
import { openai } from "@ai-sdk/openai" ;
import { z } from "zod" ;
export const extractAppointment = async ( input : string ) => {
const result = await generateObject ({
model: openai ( "gpt-4o-mini" ),
prompt: "Extract appointment info for the following input: " + input,
schema: z. object ({
title: z
. string ()
. describe (
"The title of the event. This should be the main purpose of the event. No need to mention names. Clean up formatting (capitalise)." ,
),
startTime: z. string (). nullable (). describe ( "format HH:MM" ),
endTime: z
. string ()
. nullable ()
. describe ( "format HH:MM - note: default meeting duration is 1 hour" ),
attendees: z
. array (z. string ())
. nullable ()
. describe ( "comma separated list of attendees" ),
location: z. string (). nullable (),
date: z
. string ()
. describe ( "Today's date is: " + new Date (). toISOString (). split ( "T" )[ 0 ]),
}),
});
return result.object;
};
再次嘗試以下輸入,並查看輸出結果的改善:
Meeting with Guillermo Rauch about Next Conf Keynote Practice tomorrow at 2pm at Vercel HQ
這篇文章教大家透過 AI SDK 做了文本總結和從非結構化的文本資料中萃取結構化的資料。接下來讓我們將透過 AI SDK 實作一個 Chatbot。請繼續閱讀這篇文章 吧!