康心伴Logo
康心伴WellAlly
AI 应用开发

构建 AI 智能膳食规划器:Next.js + OpenAI + PostgreSQL

全栈教程:使用 Next.js 14、OpenAI GPT-4 API 和 PostgreSQL 构建个性化膳食规划应用。包含营养成分分析、过敏原过滤、膳食偏好学习等功能。

W
WellAlly 开发团队
2026-03-08
20 分钟阅读

关键要点

  • GPT-4 优化用于营养分析:使用结构化输出和 JSON 模式,确保 AI 返回的膳食计划数据格式一致、可验证
  • RAG 增强膳食建议:结合用户历史数据、营养知识库和实时偏好,生成个性化建议
  • PostgreSQL 存储膳食数据:使用 JSONB 列存储灵活的营养成分和过敏原信息
  • 流式响应提升体验:使用 Server-Sent Events 实时展示 AI 生成过程
  • 营养验证保障安全:在 AI 输出后进行营养合理性检查,防止不健康的建议

个性化膳食规划是健康管理的重要组成部分。本教程将教你如何构建一个 AI 驱动的膳食规划应用,能够根据用户的健康目标、饮食偏好、过敏原和营养需求,生成每日膳食计划。

前置条件:

  • Node.js 18+ 和 npm
  • Next.js 14 基础知识
  • OpenAI API 密钥
  • PostgreSQL 数据库(可使用 Supabase)

项目架构概览

code
┌─────────────────────────────────────────────────┐
│                  前端层                          │
│  (React 19、Tailwind CSS、用户偏好界面)          │
└─────────────────┬───────────────────────────────┘
                  │
┌─────────────────▼───────────────────────────────┐
│               API 路由层 (Next.js)               │
│  (OpenAI 集成、营养验证、用户偏好管理)           │
└─────────────────┬───────────────────────────────┘
                  │
┌─────────────────▼───────────────────────────────┐
│              数据持久化层                        │
│  (PostgreSQL - 用户、偏好、膳食历史、营养库)     │
└─────────────────────────────────────────────────┘
Code collapsed

步骤 1:项目初始化

code
npx create-next-app@latest ai-meal-planner --typescript --tailwind --app
cd ai-meal-planner
npm install openai @supabase/supabase-js zod cheerio
npm install -D @types/node
Code collapsed

配置环境变量 .env.local

code
# OpenAI 配置
OPENAI_API_KEY=sk-your-openai-api-key
OPENAI_MODEL=gpt-4-turbo-preview

# Supabase/PostgreSQL 配置
DATABASE_URL=postgresql://user:password@host:5432/mealplanner
SUPABASE_URL=your-supabase-url
SUPABASE_ANON_KEY=your-supabase-anon-key

# 应用配置
NEXT_PUBLIC_APP_URL=http://localhost:3000
Code collapsed

步骤 2:数据库模式设计

运行以下 SQL 创建数据库表:

code
-- migrations/001_initial_schema.sql

-- 用户表
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email VARCHAR(255) UNIQUE NOT NULL,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- 用户偏好表
CREATE TABLE user_preferences (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id) ON DELETE CASCADE,

  -- 健康目标
  health_goals JSONB DEFAULT '[]', -- ["weight-loss", "muscle-gain", "maintenance"]

  -- 膳食类型
  diet_type VARCHAR(50) DEFAULT 'omnivore', -- omnivore, vegetarian, vegan, keto, paleo

  -- 过敏原和限制
  allergies JSONB DEFAULT '[]', -- ["nuts", "dairy", "gluten"]
  dislikes JSONB DEFAULT '[]',

  -- 营养目标(每日)
  daily_calories INTEGER DEFAULT 2000,
  daily_protein INTEGER DEFAULT 50, -- 克
  daily_carbs INTEGER DEFAULT 250,
  daily_fats INTEGER DEFAULT 70,

  -- 用餐偏好
  meals_per_day INTEGER DEFAULT 3,
  cooking_time_minutes INTEGER DEFAULT 30,

  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- 膳食计划表
