524 lines
18 KiB
Markdown
524 lines
18 KiB
Markdown
# 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
|
|
```bash
|
|
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.
|
|
|
|
```typescript
|
|
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.
|
|
|
|
```typescript
|
|
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.
|
|
|
|
```typescript
|
|
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.
|
|
|
|
```typescript
|
|
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.
|
|
|
|
```typescript
|
|
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.
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
return {
|
|
status: "failed",
|
|
errorReason: "AI 返回结构不符合固定分析格式",
|
|
};
|
|
```
|
|
|
|
#### Correct
|
|
|
|
```typescript
|
|
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.
|
|
|
|
```typescript
|
|
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.
|
|
|
|
```typescript
|
|
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.
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
import { openai } from "@ai-sdk/openai";
|
|
|
|
const model = openai("gpt-4o-mini", {
|
|
// Optional: custom configuration
|
|
});
|
|
```
|
|
|
|
### Google Gemini
|
|
|
|
```typescript
|
|
import { google } from "@ai-sdk/google";
|
|
|
|
const model = google("gemini-1.5-flash");
|
|
```
|
|
|
|
### Anthropic
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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);
|
|
```
|
|
|
|
```typescript
|
|
// 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"],
|
|
};
|
|
```
|