الكتابات/tutorial/2026/07
Tutorial22 يوليو 2026·28 دقيقة

دمج Gemini 3.6 Flash مع TypeScript: بناء وكلاء ذكاء اصطناعي بالتفكير المتوازي

تعلّم كيفية دمج Gemini 3.6 Flash في تطبيقات TypeScript. يغطي هذا الدرس وضع التفكير مع ميزانيات قابلة للضبط، واستدعاء الأدوات بالتوازي، وبث رموز التفكير، ووكيل بحث كامل مبني على Next.js 15.

وصل Gemini 3.6 Flash في الحادي والعشرين من يوليو 2026 بثلاث قدرات تُحدث فارقًا حقيقيًا للمطورين الذين يبنون الوكلاء: ميزانية تفكير قابلة للضبط تكشف عن الاستدلال الداخلي للنموذج، واستدعاء متوازٍ حقيقي للأدوات في دورة واحدة، وتخفيض بنسبة 17% في رموز الإخراج على المهام المتعددة الخطوات مقارنةً بـ Flash 3.5. وقد أدّى تخفيض السعر إلى 1.50 دولار لكل مليون رمز إدخال و7.50 دولار لكل مليون رمز إخراج إلى جعله النموذج التفكيري الأكثر فاعلية من حيث التكلفة في تشكيلة Google.

يستعرض هذا الدرس كل طبقة — الإكمال الأساسي، وضبط وضع التفكير، وبث رموز التفكير، واستدعاء الوظائف المتوازي، ووكيل بحث كامل مبني كمسار SSE على Next.js 15.

المتطلبات الأساسية

قبل البدء، تأكّد من توافر ما يلي:

  • Node.js 20+ وpnpm مثبّتان
  • حساب على Google AI Studio مع مفتاح API (الطبقة المجانية تكفي لهذا الدرس)
  • إلمام بأنماط TypeScript async/await
  • معرفة أساسية بـ Next.js 15 App Router

ما الذي ستبنيه

وكيل بحث "Market Intel" يقوم بما يلي:

  1. قبول استعلام باللغة الطبيعية حول شركة أو سوق معيّنة
  2. استخدام وضع التفكير في Gemini 3.6 Flash لوضع خطة البحث
  3. تنفيذ ثلاث استدعاءات أدوات بالتوازي — أسعار الأسهم، والأخبار الحديثة، وبيانات المنافسين
  4. بث تفكير النموذج والتحليل النهائي إلى العميل React
  5. عرض الوكيل الكامل كمسار API على Next.js 15 مع Server-Sent Events

بحلول نهاية هذا الدرس، ستمتلك نمطًا جاهزًا للإنتاج لأي وكيل يحتاج إلى التفكير أوّلًا ثم جمع البيانات.

الخطوة 1: إعداد المشروع

أنشئ تطبيق Next.js 15 وثبّت SDK الخاص بـ Google Generative AI:

pnpm create next-app@latest market-intel --typescript --tailwind --app --no-src-dir
cd market-intel
pnpm add @google/genai zod

أنشئ ملف .env.local في جذر المشروع:

GEMINI_API_KEY=your_key_from_ai_studio

احصل على مفتاحك من Google AI Studio. تتيح الطبقة المجانية 15 طلبًا في الدقيقة و1500 طلب يوميًا، وهو أكثر من كافٍ للتطوير.

الخطوة 2: ضبط عميل Gemini

أنشئ وحدة عميل مشتركة حتى تستخدم جميع الملفات النفس المثيل وثابت النموذج:

// lib/gemini.ts
import { GoogleGenAI } from "@google/genai";
 
if (!process.env.GEMINI_API_KEY) {
  throw new Error("GEMINI_API_KEY is not set in .env.local");
}
 
export const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
 
export const MODEL = "gemini-3.6-flash";

تحقّق من الإعداد بسكريبت سريع قبل بناء أي شيء معقّد:

// scripts/test-connection.ts
import { ai, MODEL } from "../lib/gemini";
 
