تحديد معدل الطلبات في MCP: كيف تضيف حدودًا لمعدل الطلبات إلى خادم MCP
خطة عملية لتحديد معدل الطلبات في MCP باستخدام TypeScript. ابنِ دلو الرموز (token bucket)، وعرّف المستدعين بالتوكن أو بمعرّف العميل، وأعِد الرمز 429 والترويسة Retry-After عبر HTTP، وزِن الأدوات حسب تكلفتها، وسّع العدّادات باستخدام Redis، واختبر كل حد باستخدام الموقّتات الوهمية (fake timers) قبل أن يكتشف أحد الوكلاء الثغرات.
لا يشعر وكيل الذكاء الاصطناعي بالملل أبدًا. إذا وجّهته إلى خادم MCP الخاص بك بمهمة غامضة، يمكنه إطلاق 40 استدعاءً للأدوات خلال عشر ثوانٍ، وإعادة المحاولة فورًا عند كل فشل، وإرسال طلبات متوازية لم يخطط لها أحد. هذا رائع للإنتاجية وسيئ جدًا لفاتورتك. تحديد معدل طلبات MCP هو ما يُبقي خادم Model Context Protocol مفيدًا تحت هذا النوع من الضغط: يحصل كل مستدعٍ على ميزانية عادلة، وتكلّف الأدوات المكلفة أكثر من الأدوات الرخيصة، ويتلقى من استنفد ميزانيته رسالة واضحة عن موعد العودة.
يوضح هذا المقال كيفية إضافة حدود لمعدل الطلبات إلى خادم MCP بـ TypeScript، بدءًا من دلو رموز بسيط وصولًا إلى محدِّد مدعوم بـ Redis يعمل عبر عدة نسخ. سترى أين يجب وضع الفحوصات، وكيف تُعيد أخطاءً يستطيع الوكيل التصرف بناءً عليها، وكيف تختبر كل ذلك دون انتظار دقيقة حقيقية.
لماذا تحتاج خوادم MCP إلى حدود لمعدل الطلبات
حد معدل الطلبات وعد بالسعة: يحق لهذا المستدعي أن يستخدم هذا القدر، لكل وحدة زمنية، ولا يزيد. تُدرج ملاحظات الأمان الخاصة بالأدوات في مواصفة Model Context Protocol تحديد معدل استدعاءات الأدوات كمتطلب للخوادم، إلى جانب التحقق من المدخلات والتحكم في الوصول. تتولى الحزم الرسمية (SDKs) النقل والمخططات، لكنها تترك المحدِّد لك، لذلك ينتهي الأمر بكل مطوّر خادم إلى كتابته بنفسه.
الوكلاء يعيدون المحاولة دون أن يتعبوا
الشخص الذي ينقر زرًا بطيء الاستجابة، وتصرفه سهل التنبؤ به. أما حلقة الوكيل فليست كذلك. يستدعي النموذج أداة، ويقرأ النتيجة، ويقرر ما يستدعيه التالي، وغالبًا خلال أجزاء من الثانية. تتكرر ثلاثة أنماط مرارًا:
عواصف إعادة المحاولة. تفشل أداة، فيحاول النموذج مرة أخرى، ويفشل مجددًا، ويستمر في التكرار حتى ينفد سياقه أو ميزانيته.
التفرّع المتوازي. يمكن للعملاء إرسال عدة استدعاءات للأدوات في وقت واحد، فقد يتحول أمر واحد إلى عشرات الطلبات المتزامنة.
الحلقات الجامحة. تؤدي مهمة غامضة مقترنة بأداة لا تقول أبدًا "تم" إلى مئات الاستدعاءات من جلسة واحدة.
هناك زاوية أمنية أيضًا. قد تخفي صفحة ويب أو مستند يقرؤه الوكيل تعليمات تطلب منه استدعاء أداة مرارًا وتكرارًا. لا يمكنك إيقاف الحقن دائمًا، لكن حد المعدل يحدّ من الضرر.
الأدوات تغلّف واجهات APIs المدفوعة
معظم أدوات MCP ليست سوى أغلفة رقيقة حول شيء له تكلفة مالية أو حصة خاصة به: نموذج لغوي، أو مولّد صور، أو واجهة بحث API، أو قاعدة بيانات. يحمي المحدِّد ثلاثة أشياء في الوقت نفسه:
ميزانيتك، لأن جلسة واحدة صاخبة لا ينبغي أن تستنزف إنفاق يوم كامل.
حصصك لدى الجهات المزوّدة، لأن المزوّدين يردّون على الإساءة برموز 429 تطال كل مستخدم في خادمك.
زمن استجابة المستخدمين الآخرين، لأن عميلًا جشعًا يُشبع عمّال خادمك يُبطئ الجميع.
💡 الخوادم المحلية من نوع stdio تحتاج إلى حدود أيضًا. حتى عندما يشغّل الخادم شخص واحد على حاسوبه المحمول، يستطيع وكيل دخل في حلقة تكرار أن يستنزف واجهة API المدفوعة التي تقف خلفه. إضافة ميزانية لكل أداة لا تكلّف شيئًا، وتمنع مفاجأة غير سارة جدًا.
اختر الخوارزمية المناسبة
تغطي ستة تصاميم تقريبًا كل الحالات. إليك سلوكها عندما يصلها ضغط حركة الوكلاء:
الخوارزمية
سلوك الاندفاعات
الذاكرة لكل مستدعٍ
الأنسب لـ
النافذة الثابتة
تسمح حتى 2x عند حواف النافذة
عدّاد واحد
الحصص البسيطة مثل الحدود اليومية
سجل النافذة المنزلقة
دقيق، بلا اندفاعات عند الحواف
طابع زمني واحد لكل طلب
الحجم المنخفض، والحدود الصارمة
عدّاد النافذة المنزلقة
قريب من الدقة
عدّادان
نقاط HTTP عالية الحجم
Token bucket
اندفاعات خاضعة للتحكم، وتعبئة ثابتة
رقمان
استدعاءات الأدوات من الوكلاء
Leaky bucket
بلا اندفاعات، وإخراج سلس
طابور
تغذية الخدمات الخلفية الهشة
حد التزامن
يحدّ المهام المتوازية
عدّاد واحد
الأدوات طويلة التشغيل
النوافذ الثابتة والمنزلقة
عدّاد النافذة الثابتة هو أبسط تصميم: يحسب الطلبات في الدقيقة ويُعيد التعيين عند بداية كل دقيقة. وهو رخيص وسهل الشرح، لكن المستدعي يستطيع إرسال حصة كاملة عند 12:00:59 وحصة كاملة أخرى عند 12:01:00، فيتضاعف الاندفاع الذي يراه خادمك.
تزيل النافذة المنزلقة تلك الحافة بالنظر إلى الـ 60 ثانية الأخيرة من اللحظة الحالية. إما أن تخزّن كل طابع زمني (دقيقة، لكنها تستهلك ذاكرة كبيرة)، أو تُرجّح عدّاد النافذة السابقة (قريبة بما يكفي وخفيفة). استعن بالنوافذ عندما تريد حصصًا بسيطة مثل 1,000 استدعاء يوميًا، حيث لا يهم موعد التعبئة الدقيق.
لماذا يفوز دلو الرموز عادةً
حركة الوكلاء متقطعة: لا شيء لعشر ثوانٍ، ثم ستة استدعاءات للأدوات دفعة واحدة، ثم صمت. يناسب دلو الرموز هذا الشكل. لكل مستدعٍ دلو له سعة (أكبر اندفاع مسموح) ومعدل تعبئة (الوتيرة المستمرة). يسحب الاستدعاء رموزًا من الدلو، ويعيد الزمن رموزًا إليه. دلو سعته 60 رمزًا يتعبأ بمعدل رمز واحد في الثانية يسمح باندفاع من 60 استدعاءً، ثم باستدعاء واحد في الثانية، وهو أمر سهل شرحه للمستخدمين بعبارة "60 الآن، و60 في الدقيقة بعد ذلك".
خاصيتان تجعلانه مثاليًا لـ MCP:
التعبئة الكسولة. تحسب التعبئة عند وصول الطلب، لذلك لا توجد مؤقتات تحتاج إلى إدارة.
دعم التكلفة. قد تأخذ أداة فيديو 20 رمزًا بينما يأخذ البحث رمزًا واحدًا، كلها من الميزانية نفسها.
ابنِ المحدِّد بـ TypeScript
يعمل المحدِّد أدناه في أي خادم MCP مكتوب بلغة TypeScript ومبني على @modelcontextprotocol/sdk. لا يعتمد على أي تبعيات، ويحتفظ بحالته في الذاكرة.
فئة المحدِّد
// rate-limit.ts
export type Decision = {
allowed: boolean;
remaining: number;
retryAfterMs: number;
};
type Bucket = { tokens: number; updatedAt: number };
export class TokenBucket {
private buckets = new Map<string, Bucket>();
constructor(
private readonly capacity: number,
private readonly refillPerSecond: number,
) {}
take(id: string, cost = 1): Decision {
if (cost > this.capacity) {
throw new RangeError(`Cost ${cost} is larger than the bucket (${this.capacity})`);
}
const now = Date.now();
const bucket = this.buckets.get(id) ?? { tokens: this.capacity, updatedAt: now };
const elapsedSeconds = (now - bucket.updatedAt) / 1000;
bucket.tokens = Math.min(this.capacity, bucket.tokens + elapsedSeconds * this.refillPerSecond);
bucket.updatedAt = now;
this.buckets.set(id, bucket);
if (bucket.tokens >= cost) {
bucket.tokens -= cost;
return { allowed: true, remaining: Math.floor(bucket.tokens), retryAfterMs: 0 };
}
const missing = cost - bucket.tokens;
return {
allowed: false,
remaining: 0,
retryAfterMs: Math.ceil((missing / this.refillPerSecond) * 1000),
};
}
// Drop idle buckets so the map cannot grow forever.
sweep(maxIdleMs = 10 * 60_000) {
const cutoff = Date.now() - maxIdleMs;
for (const [id, bucket] of this.buckets) {
if (bucket.updatedAt < cutoff) this.buckets.delete(id);
}
}
}
export const budget = new TokenBucket(60, 1); // burst of 60, refills one per second
setInterval(() => budget.sweep(), 60_000).unref();
تستحق ثلاث تفاصيل نظرة ثانية. تأتي التعبئة من الوقت المنقضي، لذلك لا يوجد setInterval لكل مستدعٍ. ويتيح الوسيط cost لمحدِّد واحد أن يخدم الأدوات الرخيصة والمكلفة. ويوضح retryAfterMs بدقة المدة اللازمة حتى يحتوي الدلو على الرموز الكافية، وهو الرقم الذي تعرضه على الوكيل.
غلّف كل معالج أداة
ضع الفحص في غلاف واحد حتى لا تنسى أي أداة تطبيقه:
import { z } from "zod";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { budget } from "./rate-limit.js";
type Extra = { authInfo?: { clientId?: string }; sessionId?: string };
export function limited<Args>(
tool: string,
cost: number,
handler: (args: Args, extra: Extra) => Promise<CallToolResult>,
) {
return async (args: Args, extra: Extra): Promise<CallToolResult> => {
const caller = extra.authInfo?.clientId ?? "anonymous";
const decision = budget.take(caller, cost);
if (!decision.allowed) {
const seconds = Math.ceil(decision.retryAfterMs / 1000);
return {
isError: true,
content: [
{
type: "text",
text: `Rate limit reached for ${tool}. Wait ${seconds} seconds before calling it again.`,
},
],
};
}
return handler(args, extra);
};
}
server.registerTool(
"generate_image",
{ description: "Generate an image from a prompt", inputSchema: { prompt: z.string().max(4000) } },
limited("generate_image", 5, async ({ prompt }) => {
const url = await createImage(prompt);
return { content: [{ type: "text", text: url }] };
}),
);
💡 أعِد الحظر كنتيجة أداة، لا كخطأ مُلقى. تفصل المواصفة بين أخطاء البروتوكول وأخطاء تنفيذ الأدوات. تبقى النتيجة التي تحمل isError: true داخل سياق النموذج، فيقرأ الوكيل "انتظر 12 ثانية" ويتكيّف. أما الاستثناء المُلقى فيصبح خطأ JSON-RPC تعرضه كثير من العملاء كفشل ولا شيء أكثر.
عرّف المستدعين وأحكم حماية نقطة النهاية
لا يكون الحد عادلًا إلا بقدر طريقة تعريفك للمستدعي. إذا أخطأت فيه، فإما أن تخنق الجميع معًا، أو تسمح لعميل واحد بالتحايل على الحد بإعادة الاتصال.
اختر الهوية المناسبة
النقل والمصادقة
الهوية المستخدمة
انتبه إلى
stdio
ميزانية واحدة مشتركة لكل أداة
عملية واحدة تخدم عميلًا واحدًا، لذلك لا يوجد مستدعٍ لتمييزه
Streamable HTTP مع OAuth
معرّف العميل أو المستخدم من authInfo
الخيار الأفضل، لأنه يصمد أمام إعادة الاتصال
Streamable HTTP مع رمز حامل ثابت
تجزئة (hash) للرمز
دوّر الرموز وجزّئها قبل تخزينها
HTTP مجهول
عنوان IP
المكاتب المشتركة والشبكات المتنقلة تبدو كمستدعٍ واحد
تجنّب إغراء استخدام ترويسة Mcp-Session-Id كهوية رئيسية. الخادم هو من يصدرها، ويستطيع العميل ببساطة بدء جلسة جديدة للحصول على دلو جديد. استخدم الجلسات كحد ثانوي، مثلًا لتقييد المهام الجارية في كل جلسة، واربط الميزانية بشيء يصمد أمام إعادة الاتصال.
أعِد 429 مع Retry-After
ضع حدًا عامًا على طبقة HTTP وحدًا دقيقًا داخل الأدوات. طبقة HTTP رخيصة وتعمل قبل تحليل أي JSON أو إنشاء أي جلسة، لذلك تحمي الخادم من الفيضانات. لكنها لا تستطيع التمييز بين tools/list وtools/call مكلف دون قراءة المحتوى، لذا أبقِها سخية ودع طبقة الأدوات تتولى الحساب الدقيق.
import { createHash } from "node:crypto";
import type { NextFunction, Request, Response } from "express";
import { TokenBucket } from "./rate-limit.js";
const httpBudget = new TokenBucket(120, 2); // 120 burst, 2 per second sustained
function callerId(req: Request): string {
const auth = req.header("authorization");
if (auth) return "tok:" + createHash("sha256").update(auth).digest("hex").slice(0, 16);
return "ip:" + req.ip;
}
export function limitHttp(req: Request, res: Response, next: NextFunction) {
const decision = httpBudget.take(callerId(req));
res.setHeader("RateLimit-Remaining", String(decision.remaining));
if (decision.allowed) return next();
res.setHeader("Retry-After", String(Math.ceil(decision.retryAfterMs / 1000)));
res.status(429).json({
jsonrpc: "2.0",
error: { code: -32000, message: "Too many requests. Retry after the delay in Retry-After." },
id: null,
});
}
// app.set("trust proxy", 1);
// app.post("/mcp", limitHttp, handleMcp);
جزّئ الرمز الحامل قبل استخدامه كمعرّف، حتى لا تبقى بيانات الاعتماد الخام في Map أو في سطر سجل أبدًا. أرسل Retry-After بالثواني الكاملة، لأن عملاء HTTP الذين يملكون منطق إعادة المحاولة يقرؤونها. وخلف وكيل عكسي، اضبط trust proxy، وإلا فسيشترك كل مستدعٍ مجهول في عنوان الوكيل.
زِن الأدوات حسب تكلفتها
إذا دخلت مطبخ مطعم مزدحم، سترى تذاكر طلبات بأحجام مختلفة كثيرًا معلّقة على السكة نفسها. فالسلطة الجانبية والطبق المطهو ببطء لا يتطلبان الجهد نفسه، والمطبخ الجيد لا يعاملهما بالطريقة نفسها. وتعمل استدعاءات الأدوات بالطريقة ذاتها.
نوع الأداة
مثال
تكلفة الرموز
حماية إضافية
بحث للقراءة فقط
list_articles، get_article
1
لا شيء
كتابة أو نشر
save_article
2
فحص عدم التكرار (idempotency)
توليد النصوص
ملخصات من نموذج لغوي
3
تحديد طول المخرجات
توليد الصور
generate_image
5
2 متزامنان لكل مستدعٍ
توليد الفيديو
generate_image_to_video
20
1 متزامن، وإرسالات متباعدة
مع دلو سعته 60 رمزًا يتعبأ بمعدل رمز واحد في الثانية، يستطيع المستدعي تشغيل 60 بحثًا في اندفاعة واحدة، أو 12 توليد صورة، أو 3 مهام فيديو، ويتعبأ الدلو بالكامل خلال دقيقة.
أوزان التكلفة لكل أداة
احفظ الأوزان في مكان واحد ومرّرها إلى الغلاف من القسم السابق:
ابدأ بأوزان تتناسب مع ما يكلفه كل استدعاء من المال أو من ثوانٍ لدى المزوّد، ثم عدّلها بناءً على الحركة الفعلية.
حدّ المهام وتراجع المنبع
تحدّ ميزانية الرموز من عدد مرات بدء المستدعي للعمل. لكنها لا تحد من مقدار العمل الذي يجري في اللحظة نفسها.. تحتاج الأدوات طويلة التشغيل إلى حد للتزامن، وتحتاج طوابير المنبع المشتركة أحيانًا إلى تباعد بين الإرسالات. وكلاهما قصير:
const inFlight = new Map<string, number>();
export async function withConcurrency<T>(
caller: string,
max: number,
job: () => Promise<T>,
): Promise<T | "busy"> {
const current = inFlight.get(caller) ?? 0;
if (current >= max) return "busy";
inFlight.set(caller, current + 1);
try {
return await job();
} finally {
const left = (inFlight.get(caller) ?? 1) - 1;
if (left <= 0) inFlight.delete(caller);
else inFlight.set(caller, left);
}
}
// One submission per slot: concurrent callers queue behind each other.
let nextSlot = 0;
export async function waitForSlot(minGapMs = 30_000) {
const now = Date.now();
const start = Math.max(now, nextSlot);
nextSlot = start + minGapMs;
await new Promise((resolve) => setTimeout(resolve, start - now));
}
// When the upstream API answers 429 or 5xx, wait and retry with jitter.
export async function fetchWithBackoff(
send: () => Promise<Response>,
maxAttempts = 4,
): Promise<Response> {
for (let attempt = 1; ; attempt++) {
const response = await send();
const retryable = response.status === 429 || response.status >= 500;
if (!retryable || attempt >= maxAttempts) return response;
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 500;
await new Promise((resolve) => setTimeout(resolve, delayMs + Math.random() * 250));
}
}
أعِد "busy" للوكيل كخطأ أداة عادي يوضح أن مهمة تعمل بالفعل، ويقترح الاستعلام عن حالتها. يكتسب التشويش العشوائي (jitter) في fetchWithBackoff أهميته: من دونه، ستستيقظ كل نسخة تلقّت رمز 429 في اللحظة نفسها وتضرب المنبع معًا من جديد.
💡 حدود حقيقية، معلنة. تنشر واجهة API الخاصة بمنصة PicassoIA عدد 5 تنبؤات متزامنة لكل حساب، مشتركة بين رموز API واتصالات MCP، بالإضافة إلى أوامر نصية بطول 4,000 حرف، وأجسام طلبات بحجم 10 ميغابايت، ومهلة قدرها 3 ساعات. كما تعيد أدوات الصور والفيديو الخاصة بها تلميح predict_id وتلميح next_poll_in_seconds، فلا يضطر العميل أبدًا إلى التخمين حول عدد مرات الاستعلام. انسخ هذه الفكرة: الحد الذي يملك رقمًا معلنًا وتلميحًا للاستعلام هو حد يستطيع الوكلاء الالتزام به.
التوسع إلى ما بعد عملية واحدة
للمحدِّد المعتمد على الذاكرة عيب واحد: ذاكرته تخص عملية واحدة. إذا شغّلت ثلاث نسخ خلف موازن أحمال، فسيحتفظ كل منها بعدّاداته الخاصة، فيحصل المستدعي فعليًا على ثلاثة أضعاف الحد. والمنصات بلا خوادم أسوأ، لأن كل بدء بارد يبدأ بدلاء فارغة. الحل هو نقل العدّادات إلى مخزن مشترك، وRedis هو الخيار المعتاد لأن عملياته ذرّية وسريعة.
عدّادات مشتركة مع Redis
import Redis from "ioredis";
import { RateLimiterRedis, RateLimiterRes } from "rate-limiter-flexible";
import type { Decision } from "./rate-limit.js";
const redis = new Redis(process.env.REDIS_URL!);
const FAIL_OPEN = process.env.RATE_LIMIT_FAIL_OPEN === "true";
const limiter = new RateLimiterRedis({
storeClient: redis,
points: 60, // budget per window
duration: 60, // window length in seconds
});
export async function takeShared(caller: string, cost: number): Promise<Decision> {
try {
const res = await limiter.consume(caller, cost);
return { allowed: true, remaining: res.remainingPoints, retryAfterMs: 0 };
} catch (rejection) {
if (rejection instanceof RateLimiterRes) {
return { allowed: false, remaining: 0, retryAfterMs: rejection.msBeforeNext };
}
// Redis itself failed, so apply the configured failure policy.
return { allowed: FAIL_OPEN, remaining: 0, retryAfterMs: 5_000 };
}
}
تعدّ هذه المكتبة بنوافذ ثابتة، لذلك ينطبق الاندفاع عند الحواف من جدول الخوارزميات. بالنسبة لمعظم خوادم MCP، هذه المقايضة مقبولة. أما إذا احتجت إلى دلو رموز حقيقي عبر النسخ، فخزّن الرقمين في تجزئة Redis (hash) ونفّذ التعبئة والسحب في سكربت Lua واحد، لأن القراءة ثم الكتابة من نسختين مختلفتين سباق على البيانات (race).
الفشل المفتوح أو الفشل المغلق
سيتوقف Redis عن العمل يومًا ما، ويجب أن يختار المحدِّد أحد الخيارين:
فشل مغلق للأدوات التي تكلّف مالًا. فانقطاع قصير أرخص من طابور فيديو غير محدود.
فشل مفتوح للقراءات الرخيصة، حيث يضر حظر الجميع أكثر من اندفاعة من عمليات البحث.
تراجع، لا تعطيل. تدعم المكتبة insuranceLimiter في الذاكرة كبديل احتياطي. عندها تُطبّق الحدود لكل نسخة، وهذا أفضل بكثير من لا شيء.
مهما اخترت، سجّل كل فشل للمحدِّد بوضوح، لأن الفشل المفتوح الصامت هو الطريقة التي ينتهي بها الحد دون أن يلاحظ أحد.
اختبر وراقب الحدود
المحدِّد الذي لم يرفض شيئًا في أي اختبار لا يمكنك الوثوق به.
الاختبار بالموقّتات الوهمية
تتيح الموقّتات الوهمية لاختبار الوحدة أن ينتقل عبر دقيقة كاملة في جزء من الألف من الثانية:
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { TokenBucket } from "./rate-limit";
describe("TokenBucket", () => {
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it("allows a burst, then blocks with a wait time", () => {
const bucket = new TokenBucket(3, 1);
for (let i = 0; i < 3; i++) expect(bucket.take("a").allowed).toBe(true);
const blocked = bucket.take("a");
expect(blocked.allowed).toBe(false);
expect(blocked.retryAfterMs).toBeGreaterThan(0);
});
it("refills as time passes", () => {
const bucket = new TokenBucket(1, 1);
bucket.take("a");
expect(bucket.take("a").allowed).toBe(false);
vi.advanceTimersByTime(1000);
expect(bucket.take("a").allowed).toBe(true);
});
it("keeps callers apart", () => {
const bucket = new TokenBucket(1, 1);
bucket.take("a");
expect(bucket.take("b").allowed).toBe(true);
});
});
بعد اختبارات الوحدة، شغّل الخادم الحقيقي تحت MCP Inspector (npx @modelcontextprotocol/inspector) واستدعِ أداة واحدة في حلقة. يجب أن تنجح الاستدعاءات الأولى، وأن تعيد البقية رسالة حد المعدل مع مدة الانتظار. ولاختبار طبقة HTTP، أرسل اندفاعة باستخدام curl وعُدّ رموز الحالة:
for i in $(seq 1 150); do
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:3000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"ping"}'
done | sort | uniq -c
تعتمد الاستجابات الأولى على إعداد الجلسة لديك، لكن بمجرد أن يفرغ الدلو يجب أن تكون كل استجابة 429.
مقاييس تستحق المراقبة
سجّل كل رفض مع اسم الأداة ودلو المستدعي (وليس الهوية الخام أبدًا). هذه الأرقام القليلة تُظهر ما إذا كانت الحدود عادلة:
المقياس
ما الذي يخبرك به
نبّه عندما
معدل الرفض لكل أداة
حدود ضيقة أكثر من اللازم، أو مستدعٍ واحد صاخب
أعلى من 5% لمدة 10 دقائق
أعلى المستدعين من حيث الرفض
وكيل في حلقة أو وكيل مسيء
مستدعٍ واحد يستحوذ على أكثر من النصف
Retry-After، المئين 95
المدة الفعلية التي ينتظرها الوكلاء
أعلى من 60 ثانية
المهام الجارية
تشبع التزامن
عند الحد الأقصى لمدة 5 دقائق
أخطاء المحدِّد
صحة Redis
أي خطأ
أخطاء تظهر في الخوادم الحقيقية:
استخدام معرّف الجلسة كهوية وحيدة، فتعيد إعادة الاتصال ضبط الحد.
مشاركة دلو عام واحد، فيُجيع مستدعٍ ثقيل الجميع.
إسقاط الطلبات أو تعطيلها بصمت بدلًا من إعادة خطأ مع مدة انتظار.
ضبط الحدود مرة واحدة ثم عدم قراءة أرقام الرفض مجددًا.
جرّبه على PicassoIA
المحدِّد أعلاه أقل من مئة سطر، وهذا ما يجعله مهمة جيدة لنموذج لغوي: مواصفات دقيقة، وسهل الاختبار، وسريع المراجعة. تضع PicassoIA عدة نماذج قادرة على البرمجة في مكان واحد، لتتمكن من الصياغة والمقارنة والإصلاح دون التنقل بين حسابات متعددة.
Write a TypeScript token bucket limiter for an MCP server built on
@modelcontextprotocol/sdk. Budget: 60 tokens, refill 1 per second.
Costs: list_articles 1, generate_image 5, generate_image_to_video 20.
Identify callers by authInfo.clientId, fall back to "anonymous".
When blocked, return isError: true with the wait time in seconds.
Include vitest tests that use fake timers.
اطلب الاختبارات أولًا. قراءة الاختبارات تُظهر السلوك الذي افترضه النموذج قبل أن تقرأ أي سطر من التنفيذ.
شغّلها وأعد الأخطاء إليه. الصق مخرجات الخطأ الدقيقة في المحادثة نفسها واطلب إصلاحًا.
اطلب مراجعة. اختم بعبارة "راجع هذا المحدِّد بحثًا عن حالات السباق (race conditions) ونمو الذاكرة" واقرأ الإجابة بعين ناقدة.
اجعل كل طلب متعلقًا بموضوع واحد، وزوّد النموذج بأرقامك الفعلية بدلًا من "القيم الافتراضية المعقولة". تستحق نماذج أخرى رأيًا ثانيًا:
بعد أن تصبح خادمك محميًا، ضعه قيد العمل. كل صورة في هذا المقال بدأت كأمر نصي بسيط كُتب لنموذج P-Image، ويمكنك تشغيل الأمر نفسه على Flux 2 Pro لمقارنة النتائج. افتح Picasso IA، صِف مشهدًا في جملة أو جملتين، وولّد أول صورة لك. غيّر العدسة أو الإضاءة أو الزاوية، وأعد التشغيل، ثم حوّل نتيجتك المفضلة إلى فيديو قصير. أسرع طريقة لتكتشف ما تستطيع المنصة فعله هي أن تجرّب أفكارك الخاصة.