فرض کنید در یک پاسخ‌گوی صوتی، درخواست تهیهٔ یادداشت دوباره به برنامه برسد. آیا باید کار تازه‌ای آغاز شود، وضعیت کار قبلی اعلام شود یا نتیجهٔ ثبت‌شده برگردد؟ پاسخ بدون تعریف هویت درخواست و حالت آن روشن نیست. این آموزش یک مدل کوچک حافظه‌ای می‌سازد تا این تصمیم‌ها را با کد کامل و هجده بررسی ببینیم. هیچ تماس، پیام، پرداخت، ابزار واقعی یا مدل گفتاری اجرا نمی‌شود؛ حتی تهیهٔ یادداشت نیز در این مثال فقط نام یک عملیات فرضی است.

۱. کلید درخواست، نیت را از شباهت محتوا جدا می‌کند

تمایز نیت درخواست از شباهت محتوای آن
تصویر مفهومی: تمایز نیت درخواست از شباهت محتوای آن؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

دو درخواست با پارامترهای یکسان لزوماً یک نیت نیستند. ممکن است کاربر واقعاً دو یادداشت مستقل بخواهد. در مقابل، تکرار یک درخواست پس از دریافت نکردن پاسخ می‌تواند همان نیت قبلی باشد. مقالهٔ اصلی Amazon Builders’ Library شناسهٔ تعیین‌شده از سوی فراخواننده، همراه با زمینهٔ او، را برای بیان این تفاوت توضیح می‌دهد. از این ایده برای یک تمرین مستقل استفاده می‌کنیم؛ کد ما سرویس یا SDK آمازون نیست.

هویت در مدل ما جفت scope و key است. برای نمونه، CallA و Req7 یک درخواست را مشخص می‌کنند. scope در اینجا فقط رشتهٔ صریح ورودی است؛ هویت تأییدشدهٔ مشتری یا مرز مجوز سازمانی نیست. همین key در CallB درخواست جداگانه محسوب می‌شود، اما این جداسازی حافظه‌ای جای احراز هویت را نمی‌گیرد. کلید خالی، عددی، خیلی بلند یا خارج از الگوی لاتین نمونه رد می‌شود.

بدنه دقیقاً operation، sensor_id و window_ms دارد. عملیات باید prepare_note باشد؛ شناسهٔ حسگر را برای نمونه S01 و پنجره را دویست میلی‌ثانیه می‌گیریم. شناسه‌های دیگرِ مطابق الگو هم مجازند. پنجره عدد صحیح از یک تا ده‌هزار است؛ بولی، رشته و ۲۰۰٫۰ پذیرفته نمی‌شوند. این دامنه و نام‌ها قرارداد تألیفی تمرین‌اند. پس از طراحی ورودی و خروجی ابزار، باید روشن کنیم بازگشت دوبارهٔ همان ورودی چه اثری بر حالت دارد.

۲. حالت فعال هنوز نتیجهٔ کامل ندارد

حفظ حالت فعال تا ثبت نتیجهٔ کامل
تصویر مفهومی: حفظ حالت فعال تا ثبت نتیجهٔ کامل؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

اگر هویت قبلاً دیده نشده باشد، begin یک رکورد active می‌سازد و started برمی‌گرداند. این شروع فقط تغییر یک دیکشنری است و هیچ تابع اجرایی را صدا نمی‌زند. اگر همان هویت و محتوای پذیرفته‌شده دوباره برسد، پاسخ in_progress است؛ نتیجه‌ای ساخته نمی‌شود و رکورد دوم هم نداریم. active به معنی موفقیت عملیات خارجی نیست، چون اصلاً عملیات خارجی در مدل وجود ندارد.

در گام جدا، finish نتیجهٔ ساختگی را ثبت می‌کند. نتیجه دقیقاً note_id و sample_count دارد؛ شناسه باید معتبر و تعداد، عدد صحیح غیرمنفی با سقف یک‌میلیون باشد. برای دادهٔ دستی، N01 و هشت نمونه را انتخاب کرده‌ایم. finish موفق، حالت را completed می‌کند. درخواست تکراریِ بعدی با همان محتوا، وضعیت replayed و کپی همان نتیجه را می‌گیرد. finish دوم خطاست و نتیجهٔ قبلی را عوض نمی‌کند.

