AgentSkillsCN

ai-sdk-patterns

当用户提出“添加 AI 聊天”“创建聊天机器人”“使用 Vercel AI SDK”“实现流式传输”“添加 LLM 集成”“构建 AI 功能”“创建生成式 UI”“实现 RAG”,或在 Next.js 项目中进行 AI/LLM 集成时,可使用此技能。

SKILL.md
--- frontmatter
name: ai-sdk-patterns
description: "This skill should be used when the user asks to \"add AI chat\", \"create a chatbot\", \"use Vercel AI SDK\", \"implement streaming\", \"add LLM integration\", \"build AI features\", \"create generative UI\", \"implement RAG\", or is working with AI/LLM integration in a Next.js project."
version: 1.0.0

AI SDK Integration Patterns

Overview

Standards for integrating AI/LLM capabilities into Next.js applications using Vercel AI SDK, OpenAI API, and related tools. Covers streaming chat, generative UI, RAG, and structured output patterns.

Vercel AI SDK Setup

Installation

bash
pnpm add ai @ai-sdk/openai @ai-sdk/anthropic

Provider Configuration

File: src/lib/ai.ts

typescript
import { createOpenAI } from "@ai-sdk/openai"
import { createAnthropic } from "@ai-sdk/anthropic"

export const openai = createOpenAI({
  apiKey: process.env.OPENAI_API_KEY,
})

export const anthropic = createAnthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
})

Chat Patterns

Basic Streaming Chat (API Route)

File: src/app/api/chat/route.ts

typescript
import { streamText } from "ai"
import { openai } from "@/lib/ai"

export async function POST(request: Request) {
  const { messages } = await request.json()

  const result = streamText({
    model: openai("gpt-4o"),
    system: "You are a helpful assistant.",
    messages,
  })

  return result.toDataStreamResponse()
}

Chat UI Component

typescript
"use client"

import { useChat } from "@ai-sdk/react"

export const Chat = () => {
  const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
    api: "/api/chat",
  })

  return (
    <div className="flex flex-col h-full">
      <div className="flex-1 overflow-y-auto space-y-4 p-4">
        {messages.map((message) => (
          <div
            key={message.id}
            className={cn(
              "rounded-lg px-4 py-2 max-w-[80%]",
              message.role === "user"
                ? "ml-auto bg-primary text-primary-foreground"
                : "bg-muted"
            )}
          >
            {message.content}
          </div>
        ))}
      </div>
      <form onSubmit={handleSubmit} className="border-t p-4 flex gap-2">
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="Type a message..."
          className="flex-1 rounded-md border px-3 py-2"
          disabled={isLoading}
        />
        <button
          type="submit"
          disabled={isLoading}
          className="rounded-md bg-primary px-4 py-2 text-primary-foreground"
        >
          Send
        </button>
      </form>
    </div>
  )
}

Structured Output (generateObject)

typescript
import { generateObject } from "ai"
import { openai } from "@/lib/ai"
import { z } from "zod"

const recipeSchema = z.object({
  name: z.string(),
  ingredients: z.array(z.object({
    name: z.string(),
    amount: z.string(),
  })),
  steps: z.array(z.string()),
  cookingTime: z.number().describe("in minutes"),
})

export async function generateRecipe(prompt: string) {
  const { object } = await generateObject({
    model: openai("gpt-4o"),
    schema: recipeSchema,
    prompt,
  })

  return object // Fully typed as z.infer<typeof recipeSchema>
}

Generative UI Pattern

Server-side UI Generation

typescript
import { streamUI } from "ai/rsc"
import { openai } from "@/lib/ai"
import { z } from "zod"

export async function generateUI(prompt: string) {
  const result = await streamUI({
    model: openai("gpt-4o"),
    prompt,
    tools: {
      showWeather: {
        description: "Show weather for a location",
        parameters: z.object({
          location: z.string(),
          temperature: z.number(),
        }),
        generate: async function* ({ location, temperature }) {
          yield <LoadingSkeleton />
          return <WeatherCard location={location} temperature={temperature} />
        },
      },
      showStock: {
        description: "Show stock price",
        parameters: z.object({
          symbol: z.string(),
          price: z.number(),
        }),
        generate: async function* ({ symbol, price }) {
          yield <LoadingSkeleton />
          return <StockCard symbol={symbol} price={price} />
        },
      },
    },
  })

  return result.value
}