const response = await ai.models.generateContent({
  model: MODEL,
  contents: "Reply with: Gemini 3.6 Flash is online.",
});
 
console.log(response.text);

شغّله بـ:

npx tsx scripts/test-connection.ts

يجب أن ترى رسالة التأكيد خلال ثانية إلى ثانيتين.

الخطوة 3: تفعيل وضع التفكير

يدعم Gemini 3.6 Flash ضبط thinkingBudget الذي يُعيّن المستويات الثلاثة التي تراها في واجهة Google Antigravity. تُقاس الميزانية برموز التفكير — رموز الاستدلال الداخلي التي يستخدمها النموذج قبل إنشاء استجابته.

قيمة الميزانيةمستوى Antigravityأفضل حالة استخدام
512–2048منخفضتوليد CRUD، التصنيف، الأسئلة البسيطة
4096–8192متوسطتطوير الميزات، استعلامات البحث، التحليل
-1 (غير محدود)مرتفعقرارات معمارية، استدلال معقّد متعدد الخطوات
0معطّلأقصى سرعة بدون استدلال مرئي

أنشئ دالة مساعدة تُعيّن أسماء المستويات لقيمة الميزانية الصحيحة:

// lib/thinking.ts
export type ThinkingTier = "low" | "medium" | "high" | "disabled";
 
export function thinkingConfig(tier: ThinkingTier) {
  const budgets: Record<ThinkingTier, number> = {
    low: 1024,
    medium: 8192,
    high: -1,
    disabled: 0,
  };
  return { thinkingBudget: budgets[tier] };
}

نصيحة: ابدأ كل وكيل جديد بمستوى "medium". شغّل المهمة ذاتها عشر مرات بمستوى "medium" ومرات أخرى بمستوى "high" وقارن جودة الإخراج. في معظم الحالات العملية، ستجد مستوى medium لا يختلف ملحوظًا عن high بتكلفة نصف رموز التفكير تقريبًا.

الخطوة 4: بث رموز التفكير

تُصدر نماذج التفكير فئتين من المحتوى في التدفق: أجزاء thought (الاستدلال الداخلي، يظهر باللون الكهرماني في واجهة Antigravity) وأجزاء text (الاستجابة المرئية). يمكن لتطبيقك عرض كلا النوعين.

// lib/stream-agent.ts
import { ai, MODEL } from "./gemini";
import { thinkingConfig } from "./thinking";
 
export type StreamChunk =
  | { type: "thinking"; text: string }
  | { type: "response"; text: string };
 
export async function* streamWithThinking(
  prompt: string,
  tier: "low" | "medium" | "high" = "medium"
): AsyncGenerator<StreamChunk> {
  const stream = await ai.models.generateContentStream({
    model: MODEL,
    contents: prompt,
    config: {
      thinkingConfig: thinkingConfig(tier),
    },
  });
 
  for await (const chunk of stream) {
    const parts = chunk.candidates?.[0]?.content?.parts ?? [];
    for (const part of parts) {
      if (part.thought && part.text) {
        yield { type: "thinking", text: part.text };
      } else if (part.text) {
        yield { type: "response", text: part.text };
      }
    }
  }
}

اختبر التدفق في سكريبت صغير:

// scripts/test-thinking.ts
import { streamWithThinking } from "../lib/stream-agent";
 
for await (const chunk of streamWithThinking(
  "قارن PostgreSQL وSQLite لتطبيق يُعطي الأولوية للهاتف المحمول.",
  "medium"
)) {
  if (chunk.type === "thinking") process.stdout.write("[T] " + chunk.text);
  else process.stdout.write("[R] " + chunk.text);
}

تُظهر الأسطر المبدوءة بـ [T] استدلال النموذج قبل أن يلتزم بالإجابة النهائية.

الخطوة 5: تعريف الأدوات

يستخدم Gemini 3.6 Flash نفس تنسيق تعريف الوظائف كإصدارات Gemini السابقة. عرّف كل أداة كـ FunctionDeclaration مع كائن JSON Schema للمعاملات:

