الفاتورة التي لم يضعها أحد في الميزانية
مساعد RAG لديك يعمل بكفاءة. يسترجع المستندات الصحيحة، والإجابات جيدة، والمستخدمون راضون. ثم يأتي أول شهر كامل من الاستخدام الفعلي، وتكون فاتورة الواجهة البرمجية أربعة أضعاف ما أشار إليه النموذج الأولي.
لم يتعطّل شيء. أنت ببساطة تدفع السعر الكامل، في كل طلب على حدة، لإعادة إرسال نفس مطالبة النظام البالغة 40,000 توكن، ونفس تعريفات الأدوات، ونفس سجل المحادثة الذي عالجه النموذج قبل ثلاثين ثانية.
التخزين المؤقت للمطالبات يعالج هذه المشكلة تحديدًا. ليس إعادة كتابة، ولا حزمة تطوير جديدة، ولا نموذجًا مختلفًا — إنه حقل تضيفه إلى كتلة محتوى. لكن من السهل أيضًا إضافته بطريقة لا تُحدث أي أثر على الإطلاق، وهذا هو سبب أن معظم الفرق التي "فعّلت التخزين المؤقت" ما زالت تدفع السعر الكامل دون أن تدري.
يغطي هذا الدرس الآلية بصدق: كيف تعمل مطابقة البادئة، ولماذا يمكن لطابع زمني واحد أن يعطّل كل شيء بصمت، وأين تضع نقاط التوقف فعليًا، وكيف تُثبت التوفير بأرقام حقيقية بدلًا من الأمل.
بنهاية هذا الدرس سيكون لديك:
- عميل Claude يعمل بـ TypeScript مع تطبيق صحيح للتخزين المؤقت
- مسار API في Next.js يخدم سياق مستند كبير مع تخزين مؤقت
- أداة قياس لمعدل الإصابة تُبلّغ عن التوفير الحقيقي في التوكنات لكل طلب
- قائمة تدقيق للمبطلات التي تعطّل التخزين المؤقت بصمت
- تخزين مؤقت لمحادثات متعددة الأدوار يستمر في تحقيق العائد مع نمو المحادثة
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- Node.js 20+ مثبّتًا
- أساسيات TypeScript — الواجهات، async/await، الاتحادات المميّزة
- مفتاح Anthropic API (مضبوط كـ
ANTHROPIC_API_KEY) أو ملف تعريف نشط منant auth login - إلمام باستدعاء واجهة نموذج لغوي مرة واحدة على الأقل — هذا ليس دليلًا تمهيديًا للنماذج اللغوية
- اختياري: تطبيق Next.js 15+ إذا أردت متابعة قسم مسار الـ API
كل ما هنا يستخدم حزمة @anthropic-ai/sdk الرسمية. لا أغلفة، ولا LangChain، ولا طبقات تجريد وسيطة.
ما الذي ستبنيه
نقطة نهاية للأسئلة والأجوبة على المستندات تُحمّل مجموعة مرجعية كبيرة في مطالبة النظام مرة واحدة، ثم تجيب على عدد غير محدود من أسئلة المستخدمين مقابلها بينما تدفع تقريبًا عُشر سعر الإدخال للجزء المشترك.
على الطريق سنبني أداة صغيرة باسم logCacheUsage تطبع، لكل طلب، العدد الدقيق للتوكنات المكتوبة في الذاكرة المؤقتة، والمقروءة منها، والمعالجة بالسعر الكامل — لأن التخزين المؤقت الذي لا تستطيع قياسه هو تخزين مؤقت لا يمكنك الوثوق به.
الخطوة 1: القاعدة الوحيدة التي ينبثق منها كل شيء
اقرأ هذا القسم مرتين. كل خلل غريب في التخزين المؤقت يعود إليه.
التخزين المؤقت للمطالبات هو مطابقة بادئة. أي تغيير في أي موضع من البادئة يُبطل كل ما يليه.
يُشتق مفتاح الذاكرة المؤقتة من البايتات الدقيقة للمطالبة المُصيَّرة حتى كل نقطة توقف. اختلاف بايت واحد في الموضع N يُبطل الذاكرة المؤقتة لكل نقطة توقف عند الموضع N أو بعده.
تُصيّر الواجهة البرمجية طلبك بترتيب ثابت:
tools → system → messages
هذا الترتيب هو جوهر المسألة كلها. الأدوات تُصيَّر أولًا، لذا فإن تغيير تعريف أداة يُبطل كل شيء. مطالبة النظام تُصيَّر ثانيًا، لذا فإن طابعًا زمنيًا مُدرجًا داخل مطالبة النظام يُبطل سجل المحادثة الكامل الذي يليه.
هذا يمنحك مبدأ تصميم واحدًا تتبعه في بقية الدرس:
المحتوى المستقر أولًا. المحتوى المتغيّر أخيرًا.
إذا كان جزء من مطالبتك يتغير في كل طلب، فيجب أن يقع بعد نقطة التوقف الأخيرة. وإذا وقع قبلها، فلن يُخزَّن أي شيء يليه مؤقتًا مهما نثرت من علامات cache_control.
الخطوة 2: إعداد المشروع
أنشئ المشروع وثبّت حزمة التطوير.
mkdir claude-caching-demo && cd claude-caching-demo
npm init -y
npm install @anthropic-ai/sdk
npm install -D typescript tsx @types/node
npx tsc --initاضبط بيانات الاعتماد. لا تضع المفتاح في الشيفرة مطلقًا.
export ANTHROPIC_API_KEY="sk-ant-..."أنشئ src/client.ts:
import Anthropic from "@anthropic-ai/sdk";
// المُنشئ بدون وسائط يحل بيانات الاعتماد من البيئة:
// ANTHROPIC_API_KEY، ثم ANTHROPIC_AUTH_TOKEN، ثم ملف تعريف من `ant auth login`.
export const client = new Anthropic();
export const MODEL = "claude-opus-5";ملاحظة حول اختيار النموذج أهم مما تبدو عليه: الحد الأدنى للبادئة القابلة للتخزين المؤقت يختلف حسب النموذج، وهو ليس تصاعديًا عبر الأجيال.
| النموذج | الحد الأدنى للبادئة القابلة للتخزين |
|---|---|
| Claude Opus 5 | 512 توكن |
| Claude Opus 4.8، Claude Sonnet 5، Sonnet 4.6 | 1024 توكن |
| Claude Opus 4.7 | 2048 توكن |
| Claude Opus 4.6، Haiku 4.5 | 4096 توكن |
مطالبة بحجم 3,000 توكن تُخزَّن مؤقتًا على Claude Opus 5 ولا تُخزَّن بصمت على Haiku 4.5. لا يوجد خطأ ولا تحذير — تحصل فقط على cache_creation_input_tokens: 0 وفاتورة تبدو طبيعية. إذا انتقلت بين النماذج وانخفض معدل الإصابة إلى الصفر، راجع هذا الجدول قبل تصحيح أي شيء آخر.
الخطوة 3: طلب مرجعي بدون تخزين مؤقت
ابدأ بالنسخة الساذجة ليكون لدينا ما نقارن به. أنشئ src/baseline.ts:
import { client, MODEL } from "./client";
import { readFileSync } from "fs";
// تخيّل أن هذا هو توثيق منتجك، أو مجموعتك القانونية،
// أو ملخص قاعدة الشيفرة — أي شيء كبير ومستقر.
const REFERENCE_DOC = readFileSync("./data/handbook.md", "utf-8");
const SYSTEM_PROMPT = `You are a support assistant for Noqta.
Answer strictly from the reference document below.
If the answer is not in the document, say so plainly.
<reference_document>
${REFERENCE_DOC}
</reference_document>`;
async function ask(question: string) {
const response = await client.messages.create({
model: MODEL,
max_tokens: 16000,
system: SYSTEM_PROMPT,
messages: [{ role: "user", content: question }],
});
for (const block of response.content) {
if (block.type === "text") {
console.log(block.text);
}
}
console.log("--- usage ---", response.usage);
}
await ask("What is the refund window for annual plans?");
await ask("Do you support SSO on the team tier?");شغّله:
npx tsx src/baseline.tsكلا الطلبين يُبلّغان عن قيمة input_tokens كبيرة ونشاط تخزين مؤقت معدوم. إذا كان الدليل بحجم 40,000 توكن، فقد دفعت مقابل 40,000 توكن إدخال مرتين للإجابة على سؤالين غير مترابطين من سطر واحد. عشرة آلاف سؤال شهريًا تعني 400 مليون توكن إدخال من التكرار الخالص.
لاحظ تضييق الاتحاد المميّز في تلك الحلقة. response.content من نوع ContentBlock[]، وسيرفض TypeScript الوصول إلى content[0].text دون فحص block.type === "text". هذا ليس خاصًا بالتخزين المؤقت، لكنه يُعثِر الجميع في أول طلب Claude لهم بـ TypeScript.
الخطوة 4: أضف التخزين المؤقت — الطريقة البسيطة
أسرع إصلاح صحيح هو التخزين المؤقت التلقائي على المستوى الأعلى. أضف حقلًا واحدًا.
أنشئ src/cached-auto.ts:
import { client, MODEL } from "./client";
import { readFileSync } from "fs";
const REFERENCE_DOC = readFileSync("./data/handbook.md", "utf-8");
const SYSTEM_PROMPT = `You are a support assistant for Noqta.
Answer strictly from the reference document below.
If the answer is not in the document, say so plainly.
<reference_document>
${REFERENCE_DOC}
</reference_document>`;
async function ask(question: string) {
const response = await client.messages.create({
model: MODEL,
max_tokens: 16000,
// يضع نقطة التوقف تلقائيًا على آخر كتلة قابلة للتخزين.
cache_control: { type: "ephemeral" },
system: SYSTEM_PROMPT,
messages: [{ role: "user", content: question }],
});
console.log("--- usage ---", response.usage);
return response;
}
await ask("What is the refund window for annual plans?");
await ask("Do you support SSO on the team tier?");شغّله وانظر إلى كائني الاستخدام:
--- usage --- { input_tokens: 14, cache_creation_input_tokens: 41203, cache_read_input_tokens: 0, output_tokens: 87 }
--- usage --- { input_tokens: 15, cache_creation_input_tokens: 0, cache_read_input_tokens: 41203, output_tokens: 64 }
السطر الثاني هو بيت القصيد. الطلب الأول كتب 41,203 توكن في الذاكرة المؤقتة بنحو 1.25 ضعف سعر الإدخال العادي. الطلب الثاني قرأها بنحو 0.1 من السعر.
حقل واحد في كائن الاستخدام يستحق أن تستوعبه: input_tokens هو الباقي غير المخزَّن فقط، وليس إجمالي حجم المطالبة. الإجمالي الحقيقي هو:
input_tokens + cache_creation_input_tokens + cache_read_input_tokens
إذا كان لديك وكيل عمل لساعتين وinput_tokens يُبلّغ عن 14، فهذا ليس خللًا ولا معجزة — الباقي جاء من الذاكرة المؤقتة. لوحات التكلفة التي ترسم input_tokens وحده ستُبلّغ عن استخدامك الحقيقي بأقل من قيمته بهدوء.
الخطوة 5: نقاط التوقف اليدوية، ومتى تحتاجها
التخزين التلقائي يضع نقطة توقف واحدة على آخر كتلة قابلة للتخزين. هذا صحيح لحالة السياق المشترك أعلاه، وخاطئ لحالة شائعة جدًا: بادئة مشتركة تليها لاحقة متغيرة في نفس الرسالة.
خذ مصنّفًا يعتمد على أمثلة قليلة يُرسل في كل طلب نفس مجموعة الأمثلة الكبيرة مع مدخل مختلف:
// خطأ — نقطة التوقف تقع بعد المدخل المتغير،
// لذا كل طلب يكتب مدخلة ذاكرة مؤقتة جديدة ولا يقرأ أبدًا.
const response = await client.messages.create({
model: MODEL,
max_tokens: 1024,
cache_control: { type: "ephemeral" },
messages: [
{
role: "user",
content: [
{ type: "text", text: FEW_SHOT_EXAMPLES },
{ type: "text", text: `Classify this ticket: ${ticketBody}` },
],
},
],
});كل طلب يُنتج كتلة أخيرة مختلفة، لذا كل طلب يُفهرس مدخلة ذاكرة مؤقتة مختلفة. تدفع علاوة الكتابة البالغة 1.25 ضعف إلى الأبد ولا تقرأ شيئًا. هذه هي الطريقة الأكثر شيوعًا لجعل التخزين المؤقت أغلى من عدم استخدامه.
الإصلاح هو تعليم نهاية الجزء المشترك صراحةً:
// صحيح — نقطة التوقف على الأمثلة المشتركة، والمدخل المتغير بعدها.
const response = await client.messages.create({
model: MODEL,
max_tokens: 1024,
messages: [
{
role: "user",
content: [
{
type: "text",
text: FEW_SHOT_EXAMPLES,
cache_control: { type: "ephemeral" },
},
// بدون علامة — هذا يختلف في كل مرة ويجب أن يأتي أخيرًا.
{ type: "text", text: `Classify this ticket: ${ticketBody}` },
],
},
],
});قواعد الوضع اليدوي:
- بحد أقصى 4 نقاط توقف لكل طلب
- صالحة على كتل نص النظام، وتعريفات الأدوات، وكتل محتوى الرسائل (
text،image،tool_use،tool_result،document) - نقطة توقف على آخر كتلة نظام تُخزّن الأدوات و النظام معًا، لأن الأدوات تُصيَّر أولًا
الخطوة 6: ابنِ أداة قياس للتخزين المؤقت
التخمين في سلوك الذاكرة المؤقتة هو ما يجعل الفرق تنتهي بتطبيق تخزين مؤقت لم يُنتج إصابة واحدة قط. ابنِ القياس أولًا.
أنشئ src/cache-usage.ts:
import type Anthropic from "@anthropic-ai/sdk";
// أسعار تقريبية لـ Claude Opus 5، بالدولار لكل مليون توكن.
const INPUT_RATE = 5.0;
const CACHE_WRITE_MULTIPLIER = 1.25; // 2.0 لمدة صلاحية الساعة
const CACHE_READ_MULTIPLIER = 0.1;
export function logCacheUsage(label: string, usage: Anthropic.Usage) {
const fresh = usage.input_tokens;
const written = usage.cache_creation_input_tokens ?? 0;
const read = usage.cache_read_input_tokens ?? 0;
const total = fresh + written + read;
const actual =
(fresh +
written * CACHE_WRITE_MULTIPLIER +
read * CACHE_READ_MULTIPLIER) *
(INPUT_RATE / 1_000_000);
const uncached = total * (INPUT_RATE / 1_000_000);
const saved = uncached - actual;
const hitRate = total > 0 ? (read / total) * 100 : 0;
console.log(
[
`[${label}]`,
`total=${total}`,
`fresh=${fresh}`,
`written=${written}`,
`read=${read}`,
`hit=${hitRate.toFixed(1)}%`,
`cost=$${actual.toFixed(5)}`,
`saved=$${saved.toFixed(5)}`,
].join(" "),
);
return { total, fresh, written, read, hitRate, actual, saved };
}اربطها بأي استدعاء:
import { logCacheUsage } from "./cache-usage";
const response = await client.messages.create({ /* ... */ });
logCacheUsage("doc-qa", response.usage);الآن أصبحت قاعدة التشخيص ملموسة: إذا بقيت قيمة read عند 0 عبر طلبات متكررة ببادئة يُفترض أنها متطابقة، فلديك مُبطِل صامت. هذا ليس قيدًا في التخزين المؤقت — إنه خلل في تجميع مطالبتك، والخطوة التالية هي كيفية إيجاده.
الخطوة 7: دقّق المبطلات الصامتة
هذه هي الأنماط التي تعطّل التخزين المؤقت بهدوء. ابحث عن كل واحد منها في شيفرة تجميع المطالبات لديك.
| النمط | لماذا يعطّل التخزين المؤقت |
|---|---|
Date.now() أو new Date() في مطالبة النظام | البادئة تتغير في كل طلب |
crypto.randomUUID() أو معرّف طلب موضوع مبكرًا | نفس الشيء — كل طلب فريد على مستوى البايت |
JSON.stringify(obj) على كائن بترتيب مفاتيح غير مستقر | التسلسل يختلف من تشغيل لآخر |
| إدراج معرّف جلسة أو مستخدم في مطالبة النظام | بادئة لكل مستخدم؛ لا مشاركة بين المستخدمين |
أقسام نظام شرطية مبنية بـ if (flag) | كل تركيبة أعلام هي بادئة مستقلة |
tools: buildTools(user) حيث تختلف المجموعة لكل مستخدم | الأدوات تُصيَّر في الموضع 0، فلا يُخزَّن شيء إطلاقًا |
المخالف الأكثر شيوعًا بفارق كبير:
// هذا السطر الواحد يجعل المحادثة كاملة غير قابلة للتخزين المؤقت.
const system = `You are a helpful assistant.
Current date: ${new Date().toISOString()}
${LARGE_STABLE_INSTRUCTIONS}`;التاريخ يقع في مقدمة البادئة، لذا فإن الـ 30,000 توكن من التعليمات المستقرة التي تليه تُعاد معالجتها بالسعر الكامل في كل مرة.
الإصلاح هو نقل السياق المتغير خارج مطالبة النظام تمامًا:
const system = [
{
type: "text" as const,
text: LARGE_STABLE_INSTRUCTIONS,
cache_control: { type: "ephemeral" as const },
},
];
const messages = [
...history,
{
role: "user" as const,
content: `Current date: ${new Date().toISOString()}\n\n${userQuestion}`,
},
];نفس المعلومة، تصل إلى النموذج، مع بقاء الذاكرة المؤقتة سليمة.
سلسِل الأدوات بشكل حتمي
الأدوات تُصيَّر في الموضع 0، ما يجعلها أعلى العناصر أثرًا في الحفاظ على الاستقرار:
// الترتيب حسب الاسم حتى لا يعتمد الترتيب على تكرار مفاتيح كائن أو Set.
const tools = Object.values(toolRegistry).sort((a, b) =>
a.name.localeCompare(b.name),
);لا تُبدّل مجموعة الأدوات لتطبيق "الأوضاع". إذا احتجت تبديل وضع، مرّره كمحتوى رسالة، أو امنح النموذج أداة تُسجّل الانتقال — أي شيء عدا تغيير مصفوفة الأدوات في منتصف المحادثة.
الخطوة 8: ليس كل شيء يُبطل كل شيء
هنا ينشأ كثير من الحذر الزائد. تغيير معامل في الطلب لا يُدمّر الذاكرة المؤقتة كلها تلقائيًا. هناك ثلاث طبقات، والتغيير يُبطل طبقته والطبقات التي تحتها فقط.
| التغيير | ذاكرة الأدوات تصمد | ذاكرة النظام تصمد | ذاكرة الرسائل تصمد |
|---|---|---|---|
| إضافة أو حذف أو إعادة ترتيب تعريفات الأدوات | لا | لا | لا |
| تبديل النموذج | لا | لا | لا |
| تفعيل أو تعطيل البحث على الويب أو الاستشهادات | نعم | لا | لا |
| تعديل محتوى مطالبة النظام | نعم | لا | لا |
تغيير tool_choice، إضافة صور، تبديل thinking | نعم | نعم | لا |
| إلحاق محتوى رسالة | نعم | نعم | لا |
الخلاصة العملية: يمكنك تبديل tool_choice لكل طلب، أو تفعيل التفكير وتعطيله، دون فقدان ذاكرة الأدوات والنظام. تغييرات تعريف الأدوات وتبديل النموذج وحدها تفرض إعادة بناء كاملة.
صفّان من هذه الجدول لهما مخرج يستحق المعرفة:
- تعديلات مطالبة النظام. على Claude Opus 5 وClaude Opus 4.8 وClaude Fable 5 وClaude Mythos 5، يمكنك إلحاق رسالة
{ role: "system", content: "..." }بمصفوفةmessagesبدلًا من تعديل حقلsystemفي المستوى الأعلى. تقع بعد السجل المخزَّن مؤقتًا، فتصمد البادئة. لا حاجة لترويسة بيتا. وهي أيضًا الطريقة الآمنة ضد الحقن لتوصيل تعليمات المشغّل، لأن النص داخل دور المستخدم يمكن تزويره بواسطة أي شيء يكتب في مدخلات المستخدم. لاحظ أن هذا غير مدعوم على Claude Sonnet 5 — يُرجع الخطأ 400. - تغييرات مجموعة الأدوات. ابتداءً من Claude Opus 5، خلف ترويسة بيتا
mid-conversation-tool-changes-2026-07-01، يمكنك إضافة أدوات وحذفها بين الأدوار باستخدام كتلtool_additionوtool_removalدون إبطال الذاكرة المؤقتة.
تبديل النموذج ليس له مخرج — الذواكر المؤقتة مرتبطة بالنموذج. إذا أردت نموذجًا أرخص لمهمة فرعية، أطلق استدعاءً منفصلًا لها وأبقِ حلقتك الرئيسية على نموذج واحد.
الخطوة 9: التخزين المؤقت للمحادثات متعددة الأدوار
في واجهة محادثة، يتراكم المكسب. ضع نقطة التوقف على آخر كتلة محتوى في الدور المضاف حديثًا، وسيُعيد كل طلب استخدام المحادثة السابقة بكاملها.
أنشئ src/conversation.ts:
import Anthropic from "@anthropic-ai/sdk";
import { client, MODEL } from "./client";
import { logCacheUsage } from "./cache-usage";
const SYSTEM_PROMPT = "You are a concise technical assistant.";
export class CachedConversation {
private messages: Anthropic.MessageParam[] = [];
async send(userText: string): Promise<string> {
this.messages.push({
role: "user",
content: [
{
type: "text",
text: userText,
// نقطة توقف على أحدث دور: المحادثة السابقة
// كلها تصبح البادئة المخزَّنة القابلة لإعادة الاستخدام.
cache_control: { type: "ephemeral" },
},
],
});
const response = await client.messages.create({
model: MODEL,
max_tokens: 16000,
system: SYSTEM_PROMPT,
messages: this.messages,
});
// ألحق مصفوفة المحتوى كاملة، وليس النص فقط —
// إسقاط الكتل يكسر استخدام الأدوات واستمرارية التفكير.
this.messages.push({ role: "assistant", content: response.content });
logCacheUsage(`turn-${this.messages.length / 2}`, response.usage);
return response.content
.filter((b): b is Anthropic.TextBlock => b.type === "text")
.map((b) => b.text)
.join("");
}
}شغّل بضعة أدوار وراقب read وهي ترتفع بينما تبقى fresh ضئيلة. نقاط التوقف الأقدم تبقى نقاط قراءة صالحة، لذا تتراكم الإصابات تدريجيًا مع نمو المحادثة بدلًا من إعادة الضبط في كل دور.
فخ نافذة الـ 20 كتلة
كل نقطة توقف ترجع للخلف بحد أقصى 20 كتلة محتوى بحثًا عن مدخلة ذاكرة مؤقتة سابقة. في حلقة وكيل، يمكن لدور واحد أن يضيف أكثر من 20 كتلة بسهولة — كل زوج tool_use وtool_result يُحسب.
عندما يحدث ذلك، لا تستطيع نقطة توقف الطلب التالي رؤية الذاكرة السابقة وتُخفق بصمت. لا يوجد خطأ؛ معدل الإصابة لديك ينهار فقط في الأدوار الطويلة كثيفة الأدوات.
الإصلاح هو وضع نقطة توقف وسيطة كل 15 كتلة تقريبًا في الأدوار الطويلة، مع إبقاء كل علامة ضمن 20 كتلة من آخر كتلة مخزَّنة. هذا بالضبط نوع الإخفاق غير المرئي بدون أداة logCacheUsage من الخطوة 6.
الخطوة 10: مسار API مخزَّن مؤقتًا في Next.js
لنجمع كل شيء في نقطة نهاية حقيقية. أنشئ app/api/ask/route.ts:
import Anthropic from "@anthropic-ai/sdk";
import { NextResponse } from "next/server";
import { getHandbook } from "@/lib/handbook";
const client = new Anthropic();
export async function POST(request: Request) {
const { question } = await request.json();
if (typeof question !== "string" || question.trim().length === 0) {
return NextResponse.json(
{ error: "A non-empty question is required." },
{ status: 400 },
);
}
// يُحمَّل مرة واحدة في نطاق الوحدة داخل getHandbook — متطابق
// على مستوى البايت عبر الطلبات، وهذا ما يُحقق إصابة الذاكرة.
const handbook = await getHandbook();
try {
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 4096,
system: [
{
type: "text",
text: `You are a support assistant. Answer only from the handbook below.\n\n<handbook>\n${handbook}\n</handbook>`,
cache_control: { type: "ephemeral", ttl: "1h" },
},
],
messages: [{ role: "user", content: question }],
});
const answer = response.content
.filter((b): b is Anthropic.TextBlock => b.type === "text")
.map((b) => b.text)
.join("");
return NextResponse.json({
answer,
usage: {
cached: response.usage.cache_read_input_tokens ?? 0,
fresh: response.usage.input_tokens,
},
});
} catch (error) {
if (error instanceof Anthropic.RateLimitError) {
return NextResponse.json(
{ error: "Rate limited. Retry shortly." },
{ status: 429 },
);
}
if (error instanceof Anthropic.APIError) {
return NextResponse.json(
{ error: `Upstream error: ${error.message}` },
{ status: 502 },
);
}
throw error;
}
}تفصيلان يحملان وزنًا حقيقيًا هنا.
معالجة الأخطاء المُنمّطة. Anthropic.RateLimitError وأخواتها أصناف مُصدَّرة بحقل status مُنمّط. افحص من الأخص إلى الأعم، ولا تطابق نصوص رسائل الأخطاء أبدًا — فهي تتغير دون إشعار.
مدة الصلاحية ساعة واحدة. لاحظ ttl: "1h" في cache_control. هذا الاختيار يستحق قسمًا خاصًا به.
الخطوة 11: اختيار مدة الصلاحية، بصراحة
الاقتصاديات بسيطة بما يكفي للتفكير فيها مباشرة:
| العملية | التكلفة نسبةً لسعر الإدخال الأساسي |
|---|---|
| قراءة من الذاكرة المؤقتة | ~0.1× |
| كتابة، صلاحية 5 دقائق | 1.25× |
| كتابة، صلاحية ساعة | 2× |
نقطة التعادل تنبثق من هذه الأرقام:
- صلاحية 5 دقائق: طلبان يحققان التعادل. كتابة واحدة وقراءة واحدة تساوي 1.35× مقابل 2× بدون تخزين.
- صلاحية ساعة: تحتاج ثلاثة طلبات على الأقل. علاوة كتابة بـ 2× مع قراءتين تساوي 2.2× مقابل 3× بدون تخزين.
إذن صلاحية الساعة ليست أفضل بإطلاق. تُبقي المدخلات حية عبر فجوات الحركة، وهو ما يهم أحمال العمل المتقطعة، لكن مضاعفة تكلفة الكتابة تعني أنها تحتاج قراءات أكثر لتُغطي نفسها. لنقطة نهاية ذات حركة مستقرة تصلها الطلبات أكثر من مرة كل خمس دقائق، تكون صلاحية الخمس دقائق الافتراضية عادةً الخيار الأرخص — الحركة الحقيقية تُبقي الذاكرة دافئة بنفسها.
الخطوة 12: سلوكان زمنيان يفاجئان الناس
الطلبات المتزامنة كلها تُخفق
تصبح مدخلة الذاكرة المؤقتة قابلة للقراءة فقط بعد أن يبدأ أول استجابة بالتدفق. أطلق عشرة طلبات متوازية ببادئة متطابقة وستدفع العشرة السعر الكامل — لا أحد يستطيع قراءة ما يكتبه الآخرون بعد.
لأنماط التوزيع، رتّب الطلب الأول أولًا:
// أرسل طلبًا واحدًا، انتظر أول توكن متدفق، ثم وزّع الباقي.
const first = client.messages.stream({ /* ...البادئة المشتركة... */ });
for await (const _event of first) break; // انتظر فتح التدفق
const rest = await Promise.all(
remainingInputs.map((input) => client.messages.create({ /* ... */ })),
);أنت تنتظر التوكن الأول، لا الاستجابة الكاملة. الذاكرة المؤقتة تصبح حية من تلك اللحظة.
التسخين المسبق بـ max_tokens: 0
لإزالة زمن البدء البارد من أول طلب حقيقي، أرسل طلبًا بمخرجات صفرية عند الإقلاع. تُنفّذ الواجهة البرمجية مرحلة التعبئة المسبقة، وتكتب الذاكرة المؤقتة، وتعود فورًا بمحتوى فارغ:
await client.messages.create({
model: MODEL,
max_tokens: 0,
system: [
{
type: "text",
text: SYSTEM_PROMPT,
cache_control: { type: "ephemeral" },
},
],
messages: [{ role: "user", content: "warmup" }],
});تُحاسَب على رسوم الكتابة العادية وصفر توكنات إخراج. تعود الاستجابة بـ content: [] وstop_reason: "max_tokens".
كن مقصودًا في تحديد متى يستحق هذا العناء. التسخين المسبق يقايض رسم كتابة الآن مقابل زمن أقل لأول توكن لاحقًا. يؤتي ثماره عندما تتحقق الشروط الثلاثة: زمن الطلب الأول مرئي للمستخدم، والبادئة المشتركة كبيرة، وهناك لحظة هدوء قبل الحركة — إقلاع التطبيق، أو تشغيل عامل، أو ما بعد النشر.
تجاوزه عندما تكون الحركة مستمرة (الطلبات الحقيقية تُبقي الذاكرة دافئة مجانًا)، أو عندما تختلف البادئة لكل مستخدم (لا شيء مشترك لتسخينه)، أو عندما ستُسخّن تخمينًا بادئات كثيرة متمايزة (كل واحدة كتابة قد لا تقرأها أبدًا).
ضع نقطة التوقف على آخر كتلة مشتركة مع الطلب الحقيقي — مطالبة النظام أو تعريفات الأدوات — لا على رسالة المستخدم النائبة، ولا تستخدم التخزين التلقائي على المستوى الأعلى هنا، لأنه سيربط الذاكرة بالنائب المؤقت.
max_tokens: 0 مرفوض مع stream: true، أو التفكير المُفعّل، أو output_config.format، أو tool_choice مفروض، أو واجهة الدُفعات.
اختبار تطبيقك
التحقق مباشر لأن الواجهة البرمجية تقول لك الحقيقة:
- شغّل نفس الطلب مرتين. يجب أن يُظهر الثاني قيمة
cache_read_input_tokensغير صفرية. إن لم يفعل، فلديك مُبطِل. - افحص المجموع.
input_tokens + cache_creation_input_tokens + cache_read_input_tokensيجب أن يساوي تقريبًا حجم مطالبتك الكامل. إذا كان المجموع أقل بكثير مما تتوقع، فأنت لا تُرسل ما تظن أنك تُرسله. - قارن المطالبة المُصيَّرة. حين ترفض الإصابة الظهور، سلسِل جسم الطلب الكامل في استدعاءين متتاليين وقارن البايتات. الطابع الزمني المخالف أو المفتاح المُعاد ترتيبه سيكون واضحًا.
- راقب معدل الإصابة عبر جلسة حقيقية. محادثة تبدأ من 0% وتتجاوز 90% بحلول الدور الخامس تتصرف بشكل صحيح. أما التي تتذبذب فهي تصطدم بحد نافذة الـ 20 كتلة.
استكشاف الأخطاء
cache_read_input_tokens دائمًا 0.
البادئة تختلف بين الطلبات. راجع جدول الخطوة 7. عمليًا هو طابع زمني أو UUID أو معرّف لكل مستخدم داخل مطالبة النظام في نحو 80% من الحالات.
cache_creation_input_tokens صفر أيضًا، ولا يظهر أي خطأ.
بادئتك دون الحد الأدنى القابل للتخزين في النموذج. راجع جدول الخطوة 2 — يتراوح الحد الأدنى من 512 توكن على Claude Opus 5 حتى 4096 على Opus 4.6 وHaiku 4.5.
التخزين المؤقت رفع فاتورتي. أنت تكتب دون أن تقرأ. هذا خطأ البادئة المشتركة مع اللاحقة المتغيرة من الخطوة 5 — نقطة التوقف بعد المحتوى المتغير، فكل طلب يكتب مدخلة جديدة. انقل العلامة إلى نهاية الجزء المشترك.
معدل الإصابة ينهار في أدوار الوكلاء الطويلة. نافذة الرجوع البالغة 20 كتلة. أضف نقاط توقف وسيطة كل 15 كتلة تقريبًا.
الإصابات تعمل محليًا وتفشل في الإنتاج. عادةً التزامن. الطلبات المتوازية لا تستطيع قراءة ذاكرة ما تزال قيد الكتابة. رتّب الطلب الأول، أو اقبل الإخفاق عند البدء البارد.
الإصابات توقفت بعد النشر. تحقق مما إذا كان نص النموذج أو مجموعة الأدوات أو مطالبة النظام قد تغيّر. الثلاثة تُبطل. الذواكر مرتبطة بالنموذج أيضًا، لذا ترقية النموذج تبدأ باردة دائمًا.
الخطوات التالية
- أضف عدّ التوكنات قبل الطلبات.
client.messages.countTokens()يمنحك أعدادًا دقيقة وخاصة بالنموذج. لا تستخدمtiktoken— فهو مُجزّئ OpenAI ويُقدّر توكنات Claude بأقل من قيمتها بنسبة 15–20% في النص العادي، وبفارق أكبر بكثير في الشيفرة. - اجمعه مع ضبط
effort.output_config: { effort: "low" | "medium" | "high" | "xhigh" | "max" }يتحكم في عمق التفكير وإجمالي إنفاق التوكنات. التخزين المؤقت يخفض تكلفة الإدخال؛ والجهد يخفض تكلفة الإخراج. إنهما رافعتان مستقلتان، ومعظم الفرق تسحب واحدة فقط. - قِس في الإنتاج. أرسل مخرجات أداة الخطوة 6 إلى منصة المراقبة لديك. رسم بياني لمعدل إصابة الذاكرة يلتقط انحدارًا في تجميع المطالبات في نفس يوم حدوثه، بدلًا من اكتشافه في الفاتورة.
- استكشف دروسًا ذات صلة على noqta.tn: دليل Claude Agent SDK بـ TypeScript لحلقات الوكلاء التي تستفيد بشدة من التخزين المؤقت، ومراقبة النماذج اللغوية مع Langfuse لتتبع إنفاق التوكنات من طرف إلى طرف، وتوجيه Vercel AI Gateway لإعدادات متعددة المزوّدين.
الخاتمة
التخزين المؤقت للمطالبات من التحسينات النادرة التي تكون كبيرة الأثر ورخيصة التبنّي في آن واحد. نقطة توقف موضوعة بشكل صحيح على بادئة مستقرة تستغرق نحو عشر دقائق لتطبيقها وتخفض تكلفة إدخال الجزء المشترك بنحو 90%.
المأخذ أن عبارة "موضوعة بشكل صحيح" تحمل الوزن كله. الآلية هي مطابقة بادئة دقيقة على مستوى البايت، لذا فإن طابعًا زمنيًا واحدًا مُدرجًا، أو تسلسل JSON غير مرتّب، أو نقطة توقف موضوعة بعد كتلة واحدة زيادة، يحوّل توفيرًا بنسبة 90% إلى رسم إضافي بنسبة 25% — بصمت، بلا خطأ وبلا تحذير.
ولهذا فإن القياس في الخطوة 6 ليس إضافة اختيارية. سجّل cache_read_input_tokens في كل طلب، ونبّه عند انخفاضه، وتعامل مع معدل الإصابة الهابط باعتباره الانحدار الذي هو عليه. الفرق التي تجني قيمة حقيقية من التخزين المؤقت ليست تلك التي فعّلته — بل تلك التي تستطيع أن تُثبت، اليوم، أنه ما زال يعمل.