RAG (Retrieval-Augmented Generation)

With Supabase pgvector

typescript
import { embed } from "ai"
import { openai } from "@/lib/ai"
import { createClient } from "@/lib/supabase/server"

// Generate embedding
async function generateEmbedding(text: string) {
  const { embedding } = await embed({
    model: openai.embedding("text-embedding-3-small"),
    value: text,
  })
  return embedding
}

// Search similar documents
async function searchDocuments(query: string, limit = 5) {
  const supabase = await createClient()
  const queryEmbedding = await generateEmbedding(query)

  const { data } = await supabase.rpc("match_documents", {
    query_embedding: queryEmbedding,
    match_threshold: 0.7,
    match_count: limit,
  })

  return data
}

// RAG Chat
export async function POST(request: Request) {
  const { messages } = await request.json()
  const lastMessage = messages[messages.length - 1].content

  // Retrieve relevant context
  const context = await searchDocuments(lastMessage)
  const contextText = context.map((d: any) => d.content).join("\n\n")

  const result = streamText({
    model: openai("gpt-4o"),
    system: `Answer based on the following context:\n\n${contextText}`,
    messages,
  })

  return result.toDataStreamResponse()
}

Supabase pgvector SQL Setup

sql
-- Enable vector extension
CREATE EXTENSION IF NOT EXISTS vector;

-- Create documents table
CREATE TABLE documents (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  content TEXT NOT NULL,
  embedding VECTOR(1536),
  metadata JSONB DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Create similarity search function
CREATE OR REPLACE FUNCTION match_documents(
  query_embedding VECTOR(1536),
  match_threshold FLOAT,
  match_count INT
)
RETURNS TABLE (id UUID, content TEXT, similarity FLOAT)
LANGUAGE SQL STABLE
AS $$
  SELECT id, content, 1 - (embedding <=> query_embedding) AS similarity
  FROM documents
  WHERE 1 - (embedding <=> query_embedding) > match_threshold
  ORDER BY embedding <=> query_embedding
  LIMIT match_count;
$$;

Tool Calling Pattern

typescript
import { streamText, tool } from "ai"
import { openai } from "@/lib/ai"
import { z } from "zod"

const result = streamText({
  model: openai("gpt-4o"),
  messages,
  tools: {
    getWeather: tool({
      description: "Get current weather for a location",
      parameters: z.object({
        location: z.string().describe("City name"),
      }),
      execute: async ({ location }) => {
        // API call
        return { temperature: 22, condition: "sunny" }
      },
    }),
    searchProducts: tool({
      description: "Search for products in the catalog",
      parameters: z.object({
        query: z.string(),
        maxPrice: z.number().optional(),
      }),
      execute: async ({ query, maxPrice }) => {
        // DB query
        return []
      },
    }),
  },
  maxSteps: 5, // Allow multi-step tool calls
})

useCompletion (Non-Chat Streaming)

typescript
"use client"

import { useCompletion } from "@ai-sdk/react"

export const TextGenerator = () => {
  const { completion, input, handleInputChange, handleSubmit, isLoading } = useCompletion({
    api: "/api/generate",
  })

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input value={input} onChange={handleInputChange} />
        <button type="submit" disabled={isLoading}>Generate</button>
      </form>
      <div className="prose">{completion}</div>
    </div>
  )
}

Anti-Patterns

  • Never expose API keys to client - always use server-side routes
  • Never skip error handling on AI calls - models can fail or timeout
  • Never send unlimited context - truncate/summarize conversation history
  • Never use blocking (non-streaming) calls for long AI responses in UI
  • Never trust AI output without validation - always validate with Zod for structured output
  • Never hardcode model names - use configuration for easy switching

Additional Resources

  • references/prompt-engineering.md - Effective prompt design patterns
  • references/cost-optimization.md - Token usage optimization strategies