Phase 17: Infrastructure & Production

نموذج واجهات التجربة من وظيفة إلى تطبيق قابل للمشاركة

نموذج التجربة لديه عقد واحد: مدخل، دعوة نموذج، ومخرج. ضع حدًا على كل جزء قبل مشاركة الرابط.

Type: Build

Languages: Python, TypeScript

Prerequisites: Phase 11 · 13 (Building a Production LLM Application), Phase 17 · 08 (Inference Metrics), Phase 17 · 19 (AI Gateways)

Time: ~90 minutes

أهداف التعلم

  • اكتب الدخول، النموذج الدعوة، والعقد الخروج من عرض التجربة، مع حد لكل جزء.
  • قم ببناء تطبيق HTTP المكتبة القياسي مع صفحة HTML واحدة ، نقطة نهاية JSON ، ونقطة نهاية Server-Sent Events.
  • تأكيد المدخلات حسب التكلفة، وعيد أخطاء JSON التي تخبر المستخدم بالعمل التالي.
  • حماية عرض عرض عام مع علبة رموز لكل عميل، حد استخدام يومي، ورمز الوصول الاختياري.
  • اكتب سجلات الطلبات التي لا تحتفظ بنص إدخال ولا عنوان عميل.
  • مقارنة عرض المكتبة القياسي مع إصدارين إطارية، وتسمية الإشارات التي يجب أن يصبح عرض عرض منتج.

المشكلة

نموذجك يعمل في دفتر ملاحظات. الآن المراجعة تريد أن تجرب عشرة مدخلات، والعميل يريد أن يرى نتيجة واحدة على الهاتف. لا أحد منهم سوف تفتح دفتر ملاحظاتك.

خلية دفتر الملاحظات لا توجد حدود إدخال ولا توجد عرض تقدم. أخطائها هي آثار التعبئة التي يمكنك قراءتها فقط. كما أنها تعمل مع إشعارك وال ميزانيتك. إذا وضعت الخلية وراء عنوان URL عام دون تغييرات، يمكنك توقع أخطاء مثل هذه:

  • يضم المستخدم وثيقة من 40 ألف حرف، وتجري المكالمة النموذجية لمدة دقيقة ثم تفشل مع تعقب كومة.
  • نص واحد يرسل 500 طلب في دقيقة واحدة كل طلب هو مكالمة نموذجية مدفوعة
  • الصفحة لا تظهر شيئا لمدة ثمانية ثوان، لذلك المستخدم يضغط على الزر أربع مرات أخرى.
  • سجل الخادم يحفظ كل إدخال، بما في ذلك الإدخال مع رقم حساب العميل.

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

المفهوم

الاكتشاف هو عقد

كل عرض عرض يحتوي على ثلاثة أجزاء. المدخل هو ما يمكن للمستخدم إرساله. استدعاء النموذج يحول المدخل إلى خروج. الخروج هو ما يراه المستخدم، وهذا يشمل الأخطاء.

اكتب العقد قبل كتابة الصفحة:

PartQuestion to answerAnswer in this lesson
InputWhat type, what size, which characters?A string of 1 to 500 characters, with no control characters
Model callHow long does one call take, and what does it cost?A toy keyword model that runs in under a millisecond at no cost
OutputWhat does the user see on success and on failure?A label, a score, and a reply, or a JSON error with a hint

لا يوجد حد للكتب الملاحظة لأي من الأجزاء الثلاثة. تحدد المشاهدة حدًا لكل جزء ، ويتم تطبيق الخادم الحدود على كل عميل.

flowchart LR
  Page[HTML form] --> Size[Body size check]
  Size --> Token[Access token check]
  Token --> Bucket[Token bucket per client]
  Bucket --> Valid[Input validation]
  Valid --> Cap[Daily usage cap]
  Cap --> Model[Model call]
  Model --> Reply[JSON reply or token events]
  Size -.->|413| Err[JSON error with a hint]
  Token -.->|401| Err
  Bucket -.->|429| Err
  Valid -.->|422| Err
  Cap -.->|503| Err

التحققات تعمل في ترتيب التكلفة. طلب سيء يتوقف عند التحقق الأول أنه يفشل، لذلك يكلف الخادم تقريبا أي شيء. النموذج المكالمة تأتي الأخيرة، لأنه الخطوة الوحيدة التي تكلف المال.

التحقق من الصلاحية قبل تشغيل النموذج

