هوش مصنوعی با Java و Spring Boot؛ آموزش ساخت API، چت‌بات و اپلیکیشن AI

در این آموزش عملی با Java و Spring Boot یک Backend هوش مصنوعی می‌سازیم، آن را به API درواره متصل می‌کنیم و چت، Streaming، خروجی JSON، Validation و مدیریت خطا را پیاده‌سازی می‌کنیم.

Share
هوش مصنوعی با Java و Spring Boot؛ آموزش ساخت API، چت‌بات و اپلیکیشن AI

Java و Spring Boot از فناوری‌های پرکاربرد برای ساخت Backend، میکروسرویس، سامانه‌های سازمانی و REST API هستند. بسیاری از بانک‌ها، فروشگاه‌های اینترنتی، شرکت‌های نرم‌افزاری و محصولات سازمانی از اکوسیستم Java استفاده می‌کنند؛ بنابراین اضافه‌کردن قابلیت‌های هوش مصنوعی به Spring Boot می‌تواند مسیر مناسبی برای هوشمندسازی نرم‌افزارهای موجود باشد.

با اتصال یک پروژه Java به API مدل‌های هوش مصنوعی می‌توان قابلیت‌هایی مانند موارد زیر را پیاده‌سازی کرد:

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

در این مقاله، یک پروژه عملی با Java و Spring Boot می‌سازیم که به API هوش مصنوعی درواره متصل می‌شود و قابلیت‌های زیر را ارائه می‌دهد:

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

در پایان، یک Backend قابل توسعه خواهیم داشت که می‌تواند به React، Angular، Vue، Flutter، Android یا هر Client دیگری متصل شود.

Spring Boot چیست؟

Spring Boot فریم‌ورکی برای ساخت سریع‌تر اپلیکیشن‌های مبتنی بر Spring است. این فریم‌ورک با Auto Configuration، سرور داخلی و مدیریت Dependencyها، بخش زیادی از تنظیمات تکراری پروژه را انجام می‌دهد.

براساس راهنمای رسمی Spring، برای شروع می‌توان پروژه را از Spring Initializr ساخت، Dependency موردنیاز را انتخاب و فایل ZIP آماده را دریافت کرد. Spring Boot نیز براساس ماژول‌های موجود در Classpath، بسیاری از Beanها و تنظیمات متداول را به‌صورت خودکار آماده می‌کند. راهنمای رسمی ساخت اپلیکیشن با Spring Boot

Spring Boot برای این پروژه مزایای مهمی دارد:

  • ساخت آسان REST API
  • Dependency Injection
  • Validation ورودی
  • مدیریت تنظیمات محیطی
  • Exception Handling مرکزی
  • پشتیبانی از HTTP Client
  • تست Controller و Service
  • اتصال به PostgreSQL، Redis و Queue
  • قابلیت توسعه به Microservice
  • ابزارهای مانیتورینگ و Health Check

چرا Java برای اپلیکیشن‌های هوش مصنوعی مناسب است؟

اگرچه Python در آموزش و پژوهش هوش مصنوعی بسیار محبوب است، برای استفاده از مدل‌های آماده از طریق API لازم نیست Backend حتماً با Python نوشته شود.

در معماری APIمحور، مدل روی زیرساخت سرویس اجرا می‌شود و اپلیکیشن Java فقط این کارها را انجام می‌دهد:

  1. درخواست کاربر را دریافت می‌کند.
  2. ورودی را اعتبارسنجی می‌کند.
  3. قوانین کسب‌وکار را اجرا می‌کند.
  4. درخواست HTTP را برای مدل می‌فرستد.
  5. پاسخ را دریافت و پردازش می‌کند.
  6. نتیجه را برای Client برمی‌گرداند.

بنابراین Java برای پروژه‌هایی مناسب است که:

  • Backend فعلی آن‌ها Spring Boot است.
  • تیم توسعه Java دارد.
  • به ساختار سازمانی و تایپ قوی نیاز دارند.
  • از PostgreSQL، Kafka، Redis یا سرویس‌های Spring استفاده می‌کنند.
  • قصد دارند قابلیت هوش مصنوعی را به سامانه موجود اضافه کنند.
  • به تست‌پذیری و نگهداری بلندمدت اهمیت می‌دهند.

معماری پروژه

معماری کلی:

Frontend یا اپلیکیشن
        ↓
Spring Boot Controller
        ↓
Validation
        ↓
AI Service
        ↓
WebClient
        ↓
API درواره
        ↓
مدل هوش مصنوعی
        ↓
پاسخ به Spring Boot
        ↓
Frontend یا اپلیکیشن

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

چرا از WebClient استفاده می‌کنیم؟

Spring WebFlux ابزاری به نام WebClient برای ارسال درخواست HTTP ارائه می‌دهد. WebClient API روان و مبتنی بر Reactor دارد، غیرمسدودکننده است و از Streaming پشتیبانی می‌کند. مستندات رسمی Spring WebClient

در این مقاله از WebClient برای این موارد استفاده می‌کنیم:

  • ارسال درخواست Chat Completions
  • تنظیم Authorization Header
  • مدیریت Timeout
  • دریافت پاسخ JSON
  • دریافت Stream
  • مدیریت Status Codeهای ناموفق

استفاده از WebClient به این معنی نیست که کل پروژه باید کاملاً Reactive باشد. می‌توانید در Endpoint معمولی پاسخ نهایی را دریافت کنید و فقط برای Streaming از Flux استفاده کنید.

پیش‌نیازها

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

  • Java 17 یا جدیدتر
  • Maven
  • IntelliJ IDEA، VS Code یا Eclipse
  • آشنایی مقدماتی با Java
  • حساب درواره
  • API Key درواره
  • Model ID یکی از مدل‌های متنی

