ساخت یک اپلیکیشن چت ترکیبی هوش مصنوعی روی GKE با Gemma و Gemini

۱. مقدمه

نمای کلی

در این آزمایشگاه، شما یک برنامه چت فول‌استک مبتنی بر هوش مصنوعی را بر روی موتور گوگل کوبرنتیز (GKE) خواهید ساخت و مستقر خواهید کرد. این برنامه "هیبریدی" یک الگوی معماری قدرتمند را نشان می‌دهد: توانایی جابجایی یکپارچه بین یک مدل باز خود-میزبان (Gemma 3 12B) که مستقیماً در کلاستر شما اجرا می‌شود و یک سرویس هوش مصنوعی مدیریت‌شده (Gemini 2.5 Flash از طریق Vertex AI).

                                   +----------------------+
                                   |   User (Web Browser) |
                                   +-----------+----------+
                                               |
                                               v
+----------------------------------------------+---------------------------------------------+
| Google Cloud Platform                        |                                             |
|   |                                  +-------+-------+                                     |
|   |                                  | Load Balancer |                                     |
|   |                                  +-------+-------+                                     |
|   |                                          v                                             |
|   +------------------------------------------+-----------------------------------------+   |
|   |  Google Kubernetes Engine (GKE)          |                                         |   |
|   |                                          v                                         |   |
|   |                              +-----------+-----------+                             |   |
|   |                              |    Gradio Chat App    |                             |   |
|   |                              +--+-----------------+--+                             |   |
|   |                                 |                 |                                |   |
|   |                   (Self-hosted) |                 | (Managed via SDK)              |   |
|   |                                 v                 |                                |   |
|   |                  +--------------+---+             |                                |   |
|   |                  | Gemma 3 Model    |             |                                |   |
|   |                  | (GPU Node)       |             |                                |   |
|   |                  +------------------+             |                                |   |
|   +---------------------------------------------------|--------------------------------+   |
|                                                       |                                    |
|                                                       v                                    |
|                                            +----------+-----------+                        |
|                                            | Vertex AI (Gemini)   |                        |
|                                            +----------------------+                        |
|                                                       | (Save History)                     |
|                                                       v                                    |
|                                            +----------+-----------+                        |
|                                            | Firestore Database   |                        |
|                                            +----------------------+                        |
+--------------------------------------------------------------------------------------------+

شما از Terraform برای تأمین زیرساخت، شامل یک خوشه GKE Autopilot و یک پایگاه داده Firestore برای ذخیره تاریخچه جلسات چت استفاده خواهید کرد. سپس کد برنامه پایتون را برای مدیریت مکالمات چند نوبتی، رابط کاربری با هر دو مدل هوش مصنوعی و استقرار برنامه نهایی با استفاده از Cloud Build و Skaffold تکمیل خواهید کرد.

آنچه یاد خواهید گرفت

  • فراهم‌سازی زیرساخت GKE و Firestore با استفاده از Terraform
  • با استفاده از مانیفست‌های Kubernetes، یک مدل زبان بزرگ (Gemma) را روی GKE Autopilot مستقر کنید.
  • یک رابط چت Gradio را در پایتون پیاده‌سازی کنید که بتواند بین بک‌اندهای مختلف هوش مصنوعی جابجا شود.
  • از Firestore برای ذخیره و بازیابی تاریخچه جلسات چت استفاده کنید.
  • پیکربندی هویت بار کاری (Workload Identity) برای دسترسی ایمن به بارهای کاری GKE شما به سرویس‌های Google Cloud (Vertex AI، Firestore).

پیش‌نیازها

  • یک پروژه گوگل کلود با قابلیت پرداخت.
  • آشنایی اولیه با پایتون، Kubernetes و ابزارهای استاندارد خط فرمان.
  • یک توکن Hugging Face با دسترسی به مدل‌های Gemma.

۲. راه‌اندازی پروژه

  1. اگر از قبل حساب گوگل ندارید، باید یک حساب گوگل ایجاد کنید .
    • به جای حساب کاری یا تحصیلی از یک حساب شخصی استفاده کنید. حساب‌های کاری و تحصیلی ممکن است محدودیت‌هایی داشته باشند که مانع از فعال کردن APIهای مورد نیاز برای این آزمایشگاه توسط شما شود.
  2. وارد کنسول ابری گوگل شوید.
  3. فعال کردن پرداخت در کنسول ابری
    • تکمیل این آزمایشگاه باید کمتر از ۱ دلار آمریکا از طریق منابع ابری هزینه داشته باشد.
    • شما می‌توانید مراحل انتهای این آزمایش را برای حذف منابع دنبال کنید تا از هزینه‌های بیشتر جلوگیری شود.
    • کاربران جدید واجد شرایط استفاده از دوره آزمایشی رایگان ۳۰۰ دلاری هستند.
  4. یک پروژه جدید ایجاد کنید یا از یک پروژه موجود دوباره استفاده کنید.