يمكن للمتنشر التحقق من المدخل قبل إرسال أي شيء.maxlengthوrequiredتعطي الميزات على منطقة نصية للمستخدم تعليقات دون اتصال شبكي. هذه التحققات تساعد المستخدمين الصادقين فقط. أي عميل يمكن إرسال طلب دون الصفحة، لذلك يقوم الخادم بتكرار كل التحقق.

ضع الشيكات على الخادم حسب التكلفة:

  1. اقرأContent-Lengthرفض الجثة فوق الحد قبل قراءتها
  2. تحليل جسم JSON. رفض جسم ليس كائن JSON.
  3. تحقق من الحقول، ورفض النص الفارغ، النص فوق الحد، وترتيب الأحرف.
  4. إضافة مكالمة نموذجية فقط على طلب تمكنت من إجراء كل شيك
Failed checkHTTP statusError code
No Content-Length header411 Length Requiredlength_required
Content-Length is not a number400 Bad Requestbad_length
Body over 4,096 bytes413 Content Too Largebody_too_large
Body stops before Content-Length bytes arrive408 Request Timeoutrequest_timeout
Content type is not JSON415 Unsupported Media Typeunsupported_media_type
Body is not a JSON object400 Bad Requestbad_json
Text is missing, empty, too long, or has control characters422 Unprocessable Contentmissing_text, empty_text, text_too_long, control_characters

RFC 9110 تعريف هذه الرموز الحالة وأسمائها.

المثال هو جزء من تصميم المدخلات. يظهر مثال للمستخدم كيف يبدو مدخل جيد. كما يعطي المراجع اختبارًا بنقرة واحدة. اختر مثالًا واحدًا لكل فئة خروج. هذا التجربة لديها مثال إيجابي وسلبي ومثال محايد.

أظهر أن النموذج يعمل

يمكن أن تستغرق مكالمة النموذج عدة ثوان. بدون تعليقات، لا يمكن للمستخدم معرفة النموذج البطيء من صفحة مكسورة. يمكن للمستخدم الذي لا يرى أي تغيير انقر مرة أخرى، ثم يحصل الخادم على طلب مدفوع ثان.

الصفحة في هذا الدروس تعطى أربعة أنواع من المراجعة:

  • إنه يعطى زر الإرسال أثناء تشغيل طلب.
  • يظهر الوقت المتأخر ويحديثه كل 100 ميل ثانية
  • يظهر الوقت لأول رمز عندما يصل الرمز الأول.
  • يظهر الوقت الإجمالي عندما ينتهي الطلب.

عندما كنتِ تدرسينinference metricsوقت إلى أول رمز (TTFT) كان مقياس الخادم. الصفحة التجريبية تظهر نفس الرقم للمستخدم، التي يتم قياسها في المتصفح.

إرسال الخروج كشريان

إن نموذج اللغة ينتج ردته رمزًا واحدًا في كل مرة. إذا انتظر الخادم آخر رمز ، ينتظر المستخدم الجيل الكامل. إذا أرسل الخادم كل رمز عندما يكون جاهزًا ، يبدأ المستخدم في قراءة بعد الرمز الأول.

الأحداث المرسلة من الخادم (SSE) هي النموذج القياسي لهذا.text/event-streamكل حدث هو مجموعة منevent:وdata:الخطوط، والخط الفارغ ينهي الحدث. معيار WHATWG HTML يحدد النموذج.

output
event: token
data: {"index":0,"text":"Label:"}

event: token
data: {"index":1,"text":" positive."}

event: done
data: {"label":"positive","score":1.0,"tokens":11}

المتصفح لديه EventSourceواجهة SSE ، ولكنها ترسل طلبات GET فقط. طلب GET يضع المدخل في عنوان URL ، وكتب خوادم الويب والوكسيات عناوين URL في سجلات الوصول الخاصة بهم. هذا التجربة يرسل طلب POST مع جسم JSON بدلاً من ذلك.fetch()وresponse.body.getReader()، ويقوم بتحليل شكل الحدث نفسه

يحتوي سلسلة على مشكلة واحدة لا يوجد فيها رد JSON واحد. سيرفر يرسل الحالة 200 قبل الرمز الأول. إذا فشل النموذج بعد ذلك، لا يمكن للخادم تغيير الحالة. لذلك سيرفر يرسل errorحدث في التيار، والصفحة تظهر هذا الحدث كخطأ.

عندما بنيتproduction LLM applicationهذا الدروس يكتب نفس البروتوكول يدويا، حتى تتمكن من رؤية كل سطر الذي يرسله الخادم.

