Files
teatea-pension/.trellis/spec/backend/ai-sdk-integration.md

13 KiB

AI SDK Backend Integration Guidelines

1. Overview

This document covers backend integration patterns using the Vercel AI SDK (ai package) for AI-powered features.

Supported Providers

  • OpenAI: GPT-4o, GPT-4o-mini, GPT-4-turbo
  • Google Gemini: gemini-1.5-pro, gemini-1.5-flash
  • Anthropic: Claude 3.5 Sonnet, Claude 3 Opus

Package Dependencies

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

2. Basic Usage

generateText

Use generateText for simple text generation tasks where you need a complete response.

import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";

const { text } = await generateText({
  model: openai("gpt-4o-mini"),
  prompt: "Summarize this document...",
});

generateObject (Structured Output with Zod)

Use generateObject when you need type-safe structured output. The AI SDK validates the response against your Zod schema automatically.

import { generateObject } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";

const classificationSchema = z.object({
  category: z.enum(["urgent", "normal", "low"]),
  confidence: z.number().min(0).max(1),
  reasoning: z.string(),
});

const { object } = await generateObject({
  model: openai("gpt-4o-mini"),
  schema: classificationSchema,
  prompt: "Classify the priority of this task...",
});
// object is typed as { category: "urgent" | "normal" | "low", confidence: number, reasoning: string }

streamText (For SSE/Streaming)

Use streamText for real-time streaming responses, ideal for chat interfaces and long-form content generation.

import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";

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

// Return as SSE stream
return result.toDataStreamResponse();

3. Telemetry Configuration

IMPORTANT: Always enable telemetry for token tracking and performance monitoring.

import { generateObject } from "ai";
import { openai } from "@ai-sdk/openai";

const { object } = await generateObject({
  model: openai("gpt-4o-mini"),
  schema: mySchema,
  prompt,
  experimental_telemetry: {
    isEnabled: true,
    functionId: "orders.classify",  // Module.function naming
    metadata: {
      orderId,
      userId,
    },
  },
});

Telemetry Naming Convention

Use dot-separated format for functionId: module.function

Module Example functionId
Orders orders.classify, orders.summarize
Support support.generateReply, support.categorize
Content content.summarize, content.translate
Users users.analyzePreferences

Auto-recorded Metrics

When telemetry is enabled, these metrics are automatically tracked:

Metric Description
ai.model.id Model identifier (e.g., gpt-4o-mini)
ai.model.provider Provider name (e.g., openai)
ai.usage.prompt_tokens Input tokens consumed
ai.usage.completion_tokens Output tokens generated
ai.usage.total_tokens Total tokens used
ai.response.finish_reason Completion reason (stop, length, etc.)

4. Tool Calling

Define tools that the AI model can invoke to perform actions in your system.

import { generateText, tool } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";

const result = await generateText({
  model: openai("gpt-4o"),
  prompt: "Create a task for the user...",
  tools: {
    createTask: tool({
      description: "Create a new task in the system",
      parameters: z.object({
        title: z.string(),
        dueDate: z.string().optional(),
        priority: z.enum(["high", "medium", "low"]),
      }),
      execute: async ({ title, dueDate, priority }) => {
        const task = await db.insert(tasks).values({
          title,
          dueDate: dueDate ? new Date(dueDate) : null,
          priority,
        }).returning();
        return { success: true, taskId: task[0].id };
      },
    }),
    searchOrders: tool({
      description: "Search for orders by criteria",
      parameters: z.object({
        query: z.string(),
        status: z.enum(["pending", "completed", "cancelled"]).optional(),
        limit: z.number().default(10),
      }),
      execute: async ({ query, status, limit }) => {
        const orders = await db.query.orders.findMany({
          where: and(
            like(orders.title, `%${query}%`),
            status ? eq(orders.status, status) : undefined
          ),
          limit,
        });
        return { orders };
      },
    }),
  },
});