این تمایز برای آزمون سامانهٔ صوتی مفید است: شنیدن دوبارهٔ یک جمله به‌تنهایی مجوز صدور کلید تازه نیست. سیاست ساخت و نگهداری کلید باید در لایهٔ کاربرد تعریف شود. کد نه جمله را می‌فهمد و نه نیت گفتاری را تشخیص می‌دهد. معماری تلفن هوشمند سازمانی زمینهٔ اتصال صوت، دانش و ابزار را توضیح می‌دهد؛ اینجا فقط یک مرز کوچک پس از تشکیل درخواست را بررسی می‌کنیم.

۳. یک کلید با محتوای متفاوت، تعارض است

تعارض محتوای متفاوت با کلید یکسان
تصویر مفهومی: تعارض محتوای متفاوت با کلید یکسان؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

با ثابت ماندن CallA و Req7، تغییر window_ms از دویست به سیصد conflict می‌دهد. این پاسخ در حالت فعال و پس از تکمیل یکسان است؛ بدنهٔ قبلی جایگزین نمی‌شود. finish با بدنهٔ ناسازگار نیز رد می‌شود. منبع آمازون همین مسئلهٔ شناسهٔ ثابت و نیت متفاوت را با بررسی پارامترهای اولیه مطرح می‌کند؛ نام وضعیت‌ها، ترتیب اعتبارسنجی و محدودهٔ تمرین حاضر انتخاب ماست.

برای مقایسهٔ محتوا، json.dumps پایتون را با مرتب‌سازی کلیدها و جداکننده‌های ثابت به کار می‌بریم. بنابراین تغییر ترتیب اعضای دیکشنری، درخواست جدید نمی‌سازد. این نمایش فقط برای طرح محدود و انواع پذیرفته‌شدهٔ همین مثال است؛ استاندارد عمومی یکسان‌سازی JSON، امضای دیجیتال یا اثبات اصالت نیست. متن خام JSON هم خوانده نمی‌شود؛ ورودی از ابتدا شیء پایتونی است.

برای محافظت از نتیجهٔ ذخیره‌شده در برابر تغییر دیکشنریِ فراخواننده، هنگام ثبت و بازگرداندن از کپی عمیق استفاده شده است. اگر sample_count در نتیجهٔ اولیه یا پاسخ دریافتی تغییر کند، بازپخش بعدی همچنان هشت را می‌دهد. این یک خاصیت محلی برنامه است؛ دسترسی مستقیم به صفت خصوصی، دستکاری حافظه یا مجوز فراخوانی finish را کنترل نمی‌کند. طرح نتیجه فقط دو فیلد با مقدارهای تغییرناپذیر دارد و ادعای جداسازی دلخواه همهٔ اشیای پایتون نداریم.

۴. کد کامل، خط زمانی دستی و هجده بررسی

آزمون بازپخش نتیجه بدون اجرای اقدام واقعی
تصویر مفهومی: آزمون بازپخش نتیجه بدون اجرای اقدام واقعی؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

فایل voice_idempotency.py فقط کتابخانهٔ استاندارد می‌خواهد و اجرای ثبت‌شده با Python 3.14.5 انجام شده است. قبل از اجرا، پاسخ خط زمانی را دستی بنویسید: آغاز دویست، تکرار دویست، تعارض سیصد، ثبت نتیجه و تکرار دویست؛ به ترتیب started، in_progress، conflict، completed و replayed. نتیجهٔ دو گام آخر باید N01 و هشت باشد؛ conflict نباید پنجرهٔ اولیه را عوض کند.

"""Offline sequential request-state model; never invokes any real tool or service."""
from copy import deepcopy
import json
import platform
import re


def identifier(value):
    if type(value) is not str or re.fullmatch(r'[A-Za-z][A-Za-z0-9_-]{0,31}', value) is None:
        raise ValueError('identifier')
    return value


def request_text(payload):
    if type(payload) is not dict or set(payload) != {'operation', 'sensor_id', 'window_ms'}:
        raise ValueError('payload_fields')
    if payload['operation'] != 'prepare_note':
        raise ValueError('operation')
    identifier(payload['sensor_id'])
    if type(payload['window_ms']) is not int or not 1 <= payload['window_ms'] <= 10_000:
        raise ValueError('window_ms')
    # A deterministic comparison representation for this exact restricted schema.
    # This is not a universal JSON canonicalization or an authentication mechanism.
    return json.dumps(payload, sort_keys=True, separators=(',', ':'), ensure_ascii=True)