راهنمای رسمی Spring Boot نیز برای نمونه شروع سریع خود Java 17 یا جدیدتر را ذکر می‌کند.

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

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

YOUR_DARVAREH_API_KEY
YOUR_MODEL_ID

بررسی نصب Java و Maven

نسخه Java:

java --version

نسخه Maven:

mvn --version

اگر هر دو دستور با موفقیت اجرا شدند، می‌توانید پروژه را بسازید.

ساخت پروژه با Spring Initializr

وارد این آدرس شوید:

https://start.spring.io

تنظیمات پیشنهادی:

Project: Maven
Language: Java
Spring Boot: نسخه پایدار فعلی
Packaging: Jar
Java: 17 یا جدیدتر

نام‌ها:

Group: ir.darvareh
Artifact: spring-ai-api
Name: spring-ai-api
Package name: ir.darvareh.ai

Dependencyها:

  • Spring Web
  • Spring Reactive Web
  • Validation
  • Spring Boot Actuator

وجود Spring Web برای Controllerهای MVC و Spring Reactive Web برای WebClient و Streaming استفاده می‌شود.

پس از انتخاب گزینه‌ها، روی Generate کلیک و پروژه را دانلود کنید.

Dependencyهای Maven

قسمت Dependencyهای pom.xml باید شامل موارد زیر باشد:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>io.projectreactor</groupId>
        <artifactId>reactor-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

نسخه Spring Boot را همان نسخه پایداری قرار دهید که Spring Initializr برای پروژه تولید کرده است.

ساختار نهایی پروژه

src/
├── main/
│   ├── java/
│   │   └── ir/darvareh/ai/
│   │       ├── SpringAiApiApplication.java
│   │       ├── config/
│   │       │   ├── AiProperties.java
│   │       │   └── WebClientConfig.java
│   │       ├── controller/
│   │       │   ├── ChatController.java
│   │       │   ├── FeedbackController.java
│   │       │   └── HealthController.java
│   │       ├── dto/
│   │       │   ├── ChatMessage.java
│   │       │   ├── ChatRequest.java
│   │       │   ├── ChatResponse.java
│   │       │   ├── FeedbackRequest.java
│   │       │   └── FeedbackResult.java
│   │       ├── exception/
│   │       │   ├── AiServiceException.java
│   │       │   └── GlobalExceptionHandler.java
│   │       └── service/
│   │           ├── AiClient.java
│   │           ├── ChatService.java
│   │           └── FeedbackService.java
│   └── resources/
│       └── application.yml
└── test/
    └── java/
        └── ir/darvareh/ai/

تنظیم application.yml

فایل:

src/main/resources/application.yml

محتوا:

server:
  port: ${PORT:8080}

spring:
  application:
    name: spring-ai-api

ai:
  base-url: ${DARVAREH_BASE_URL:https://api.darvareh.ir/v1}
  api-key: ${DARVAREH_API_KEY}
  model: ${DARVAREH_MODEL}
  connect-timeout: ${AI_CONNECT_TIMEOUT:10s}
  response-timeout: ${AI_RESPONSE_TIMEOUT:45s}
  max-output-tokens: ${AI_MAX_OUTPUT_TOKENS:1200}

management:
  endpoints:
    web:
      exposure:
        include: health,info

متغیرهای محیطی:

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

در Linux یا macOS:

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

در PowerShell:

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

خواندن تنظیمات با Configuration Properties

فایل AiProperties.java:

package ir.darvareh.ai.config;

import java.time.Duration;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "ai")
public record AiProperties(
    String baseUrl,
    String apiKey,
    String model,
    Duration connectTimeout,
    Duration responseTimeout,
    Integer maxOutputTokens
) {
    public AiProperties {
        if (baseUrl == null || baseUrl.isBlank()) {
            throw new IllegalArgumentException(
                "AI base URL must not be empty"
            );
        }

        if (apiKey == null || apiKey.isBlank()) {
            throw new IllegalArgumentException(
                "DARVAREH_API_KEY is not configured"
            );
        }

        if (model == null || model.isBlank()) {
            throw new IllegalArgumentException(
                "DARVAREH_MODEL is not configured"
            );
        }
    }
}

فایل اصلی برنامه:

package ir.darvareh.ai;

import ir.darvareh.ai.config.AiProperties;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

@SpringBootApplication
@EnableConfigurationProperties(AiProperties.class)
public class SpringAiApiApplication {

    public static void main(String[] args) {
        SpringApplication.run(
            SpringAiApiApplication.class,
            args
        );
    }
}

تنظیم WebClient

فایل WebClientConfig.java:

package ir.darvareh.ai.config;

import io.netty.channel.ChannelOption;
import reactor.netty.http.client.HttpClient;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient aiWebClient(
        AiProperties properties
    ) {
        HttpClient httpClient = HttpClient
            .create()
            .option(
                ChannelOption.CONNECT_TIMEOUT_MILLIS,
                Math.toIntExact(
                    properties
                        .connectTimeout()
                        .toMillis()
                )
            )
            .responseTimeout(
                properties.responseTimeout()
            );

        return WebClient
            .builder()
            .baseUrl(properties.baseUrl())
            .defaultHeader(
                HttpHeaders.AUTHORIZATION,
                "Bearer " + properties.apiKey()
            )
            .defaultHeader(
                HttpHeaders.CONTENT_TYPE,
                MediaType.APPLICATION_JSON_VALUE
            )
            .clientConnector(
                new ReactorClientHttpConnector(
                    httpClient
                )
            )
            .build();
    }
}

