هوش مصنوعی با Node.js؛ آموزش ساخت اپلیکیشن AI با Express و API درواره

در این آموزش عملی، با Node.js و Express یک Backend هوش مصنوعی می‌سازیم، آن را به API درواره متصل می‌کنیم و قابلیت‌های چت، Streaming، خروجی JSON، اعتبارسنجی و مدیریت خطا را پیاده‌سازی می‌کنیم.

Share
هوش مصنوعی با Node.js؛ آموزش ساخت اپلیکیشن AI با Express و API درواره

Node.js یکی از محبوب‌ترین محیط‌ها برای ساخت Backend، REST API، اپلیکیشن‌های بلادرنگ و سرویس‌های تحت وب است. اگر با JavaScript یا TypeScript کار می‌کنید، می‌توانید بدون تغییر زبان برنامه‌نویسی، قابلیت‌های هوش مصنوعی را نیز به Backend خود اضافه کنید.

ترکیب Node.js با API مدل‌های هوش مصنوعی برای ساخت این محصولات مناسب است:

  • چت‌بات پشتیبانی
  • دستیار هوشمند داخل وب‌سایت
  • ابزار تولید محتوا
  • خلاصه‌ساز متن
  • تحلیل بازخورد مشتری
  • تولید توضیحات محصول
  • استخراج داده از متن
  • دستیار برنامه‌نویسی
  • سرویس ترجمه
  • سیستم پرسش‌وپاسخ
  • ایجنت هوش مصنوعی
  • قابلیت هوشمند در محصولات SaaS

در این مقاله یک پروژه عملی و قابل توسعه می‌سازیم که از Node.js، Express و API هوش مصنوعی درواره استفاده می‌کند.

قابلیت‌های نهایی پروژه:

  • REST API استاندارد
  • اتصال به API سازگار با OpenAI درواره
  • دریافت پاسخ متنی
  • پشتیبانی از تاریخچه مکالمه
  • Streaming پاسخ
  • دریافت خروجی JSON
  • اعتبارسنجی ورودی و خروجی
  • مدیریت خطاهای رایج
  • Timeout و Retry کنترل‌شده
  • ساختار مناسب برای توسعه
  • امکان اتصال به React، Vue، Flutter یا اپلیکیشن موبایل

Node.js چیست و چرا برای هوش مصنوعی مناسب است؟

Node.js یک محیط اجرای متن‌باز و چندسکویی برای اجرای JavaScript خارج از مرورگر است. این محیط از موتور V8 استفاده می‌کند و برای عملیات ورودی و خروجی غیرمسدودکننده مانند درخواست شبکه، دیتابیس و فایل مناسب است. معرفی رسمی Node.js

در یک اپلیکیشن مبتنی بر API هوش مصنوعی، Backend بیشتر زمان خود را صرف انتظار برای این عملیات می‌کند:

  • دریافت درخواست کاربر
  • فراخوانی API مدل
  • دریافت Stream
  • خواندن یا ذخیره‌سازی تاریخچه
  • ارتباط با Redis یا PostgreSQL
  • ارسال نتیجه به Client

مدل غیرمسدودکننده Node.js برای چنین Workflowهایی مناسب است.

مزایای مهم Node.js:

  • استفاده از JavaScript در Frontend و Backend
  • اکوسیستم گسترده npm
  • پشتیبانی مناسب از Promise و async/await
  • پیاده‌سازی ساده REST API
  • پشتیبانی از Stream
  • مناسب برای WebSocket و SSE
  • امکان استفاده از TypeScript
  • استقرار ساده روی سرور و Container
  • کتابخانه‌های متعدد برای دیتابیس، Queue و Cache

Express چیست؟

Express فریم‌ورکی سبک برای ساخت وب‌سرور و API روی Node.js است. با Express می‌توانید Route، Middleware، Error Handler و پاسخ HTTP را مدیریت کنید.

طبق راهنمای رسمی Express، پس از ساخت پروژه با npm init می‌توان Express را با دستور زیر نصب کرد: راهنمای نصب Express

npm install express

در این مقاله از Express برای ایجاد Endpointهای زیر استفاده می‌کنیم:

GET  /health
POST /api/chat
POST /api/chat/stream
POST /api/analyze-feedback

تفاوت استفاده از ChatGPT و API در Node.js

ChatGPT یک محصول آماده برای تعامل مستقیم کاربر است. API هوش مصنوعی برای اتصال نرم‌افزار به مدل طراحی شده است.

ویژگیChatGPTAPI در Node.js
استفاده مستقیم کاربربلهاز طریق اپلیکیشن شما
رابط اختصاصیمحدودکاملاً قابل طراحی
اتصال به دیتابیسمستقیم نیستبله
کنترل System Promptمحدودتربله
مدیریت کاربرانخارج از اختیار برنامهدر Backend شما
ثبت مصرفمحدودقابل پیاده‌سازی
انتخاب مدلدر محیط محصولبا Model ID
ساخت Workflowمحدودکاملاً برنامه‌پذیر
خروجی JSONبرای استفاده دستیقابل اعتبارسنجی
اتصال به CRM و ERPمحدودبله

اگر هدف شما استفاده شخصی است، یک ابزار گفت‌وگویی کافی است. اگر می‌خواهید هوش مصنوعی بخشی از سایت، اپلیکیشن یا محصول شما باشد، به API نیاز دارید.

معماری پروژه

جریان درخواست به این شکل خواهد بود:

مرورگر یا اپلیکیشن
        ↓
Express REST API
        ↓
اعتبارسنجی ورودی
        ↓
سرویس هوش مصنوعی
        ↓
API درواره
        ↓
مدل هوش مصنوعی
        ↓
پاسخ Express
        ↓
مرورگر یا اپلیکیشن

کلید API درواره فقط در Backend قرار می‌گیرد. مرورگر و اپلیکیشن موبایل نباید به کلید اصلی دسترسی داشته باشند.

پیش‌نیازها