ویرایشگر Cloud Shell را باز کنید

  1. برای دسترسی مستقیم به ویرایشگر Cloud Shell ، روی این لینک کلیک کنید.
  2. اگر امروز در هر مرحله‌ای از شما خواسته شد که مجوز دهید، برای ادامه روی تأیید کلیک کنید. برای تأیید Cloud Shell کلیک کنید
  3. اگر ترمینال در پایین صفحه نمایش داده نشد، آن را باز کنید:
    • روی مشاهده کلیک کنید
    • روی ترمینال کلیک کنید باز کردن ترمینال جدید در ویرایشگر Cloud Shell
  4. در ترمینال، پروژه خود را با این دستور تنظیم کنید:
    • قالب:
      gcloud config set project [PROJECT_ID]
      
    • مثال:
      gcloud config set project lab-project-id-example
      
    • اگر نمی‌توانید شناسه پروژه خود را به خاطر بیاورید:
      • شما می‌توانید تمام شناسه‌های پروژه خود را با دستور زیر فهرست کنید:
        gcloud projects list | awk '/PROJECT_ID/{print $2}'
        
      شناسه پروژه را در ترمینال ویرایشگر Cloud Shell تنظیم کنید
  5. شما باید این پیام را ببینید:
    Updated property [core/project].
    
    اگر یک WARNING مشاهده کردید و از شما پرسیده شد Do you want to continue (Y/n)? احتمالاً شناسه پروژه را اشتباه وارد کرده‌اید. n را فشار دهید، Enter را بزنید و دوباره سعی کنید دستور gcloud config set project اجرا کنید.

مخزن را کلون کنید

در ترمینال Cloud Shell خود، مخزن پروژه را کلون کنید و به دایرکتوری پروژه بروید:

git clone https://github.com/GoogleCloudPlatform/devrel-demos.git
cd devrel-demos/containers/gradio-chat-gke

لحظه‌ای به بررسی ساختار پروژه بپردازید:

gradio-chat-gke/
├── app/
   ├── app.py                # Main application logic (you will edit this)
   ├── requirements.txt      # Python dependencies
   └── themes.py             # UI theming
├── deploy/
   ├── chat-deploy.yaml      # Kubernetes deployment for the chat app
   ├── Dockerfile            # Container definition for the chat app
   └── gemma3-12b-deploy.yaml# Kubernetes deployment for Gemma model
├── infra/
   └── main.tf               # Terraform infrastructure definition
└── skaffold.yaml             # Skaffold configuration for building/deploying

تنظیم متغیرهای محیطی

متغیرهای محیطی را برای شناسه پروژه و شماره پروژه خود تنظیم کنید. این متغیرها توسط Terraform و دستورات بعدی استفاده خواهند شد.

export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
export PROJECT_NUMBER=$(gcloud projects describe $GOOGLE_CLOUD_PROJECT --format="value(projectNumber)")
export REGION=us-central1

فعال کردن API مدیریت منابع ابری

Terraform برای مدیریت منابع پروژه شما نیاز به فعال بودن API مدیریت منابع ابری (Cloud Resource Manager API) دارد، بنابراین ابتدا باید آن را فعال کنیم. بعداً، برنامه چت خود را با Skaffold مستقر خواهیم کرد که از Cloud Build برای ساخت تصویر کانتینر ما استفاده می‌کند. ما API ذخیره‌سازی را فعال کرده و اکنون سطل مورد نیاز برای Cloud Build را ایجاد خواهیم کرد. ما از خود Terraform برای فعال کردن بقیه APIهای مورد نیاز برای این پروژه استفاده خواهیم کرد.

gcloud services enable cloudresourcemanager.googleapis.com storage-api.googleapis.com

ایجاد سطل آماده‌سازی ساخت ابری

Skaffold از Google Cloud Build استفاده می‌کند که برای آماده‌سازی کد منبع شما به یک مخزن ذخیره‌سازی ابری نیاز دارد.

اکنون آن را ایجاد کنید تا از وجود آن اطمینان حاصل کنید:

gcloud storage buckets create gs://${GOOGLE_CLOUD_PROJECT}_cloudbuild

(اگر با خطایی مبنی بر وجود سطل مواجه شدید، می‌توانید با خیال راحت آن را نادیده بگیرید.)

۳. تأمین زیرساخت با Terraform

ما از Terraform برای راه‌اندازی منابع لازم Google Cloud استفاده خواهیم کرد. این امر یک محیط قابل تکرار و سازگار را تضمین می‌کند.

  1. به دایرکتوری زیرساخت بروید:
    cd infra
    

این فایل APIهای اضافی مورد نیاز برای این پروژه را تعریف می‌کند: cloudbuild، artifactregistry، container (gke)، firestore و aiplatform (vertexai). برای مشاهده نحوه فعال‌سازی APIها از طریق Terraform، به فایل یا فایل زیر نگاهی بیندازید:

resource "google_project_service" "cloudbuild" {
  service            = "cloudbuild.googleapis.com"
  disable_on_destroy = false
  project            = var.project_id
}

resource "google_project_service" "artifactregistry" {
  service            = "artifactregistry.googleapis.com"
  disable_on_destroy = false
  project            = var.project_id
}

resource "google_project_service" "container" {
  service            = "container.googleapis.com"
  disable_on_destroy = false
  project            = var.project_id
}

resource "google_project_service" "firestore" {
  service            = "firestore.googleapis.com"
  disable_on_destroy = false
  project            = var.project_id
}

resource "google_project_service" "vertexai" {
  service            = "aiplatform.googleapis.com"
  disable_on_destroy = false
  project            = var.project_id
}

خوشه GKE را تعریف کنید

infra/main.tf در ویرایشگر خود باز کنید. چندین کامنت # TODO خواهید دید. می‌توانید آن را به صورت دستی باز کنید یا از این دستور برای باز کردن فایل در ویرایشگر استفاده کنید:

cloudshell edit main.tf

ابتدا باید کلاستر Kubernetes خود را تعریف کنیم. ما از GKE Autopilot استفاده خواهیم کرد که برای بارهای کاری هوش مصنوعی ایده‌آل است زیرا مدیریت گره را به طور خودکار انجام می‌دهد.

# TODO: Create a GKE Autopilot Cluster و بلوک زیر را در زیر آن اضافه کنید:

# Create a GKE Autopilot Cluster
resource "google_container_cluster" "primary" {
  name     = var.cluster_name
  location = var.region
  project  = var.project_id

  # Enable Autopilot mode
  enable_autopilot = true

  deletion_protection = false

  # Networking
  network    = "default"
  subnetwork = "projects/${var.project_id}/regions/${var.region}/subnetworks/default"

  # Timeout for cluster creation
  timeouts {
    create = "30m"
    update = "30m"
  }

  depends_on = [google_project_service.container]
}

به enable_autopilot = true توجه کنید. همین خط ما را از مدیریت گره‌های مختلف، مقیاس‌بندی خودکار و دسته‌بندی بار کاری پردازنده گرافیکی نجات می‌دهد.

تعریف پایگاه داده Firestore

در مرحله بعد، به مکانی برای ذخیره تاریخچه چت خود نیاز داریم. Firestore یک پایگاه داده NoSQL بدون سرور است که کاملاً با این نیاز مطابقت دارد.

پیدا کردن # TODO: Create a Firestore Database و اضافه کردن:

resource "google_firestore_database" "database" {
  project     = var.project_id
  name        = "chat-app-db"
  location_id = "nam5"
  type        = "FIRESTORE_NATIVE"

  depends_on = [google_project_service.firestore]
}

پس از افزودن منبع پایگاه داده، # TODO: Create an initial Firestore Document پیدا کنید و بلوک زیر را اضافه کنید. این منبع یک سند placeholder اولیه در مجموعه ما ایجاد می‌کند که برای مقداردهی اولیه ساختار پایگاه داده مفید است.

resource "google_firestore_document" "initial_document" {
  project     = var.project_id
  collection  = "chat_sessions"
  document_id = "initialize"
  fields = <<EOF
  EOF

  depends_on = [google_firestore_database.database]
}

تعریف هویت حجم کار

در نهایت، باید امنیت را پیکربندی کنیم. ما می‌خواهیم پادهای Kubernetes ما بتوانند بدون نیاز به مدیریت هیچ گونه اطلاعات محرمانه یا کلید API به Vertex AI و Firestore دسترسی داشته باشند. ما این کار را با Workload Identity انجام می‌دهیم.

ما نقش‌های IAM لازم را به حساب سرویس Kubernetes (KSA) که برنامه ما از آن استفاده خواهد کرد، اعطا خواهیم کرد.

توجه: حساب کاربری سرویس Kubernetes ( gradio-chat-ksa ) که در این bindings به آن اشاره شده است، هنوز وجود ندارد! این حساب کاربری بعداً هنگام استقرار برنامه در کلاستر ایجاد خواهد شد. کاملاً (و یک روش معمول) است که این bindings IAM را از قبل آماده کنیم.

# TODO: Configure Workload Identity IAM bindings پیدا کنید و موارد زیر را اضافه کنید:

locals {
  ksa_principal = "principal://iam.googleapis.com/projects/${var.project_number}/locations/global/workloadIdentityPools/${var.project_id}.svc.id.goog/subject/ns/default/sa/gradio-chat-ksa"
}

resource "google_project_iam_member" "ksa_token_creator" {
  project = var.project_id
  role    = "roles/iam.serviceAccountTokenCreator"
  member  = local.ksa_principal
}

resource "google_project_iam_member" "ksa_vertex_user" {
  project = var.project_id
  role    = "roles/aiplatform.user"
  member  = local.ksa_principal
}

resource "google_project_iam_member" "ksa_datastore_user" {
  project = var.project_id
  role    = "roles/datastore.user"
  member  = local.ksa_principal
}

پیکربندی را اعمال کنید

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

  1. ابتدا باید چند متغیر برای استفاده Terraform تنظیم کنیم. این کار را با استفاده از متغیرهای محیطی انجام خواهیم داد:
export TF_VAR_project_id=$(gcloud config get-value project)
export TF_VAR_project_number=$(gcloud projects describe $TF_VAR_project_id --format="value(projectNumber)")
export TF_VAR_region="us-central1"
  1. مقداردهی اولیه Terraform:
terraform init
  1. terraform plan برای پیش‌نمایش منابعی که ایجاد خواهند شد استفاده کنید.
terraform plan
  1. پیکربندی را اعمال کنید. وقتی از شما خواسته شد، برای تأیید، yes را تایپ کنید.
terraform apply

توجه: آماده‌سازی یک کلاستر GKE ممکن است ۱۰ تا ۱۵ دقیقه طول بکشد. در حین انتظار، می‌توانید به بررسی کد برنامه در بخش بعدی بپردازید.

  1. پس از اتمام، kubectl را برای ارتباط با کلاستر جدید خود پیکربندی کنید:
gcloud container clusters get-credentials gradio-chat-cluster --region us-central1 --project $TF_VAR_project_id

۴. Gemma خود-میزبان را روی GKE مستقر کنید