CREATE TABLE meal_plans (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id) ON DELETE CASCADE,
  plan_date DATE NOT NULL,

  -- 计划内容(JSONB 存储灵活结构)
  meals JSONB NOT NULL, -- [{type: "breakfast", name: "...", calories: ..., nutrition: {...}, ingredients: [...]}]

  -- AI 生成元数据
  ai_model VARCHAR(100),
  generated_at TIMESTAMP DEFAULT NOW(),

  UNIQUE(user_id, plan_date)
);

-- 营养知识库表(用于 RAG)
CREATE TABLE nutrition_knowledge (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  category VARCHAR(100) NOT NULL, -- protein, vitamins, minerals, dietary-patterns
  title VARCHAR(255) NOT NULL,
  content TEXT NOT NULL,
  embedding VECTOR(1536), -- OpenAI embeddings
  references TEXT[],
  created_at TIMESTAMP DEFAULT NOW()
);

-- 膳食历史表(用于学习用户偏好)
CREATE TABLE meal_history (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id) ON DELETE CASCADE,
  meal_date DATE NOT NULL,
  meal_type VARCHAR(50) NOT NULL, -- breakfast, lunch, dinner, snack
  meal_name TEXT NOT NULL,

  -- 实际摄入(用户记录)
  consumed_calories INTEGER,
  consumed_protein NUMERIC(5,1),
  consumed_carbs NUMERIC(5,1),
  consumed_fats NUMERIC(5,1),

  -- 用户评分
  rating INTEGER CHECK (rating >= 1 AND rating <= 5),
  notes TEXT,

  created_at TIMESTAMP DEFAULT NOW()
);

-- 索引优化查询
CREATE INDEX idx_meal_plans_user_date ON meal_plans(user_id, plan_date);
CREATE INDEX idx_meal_history_user_date ON meal_history(user_id, meal_date);
CREATE INDEX idx_nutrition_knowledge_embedding ON nutrition_knowledge USING ivfflat (embedding vector_cosine_ops);
Code collapsed

步骤 3:实现 OpenAI 服务

创建 lib/openai.ts

code
// lib/openai.ts
import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

export const MEAL_PLANNER_SYSTEM_PROMPT = `你是一位专业的注册营养师和膳食规划师。你的任务是根据用户的需求和偏好,创建健康、均衡的膳食计划。

规则:
1. 每餐应包含宏量营养素(蛋白质、碳水化合物、脂肪)的合理分配
2. 优先考虑全天然、未加工的食材
3. 考虑用户指定的过敏原和饮食限制
4. 提供清晰的烹饪说明和份量
5. 包含每餐的营养估算

输出格式:
- 必须是有效的 JSON
- 所有数值使用数字类型,不要使用字符串
- 遵循提供的 JSON Schema`;

interface NutritionInfo {
  calories: number;
  protein: number; // 克
  carbs: number;
  fats: number;
  fiber?: number;
  sugar?: number;
}

interface Meal {
  type: 'breakfast' | 'lunch' | 'dinner' | 'snack';
  name: string;
  description: string;
  ingredients: Array<{
    item: string;
    amount: string;
    calories?: number;
  }>;
  nutrition: NutritionInfo;
  instructions: string[];
  prepTime: number; // 分钟
  cookTime: number;
}

interface DayPlan {
  date: string;
  meals: Meal[];
  dailyTotals: NutritionInfo;
  notes?: string;
}

export class MealPlannerAI {
  async generateMealPlan(params: {
    healthGoals: string[];
    dietType: string;
    allergies: string[];
    dailyCalories: number;
    mealsPerDay: number;
    cookingTime: number;
    date: string;
  }): Promise<DayPlan> {
    const prompt = this.buildPrompt(params);

    const response = await openai.chat.completions.create({
      model: process.env.OPENAI_MODEL || 'gpt-4-turbo-preview',
      messages: [
        { role: 'system', content: MEAL_PLANNER_SYSTEM_PROMPT },
        { role: 'user', content: prompt },
      ],
      response_format: { type: 'json_object' },
      temperature: 0.7,
    });

    const content = response.choices[0].message.content;
    if (!content) {
      throw new Error('AI response is empty');
    }

    const plan = JSON.parse(content);
    return this.validateAndNormalizePlan(plan, params.date);
  }

