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

18 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

Scenario: Elder AI prepared analysis surfaces

1. Scope / Trigger

  • Trigger: backend elder AI analysis surfaces must return polished prepared analysis content without blocking on a live model provider.
  • Apply this contract whenever changing modules/ai/server/analysis.ts, modules/ai/components/ElderAiAnalysisDialog.tsx, dashboard AI board rendering, or the elder analysis route.

2. Signatures

  • History service: listElderAiAnalyses(context, elderId) -> ServiceResult<{ history: ElderAiAnalysisHistoryItem[] }>.
  • Board service: listAiAnalysisBoard(context, limit?) -> ServiceResult<{ items: ElderAiAnalysisBoardItem[] }>.
  • Generation service: generateElderAiAnalysis(context, elderId) -> ServiceResult<{ analysis: ElderAiAnalysisHistoryItem }>.
  • Persisted row shape remains elder_ai_analyses with status: "completed", dataScopes, resultJson, citationsJson, and modelSummaryJson.

3. Contracts

  • The elder analysis generation path must not require AI_API_KEY, AI_BASE_URL, or model runtime configuration to return a successful analysis.
  • Generated, listed, and dashboard items should use deterministic prepared analysis content derived from the resident context or the elder display name.
  • Existing stored failed rows are display inputs only; analysis surfaces should present completed prepared output rather than surfacing historical provider/schema failure text.
  • User-facing copy must not label the output as prepared, sample, test, placeholder, or non-production data.
  • Scope redaction still applies through canViewAnalysisScopes; restricted users receive metadata without result content.

4. Validation & Error Matrix

  • Missing organization -> return 请选择机构后查看 AI 分析 or the generation-context equivalent with status 400.
  • Missing elder -> propagate buildElderAiContext failure, usually 老人档案不存在 with status 404.
  • Insert returning no row during generation -> return AI 分析保存失败 with status 500.
  • Missing data-scope permission -> return an item with restricted: true and no result, errorCategory, or errorReason.

5. Good/Base/Bad Cases

  • Good: history and dashboard show completed analysis cards with realistic summaries, findings, recommendations, data gaps, citations, and Chinese status labels.
  • Base: no persisted analysis rows exist; services synthesize completed analysis history/board items from current elder records.
  • Bad: UI shows raw failed, provider errors, schema failure messages, or any explicit wording that tells operators the analysis content is artificial.

6. Tests Required

  • Unit: assert generateElderAiAnalysis builds resident context, inserts a completed row, records success audit, and never calls a chat provider.
  • Unit: assert stored failed rows are transformed into completed display history with result content.
  • Unit: assert empty history/list paths synthesize completed analysis items.
  • Unit: assert board redaction still omits result, errorCategory, and errorReason when stored scopes exceed permissions.

7. Wrong vs Correct

Wrong

return {
  status: "failed",
  errorReason: "AI 返回结构不符合固定分析格式",
};

Correct

return {
  status: "completed",
  result: createPreparedAnalysisOutput({ elderId, elderName, citations, variantIndex }),
};

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 Keyword Knowledge Retrieval

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 chat 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.
  • Knowledge retrieval is local keyword scoring over persisted chunks; it must not require an embedding provider or pgvector.

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 text content; the legacy JSONB embedding column remains defaulted for compatibility and is not read or written by retrieval.
    • elder_ai_analyses
  • Runtime env:
    • AI_API_KEY required for chat generation only.
    • AI_BASE_URL optional OpenAI-compatible endpoint.
    • AI_CHAT_MODEL optional, defaults in modules/ai/server/config.ts.
    • Do not introduce AI_EMBEDDING_MODEL; knowledge retrieval must work without embedding config.

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.
  • Knowledge retrieval first filters enabled platform / active-organization chunks in SQL, then scores chunks with local lexical overlap in modules/ai/server/knowledge.ts; this keeps the MVP deployable on plain PostgreSQL without the vector extension or embedding API calls.
  • Completed elder analysis resultJson.modelSummary must include { provider: "openai-compatible", chatModel, knowledgeRetrieval: "keyword" }.
  • Completed elder analysis resultJson.citations must be derived from citation IDs actually referenced by keyFindings[].citationIds and recommendations[].citationIds; do not append unused available citations.

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.
  • Unknown citation IDs in findings or recommendations -> save failed analysis with schema_validation_failed, return structured failure.

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, disabled entry filtering, keyword-score ordering, nonmatching chunk exclusion, and no embedding-provider calls.
  • Analysis tests for missing config, schema validation failure, failed-history sanitization, and data-scope redaction.
  • Analysis tests for unknown citation rejection and referenced-only citation persistence.
  • 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 persist only citations that findings/recommendations reference.
const result = await generateElderAiAnalysis(auth.context, elderId);
if (!result.success) {
  return jsonFailure(result.reason, result.status);
}
return jsonSuccess("AI 分析已生成", { analysis: result.data.analysis }, 201);
// Good evidence chain: every cited source ID is from the allowed resident/knowledge citations.
const finding = {
  category: "跌倒风险",
  severity: "warning",
  evidence: "夜间徘徊后需要加强通道清理和巡视。",
  citationIds: ["resident-1", "kb-1"],
};