def checked_result(result):
    if type(result) is not dict or set(result) != {'note_id', 'sample_count'}:
        raise ValueError('result_fields')
    identifier(result['note_id'])
    if type(result['sample_count']) is not int or not 0 <= result['sample_count'] <= 1_000_000:
        raise ValueError('sample_count')
    return deepcopy(result)


class MemoryRequests:
    """Only sequential calls within this object's lifetime; no crash/concurrency model."""
    def __init__(self):
        self._records = {}

    def begin(self, scope, key, payload):
        pair = (identifier(scope), identifier(key))
        text = request_text(payload)
        previous = self._records.get(pair)
        if previous is None:
            self._records[pair] = {'request_text': text, 'state': 'active'}
            return {'status': 'started'}
        if previous['request_text'] != text:
            return {'status': 'conflict'}
        if previous['state'] == 'active':
            return {'status': 'in_progress'}
        return {'status': 'replayed', 'result': deepcopy(previous['result'])}

    def finish(self, scope, key, payload, result):
        pair = (identifier(scope), identifier(key))
        text = request_text(payload)
        copied_result = checked_result(result)
        if pair not in self._records:
            raise ValueError('unknown_request')
        record = self._records[pair]
        if record['request_text'] != text:
            raise ValueError('payload_conflict')
        if record['state'] != 'active':
            raise ValueError('already_completed')
        record['result'] = copied_result
        record['state'] = 'completed'
        return {'status': 'completed', 'result': deepcopy(copied_result)}


def rejects(call, message):
    try:
        call()
    except ValueError as error:
        return str(error) == message
    return False