در مرحله بعد، مدل Gemma 3 12B را مستقیماً روی کلاستر GKE شما مستقر خواهیم کرد. این امر امکان استنتاج با تأخیر کم و کنترل کامل بر محیط اجرای مدل را فراهم می‌کند.

پیکربندی اعتبارنامه‌های در آغوش گرفتن چهره

برای دانلود مدل Gemma، کلاستر شما نیاز به احراز هویت با Hugging Face دارد.

  1. مطمئن شوید که یک توکن چهره در آغوش گرفته دارید.
  2. یک Kubernetes Secret با توکن خود ایجاد کنید - [YOUR_HF_TOKEN] را با توکن واقعی خود جایگزین کنید :
    kubectl create secret generic hf-secret --from-literal=hf_api_token=[YOUR_HF_TOKEN]
    

استقرار مدل

ما از یک Kubernetes Deployment استاندارد برای اجرای مدل استفاده خواهیم کرد. فایل مانیفست در deploy/gemma3-12b-deploy.yaml قرار دارد. می‌توانید آن را به صورت دستی باز کنید یا از این دستور برای باز کردن فایل در ویرایشگر استفاده کنید:

cd ../deploy
cloudshell edit gemma3-12b-deploy.yaml

لحظه‌ای وقت بگذارید و این فایل را بررسی کنید. به بخش resources توجه کنید:

        resources:
          requests:
            nvidia.com/gpu: 4
      nodeSelector:
        cloud.google.com/gke-accelerator: nvidia-l4

این یک زیرساخت هوش مصنوعی اعلانی است. ما به GKE Autopilot می‌گوییم که این پاد خاص به ۴ پردازنده گرافیکی NVIDIA L4 نیاز دارد. Autopilot گره‌ای را پیدا یا آماده می‌کند که دقیقاً این الزامات را برآورده کند. اگر گره‌ای برای آماده‌سازی در دسترس نباشد، تا زمانی که گره‌ای که الزامات را برآورده کند در دسترس باشد، به تلاش ادامه می‌دهد.

  1. مانیفست استقرار را اعمال کنید:
    cd ..
    kubectl apply -f deploy/gemma3-12b-deploy.yaml
    
    این کار فرآیند دانلود وزن‌های مدل و شروع سرور استنتاج را آغاز می‌کند. عموماً، این کار می‌تواند چند دقیقه طول بکشد. این استقرار Gemma از GPUها استفاده می‌کند که می‌توانند در معرض کمبود در دسترس بودن باشند. اگر GPUها در دسترس نباشند، غلاف gemma تا زمانی که در دسترس باشند، "در انتظار" باقی می‌ماند و کنسول Google Cloud خطایی مانند "نمی‌توان غلاف‌ها را برنامه‌ریزی کرد: Preemption برای برنامه‌ریزی مفید نیست" و/یا "نمی‌توان غلاف‌ها را برنامه‌ریزی کرد: گره(ها) با میل/انتخابگر گره غلاف مطابقت ندارند" را نشان می‌دهد. این بدان معناست که GKE هنوز نتوانسته هیچ GPU برای شما به دست آورد. تا زمانی که بتواند GPUها را به دست آورد، به تلاش خود ادامه خواهد داد. این امر بسته به در دسترس بودن GPU می‌تواند چند دقیقه یا چند روز طول بکشد. می‌توانید وضعیت را با موارد زیر بررسی کنید:
    kubectl get pods
    
    حتی اگر gemma pod هنوز راه‌اندازی نشده باشد، می‌توانید برنامه را مستقر کنید . هر زمان که سرویس gemma در دسترس قرار گیرد، برنامه چت به آن متصل خواهد شد. توجه داشته باشید که تا زمانی که gemma pod وضعیت Running و 1/1 را نشان ندهد، نمی‌توانید از طریق برنامه چت با Gemma تعامل داشته باشید . اما در این فاصله می‌توانید با Gemini چت کنید!

۵. ساخت برنامه چت

حالا، بیایید برنامه پایتون را کامل کنیم. app/app.py در ویرایشگر Cloud Shell باز کنید. چندین بلوک # TODO خواهید دید که برای عملکرد برنامه باید پر شوند.

cloudshell edit app/app.py

مرحله ۱: پردازش تاریخچه مکالمات

دانشجویان LLM نیاز دارند که تاریخچه مکالمات به طور خاص قالب بندی شود تا آنها بفهمند چه کسی چه چیزی گفته است.

الگوی «مترجم جهانی»: توجه داشته باشید که ما در شرف نوشتن دو تابع مختلف برای پردازش یک تاریخچه چت واحد هستیم. این یک الگوی کلیدی در برنامه‌های چند مدله است.

  • منبع حقیقت (Gradio): برنامه ما تاریخچه را در یک قالب ساده و عمومی نگه می‌دارد: [[user_msg1, bot_msg1], ...] .
  • هدف ۱ (جما): نیاز دارد که این مورد به یک رشته خام واحد با توکن‌های ویژه تبدیل شود.
  • هدف ۲ (Gemini): نیاز دارد که این مورد به یک لیست ساختاریافته از اشیاء API تبدیل شود.

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

برای جما (میزبان شخصی)

درک قالب‌های چت: هنگام میزبانی مدل‌های باز خودتان، معمولاً باید اعلان را به صورت دستی در یک رشته خاص قالب‌بندی کنید که مدل برای تشخیص آن به عنوان یک مکالمه آموزش دیده باشد. این به عنوان "قالب چت" شناخته می‌شود.