اكتب الأخطاء التي يمكن للمستخدم العمل عليها

رسالة الخطأ لها وظائف اثنتين. يخبر المستخدم بما حدث، ويخبر المستخدم ما يجب القيام به التالي. لا يعمل تعقب كومة أي من وظائف الغرباء.

كل خطأ في هذا التجربة لديه نفس شكل JSON:

json
{
  "error": {
    "code": "text_too_long",
    "message": "The text has 812 characters. The limit is 500.",
    "hint": "Shorten the text to 500 characters or fewer.",
    "request_id": "a8f3c2b435e3"
  }
}
  • codeهو اسم مستقر، وترتيبات العميل و اختباراتها مقارنة معه
  • messageيظهر الحقيقة، مع الأرقام التي يحتاجها المستخدم.
  • hintيمنح المستخدم عمل واحد.
  • request_idيربط تقرير المستخدم بسجل تسجيل واحد.

يحدد RFC 9457 تنسيق خطأ قياسي لبرامج API HTTP ، يسمى تفاصيل المشكلة. يستخدم هذا التجربة شكلًا أصغر لنفس الغرض. استخدم RFC 9457 عندما تستدعي فرق أخرى API الخاص بك.

مشاركة عرض عرض عام بأمان

يغير رابط عام من يمكنه إرسال الطلبات. خطط لتحكم واحد لكل مخاطر:

RiskControl in this demoControl for a larger audience
One client sends too many requestsA token bucket per client, with 429 and Retry-AfterPer-user limits after sign-in
Strangers find the linkAn optional access token in the Authorization headerSingle sign-on with an identity provider
Total cost grows with no limitA daily usage cap, with 503 and Retry-AfterSpend limits and alerts at the model provider
Very large or unusual inputBody size limit, text length limit, control character checkContent moderation on input and output
Logs keep private inputRecords with the input length and a keyed hash of the clientA retention period and access control on the logs

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

output
tokens = min(capacity, tokens + refill_rate * seconds_since_last_request)
accept the request if tokens >= 1, then subtract 1

عندما يكون السد فارغ، يستجيب الخادم429 Too Many Requests. RFC 6585 تعريف الحالة 429 ، و يسمح للخادم إضافة Retry-Afterهذا التجربة دائما يضيف الرأس، مع عدد الثواني حتى الرمز التالي. الصفحة تقرأ الرأس وتعطيل الزر لهذا الوقت.

يستخدم التجربة عنوان IP العميل كمفتاح سطل. وراء وكيل عكسية، كل طلب يأتي من عنوان البروكسي، لذلك جميع المستخدمين يشتركون سطل واحد. في هذه الحالة، قراءة عنوان العميل من عنوان الذي يحدده وكيل الخاص بك. لا تثق في هذا العنوان من أي مصدر آخر، لأن العميل يمكن أن يضع أي قيمة فيه.

عندما كنتِ تدرسينAI gatewaysيستخدم الموقع الحد الأقصى لتحديد المبلغ المطلوب من خلال الموقع. يستخدم الموقع الحد الأقصى لتحديد المبلغ المطلوب من خلال الموقع. يستخدم الموقع الحد الأقصى لتحديد المبلغ.

السجلات تحتاج إلى نفس الرعاية مثل المدخلات.http.serverيكتب المدير عنوان IP العميل وعنوان الطلب إلى خطأ قياسي. إذا كانت المدخلات في عنوان URL ، فإن المدخلات تذهب إلى السجل. هذا التجربة يستبدل ذلك السجل. يكتب كل طلب سجلًا واحدًا مع الطريق ، والحالة ، وكود الخطأ ، والبطء ، وطول المدخل. يصبح عنوان العميل هشًا مفتاحًا ، بحيث يمكنك حساب الطلبات لكل عميل دون تخزين العنوان.

أخبر المستخدمين ما تحفظه. صفحة التجربة تقول أن الخادم يحفظ طول النص وليس النص نفسه أبدا.

عندما يصبح التجربة منتجاً

إشارات إشارة هذه تخبرك أن الإشارة الإعلانية هي الآن منتج:

  • الناس خارج فريقك يستخدمونها كل أسبوع.
  • شخص ما يطلب هدف التشغيل أو يبلغ عن انقطاع
  • يطلب المستخدمون تسجيل الدخول أو حفظ النتائج أو مشاهدة تاريخهم.
  • تكلفة النموذج الأسبوعي هي خط في الميزانية.
  • وتشمل المدخلات البيانات التي تغطي العقد أو القانون.