// Access tool results
if (result.toolCalls) {
  for (const toolCall of result.toolCalls) {
    console.log(`Tool: ${toolCall.toolName}`, toolCall.result);
  }
}

5. Error Handling

Always implement graceful error handling for AI operations.

import { generateObject } from "ai";
import { openai } from "@ai-sdk/openai";
import { logger } from "@your-app/logs";

async function classifyOrder(orderData: OrderData) {
  try {
    const { object } = await generateObject({
      model: openai("gpt-4o-mini"),
      schema: classificationSchema,
      prompt: buildClassificationPrompt(orderData),
      experimental_telemetry: {
        isEnabled: true,
        functionId: "orders.classify",
      },
    });
    return { success: true, data: object };
  } catch (error) {
    logger.error("AI generation failed", {
      error,
      orderId: orderData.id,
      prompt: buildClassificationPrompt(orderData).slice(0, 100)
    });

    // Return graceful fallback
    return {
      success: false,
      reason: "AI processing failed",
      error: error instanceof Error ? error.message : "Unknown error",
    };
  }
}

Common Error Types

Error Cause Resolution
Rate limit exceeded Too many requests Implement exponential backoff
Context length exceeded Prompt too long Truncate or summarize input
Invalid API key Missing/wrong credentials Check environment variables
Schema validation failed AI output doesn't match schema Adjust schema or prompt

6. Prompt Engineering Best Practices

Use XML Structure for Complex Prompts

XML tags help the AI model better understand the structure of your request.

const prompt = `
<context>
${contextData}
</context>

<task>
Analyze the above context and extract key information.
</task>

<output_format>
Return a JSON object with the following fields:
- summary: A brief summary
- keyPoints: Array of key points
- sentiment: positive, negative, or neutral
</output_format>
`;

System Prompts

Define consistent behavior with system prompts.

import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";

const result = await generateText({
  model: openai("gpt-4o"),
  system: `You are a professional assistant.
Always respond in a structured format.
Be concise and accurate.
Never make up information - if unsure, say so.`,
  messages: userMessages,
});

Multi-step Prompts

For complex tasks, break down into multiple AI calls.

// Step 1: Extract entities
const { object: entities } = await generateObject({
  model: openai("gpt-4o-mini"),
  schema: entitiesSchema,
  prompt: `Extract entities from: ${document}`,
});

// Step 2: Classify based on entities
const { object: classification } = await generateObject({
  model: openai("gpt-4o-mini"),
  schema: classificationSchema,
  prompt: `
<entities>
${JSON.stringify(entities, null, 2)}
</entities>

<task>
Based on these entities, classify the document category.
</task>
`,
});

7. Provider-Specific Configuration

OpenAI

import { openai } from "@ai-sdk/openai";

const model = openai("gpt-4o-mini", {
  // Optional: custom configuration
});

Google Gemini

import { google } from "@ai-sdk/google";

const model = google("gemini-1.5-flash");

Anthropic

import { anthropic } from "@ai-sdk/anthropic";

const model = anthropic("claude-3-5-sonnet-20241022");

8. Best Practices Summary

Rule Description
Always enable telemetry Track token usage and performance for cost monitoring
Use generateObject for structured output Leverage Zod schemas for type safety and validation
Use XML prompts for complex tasks Better structure improves AI understanding
Handle errors gracefully Return fallback responses, never crash
Log AI failures Include context (truncated prompt, IDs) for debugging
Use appropriate model sizes Use mini models for simple tasks, larger for complex
Implement rate limiting Protect against API quota exhaustion
Cache responses when appropriate Reduce costs for repeated queries

9. Environment Variables

Required environment variables for AI providers:

# OpenAI
OPENAI_API_KEY=sk-...

# Google Gemini
GOOGLE_GENERATIVE_AI_API_KEY=...

# Anthropic
ANTHROPIC_API_KEY=sk-ant-...