تابع process_message_gemma را در app.py پیدا کنید و آن را با کد زیر جایگزین کنید:

# This function takes a user's message and the conversation history as input.
#   Its job is to format these elements into a single,
#   structured prompt that can be understood by the language model (LLM).
#   This structured format helps the LLM maintain context and generate more relevant responses.
def process_message_gemma(message, history):
    user_prompt_format = "User's Turn:\n>>> {prompt}\n"
    assistant_prompt_format = "Assistant's Turn:\n>>> {prompt}\n"

    history_message = ""
    for user_turn, assistant_turn in history:
        history_message += user_prompt_format.format(prompt=user_turn)
        history_message += assistant_prompt_format.format(prompt=assistant_turn)

    # Format the new user message
    new_user_message = user_prompt_format.format(prompt=message)
    # Create a new aggregated message to be used as a single flat string in a json object sent to the LLM
    aggregated_message = (
        history_message + new_user_message + assistant_prompt_format.format(prompt="")
    )
    return aggregated_message

برای جمینی (مدیریت‌شده)

سرویس‌های مدیریت‌شده اغلب اشیاء ساختاریافته را به رشته‌های خام ترجیح می‌دهند. ما به یک تابع جداگانه برای قالب‌بندی تاریخچه به types.Content اشیاء محتوا برای Gemini SDK نیاز داریم.

process_message_gemini پیدا کنید و آن را با موارد زیر جایگزین کنید:

def process_message_gemini(message, history):
    contents = []
    for user_turn, model_turn in history:
        contents.append(
            types.Content(role="user", parts=[types.Part.from_text(text=user_turn)])
        )
        contents.append(
            types.Content(role="model", parts=[types.Part.from_text(text=model_turn)])
        )

    contents.append(
        types.Content(role="user", parts=[types.Part.from_text(text=message)])
    )
    return contents

مرحله ۲: فراخوانی مدل خود-میزبانی Gemma

ما باید اعلان فرمت‌شده‌ی خود را به سرویس Gemma که در کلاستر ما در حال اجرا است ارسال کنیم. ما از یک درخواست استاندارد HTTP POST به نام DNS داخلی سرویس استفاده خواهیم کرد.

تابع call_gemma_model را پیدا کنید و آن را با کد زیر جایگزین کنید:

# Construct the request, send it to Gemma, return the model's response
# aggregated_message = current user message + history
def call_gemma_model(aggregated_message, model_temperature, top_p, max_tokens):
    json_message = {
        "prompt": aggregated_message,
        "temperature": model_temperature,
        "top_p": top_p,
        "max_tokens": max_tokens,
        "stop": ["User's Turn:"],
    }

    # Log what will be sent to the LLM
    print("*** JSON request: " + str(json_message))

    # Send the constructed json with the user prompt to the model and put the model's response in the json_data variable
    json_data = post_request(json_message)

    # The response from the model is a list of predictions. We'll take the first result.
    raw_output = json_data["predictions"][0]

    # The vLLM server returns the full prompt in the response. We need to extract
    # just the newly generated text from the model.
    assistant_turn_marker = "Assistant's Turn:\n>>>"
    marker_pos = raw_output.rfind(assistant_turn_marker)

    if marker_pos != -1:
        output = raw_output[marker_pos + len(assistant_turn_marker) :]
    else:
        output = raw_output

    # Clean up potential over-generation
    stop_marker = "User's Turn:"
    stop_pos = output.lower().find(stop_marker.lower())
    if stop_pos != -1:
        output = output[:stop_pos]

    return output.strip()

مرحله 3: مدل Vertex AI Gemini را فراخوانی کنید

برای مدل مدیریت‌شده، ما از Google GenAI SDK استفاده خواهیم کرد. این روش بسیار ساده‌تر است زیرا فراخوانی‌های شبکه را برای ما مدیریت می‌کند.

تابع call_gemini_model را پیدا کنید و آن را با کد زیر جایگزین کنید:

# Send a request to Gemini via the VertexAI API. Return the model's response
# contents = list of types.Content objects
def call_gemini_model(contents, model_temperature, top_p, max_tokens):
    gemini_model = "gemini-2.5-flash"

    response = client.models.generate_content(
        model=gemini_model,
        contents=contents,
        config={
            "temperature": model_temperature,
            "max_output_tokens": max_tokens,
            "top_p": top_p,
        },
    )
    return response.text

مرحله ۴: پیاده‌سازی رابط استنتاج اصلی

در نهایت، به تابع ارکستراتور اصلی که Gradio آن را فراخوانی می‌کند نیاز داریم. این تابع باید:

  1. اگر تاریخچه خالی است، آن را مقداردهی اولیه کن.
  2. پیام را پردازش کنید.
  3. درخواست را به مدل انتخاب شده (Gemma یا Gemini) هدایت کنید.
  4. تعامل را در Firestore ذخیره کنید.
  5. پاسخ را به رابط کاربری (UI) برگردانید.

مدیریت وضعیت و حالت Gradio: ChatInterface در Gradio به طور خودکار وضعیت سطح جلسه (نمایش پیام‌ها در مرورگر) را مدیریت می‌کند. با این حال، پشتیبانی داخلی از پایگاه‌های داده خارجی ندارد.

