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 analysis latency controls
1. Scope / Trigger
- Trigger: backend elder AI analysis calls an OpenAI-compatible chat model through LangChain and returns a fixed API response to the UI.
- Apply this contract whenever changing
modules/ai/server/config.ts,modules/ai/server/analysis.ts, or any elder AI analysis route that can block on a provider call.
2. Signatures
- Runtime config:
getAiRuntimeConfig() -> { success: true, config } | { success: false, reason }. - Config fields:
baseUrl,apiKey,chatModel,requestTimeoutMs,maxTokens,maxRetries. - Generation service:
generateElderAiAnalysis(context, elderId) -> ServiceResult<{ analysis }>.
3. Contracts
- Environment keys:
AI_API_KEY: required.AI_BASE_URL: optional OpenAI-compatible base URL.AI_CHAT_MODEL: optional model name.AI_REQUEST_TIMEOUT_MS: optional bounded integer request timeout.AI_MAX_TOKENS: optional bounded integer generation cap.AI_MAX_RETRIES: optional bounded integer retry count.
ChatOpenAIconstruction must pass timeout, token cap, retry count, API key, model, and base URL from the runtime config.- Provider errors must never persist prompts, resident context, API keys, raw provider messages, or stack traces in
elder_ai_analyses.
4. Validation & Error Matrix
- Missing
AI_API_KEY-> returnAI_API_KEY 未配置, persist failed history witherrorCategory: "missing_config"when organization context exists. - Provider timeout,
AbortError, or timeout-like provider message -> returnAI 服务响应超时, persist failed history witherrorCategory: "timeout". - Other provider failure -> return
AI 服务暂时不可用, persist failed history witherrorCategory: "provider_error". - Invalid model output -> return
AI 返回结构不符合固定分析格式, persist failed history witherrorCategory: "schema_validation_failed".
5. Good/Base/Bad Cases
- Good: configured runtime uses a short timeout, bounded max tokens, and no hidden retry amplification; timeout failures are visible as sanitized failed history.
- Base: provider is slow or unreachable; user receives the timeout reason instead of waiting for default SDK retries and transport timeouts.
- Bad: service relies on LangChain/OpenAI defaults, causing long waits from high token caps, implicit retries, or unbounded request timeouts.
6. Tests Required
- Unit: assert
generateElderAiAnalysisconstructsChatOpenAIwith runtime-configuredtimeout,maxTokens, andmaxRetries. - Unit: assert
AbortErrorand timeout-like model failures persisterrorCategory: "timeout", returnAI 服务响应超时, and do not persist provider secrets or prompt text. - Existing failure tests must continue to assert provider errors and schema failures use sanitized persisted history.
7. Wrong vs Correct
Wrong
new ChatOpenAI({
apiKey,
model,
maxTokens: 1800,
});
Correct
new ChatOpenAI({
apiKey: config.apiKey,
model: config.chatModel,
timeout: config.requestTimeoutMs,
maxTokens: config.maxTokens,
maxRetries: config.maxRetries,
configuration: config.baseUrl ? { baseURL: config.baseUrl } : undefined,
});
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]/analysesPOST /api/ai/elders/[id]/analysesGET /api/ai/knowledgePOST /api/ai/knowledgePATCH /api/ai/knowledge/[id]DELETE /api/ai/knowledge/[id]
- DB:
ai_knowledge_entriesai_knowledge_chunkswith textcontent; the legacy JSONBembeddingcolumn remains defaulted for compatibility and is not read or written by retrieval.elder_ai_analyses
- Runtime env:
AI_API_KEYrequired for chat generation only.AI_BASE_URLoptional OpenAI-compatible endpoint.AI_CHAT_MODELoptional, defaults inmodules/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 stringcategory: stringtags: stringbody: non-empty stringstatus:"enabled" | "disabled"
- Elder analysis history stores:
status:"completed" | "failed"dataScopes: JSON array of included familiesresultJson: only for completed rowserrorCategory/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 thevectorextension or embedding API calls. - Completed elder analysis
resultJson.modelSummarymust include{ provider: "openai-compatible", chatModel, knowledgeRetrieval: "keyword" }. - Completed elder analysis
resultJson.citationsmust be derived from citation IDs actually referenced bykeyFindings[].citationIdsandrecommendations[].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 return403. - Platform knowledge mutation without
platform:manage->403. - Missing
AI_API_KEY-> save failed analysis withmissing_config, return structured failure. - Provider failure -> save failed analysis with
provider_errorortimeout. - 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:readandknowledge:readby default, but cannot mutate knowledge. - Bad: viewer has
elder:readbut noai:read; the AI row action must not be exposed and API must return403. - 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:*andknowledge:*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"],
};