def run_example():
    payload = {'operation': 'prepare_note', 'sensor_id': 'S01', 'window_ms': 200}
    changed = dict(payload, window_ms=300)
    result = {'note_id': 'N01', 'sample_count': 8}
    store = MemoryRequests()
    start = store.begin('CallA', 'Req7', payload)
    active = store.begin('CallA', 'Req7', payload)
    conflict_active = store.begin('CallA', 'Req7', changed)
    completed = store.finish('CallA', 'Req7', payload, result)
    replay = store.begin('CallA', 'Req7', payload)
    expected_result = {'note_id': 'N01', 'sample_count': 8}
    invalid_store = MemoryRequests()
    invalid_store.begin('CallA', 'Req7', payload)
    invalid_result_rejected = rejects(lambda: invalid_store.finish('CallA', 'Req7', payload, {'note_id': 'N01', 'sample_count': True}), 'sample_count')
    ordered = {'window_ms': 200, 'sensor_id': 'S01', 'operation': 'prepare_note'}
    copied_store = MemoryRequests()
    copied_store.begin('CallA', 'Req7', payload)
    mutable = {'note_id': 'N01', 'sample_count': 8}
    first_result = copied_store.finish('CallA', 'Req7', payload, mutable)
    mutable['sample_count'] = 999
    first_result['result']['sample_count'] = 777
    returned_replay = copied_store.begin('CallA', 'Req7', payload)
    returned_replay['result']['sample_count'] = 555
    checks = {
        'unseen_identity_starts_once_in_hand_timeline': start == {'status': 'started'},
        'same_active_request_reports_progress_without_result': active == {'status': 'in_progress'} and 'result' not in active,
        'different_active_payload_conflicts_without_replacing_original': conflict_active == {'status': 'conflict'},
        'explicit_finish_records_independent_hand_result': completed == {'status': 'completed', 'result': expected_result},
        'completed_retry_replays_same_saved_result': replay == {'status': 'replayed', 'result': expected_result},
        'changed_payload_still_conflicts_after_completion': store.begin('CallA', 'Req7', changed) == {'status': 'conflict'},
        'second_finish_cannot_overwrite_recorded_result': rejects(lambda: store.finish('CallA', 'Req7', payload, dict(result, sample_count=9)), 'already_completed') and store.begin('CallA', 'Req7', payload) == replay,
        'finish_of_unseen_identity_is_rejected': rejects(lambda: store.finish('CallA', 'Missing', payload, result), 'unknown_request'),
        'finish_payload_mismatch_does_not_complete_active_request': rejects(lambda: invalid_store.finish('CallA', 'Req7', changed, result), 'payload_conflict') and invalid_store.begin('CallA', 'Req7', payload) == {'status': 'in_progress'},
        'invalid_result_does_not_consume_active_state': invalid_result_rejected and invalid_store.begin('CallA', 'Req7', payload) == {'status': 'in_progress'},
        'same_key_in_different_explicit_scope_is_new_intent': store.begin('CallB', 'Req7', payload) == {'status': 'started'},
        'identical_payload_with_different_key_is_not_deduplicated': store.begin('CallA', 'Req8', payload) == {'status': 'started'},
        'empty_nonstring_long_or_nonascii_identity_rejected': all(rejects(lambda value=value: store.begin('CallA', value, payload), 'identifier') for value in ('', 7, True, 'R' * 33, 'درخواست')),
        'payload_fields_operation_sensor_and_exact_integer_types_checked': rejects(lambda: request_text(dict(payload, extra=1)), 'payload_fields') and rejects(lambda: request_text({'operation': 'prepare_note'}), 'payload_fields') and rejects(lambda: request_text(dict(payload, operation='send_message')), 'operation') and rejects(lambda: request_text(dict(payload, sensor_id='1bad')), 'identifier') and all(rejects(lambda value=value: request_text(dict(payload, window_ms=value)), 'window_ms') for value in (True, 200.0, '200')),
        'inclusive_window_limits_and_invalid_values_checked': all(request_text(dict(payload, window_ms=value)) for value in (1, 10_000)) is True and all(rejects(lambda value=value: request_text(dict(payload, window_ms=value)), 'window_ms') for value in (0, -1, 10_001)),
        'dictionary_member_order_does_not_change_accepted_request_identity': request_text(ordered) == '{"operation":"prepare_note","sensor_id":"S01","window_ms":200}' and store.begin('CallA', 'Req7', ordered) == replay,
        'mutating_input_finish_response_or_replay_cannot_change_saved_result': copied_store.begin('CallA', 'Req7', payload) == {'status': 'replayed', 'result': expected_result},
        'new_memory_store_forgets_prior_completion_not_durable_exactly_once': MemoryRequests().begin('CallA', 'Req7', payload) == {'status': 'started'},
    }
    assert len(checks) == 18 and all(type(value) is bool and value for value in checks.values()), checks
    return {'python': platform.python_version(), 'dependencies': 'standard library only',
            'scope': 'Sequential in-memory state transitions; no model, tool action, network, payment, persistence, expiry, authentication or exactly-once guarantee',
            'payload': payload, 'comparison_representation': request_text(payload),
            'hand_timeline': [start, active, conflict_active, completed, replay],
            'hand_expected_statuses': ['started', 'in_progress', 'conflict', 'completed', 'replayed'],
            'scope_or_key_distinct_requests_start_independently': True,
            'checks': checks}


if __name__ == '__main__':
    print(json.dumps(run_example(), ensure_ascii=False, indent=2, allow_nan=False))

بررسی‌ها فقط خروجی چاپ‌شده را تکرار نمی‌کنند؛ تمامیت حالت پس از خطای نتیجه، منع تکمیل ناشناخته، منع بازنویسی تکمیل‌شده، استقلال scope و key و مرزهای پنجره را می‌سنجند. یک مرجع دستی برای نام وضعیت‌ها و نتیجهٔ نمونه داریم. هجده نام، هجده گروه بررسی‌اند؛ اجرای دوباره یا چند ورودی در یک گروه، تعداد گروه‌های مستقل را افزایش نمی‌دهد. خروجی زیر حاصل همین فایل مستقل است.