کلید API در Header پیش‌فرض WebClient قرار می‌گیرد و لازم نیست در هر درخواست تکرار شود.

تعریف DTO پیام‌ها

فایل ChatMessage.java:

package ir.darvareh.ai.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

public record ChatMessage(
    @NotBlank
    @Pattern(
        regexp = "user|assistant",
        message = "role must be user or assistant"
    )
    String role,

    @NotBlank
    @Size(max = 10000)
    String content
) {
}

در Endpoint عمومی اجازه ارسال پیام system از Client را نمی‌دهیم. System Prompt در Backend مدیریت خواهد شد.

فایل ChatRequest.java:

package ir.darvareh.ai.dto;

import java.util.List;

import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;

public record ChatRequest(
    @NotEmpty
    @Size(max = 30)
    List<@Valid ChatMessage> messages,

    @DecimalMin("0.0")
    @DecimalMax("2.0")
    Double temperature,

    @Min(1)
    @Max(4000)
    Integer maxTokens
) {
}

فایل ChatResponse.java:

package ir.darvareh.ai.dto;

public record ChatResponse(
    String answer,
    String model,
    Usage usage
) {
    public record Usage(
        Integer promptTokens,
        Integer completionTokens,
        Integer totalTokens
    ) {
    }
}

مدل درخواست بالادستی

در همان Package می‌توانید فایل AiChatCompletionRequest.java بسازید:

package ir.darvareh.ai.dto;

import java.util.List;

import com.fasterxml.jackson.annotation.JsonProperty;

public record AiChatCompletionRequest(
    String model,
    List<AiMessage> messages,
    Double temperature,

    @JsonProperty("max_tokens")
    Integer maxTokens,

    Boolean stream
) {
    public record AiMessage(
        String role,
        String content
    ) {
    }
}

فایل AiChatCompletionResponse.java:

package ir.darvareh.ai.dto;

import java.util.List;

import com.fasterxml.jackson.annotation.JsonProperty;

public record AiChatCompletionResponse(
    List<Choice> choices,
    Usage usage
) {
    public record Choice(
        Message message
    ) {
    }

    public record Message(
        String role,
        String content
    ) {
    }

    public record Usage(
        @JsonProperty("prompt_tokens")
        Integer promptTokens,

        @JsonProperty("completion_tokens")
        Integer completionTokens,

        @JsonProperty("total_tokens")
        Integer totalTokens
    ) {
    }
}

ساخت Exception اختصاصی

فایل AiServiceException.java:

package ir.darvareh.ai.exception;

import org.springframework.http.HttpStatus;

public class AiServiceException
    extends RuntimeException {

    private final HttpStatus status;

    public AiServiceException(
        HttpStatus status,
        String message
    ) {
        super(message);
        this.status = status;
    }

    public AiServiceException(
        HttpStatus status,
        String message,
        Throwable cause
    ) {
        super(message, cause);
        this.status = status;
    }

    public HttpStatus getStatus() {
        return status;
    }
}

ساخت AiClient

فایل AiClient.java:

package ir.darvareh.ai.service;

import java.util.List;
import java.util.Objects;
import java.util.Optional;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import ir.darvareh.ai.dto.AiChatCompletionRequest;
import ir.darvareh.ai.dto.AiChatCompletionResponse;
import ir.darvareh.ai.exception.AiServiceException;

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientRequestException;
import org.springframework.web.reactive.function.client.WebClientResponseException;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Component
public class AiClient {

    private final WebClient webClient;
    private final ObjectMapper objectMapper;

    public AiClient(
        WebClient aiWebClient,
        ObjectMapper objectMapper
    ) {
        this.webClient = aiWebClient;
        this.objectMapper = objectMapper;
    }

    public AiChatCompletionResponse complete(
        AiChatCompletionRequest request
    ) {
        try {
            AiChatCompletionResponse response =
                webClient
                    .post()
                    .uri("/chat/completions")
                    .bodyValue(request)
                    .retrieve()
                    .onStatus(
                        status ->
                            status.is4xxClientError(),
                        clientResponse ->
                            clientResponse
                                .bodyToMono(String.class)
                                .defaultIfEmpty("")
                                .flatMap(body ->
                                    Mono.error(
                                        new AiServiceException(
                                            HttpStatus.BAD_GATEWAY,
                                            "درخواست مدل پذیرفته نشد."
                                        )
                                    )
                                )
                    )
                    .onStatus(
                        status ->
                            status.is5xxServerError(),
                        clientResponse ->
                            Mono.error(
                                new AiServiceException(
                                    HttpStatus.BAD_GATEWAY,
                                    "سرویس مدل با خطا مواجه شد."
                                )
                            )
                    )
                    .bodyToMono(
                        AiChatCompletionResponse.class
                    )
                    .block();

            if (response == null) {
                throw new AiServiceException(
                    HttpStatus.BAD_GATEWAY,
                    "پاسخ سرویس هوش مصنوعی خالی است."
                );
            }

            return response;
        } catch (AiServiceException exception) {
            throw exception;
        } catch (
            WebClientRequestException exception
        ) {
            throw new AiServiceException(
                HttpStatus.SERVICE_UNAVAILABLE,
                "ارتباط با سرویس هوش مصنوعی برقرار نشد.",
                exception
            );
        } catch (
            WebClientResponseException exception
        ) {
            throw new AiServiceException(
                HttpStatus.BAD_GATEWAY,
                "سرویس هوش مصنوعی پاسخ موفقی نداد.",
                exception
            );
        }
    }

