Vercel AI SDK (Part 2)

文本總結與提轉換文字資料成結構化資訊

eric

最後更新時間

2025年2月3日

文章列表

概述

本篇為系列文章的第二部分,第一部分請看這裡。本次內容會說明如何透過 Vercel AI SDK 進行文字的總結和從文字中萃取資訊並轉成結構化的資料。

文字總結

啟動 dev server 並到 /summarization 頁面。

bun run dev

你應該會看到一個評論列表。這個列表中的資料是從 /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 頁面。

bun run dev

你應該會看到一個輸入欄位,可以在其中輸入關於預約的非結構化文本。讓我們構建一個系統,使用 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>
  );
}

測試提取功能:

  1. 如果 dev server 尚未運行,請執行以下命令:

    bun run dev
    
  2. 進入 /extraction 頁面。

  3. 在輸入欄位中輸入一段未結構化的預約文本,例如:

    Meeting with Guillermo Rauch about Next Conf Keynote Practice tomorrow at 2pm at Vercel HQ
  4. 點擊 "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。請繼續閱讀這篇文章吧!