برای ادامه آموزش به این موارد نیاز دارید:

  • Node.js
  • npm
  • ویرایشگر کد مانند VS Code
  • آشنایی مقدماتی با JavaScript
  • حساب درواره
  • API Key درواره
  • Model ID یکی از مدل‌های متنی

برای ایجاد حساب و دریافت کلید به درواره مراجعه کنید. فهرست مدل‌ها، شناسه و قیمت به‌روز آن‌ها در صفحه مدل‌های درواره قرار دارد.

در نمونه‌کدها از Placeholderهای زیر استفاده می‌کنیم:

YOUR_DARVAREH_API_KEY
YOUR_MODEL_ID

آن‌ها را با کلید و Model ID واقعی جایگزین کنید.

بررسی نصب Node.js

نسخه Node.js:

node --version

نسخه npm:

npm --version

بهتر است از یکی از نسخه‌های LTS پشتیبانی‌شده Node.js استفاده کنید.

ساخت پروژه

پوشه پروژه را بسازید:

mkdir nodejs-ai-api
cd nodejs-ai-api

فایل package.json را ایجاد کنید:

npm init -y

وابستگی‌های اصلی:

npm install express openai dotenv cors zod

وابستگی Development:

npm install --save-dev nodemon

کاربرد بسته‌ها:

Packageکاربرد
expressساخت REST API
openaiاستفاده از SDK سازگار با API
dotenvخواندن متغیرهای محیطی
corsتنظیم دسترسی Frontend
zodاعتبارسنجی ورودی و خروجی
nodemonاجرای مجدد خودکار در Development

کتابخانه رسمی JavaScript و TypeScript امکان ساخت Client و ارسال درخواست به API را فراهم می‌کند. مخزن رسمی OpenAI JavaScript SDK

تنظیم ES Modules

فایل package.json را به این شکل ویرایش کنید:

{
  "name": "nodejs-ai-api",
  "version": "1.0.0",
  "description": "AI API with Node.js, Express and Darvareh",
  "type": "module",
  "main": "src/app.js",
  "scripts": {
    "dev": "nodemon src/app.js",
    "start": "node src/app.js",
    "test": "node --test"
  },
  "engines": {
    "node": ">=20"
  },
  "dependencies": {
    "cors": "^2",
    "dotenv": "^16",
    "express": "^5",
    "openai": "^5",
    "zod": "^3"
  },
  "devDependencies": {
    "nodemon": "^3"
  }
}

شماره نسخه‌های دقیق ثبت‌شده توسط npm ممکن است متفاوت باشد. پس از نصب، فایل package-lock.json را در Repository نگه دارید.

ساختار پروژه

ساختار پیشنهادی:

nodejs-ai-api/
├── src/
│   ├── ai/
│   │   ├── client.js
│   │   └── chat.service.js
│   ├── config/
│   │   └── env.js
│   ├── middleware/
│   │   └── error-handler.js
│   ├── routes/
│   │   ├── chat.routes.js
│   │   └── analysis.routes.js
│   └── app.js
├── test/
│   └── chat.service.test.js
├── .env
├── .env.example
├── .gitignore
├── package.json
└── package-lock.json

این ساختار باعث می‌شود Route، تنظیمات، Client و منطق تعامل با مدل در فایل‌های جداگانه قرار بگیرند.

ساخت فایل متغیرهای محیطی

فایل .env:

PORT=3000
NODE_ENV=development

DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL=YOUR_MODEL_ID
DARVAREH_BASE_URL=https://api.darvareh.ir/v1

AI_TIMEOUT_MS=30000
AI_MAX_RETRIES=2

فایل .env.example:

PORT=3000
NODE_ENV=development

DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL=YOUR_MODEL_ID
DARVAREH_BASE_URL=https://api.darvareh.ir/v1

AI_TIMEOUT_MS=30000
AI_MAX_RETRIES=2

فایل .gitignore:

node_modules/
.env
coverage/
npm-debug.log*
.DS_Store

کلید API را در GitHub، کد Frontend یا فایل قابل دانلود قرار ندهید.

اعتبارسنجی تنظیمات برنامه

فایل:

src/config/env.js

کد:

import "dotenv/config";
import { z } from "zod";

const envSchema = z.object({
  NODE_ENV: z
    .enum(["development", "test", "production"])
    .default("development"),

  PORT: z.coerce
    .number()
    .int()
    .positive()
    .default(3000),

  DARVAREH_API_KEY: z
    .string()
    .min(1),

  DARVAREH_MODEL: z
    .string()
    .min(1),

  DARVAREH_BASE_URL: z
    .string()
    .url()
    .default("https://api.darvareh.ir/v1"),

  AI_TIMEOUT_MS: z.coerce
    .number()
    .int()
    .positive()
    .default(30000),

  AI_MAX_RETRIES: z.coerce
    .number()
    .int()
    .min(0)
    .max(5)
    .default(2),
});

const parsed = envSchema.safeParse(process.env);

if (!parsed.success) {
  console.error(
    "Invalid environment configuration:",
    parsed.error.flatten().fieldErrors,
  );

  process.exit(1);
}

export const env = parsed.data;

با این کار، برنامه در زمان شروع متوجه تنظیمات ناقص یا نامعتبر می‌شود.

ساخت Client اتصال به درواره

فایل:

src/ai/client.js

کد:

import OpenAI from "openai";

import { env } from "../config/env.js";

export const aiClient = new OpenAI({
  apiKey: env.DARVAREH_API_KEY,
  baseURL: env.DARVAREH_BASE_URL,
  timeout: env.AI_TIMEOUT_MS,
  maxRetries: env.AI_MAX_RETRIES,
});

آدرس پایه درواره:

https://api.darvareh.ir/v1

مدل در درخواست مشخص خواهد شد:

model: env.DARVAREH_MODEL

ساخت سرویس Chat

فایل:

src/ai/chat.service.js

کد:

import { aiClient } from "./client.js";
import { env } from "../config/env.js";

const defaultSystemPrompt = `
تو یک دستیار فارسی دقیق و کاربردی هستی.
پاسخ را روشن و متناسب با پرسش کاربر بنویس.
اطلاعاتی را که در اختیار نداری حدس نزن.
اگر سؤال مبهم است، ابهام را اعلام کن.
`.trim();