  async generateStreamingMealPlan(
    params: any,
    onData: (chunk: string) => void,
    onComplete: (plan: DayPlan) => void,
    onError: (error: Error) => void
  ): Promise<void> {
    const prompt = this.buildPrompt(params);

    const stream = await openai.chat.completions.create({
      model: process.env.OPENAI_MODEL || 'gpt-4-turbo-preview',
      messages: [
        { role: 'system', content: MEAL_PLANNER_SYSTEM_PROMPT },
        { role: 'user', content: prompt },
      ],
      response_format: { type: 'json_object' },
      temperature: 0.7,
      stream: true,
    });

    let fullContent = '';

    for await (const chunk of stream) {
      const content = chunk.choices[0]?.delta?.content || '';
      if (content) {
        fullContent += content;
        onData(content);
      }
    }

    try {
      const plan = JSON.parse(fullContent);
      onComplete(this.validateAndNormalizePlan(plan, params.date));
    } catch (error) {
      onError(error as Error);
    }
  }

  private buildPrompt(params: any): string {
    return `请为 ${params.date} 创建膳食计划。

用户资料:
- 健康目标: ${params.healthGoals.join(', ') || '维持健康'}
- 饮食类型: ${params.dietType}
- 过敏原/限制: ${params.allergies.join(', ') || '无'}
- 每日目标卡路里: ${params.dailyCalories}
- 每日用餐次数: ${params.mealsPerDay}
- 烹饪时间限制: ${params.cookingTime} 分钟

请提供:
1. 详细的食谱
2. 营养成分估算
3. 准备和烹饪时间
4. 清晰的说明`;
  }

  private validateAndNormalizePlan(plan: any, date: string): DayPlan {
    // 验证必需字段
    if (!plan.meals || !Array.isArray(plan.meals)) {
      throw new Error('Invalid meal plan: missing meals array');
    }

    // 确保每餐都有必需字段
    const normalizedMeals: Meal[] = plan.meals.map((meal: any) => ({
      type: meal.type || 'snack',
      name: meal.name || 'Unnamed Meal',
      description: meal.description || '',
      ingredients: meal.ingredients || [],
      nutrition: {
        calories: Number(meal.nutrition?.calories) || 0,
        protein: Number(meal.nutrition?.protein) || 0,
        carbs: Number(meal.nutrition?.carbs) || 0,
        fats: Number(meal.nutrition?.fats) || 0,
        fiber: Number(meal.nutrition?.fiber) || 0,
        sugar: Number(meal.nutrition?.sugar) || 0,
      },
      instructions: meal.instructions || [],
      prepTime: Number(meal.prepTime) || 10,
      cookTime: Number(meal.cookTime) || 20,
    }));

    // 计算每日总计
    const dailyTotals = normalizedMeals.reduce(
      (acc, meal) => ({
        calories: acc.calories + meal.nutrition.calories,
        protein: acc.protein + meal.nutrition.protein,
        carbs: acc.carbs + meal.nutrition.carbs,
        fats: acc.fats + meal.nutrition.fats,
        fiber: acc.fiber + (meal.nutrition.fiber || 0),
        sugar: acc.sugar + (meal.nutrition.sugar || 0),
      }),
      { calories: 0, protein: 0, carbs: 0, fats: 0, fiber: 0, sugar: 0 }
    );

    return {
      date,
      meals: normalizedMeals,
      dailyTotals,
      notes: plan.notes,
    };
  }
}

export const mealPlannerAI = new MealPlannerAI();
Code collapsed

步骤 4:实现营养验证服务

创建 lib/nutrition-validator.ts

code
// lib/nutrition-validator.ts
import { z } from 'zod';
import { NutritionInfo, Meal, DayPlan } from './openai';