PropertyDemoProduct
UsersKnown people, short sessionsUnknown people, daily use
StateNoneAccounts, history, saved results
AvailabilityBest effort, one processA written target, replicas, an on-call owner
Cost controlOne daily usage capPer-user quotas and billing
ObservabilityOne log record per requestTraces, metrics, and alerts

عندما ترى هذه الإشارات، نقلها إلى كومة الإنتاج من دروس التطبيقات الإنتاج LLM. هذه الكومة تضيف المصادقة الحقيقية، النماذج الخلفية، التتبع، وتتبع التكلفة لكل مستخدم.

بناءها

الرمز هو ملف واحد،code/main.pyلا يوجد أي اعتماد خارج المكتبة القياسية إجر الاختبار الذاتي أولاً

bash
python3 code/main.py
npx tsx code/main.ts

يختبر الاختبار الذاتي المصادق والسلة الوهمية ومصيغة التدفق، وجولة ذهاب وإياب HTTP واحدة حقيقية. يبدأ الخادم على منفذ مجاني، يرسل ثلاثة طلبات، ويقف الخادم، ويقوم بالخروج. يبدأ الخادم لمصفح فقط عندما تحتاج إليه:

bash
python3 code/main.py --serve
npx tsx code/main.ts --serve

افتحhttp://127.0.0.1:8000في متصفح. اضغط على Ctrl + C لإيقاف الخادم.

الخطوة الأولى: المكالمة النموذجية

النموذج هو عداد الكلمات الرئيسية لعبة. يسمي النص الإيجابي أو السلبي أو المحايد. كما ينتج إجابة قصيرة، رمز واحد في كل مرة.

python
def classify(text):
    words = WORD_RE.findall(text.lower())
    positive = sum(1 for word in words if word in POSITIVE)
    negative = sum(1 for word in words if word in NEGATIVE)
    score = (positive - negative) / max(1, positive + negative)
    if score > 0:
        label = "positive"
    elif score < 0:
        label = "negative"
    else:
        label = "neutral"
    return {"label": label, "score": round(score, 3), "positive_cues": positive,
            "negative_cues": negative, "words": len(words)}


def generate(text):
    result = classify(text)
    reply = (
        f"Label: {result['label']}. Positive cues: {result['positive_cues']}. "
        f"Negative cues: {result['negative_cues']}. Words read: {result['words']}."
    )
    parts = reply.split(" ")
    yield parts[0]
    for part in parts[1:]:
        yield " " + part

نموذج حقيقي يُستبدلgenerateو لا شيء آخر. ضع مكالمة API أو نموذج محلي داخلها. حافظ على التوقيع: تدخل سلسلة، وتخرج الرموز. المحقق، والحدود، والصفحة تبقى نفسها.

الخطوة الثانية: التحقق من الصحة والردود على الأخطاء

فئة استثناء واحدة تحمل كل خطأ. تحتوي على حالة HTTP، رمز مستقرة، رسالة، إشارة، و عناوين الاستجابة الاختيارية.

python
class DemoError(Exception):
    def __init__(self, status, code, message, hint, headers=None):
        super().__init__(message)
        self.status = status
        self.code = code
        self.message = message
        self.hint = hint
        self.headers = dict(headers or {})


def validate_text(payload, max_chars=MAX_CHARS):
    if "text" not in payload:
        raise DemoError(422, "missing_text", 'The field "text" is missing.', 'Add a "text" field with your input.')
    text = payload["text"]
    if not isinstance(text, str):
        raise DemoError(422, "text_not_string", 'The field "text" is not a string.', "Send the input as a string.")
    text = text.strip()
    if not text:
        raise DemoError(422, "empty_text", "The text is empty.", "Type a sentence or pick an example.")
    if len(text) > max_chars:
        raise DemoError(422, "text_too_long", f"The text has {len(text)} characters. The limit is {max_chars}.",
                        f"Shorten the text to {max_chars} characters or fewer.")
    if CONTROL_RE.search(text):
        raise DemoError(422, "control_characters", "The text contains control characters.",
                        "Remove the hidden characters, then send the text again.")
    return text

يقوم المحقق بإزالة المساحة البيضاء الرائدة والمتأخرة أولاً. يظهر النص الذي يضم مساحات فقط بعد ذلك كبيراً، ويقول الإشارة للمستخدم ما يجب كتابته. check_body_sizeوparse_json_bodyاستخدم نفس النمط في الحالات 411, 413, 415 و 400. error_bodyيُحوّل أيّ شيءDemoErrorفي شكل JSON من قسم المفهوم.