export async function generateChatResponse({
  messages,
  systemPrompt = defaultSystemPrompt,
  temperature = 0.3,
  maxTokens = 1000,
}) {
  const response =
    await aiClient.chat.completions.create({
      model: env.DARVAREH_MODEL,
      messages: [
        {
          role: "system",
          content: systemPrompt,
        },
        ...messages,
      ],
      temperature,
      max_tokens: maxTokens,
    });

  const answer =
    response.choices[0]?.message?.content;

  if (!answer || !answer.trim()) {
    const error = new Error(
      "مدل پاسخ قابل استفاده‌ای تولید نکرد.",
    );

    error.statusCode = 502;
    throw error;
  }

  return {
    answer: answer.trim(),
    model: env.DARVAREH_MODEL,
    usage: response.usage ?? null,
  };
}


export async function createChatStream({
  messages,
  systemPrompt = defaultSystemPrompt,
  temperature = 0.3,
  maxTokens = 1000,
}) {
  return aiClient.chat.completions.create({
    model: env.DARVAREH_MODEL,
    messages: [
      {
        role: "system",
        content: systemPrompt,
      },
      ...messages,
    ],
    temperature,
    max_tokens: maxTokens,
    stream: true,
  });
}

طراحی Schema پیام‌ها

هر پیام از دو فیلد تشکیل می‌شود:

{
  "role": "user",
  "content": "Node.js چیست؟"
}

نقش‌های قابل قبول برای Endpoint عمومی پروژه:

  • user
  • assistant

پیام system را از Client نمی‌پذیریم. System Prompt در Backend مدیریت می‌شود.

ساخت Route چت معمولی

فایل:

src/routes/chat.routes.js

کد:

import { Router } from "express";
import { z } from "zod";

import {
  createChatStream,
  generateChatResponse,
} from "../ai/chat.service.js";

export const chatRouter = Router();

const messageSchema = z.object({
  role: z.enum([
    "user",
    "assistant",
  ]),
  content: z
    .string()
    .trim()
    .min(1)
    .max(10000),
});

const chatRequestSchema = z.object({
  messages: z
    .array(messageSchema)
    .min(1)
    .max(30),

  temperature: z
    .number()
    .min(0)
    .max(2)
    .optional(),

  maxTokens: z
    .number()
    .int()
    .min(1)
    .max(4000)
    .optional(),
});


chatRouter.post(
  "/",
  async (request, response, next) => {
    try {
      const parsed =
        chatRequestSchema.safeParse(
          request.body,
        );

      if (!parsed.success) {
        return response.status(422).json({
          error: "validation_error",
          message:
            "ساختار درخواست معتبر نیست.",
          details:
            parsed.error.flatten().fieldErrors,
        });
      }

      const result =
        await generateChatResponse({
          messages:
            parsed.data.messages,
          temperature:
            parsed.data.temperature,
          maxTokens:
            parsed.data.maxTokens,
        });

      return response.json({
        data: result,
      });
    } catch (error) {
      next(error);
    }
  },
);

نمونه Request:

{
  "messages": [
    {
      "role": "user",
      "content": "سه کاربرد Node.js در پروژه‌های هوش مصنوعی را توضیح بده."
    }
  ],
  "temperature": 0.3,
  "maxTokens": 800
}

نمونه Response:

{
  "data": {
    "answer": "Node.js برای ساخت Backend، پردازش درخواست‌های هم‌زمان و پیاده‌سازی Streaming کاربرد دارد.",
    "model": "YOUR_MODEL_ID",
    "usage": {
      "prompt_tokens": 40,
      "completion_tokens": 35,
      "total_tokens": 75
    }
  }
}

وجود و شکل دقیق usage می‌تواند به مدل و Endpoint وابسته باشد.

مدیریت تاریخچه مکالمه

برای مکالمه چندمرحله‌ای، پیام‌های قبلی را در آرایه messages قرار دهید:

{
  "messages": [
    {
      "role": "user",
      "content": "Express چیست؟"
    },
    {
      "role": "assistant",
      "content": "Express یک فریم‌ورک وب برای Node.js است."
    },
    {
      "role": "user",
      "content": "چه زمانی از آن استفاده کنم؟"
    }
  ]
}

در یک محصول واقعی بهتر است Client تمام تاریخچه را بدون کنترل ارسال نکند. Backend می‌تواند conversationId دریافت و تاریخچه را از دیتابیس بازیابی کند.

ساختار پیشنهادی:

Conversation
- id
- user_id
- title
- created_at
- updated_at

Message
- id
- conversation_id
- role
- content
- created_at

ساخت Streaming API

Streaming باعث می‌شود پاسخ مدل به‌تدریج برای کاربر ارسال شود.

کد Route را در همان فایل chat.routes.js اضافه کنید:

chatRouter.post(
  "/stream",
  async (request, response, next) => {
    let headersSent = false;

    try {
      const parsed =
        chatRequestSchema.safeParse(
          request.body,
        );

      if (!parsed.success) {
        return response.status(422).json({
          error: "validation_error",
          message:
            "ساختار درخواست معتبر نیست.",
          details:
            parsed.error.flatten().fieldErrors,
        });
      }

      const stream =
        await createChatStream({
          messages:
            parsed.data.messages,
          temperature:
            parsed.data.temperature,
          maxTokens:
            parsed.data.maxTokens,
        });

      response.status(200);
      response.setHeader(
        "Content-Type",
        "text/plain; charset=utf-8",
      );
      response.setHeader(
        "Cache-Control",
        "no-cache, no-transform",
      );
      response.setHeader(
        "Connection",
        "keep-alive",
      );
      response.setHeader(
        "X-Content-Type-Options",
        "nosniff",
      );

      response.flushHeaders();
      headersSent = true;

      for await (const chunk of stream) {
        const content =
          chunk.choices[0]?.delta?.content;

        if (content) {
          response.write(content);
        }
      }

      response.end();
    } catch (error) {
      if (headersSent) {
        response.end();
        return;
      }

      next(error);
    }
  },
);