// lib/tools.ts
import type { FunctionDeclaration } from "@google/genai";
 
export const researchTools: FunctionDeclaration[] = [
  {
    name: "get_stock_quote",
    description:
      "Get the current stock price, market cap, and key financial ratios for a ticker symbol.",
    parameters: {
      type: "object",
      properties: {
        symbol: {
          type: "string",
          description: "Stock ticker e.g. NVDA, MSFT, GOOGL",
        },
      },
      required: ["symbol"],
    },
  },
  {
    name: "search_news",
    description: "Search recent news articles for a company or topic.",
    parameters: {
      type: "object",
      properties: {
        query: { type: "string", description: "News search query" },
        days: {
          type: "integer",
          description: "Number of past days to search. Default is 7.",
        },
      },
      required: ["query"],
    },
  },
  {
    name: "get_competitors",
    description:
      "Return a list of direct competitors and approximate market share for a company.",
    parameters: {
      type: "object",
      properties: {
        company: {
          type: "string",
          description: "Company name or ticker symbol",
        },
      },
      required: ["company"],
    },
  },
];

الخطوة 6: معالجات الأدوات

في هذا الدرس، تُعيد المعالجات بيانات وهمية. في الإنتاج، ستستبدل كل دالة باستدعاء API حقيقي:

// lib/tool-handlers.ts
export async function executeToolCall(
  name: string,
  args: Record<string, unknown>
): Promise<unknown> {
  switch (name) {
    case "get_stock_quote":
      return getStockQuote(args.symbol as string);
    case "search_news":
      return searchNews(args.query as string, (args.days as number) ?? 7);
    case "get_competitors":
      return getCompetitors(args.company as string);
    default:
      throw new Error(`Unknown tool: ${name}`);
  }
}
 
function getStockQuote(symbol: string) {
  const data: Record<string, object> = {
    NVDA: { price: 1247.5, change: "+3.2%", marketCap: "3.1T", peRatio: 48.2 },
    MSFT: { price: 512.3, change: "+0.8%", marketCap: "3.8T", peRatio: 35.1 },
    GOOGL: { price: 198.45, change: "+1.5%", marketCap: "2.4T", peRatio: 28.7 },
  };
  return data[symbol.toUpperCase()] ?? { error: `Symbol ${symbol} not found` };
}
 
function searchNews(query: string, days: number) {
  return {
    articles: [
      {
        title: `${query}: Key Developments`,
        summary: "AI infrastructure spending continues to accelerate...",
        source: "Reuters",
        publishedAt: new Date().toISOString(),
      },
    ],
    totalResults: 1,
    daysSearched: days,
  };
}
 
function getCompetitors(company: string) {
  return {
    company,
    competitors: [
      { name: "AMD", marketShare: "18%", focus: "GPU, CPU" },
      { name: "Intel", marketShare: "12%", focus: "CPU, GPU" },
      { name: "Qualcomm", marketShare: "8%", focus: "Edge AI, Mobile" },
    ],
  };
}

الخطوة 7: حلقة الوكيل المتوازي

الفكرة الجوهرية عند التعامل مع استدعاءات أدوات Gemini 3.6 Flash: عندما يُعيد النموذج أجزاء functionCall متعددة في دورة استجابة واحدة، فهو يطلب صراحةً التنفيذ المتوازي. تشغيلها بالتسلسل يُضيف زمن انتظار غير ضروري — استخدم Promise.all لإطلاق جميع استدعاءات الأدوات بالتزامن، ثم أرسل جميع النتائج في دورة مستخدم واحدة:

// lib/agent-loop.ts
import type { Content, FunctionCall, Part } from "@google/genai";
import { ai, MODEL } from "./gemini";
import { thinkingConfig } from "./thinking";
import { researchTools } from "./tools";
import { executeToolCall } from "./tool-handlers";
 
export interface AgentResult {
  thoughts: string[];
  answer: string;
  toolCalls: Array<{ name: string; args: Record<string, unknown> }>;
}
 