برای حفظ تاریخچه چت برای مدت طولانی، از یک الگوی استاندارد استفاده می‌کنیم: ما به تابع inference_interface قلاب می‌کنیم. با پذیرش request: gr.Request به عنوان یک آرگومان، Gradio به طور خودکار جزئیات جلسه کاربر فعلی را به ما منتقل می‌کند. ما از این برای ایجاد یک سند Firestore منحصر به فرد برای هر کاربر استفاده می‌کنیم و اطمینان حاصل می‌کنیم که مکالمات در یک محیط چند کاربره با هم قاطی نمی‌شوند.

تابع inference_interface را پیدا کنید و آن را با این جایگزین کنید:

# This is the primary chat function. Every time a user sends a message, gradio calls this function,
# which sends the user's input to the appropriate AI (as indicated on the user interface), updates
# the chat history for future use during this session, and records the chat history in Firestore.
def inference_interface(
    message,
    history,
    model_name,
    model_temperature,
    top_p,
    max_tokens,
    request: gr.Request,
):

    # set history to empty array
    if history is None:
        history = []

    # Get or create session document
    session_hash = request.session_hash
    doc_id = f"session-{session_hash}"
    doc_ref = db.collection("chat_sessions").document(doc_id)

    # Create the session document if it doesn't exist
    if not doc_ref.get().exists:
        doc_ref.set({"Session start": datetime.datetime.now()})

    # Log info
    print("Model: " + model_name)
    print("LLM Engine: " + llm_engine)
    print("* History: " + str(history))

    # Pass the message and history to the appropriate model, as indicated by the user via the ui
    if model_name == "Gemma3 12b it":
        aggregated_message = process_message_gemma(message, history)
        output = call_gemma_model(
            aggregated_message, model_temperature, top_p, max_tokens
        )

    elif model_name == "Gemini":
        gemini_contents = process_message_gemini(message, history)
        output = call_gemini_model(
            gemini_contents, model_temperature, top_p, max_tokens
        )

    else:
        # Handle the case where no valid model is selected
        output = "Error: Invalid model selected."

    interaction = {"user": message, model_name: output}

    # Log the updated chat history
    print("* History: " + str(history) + " " + str(interaction))

    # Save the updated history to Firestore
    save_chat_history(interaction, doc_ref)

    return output

۶. فایل app.py خود را بررسی کنید

در این مرحله، برنامه چت مبتنی بر گرادیو شما باید آماده استقرار باشد. مطمئن شوید که دقیقاً با فایل کامل زیر مطابقت دارد.

عیب‌یابی: اگر برنامه خود را مستقر کردید و هنگام تلاش برای اتصال به آن با خطای «رد شدن اتصال» یا «این سایت قابل دسترسی نیست» مواجه شدید، مراحل را از این نقطه تکرار کنید، با کپی کردن کل این فایل و چسباندن آن به app.py خود شروع کنید.

# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import datetime

import google.auth
import google.cloud
import gradio as gr
import requests
import themes
from google import genai

from google.cloud import firestore
from google.genai import types

## Do one-time initialization things

## grab the project id from google auth
_, project = google.auth.default()
print(f"Project: {project}")

# Set initial values for model
llm_engine = "vllm"
host = "http://gemma-service:8000"
context_path = "/generate"

# initialize vertex for interacting with Gemini
client = genai.Client(
    vertexai=True,
    project=project,
    location="global",
)

# Initialize Firestore client
db = firestore.Client(database="chat-app-db")


# This is the primary chat function. Every time a user sends a message, gradio calls this function,
# which sends the user's input to the appropriate AI (as indicated on the user interface), updates
# the chat history for future use during this session, and records the chat history in Firestore.
def inference_interface(
    message,
    history,
    model_name,
    model_temperature,
    top_p,
    max_tokens,
    request: gr.Request,
):

    # set history to empty array
    if history is None:
        history = []

    # Get or create session document
    session_hash = request.session_hash
    doc_id = f"session-{session_hash}"
    doc_ref = db.collection("chat_sessions").document(doc_id)

    # Create the session document if it doesn't exist
    if not doc_ref.get().exists:
        doc_ref.set({"Session start": datetime.datetime.now()})

    # Log info
    print("Model: " + model_name)
    print("LLM Engine: " + llm_engine)
    print("* History: " + str(history))

    # Pass the message and history to the appropriate model, as indicated by the user via the ui
    if model_name == "Gemma3 12b it":
        aggregated_message = process_message_gemma(message, history)
        output = call_gemma_model(
            aggregated_message, model_temperature, top_p, max_tokens
        )

    elif model_name == "Gemini":
        gemini_contents = process_message_gemini(message, history)
        output = call_gemini_model(
            gemini_contents, model_temperature, top_p, max_tokens
        )

    else:
        # Handle the case where no valid model is selected
        output = "Error: Invalid model selected."

    interaction = {"user": message, model_name: output}

    # Log the updated chat history
    print("* History: " + str(history) + " " + str(interaction))

    # Save the updated history to Firestore
    save_chat_history(interaction, doc_ref)

    return output