الخطوة الثالثة: علبة الوسائل

python
class TokenBucket:
    def __init__(self, capacity, refill_per_second, now):
        if capacity < 1:
            raise ValueError("capacity must be at least 1")
        if refill_per_second <= 0:
            raise ValueError("refill_per_second must be positive")
        self.capacity = float(capacity)
        self.refill_per_second = float(refill_per_second)
        self.tokens = float(capacity)
        self.updated = now

    def take(self, now, cost=1.0):
        elapsed = max(0.0, now - self.updated)
        self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_per_second)
        self.updated = now
        if self.tokens >= cost:
            self.tokens -= cost
            return 0.0
        return (cost - self.tokens) / self.refill_per_second

takeيعود 0.0 عندما يقبل الطلب. وإلا يعود الثواني حتى يكون العبوة كافية الوهم. الخادم يدور التي قيمة وترسلها Retry-After. . .

RateLimiterيحتفظ بالسفن الواحدة لكل عميل فيOrderedDictقفل يحمي القاموس، لأنThreadingHTTPServerيُعامل كل طلب في خيط خاص به.max_clientsإدخالات، القيود إزالة العميل مع أقدم طلب. العميل إزالة يحصل على سطل كامل على طلبها التالي. حدد max_clientsأكثر من عدد العملاء النشطين الذي تتوقعه

المحدد يأخذclockالاختبارات تمرFakeClock، لذا يمكن للاختبار أن يقدم الوقت بعشر ثواني دون انتظارUsageCapيستخدم نفس نمط الساعة لجميع اليوم. يعد المكالمات في نوافذ ثابتة من 86,400 ثانية، ويرد الثواني حتى النافذة التالية عندما يكون العد كامل.

الخطوة الرابعة: التيار

python
def sse_event(event, data):
    payload = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
    return f"event: {event}\ndata: {payload}\n\n".encode("utf-8")


def stream_events(text, generate_fn=generate, delay=0.0, sleep=time.sleep):
    count = 0
    try:
        for token in generate_fn(text):
            yield sse_event("token", {"index": count, "text": token})
            count += 1
            if delay:
                sleep(delay)
    except Exception:
        yield sse_event("error", MODEL_ERROR)
        return
    result = classify(text)
    yield sse_event("done", {"label": result["label"], "score": result["score"], "tokens": count})

json.dumpsيكتب خط جديد داخل سلسلة كالعلامتين\n. لذلك كل حدث لديه سطر بيان واحد بدون خط جديد خام فيه.

الجهاز المصنع يلاحظ فشل النموذج ويرسل إرسالerrorحدث، لأن الخادم قد أرسل بالفعل خط الحالة.delayوsleepالحجج تبطئ تدفق المصفح، وتحافظ على الاختبارات سريعة.

الخطوة 5: معالج الطلب

DemoHandlerيمتد BaseHTTPRequestHandler. يدير POST التحققات بالترتيب من قسم المفهوم:

python
def do_POST(self):
    app = self.server.app
    request_id = uuid.uuid4().hex[:12]
    client_id = self.client_address[0]
    started = time.perf_counter()
    route = self.path if self.path in self.routes else "other"
    status, code, chars = 200, None, 0
    try:
        declared = self.headers.get("Content-Length")
        try:
            if route == "other":
                raise DemoError(404, "not_found", "This endpoint does not exist.",
                                "Send requests to /api/predict or /api/stream.")
            size = check_body_size(declared, app.config.max_body_bytes)
        except DemoError:
            self.drain(int(declared) if declared and declared.isdigit() else 0)
            raise
        try:
            body = self.rfile.read(size)
        except TimeoutError:
            self.close_connection = True
            raise DemoError(408, "request_timeout", "The request body did not arrive in time.",
                            "Send the request again with the full body.") from None
        app.admit(client_id, self.headers.get("Authorization"))
        text = app.read_text(self.headers.get("Content-Type"), body)
        chars = len(text)
        app.charge()
        if route == "/api/predict":
            self.send_json(200, predict(text), request_id)
        else:
            code = self.send_stream(text, request_id)
    except DemoError as err:
        status, code = err.status, err.code
        self.send_error_json(err, request_id)
    except TimeoutError:
        status, code = 408, "request_timeout"
        self.close_connection = True
    except Exception:
        status, code = 500, "internal_error"
        self.send_error_json(DemoError(500, "internal_error", "The server failed on this request.",
                                       "Try again. Quote the request id if you report the problem."), request_id)
    finally:
        latency_ms = (time.perf_counter() - started) * 1000
        app.log.record(request_id, route, client_id, status, chars, latency_ms, code)