    public Flux<String> stream(
        AiChatCompletionRequest request
    ) {
        return webClient
            .post()
            .uri("/chat/completions")
            .accept(MediaType.TEXT_EVENT_STREAM)
            .bodyValue(request)
            .retrieve()
            .bodyToFlux(String.class)
            .flatMapIterable(this::splitEvents)
            .map(this::extractContent)
            .filter(Optional::isPresent)
            .map(Optional::get)
            .onErrorMap(
                WebClientRequestException.class,
                exception ->
                    new AiServiceException(
                        HttpStatus.SERVICE_UNAVAILABLE,
                        "ارتباط Streaming برقرار نشد.",
                        exception
                    )
            );
    }

    private List<String> splitEvents(
        String rawChunk
    ) {
        return rawChunk
            .lines()
            .map(String::trim)
            .filter(line -> !line.isBlank())
            .toList();
    }

    private Optional<String> extractContent(
        String line
    ) {
        String data = line.startsWith("data:")
            ? line.substring(5).trim()
            : line.trim();

        if (
            data.isBlank() ||
            Objects.equals(data, "[DONE]")
        ) {
            return Optional.empty();
        }

        try {
            JsonNode root =
                objectMapper.readTree(data);

            JsonNode content = root
                .path("choices")
                .path(0)
                .path("delta")
                .path("content");

            if (
                content.isMissingNode() ||
                content.isNull()
            ) {
                return Optional.empty();
            }

            String value = content.asText();

            return value.isEmpty()
                ? Optional.empty()
                : Optional.of(value);
        } catch (Exception exception) {
            return Optional.empty();
        }
    }
}

Streaming میان مدل‌ها و Providerهای بالادستی ممکن است جزئیات متفاوتی داشته باشد. این پیاده‌سازی باید با مدل انتخابی در محیط واقعی آزمایش شود.

ساخت ChatService

فایل ChatService.java:

package ir.darvareh.ai.service;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import ir.darvareh.ai.config.AiProperties;
import ir.darvareh.ai.dto.AiChatCompletionRequest;
import ir.darvareh.ai.dto.AiChatCompletionResponse;
import ir.darvareh.ai.dto.ChatMessage;
import ir.darvareh.ai.dto.ChatRequest;
import ir.darvareh.ai.dto.ChatResponse;
import ir.darvareh.ai.exception.AiServiceException;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

import reactor.core.publisher.Flux;

@Service
public class ChatService {

    private static final String SYSTEM_PROMPT = """
        تو یک دستیار فارسی دقیق و کاربردی هستی.
        پاسخ‌ها را روشن و متناسب با سؤال کاربر بنویس.
        اطلاعاتی را که در اختیار نداری حدس نزن.
        اگر سؤال مبهم است، ابهام را اعلام کن.
        """.strip();

    private final AiClient aiClient;
    private final AiProperties properties;

    public ChatService(
        AiClient aiClient,
        AiProperties properties
    ) {
        this.aiClient = aiClient;
        this.properties = properties;
    }

    public ChatResponse chat(
        ChatRequest request
    ) {
        AiChatCompletionRequest aiRequest =
            createRequest(
                request,
                false
            );

        AiChatCompletionResponse aiResponse =
            aiClient.complete(aiRequest);

        String answer = aiResponse
            .choices()
            .stream()
            .findFirst()
            .map(
                AiChatCompletionResponse
                    .Choice::message
            )
            .map(
                AiChatCompletionResponse
                    .Message::content
            )
            .filter(content ->
                !content.isBlank()
            )
            .orElseThrow(() ->
                new AiServiceException(
                    HttpStatus.BAD_GATEWAY,
                    "مدل پاسخ قابل استفاده‌ای تولید نکرد."
                )
            );

        ChatResponse.Usage usage = null;

        if (aiResponse.usage() != null) {
            usage = new ChatResponse.Usage(
                aiResponse
                    .usage()
                    .promptTokens(),
                aiResponse
                    .usage()
                    .completionTokens(),
                aiResponse
                    .usage()
                    .totalTokens()
            );
        }

        return new ChatResponse(
            answer.strip(),
            properties.model(),
            usage
        );
    }

    public Flux<String> stream(
        ChatRequest request
    ) {
        return aiClient.stream(
            createRequest(
                request,
                true
            )
        );
    }

    private AiChatCompletionRequest
        createRequest(
            ChatRequest request,
            boolean stream
        ) {

        List<AiChatCompletionRequest.AiMessage>
            messages = new ArrayList<>();

        messages.add(
            new AiChatCompletionRequest.AiMessage(
                "system",
                SYSTEM_PROMPT
            )
        );

        request.messages().stream()
            .map(this::toAiMessage)
            .forEach(messages::add);

        double temperature =
            Optional
                .ofNullable(
                    request.temperature()
                )
                .orElse(0.3);

        int maxTokens =
            Optional
                .ofNullable(
                    request.maxTokens()
                )
                .orElse(
                    properties.maxOutputTokens()
                );

        return new AiChatCompletionRequest(
            properties.model(),
            messages,
            temperature,
            maxTokens,
            stream
        );
    }

    private AiChatCompletionRequest.AiMessage
        toAiMessage(ChatMessage message) {

        return new AiChatCompletionRequest.AiMessage(
            message.role(),
            message.content().strip()
        );
    }
}

ساخت ChatController

فایل ChatController.java:

package ir.darvareh.ai.controller;

import ir.darvareh.ai.dto.ChatRequest;
import ir.darvareh.ai.dto.ChatResponse;
import ir.darvareh.ai.service.ChatService;

import jakarta.validation.Valid;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/api/chat")
public class ChatController {

    private final ChatService chatService;

    public ChatController(
        ChatService chatService
    ) {
        this.chatService = chatService;
    }

