قبل بضعة أسابيع، بدأ agent البحث الخاص بي يكلّف أربعة أضعاف ما كان يكلفه الأسبوع السابق. نفس الميزة، نفس الحركة، نفس النموذج. بدا الناتج على ما يرام. الفاتورة لم تكن كذلك.
أمضيت بعد الظهر في إضافة تعليمات print قبل أن أعترف بالواضح: لم يكن لديّ أي فكرة عما كان agent يفعله فعلًا في أي تشغيل معطى. كان يُجري عددًا متفاوتًا من استدعاءات الأدوات، ويعيد المحاولة بصمت عند الإخفاقات التي لم أسجّلها قط، وكان يقع أحيانًا في حلقة يُعيد فيها قراءة المستند نفسه ثلاث مرات قبل الاستسلام. لم يظهر أي من ذلك في أي مكان سوى الفاتورة.
تبيّن أن المذنب كان مسار إعادة المحاولة. كانت أداة غير مستقرة تنتهي مهلتها، وكان agent يعيد المحاولة بصمت مع إرسال السياق الكامل في كل مرة، وكان تشغيل "ناجح" واحد يُجري تسعة استدعاءات للنموذج بدلًا من اثنين. وجدته فقط لأنني كتبت أخيرًا ما يفعله كل خطوة. كلّفني ذلك الظهر أكثر من شهر كامل من أي أداة تتبع.
الخلاصة: agent هو حلقة لا تتحكم فيها تمامًا، ولا يمكنك تحسين حلقة لا تستطيع مراقبتها. تحتاج إلى التقاط، لكل تشغيل، ما حدث في كل خطوة، وكم token أحرق، وكم تكلّف، وكم استغرق، وما إذا كان قد فشل. لا تحتاج إلى منصة للبدء. تحتاج إلى سجل.
ما الذي تلتقطه فعلًا
انسَ أدوات العمل للحظة. السؤال هو: عندما يسوء تشغيل ما، ماذا ستريد أن تعرف؟ اعمل بشكل عكسي من هناك وستحصل على القائمة.
لكل تشغيل agent، أريد trace واحدة — التشغيل بأكمله — مكوّنة من spans، واحد لكل خطوة. يجب أن يحمل كل span:
- نوع الخطوة — استدعاء نموذج، أو استدعاء أداة، أو استرجاع، أو استدعاء agent من المستوى الأعلى.
- استخدام الـ Tokens — tokens الإدخال وtokens الإخراج بشكل منفصل. تكلّفان مبالغ مختلفة وتخبرانك بأشياء مختلفة.
- التكلفة — مستمدة من الـ Tokens وسعر النموذج. احسبها مرة واحدة، وخزّنها في الـ span.
- زمن الاستجابة — وقت البداية والنهاية. التشغيلات البطيئة والتشغيلات المكلفة عادةً ما تكون مشكلات مختلفة.
- استدعاءات الأدوات — أي أداة، وما هي المعطيات، وما أعادته (أو كيف أخفقت).
- النتيجة — هل نجحت الخطوة، أم أخفقت، أم أعادت المحاولة، ولماذا.
- درجة Eval — إذا كنت تُقيّم التشغيل (يجب أن تفعل ذلك)، أرفق الدرجة حتى تتمكن من ربط الجودة بالتكلفة.
هذه النقطة الأخيرة هي التي يتخطاها الناس، وهي التي تُعطي أكبر عائد. Trace يخبرك بما حدث؛ درجة eval تخبرك بما إذا كان ذلك مهمًا. ربطهما يتيح لك طرح السؤال الوحيد الذي يهم: هل النسخة الأغلى أفضل فعلًا؟ في أغلب الأحيان، لا.
لاحظ ما ليس في القائمة: النص الكامل للـ prompt والرد في كل span بشكل افتراضي. التقاط محتوى الرسائل مفيد لتصحيح الأخطاء ومحفوف بمخاطر الخصوصية وتكلفة التخزين. أقوم بالتقاطه خلف علامة يمكنني تشغيلها لتشغيل محدد، وليس دائمًا نشطة لكل شيء.
اتفاقيات GenAI الخاصة بـ OpenTelemetry كخط أساس
قبل أن تخترع أسماء الحقول الخاصة بك، انظر إلى ما هو موجود بالفعل. OpenTelemetry — نفس المعيار المحايد من البائع الذي تستخدمه لأي تتبع خلفي — يمتلك مجموعة من اتفاقيات دلالات GenAI: مفردات متفق عليها لتسمية الأشياء التي ذكرتها للتو.
الجزء المفيد هو أسماء السمات. يحصل span استدعاء النموذج على gen_ai.request.model (أي نموذج)، وgen_ai.usage.input_tokens وgen_ai.usage.output_tokens (توزيع الـ Tokens)، وgen_ai.response.finish_reasons (لماذا توقف — stop، tool_calls، وما إلى ذلك)، وgen_ai.provider.name لتمييز المزوّد. هناك حتى سمات لمحاسبة ذاكرة التخزين المؤقت للـ prompt — gen_ai.usage.cache_read.input_tokens وgen_ai.usage.cache_creation.input_tokens — التي تهم كثيرًا بمجرد تفعيل التخزين المؤقت، لأن الإدخال المخزّن مؤقتًا يمكن أن يكون أرخص بمرتبة كاملة. تتبع أسماء الـ spans تنسيق {gen_ai.operation.name} {gen_ai.request.model}، وتحدد اتفاقيات مستوى agent عمليات مثل invoke_agent (التشغيل الكامل)، وchat (استدعاء نموذج واحد)، وexecute_tool (استدعاء أداة). (OTel GenAI spans, OTel GenAI observability blog)
تحذيران صادقان. أولًا، اعتبارًا من منتصف عام 2026، لا تزال هذه الاتفاقيات محددة بحالة Development — إنها تستقر لكنها لم تُجمَّد، لذا لا تزال أسماء السمات قد تتغير. انتقلت مؤخرًا إلى مستودعها الخاص، وهو ما يدل على أنها تُؤخذ بجدية وأنها لا تزال في حالة تطور. ثانيًا، لا يتعين عليك اعتماد آلية OTel للاستفادة. حتى لو كتبت سجل JSON بسيطًا، استخدم أسماء الحقول هذه. في اليوم الذي تتجاوز فيه سجلك المصنوع منزليًا وتنتقل إلى خلفية حقيقية، تتحدث بياناتك اللغة بالفعل. هذا هو أرخص توافق مستقبلي ستشتريه على الإطلاق.
سجل JSONL المصنوع منزليًا الذي يحل 80% من المشكلة
إليك الإعداد الذي أستخدمه فعلًا للمشاريع الصغيرة. ملف JSONL للإلحاق فقط، كائن واحد لكل span، أسماء حقول بأسلوب OTel. لا خدمة، لا SDK، لا حساب.
import json, time, uuid
PRICES = { # USD per token; see your provider's pricing page
"claude-haiku-4.5": {"in": 1.00/1e6, "out": 5.00/1e6},
"claude-sonnet-4.6": {"in": 3.00/1e6, "out": 15.00/1e6},
}
def log_span(trace_id, kind, model, usage, t0, status, **extra):
p = PRICES.get(model, {"in": 0, "out": 0})
cost = usage["in"] * p["in"] + usage["out"] * p["out"]
record = {
"trace_id": trace_id,
"span_id": uuid.uuid4().hex[:12],
"gen_ai.operation.name": kind, # invoke_agent | chat | execute_tool
"gen_ai.request.model": model,
"gen_ai.usage.input_tokens": usage["in"],
"gen_ai.usage.output_tokens": usage["out"],
"cost_usd": round(cost, 6),
"latency_ms": int((time.time() - t0) * 1000),
"status": status, # ok | error | retry
**extra, # feature, tool_name, eval_score...
}
with open("traces.jsonl", "a") as f:
f.write(json.dumps(record) + "\n")
كل خطوة في حلقة agent الخاصة بك تستدعي log_span مع trace_id مشترك. قم بتوسيم كل تشغيل بالميزة التي خدمها (feature="research", feature="summarize"). يمكنك الآن الإجابة على أسئلة حقيقية بسطر واحد على الملف:
# cost per feature, last run-set
jq -s 'group_by(.feature)[] | {feature: .[0].feature,
usd: (map(.cost_usd) | add)}' traces.jsonl
هذا كل شيء. التكلفة لكل تشغيل، والإسناد لكل ميزة، وزمن الاستجابة، وعدد إعادات المحاولة، و— إذا كتبت درجة eval في الـ span — الجودة مقابل التكلفة، كل ذلك من ملف يمكنك قراءته باستخدام jq أو grep أو DuckDB. كانت حلقة إعادة المحاولة التي ضاعفت فاتورتي أربع مرات ستظهر هنا كصفوف status: "retry" مكدّسة تحت trace_id واحد، ثلاث دقائق من النظر بدلًا من بعد ظهر من التخمين.
جدول الأسعار هو الشيء الوحيد الذي يجب أن تحافظ على صحته. تكلّف tokens الإخراج حاليًا حوالي 5 أضعاف tokens الإدخال عبر مستويات Claude الحالية، وكانت الأسعار الرئيسية التي استخدمتها أعلاه هي $1/$5 لكل مليون لـ Haiku 4.5 و$3/$15 لـ Sonnet 4.6 عند التحقق — لكن الأسعار تتغير وخصومات التخزين المؤقت والدُفعات تحرّك الرقم الفعلي كثيرًا، لذا عامل أي جدول كلقطة وتحقق من صفحة مزوّدك. (Claude API pricing breakdown)
إسناد تكلفة الـ Tokens: السؤال الذي يعود بالنفع فعلًا
هنا يُظهر الانضباط قيمته. بمجرد أن يحمل كل span تكلفة وعلامة ميزة، يمكنك التوقف عن التخمين بشأن اقتصادياتك الوحدوية.
شغّلت هذا على مشروعي الخاص واكتشفت أن ميزة "مجانية" — ملخص تلقائي يُشغَّل عند كل رفع مستند — كانت تمثّل 60% من إنفاقي على النماذج، ويستخدمها ربما عُشر مستخدميّ. لم يكن ذلك خللًا. كان قرارًا للمنتج اتخذته بشكل أعمى. إن رؤية التكلفة لكل ميزة حوّله إلى قرار يمكنني اتخاذه بوعي: جعلته اختياريًا، فانخفضت الفاتورة إلى النصف دون أي شكاوى.
هذا هو الهدف الكامل من الإسناد. إجمالي الإنفاق رقم لا يمكنك التصرف بناءً عليه. "الميزة X تكلّف $0.40 لكل تشغيل والميزة Y تكلّف $0.02" رقم يمكنك ذلك. وعندما تجمعه مع درجات eval، تحصل على النسخة الأحدّ: التكلفة لكل تشغيل جيد. إذا حصل المسار المكلف على نفس درجة المسار الرخيص، فإن المسار المكلف ليس سوى تسريب ذو وصف جميل.
البناء مقابل الشراء: متى لا يكفي سجل JSONL
السجل المصنوع منزليًا كافٍ فعلًا لفترة طويلة. يتوقف عن الكفاية عندما تريد أشياء لا يمكن للملف توفيرها بتكلفة منخفضة: واجهة مستخدم للنقر عبر trace واحد فوضوي، وأدوات قياس تلقائية حتى لا تضطر إلى استدعاء log_span يدويًا في كل مكان، وتشغيلات eval مرتبطة بـ CI، أو مشاركة traces مع شخص لا يعيش في طرفية.
عند تلك النقطة، تستحق الأدوات المُستضافة النظر، والملخص الصادق هو أنها تتمركز حول ما تُجيده، لا حول أيها "الأفضل":
- Langfuse يلتقط الـ prompt والرد واستخدام الـ Tokens والكمون والتكلفة لكل trace، ومرخّص بـ MIT وقابل للاستضافة الذاتية — الخطوة التالية الطبيعية من ملف JSONL إذا كنت تريد واجهة مستخدم مع الاحتفاظ ببياناتك. (استُحوذ عليه من قِبَل ClickHouse في أوائل عام 2026، وهو أمر يستحق المعرفة إذا كانت استقرارية البائع تهمك.)
- Arize Phoenix أصيل في OpenTelemetry — مبني على أدوات قياس OTel وOpenInference — ويعمل محليًا عبر Docker، مع مكتبة مفتوحة المصدر قوية لمقاييس eval. إذا اشتريت اتفاقيات OTel أعلاه، فهذا هو الخلفي الأقل احتكاكًا.
- LangSmith هو الأعمق إذا كنت تعمل بالفعل على LangChain/LangGraph، وأقل كذلك إذا لم تكن.
- Braintrust يميل نحو الـ eval أولًا، مع بوابات CI يمكنها منع دمج عند تراجع الجودة — مفيد عندما تكون انضباطية الشحن أهم من لوحات المعلومات.
لن أتوّج أيًا منها. القاعدة التي أستخدمها للقرار: إذا أردت لوحة معلومات، أنظر إلى Langfuse أو Phoenix؛ إذا أردت بوابات eval في CI، أنظر إلى Braintrust؛ إذا كنت عميقًا في LangChain، فـ LangSmith. وإذا لم أُرد أيًا منهما بعد، أبقى على ملف JSONL، لأن الملف يجيب بالفعل على الأسئلة التي لديّ اليوم. تجدر الإشارة أيضًا إلى أن agents الترميز الرئيسية — Claude Code وCodex وCopilot — تُصدّر الآن بيانات قياس OTel بنفسها، لذا بعض هذا تحصل عليه دون كتابة أي كود ربط.
متى يكون هذا مبالغًا فيه
دعني أنا من يقول ذلك: بالنسبة لنموذج أولي جديد تمامًا مع ثلاثة مستخدمين وميزة واحدة، التتبع الكامل هو مماطلة متنكّرة في هيئة صرامة. إذا كان بإمكانك قراءة كل تشغيل بعينيك وكانت فاتورتك الشهرية خطأ تقريبًا، فسجّل عدد الـ Tokens والتكلفة، تخطَّ الباقي، واذهب لبناء الشيء.
الحد الصادق هو إشارتان: عندما تفاجئك فاتورة، أو عندما يختبئ منك إخفاق. في المرة الأولى التي لا تستطيع فيها شرح ارتفاع التكلفة، أو المرة الأولى التي يفشل فيها agent بطريقة لا تستطيع إعادة إنتاجها، تكون قد تجاوزت الحد — والحل هو بعد ظهر لإضافة log_span، وليس أسبوع لإنشاء منصة.
ابدأ بالملف. استخدم أسماء حقول OTel حتى يكون الاختراق اليوم مسار الهجرة في الغد. قم بتوسيم كل span بميزة وتكلفة. اشترِ أداة في اليوم الذي يتوقف فيه الملف عن الإجابة على أسئلتك — ليس قبل ذلك. لم يكن الهدف في أي يوم هو المراقبة لذاتها. كان القدرة على النظر إلى تشغيل جانحٍ ومعرفة، في ثلاث دقائق، بالضبط أين ذهب المال.