# Construct the request, send it to Gemma, return the model's response
# aggregated_message = current user message + history
def call_gemma_model(aggregated_message, model_temperature, top_p, max_tokens):
    json_message = {
        "prompt": aggregated_message,
        "temperature": model_temperature,
        "top_p": top_p,
        "max_tokens": max_tokens,
        "stop": ["User's Turn:"],
    }

    # Log what will be sent to the LLM
    print("*** JSON request: " + str(json_message))  # Log the JSON request

    # Send the constructed json with the user prompt to the model and put the model's response in the json_data variable
    json_data = post_request(json_message)

    # The response from the model is a list of predictions.
    # We'll take the first result.
    raw_output = json_data["predictions"][0]

    # The vLLM server returns the full prompt in the response. We need to extract
    # just the newly generated text from the model. The prompt ends with
    # "Assistant's Turn:\n>>>", so we find the last occurrence of that and
    # take everything after it.
    assistant_turn_marker = "Assistant's Turn:\n>>>"
    marker_pos = raw_output.rfind(assistant_turn_marker)

    if marker_pos != -1:
        # Get the text generated by the assistant
        output = raw_output[marker_pos + len(assistant_turn_marker) :]
    else:
        # Fallback in case the marker isn't found
        output = raw_output

    # The model sometimes continues the conversation and includes the next user's turn.
    # The 'stop' parameter is a good hint, but we parse the output as a safeguard.
    stop_marker = "User's Turn:"
    stop_pos = output.lower().find(stop_marker.lower())
    if stop_pos != -1:
        output = output[:stop_pos]

    # The model also sometimes prefixes its response with "Output:". We'll remove this.
    output = output.lstrip()
    prefix_marker = "Output:"
    if output.lower().startswith(prefix_marker.lower()):
        output = output[len(prefix_marker) :]

    return output.strip()


# Send a request to Gemini via the VertexAI API. Return the model's response
# contents = list of types.Content objects
def call_gemini_model(contents, model_temperature, top_p, max_tokens):
    gemini_model = "gemini-2.5-flash"

    response = client.models.generate_content(
        model=gemini_model,
        contents=contents,
        config={
            "temperature": model_temperature,
            "max_output_tokens": max_tokens,
            "top_p": top_p,
        },
    )
    output = response.text  # Extract the generated text
    # Consider handling additional response attributes (safety, usage, etc.)
    return output


def process_message_gemini(message, history):
    contents = []
    for user_turn, model_turn in history:
        contents.append(
            types.Content(role="user", parts=[types.Part.from_text(text=user_turn)])
        )
        contents.append(
            types.Content(role="model", parts=[types.Part.from_text(text=model_turn)])
        )

    contents.append(
        types.Content(role="user", parts=[types.Part.from_text(text=message)])
    )
    return contents


# This function takes a user's message and the conversation history as input.
#   Its job is to format these elements into a single,
#   structured prompt that can be understood by the language model (LLM).
#   This structured format helps the LLM maintain context and generate more relevant responses.
def process_message_gemma(message, history):
    user_prompt_format = "User's Turn:\n>>> {prompt}\n"
    assistant_prompt_format = "Assistant's Turn:\n>>> {prompt}\n"

    history_message = ""
    for user_turn, assistant_turn in history:
        history_message += user_prompt_format.format(prompt=user_turn)
        history_message += assistant_prompt_format.format(prompt=assistant_turn)

    # Format the new user message
    new_user_message = user_prompt_format.format(prompt=message)
    # Create a new aggregated message to be used as a single flat string in a json object sent to the LLM
    aggregated_message = (
        history_message + new_user_message + assistant_prompt_format.format(prompt="")
    )
    return aggregated_message


# Function to save chat history to Firestore
def save_chat_history(interaction, doc_ref):
    timestamp_str = str(datetime.datetime.now())

    # Save the chat history, merging with existing data
    doc_ref.update({timestamp_str: interaction})

    print("Chat history saved successfully!")  # Optional: Log success


# Send the json message to the model and return the model's response. This is used for Gemma but not Gemini. It could also be used for other models.
def post_request(json_message):
    print("*** Request" + str(json_message), flush=True)
    # Set a timeout and check for HTTP errors. This will raise an exception on a bad status code (4xx or 5xx).
    response = requests.post(host + context_path, json=json_message, timeout=60)
    response.raise_for_status()
    json_data = response.json()
    print("*** Output: " + str(json_data), flush=True)
    return json_data


# custom css to hide default footer
css = """
footer {display: none !important;} .gradio-container {min-height: 0px !important;}
"""

# Add a dropdown to select the model to chat with
model_dropdown = gr.Dropdown(
    ["Gemma3 12b it", "Gemini"],
    label="Model",
    info="Select the model you would like to chat with.",
    value="Gemma3 12b it",
)

# Make the model temperature, top_p, and max tokents modifiable via sliders in the GUI
model_temperature = gr.Slider(
    minimum=0.1, maximum=1.0, value=0.9, label="Temperature", render=False
)
top_p = gr.Slider(minimum=0.1, maximum=1.0, value=0.95, label="Top_p", render=False)
max_tokens = gr.Slider(
    minimum=1, maximum=4096, value=1024, label="Max Tokens", render=False
)

# Call gradio to create the chat interface
app = gr.ChatInterface(
    inference_interface,
    additional_inputs=[model_dropdown, model_temperature, top_p, max_tokens],
    theme=themes.google_theme(),
    css=css,
    title="Chat with AI",
)

app.launch(server_name="0.0.0.0", allowed_paths=["images"])

۷. برنامه چت را مستقر کنید

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

توجه: این دستور، حساب کاربری Kubernetes Service که برای Workload Identity به آن نیاز داریم را نیز مستقر می‌کند. می‌توانید تعریف آن را در فایل deploy/chat-deploy.yaml مشاهده کنید. برای مرجع، تعریف آن را اینجا بررسی کنید:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: gradio-chat-ksa

