概述
本篇為系列文章的第二部分,第一部分請看這裡。本次內容會說明如何透過 Vercel AI SDK 進行文字的總結和從文字中萃取資訊並轉成結構化的資料。
文字總結
啟動 dev server 並到 /summarization 頁面。
bun run devnpm run devpnpm run dev你應該會看到一個評論列表。這個列表中的資料是從 /app/(3-summarization)/summarization 來的,包含來自團隊成員的 20 條消息,討論了項目更新、客戶反饋、時間表以及即將到來的客戶電話會議的準備工作。
如果能夠獲得所有評論的摘要,這樣我們就不必閱讀所有內容了,這不是很棒嗎?讓我們使用 generateObject 來構建這個功能。
在 /app/(3-summarization)/summarization 目錄中創建一個名為 actions.ts 的新檔案。這將是我們的伺服器端環境,在這裡我們將與模型進行交互。這個動作將接受一個包含評論的 Array 作為參數。
"use server";
import { generateObject } from "ai";
export const generateSummary = async (comments: any[]) => {
const result = await generateObject();
return result.object;
};我們希望模型能夠總結這些評論,但由於模型只看得懂文字資料,所以我們需要先將評論從 Json 格式轉換為純文字並與 prompt 一起傳遞。
"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 來驗證模型生成的資訊是否符合我們的需求。
"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 範例代碼:
"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 狀態變量中
"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 元件並渲染摘要。
"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:
"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 頁面。
bun run devnpm run devpnpm run dev你應該會看到一個輸入欄位,可以在其中輸入關於預約的非結構化文本。讓我們構建一個系統,使用 generateObject 從這些輸入中提取結構化資訊。
在 extraction 目錄中創建一個名為 actions.ts 的新檔案。在其中定義一個新的伺服器動作 extractAppointment,這個動作將接受一個參數 input,它是一個字串。匯入並呼叫 generateObject,並返回生成的物件。
"use server";
import { generateObject } from "ai";
export const extractAppointment = async (input: string) => {
const result = await generateObject();
return result.object;
};我們希望模型從輸入文本中提取預約資訊。這邊我們使用 gpt-4o-mini 模型,並透過 prompt 告訴模型我們的需求。
"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;
};現在,讓我們定義一個模式來指定我們想要提取的準確資訊。這個模式將確保我們能夠提取到正確的預約資料,像是日期、時間、地點等。
"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元件來顯示提取的預約資訊。
"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 並呼叫它。將表單中的輸入值傳遞給它,並使用等待的結果更新提取的預約狀態。
"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。
"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 尚未運行,請執行以下命令:
bun run dev -
進入 /extraction 頁面。
-
在輸入欄位中輸入一段未結構化的預約文本,例如:
Meeting with Guillermo Rauch about Next Conf Keynote Practice tomorrow at 2pm at Vercel HQ -
點擊 "Submit" 按鈕並觀察顯示結構化的預約詳細資訊。
現在你應該能看到使用 CalendarAppointment 元件呈現的提取預約資訊。
但請注意,日期和時間格式並不理想,預約名稱也不完美。我們來添加一些 Zod 描述來澄清提取欄位的預期格式。
更新 action.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。請繼續閱讀這篇文章吧!