Client می‌تواند متن را به‌صورت Stream بخواند و در رابط چت نمایش دهد.

آزمایش Streaming با cURL

curl -N \
  -X POST \
  http://localhost:3000/api/chat/stream \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {
        "role": "user",
        "content": "Node.js را در پنج بند معرفی کن."
      }
    ]
  }'

گزینه -N بافر داخلی cURL را غیرفعال می‌کند تا Chunkها سریع‌تر نمایش داده شوند.

دریافت Stream در JavaScript مرورگر

async function streamChat(messages) {
  const response = await fetch(
    "http://localhost:3000/api/chat/stream",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        messages,
      }),
    },
  );

  if (!response.ok || !response.body) {
    throw new Error(
      "Streaming request failed",
    );
  }

  const reader =
    response.body.getReader();

  const decoder =
    new TextDecoder("utf-8");

  let fullText = "";

  while (true) {
    const { value, done } =
      await reader.read();

    if (done) {
      break;
    }

    const chunk = decoder.decode(
      value,
      {
        stream: true,
      },
    );

    fullText += chunk;

    console.log(fullText);
  }

  return fullText;
}

در React می‌توانید fullText را پس از هر Chunk در State قرار دهید.

خروجی JSON با Node.js

در بسیاری از پروژه‌ها پاسخ متنی کافی نیست. ممکن است بخواهید بازخورد مشتری را به این ساختار تبدیل کنید:

{
  "sentiment": "negative",
  "category": "delivery",
  "priority": "high",
  "summary": "سفارش با تأخیر تحویل شده است.",
  "requiresHuman": true
}

برای دریافت نتیجه قابل استفاده باید:

  1. ساختار را دقیق تعریف کنید.
  2. از مدل بخواهید فقط JSON برگرداند.
  3. متن را Parse کنید.
  4. خروجی را با Schema اعتبارسنجی کنید.
  5. خطای JSON را مدیریت کنید.
  6. برای مدل‌های پشتیبانی‌شده از Structured Outputs استفاده کنید.

ساخت Route تحلیل بازخورد

فایل:

src/routes/analysis.routes.js

کد:

import { Router } from "express";
import { z } from "zod";

import { aiClient }
  from "../ai/client.js";
import { env }
  from "../config/env.js";

export const analysisRouter = Router();

const feedbackRequestSchema = z.object({
  text: z
    .string()
    .trim()
    .min(3)
    .max(10000),
});

const feedbackResultSchema = z.object({
  sentiment: z.enum([
    "positive",
    "neutral",
    "negative",
  ]),

  category: z.enum([
    "product",
    "delivery",
    "support",
    "billing",
    "other",
  ]),

  priority: z.enum([
    "low",
    "medium",
    "high",
  ]),

  summary: z
    .string()
    .min(1)
    .max(500),

  requiresHuman: z.boolean(),
});