    @PostMapping
    public ChatResponse chat(
        @Valid
        @RequestBody
        ChatRequest request
    ) {
        return chatService.chat(request);
    }

    @PostMapping(
        value = "/stream",
        produces =
            MediaType.TEXT_EVENT_STREAM_VALUE
    )
    public Flux<String> stream(
        @Valid
        @RequestBody
        ChatRequest request
    ) {
        return chatService.stream(request);
    }
}

ساخت Health Controller

فایل HealthController.java:

package ir.darvareh.ai.controller;

import java.util.Map;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/health")
public class HealthController {

    @GetMapping
    public Map<String, String> health() {
        return Map.of(
            "status", "ok",
            "service", "spring-ai-api"
        );
    }
}

اجرای پروژه

در Linux و macOS:

export DARVAREH_API_KEY="YOUR_DARVAREH_API_KEY"
export DARVAREH_MODEL="YOUR_MODEL_ID"

./mvnw spring-boot:run

در PowerShell:

$env:DARVAREH_API_KEY="YOUR_DARVAREH_API_KEY"
$env:DARVAREH_MODEL="YOUR_MODEL_ID"

.\mvnw.cmd spring-boot:run

بررسی سلامت:

curl http://localhost:8080/api/health

خروجی:

{
  "service": "spring-ai-api",
  "status": "ok"
}

آزمایش Chat Endpoint

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

نمونه پاسخ:

{
  "answer": "Spring Boot فریم‌ورکی برای ساخت سریع اپلیکیشن‌های Java است.",
  "model": "YOUR_MODEL_ID",
  "usage": {
    "promptTokens": 50,
    "completionTokens": 60,
    "totalTokens": 110
  }
}

وجود و جزئیات Usage می‌تواند به مدل و Endpoint وابسته باشد.

آزمایش Streaming

curl -N \
  -X POST \
  http://localhost:8080/api/chat/stream \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {
        "role": "user",
        "content": "Java را مرحله‌به‌مرحله معرفی کن."
      }
    ]
  }'

گزینه -N باعث می‌شود cURL پاسخ را کمتر Buffer کند و Chunkها سریع‌تر نمایش داده شوند.

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

برای یک مکالمه چندمرحله‌ای:

{
  "messages": [
    {
      "role": "user",
      "content": "WebClient چیست؟"
    },
    {
      "role": "assistant",
      "content": "WebClient یک HTTP Client در Spring است."
    },
    {
      "role": "user",
      "content": "چگونه Streaming انجام می‌دهد؟"
    }
  ]
}

در یک محصول واقعی بهتر است Client فقط این اطلاعات را بفرستد:

{
  "conversationId": "conversation-123",
  "message": "سؤال جدید کاربر"
}

Backend تاریخچه را از دیتابیس بازیابی و فقط پیام‌های مرتبط را برای مدل ارسال می‌کند.

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

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

ورودی:

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

خروجی:

{
  "sentiment": "NEGATIVE",
  "category": "DELIVERY",
  "priority": "HIGH",
  "summary": "مشتری از تأخیر سفارش و پاسخ‌گویی پشتیبانی ناراضی است.",
  "requiresHuman": true
}

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

فایل FeedbackRequest.java:

package ir.darvareh.ai.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record FeedbackRequest(
    @NotBlank
    @Size(min = 3, max = 10000)
    String text
) {
}

فایل FeedbackResult.java:

package ir.darvareh.ai.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record FeedbackResult(
    Sentiment sentiment,
    Category category,
    Priority priority,

    @NotBlank
    @Size(max = 500)
    String summary,

    boolean requiresHuman
) {
    public enum Sentiment {
        POSITIVE,
        NEUTRAL,
        NEGATIVE
    }

    public enum Category {
        PRODUCT,
        DELIVERY,
        SUPPORT,
        BILLING,
        OTHER
    }

    public enum Priority {
        LOW,
        MEDIUM,
        HIGH
    }
}

ساخت FeedbackService

فایل FeedbackService.java:

package ir.darvareh.ai.service;

import java.util.List;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import ir.darvareh.ai.config.AiProperties;
import ir.darvareh.ai.dto.AiChatCompletionRequest;
import ir.darvareh.ai.dto.AiChatCompletionResponse;
import ir.darvareh.ai.dto.FeedbackResult;
import ir.darvareh.ai.exception.AiServiceException;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

@Service
public class FeedbackService {

    private static final String PROMPT = """
        تو یک سیستم تحلیل بازخورد مشتری هستی.

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

        ساختار دقیق:
        {
          "sentiment": "POSITIVE | NEUTRAL | NEGATIVE",
          "category": "PRODUCT | DELIVERY | SUPPORT | BILLING | OTHER",
          "priority": "LOW | MEDIUM | HIGH",
          "summary": "خلاصه فارسی",
          "requiresHuman": true
        }

        اطلاعاتی را که در متن وجود ندارد حدس نزن.
        اگر دسته مشخص نیست، OTHER را انتخاب کن.
        """.strip();

    private final AiClient aiClient;
    private final AiProperties properties;
    private final ObjectMapper objectMapper;

    public FeedbackService(
        AiClient aiClient,
        AiProperties properties,
        ObjectMapper objectMapper
    ) {
        this.aiClient = aiClient;
        this.properties = properties;
        this.objectMapper = objectMapper;
    }