export async function runResearchAgent(query: string): Promise<AgentResult> {
  const history: Content[] = [
    { role: "user", parts: [{ text: query }] },
  ];
  const thoughts: string[] = [];
  const toolCalls: AgentResult["toolCalls"] = [];
 
  for (let turn = 0; turn < 6; turn++) {
    const response = await ai.models.generateContent({
      model: MODEL,
      contents: history,
      config: {
        tools: [{ functionDeclarations: researchTools }],
        thinkingConfig: thinkingConfig("medium"),
      },
    });
 
    const parts = response.candidates?.[0]?.content?.parts ?? [];
 
    for (const part of parts) {
      if (part.thought && part.text) {
        thoughts.push(part.text);
      }
    }
 
    const fnCalls = parts.filter(
      (p): p is Part & { functionCall: FunctionCall } => !!p.functionCall
    );
 
    if (fnCalls.length === 0) {
      const answer = parts
        .filter((p) => !p.thought && p.text)
        .map((p) => p.text ?? "")
        .join("");
      return { thoughts, answer, toolCalls };
    }
 
    history.push({ role: "model", parts });
 
    for (const { functionCall } of fnCalls) {
      toolCalls.push({
        name: functionCall.name,
        args: (functionCall.args ?? {}) as Record<string, unknown>,
      });
    }
 
    // تنفيذ جميع استدعاءات الأدوات بالتوازي
    const toolResults = await Promise.all(
      fnCalls.map(async ({ functionCall }) => {
        const result = await executeToolCall(
          functionCall.name,
          (functionCall.args ?? {}) as Record<string, unknown>
        );
        return {
          functionResponse: {
            name: functionCall.name,
            response: { output: result },
          },
        };
      })
    );
 
    history.push({ role: "user", parts: toolResults });
  }
 
  return {
    thoughts,
    answer: "اكتمل البحث — تم بلوغ الحد الأقصى للتكرارات.",
    toolCalls,
  };
}

الخطوة 8: مسار API على Next.js 15 مع SSE

اعرض الوكيل كنقطة نهاية Server-Sent Events متدفقة. سيتلقّى العميل تحديثات فورية لرموز التفكير وتنفيذ الأدوات والإجابة النهائية:

// app/api/research/route.ts
import { NextRequest } from "next/server";
import type { Content, FunctionCall, Part } from "@google/genai";
import { ai, MODEL } from "@/lib/gemini";
import { thinkingConfig } from "@/lib/thinking";
import { researchTools } from "@/lib/tools";
import { executeToolCall } from "@/lib/tool-handlers";
 
export const maxDuration = 120;
 
export async function POST(req: NextRequest) {
  const { query } = await req.json() as { query: string };
  const encoder = new TextEncoder();
 
  const stream = new ReadableStream({
    async start(controller) {
      const send = (data: object) =>
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
        );
 
      const history: Content[] = [
        { role: "user", parts: [{ text: query }] },
      ];
 
      for (let turn = 0; turn < 6; turn++) {
        const response = await ai.models.generateContent({
          model: MODEL,
          contents: history,
          config: {
            tools: [{ functionDeclarations: researchTools }],
            thinkingConfig: thinkingConfig("medium"),
          },
        });
 
        const parts = response.candidates?.[0]?.content?.parts ?? [];
 
        for (const part of parts) {
          if (part.thought && part.text) {
            send({ type: "thinking", text: part.text });
          }
        }
 
        const fnCalls = parts.filter(
          (p): p is Part & { functionCall: FunctionCall } => !!p.functionCall
        );
 
        if (fnCalls.length === 0) {
          const answer = parts
            .filter((p) => !p.thought && p.text)
            .map((p) => p.text ?? "")
            .join("");
          send({ type: "result", text: answer });
          break;
        }
 
        history.push({ role: "model", parts });
        send({ type: "tools_start", names: fnCalls.map((f) => f.functionCall.name) });
 
        const toolResults = await Promise.all(
          fnCalls.map(async ({ functionCall }) => {
            const result = await executeToolCall(
              functionCall.name,
              (functionCall.args ?? {}) as Record<string, unknown>
            );
            send({ type: "tool_done", name: functionCall.name });
            return {
              functionResponse: {
                name: functionCall.name,
                response: { output: result },
              },
            };
          })
        );
 
        history.push({ role: "user", parts: toolResults });
      }
 
      controller.close();
    },
  });
 
  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}