برای ساخت و استقرار، Skaffold را اجرا کنید:

skaffold run --default-repo=us-central1-docker.pkg.dev/$GOOGLE_CLOUD_PROJECT/chat-app-repo

Skaffold از Cloud Build برای ساخت تصویر کانتینر استفاده می‌کند، آن را به Artifact Registry ایجاد شده توسط Terraform منتقل می‌کند و سپس مانیفست‌های Kubernetes را به کلاستر شما اعمال می‌کند.

۸. برنامه را آزمایش کنید

  1. منتظر بمانید تا سرویس برنامه چت یک آدرس IP خارجی دریافت کند:
    kubectl get svc gradio-chat-service --watch
    
    به محض اینکه EXTERNAL-IP از pending به یک آدرس IP واقعی تغییر کرد، برای توقف تماشا، Ctrl+C را فشار دهید.
  2. یک مرورگر وب باز کنید و به آدرس http://[EXTERNAL-IP]:7860 بروید.
  3. سعی کنید با مدل تعامل داشته باشید! برنامه طوری پیکربندی شده است که به طور پیش‌فرض شما را وادار به چت با مدل Gemma که به صورت محلی میزبانی می‌شود، کند. اگر می‌خواهید با Gemini چت کنید، مدل را در منوی کشویی «ورودی‌های اضافی» تغییر دهید. برای مثال، سعی کنید از هوش مصنوعی بپرسید: «یک جوک Kubernetes به من بگو.»

عیب‌یابی:

  1. اگر با خطایی مانند «این سایت قابل دسترسی نیست» یا «[EXTERNAL-IP] از اتصال امتناع ورزید» مواجه شدید، ممکن است مشکلی در فایل app.py شما رخ داده باشد. به مرحله «بررسی فایل app.py» برگردید و مراحل را از آنجا تکرار کنید.
  2. رابط کاربری به صورت پیش‌فرض روی مدل "Gemma3 12b it" تنظیم شده است. اگر بلافاصله با خطا مواجه شدید، احتمالاً به این دلیل است که Gemma pod هنوز آماده نیست. نکته: می‌توانید منوی کشویی را به "Gemini" تغییر دهید تا در حین انتظار برای راه‌اندازی Gemma، تعامل با برنامه چت را آزمایش کنید!

تست جما: مطمئن شوید که گزینه "Gemma3 12b it" در منوی کشویی انتخاب شده است و یک پیام ارسال کنید (مثلاً "یک جوک در مورد Kubernetes به من بگو").

تست جمینی: منوی کشویی را به «جمینی» تغییر دهید و سوال دیگری بپرسید (مثلاً «تفاوت بین پاد و گره چیست؟»).

تأیید تاریخچه: پس از اینکه با موفقیت با یک مدل (Gemma یا Gemini) در برنامه چت چت کردید، پایگاه داده "chat-app-db" خود را در Firestore بررسی کنید تا گزارش‌های چت را مشاهده کنید. اگر توانستید با هر دو مدل چت کنید، توجه داشته باشید که تاریخچه مکالمات حتی هنگام تعویض مدل نیز حفظ می‌شود.

۹. فراتر رفتن

حالا که یک اپلیکیشن چت ترکیبی کارآمد دارید، برای تعمیق درک خود، این چالش‌ها را در نظر بگیرید:

  1. پرسونا سفارشی: سعی کنید توابع process_message_gemma و process_message_gemini را طوری تغییر دهید که در ابتدا یک "دستور سیستم" قرار گیرد. برای مثال، به مدل‌ها بگویید "شما یک دستیار دزد دریایی مفید هستید." و ببینید که چگونه پاسخ‌های آنها را تغییر می‌دهد.
  2. هویت کاربر پایدار: در حال حاضر، برنامه برای هر جلسه یک UUID تصادفی جدید تولید می‌کند. چگونه می‌توانید یک سیستم احراز هویت واقعی (مانند ورود به سیستم گوگل) را ادغام کنید تا کاربر بتواند تاریخچه مکالمات گذشته خود را در دستگاه‌های مختلف مشاهده کند؟
  3. آزمایش مدل: سعی کنید نوار لغزنده temperature را در رابط کاربری تغییر دهید. دمای بالا (نزدیک به ۱.۰) در مقایسه با دمای پایین (نزدیک به ۰.۱) چگونه بر خلاقیت در مقابل دقت پاسخ‌ها تأثیر می‌گذارد؟

۱۰. نتیجه‌گیری

تبریک! شما با موفقیت یک برنامه هوش مصنوعی ترکیبی ساختید. شما یاد گرفته‌اید که چگونه:

  • از Terraform برای زیرساخت به عنوان کد در Google Cloud استفاده کنید.
  • برای کنترل کامل، LLM های وزن آزاد خود را در GKE برگزار کنید.
  • برای انعطاف‌پذیری، سرویس‌های هوش مصنوعی مدیریت‌شده مانند Vertex AI را ادغام کنید.
  • برای پایداری، یک برنامه‌ی دارای وضعیت با استفاده از Firestore بسازید.
  • با استفاده از Workload Identity، حجم کاری خود را ایمن کنید.

تمیز کردن

برای جلوگیری از تحمیل هزینه‌ها، منابعی را که ایجاد کرده‌اید از بین ببرید:

cd infra
terraform destroy -var="project_id=$GOOGLE_CLOUD_PROJECT" -var="project_number=$PROJECT_NUMBER" -var="region=$REGION"