// 营养合理性验证规则
const NUTRITION_RULES = {
  calories: { min: 1200, max: 4000, warningThreshold: 0.8 },
  protein: { min: 0.1, max: 0.35 }, // 占总卡路里的百分比
  carbs: { min: 0.25, max: 0.55 },
  fats: { min: 0.2, max: 0.35 },
  fiber: { min: 25, max: 50 }, // 克
  sugar: { max: 50 }, // 克
};

export interface ValidationResult {
  isValid: boolean;
  warnings: string[];
  errors: string[];
  correctedPlan?: DayPlan;
}

export class NutritionValidator {
  validate(plan: DayPlan, targetCalories: number): ValidationResult {
    const warnings: string[] = [];
    const errors: string[] = [];

    // 1. 验证总卡路里
    const calorieDeviation = Math.abs(
      (plan.dailyTotals.calories - targetCalories) / targetCalories
    );

    if (calorieDeviation > 0.15) {
      errors.push(
        `总卡路里 ${plan.dailyTotals.calories} 与目标 ${targetCalories} 偏差超过 15%`
      );
    } else if (calorieDeviation > 0.1) {
      warnings.push(
        `总卡路里 ${plan.dailyTotals.calories} 与目标 ${targetCalories} 偏差较大`
      );
    }

    // 2. 验证宏量营养素比例
    const proteinPercent = (plan.dailyTotals.protein * 4) / plan.dailyTotals.calories;
    const carbsPercent = (plan.dailyTotals.carbs * 4) / plan.dailyTotals.calories;
    const fatsPercent = (plan.dailyTotals.fats * 9) / plan.dailyTotals.calories;

    if (proteinPercent < NUTRITION_RULES.protein.min) {
      warnings.push('蛋白质摄入偏低,建议增加');
    }
    if (carbsPercent < NUTRITION_RULES.carbs.min) {
      warnings.push('碳水化合物摄入偏低');
    }

    // 3. 验证每餐分布
    const mealDistribution = this.validateMealDistribution(plan.meals);
    warnings.push(...mealDistribution);

    // 4. 检查过敏原(需要在调用方传入过敏原列表)

    return {
      isValid: errors.length === 0,
      warnings,
      errors,
    };
  }

  private validateMealDistribution(meals: Meal[]): string[] {
    const warnings: string[] = [];
    const mealTypes = new Map<string, Meal[]>();

    meals.forEach(meal => {
      if (!mealTypes.has(meal.type)) {
        mealTypes.set(meal.type, []);
      }
      mealTypes.get(meal.type)!.push(meal);
    });

    // 检查是否有早餐
    if (!mealTypes.has('breakfast')) {
      warnings.push('建议包含早餐以维持能量水平');
    }

    // 检查晚餐是否过晚(假设晚上 9 点后)
    // 这里可以添加更复杂的逻辑

    return warnings;
  }

  // 检查食材中是否包含过敏原
  checkAllergens(meal: Meal, allergens: string[]): string[] {
    const foundAllergens: string[] = [];
    const lowerAllergens = allergens.map(a => a.toLowerCase());

    meal.ingredients.forEach(ingredient => {
      const item = ingredient.item.toLowerCase();
      lowerAllergens.forEach(allergen => {
        if (item.includes(allergen)) {
          foundAllergens.push(allergen);
        }
      });
    });

    return foundAllergens;
  }
}

export const nutritionValidator = new NutritionValidator();
Code collapsed

步骤 5:创建 API 路由

生成膳食计划