function cleanJsonText(text) {
  return text
    .trim()
    .replace(/^```json\s*/i, "")
    .replace(/^```\s*/i, "")
    .replace(/\s*```$/i, "")
    .trim();
}


analysisRouter.post(
  "/feedback",
  async (request, response, next) => {
    try {
      const requestResult =
        feedbackRequestSchema.safeParse(
          request.body,
        );

      if (!requestResult.success) {
        return response.status(422).json({
          error: "validation_error",
          message:
            "متن بازخورد معتبر نیست.",
          details:
            requestResult
              .error
              .flatten()
              .fieldErrors,
        });
      }

      const completion =
        await aiClient
          .chat
          .completions
          .create({
            model: env.DARVAREH_MODEL,
            messages: [
              {
                role: "system",
                content: `
تو یک سیستم تحلیل بازخورد مشتری هستی.

فقط JSON معتبر برگردان.
هیچ توضیح یا Markdown اضافه نکن.

ساختار دقیق:
{
  "sentiment": "positive | neutral | negative",
  "category": "product | delivery | support | billing | other",
  "priority": "low | medium | high",
  "summary": "خلاصه فارسی",
  "requiresHuman": true
}

اگر دسته مشخص نیست، other را انتخاب کن.
اطلاعات ناموجود را حدس نزن.
                `.trim(),
              },
              {
                role: "user",
                content:
                  requestResult.data.text,
              },
            ],
            temperature: 0,
            max_tokens: 400,
          });

      const rawContent =
        completion
          .choices[0]
          ?.message
          ?.content;

      if (!rawContent) {
        const error = new Error(
          "مدل خروجی تولید نکرد.",
        );

        error.statusCode = 502;
        throw error;
      }

      let parsedJson;

      try {
        parsedJson = JSON.parse(
          cleanJsonText(rawContent),
        );
      } catch {
        const error = new Error(
          "خروجی مدل JSON معتبر نیست.",
        );

        error.statusCode = 502;
        throw error;
      }

      const result =
        feedbackResultSchema.safeParse(
          parsedJson,
        );

      if (!result.success) {
        const error = new Error(
          "خروجی مدل با Schema مورد انتظار سازگار نیست.",
        );

        error.statusCode = 502;
        error.details =
          result
            .error
            .flatten()
            .fieldErrors;

        throw error;
      }

      return response.json({
        data: result.data,
      });
    } catch (error) {
      next(error);
    }
  },
);

پشتیبانی از response_format، JSON Schema و حالت Strict به مدل انتخابی وابسته است. پیش از استفاده در Production، قابلیت مدل را در صفحه مدل‌های درواره بررسی و روی داده واقعی آزمایش کنید.

نمونه تحلیل بازخورد

Request:

{
  "text": "سفارشم سه روز دیر رسید و پاسخ پشتیبانی هم خیلی دیر ارسال شد."
}

Response احتمالی:

{
  "data": {
    "sentiment": "negative",
    "category": "delivery",
    "priority": "high",
    "summary": "مشتری از تأخیر سفارش و پاسخ‌گویی کند پشتیبانی ناراضی است.",
    "requiresHuman": true
  }
}

اعتبارسنجی Schema فقط شکل داده را بررسی می‌کند. صحت معنایی خروجی همچنان باید ارزیابی شود.

ساخت Error Handler

فایل:

src/middleware/error-handler.js

کد:

import OpenAI from "openai";

export function errorHandler(
  error,
  request,
  response,
  next,
) {
  if (response.headersSent) {
    return next(error);
  }

  if (
    error instanceof
    OpenAI.AuthenticationError
  ) {
    return response.status(500).json({
      error: "ai_authentication_error",
      message:
        "تنظیمات سرویس هوش مصنوعی نامعتبر است.",
    });
  }

  if (
    error instanceof
    OpenAI.RateLimitError
  ) {
    return response.status(429).json({
      error: "rate_limit_error",
      message:
        "تعداد درخواست‌ها بیش از حد مجاز است.",
    });
  }

  if (
    error instanceof
    OpenAI.APIConnectionError
  ) {
    return response.status(503).json({
      error: "ai_connection_error",
      message:
        "ارتباط با سرویس هوش مصنوعی برقرار نشد.",
    });
  }

  if (
    error instanceof
    OpenAI.APIStatusError
  ) {
    console.error({
      name: error.name,
      status: error.status,
      requestId: error.request_id,
    });

    return response.status(502).json({
      error: "upstream_ai_error",
      message:
        "سرویس هوش مصنوعی پاسخ موفقی نداد.",
    });
  }

  const statusCode =
    Number.isInteger(error.statusCode)
      ? error.statusCode
      : 500;

  if (statusCode >= 500) {
    console.error(error);
  }

  return response.status(statusCode).json({
    error: "request_failed",
    message:
      statusCode >= 500
        ? "پردازش درخواست با خطا مواجه شد."
        : error.message,
    details: error.details,
  });
}

پارامتر next باید در امضای Middleware خطا وجود داشته باشد تا Express آن را Error Handler تشخیص دهد.

ساخت فایل اصلی Express

فایل:

src/app.js

کد:

import cors from "cors";
import express from "express";

import { env }
  from "./config/env.js";
import { analysisRouter }
  from "./routes/analysis.routes.js";
import { chatRouter }
  from "./routes/chat.routes.js";
import { errorHandler }
  from "./middleware/error-handler.js";

const app = express();

app.disable("x-powered-by");

app.use(
  cors({
    origin:
      env.NODE_ENV === "production"
        ? [
            "https://your-app.example",
          ]
        : [
            "http://localhost:5173",
            "http://localhost:3001",
          ],
    methods: [
      "GET",
      "POST",
    ],
    allowedHeaders: [
      "Content-Type",
      "Authorization",
    ],
  }),
);

app.use(
  express.json({
    limit: "100kb",
  }),
);

app.get(
  "/health",
  (request, response) => {
    response.json({
      status: "ok",
      service: "nodejs-ai-api",
      environment: env.NODE_ENV,
    });
  },
);

app.use(
  "/api/chat",
  chatRouter,
);

app.use(
  "/api/analyze",
  analysisRouter,
);

app.use(
  (request, response) => {
    response.status(404).json({
      error: "not_found",
      message:
        "مسیر درخواست‌شده وجود ندارد.",
    });
  },
);

app.use(errorHandler);

app.listen(
  env.PORT,
  () => {
    console.log(
      `Server running on http://localhost:${env.PORT}`,
    );
  },
);

در Production مقدار واقعی Origin فرانت‌اند خود را جایگزین کنید:

https://your-app.example

اجرای پروژه

Development:

npm run dev

Production:

npm start

بررسی سلامت:

curl http://localhost:3000/health

خروجی:

{
  "status": "ok",
  "service": "nodejs-ai-api",
  "environment": "development"
}

آزمایش Chat Endpoint

curl \
  -X POST \
  http://localhost:3000/api/chat \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {
        "role": "user",
        "content": "Express را در سه بند معرفی کن."
      }
    ]
  }'

اتصال React به Backend

نمونه تابع ساده:

export async function sendChatMessage(
  messages,
) {
  const response = await fetch(
    "http://localhost:3000/api/chat",
    {
      method: "POST",
      headers: {
        "Content-Type":
          "application/json",
      },
      body: JSON.stringify({
        messages,
      }),
    },
  );

  const body = await response.json();

  if (!response.ok) {
    throw new Error(
      body.message
        ?? "Request failed",
    );
  }

  return body.data;
}

در Production آدرس API را از متغیر محیطی Frontend دریافت کنید، اما کلید اصلی درواره را در Frontend قرار ندهید.

چرا ورودی را محدود کردیم؟

در Schema چت این محدودیت‌ها وجود دارند:

messages: z
  .array(messageSchema)
  .min(1)
  .max(30)

و:

content: z
  .string()
  .trim()
  .min(1)
  .max(10000)

دلایل:

  • جلوگیری از درخواست خالی
  • کنترل اندازه Context
  • کاهش مصرف ناخواسته
  • جلوگیری از فشار بیش‌ازحد روی Backend
  • پاسخ‌گویی قابل پیش‌بینی‌تر
  • مدیریت بهتر هزینه

در محصول واقعی محدودیت باید براساس کاربرد، مدل و بودجه تنظیم شود.

مدیریت توکن و هزینه

مدل‌های متنی ورودی و خروجی را به واحدهایی به نام توکن (Token) پردازش می‌کنند.

مصرف معمولاً تحت تأثیر این موارد است:

  • طول System Prompt
  • تعداد پیام‌های تاریخچه
  • طول هر پیام
  • Context اضافه‌شده
  • طول پاسخ
  • مدل انتخابی
  • Retry
  • فراخوانی ابزار

برای کاهش مصرف:

  • پیام‌های غیرضروری را حذف کنید.
  • تاریخچه قدیمی را خلاصه کنید.
  • maxTokens را براساس کاربرد محدود کنید.
  • پاسخ تکراری را Cache کنید.
  • مدل مناسب وظیفه انتخاب کنید.
  • برای دسته‌بندی از Prompt کوتاه استفاده کنید.
  • مصرف را به تفکیک کاربر و Feature ثبت کنید.
  • Retry را محدود کنید.
  • درخواست تکراری Frontend را کنترل کنید.