    public FeedbackResult analyze(
        String feedback
    ) {
        AiChatCompletionRequest request =
            new AiChatCompletionRequest(
                properties.model(),
                List.of(
                    new AiChatCompletionRequest.AiMessage(
                        "system",
                        PROMPT
                    ),
                    new AiChatCompletionRequest.AiMessage(
                        "user",
                        feedback.strip()
                    )
                ),
                0.0,
                400,
                false
            );

        AiChatCompletionResponse response =
            aiClient.complete(request);

        String rawContent = response
            .choices()
            .stream()
            .findFirst()
            .map(
                AiChatCompletionResponse
                    .Choice::message
            )
            .map(
                AiChatCompletionResponse
                    .Message::content
            )
            .orElseThrow(() ->
                new AiServiceException(
                    HttpStatus.BAD_GATEWAY,
                    "مدل خروجی تولید نکرد."
                )
            );

        String cleaned =
            cleanJson(rawContent);

        try {
            return objectMapper.readValue(
                cleaned,
                FeedbackResult.class
            );
        } catch (
            JsonProcessingException exception
        ) {
            throw new AiServiceException(
                HttpStatus.BAD_GATEWAY,
                "خروجی مدل JSON معتبر یا سازگار نیست.",
                exception
            );
        }
    }

    private String cleanJson(
        String value
    ) {
        String cleaned = value.strip();

        if (cleaned.startsWith("```json")) {
            cleaned = cleaned
                .substring(7)
                .strip();
        } else if (
            cleaned.startsWith("```")
        ) {
            cleaned = cleaned
                .substring(3)
                .strip();
        }

        if (cleaned.endsWith("```")) {
            cleaned = cleaned
                .substring(
                    0,
                    cleaned.length() - 3
                )
                .strip();
        }

        return cleaned;
    }
}

ساخت FeedbackController

package ir.darvareh.ai.controller;

import ir.darvareh.ai.dto.FeedbackRequest;
import ir.darvareh.ai.dto.FeedbackResult;
import ir.darvareh.ai.service.FeedbackService;

import jakarta.validation.Valid;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/feedback")
public class FeedbackController {

    private final FeedbackService service;

    public FeedbackController(
        FeedbackService service
    ) {
        this.service = service;
    }

    @PostMapping("/analyze")
    public FeedbackResult analyze(
        @Valid
        @RequestBody
        FeedbackRequest request
    ) {
        return service.analyze(
            request.text()
        );
    }
}

چرا خروجی مدل را Parse می‌کنیم؟

حتی اگر در پرامپت از مدل بخواهیم فقط JSON برگرداند، همچنان احتمال این مشکلات وجود دارد:

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

Jackson هنگام تبدیل پاسخ به FeedbackResult بخشی از ناسازگاری‌ها را تشخیص می‌دهد. برای کنترل دقیق‌تر می‌توانید پس از Parse از Jakarta Validation نیز استفاده کنید.

اگر مدل انتخابی از Structured Outputs یا JSON Schema پشتیبانی می‌کند، بهتر است از آن قابلیت استفاده کنید؛ اما Validation سمت Backend همچنان ضروری است.

ساخت Global Exception Handler

فایل GlobalExceptionHandler.java:

package ir.darvareh.ai.exception;

import java.time.Instant;
import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(
        MethodArgumentNotValidException.class
    )
    public ResponseEntity<Object>
        handleValidation(
            MethodArgumentNotValidException exception
        ) {

        Map<String, String> fields =
            exception
                .getBindingResult()
                .getFieldErrors()
                .stream()
                .collect(
                    java.util.stream.Collectors
                        .toMap(
                            FieldError::getField,
                            error ->
                                error
                                    .getDefaultMessage()
                                    == null
                                        ? "invalid"
                                        : error
                                            .getDefaultMessage(),
                            (first, second) -> first
                        )
                );

        return ResponseEntity
            .unprocessableEntity()
            .body(
                Map.of(
                    "timestamp",
                    Instant.now().toString(),
                    "error",
                    "validation_error",
                    "message",
                    "ساختار درخواست معتبر نیست.",
                    "fields",
                    fields
                )
            );
    }

    @ExceptionHandler(
        AiServiceException.class
    )
    public ResponseEntity<Object>
        handleAiService(
            AiServiceException exception
        ) {

        return ResponseEntity
            .status(exception.getStatus())
            .body(
                Map.of(
                    "timestamp",
                    Instant.now().toString(),
                    "error",
                    "ai_service_error",
                    "message",
                    exception.getMessage()
                )
            );
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object>
        handleUnexpected(
            Exception exception
        ) {

        return ResponseEntity
            .status(
                HttpStatus.INTERNAL_SERVER_ERROR
            )
            .body(
                Map.of(
                    "timestamp",
                    Instant.now().toString(),
                    "error",
                    "internal_error",
                    "message",
                    "پردازش درخواست با خطا مواجه شد."
                )
            );
    }
}

در محیط Production، Exception کامل را در Log کنترل‌شده ثبت کنید، اما جزئیات داخلی و کلید API را برای Client برنگردانید.

اتصال React به Spring Boot

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

  const body = await response.json();

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

  return body;
}

برای اتصال از یک Origin متفاوت باید CORS را سمت Spring Boot تنظیم کنید.

تنظیم CORS

فایل CorsConfig.java:

package ir.darvareh.ai.config;

import java.util.List;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig
    implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(
        CorsRegistry registry
    ) {
        registry
            .addMapping("/api/**")
            .allowedOrigins(
                "http://localhost:5173"
            )
            .allowedMethods(
                "GET",
                "POST"
            )
            .allowedHeaders(
                "Content-Type",
                "Authorization"
            );
    }
}

در Production فقط دامنه‌های واقعی Frontend را مجاز کنید.

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

برای ذخیره مکالمه می‌توانید از Spring Data JPA و PostgreSQL استفاده کنید.

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

Conversation
- id
- userId
- title
- createdAt
- updatedAt