code
// app/api/meal-plans/generate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { mealPlannerAI } from '@/lib/openai';
import { nutritionValidator } from '@/lib/nutrition-validator';
import { supabase } from '@/lib/supabase';

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { userId, date } = body;

    // 1. 获取用户偏好
    const { data: preferences } = await supabase
      .from('user_preferences')
      .select('*')
      .eq('user_id', userId)
      .single();

    if (!preferences) {
      return NextResponse.json(
        { error: 'User preferences not found' },
        { status: 404 }
      );
    }

    // 2. 获取用户历史(用于个性化)
    const { data: history } = await supabase
      .from('meal_history')
      .select('*')
      .eq('user_id', userId)
      .order('meal_date', { ascending: false })
      .limit(20);

    // 3. 生成膳食计划
    const plan = await mealPlannerAI.generateMealPlan({
      healthGoals: preferences.health_goals,
      dietType: preferences.diet_type,
      allergies: preferences.allergies,
      dailyCalories: preferences.daily_calories,
      mealsPerDay: preferences.meals_per_day,
      cookingTime: preferences.cooking_time_minutes,
      date,
    });

    // 4. 验证营养合理性
    const validation = nutritionValidator.validate(
      plan,
      preferences.daily_calories
    );

    if (!validation.isValid) {
      return NextResponse.json(
        { error: 'Generated meal plan failed validation', details: validation.errors },
        { status: 400 }
      );
    }

    // 5. 保存到数据库
    const { data: savedPlan, error } = await supabase
      .from('meal_plans')
      .insert({
        user_id: userId,
        plan_date: date,
        meals: plan.meals,
        ai_model: process.env.OPENAI_MODEL,
      })
      .select()
      .single();

    if (error) {
      console.error('Failed to save meal plan:', error);
    }

    return NextResponse.json({
      plan,
      validation,
      savedPlanId: savedPlan?.id,
    });

  } catch (error) {
    console.error('Meal plan generation error:', error);
    return NextResponse.json(
      { error: 'Failed to generate meal plan' },
      { status: 500 }
    );
  }
}
Code collapsed

流式生成端点

code
// app/api/meal-plans/generate-stream/route.ts
import { NextRequest } from 'next/server';
import { mealPlannerAI } from '@/lib/openai';

export async function POST(request: NextRequest) {
  const body = await request.json();

  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      await mealPlannerAI.generateStreamingMealPlan(
        body,
        (chunk) => {
          controller.enqueue(encoder.encode(`data: ${JSON.stringify({ chunk })}\n\n`));
        },
        (plan) => {
          controller.enqueue(encoder.encode(`data: ${JSON.stringify({ done: true, plan })}\n\n`));
          controller.close();
        },
        (error) => {
          controller.enqueue(encoder.encode(`data: ${JSON.stringify({ error: error.message })}\n\n`));
          controller.close();
        }
      );
    },
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    },
  });
}
Code collapsed

步骤 6:前端组件实现

膳食计划生成器组件

code
// components/MealPlanGenerator.tsx
'use client';

import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';

interface GenerateParams {
  userId: string;
  date: string;
}

export function MealPlanGenerator() {
  const [date, setDate] = useState(new Date().toISOString().split('T')[0]);
  const [generatedPlan, setGeneratedPlan] = useState<any>(null);

  const generateMutation = useMutation({
    mutationFn: async (params: GenerateParams) => {
      const res = await fetch('/api/meal-plans/generate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(params),
      });
      if (!res.ok) throw new Error('Generation failed');
      return res.json();
    },
    onSuccess: (data) => {
      setGeneratedPlan(data.plan);
    },
  });

  const handleGenerate = () => {
    generateMutation.mutate({ userId: 'current-user-id', date });
  };

  return (
    <div className: "max-w-4xl mx-auto p-6">
      <div className: "mb-6">
        <label className: "block text-sm font-medium text-gray-700 mb-2">
          选择日期
        </label>
        <input
          type: "date"
          value={date}
          onChange={(e) => setDate(e.target.value)}
          className: "border rounded-lg px-4 py-2 w-full max-w-xs"
        />
        <button
          onClick={handleGenerate}
          disabled={generateMutation.isPending}
          className: "ml-4 bg-blue-600 text-white px-6 py-2 rounded-lg disabled:opacity-50"
        >
          {generateMutation.isPending ? '生成中...' : '生成膳食计划'}
        </button>
      </div>

      {generateMutation.error && (
        <div className: "bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
          生成失败: {(generateMutation.error as Error).message}
        </div>
      )}

      {generatedPlan && (
        <MealPlanDisplay plan={generatedPlan} />
      )}
    </div>
  );
}