{
  "python": "3.14.5",
  "dependencies": "standard library only",
  "scope": "Sequential in-memory state transitions; no model, tool action, network, payment, persistence, expiry, authentication or exactly-once guarantee",
  "payload": {
    "operation": "prepare_note",
    "sensor_id": "S01",
    "window_ms": 200
  },
  "comparison_representation": "{\"operation\":\"prepare_note\",\"sensor_id\":\"S01\",\"window_ms\":200}",
  "hand_timeline": [
    {
      "status": "started"
    },
    {
      "status": "in_progress"
    },
    {
      "status": "conflict"
    },
    {
      "status": "completed",
      "result": {
        "note_id": "N01",
        "sample_count": 8
      }
    },
    {
      "status": "replayed",
      "result": {
        "note_id": "N01",
        "sample_count": 8
      }
    }
  ],
  "hand_expected_statuses": [
    "started",
    "in_progress",
    "conflict",
    "completed",
    "replayed"
  ],
  "scope_or_key_distinct_requests_start_independently": true,
  "checks": {
    "unseen_identity_starts_once_in_hand_timeline": true,
    "same_active_request_reports_progress_without_result": true,
    "different_active_payload_conflicts_without_replacing_original": true,
    "explicit_finish_records_independent_hand_result": true,
    "completed_retry_replays_same_saved_result": true,
    "changed_payload_still_conflicts_after_completion": true,
    "second_finish_cannot_overwrite_recorded_result": true,
    "finish_of_unseen_identity_is_rejected": true,
    "finish_payload_mismatch_does_not_complete_active_request": true,
    "invalid_result_does_not_consume_active_state": true,
    "same_key_in_different_explicit_scope_is_new_intent": true,
    "identical_payload_with_different_key_is_not_deduplicated": true,
    "empty_nonstring_long_or_nonascii_identity_rejected": true,
    "payload_fields_operation_sensor_and_exact_integer_types_checked": true,
    "inclusive_window_limits_and_invalid_values_checked": true,
    "dictionary_member_order_does_not_change_accepted_request_identity": true,
    "mutating_input_finish_response_or_replay_cannot_change_saved_result": true,
    "new_memory_store_forgets_prior_completion_not_durable_exactly_once": true
  }
}

در گزارش ۱۴ اکتبر ۲۰۲۶ NVIDIA دربارهٔ ابزارهای عامل طراحی تراشه، نویسندگان بررسی خروجی نهایی در یک محیط کنترل‌شده را توضیح می‌دهند. این زمینهٔ تازه، انگیزه‌ای برای توجه به حالت نهایی است؛ نتیجهٔ تجاری یا نسبت‌های عملکرد آن گزارش را برای مثال خودمان تکرار نکرده‌ایم. NeMo Gym، ابزار EDA یا مدل عامل در این درس اجرا نشده است.

۵. چهار پرسش پاسخ‌دار و مرز تضمین مدل

مرز مدل حافظه‌ای با پایداری و اجرای واقعی
تصویر مفهومی: مرز مدل حافظه‌ای با پایداری و اجرای واقعی؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

پرسش اول: آیا بدنهٔ یکسان با Req8 باید نتیجهٔ Req7 را بگیرد؟ پاسخ: خیر؛ کلید متفاوت نیت مستقل است و started می‌دهد. حذف تکرار بر اساس شباهت محتوا به‌تنهایی، می‌تواند درخواست مستقل را از بین ببرد.

پرسش دوم: آیا in_progress را می‌توان به موفقیت تبدیل کرد؟ پاسخ: خیر؛ هنوز نتیجه‌ای ثبت نشده است. برنامهٔ نمونه نه زمان انتظار دارد، نه انقضای حالت فعال و نه راه بازیابی کار رهاشده. این سیاست‌ها باید جدا مشخص و آزموده شوند.

پرسش سوم: پس از completed، تغییر پنجره با همان کلید چه می‌شود؟ پاسخ: conflict؛ نتیجه و پارامتر اولیه باقی می‌مانند. برای نیت تازه، باید هویت تازه و معتبر تعریف شود، نه اینکه محتوای رکورد قبلی را بی‌صدا تغییر دهیم.

پرسش چهارم: آیا این کد اجرای دقیقاً یک‌باره را در سامانهٔ واقعی تضمین می‌کند؟ پاسخ: خیر؛ ساخت MemoryRequests جدید، تکمیل قبلی را فراموش می‌کند و همان درخواست دوباره started می‌شود. پایداری ذخیره، اتمی بودن ثبت و اقدام، هم‌زمانی، خرابی و نگهداری شناسه در این مدل پیاده نشده‌اند. منابع در ۲۴ مهر ۱۴۰۵ خوانده شدند؛ تاریخ اعلام انتشار مقالهٔ قدیمی آمازون ۱۵ ژانویهٔ ۲۰۲۱ است و تاریخ دسترسی، خبر تازه محسوب نمی‌شود.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۴ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.