Message
- id
- conversationId
- role
- content
- promptTokens
- completionTokens
- createdAt

جریان مناسب:

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

مدیریت Context طولانی

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

راهکارها:

  • نگهداری فقط چند پیام اخیر
  • خلاصه‌سازی پیام‌های قدیمی
  • حذف پیام‌های نامرتبط
  • ذخیره Summary گفتگو
  • محدودکردن تعداد توکن ورودی
  • جداکردن اسناد از تاریخچه چت
  • استفاده از RAG برای اطلاعات سازمانی

نمونه Context:

System Prompt
+ Conversation Summary
+ Last 10 Messages
+ Relevant Retrieved Documents
+ Current User Message

کنترل هزینه

روش‌های عملی:

  • محدودیت تعداد درخواست هر کاربر
  • محدودیت طول پیام
  • محدودیت تعداد پیام‌های Context
  • محدودیت maxTokens
  • انتخاب مدل اقتصادی برای کار ساده
  • Cache کردن پاسخ‌های تکراری
  • خلاصه‌سازی تاریخچه
  • ثبت Usage
  • تعریف بودجه روزانه و ماهانه
  • جلوگیری از Retry چندلایه
  • اجرای Batch برای پردازش‌های غیرفوری

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

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

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

پیش از انتخاب مدل نهایی، چند مدل را روی مجموعه‌ای از داده‌های واقعی خود ارزیابی کنید.

Retry در Spring Boot

Retry باید فقط برای خطاهای موقت انجام شود:

  • قطع موقت اتصال
  • Timeout کوتاه
  • خطای موقت سرویس
  • محدودیت نرخ با تأخیر مناسب

برای این خطاها Retry نکنید:

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

اگر Gateway، WebClient، Service و Queue هم‌زمان Retry داشته باشند، یک درخواست ممکن است چندین بار اجرا شود. سیاست Retry را در یک لایه مشخص طراحی کنید.

پردازش‌های طولانی با Queue

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

نمونه‌ها:

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

معماری:

Client
  ↓
POST /api/jobs
  ↓
ایجاد Job
  ↓
Kafka یا RabbitMQ
  ↓
Worker
  ↓
API درواره
  ↓
ذخیره نتیجه
  ↓
Polling یا Webhook

Spring Boot می‌تواند Worker و Consumer جداگانه داشته باشد و در آینده به چند سرور مقیاس پیدا کند.

Cache با Redis

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

  • ورودی ثابت دارند.
  • پاسخ وابسته به وضعیت لحظه‌ای نیست.
  • کاربران متعدد سؤال مشابه دارند.
  • مدل و Prompt تغییر نکرده‌اند.

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

feature
model
promptVersion
normalizedInput
relevantParameters

با تغییر Prompt یا مدل باید Cache Key نیز تغییر کند.

تست ChatService

برای Unit Test می‌توانید AiClient را Mock کنید.

نمونه با Mockito:

package ir.darvareh.ai.service;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

import java.time.Duration;
import java.util.List;

import ir.darvareh.ai.config.AiProperties;
import ir.darvareh.ai.dto.AiChatCompletionResponse;
import ir.darvareh.ai.dto.ChatMessage;
import ir.darvareh.ai.dto.ChatRequest;

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

class ChatServiceTest {

    @Test
    void returnsModelAnswer() {
        AiClient client =
            Mockito.mock(AiClient.class);

        AiProperties properties =
            new AiProperties(
                "https://api.darvareh.ir/v1",
                "test-key",
                "YOUR_MODEL_ID",
                Duration.ofSeconds(5),
                Duration.ofSeconds(30),
                1000
            );

        AiChatCompletionResponse response =
            new AiChatCompletionResponse(
                List.of(
                    new AiChatCompletionResponse.Choice(
                        new AiChatCompletionResponse.Message(
                            "assistant",
                            "پاسخ آزمایشی"
                        )
                    )
                ),
                null
            );

        when(client.complete(any()))
            .thenReturn(response);

        ChatService service =
            new ChatService(
                client,
                properties
            );

        ChatRequest request =
            new ChatRequest(
                List.of(
                    new ChatMessage(
                        "user",
                        "سلام"
                    )
                ),
                0.3,
                500
            );

        assertEquals(
            "پاسخ آزمایشی",
            service.chat(request).answer()
        );
    }
}

در Unit Test نباید درخواست واقعی و هزینه‌دار به مدل ارسال شود.

تست Streaming

با Reactor Test:

Flux<String> stream = Flux.just(
    "سلام",
    "، ",
    "چطور کمک کنم؟"
);

StepVerifier
    .create(stream)
    .expectNext(
        "سلام",
        "، ",
        "چطور کمک کنم؟"
    )
    .verifyComplete();

سناریوهای ضروری:

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

تست‌های ضروری پروژه

Chat API

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

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

  • JSON معتبر
  • JSON داخل Code Fence
  • JSON ناقص
  • Enum نامعتبر
  • فیلد حذف‌شده
  • پاسخ متنی
  • پاسخ خالی
  • خلاصه بسیار طولانی

تنظیمات

  • نبود API Key
  • نبود Model ID
  • Base URL نامعتبر
  • Timeout نامعتبر
  • مقدار Token نامعتبر

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

قرار‌دادن کلید در Frontend

کلید API را در React، Angular، Android یا Flutter قرار ندهید. درخواست باید از Spring Boot ارسال شود.

ساخت WebClient برای هر درخواست

یک Bean قابل استفاده مجدد بسازید تا Connectionها و منابع بهتر مدیریت شوند.

ارسال System Prompt از Client

System Prompt رفتار اصلی قابلیت را تعیین می‌کند و بهتر است سمت Backend کنترل شود.