Scenario: Elder AI Analysis MVP With LangChain And pgvector

1. Scope / Trigger

  • Trigger: AI features that inspect resident, care, health, family, admission, alert, incident, or knowledge data.
  • Current project contract: the elder analysis MVP uses LangChain on the server with an OpenAI-compatible provider, not the Vercel AI SDK runtime path.
  • Keep provider calls under modules/ai/server/*; feature modules and API routes must not instantiate model clients directly.

2. Signatures

  • API:
    • GET /api/ai/elders/[id]/analyses
    • POST /api/ai/elders/[id]/analyses
    • GET /api/ai/knowledge
    • POST /api/ai/knowledge
    • PATCH /api/ai/knowledge/[id]
    • DELETE /api/ai/knowledge/[id]
  • DB:
    • ai_knowledge_entries
    • ai_knowledge_chunks with vector(1536)
    • elder_ai_analyses
  • Runtime env:
    • AI_API_KEY required for generation and embedding.
    • AI_BASE_URL optional OpenAI-compatible endpoint.
    • AI_CHAT_MODEL optional, defaults in modules/ai/server/config.ts.
    • AI_EMBEDDING_MODEL optional, defaults in modules/ai/server/config.ts.

3. Contracts

  • All API responses use the project API shape: { success: boolean, reason: string, ...payload }.
  • Knowledge entry input fields:
    • scope: "platform" | "organization"
    • title: non-empty string
    • category: string
    • tags: string
    • body: non-empty string
    • status: "enabled" | "disabled"
  • Elder analysis history stores:
    • status: "completed" | "failed"
    • dataScopes: JSON array of included families
    • resultJson: only for completed rows
    • errorCategory / errorReason: sanitized only for failed rows
  • Failed rows must not store prompts, full resident context snapshots, API keys, or raw provider errors.
  • Knowledge retrieval may return enabled platform knowledge and enabled active-organization knowledge only.

4. Validation & Error Matrix

  • Missing ai:read -> 403 权限不足.
  • Missing elder:read -> 403 权限不足.
  • Missing active organization for elder analysis -> 400.
  • Target elder outside active organization -> 404.
  • Missing knowledge:read -> knowledge scope is omitted from analysis context.
  • Missing knowledge:manage -> knowledge mutation routes return 403.
  • Platform knowledge mutation without platform:manage -> 403.
  • Missing AI_API_KEY -> save failed analysis with missing_config, return structured failure.
  • Provider failure -> save failed analysis with provider_error or timeout.
  • Invalid model output -> save failed analysis with schema_validation_failed.

5. Good/Base/Bad Cases

  • Good: org manager generates elder analysis; context includes only data families allowed by the manager's current permissions; history stores matching dataScopes.
  • Base: caregiver generates analysis; caregiver gets ai:read and knowledge:read by default, but cannot mutate knowledge.
  • Bad: viewer has elder:read but no ai:read; the AI row action must not be exposed and API must return 403.
  • Bad: organization user attempts to retrieve another organization's private knowledge; the retrieval query must not include those chunks.

6. Tests Required

  • Permission seeding assertions for ai:* and knowledge:* default role grants.
  • Knowledge retrieval tests for platform shared, own organization, other organization, and disabled entry filtering.
  • Analysis tests for missing config, schema validation failure, failed-history sanitization, and data-scope redaction.
  • Route tests for auth/permission failures and standard response shape.

7. Wrong vs Correct

Wrong

// Do not call a model from a feature route and pass raw resident rows directly.
const model = new ChatOpenAI({ apiKey: process.env.AI_API_KEY });
await model.invoke(JSON.stringify(residentRows));

Correct

// Keep model calls in modules/ai/server and build permission-aware context first.
const result = await generateElderAiAnalysis(auth.context, elderId);
if (!result.success) {
  return jsonFailure(result.reason, result.status);
}
return jsonSuccess("AI 分析已生成", { analysis: result.data.analysis }, 201);