هوش مصنوعی با Go؛ آموزش ساخت API، چتبات و سرویس AI با Golang و Gin
در این آموزش عملی یاد میگیرید با Go و Gin یک REST API و چتبات هوش مصنوعی بسازید، آن را به API درواره متصل کنید و پاسخ عادی، Streaming و JSON دریافت کنید.
زبان Go یا Golang یکی از انتخابهای مناسب برای ساخت Backendهای سریع، سبک و مقیاسپذیر است. اگر در حال توسعه یک Microservice، چتبات، ابزار پردازش متن، پنل سازمانی یا سرویس پرترافیک هستید، میتوانید قابلیتهای هوش مصنوعی را بدون تغییر اساسی در معماری برنامه به پروژه Go اضافه کنید.
برای انجام این کار لازم نیست مدل هوش مصنوعی را روی سرور خود آموزش دهید یا زیرساخت پردازشی سنگینی بسازید. برنامه Go میتواند از طریق API به مدل موردنظر متصل شود، پرامپت (Prompt) را ارسال کند و پاسخ مدل را بهصورت متن، JSON یا Streaming دریافت کند.
در این آموزش یک پروژه عملی با Go و فریمورک Gin میسازیم و آن را به API درواره متصل میکنیم.
در پایان مقاله میتوانید:
- یک REST API با Go و Gin بسازید.
- درخواست Chat Completions به درواره ارسال کنید.
- API Key را خارج از کد منبع نگه دارید.
- یک Endpoint چتبات ایجاد کنید.
- پاسخ مدل را بهصورت عادی دریافت کنید.
- پاسخ Streaming را با SSE پیادهسازی کنید.
- خروجی JSON ساختاریافته تولید و اعتبارسنجی کنید.
- Timeout و لغو درخواست را با
context.Contextمدیریت کنید. - تست واحد و تست یکپارچه بنویسید.
- پروژه را با Docker اجرا کنید.
- سرویس را برای محیط Production آماده کنید.
چرا Go برای ساخت برنامههای هوش مصنوعی مناسب است؟
بخش زیادی از آموزشهای هوش مصنوعی با پایتون (Python)، جاوااسکریپت (JavaScript) یا TypeScript نوشته میشوند. بااینحال، اگر هدف شما ساخت یک Backend سریع و قابلاتکا باشد، Go مزایای مهمی دارد.
مهمترین مزایای Go برای سرویسهای هوش مصنوعی عبارتاند از:
- مصرف حافظه نسبتاً کم
- زمان راهاندازی سریع برنامه
- پشتیبانی داخلی از پردازش همزمان با Goroutine
- کتابخانه استاندارد قدرتمند برای HTTP و JSON
- ساخت فایل اجرایی مستقل
- استقرار آسان با Docker
- مناسب برای Microserviceها
- پشتیبانی مناسب از Context، Timeout و Cancellation
- سادگی ساخت سرویسهای پرترافیک
- امکان اجرای یک کد روی Linux، Windows و macOS
نکته مهم این است که Go معمولاً برای آموزش مدلهای بزرگ استفاده نمیشود؛ اما برای ساخت لایه API، مدیریت کاربران، پردازش درخواستها، اتصال به پایگاه داده، اجرای صف وظایف و ارتباط با مدلهای هوش مصنوعی گزینه بسیار مناسبی است.
با Go و هوش مصنوعی چه برنامههایی میتوان ساخت؟
برنامه Go شما میتواند تقریباً هر قابلیت متنی یا چندوجهی را از طریق API ارائه کند.
| کاربرد | نمونه پیادهسازی |
|---|---|
| چتبات | پاسخگویی به کاربران سایت یا اپلیکیشن |
| دستیار پشتیبانی | پاسخ بر اساس اطلاعات محصولات و خدمات |
| تولید محتوا | تولید عنوان، توضیحات محصول و متن تبلیغاتی |
| خلاصهسازی | خلاصه مقاله، گزارش، تیکت یا جلسه |
| تحلیل نظرات | تشخیص موضوع و احساس کلی بازخوردها |
| استخراج اطلاعات | تبدیل متن آزاد به JSON |
| دستیار برنامهنویسی | توضیح، بازنویسی و مستندسازی کد |
| جستوجوی هوشمند | ترکیب مدل زبانی با پایگاه داده یا RAG |
| پردازش گروهی | تحلیل تعداد زیادی سند در Background Worker |
| ایجنت هوش مصنوعی | اتصال مدل به ابزارها و سرویسهای داخلی |
معماری صحیح اتصال Go به مدل هوش مصنوعی
در یک محصول واقعی، Frontend یا اپلیکیشن موبایل نباید مستقیماً API Key درواره را در اختیار داشته باشد.
معماری پیشنهادی به این شکل است:
- کاربر درخواست خود را در Frontend وارد میکند.
- Frontend درخواست را به Backend نوشتهشده با Go ارسال میکند.
- Backend ورودی، هویت کاربر و محدودیت مصرف را بررسی میکند.
- برنامه Go با API Key محرمانه به درواره درخواست میفرستد.
- درواره درخواست را به مدل انتخابی منتقل میکند.
- پاسخ مدل به Backend برمیگردد.
- Backend پاسخ کنترلشده را به کاربر تحویل میدهد.
در این معماری میتوانید قابلیتهای زیر را در Backend اجرا کنید:
- احراز هویت
- Rate Limiting
- ثبت میزان مصرف
- مدیریت اعتبار کاربران
- کش پاسخها
- حذف دادههای حساس
- انتخاب مدل
- Fallback
- مدیریت خطا
- ثبت لاگ
- اعتبارسنجی خروجی مدل
Gin چیست؟
Gin یک Web Framework برای زبان Go است که ساخت REST API، مسیریابی، Middleware، اعتبارسنجی ورودی و تولید پاسخ JSON را سادهتر میکند.
البته با کتابخانه استاندارد net/http نیز میتوان API ساخت؛ اما Gin در پروژههایی که Endpointهای متعدد، Middleware و اعتبارسنجی دارند، سرعت توسعه را افزایش میدهد.
در آموزش رسمی Go برای ساخت REST API با Gin نیز از Gin برای Routing، خواندن اطلاعات درخواست و تولید پاسخ JSON استفاده شده است.
پیشنیازهای آموزش
برای انجام پروژه به موارد زیر نیاز دارید:
- یک نسخه جدید و پشتیبانیشده از Go
- آشنایی مقدماتی با زبان Go
- یک ویرایشگر مانند VS Code یا GoLand
- حساب کاربری در درواره
- API Key درواره
- شناسه یک مدل متنی
برای بررسی نسخه نصبشده Go، دستور زیر را اجرا کنید:
go version
برای انتخاب مدل مناسب و مشاهده قیمتها به صفحه مدلهای درواره مراجعه کنید.
در نمونهکدها از مقادیر زیر استفاده میکنیم:
Base URL:
https://api.darvareh.ir/v1
API Key:
YOUR_DARVAREH_API_KEY
Model ID:
YOUR_MODEL_ID
ساخت پروژه Go
یک پوشه جدید ایجاد کنید:
mkdir darvareh-go-ai
cd darvareh-go-ai
ماژول Go را راهاندازی کنید:
go mod init example.com/darvareh-go-ai
Gin را نصب کنید:
go get github.com/gin-gonic/gin
در نسخههای جدید Gin ممکن است حداقل نسخه موردنیاز Go تغییر کند؛ بنابراین در صورت بروز خطا، نسخه موردنیاز را در راهنمای رسمی Gin بررسی کنید.
ساختار پروژه
برای اینکه کد قابلتوسعه باشد، از ساختار زیر استفاده میکنیم:
darvareh-go-ai/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── ai/
│ │ ├── client.go
│ │ └── models.go
│ └── httpapi/
│ └── handler.go
├── Dockerfile
├── go.mod
└── go.sum
پوشه internal باعث میشود Packageهای داخلی پروژه از بیرون ماژول قابل Import نباشند.
تعریف مدلهای درخواست و پاسخ
فایل internal/ai/models.go را ایجاد کنید:
package ai
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
type ChatCompletionRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
Temperature float64 `json:"temperature,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
Stream bool `json:"stream,omitempty"`
ResponseFormat *ResponseFormat `json:"response_format,omitempty"`
}
type ResponseFormat struct {
Type string `json:"type"`
}
type ChatCompletionResponse struct {
Model string `json:"model"`
Choices []Choice `json:"choices"`
Usage *Usage `json:"usage,omitempty"`
}
type Choice struct {
Index int `json:"index"`
Message Message `json:"message"`
}
type Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}
type StreamResponse struct {
Choices []StreamChoice `json:"choices"`
}
type StreamChoice struct {
Delta StreamDelta `json:"delta"`
}
type StreamDelta struct {
Content string `json:"content"`
}
type ChatResult struct {
Answer string `json:"answer"`
Model string `json:"model"`
Usage *Usage `json:"usage,omitempty"`
}
تگهایی مانند json:"model" مشخص میکنند هر فیلد Go با چه نامی در JSON ارسال یا دریافت شود.
ساخت Client اتصال به درواره
فایل internal/ai/client.go را بسازید:
package ai
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
)
var ErrEmptyResponse = errors.New("AI response contains no text")
type Client struct {
baseURL string
apiKey string
model string
httpClient *http.Client
}
type ClientConfig struct {
BaseURL string
APIKey string
Model string
HTTPClient *http.Client
}
func NewClient(config ClientConfig) (*Client, error) {
if strings.TrimSpace(config.BaseURL) == "" {
return nil, errors.New("base URL is required")
}
if strings.TrimSpace(config.APIKey) == "" {
return nil, errors.New("API key is required")
}
if strings.TrimSpace(config.Model) == "" {
return nil, errors.New("model ID is required")
}
httpClient := config.HTTPClient
if httpClient == nil {
httpClient = &http.Client{
Timeout: 90 * time.Second,
}
}
return &Client{
baseURL: strings.TrimRight(config.BaseURL, "/"),
apiKey: config.APIKey,
model: config.Model,
httpClient: httpClient,
}, nil
}
func (c *Client) Complete(
ctx context.Context,
message string,
systemPrompt string,
) (*ChatResult, error) {
messages := buildMessages(message, systemPrompt)
payload := ChatCompletionRequest{
Model: c.model,
Messages: messages,
Temperature: 0.3,
MaxTokens: 1000,
}
var response ChatCompletionResponse
if err := c.doJSON(
ctx,
http.MethodPost,
"/chat/completions",
payload,
&response,
); err != nil {
return nil, err
}
if len(response.Choices) == 0 {
return nil, ErrEmptyResponse
}
answer := strings.TrimSpace(
response.Choices[0].Message.Content,
)
if answer == "" {
return nil, ErrEmptyResponse
}
return &ChatResult{
Answer: answer,
Model: response.Model,
Usage: response.Usage,
}, nil
}
func (c *Client) CompleteJSON(
ctx context.Context,
prompt string,
target any,
) error {
payload := ChatCompletionRequest{
Model: c.model,
Messages: []Message{
{
Role: "system",
Content: "فقط یک JSON معتبر تولید کن. " +
"هیچ توضیح یا Markdown خارج از JSON ننویس.",
},
{
Role: "user",
Content: prompt,
},
},
Temperature: 0,
MaxTokens: 600,
ResponseFormat: &ResponseFormat{
Type: "json_object",
},
}
var response ChatCompletionResponse
if err := c.doJSON(
ctx,
http.MethodPost,
"/chat/completions",
payload,
&response,
); err != nil {
return err
}
if len(response.Choices) == 0 {
return ErrEmptyResponse
}
content := strings.TrimSpace(
response.Choices[0].Message.Content,
)
if content == "" {
return ErrEmptyResponse
}
if err := json.Unmarshal([]byte(content), target); err != nil {
return fmt.Errorf("decode model JSON: %w", err)
}
return nil
}
func (c *Client) Stream(
ctx context.Context,
message string,
systemPrompt string,
onToken func(string) error,
) error {
payload := ChatCompletionRequest{
Model: c.model,
Messages: buildMessages(message, systemPrompt),
Temperature: 0.3,
MaxTokens: 1000,
Stream: true,
}
body, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("encode streaming request: %w", err)
}
request, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
c.baseURL+"/chat/completions",
bytes.NewReader(body),
)
if err != nil {
return fmt.Errorf("create streaming request: %w", err)
}
request.Header.Set("Authorization", "Bearer "+c.apiKey)
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Accept", "text/event-stream")
response, err := c.httpClient.Do(request)
if err != nil {
return fmt.Errorf("send streaming request: %w", err)
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return decodeAPIError(response)
}
scanner := bufio.NewScanner(response.Body)
scanner.Buffer(
make([]byte, 64*1024),
1024*1024,
)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || !strings.HasPrefix(line, "data:") {
continue
}
data := strings.TrimSpace(
strings.TrimPrefix(line, "data:"),
)
if data == "[DONE]" {
return nil
}
var event StreamResponse
if err := json.Unmarshal([]byte(data), &event); err != nil {
continue
}
if len(event.Choices) == 0 {
continue
}
token := event.Choices[0].Delta.Content
if token == "" {
continue
}
if err := onToken(token); err != nil {
return fmt.Errorf("consume stream token: %w", err)
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("read streaming response: %w", err)
}
return nil
}
func (c *Client) doJSON(
ctx context.Context,
method string,
path string,
input any,
output any,
) error {
body, err := json.Marshal(input)
if err != nil {
return fmt.Errorf("encode request: %w", err)
}
request, err := http.NewRequestWithContext(
ctx,
method,
c.baseURL+path,
bytes.NewReader(body),
)
if err != nil {
return fmt.Errorf("create request: %w", err)
}
request.Header.Set("Authorization", "Bearer "+c.apiKey)
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Accept", "application/json")
response, err := c.httpClient.Do(request)
if err != nil {
return fmt.Errorf("send AI request: %w", err)
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return decodeAPIError(response)
}
if err := json.NewDecoder(response.Body).Decode(output); err != nil {
return fmt.Errorf("decode AI response: %w", err)
}
return nil
}
func decodeAPIError(response *http.Response) error {
const maxErrorBody = 64 * 1024
body, err := io.ReadAll(
io.LimitReader(response.Body, maxErrorBody),
)
if err != nil {
return fmt.Errorf(
"AI API returned status %d",
response.StatusCode,
)
}
return fmt.Errorf(
"AI API returned status %d: %s",
response.StatusCode,
strings.TrimSpace(string(body)),
)
}
func buildMessages(
message string,
systemPrompt string,
) []Message {
messages := make([]Message, 0, 2)
if strings.TrimSpace(systemPrompt) != "" {
messages = append(messages, Message{
Role: "system",
Content: systemPrompt,
})
}
messages = append(messages, Message{
Role: "user",
Content: message,
})
return messages
}
این Client سه قابلیت اصلی دارد:
Complete: دریافت پاسخ متنی کاملStream: دریافت پاسخ بهصورت StreamingCompleteJSON: تبدیل پاسخ مدل به یک ساختار Go
چرا باید http.Client را دوباره استفاده کنیم؟
در کد بالا یک http.Client ساخته و در تمام درخواستها استفاده میشود. ساختن یک Client جدید برای هر درخواست الگوی مناسبی نیست.
طبق مستندات رسمی net/http، Transport مربوط به Client وضعیت داخلی مانند اتصالهای TCP کششده دارد و Clientها باید دوباره استفاده شوند. همچنین http.Client برای استفاده همزمان توسط چند Goroutine مناسب است.
بنابراین این الگو اشتباه است:
func sendRequest() {
client := &http.Client{}
// یک Client جدید در هر درخواست
}
الگوی مناسب:
client := &http.Client{
Timeout: 90 * time.Second,
}
// استفاده مجدد از client
تعریف Handlerهای HTTP
فایل internal/httpapi/handler.go را ایجاد کنید:
package httpapi
import (
"encoding/json"
"fmt"
"log/slog"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"example.com/darvareh-go-ai/internal/ai"
)
type AIService interface {
Complete(
ctx context.Context,
message string,
systemPrompt string,
) (*ai.ChatResult, error)
CompleteJSON(
ctx context.Context,
prompt string,
target any,
) error
Stream(
ctx context.Context,
message string,
systemPrompt string,
onToken func(string) error,
) error
}
type Handler struct {
aiClient AIService
logger *slog.Logger
}
func NewHandler(
aiClient AIService,
logger *slog.Logger,
) *Handler {
return &Handler{
aiClient: aiClient,
logger: logger,
}
}
type ChatRequest struct {
Message string `json:"message" binding:"required,min=1,max=12000"`
SystemPrompt string `json:"system_prompt" binding:"max=2000"`
}
type SummarizeRequest struct {
Text string `json:"text" binding:"required,min=20,max=30000"`
MaxParagraphs int `json:"max_paragraphs" binding:"omitempty,min=1,max=10"`
}
type FeedbackRequest struct {
Text string `json:"text" binding:"required,min=3,max=5000"`
}
type FeedbackAnalysis struct {
Category string `json:"category"`
Sentiment string `json:"sentiment"`
Priority int `json:"priority"`
Summary string `json:"summary"`
}
func (h *Handler) RegisterRoutes(router *gin.Engine) {
router.GET("/health", h.health)
api := router.Group("/api/ai")
{
api.POST("/chat", h.chat)
api.POST("/chat/stream", h.stream)
api.POST("/summarize", h.summarize)
api.POST("/analyze-feedback", h.analyzeFeedback)
}
}
func (h *Handler) health(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
})
}
func (h *Handler) chat(c *gin.Context) {
var input ChatRequest
c.Request.Body = http.MaxBytesReader(
c.Writer,
c.Request.Body,
32*1024,
)
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "ورودی درخواست معتبر نیست.",
})
return
}
result, err := h.aiClient.Complete(
c.Request.Context(),
input.Message,
input.SystemPrompt,
)
if err != nil {
h.logger.Error(
"AI completion failed",
"error", err,
)
c.JSON(http.StatusBadGateway, gin.H{
"error": "دریافت پاسخ از سرویس هوش مصنوعی ناموفق بود.",
})
return
}
c.JSON(http.StatusOK, result)
}
func (h *Handler) stream(c *gin.Context) {
var input ChatRequest
c.Request.Body = http.MaxBytesReader(
c.Writer,
c.Request.Body,
32*1024,
)
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "ورودی درخواست معتبر نیست.",
})
return
}
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no")
c.Status(http.StatusOK)
c.Writer.Flush()
err := h.aiClient.Stream(
c.Request.Context(),
input.Message,
input.SystemPrompt,
func(token string) error {
data, err := json.Marshal(gin.H{
"token": token,
})
if err != nil {
return err
}
if _, err := fmt.Fprintf(
c.Writer,
"data: %s\n\n",
data,
); err != nil {
return err
}
c.Writer.Flush()
return nil
},
)
if err != nil {
h.logger.Error(
"AI streaming failed",
"error", err,
)
errorData, _ := json.Marshal(gin.H{
"error": "stream_failed",
})
_, _ = fmt.Fprintf(
c.Writer,
"event: error\ndata: %s\n\n",
errorData,
)
c.Writer.Flush()
return
}
_, _ = fmt.Fprint(
c.Writer,
"data: [DONE]\n\n",
)
c.Writer.Flush()
}
func (h *Handler) summarize(c *gin.Context) {
var input SummarizeRequest
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "متن یا تنظیمات خلاصهسازی معتبر نیست.",
})
return
}
if input.MaxParagraphs == 0 {
input.MaxParagraphs = 3
}
prompt := fmt.Sprintf(`
متن زیر را حداکثر در %d پاراگراف خلاصه کن.
الزامات:
- اطلاعات مهم متن حفظ شوند.
- مطلب جدیدی اضافه نشود.
- پاسخ به زبان فارسی روان باشد.
- از تکرار پرهیز شود.
متن:
%s`,
input.MaxParagraphs,
input.Text,
)
result, err := h.aiClient.Complete(
c.Request.Context(),
prompt,
"تو یک ویراستار دقیق فارسی هستی.",
)
if err != nil {
h.logger.Error(
"summarization failed",
"error", err,
)
c.JSON(http.StatusBadGateway, gin.H{
"error": "خلاصهسازی متن ناموفق بود.",
})
return
}
c.JSON(http.StatusOK, result)
}
func (h *Handler) analyzeFeedback(c *gin.Context) {
var input FeedbackRequest
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "متن بازخورد معتبر نیست.",
})
return
}
prompt := fmt.Sprintf(`
بازخورد زیر را تحلیل کن:
%s
خروجی دقیقاً باید این ساختار را داشته باشد:
{
"category": "product | delivery | payment | support | other",
"sentiment": "positive | neutral | negative",
"priority": 1,
"summary": "خلاصه کوتاه فارسی"
}
priority باید عددی بین 1 تا 5 باشد.`,
input.Text,
)
var result FeedbackAnalysis
if err := h.aiClient.CompleteJSON(
c.Request.Context(),
prompt,
&result,
); err != nil {
h.logger.Error(
"feedback analysis failed",
"error", err,
)
c.JSON(http.StatusBadGateway, gin.H{
"error": "تحلیل بازخورد ناموفق بود.",
})
return
}
if err := validateFeedback(result); err != nil {
h.logger.Warn(
"model returned invalid feedback",
"error", err,
)
c.JSON(http.StatusBadGateway, gin.H{
"error": "خروجی مدل با ساختار مورد انتظار سازگار نبود.",
})
return
}
c.JSON(http.StatusOK, result)
}
func validateFeedback(result FeedbackAnalysis) error {
validCategories := map[string]bool{
"product": true,
"delivery": true,
"payment": true,
"support": true,
"other": true,
}
validSentiments := map[string]bool{
"positive": true,
"neutral": true,
"negative": true,
}
if !validCategories[result.Category] {
return fmt.Errorf(
"invalid category: %s",
result.Category,
)
}
if !validSentiments[result.Sentiment] {
return fmt.Errorf(
"invalid sentiment: %s",
result.Sentiment,
)
}
if result.Priority < 1 || result.Priority > 5 {
return fmt.Errorf(
"invalid priority: %d",
result.Priority,
)
}
if strings.TrimSpace(result.Summary) == "" {
return fmt.Errorf("summary is empty")
}
return nil
}
یک Import در ابتدای فایل لازم است که در فهرست بالا باید وجود داشته باشد:
import "context"
بنابراین بخش کامل Import شامل context نیز خواهد بود:
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"strings"
)
چرا خروجی مدل را اعتبارسنجی میکنیم؟
مدل هوش مصنوعی یک سیستم احتمالاتی است. حتی اگر از آن بخواهیم JSON مشخصی تولید کند، ممکن است:
- یکی از فیلدها را حذف کند.
- نام مقدار را تغییر دهد.
- عددی خارج از محدوده برگرداند.
- متن توضیحی به پاسخ اضافه کند.
- JSON ناقص تولید کند.
به همین دلیل، خروجی مدل باید مانند ورودی کاربر اعتبارسنجی شود.
برای نمونه، این پاسخ از نظر JSON معتبر است اما با قرارداد برنامه سازگار نیست:
{
"category": "shipping_problem",
"sentiment": "angry",
"priority": 20,
"summary": ""
}
اعتبارسنجی Backend از ورود چنین دادهای به پایگاه داده یا منطق اصلی برنامه جلوگیری میکند.
ساخت فایل اصلی برنامه
فایل cmd/api/main.go را ایجاد کنید:
package main
import (
"context"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"example.com/darvareh-go-ai/internal/ai"
"example.com/darvareh-go-ai/internal/httpapi"
)
func main() {
logger := slog.New(
slog.NewJSONHandler(os.Stdout, nil),
)
apiKey := os.Getenv("DARVAREH_API_KEY")
model := os.Getenv("DARVAREH_MODEL")
baseURL := os.Getenv("DARVAREH_BASE_URL")
if baseURL == "" {
baseURL = "https://api.darvareh.ir/v1"
}
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 20,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
httpClient := &http.Client{
Transport: transport,
Timeout: 120 * time.Second,
}
aiClient, err := ai.NewClient(ai.ClientConfig{
BaseURL: baseURL,
APIKey: apiKey,
Model: model,
HTTPClient: httpClient,
})
if err != nil {
logger.Error(
"AI client configuration is invalid",
"error", err,
)
os.Exit(1)
}
router := gin.New()
router.Use(gin.Logger())
router.Use(gin.Recovery())
handler := httpapi.NewHandler(
aiClient,
logger,
)
handler.RegisterRoutes(router)
server := &http.Server{
Addr: ":" + port,
Handler: router,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
logger.Info(
"HTTP server started",
"port", port,
)
if err := server.ListenAndServe(); err != nil &&
err != http.ErrServerClosed {
logger.Error(
"HTTP server failed",
"error", err,
)
os.Exit(1)
}
}()
stop := make(
chan os.Signal,
1,
)
signal.Notify(
stop,
syscall.SIGINT,
syscall.SIGTERM,
)
<-stop
shutdownContext, cancel := context.WithTimeout(
context.Background(),
15*time.Second,
)
defer cancel()
logger.Info("shutting down HTTP server")
if err := server.Shutdown(shutdownContext); err != nil {
logger.Error(
"graceful shutdown failed",
"error", err,
)
}
}
در این فایل چند نکته مهم رعایت شده است:
- API Key از Environment Variable خوانده میشود.
- یک
http.Clientمشترک ساخته میشود. - Connection Pool تنظیم شده است.
- Timeoutهای سرور مشخص شدهاند.
- Structured Logging با
slogفعال است. - Graceful Shutdown پیادهسازی شده است.
- هنگام توقف برنامه، درخواستهای در حال اجرا فرصت تکمیل دارند.
تنظیم Environment Variable
در 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"
export PORT="8080"
در PowerShell ویندوز:
$env:DARVAREH_API_KEY="YOUR_DARVAREH_API_KEY"
$env:DARVAREH_MODEL="YOUR_MODEL_ID"
$env:DARVAREH_BASE_URL="https://api.darvareh.ir/v1"
$env:PORT="8080"
API Key را داخل فایل Go ننویسید:
// این کار مناسب نیست
apiKey := "sk-..."
فایلهای .env نیز نباید وارد Git شوند. اگر از آنها در محیط توسعه استفاده میکنید، نام فایل را به .gitignore اضافه کنید.
مرتبسازی و بررسی کد
دستورهای زیر را اجرا کنید:
go fmt ./...
go vet ./...
go mod tidy
سپس پروژه را اجرا کنید:
go run ./cmd/api
در صورت موفقیت، باید یک Log مشابه زیر مشاهده کنید:
{
"level": "INFO",
"msg": "HTTP server started",
"port": "8080"
}
آزمایش Health Check
curl http://localhost:8080/health
پاسخ:
{
"status": "ok"
}
این Endpoint برای بررسی وضعیت Container، Load Balancer و سیستم Monitoring مناسب است.
آزمایش Endpoint چت
curl -X POST "http://localhost:8080/api/ai/chat" \
-H "Content-Type: application/json" \
-d '{
"message": "سه مزیت استفاده از Go برای ساخت Microservice را توضیح بده.",
"system_prompt": "پاسخ را کوتاه، دقیق و فارسی بنویس."
}'
نمونه پاسخ:
{
"answer": "Go به دلیل مصرف حافظه مناسب، پشتیبانی ساده از همزمانی و تولید فایل اجرایی مستقل، گزینه مطلوبی برای ساخت Microservice است.",
"model": "YOUR_MODEL_ID",
"usage": {
"prompt_tokens": 41,
"completion_tokens": 52,
"total_tokens": 93
}
}
محتوا و تعداد توکنهای واقعی به مدل و پرامپت بستگی دارند.
آزمایش Endpoint خلاصهسازی
curl -X POST "http://localhost:8080/api/ai/summarize" \
-H "Content-Type: application/json" \
-d '{
"text": "متن طولانی موردنظر خود را در این قسمت قرار دهید. این متن باید حداقل چند جمله داشته باشد تا خلاصهسازی آن قابل ارزیابی باشد.",
"max_paragraphs": 2
}'
آزمایش تحلیل بازخورد
curl -X POST "http://localhost:8080/api/ai/analyze-feedback" \
-H "Content-Type: application/json" \
-d '{
"text": "محصول خوب بود اما سفارش من سه روز دیرتر از زمان اعلامشده تحویل داده شد."
}'
نمونه خروجی:
{
"category": "delivery",
"sentiment": "negative",
"priority": 3,
"summary": "مشتری از تأخیر در تحویل سفارش ناراضی است."
}
پارامتر response_format باید توسط مدل انتخابی پشتیبانی شود. اگر مدل از JSON Mode پشتیبانی نمیکند، میتوانید این پارامتر را حذف کنید؛ اما همچنان باید خروجی را با json.Unmarshal و قوانین برنامه اعتبارسنجی کنید.
پیادهسازی Streaming در Go
Endpoint زیر پاسخ را بهصورت Server-Sent Events یا SSE ارائه میکند:
POST /api/ai/chat/stream
برای آزمایش از گزینه -N در cURL استفاده کنید تا Buffering خروجی غیرفعال شود:
curl -N -X POST "http://localhost:8080/api/ai/chat/stream" \
-H "Content-Type: application/json" \
-d '{
"message": "Goroutine را برای یک برنامهنویس تازهکار توضیح بده.",
"system_prompt": "با یک مثال ساده و به زبان فارسی پاسخ بده."
}'
خروجی بهتدریج دریافت میشود:
data: {"token":"Goroutine"}
data: {"token":" یک"}
data: {"token":" واحد"}
data: {"token":" اجرای سبک..."}
data: [DONE]
چرا پاسخ Streaming بهتر به نظر میرسد؟
در پاسخ معمولی، کاربر باید تا پایان تولید متن منتظر بماند. اگر تولید پاسخ ۲۰ ثانیه زمان ببرد، صفحه برای ۲۰ ثانیه بدون تغییر باقی میماند.
در Streaming، اولین بخشهای پاسخ زودتر نمایش داده میشوند. زمان کل پردازش لزوماً کمتر نمیشود، اما زمان دریافت اولین Token کاهش مییابد و تجربه کاربری بهتر میشود.
Streaming برای این موارد مناسب است:
- چتبات
- دستیار تولید محتوا
- پاسخهای طولانی
- توضیح کد
- تولید گزارش
- دستیار جستوجو
- رابطهای مکالمهای
برای طبقهبندی کوتاه یا دریافت یک JSON کوچک، پاسخ عادی سادهتر است.
دریافت Streaming در JavaScript
Frontend میتواند پاسخ Streaming را با fetch بخواند:
async function streamChat(message, onToken) {
const response = await fetch(
"https://api.example.com/api/ai/chat/stream",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
message,
system_prompt: "پاسخ را فارسی بنویس."
})
}
);
if (!response.ok || !response.body) {
throw new Error(`Request failed: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
buffer += decoder.decode(value, {
stream: true
});
const events = buffer.split("\n\n");
buffer = events.pop() ?? "";
for (const event of events) {
const dataLine = event
.split("\n")
.find(line => line.startsWith("data:"));
if (!dataLine) {
continue;
}
const data = dataLine.slice(5).trim();
if (data === "[DONE]") {
return;
}
const parsed = JSON.parse(data);
onToken(parsed.token);
}
}
}
let answer = "";
streamChat(
"Context در Go چه کاربردی دارد؟",
token => {
answer += token;
document.querySelector("#answer").textContent = answer;
}
);
استفاده از buffer ضروری است؛ زیرا یک Chunk شبکه ممکن است نصف یک رویداد SSE یا چند رویداد را همزمان در خود داشته باشد.
مدیریت Context و لغو درخواست
در Handler این مقدار را به Client منتقل کردیم:
c.Request.Context()
اگر کاربر اتصال را قطع کند، Context درخواست لغو میشود. چون همان Context به http.NewRequestWithContext منتقل شده است، درخواست برنامه Go به سرویس بالادستی نیز امکان توقف پیدا میکند.
این الگو منابع سرور را آزاد میکند و از ادامه پردازشی که دیگر مصرفکنندهای ندارد جلوگیری میکند.
طبق راهنمای رسمی Go درباره لغو عملیات، context.Context میتواند لغو یا پایان مهلت یک عملیات را در زنجیره فراخوانیها منتقل کند.
الگوی مناسب:
func handler(c *gin.Context) {
ctx := c.Request.Context()
result, err := service.Run(ctx)
// ...
}
الگوی نامناسب:
func handler(c *gin.Context) {
result, err := service.Run(
context.Background(),
)
// Context درخواست از بین رفته است
}
تعیین Timeout برای هر عملیات
میتوانید علاوه بر Timeout کلی http.Client، برای یک Endpoint مهلت اختصاصی تعریف کنید:
ctx, cancel := context.WithTimeout(
c.Request.Context(),
45*time.Second,
)
defer cancel()
result, err := h.aiClient.Complete(
ctx,
input.Message,
input.SystemPrompt,
)
Timeout مناسب به کاربرد بستگی دارد:
| کاربرد | بازه شروع پیشنهادی |
|---|---|
| طبقهبندی کوتاه | ۱۰ تا ۳۰ ثانیه |
| استخراج JSON | ۱۵ تا ۴۵ ثانیه |
| چت معمولی | ۳۰ تا ۹۰ ثانیه |
| پاسخ طولانی | Streaming با مهلت بیشتر |
| پردازش چند دقیقهای | Job Queue و Worker |
این مقادیر قطعی نیستند و باید با داده واقعی محصول تنظیم شوند.
مدیریت تاریخچه چت
نمونه فعلی فقط یک پیام کاربر را ارسال میکند. برای چتبات واقعی باید تاریخچه مکالمه را نیز در messages قرار دهید:
messages := []ai.Message{
{
Role: "system",
Content: "تو دستیار پشتیبانی فروشگاه هستی.",
},
{
Role: "user",
Content: "زمان ارسال سفارش چقدر است؟",
},
{
Role: "assistant",
Content: "معمولاً دو تا چهار روز کاری.",
},
{
Role: "user",
Content: "برای شهرستان چطور؟",
},
}
اگر فقط پیام آخر ارسال شود، مدل نمیداند عبارت «برای شهرستان چطور؟» درباره چه موضوعی است.
برای نگهداری تاریخچه میتوانید از موارد زیر استفاده کنید:
- PostgreSQL
- MySQL
- Redis
- MongoDB
- حافظه موقت برای نمونه اولیه
ساختار پیشنهادی جدول پیامها:
id
conversation_id
user_id
role
content
model
prompt_tokens
completion_tokens
created_at
کنترل طول تاریخچه
ارسال تمام پیامهای قدیمی باعث افزایش تعداد توکن (Token)، زمان پاسخ و هزینه میشود.
یک سیاست عملی میتواند چنین باشد:
- System Prompt ثابت را نگه دارید.
- آخرین ۱۰ تا ۲۰ پیام را ارسال کنید.
- پیامهای قدیمیتر را خلاصه کنید.
- اطلاعات ثابت کاربر را جدا از تاریخچه نگه دارید.
- پیامهای غیرضروری را حذف کنید.
- تعداد توکن را پیش از ارسال تخمین بزنید.
ساختار پیامها میتواند شامل این بخشها باشد:
System Prompt
خلاصه مکالمه قدیمی
اطلاعات ضروری کاربر
چند پیام اخیر
پیام جدید
انتخاب Temperature مناسب
پارامتر temperature میزان تنوع پاسخ را کنترل میکند.
| کاربرد | Temperature پیشنهادی |
|---|---|
| استخراج JSON | 0 تا 0.2 |
| طبقهبندی | 0 تا 0.2 |
| خلاصهسازی دقیق | 0.1 تا 0.4 |
| پاسخگویی عمومی | 0.3 تا 0.7 |
| ایدهپردازی | 0.7 تا 1 |
| نوشتن خلاقانه | 0.8 تا 1.2 |
برای عملیات قابلاندازهگیری مانند استخراج داده، مقدار پایینتر معمولاً خروجی باثباتتری ایجاد میکند.
مدیریت خطاهای API
در Client، هر پاسخ خارج از محدوده ۲xx به خطا تبدیل میشود. بهتر است وضعیتهای مختلف را در سطح Handler به پاسخ مناسب تبدیل کنید.
| وضعیت | علت احتمالی | اقدام پیشنهادی |
|---|---|---|
| 400 | ساختار درخواست نامعتبر | بررسی JSON و پارامترها |
| 401 | API Key اشتباه | بررسی متغیر محیطی |
| 403 | دسترسی نامعتبر | بررسی حساب یا مدل |
| 404 | Endpoint یا مدل اشتباه | بررسی Base URL و Model ID |
| 429 | محدودیت درخواست | Backoff و کنترل مصرف |
| 500 تا 599 | خطای موقت سرویس | Retry محدود یا Fallback |
| Timeout | طولانیشدن پاسخ | کاهش خروجی یا استفاده از Streaming |
خطای داخلی سرویس بالادستی را مستقیماً به کاربر نمایش ندهید. جزئیات را در Log ثبت و یک پیام کنترلشده بازگردانید.
پیادهسازی Retry محدود
Retry نباید برای همه خطاها انجام شود. برای نمونه، ارسال دوباره درخواست با API Key اشتباه نتیجهای ندارد.
Retry بیشتر برای این موارد مناسب است:
- بعضی خطاهای ۵xx
- قطع موقت اتصال
- خطای موقت DNS
- وضعیت 429 با رعایت
Retry-After
یک Backoff ساده:
func retryDelay(attempt int) time.Duration {
switch attempt {
case 0:
return 500 * time.Millisecond
case 1:
return time.Second
default:
return 2 * time.Second
}
}
پیش از Retry باید بررسی کنید:
- Context لغو نشده باشد.
- وضعیت خطا موقت باشد.
- تعداد تلاشها محدود باشد.
- درخواست تکراری اثر جانبی ناخواسته ایجاد نکند.
- تأخیر باعث عبور از Deadline نشود.
Rate Limiting در API
اگر Endpoint هوش مصنوعی بدون محدودیت منتشر شود، یک کاربر میتواند تعداد زیادی درخواست ایجاد کند.
Rate Limit را میتوانید بر اساس این شناسهها اعمال کنید:
- User ID
- API Key داخلی
- Organization ID
- IP Address
- نوع اشتراک
برای سرویس تکسرور میتوان از حافظه برنامه استفاده کرد؛ اما در چند Instance بهتر است وضعیت Rate Limit در Redis یا سامانه مشترک نگهداری شود.
یک سیاست نمونه:
کاربر عادی: 20 درخواست در دقیقه
کاربر حرفهای: 100 درخواست در دقیقه
سازمان: بر اساس قرارداد
Rate Limiting جایگزین محدودیت هزینه نیست. بهتر است سقف مصرف روزانه یا ماهانه نیز تعریف شود.
کاهش هزینه API هوش مصنوعی
برای کنترل هزینه در برنامه Go:
- طول ورودی را محدود کنید.
max_tokensرا متناسب با کاربرد تنظیم کنید.- تاریخچه قدیمی را خلاصه کنید.
- پاسخهای تکراری را Cache کنید.
- از مدل مناسب همان وظیفه استفاده کنید.
- درخواستهای نامعتبر را پیش از ارسال رد کنید.
- مصرف هر کاربر را ثبت کنید.
- سقف روزانه و ماهانه تعریف کنید.
- System Promptهای بسیار طولانی را بازبینی کنید.
- عملیات سنگین را وارد صف کنید.
برای مشاهده قیمت بهروز مدلها به صفحه مدلهای درواره مراجعه کنید.
کشکردن پاسخها
اگر یک پرامپت ثابت بارها تکرار میشود، میتوانید پاسخ را Cache کنید.
کلید Cache باید حداقل شامل این موارد باشد:
Model
System Prompt
User Prompt
Temperature
Max Tokens
Prompt Version
ساخت Hash در Go:
package cachekey
import (
"crypto/sha256"
"encoding/hex"
)
func Create(value string) string {
sum := sha256.Sum256([]byte(value))
return hex.EncodeToString(sum[:])
}
کش برای این سناریوها مناسب است:
- سؤالهای متداول
- خلاصه یک سند ثابت
- تولید توضیح برای دادهای که تغییر نمیکند
- طبقهبندی ورودی تکراری
برای پاسخ شخصیسازیشده یا اطلاعات لحظهای، سیاست Cache باید با دقت بیشتری طراحی شود.
ثبت مصرف توکن
اگر پاسخ API شامل اطلاعات usage باشد، آن را در پایگاه داده ثبت کنید:
type UsageRecord struct {
UserID string
Model string
PromptTokens int
CompletionTokens int
TotalTokens int
DurationMS int64
Status string
}
این اطلاعات برای موارد زیر مفید است:
- محاسبه هزینه هر قابلیت
- محدودکردن مصرف کاربران
- شناسایی Endpointهای پرهزینه
- مقایسه مدلها
- بررسی افزایش ناگهانی مصرف
- طراحی بستههای اشتراک
API Key، پرامپت حساس یا اطلاعات خصوصی کاربر را بدون ضرورت در Log ذخیره نکنید.
نوشتن تست برای Handler
برای تست HTTP در Go میتوان از Package استاندارد net/http/httptest استفاده کرد.
ابتدا یک Fake Client ایجاد کنید:
package httpapi_test
import (
"context"
"example.com/darvareh-go-ai/internal/ai"
)
type fakeAIClient struct {
result *ai.ChatResult
err error
}
func (f *fakeAIClient) Complete(
ctx context.Context,
message string,
systemPrompt string,
) (*ai.ChatResult, error) {
return f.result, f.err
}
func (f *fakeAIClient) CompleteJSON(
ctx context.Context,
prompt string,
target any,
) error {
return f.err
}
func (f *fakeAIClient) Stream(
ctx context.Context,
message string,
systemPrompt string,
onToken func(string) error,
) error {
if f.err != nil {
return f.err
}
for _, token := range []string{
"پاسخ",
" ",
"آزمایشی",
} {
if err := onToken(token); err != nil {
return err
}
}
return nil
}
نمونه تست Endpoint چت:
package httpapi_test
import (
"bytes"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"example.com/darvareh-go-ai/internal/ai"
"example.com/darvareh-go-ai/internal/httpapi"
)
func TestChatEndpoint(t *testing.T) {
gin.SetMode(gin.TestMode)
client := &fakeAIClient{
result: &ai.ChatResult{
Answer: "پاسخ آزمایشی",
Model: "test-model",
},
}
logger := slog.New(
slog.NewTextHandler(io.Discard, nil),
)
router := gin.New()
handler := httpapi.NewHandler(
client,
logger,
)
handler.RegisterRoutes(router)
body := bytes.NewBufferString(`{
"message": "سلام"
}`)
request := httptest.NewRequest(
http.MethodPost,
"/api/ai/chat",
body,
)
request.Header.Set(
"Content-Type",
"application/json",
)
recorder := httptest.NewRecorder()
router.ServeHTTP(
recorder,
request,
)
if recorder.Code != http.StatusOK {
t.Fatalf(
"expected status 200, got %d",
recorder.Code,
)
}
expected := `"answer":"پاسخ آزمایشی"`
if !bytes.Contains(
recorder.Body.Bytes(),
[]byte(expected),
) {
t.Fatalf(
"unexpected response: %s",
recorder.Body.String(),
)
}
}
دستور اجرای تست:
go test ./...
توصیه رسمی Gin نیز استفاده از httptest برای تست Handlerهای HTTP است که در مستندات تست Gin توضیح داده شده است.
تست اتصال واقعی به API
تستهای واحد نباید در هر بار اجرا به مدل واقعی درخواست بفرستند؛ زیرا:
- هزینه دارند.
- به شبکه وابستهاند.
- ممکن است کند باشند.
- پاسخ مدل کاملاً ثابت نیست.
بهتر است تستها را تفکیک کنید:
- Unit Test با Fake Client
- Handler Test با
httptest - Integration Test محدود با API واقعی
- Evaluation Test برای کیفیت خروجی
Integration Test واقعی را میتوانید فقط در Pipeline مشخص یا بهصورت دستی اجرا کنید:
RUN_AI_INTEGRATION_TESTS=true go test ./...
آزمایش کیفیت مدل
وضعیت HTTP موفق به معنی مناسببودن پاسخ نیست. برای هر قابلیت، یک مجموعه داده آزمایشی بسازید.
برای تحلیل بازخورد میتوانید این موارد را آزمایش کنید:
- نظر مثبت
- نظر منفی
- نظر خنثی
- متن دارای چند موضوع
- متن بسیار کوتاه
- متن فارسی و انگلیسی ترکیبی
- متن نامرتبط
- متن دارای غلط املایی
- ورودی طولانی
معیارها:
- JSON معتبر باشد.
- Category معتبر باشد.
- Sentiment درست تشخیص داده شود.
- Priority در محدوده باشد.
- Summary خالی نباشد.
- زمان پاسخ قابلقبول باشد.
- مصرف توکن بیشازحد نباشد.
پس از تغییر مدل یا پرامپت، همین مجموعه را دوباره اجرا کنید.
فعالکردن CORS در Gin
اگر Frontend روی دامنه دیگری اجرا میشود، باید CORS را تنظیم کنید.
Package رسمی Middleware را نصب کنید:
go get github.com/gin-contrib/cors
سپس در main.go:
import (
"time"
"github.com/gin-contrib/cors"
)
Middleware:
router.Use(cors.New(cors.Config{
AllowOrigins: []string{
"https://example.com",
},
AllowMethods: []string{
"GET",
"POST",
"OPTIONS",
},
AllowHeaders: []string{
"Origin",
"Content-Type",
"Authorization",
},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
آدرس https://example.com را با دامنه Frontend خود جایگزین کنید.
از AllowOrigins: []string{"*"} برای API دارای Cookie یا اطلاعات حساس استفاده نکنید. CORS نیز جایگزین احراز هویت و مجوزدهی نیست.
ساخت Dockerfile
فایل Dockerfile را ایجاد کنید:
FROM golang:1.25-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux \
go build \
-trimpath \
-ldflags="-s -w" \
-o /out/api \
./cmd/api
FROM alpine:3.22
RUN addgroup -S appgroup \
&& adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /out/api /app/api
USER appuser
EXPOSE 8080
ENTRYPOINT ["/app/api"]
نسخه Go در Dockerfile را با نسخه موردنیاز پروژه و Gin هماهنگ کنید.
ساخت Image:
docker build -t darvareh-go-ai .
اجرای Container:
docker run --rm \
-p 8080:8080 \
-e DARVAREH_API_KEY="YOUR_DARVAREH_API_KEY" \
-e DARVAREH_MODEL="YOUR_MODEL_ID" \
-e DARVAREH_BASE_URL="https://api.darvareh.ir/v1" \
darvareh-go-ai
API Key را داخل Dockerfile یا Image قرار ندهید.
تنظیم Production Mode در Gin
در محیط Production مقدار زیر را تنظیم کنید:
export GIN_MODE=release
یا هنگام اجرای Container:
docker run \
-e GIN_MODE=release \
...
Release Mode خروجیهای Debug غیرضروری Gin را غیرفعال میکند. تنظیمات استقرار Gin در مستندات رسمی Deployment توضیح داده شده است.
نکات Reverse Proxy برای Streaming
اگر برنامه پشت Nginx، CDN یا Reverse Proxy اجرا شود، ممکن است پاسخ Streaming بافر شود و تمام Tokenها یکباره نمایش داده شوند.
نمونه تنظیم Nginx برای مسیر Streaming:
location /api/ai/chat/stream {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 180s;
proxy_send_timeout 180s;
gzip off;
}
پیش از تغییر تنظیمات، Endpoint را مستقیماً روی پورت برنامه آزمایش کنید. اگر Streaming مستقیم درست است اما پشت Proxy یکباره نمایش داده میشود، احتمالاً مشکل از Buffering واسط است.
اجرای وظایف طولانی با Job Queue
درخواست HTTP برای کارهایی که چند دقیقه زمان میبرند مناسب نیست.
برای پردازش طولانی بهتر است:
- کاربر درخواست را ثبت کند.
- Backend یک Job بسازد.
- Job وارد Queue شود.
- Worker پردازش را انجام دهد.
- نتیجه در پایگاه داده ذخیره شود.
- کاربر وضعیت را با Job ID دریافت کند.
نمونه پاسخ اولیه:
{
"job_id": "job_8f2a1",
"status": "queued"
}
این الگو برای موارد زیر مناسب است:
- تحلیل تعداد زیادی سند
- پردازش فایلهای طولانی
- تولید محتوای گروهی
- ساخت گزارشهای بزرگ
- اجرای چند درخواست متوالی
- پردازش فایل صوتی یا تصویری
استفاده از Goroutine؛ اشتباه رایج
ممکن است وسوسه شوید برای هر درخواست مدل یک Goroutine بدون مدیریت بسازید:
go func() {
_, _ = client.Complete(
context.Background(),
message,
systemPrompt,
)
}()
این کد چند مشکل دارد:
- Context کاربر از بین میرود.
- نتیجه عملیات مشخص نیست.
- خطا مدیریت نمیشود.
- تعداد Goroutineها ممکن است نامحدود شود.
- خاموششدن برنامه میتواند کار را ناقص کند.
برای Background Job از Queue، Worker Pool و ذخیره وضعیت استفاده کنید. Goroutine بهتنهایی جایگزین سامانه مدیریت Job نیست.
چکلیست آمادهسازی برای Production
قبل از انتشار، موارد زیر را بررسی کنید:
- API Key در کد یا Repository وجود ندارد.
- Frontend مستقیماً به درواره متصل نمیشود.
- ورودی JSON اعتبارسنجی میشود.
- اندازه بدنه درخواست محدود شده است.
- Endpointها احراز هویت دارند.
- Rate Limiting فعال است.
- Timeout مشخص شده است.
- Context درخواست به سرویسهای پاییندستی منتقل میشود.
http.Clientبین درخواستها به اشتراک گذاشته میشود.- تعداد Retry محدود است.
- پاسخ JSON مدل اعتبارسنجی میشود.
- میزان مصرف توکن ثبت میشود.
- سقف هزینه کاربران مشخص است.
- API Key در Logها ذخیره نمیشود.
- CORS فقط برای دامنههای لازم فعال است.
- Streaming پشت Reverse Proxy آزمایش شده است.
- Graceful Shutdown فعال است.
- Health Check وجود دارد.
- تستهای واحد و یکپارچه نوشته شدهاند.
- برای عملیات طولانی Job Queue در نظر گرفته شده است.
- مدل و پرامپت نسخهبندی شدهاند.
- رفتار Fallback تعریف شده است.
خطاهای رایج اتصال Go به API هوش مصنوعی
خطای API key is required
متغیر محیطی تنظیم نشده است:
export DARVAREH_API_KEY="YOUR_DARVAREH_API_KEY"
در Windows PowerShell:
$env:DARVAREH_API_KEY="YOUR_DARVAREH_API_KEY"
خطای model ID is required
شناسه مدل را تنظیم کنید:
export DARVAREH_MODEL="YOUR_MODEL_ID"
شناسه مدل را از صفحه مدلهای درواره دریافت کنید.
خطای 401
موارد زیر را بررسی کنید:
- API Key صحیح باشد.
- فاصله اضافی در مقدار کلید وجود نداشته باشد.
- Header بهشکل صحیح ارسال شود.
- متغیر محیطی در همان Shell تعریف شده باشد.
Header صحیح:
Authorization: Bearer YOUR_DARVAREH_API_KEY
خطای 404
آدرس نهایی Chat Completions باید به این شکل باشد:
https://api.darvareh.ir/v1/chat/completions
Base URL:
https://api.darvareh.ir/v1
مسیر Client:
/chat/completions
خطای Timeout
این موارد را بررسی کنید:
- مقدار
max_tokensبیشازحد نباشد. - تاریخچه طولانی غیرضروری حذف شود.
- مدل سریعتری انتخاب شود.
- برای پاسخ طولانی از Streaming استفاده شود.
- Timeout Client و Reverse Proxy هماهنگ باشند.
پاسخ Streaming یکباره ظاهر میشود
curl -Nرا استفاده کنید.Flushرا پس از هر Token اجرا کنید.- Buffering در Nginx را غیرفعال کنید.
- فشردهسازی مسیر SSE را بررسی کنید.
- ابتدا مسیر مستقیم برنامه را آزمایش کنید.
خطای invalid character هنگام Parse JSON
ممکن است مدل علاوه بر JSON، Markdown یا توضیح متنی تولید کرده باشد.
راهکارها:
- System Prompt را دقیقتر کنید.
- Temperature را روی صفر قرار دهید.
- از مدل دارای JSON Mode استفاده کنید.
response_formatرا فعال کنید.- خروجی را پیش از استفاده اعتبارسنجی کنید.
آیا باید از Gin استفاده کنیم یا net/http؟
هر دو انتخاب معتبر هستند.
استفاده از net/http
مناسب برای:
- سرویسهای کوچک
- وابستگی حداقلی
- کنترل کامل روی HTTP
- تیمی که کتابخانه استاندارد را ترجیح میدهد
استفاده از Gin
مناسب برای:
- Endpointهای متعدد
- نیاز به Middleware
- اعتبارسنجی ورودی
- توسعه سریع REST API
- تیمی که ساختار Framework را ترجیح میدهد
اتصال به API درواره به Gin وابسته نیست. Client نوشتهشده در این مقاله را میتوانید در Echo، Fiber، Chi یا یک برنامه مبتنی بر net/http نیز استفاده کنید.
چرا درواره برای پروژههای Go مناسب است؟
اگر برنامه شما به مدلهای مختلف هوش مصنوعی نیاز داشته باشد، اتصال جداگانه به هر ارائهدهنده باعث افزایش کد، تنظیمات و هزینه نگهداری میشود.
درواره یک API یکپارچه در اختیار توسعهدهندگان قرار میدهد. در نتیجه میتوانید:
- از یک Base URL ثابت استفاده کنید.
- API Key را در Backend نگه دارید.
- مدل مناسب هر کاربرد را انتخاب کنید.
- بدون بازنویسی کل Client، مدل را تغییر دهید.
- مصرف مدلها را از یک مسیر مدیریت کنید.
- از Go، Python، Java، C#، Node.js و سایر زبانها استفاده کنید.
برای شروع، در درواره ثبتنام کنید، API Key بسازید و مدل موردنظر را از فهرست مدلها انتخاب کنید.
پرسشهای متداول
آیا میتوان با Go برنامه هوش مصنوعی ساخت؟
بله. Go برای ساخت Backend، REST API، Microservice، چتبات، Worker و سرویسهای متصل به مدلهای هوش مصنوعی مناسب است.
آیا Go برای آموزش مدل هوش مصنوعی مناسب است؟
بیشتر ابزارهای آموزش مدل با Python توسعه یافتهاند. Go معمولاً برای لایه API، پردازش همزمان، مدیریت درخواستها و اتصال محصول به مدل استفاده میشود.
Golang با Go چه تفاوتی دارد؟
نام رسمی زبان Go است. واژه Golang به دلیل نام دامنه قدیمی پروژه و جستوجوی آسانتر رایج شده است. هر دو معمولاً به همان زبان اشاره میکنند.
آیا برای اتصال Go به هوش مصنوعی به SDK نیاز داریم؟
خیر. کتابخانه استاندارد net/http و encoding/json برای ارسال درخواست و دریافت پاسخ کافی هستند.
آیا میتوان از Gin برای چتبات استفاده کرد؟
بله. Gin میتواند Endpoint چت، Streaming، احراز هویت، Rate Limiting و مدیریت تاریخچه را در Backend ارائه کند.
آیا API Key را میتوان در Frontend قرار داد؟
خیر. API Key باید فقط در Backend نگهداری شود. قرار دادن آن در JavaScript، اپلیکیشن موبایل یا Repository عمومی میتواند باعث افشای کلید شود.
آیا میتوان پاسخ مدل را Stream کرد؟
بله. با تنظیم stream: true و خواندن SSE میتوانید Tokenها را هنگام تولید دریافت کنید.
آیا Go میتواند چند درخواست هوش مصنوعی را همزمان پردازش کند؟
بله. سرور HTTP در Go درخواستها را همزمان مدیریت میکند. بااینحال باید محدودیت اتصال، Rate Limit و هزینه را کنترل کنید.
آیا خروجی مدل همیشه JSON معتبر است؟
خیر. خروجی باید با json.Unmarshal خوانده و سپس بر اساس قوانین برنامه اعتبارسنجی شود.
بهترین مدل برای Go کدام است؟
انتخاب مدل به نوع کاربرد، کیفیت، سرعت، هزینه و طول Context بستگی دارد. Go به مدل خاصی وابسته نیست. مدلهای موجود و قیمت آنها را در صفحه مدلهای درواره بررسی کنید.
چگونه هزینه API را کاهش دهیم؟
با محدودکردن طول ورودی، کاهش خروجی، خلاصهسازی تاریخچه، کش پاسخهای تکراری، انتخاب مدل متناسب و ثبت مصرف هر کاربر میتوانید هزینه را کنترل کنید.
آیا این پروژه روی Linux اجرا میشود؟
بله. میتوانید برنامه Go را مستقیماً یا با Docker روی Linux اجرا کنید.
آیا میتوان این سرویس را چندسروری کرد؟
بله. اگر وضعیت مکالمه، Rate Limit و Cache را در سرویسهای مشترکی مانند PostgreSQL و Redis نگه دارید، میتوانید چند Instance از API اجرا و آنها را پشت Load Balancer قرار دهید.
جمعبندی
Go و Gin ترکیب مناسبی برای ساخت Backend برنامههای هوش مصنوعی هستند. Go کتابخانههای استاندارد مناسبی برای HTTP، JSON، Context و پردازش همزمان دارد و Gin نیز ساخت REST API، Routing و اعتبارسنجی را ساده میکند.
در این آموزش یک پروژه عملی ساختیم که:
- به API درواره متصل میشود.
- API Key را خارج از کد نگه میدارد.
- Endpoint چت ارائه میدهد.
- متن را خلاصه میکند.
- بازخورد را به JSON تبدیل میکند.
- پاسخ مدل را بهصورت Streaming نمایش میدهد.
- Context و Cancellation را مدیریت میکند.
- از Connection Pool و Timeout استفاده میکند.
- قابلیت تست با Fake Client دارد.
- با Docker قابلاستقرار است.
- برای توسعه به یک سرویس Production آماده شده است.
برای شروع ساخت برنامه هوش مصنوعی با Go، وارد درواره شوید، یک API Key ایجاد کنید و شناسه مدل متناسب با پروژه را از صفحه مدلها انتخاب کنید.
مقالات مرتبط
- API هوش مصنوعی چیست؟ راهنمای کامل توسعهدهندگان
- آموزش دریافت API Key هوش مصنوعی
- API سازگار با OpenAI چیست؟
- چگونه API هوش مصنوعی را به نرمافزار خود اضافه کنیم؟
- آموزش Streaming API در هوش مصنوعی
- راهنمای Structured Outputs و JSON Schema
- توکن در API هوش مصنوعی چیست؟
- روشهای کاهش هزینه API هوش مصنوعی
- ساخت API هوش مصنوعی آماده Production
- مانیتورینگ و Observability سرویسهای هوش مصنوعی
- Fallback در سرویسهای هوش مصنوعی
- بهترین API هوش مصنوعی برای توسعهدهندگان
برای مطالعه شرایط استفاده و محدودیتهای مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.