الخطوة 9: مكوّن العميل React

ابنِ لوحة تحكم بسيطة تعرض تدفق SSE في الوقت الفعلي:

// app/page.tsx
"use client";
 
import { useState } from "react";
 
type Event =
  | { type: "thinking"; text: string }
  | { type: "tools_start"; names: string[] }
  | { type: "tool_done"; name: string }
  | { type: "result"; text: string };
 
export default function ResearchPage() {
  const [query, setQuery] = useState("");
  const [events, setEvents] = useState<Event[]>([]);
  const [loading, setLoading] = useState(false);
 
  async function runResearch() {
    setEvents([]);
    setLoading(true);
 
    const res = await fetch("/api/research", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ query }),
    });
 
    const reader = res.body!.getReader();
    const decoder = new TextDecoder();
 
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
 
      for (const line of decoder.decode(value).split("\n")) {
        if (!line.startsWith("data: ")) continue;
        try {
          const event = JSON.parse(line.slice(6)) as Event;
          setEvents((prev) => [...prev, event]);
        } catch {
          // تجاوز الأجزاء غير المكتملة
        }
      }
    }
 
    setLoading(false);
  }
 
  return (
    <main className="max-w-3xl mx-auto p-8 space-y-6" dir="rtl">
      <h1 className="text-2xl font-bold">وكيل استخبارات السوق</h1>
      <p className="text-gray-600 text-sm">
        مدعوم بـ Gemini 3.6 Flash مع وضع التفكير وتنفيذ الأدوات المتوازي.
      </p>
 
      <div className="flex gap-2">
        <input
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          onKeyDown={(e) => e.key === "Enter" && !loading && query && runResearch()}
          placeholder="مثال: المشهد التنافسي لـ NVIDIA في رقائق الذكاء الاصطناعي"
          className="flex-1 border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
          dir="rtl"
        />
        <button
          onClick={runResearch}
          disabled={loading || !query.trim()}
          className="bg-blue-600 text-white px-5 py-2 rounded-lg disabled:opacity-50 hover:bg-blue-700 transition-colors"
        >
          {loading ? "جارٍ البحث…" : "ابحث"}
        </button>
      </div>
 
      <div className="space-y-2">
        {events.map((event, i) => (
          <div
            key={i}
            className={`rounded-lg p-3 text-sm ${
              event.type === "thinking"
                ? "bg-amber-50 border border-amber-200 text-amber-900 font-mono text-xs"
                : event.type === "tools_start"
                ? "bg-blue-50 border border-blue-200 text-blue-800"
                : event.type === "tool_done"
                ? "bg-emerald-50 border border-emerald-200 text-emerald-800"
                : "bg-white border shadow-sm whitespace-pre-wrap"
            }`}
          >
            {event.type === "thinking" && (
              <span><span className="font-semibold">تفكير: </span>{event.text}</span>
            )}
            {event.type === "tools_start" && (
              <span><span className="font-semibold">استدعاء بالتوازي: </span>{event.names.join(", ")}</span>
            )}
            {event.type === "tool_done" && (
              <span><span className="font-semibold">انتهى: </span>{event.name}</span>
            )}
            {event.type === "result" && (
              <div>
                <p className="font-semibold mb-2">التحليل:</p>
                {event.text}
              </div>
            )}
          </div>
        ))}
      </div>
    </main>
  );
}

أنماط تحسين التكلفة

يُعدّ Gemini 3.6 Flash الأقل تكلفة في فئة نماذج التفكير، لكن ثمة أنماط تُقلل التكاليف أكثر:

ضبط ميزانية التفكير المناسبة. للاستعلامات البسيطة، اضبط thinkingBudget على 512 أو حتى 0. تُحتسب رموز التفكير كرموز إخراج بسعر 7.50 دولار لكل مليون.

تخزين موجّه النظام مؤقتًا. إذا أضفت تعليمات نظام تحتوي على سياق الشركة أو بيانات خلفية، فهذا النص يتكرر في كل دورة. ضعه في الرسالة الأولى لتدفع عنه مرة واحدة.

تقصير الدورات بالذاكرة المؤقتة. في حلقات الوكيل حيث يُمكن استدعاء الأداة ذاتها بنفس الوسيطات، احتفظ بـ Map مفتاحه name + JSON.stringify(args) وأعِد النتيجة المخزّنة بدلاً من إجراء استدعاء ثانٍ للأداة.

تجميع الاستعلامات ذات الصلة. نافذة السياق البالغة مليون رمز تتيح إرسال عشرة استعلامات للشركات في استدعاء واحد والحصول على عشر استجابات، بدلاً من عشرة طلبات API منفصلة.

استكشاف الأخطاء وإصلاحها

401 API_KEY_INVALID — تحقق من أن المفتاح من Google AI Studio وليس من حساب خدمة Vertex AI؛ نظاما المصادقة منفصلان.

thinkingConfig لا تُنتج أثرًا — تأكد من أن النموذج هو gemini-3.6-flash وليس gemini-3.6-flash-lite. لا تدعم النسخة Lite وضع التفكير وتتجاهل حقل thinkingBudget.

أجزاء التفكير فارغة — تتطلب رموز التفكير أن يكون thinkingBudget أكبر من الصفر. ضبطه على 0 يُعطّل التفكير بالكامل لتحقيق أقصى سرعة. استخدم على الأقل 512 لرؤية ناتج الاستدلال.

حلقة الوكيل لا تنتهي — أضف حدًّا صارمًا لعدد الدورات كما هو موضّح أعلاه. إذا استمرت الحلقة طويلاً، سجّل history في كل تكرار وابحث عن استدعاء أداة يُعيد خطأً باستمرار.

أخطاء CORS في المتصفح — لا تستدعِ Gemini API مباشرةً من JavaScript على جانب العميل؛ يكشف هذا مفتاح API الخاص بك. وجّه دائمًا عبر مسار API على Next.js أو Server Action كما هو موضّح في الخطوة 8.

الخطوات التالية

  • استبدل معالجات الأدوات الوهمية بـ Alpha Vantage لبيانات الأسهم الحقيقية وTavily للأخبار الموثوقة
  • استبدل حلقة SSE اليدوية بـ useChat من Vercel AI SDK 7 مع مزوّد google
  • استكشف أداة code_execution المدمجة في Google لتحليل بيانات Python داخل حلقة الوكيل
  • جرّب Gemini Live API للاستعلامات الصوتية الفورية
  • وجّه Gemini عبر بوابتك الخاصة لرصد التكاليف والحد من المعدل: درس LiteLLM Proxy

الخاتمة

يجمع Gemini 3.6 Flash الاستدلال التفكيري وتنفيذ الأدوات المتوازي ونافذة سياق تبلغ مليون رمز بسعر يجعل الوكلاء متعددي الخطوات مجدية اقتصاديًا في بيئات الإنتاج. إن تخفيض 17% في رموز الإخراج لكل سير عمل معقد ليس مجرد مقياس تسويقي — بل يترجم مباشرةً إلى دورات وكيل أقل وفواتير أخف واستجابات أسرع. باستخدام الأنماط الواردة في هذا الدرس — ضبط thinkingBudget، وحلقة الأدوات المتوازية بـ Promise.all، وبث رموز التفكير إلى عميل React — تمتلك الآن الأساس لبناء وكلاء تفكّر صراحةً فيما ستجمعه قبل أن تبدأ في الجمع.