function MealPlanDisplay({ plan }: { plan: any }) {
  return (
    <div className: "space-y-6">
      {/* 每日总计 */}
      <div className: "bg-gray-50 rounded-lg p-6">
        <h3 className: "text-lg font-semibold mb-4">每日营养总计</h3>
        <div className: "grid grid-cols-2 md:grid-cols-5 gap-4">
          <NutrientCard label: "卡路里" value={`${plan.dailyTotals.calories} kcal`} />
          <NutrientCard label: "蛋白质" value={`${plan.dailyTotals.protein}g`} />
          <NutrientCard label: "碳水" value={`${plan.dailyTotals.carbs}g`} />
          <NutrientCard label: "脂肪" value={`${plan.dailyTotals.fats}g`} />
          <NutrientCard label: "纤维" value={`${plan.dailyTotals.fiber || 0}g`} />
        </div>
      </div>

      {/* 每餐详情 */}
      {plan.meals.map((meal: any, index: number) => (
        <MealCard key={index} meal={meal} />
      ))}

      {plan.notes && (
        <div className: "bg-blue-50 border-l-4 border-blue-400 p-4">
          <p className: "text-blue-800">{plan.notes}</p>
        </div>
      )}
    </div>
  );
}

function NutrientCard({ label, value }: { label: string; value: string }) {
  return (
    <div className: "text-center">
      <p className: "text-sm text-gray-600">{label}</p>
      <p className: "text-xl font-bold text-gray-900">{value}</p>
    </div>
  );
}

function MealCard({ meal }: { meal: any }) {
  return (
    <div className: "bg-white border rounded-lg shadow-sm overflow-hidden">
      <div className: "bg-gray-50 px-6 py-4 border-b">
        <div className: "flex justify-between items-center">
          <h4 className: "text-lg font-semibold capitalize">{meal.type}</h4>
          <span className: "text-sm text-gray-500">
            {meal.prepTime + meal.cookTime} 分钟
          </span>
        </div>
        <h5 className: "text-xl font-medium text-gray-900 mt-2">{meal.name}</h5>
        {meal.description && (
          <p className: "text-gray-600 mt-1">{meal.description}</p>
        )}
      </div>

      <div className: "p-6">
        <div className: "grid grid-cols-4 gap-4 mb-4">
          <NutrientMini label: "卡路里" value={`${meal.nutrition.calories}`} />
          <NutrientMini label: "蛋白质" value={`${meal.nutrition.protein}g`} />
          <NutrientMini label: "碳水" value={`${meal.nutrition.carbs}g`} />
          <NutrientMini label: "脂肪" value={`${meal.nutrition.fats}g`} />
        </div>

        <div className: "mt-6">
          <h6 className: "font-medium text-gray-900 mb-3">食材</h6>
          <ul className: "space-y-2">
            {meal.ingredients.map((ing: any, i: number) => (
              <li key={i} className: "flex justify-between text-sm">
                <span>{ing.item}</span>
                <span className: "text-gray-500">{ing.amount}</span>
              </li>
            ))}
          </ul>
        </div>

        <div className: "mt-6">
          <h6 className: "font-medium text-gray-900 mb-3">烹饪说明</h6>
          <ol className: "space-y-2">
            {meal.instructions.map((step: string, i: number) => (
              <li key={i} className: "flex">
                <span className: "flex-shrink-0 w-6 h-6 bg-blue-100 text-blue-800 rounded-full flex items-center justify-center text-sm mr-3">
                  {i + 1}
                </span>
                <span className: "text-gray-700">{step}</span>
              </li>
            ))}
          </ol>
        </div>
      </div>
    </div>
  );
}

function NutrientMini({ label, value }: { label: string; value: string }) {
  return (
    <div className: "text-center">
      <p className: "text-xs text-gray-500">{label}</p>
      <p className: "text-sm font-semibold">{value}</p>
    </div>
  );
}
Code collapsed

步骤 7:添加 RAG 增强功能

code
// lib/rag-enhanced-meal-planner.ts