برای قیمت به‌روز مدل‌ها به صفحه مدل‌های درواره مراجعه کنید.

ثبت Usage

در پاسخ غیر Streaming می‌توانید Usage را ذخیره کنید:

const usage = response.usage;

console.log({
  promptTokens:
    usage?.prompt_tokens,
  completionTokens:
    usage?.completion_tokens,
  totalTokens:
    usage?.total_tokens,
});

برای هر درخواست این اطلاعات مفید هستند:

  • userId
  • feature
  • model
  • promptTokens
  • completionTokens
  • totalTokens
  • latencyMs
  • status
  • promptVersion
  • createdAt

API Key و داده حساس کاربران را در Log ثبت نکنید.

Timeout و Retry

در Client تنظیم کردیم:

timeout: env.AI_TIMEOUT_MS,
maxRetries: env.AI_MAX_RETRIES,

Retry فقط برای خطاهای موقت مفید است. خطاهای زیر معمولاً با تکرار حل نمی‌شوند:

  • API Key اشتباه
  • Model ID نامعتبر
  • ورودی نامعتبر
  • اعتبار ناکافی
  • پارامتر پشتیبانی‌نشده

Retry در چند لایه می‌تواند باعث تکرار بیش‌ازحد درخواست شود. اگر SDK، Service و Queue هر سه Retry داشته باشند، تعداد فراخوانی‌ها به‌سرعت افزایش می‌یابد.

لغو درخواست با AbortController

برای درخواست‌هایی که باید قابل لغو باشند، می‌توانید از AbortController استفاده کنید:

const controller =
  new AbortController();

const timeoutId = setTimeout(
  () => controller.abort(),
  30000,
);

try {
  const response =
    await aiClient
      .chat
      .completions
      .create(
        {
          model:
            env.DARVAREH_MODEL,
          messages: [
            {
              role: "user",
              content:
                "Node.js چیست؟",
            },
          ],
        },
        {
          signal:
            controller.signal,
        },
      );

  console.log(response);
} finally {
  clearTimeout(timeoutId);
}

پشتیبانی جزئیات Optionها را با نسخه SDK نصب‌شده بررسی کنید.

ذخیره تاریخچه در PostgreSQL

برای محصول واقعی می‌توانید از PostgreSQL استفاده کنید.

جداول ساده:

CREATE TABLE conversations (
  id UUID PRIMARY KEY,
  user_id UUID NOT NULL,
  title VARCHAR(200),
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE messages (
  id UUID PRIMARY KEY,
  conversation_id UUID NOT NULL
    REFERENCES conversations(id)
    ON DELETE CASCADE,
  role VARCHAR(20) NOT NULL,
  content TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_messages_conversation_created
ON messages(conversation_id, created_at);

جریان پیشنهادی:

  1. Client یک conversationId می‌فرستد.
  2. Backend مالکیت گفتگو را بررسی می‌کند.
  3. پیام جدید ذخیره می‌شود.
  4. پیام‌های مرتبط اخیر خوانده می‌شوند.
  5. درخواست مدل اجرا می‌شود.
  6. پاسخ دستیار ذخیره می‌شود.
  7. نتیجه برای Client ارسال می‌شود.

Cache پاسخ‌ها با Redis

Cache برای تمام مکالمات مناسب نیست؛ اما در درخواست‌های تکراری می‌تواند مفید باشد:

  • توضیح ثابت محصول
  • تولید FAQ از داده ثابت
  • خلاصه سند بدون تغییر
  • دسته‌بندی محتوای تکراری
  • ترجمه متن یکسان

کلید Cache باید این موارد را در نظر بگیرد:

feature
model
promptVersion
normalizedInput
relevantSettings

تغییر Prompt یا مدل باید Cache Key را تغییر دهد.

مدیریت کارهای طولانی

برای وظایفی که ممکن است زمان زیادی ببرند، Request هم‌زمان مناسب نیست.

نمونه‌ها:

  • تحلیل صدها سند
  • تولید گزارش طولانی
  • پردازش Batch
  • ساخت Embedding برای داده زیاد
  • تحلیل تعداد زیادی بازخورد
  • اجرای Workflow چندمرحله‌ای

معماری پیشنهادی:

Client
  ↓
POST /jobs
  ↓
ساخت Job در دیتابیس
  ↓
افزودن به Queue
  ↓
Worker
  ↓
API درواره
  ↓
ذخیره نتیجه
  ↓
Webhook یا Polling

ابزارهایی مانند Redis و Queueهای Node.js می‌توانند برای اجرای Worker استفاده شوند.

استفاده از TypeScript

برای پروژه‌های تیمی، TypeScript می‌تواند خطاهای نوع را زودتر مشخص کند.

نصب:

npm install --save-dev \
  typescript \
  @types/node \
  @types/express \
  @types/cors

نمونه Type:

type ChatRole =
  | "user"
  | "assistant";

interface ChatMessage {
  role: ChatRole;
  content: string;
}

interface ChatRequest {
  messages: ChatMessage[];
  temperature?: number;
  maxTokens?: number;
}

بااین‌حال، TypeScript جایگزین اعتبارسنجی Runtime نیست. داده ورودی HTTP همچنان باید با Zod یا ابزار مشابه بررسی شود.

تست سرویس بدون فراخوانی مدل واقعی

در Unit Test نباید برای هر اجرا درخواست واقعی و پرهزینه ارسال شود. Client را Inject یا Mock کنید.

نمونه سرویس قابل تست:

export function createChatService({
  client,
  model,
}) {
  return {
    async generate(messages) {
      const response =
        await client
          .chat
          .completions
          .create({
            model,
            messages,
          });

      const content =
        response
          .choices[0]
          ?.message
          ?.content;

      if (!content) {
        throw new Error(
          "Empty model response",
        );
      }

      return content;
    },
  };
}

Fake Client:

const fakeClient = {
  chat: {
    completions: {
      create: async () => ({
        choices: [
          {
            message: {
              content:
                "پاسخ آزمایشی",
            },
          },
        ],
      }),
    },
  },
};

تست با Node Test Runner:

import assert
  from "node:assert/strict";
import test
  from "node:test";

import {
  createChatService,
} from "../src/ai/testable-chat.service.js";

test(
  "returns model content",
  async () => {
    const fakeClient = {
      chat: {
        completions: {
          create: async () => ({
            choices: [
              {
                message: {
                  content:
                    "پاسخ آزمایشی",
                },
              },
            ],
          }),
        },
      },
    };

    const service =
      createChatService({
        client: fakeClient,
        model: "YOUR_MODEL_ID",
      });

    const result =
      await service.generate([
        {
          role: "user",
          content: "سلام",
        },
      ]);

    assert.equal(
      result,
      "پاسخ آزمایشی",
    );
  },
);

سناریوهای مهم تست

Chat Endpoint

  • پیام معتبر
  • آرایه پیام خالی
  • نقش نامعتبر
  • متن خالی
  • تعداد پیام بیش از سقف
  • پاسخ خالی مدل
  • خطای Rate Limit
  • خطای اتصال
  • Timeout
  • Model ID نامعتبر

Structured Output

  • JSON معتبر
  • JSON داخل Markdown
  • JSON ناقص
  • فیلد حذف‌شده
  • مقدار خارج از Enum
  • نوع داده اشتباه
  • پاسخ خالی
  • متن غیر JSON

Streaming

  • دریافت چند Chunk
  • پایان موفق
  • قطع اتصال Client
  • خطا قبل از ارسال Header
  • خطا پس از شروع Stream
  • پاسخ بدون Content
  • لغو درخواست

نکات مهم برای Production

کلید API را فقط در Backend نگه دارید

اشتباه:

const apiKey =
  "کلید واقعی در فایل Frontend";

روش مناسب:

const apiKey =
  process.env.DARVAREH_API_KEY;

System Prompt را سمت سرور مدیریت کنید

اگر Client اجازه تغییر System Prompt داشته باشد، رفتار سرویس از کنترل Backend خارج می‌شود.

برای هر کاربر محدودیت مصرف تعریف کنید

محدودیت‌های پیشنهادی:

  • درخواست در دقیقه
  • توکن در روز
  • بودجه ماهانه
  • حداکثر طول ورودی
  • حداکثر تعداد پیام
  • تعداد Stream هم‌زمان

درخواست‌های تکراری را کنترل کنید

هنگام Loading دکمه ارسال را غیرفعال کنید. برای عملیات مهم می‌توانید شناسه Idempotency در نظر بگیرید.

داده‌ها را قبل از عملیات اعتبارسنجی کنید

خروجی مدل نباید بدون بررسی:

  • وارد دیتابیس شود.
  • Query اجرا کند.
  • ایمیل ارسال کند.
  • وضعیت سفارش را تغییر دهد.
  • تابع خارجی را فراخوانی کند.

مدل را از کد جدا کنید

مدل را در متغیر محیطی قرار دهید:

DARVAREH_MODEL=YOUR_MODEL_ID

در این صورت تغییر مدل به بازنویسی منطق اصلی نیاز ندارد.

کاربردهای واقعی Node.js و هوش مصنوعی

چت‌بات پشتیبانی

Backend پیام کاربر را دریافت و براساس Context محصول پاسخ پیشنهادی ایجاد می‌کند.

تحلیل تیکت

خروجی:

{
  "category": "billing",
  "priority": "high",
  "summary": "کاربر درباره مبلغ صورتحساب اعتراض دارد."
}

تولید توضیحات محصول

ورودی:

  • نام محصول
  • ویژگی‌ها
  • مخاطب
  • لحن

خروجی:

  • عنوان
  • توضیح کوتاه
  • توضیح کامل
  • مزایا
  • پرسش‌های متداول

خلاصه‌سازی جلسه

Backend متن جلسه را دریافت و این موارد را استخراج می‌کند:

  • خلاصه
  • تصمیم‌ها
  • مسئول هر اقدام
  • موعدها
  • سؤالات حل‌نشده

دستیار برنامه‌نویسی

قابلیت‌ها:

  • توضیح کد
  • تولید تست
  • Code Review
  • ساخت مستندات
  • پیشنهاد ریفکتور
  • تحلیل Stack Trace

تحلیل بازخورد

هزاران نظر مشتری را می‌توان به دسته، احساس، اولویت و خلاصه تبدیل کرد.

انتخاب مدل مناسب

کاربردمعیار اصلی
چت فارسیکیفیت فارسی، سرعت و هزینه
دسته‌بندیثبات، سرعت و قیمت
کدنویسیتوانایی Coding
خروجی JSONپشتیبانی ساختاریافته
خلاصه‌سازیContext Window
Reasoningدقت در مسائل چندمرحله‌ای
Tool Callingپشتیبانی از ابزار
اپ پرترافیکLatency، قیمت و پایداری

برای مقایسه مدل‌ها و قیمت، صفحه مدل‌های درواره را بررسی کنید.

اشتباهات رایج

قراردادن API Key در React

هر متغیری که وارد Bundle مرورگر شود قابل مشاهده است. کلید اصلی را فقط در Backend قرار دهید.

ارسال تمام تاریخچه

این کار هزینه و زمان پاسخ را افزایش می‌دهد. تاریخچه را محدود یا خلاصه کنید.

نداشتن Validation

ورودی HTTP و خروجی مدل هر دو باید بررسی شوند.

نداشتن Timeout

Request بدون Timeout ممکن است منابع سرور را بیش‌ازحد درگیر کند.

Retry نامحدود

Retry نامحدود می‌تواند هزینه و ترافیک را افزایش دهد.

اعتماد به JSON مدل

JSON را Parse و با Schema اعتبارسنجی کنید.

استفاده از یک مدل برای تمام وظایف

مدل مناسب دسته‌بندی ممکن است با مدل مناسب برنامه‌نویسی یا تحلیل طولانی متفاوت باشد.

نداشتن نسخه Prompt

برای Prompt شناسه تعریف کنید:

const PROMPT_VERSION =
  "feedback-analysis-v1";

نمایش خطای داخلی به کاربر

جزئیات فنی را در Log کنترل‌شده ثبت کنید و پیام عمومی مناسب به کاربر برگردانید.

پرسش‌های متداول

آیا می‌توان با Node.js اپلیکیشن هوش مصنوعی ساخت؟

بله. Node.js می‌تواند Backend اپلیکیشن را بسازد، درخواست‌ها را به API مدل ارسال کند و قابلیت‌هایی مانند چت، تولید متن، تحلیل، RAG و Agent را مدیریت کند.

چگونه Node.js را به API درواره متصل کنیم؟

یک Client با کلید و Base URL بسازید:

const client = new OpenAI({
  apiKey:
    "YOUR_DARVAREH_API_KEY",
  baseURL:
    "https://api.darvareh.ir/v1",
});

سپس Model ID را در درخواست قرار دهید:

model: "YOUR_MODEL_ID"

Base URL درواره چیست؟

https://api.darvareh.ir/v1

Model ID را از کجا پیدا کنیم؟

شناسه مدل‌ها در صفحه مدل‌های درواره قرار دارد.

آیا می‌توان API Key را در JavaScript مرورگر قرار داد؟

خیر. JavaScript مرورگر برای نگهداری Secret مناسب نیست. درخواست باید از Backend Node.js ارسال شود.

Express برای ساخت API هوش مصنوعی مناسب است؟

بله. Express برای ساخت REST API، Middleware، Streaming و اتصال Frontend به سرویس هوش مصنوعی مناسب است.

آیا بهتر است از JavaScript یا TypeScript استفاده کنیم؟

برای نمونه ساده JavaScript سریع‌تر است. برای پروژه تیمی و بزرگ، TypeScript می‌تواند خوانایی و کنترل نوع بهتری ایجاد کند. هر دو به Validation Runtime نیاز دارند.

Streaming چیست؟

Streaming یعنی پاسخ مدل به‌جای انتظار برای تکمیل کامل، به‌صورت تدریجی برای Client ارسال شود.

آیا Streaming هزینه را کم می‌کند؟

معمولاً Streaming فقط نحوه دریافت پاسخ را تغییر می‌دهد و لزوماً مصرف توکن را کاهش نمی‌دهد.

چگونه خروجی JSON معتبر بگیریم؟

ساختار دقیق تعریف کنید، از قابلیت Structured Output مدل سازگار استفاده کنید و خروجی را در Backend با Schema اعتبارسنجی کنید.

آیا می‌توان تاریخچه چت را در PostgreSQL ذخیره کرد؟

بله. گفتگو و پیام‌ها را در جداول جدا ذخیره و برای هر درخواست فقط Context مرتبط را بازیابی کنید.

چگونه هزینه را کنترل کنیم؟

مدل مناسب انتخاب کنید، طول ورودی و خروجی را محدود کنید، تاریخچه را مدیریت کنید، Cache داشته باشید و مصرف را به تفکیک کاربر ثبت کنید.

آیا می‌توان React یا Flutter را به این Backend متصل کرد؟

بله. هر Client که بتواند درخواست HTTP ارسال کند می‌تواند از Endpointهای Express استفاده کند.

آیا برای هر درخواست باید System Prompt بفرستیم؟

System Prompt معمولاً در Backend تعریف و همراه درخواست مدل ارسال می‌شود. برای کنترل رفتار سرویس بهتر است Client امکان تغییر مستقیم آن را نداشته باشد.

جمع‌بندی

Node.js و Express مسیر مناسبی برای ساخت Backend اپلیکیشن‌های هوش مصنوعی فراهم می‌کنند. اگر از قبل با JavaScript کار می‌کنید، می‌توانید بدون یادگیری یک زبان Backend جدید، مدل‌های هوش مصنوعی را به وب‌سایت، اپلیکیشن یا سرویس سازمانی متصل کنید.

در این مقاله یاد گرفتیم چگونه:

  • پروژه Node.js و Express بسازیم.
  • تنظیمات را با متغیر محیطی مدیریت کنیم.
  • به API سازگار با OpenAI درواره متصل شویم.
  • REST API چت بسازیم.
  • تاریخچه مکالمه را ارسال کنیم.
  • پاسخ را به‌صورت Streaming دریافت کنیم.
  • بازخورد مشتری را به JSON تبدیل کنیم.
  • خروجی را با Zod اعتبارسنجی کنیم.
  • خطا، Timeout و Retry را مدیریت کنیم.
  • Usage و مصرف توکن را ثبت کنیم.
  • پروژه را برای PostgreSQL، Redis و Queue توسعه دهیم.
  • Frontendهای React و Flutter را به Backend متصل کنیم.

برای اجرای پروژه در درواره ثبت‌نام کنید، کلید API بسازید و یک مدل متناسب با کیفیت، سرعت و بودجه پروژه از صفحه مدل‌ها و قیمت‌ها انتخاب کنید.

مقالات مرتبط

برای مطالعه شرایط استفاده و محدودیت‌های مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.

Read more

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی با ترکیب گردش‌کارهای خودکار و مدل‌های هوش مصنوعی، پردازش متن، دسته‌بندی، استخراج اطلاعات و تصمیم‌های پیشنهادی را خودکار می‌کند. در این راهنما، معماری و ساخت نمونه عملی آن با API درواره را می‌آموزید.

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce شیوه‌ای جدید برای خرید اینترنتی است که در آن ایجنت هوش مصنوعی می‌تواند نیاز کاربر را بفهمد، محصولات را جست‌وجو و مقایسه کند و فرایند خرید را پیش ببرد. در این راهنما با معماری، UCP، ACP و پیاده‌سازی آن با API درواره آشنا می‌شوید.