admitيُحقق من رمز الوصول و سطل الوسائل. chargeيفتقد القيادة اليومية. إنه يعمل الأخير، لذلك فقط الطلبات الصالحة تعتبر ضد القيادة. كل طلب يكتب سجل تسجيل واحد في finallyو هذا يشمل طلبات رفضها.route، أبداً المسار الخام، لأن المسار يمكن أن يحمل سلسلة استفسار مع نص المستخدم.

أربعة تفاصيل تمنع الفشل الذي يسهل تفويتها:

  • timeout = 10في فئة المعاملة يغلق اتصال عندما يتوقف العميل عن إرسال. بدون ذلك ، يحتفظ العميل الذي يعلن 100 بايت ويرسل 10 بخيط خادم إلى الأبد. يرسل المعامل خطأ 408 JSON مع معرف الطلب ويقفل الاتصال. توقف الوقت بعد بدء التدفق يغلق الاتصال فقط ، لأن خط الحالة تم إرساله بالفعل.
  • log_messageيعود دون خروج. هذا يزيل خط السجل الافتراضي مع عنوان العميل وخط الطلب.
  • drainيقرأ ويستبعاد ما يصل إلى 64 كيب من الجسم الذي تم رفضه. RFC 1122 يقول أن TCP يرسل إعادة تعيين عندما يغلق مضيف اتصال مع بيانات غير قراءة. يمكن للعميل بعد ذلك فقدان الرد 413.
  • send_streamالصيدBrokenPipeErrorعندما يغلق المستخدم علامة التبويب، يتوقف النموذج، والطلب لا يكلف أكثر من ذلك.

BaseHTTPRequestHandlerيستخدم HTTP/1.0 افتراضيًا. نهاية جسم التدفق هو بعد ذلك اللحظة التي يغلق فيها الخادم الاتصال. يقوم الخادم الذي يستخدم HTTP/1.1 بإرسال الجسم مع تشفير نقل مقطوع للاستخدام نفسه.

الخطوة السادسة: الصفحة

render_pageيعيد وثيقة HTML واحدة مع نمط داخلي وخطوط داخلية. الصفحة لا تحميل خطوط خارجية أو الخطوط أو الصورة. النص يقرأ التدفق على هذا النحو:

javascript
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
  const chunk = await reader.read();
  if (chunk.done) { break; }
  buffer += decoder.decode(chunk.value, { stream: true });
  let cut = buffer.indexOf("\n\n");
  while (cut >= 0) {
    const frame = parseFrame(buffer.slice(0, cut));
    buffer = buffer.slice(cut + 2);
    cut = buffer.indexOf("\n\n");
    if (frame.event === "token") { output.textContent += frame.data.text; }
  }
}

يمكن أن ينتهي جزء من الشبكة في منتصف حدث. القارئ يحتفظ بالجزء غير الكامل فيbufferحتى يصل الخط الفارغ

الصفحة تكتب النموذج الخارجي مع textContentو لا أبداً معinnerHTML. النموذج الخارجي هو نص غير موثوق به. لنفترض أن الصفحة تضع ردًا على شكل HTML، والرد يحتوي على عنصر مع onerrorهذه الصفة يمكن أن تشغيل الرمز في متصفح المستخدم.

الخادم أيضاً يرسلContent-Security-Policyالرأس مع default-src 'none'تسمح هذه السياسة بكتابة واحدة داخل الخط وتصفيق واحد داخل الخط، يتم تحديدها بواسطة أشرطة SHA-256 . يرفض المتصفح كل الكتب الأخرى، بما في ذلك الكتب التي يقوم المهاجم بإدخالها في الصفحة.

python
def csp_hash(source):
    digest = hashlib.sha256(source.encode("utf-8")).digest()
    return "'sha256-" + base64.b64encode(digest).decode("ascii") + "'"

الخطوة السابعة: إشغلها وحاول كسرها

تشغيل الخادم، ثم استخدام curlإرسال سبع طلبات متتالية إلى خادم جديد، وعاء يحمل خمسة رموز ويحصل على رمزاً جديداً كل ثانيةين:

bash
for i in 1 2 3 4 5 6 7; do
  curl -s -o /dev/null -w '%{http_code} ' -X POST http://127.0.0.1:8000/api/predict \
    -H 'Content-Type: application/json' -d '{"text": "ok"}'
done
output
200 200 200 200 200 429 429

انتظر 10 ثواني ثم أرسل رسالة فارغة

bash
curl -s -X POST http://127.0.0.1:8000/api/predict \
  -H 'Content-Type: application/json' -d '{"text": ""}'
json
{"error": {"code": "empty_text", "message": "The text is empty.", "hint": "Type a sentence or pick an example.", "request_id": "e3e3599feaea"}}

شاهدوا سلسلة الأحداث معcurl -N، الذي يطفئ خفض الخروج:

bash
curl -N -X POST http://127.0.0.1:8000/api/stream \
  -H 'Content-Type: application/json' -d '{"text": "The screen is bright."}'

لتحقيق طلب من رمز الوصول، تعيين DEMO_ACCESS_TOKENقبل تشغيل الخادم، ثم يظهر الصفحة حقل للبرمجة.

bash
export DEMO_ACCESS_TOKEN="$(python3 -c 'import secrets; print(secrets.token_urlsafe(24))')"
echo "$DEMO_ACCESS_TOKEN"
python3 code/main.py --serve
npx tsx code/main.ts --serve

اجري الاختبارات:

bash
python3 -m unittest discover -s code/tests -v

اختبارات HTTP تبدأ الخادم على ميناء 0، لذلك يختار النظام التشغيلي ميناء حر. tearDownتوقف الخادم و تحقق من أن موضوعه انتهى.

استخدمها

إطار Python اثنين يبنون نفس النوع من الادموكات مع أقل رمز: Gradio و Streamlit. كل من الأمثلة أدناه استيراد العقد من main.py، لذلك يبقى المحقق والنموذج نفسه.main.pyأو الاختبارات

المكونات حول وظيفة واحدة

يضع Gradio واجهة ويب حول وظيفة Python واحدة. يصف مكونات المدخل والخروج العقد، و يقوم وظيفة الجناتور بإرسال خروج جزئية.

python
import os

import gradio as gr

from main import EXAMPLES, MAX_CHARS, DemoError, classify, generate, validate_text


def label_text(text):
    try:
        clean = validate_text({"text": text})
    except DemoError as err:
        raise gr.Error(f"{err.message} {err.hint}")
    label = classify(clean)["label"]
    reply = ""
    for token in generate(clean):
        reply += token
        yield reply, label


demo = gr.Interface(
    fn=label_text,
    inputs=gr.Textbox(label="Text to label", lines=5, max_length=MAX_CHARS),
    outputs=[gr.Textbox(label="Reply"), gr.Textbox(label="Label")],
    examples=list(EXAMPLES),
    flagging_mode="never",
)

if __name__ == "__main__":
    demo.queue(max_size=20).launch(auth=("demo", os.environ["DEMO_PASSWORD"]))

كل واحدyieldيُستبدل القيمة الإخراجية بأكملها، وبالتالي فإنّ الوظيفة تُعطي الإجابة الكاملة حتى الآن في كل خطوة.

المجموعةflagging_modeقبل مشاركة الرابط. الوضع الافتراضي يظهر زر العلم. عندما يضغطها المستخدم، كريديو يكتب المدخل والخروج إلى ملف CSV على الخادم. القيمة "never"يزيل الزر

queue(max_size=20)يحدد عدد الطلبات المتوقعة. الحد الافتراضي للتزامن هو مكالمة واحدة تشغيل لكل حدث. النموذج البطيء ثم يخدم مستخدم واحد في وقت واحد حتى ترفع الحد. يخلق Gradio أيضًا نقاط نهاية API عند إطلاق التطبيق ، بحيث يمكن للخطوط الدلائل التجريبية دون الصفحة. يجب أن ينطبق حد معدلك على تلك النقاط النهائية أيضًا.

نص يكرر كل دخول

يدير Streamlit النص كله من أعلى إلى أسفل كل مرة يغير فيها المستخدم الويجيت. يقوم النموذج بجمع قيم الويجيت الخاصة به ويرسلها في دفعة واحدة عندما يضغط المستخدم زر الإرسال.

python
import streamlit as st

from main import EXAMPLES, MAX_CHARS, DemoError, classify, generate, validate_text

st.title("Toy sentiment demo")
st.caption("Do not paste private data.")