import { mealPlannerAI } from './openai';
import { supabase } from './supabase';

export class RAGEnhancedMealPlanner {
  async generateWithContext(params: any, userId: string) {
    // 1. 获取相关营养知识
    const relevantKnowledge = await this.retrieveRelevantKnowledge(
      params.healthGoals
    );

    // 2. 获取用户历史偏好
    const userPreferences = await this.getUserLearnedPreferences(userId);

    // 3. 构建增强提示
    const enhancedPrompt = this.buildEnhancedPrompt({
      ...params,
      knowledge: relevantKnowledge,
      userPreferences,
    });

    // 4. 生成计划
    return mealPlannerAI.generateMealPlan({ ...params, prompt: enhancedPrompt });
  }

  private async retrieveRelevantKnowledge(goals: string[]) {
    // 使用向量相似度搜索相关营养知识
    const { data } = await supabase.rpc('match_nutrition_knowledge', {
      query_goals: goals,
      match_threshold: 0.7,
      match_count: 3,
    });

    return data || [];
  }

  private async getUserLearnedPreferences(userId: string) {
    // 分析用户历史评分,找出偏好模式
    const { data } = await supabase
      .from('meal_history')
      .select('*')
      .eq('user_id', userId)
      .gte('rating', 4);

    // 这里可以实现更复杂的模式识别
    return {
      preferredCuisines: this.extractCuisines(data),
      preferredIngredients: this.extractIngredients(data),
      avoidedMeals: this.extractAvoided(data),
    };
  }

  private extractCuisines(history: any[]) {
    // 实现: 从历史记录中提取偏好菜系
    return [];
  }

  private extractIngredients(history: any[]) {
    // 实现: 从高分膳食中提取常用食材
    return [];
  }

  private extractAvoided(history: any[]) {
    // 实现: 从低分膳食中提取应避免的元素
    return [];
  }

  private buildEnhancedPrompt(params: any): string {
    let prompt = `基于以下营养建议,创建膳食计划:\n\n`;

    if (params.knowledge?.length) {
      prompt += '营养知识:\n';
      params.knowledge.forEach((k: any) => {
        prompt += `- ${k.title}: ${k.content}\n`;
      });
      prompt += '\n';
    }

    if (params.userPreferences) {
      prompt += `用户偏好(基于历史学习):\n`;
      prompt += `- 偏好菜系: ${params.userPreferences.preferredCuisines.join(', ') || '无特别偏好'}\n`;
      prompt += `- 喜欢的食材: ${params.userPreferences.preferredIngredients.join(', ') || '无'}\n`;
    }

    return prompt;
  }
}

export const ragEnhancedMealPlanner = new RAGEnhancedMealPlanner();
Code collapsed

总结

通过本教程,你学会了如何构建一个完整的 AI 膳食规划应用:

  1. 使用 OpenAI GPT-4 生成个性化膳食计划
  2. 实现 JSON Schema 验证确保输出质量
  3. 添加营养合理性验证
  4. 使用 PostgreSQL 存储和检索数据
  5. 实现 RAG 增强以提供更个性化的建议

扩展方向

  • 添加购物清单生成功能
  • 实现膳食计划分享和社交功能
  • 集成食谱数据库 API 获取更多选项
  • 添加照片识别功能记录实际膳食

参考资料

常见问题

Q: 如何降低 OpenAI API 成本?

A: 可以使用 GPT-3.5-turbo 进行初步生成,然后用 GPT-4 进行验证。或者实现缓存机制,避免重复生成相似的计划。

Q: 如何处理用户反馈和改进建议?

A: 实现评分系统和反馈收集,使用这些数据微调提示词,或考虑进行模型微调。

Q: 营养验证规则的准确性如何保证?

A: 应该参考权威营养学指南(如 USDA 营养数据库),并考虑与注册营养师合作验证规则。

相关文章

#

文章标签

nextjs
openai
ai
nutrition
postgresql

觉得这篇文章有帮助?

立即体验康心伴,开始您的健康管理之旅