اعتماد مستقیم به خروجی مدل

خروجی را Parse، Validate و با قواعد کسب‌وکار بررسی کنید.

ارسال تاریخچه نامحدود

این کار هزینه و Latency را افزایش می‌دهد.

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

دسته‌بندی ساده به همان مدلی نیاز ندارد که تحلیل پیچیده یا Coding انجام می‌دهد.

نداشتن Timeout

بدون Timeout، Thread یا اتصال می‌تواند مدت زیادی درگیر بماند.

استفاده از .block() در جریان کاملاً Reactive

در Controller معمولی MVC می‌توان پاسخ نهایی را Block کرد، اما در جریان WebFlux کاملاً Reactive نباید Event Loop را با عملیات Blocking متوقف کنید.

ذخیره خروجی بدون Validation

JSON تولیدشده را مستقیماً در دیتابیس قرار ندهید.

نمایش Exception داخلی

جزئیات فنی، Stack Trace و پاسخ خام سرویس بالادستی را برای کاربر نمایش ندهید.

کاربردهای واقعی Java و هوش مصنوعی

دستیار سازمانی

Spring Boot می‌تواند احراز هویت، نقش‌ها، تاریخچه، RAG و اتصال مدل را مدیریت کند.

تحلیل تیکت پشتیبانی

خروجی:

{
  "category": "SUPPORT",
  "priority": "HIGH",
  "summary": "کاربر امکان ورود به حساب را ندارد."
}

تحلیل اسناد

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

دستیار CRM

  • خلاصه تعامل مشتری
  • پیشنهاد پاسخ
  • تحلیل احساس
  • دسته‌بندی Lead
  • تولید یادداشت فروش

تولید محتوای فروشگاه

  • عنوان محصول
  • توضیحات
  • مزایا
  • مشخصات قابل فهم
  • سؤال‌های متداول

Code Review داخلی

  • توضیح Diff
  • پیشنهاد تست
  • شناسایی خطای منطقی
  • تولید مستندات
  • خلاصه Pull Request

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

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

بله. Java می‌تواند از طریق API به مدل‌های آماده متصل شود و قابلیت‌هایی مانند چت، خلاصه‌سازی، استخراج اطلاعات، RAG و Agent را به نرم‌افزار اضافه کند.

آیا Spring Boot برای هوش مصنوعی مناسب است؟

بله. Spring Boot برای ساخت Backend، مدیریت کاربران، دیتابیس، API، Validation و اتصال به مدل‌های هوش مصنوعی مناسب است.

چگونه Spring Boot را به API درواره متصل کنیم؟

با WebClient یک درخواست به این Base URL ارسال کنید:

https://api.darvareh.ir/v1

Endpoint متنی:

POST /chat/completions

کلید API درواره را کجا قرار دهیم؟

در متغیر محیطی سرور:

DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY

کلید را در Source Code یا Frontend قرار ندهید.

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

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

WebClient بهتر است یا RestClient؟

هر دو می‌توانند برای درخواست معمولی استفاده شوند. WebClient برای Streaming و جریان‌های Reactive امکانات مناسبی دارد.

آیا می‌توان پاسخ را Streaming کرد؟

بله. WebClient از Streaming پشتیبانی می‌کند و Spring می‌تواند نتیجه را با Flux و text/event-stream برای Client ارسال کند.

آیا باید از Spring AI استفاده کنیم؟

برای اتصال ساده به API OpenAI-compatible الزامی نیست. WebClient کنترل مستقیمی روی ساختار درخواست ایجاد می‌کند. برای Workflowهای گسترده‌تر می‌توانید Frameworkهای بالاتر را جداگانه ارزیابی کنید.

چگونه خروجی JSON معتبر دریافت کنیم؟

در صورت پشتیبانی مدل از Structured Outputs استفاده کنید و خروجی را با Jackson و Validation سمت Backend بررسی کنید.

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

بله. Conversation و Message را در جداول جدا ذخیره و فقط Context مرتبط را برای مدل ارسال کنید.

چگونه هزینه را کاهش دهیم؟

مدل مناسب انتخاب کنید، تاریخچه را محدود کنید، طول پاسخ را کنترل کنید، Cache داشته باشید و Usage هر Feature را ثبت کنید.

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

خیر. اپلیکیشن Android باید به Backend Spring Boot متصل شود و Spring Boot درخواست را با کلید محرمانه برای درواره بفرستد.

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

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

جمع‌بندی

Java و Spring Boot انتخاب مناسبی برای اضافه‌کردن هوش مصنوعی به نرم‌افزارهای Backend و سامانه‌های سازمانی هستند. برای این کار لازم نیست مدل را روی سرور خود آموزش یا اجرا کنید؛ Backend می‌تواند از طریق API به مدل موردنظر متصل شود.

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

  • پروژه Spring Boot ایجاد کنیم.
  • تنظیمات درواره را با Environment Variable مدیریت کنیم.
  • WebClient قابل استفاده مجدد بسازیم.
  • به API سازگار با OpenAI درواره متصل شویم.
  • REST API چت ایجاد کنیم.
  • تاریخچه مکالمه را ارسال کنیم.
  • پاسخ Streaming بسازیم.
  • بازخورد مشتری را به JSON تبدیل کنیم.
  • خروجی مدل را با Jackson بررسی کنیم.
  • Validation و Exception Handling مرکزی اضافه کنیم.
  • پروژه را برای PostgreSQL، Redis و Queue توسعه دهیم.
  • هزینه، Context و Usage را مدیریت کنیم.
  • Service و Stream را تست کنیم.

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

مقالات مرتبط

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

Read more

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

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

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

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

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

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