example = st.selectbox("Start from an example", ["", *EXAMPLES])
with st.form("demo"):
    text = st.text_area("Text to label", value=example, max_chars=MAX_CHARS)
    submitted = st.form_submit_button("Run model")

if submitted:
    try:
        clean = validate_text({"text": text})
    except DemoError as err:
        st.error(f"{err.message} {err.hint}")
    else:
        st.write_stream(generate(clean))
        st.write(f"Label: {classify(clean)['label']}")

st.write_streamيأخذ المولّد مباشرة ويكتب كل قطعة عندما يصل.@st.cache_resourceلأن النص يُجرى مرة أخرى في كل عرض، ويحفظ المُزيّف نسخة واحدة من النموذج لكل جلسة.

ما يفعله كل خيار افتراضي

NeedStandard library (this lesson)GradioStreamlit
Form, examples, result areaYou write the HTMLInterface builds them from components and examplesWidgets such as st.text_area and st.selectbox
Input length limitmaxlength on the page and a server checkmax_length on Textboxmax_chars on st.text_area
Partial outputYou write SSE and the stream readerA generator function, where each yield replaces the outputst.write_stream with a generator of chunks
Progress displayYou write the timerA progress display on the output area, set by show_progressst.write_stream shows each chunk when it arrives
Errors for the userJSON with code, message, hint, request_idraise gr.Error(...) shows the messagest.error(...) shows the message
ConcurrencyOne thread per request, no limitA queue, with one running call per event by defaultEach interaction runs the script again for that session
Public linkYou run the server and the domainlaunch(share=True) gives a link that expires after one weekYou deploy the app to a server
Sign-inA bearer token checklaunch(auth=...) with user names and passwordsst.login() with an OpenID Connect provider
Rate limit and cost capToken bucket and daily capYour code, with the client address from the requestYour code
Input that the tool writes to diskOnly the input length, in the logFlagged samples in a CSV file, unless flagging_mode="never"Your code decides

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

أرسله

هذا الدرس يُنتجoutputs/skill-demo-interface-checklist.md. أعطيه وصف نموذج تجريبي أو رمزه. يعيد مراجعة مرور أو فشل للعقد ، والردود ، والخطأ ، والتحكم في رابط عام. كل فشل يأتي مع تصحيح واحد.

التمارين

  1. المجموعةBUCKET_CAPACITYإلى 2 و REFILL_PER_SECONDالتنبؤ بمخرج حلقة السبع طلبات ثم تشغيله ومقارنة.
  2. إضافةlanguageالمجال إلى الجهة الطلبة."en"أعيد خطأ 422 مع إشارة لأي قيمة أخرى، وأضف اختبارات.
  3. ابحث عن جملة يضعها النموذج على اللعبة علامة خاطئة، مثل "الشاشة ليست جيدة". أضفها كمثال رابع، مع ملاحظة على الصفحة تشرح الحد.
  4. ضع الاكتشاف خلف الوكيل العكسي على جهازكtrust_proxyالخيار الذي يقرأX-Forwarded-Forإكتب اختباراً يثبت أن العميل لا يستطيع اختيار مفتاح السلة الخاص به
  5. استبدلgenerateمع دعوة إلى نموذج API الذي يعيد الرموز كمتد. الحفاظ على المؤكد، والحدود، ونموذج الحدث. مقارنة الوقت لأول رموز في الصفحة مع التأخير في سجل الخادم.

الشروط الرئيسية

TermWhat people sayWhat it actually means
Demo contract"The UI"The input limits, the model call, and the output shape, including errors
Token bucket"A rate limiter"A per-client counter that refills at a fixed rate up to a capacity. Each request uses one token
Retry-After"Try later"A response header with the number of seconds, or a date, after which the client can try again
Server-Sent Events"Streaming"A text/event-stream response of event: and data: lines, with a blank line after each event
Time to first token"Latency"The time from the request to the first token on the user's screen
Usage cap"The budget"A limit on the total number of model calls in a time window, for all clients together
Keyed hash"An anonymous ID"An HMAC of the client address with a secret salt. It is a pseudonym: anyone with the salt can test a known address
Content Security Policy"CSP"A response header that tells the browser which scripts, styles, and connections the page can use

المزيد من القراءة

This free lesson is part of the AI Engineering from Scratch curriculum. Read the full explanation, run the lesson code, and verify the result in the interactive reader or from the repository source.

Browse the complete course catalog or open this lesson on GitHub.