از Prototype تا Production؛ درس‌هایی که هنگام ساخت یک Retrieval Engine واقعی گرفتیم

ساخت Demo RAG آسان است؛ Production Retrieval چیز دیگری است. باگ‌ها و تصمیم‌های واقعی در ACL، Chunk ID، SQLite، HNSW، OCR، Reranking، Rollback و Evaluation را مرور می‌کنیم

نوشتهٔ HomAI10 دقیقه مطالعه
  • Production RAG
  • RAG Architecture
  • RAG Production Best Practices
  • Semantic Search Production
  • Retrieval Engine
  • RAG Lessons Learned

از Prototype تا Production؛ درس‌هایی که هنگام ساخت یک Retrieval Engine واقعی گرفتیم

یک RAG demo را می‌توان در چند ساعت ساخت. چند صفحه متن، یک embedding model، یک vector index و یک prompt کافی است تا اولین پاسخ تولید شود.

اما وقتی همان سیستم باید فایل‌های واقعی سازمان را ingest کند، فارسی را درست بفهمد، ACL را رعایت کند، بعد از restart سالم بماند، هم‌زمان چند request بگیرد و نتیجه قابل اندازه‌گیری تولید کند، جنس مسئله عوض می‌شود.

Semantic Chunk Search نیز همین مسیر را طی کرد. این پروژه از استخراج یک لایه Chunking و Semantic Search شروع شد و در چند iteration تبدیل به یک retrieval core مستقل شد.

در این مقاله مهم‌ترین درس‌های فنی این مسیر را مرور می‌کنیم؛ مخصوصاً باگ‌هایی که در نگاه اول واضح نبودند.

درس اول: Green Test Suite به معنی پایان کار نیست

در هر مرحله tests سبز بودند، اما audit عمیق edge caseهای جدیدی پیدا می‌کرد.

مثلاً ممکن است unit test بگوید ingestion کار می‌کند، ولی سؤال واقعی این باشد:

اگر متن تغییر نکند ولی ACL تغییر کند چه؟

یا:

اگر parent بزرگ‌تر از token budget باشد، آیا خود hit حذف می‌شود؟

Production hardening یعنی test کردن invariantها، نه فقط functionها.

۱. ACL قدیمی روی متن بدون تغییر

سناریو:

Version 1: visibility=public
Version 2: same text, visibility=private

اگر idempotency فقط content hash را ببیند، version دوم skip می‌شود و document همچنان public می‌ماند.

راه‌حل این بود که metadata/ACL update مستقل از text change معتبر باشد.

درس: Metadata امنیتی بخشی از state source است.

۲. Duplicate Chunk ID

Stable ID برای re-indexing عالی است، اما طراحی ساده ممکن است از hash متن استفاده کند.

اگر یک جمله یکسان دو بار در یک section تکرار شود، ID هر دو یکی می‌شود و یکی روی دیگری overwrite می‌شود.

Identity باید علاوه بر content، موقعیت/ساختار duplicate را هم در نظر بگیرد.

درس: Deterministic بودن کافی نیست؛ identity باید duplicate-safe هم باشد.

۳. Async و SQLite

Facade async با asyncio.to_thread ساده به نظر می‌رسد، تا زمانی که SQLite connection در thread دیگری استفاده شود.

در تست concurrency خطای thread ownership ظاهر شد.

راه‌حل شامل connection configuration مناسب، lock و WAL بود.

درس: Async wrapper روی API sync به‌تنهایی thread safety ایجاد نمی‌کند.

۴. NDCG می‌توانست بیشتر از ۱ شود

Metricها هم bug دارند.

وقتی relevance در سطح source تعریف شده بود ولی ranking چند Chunk از همان source داشت، source چند بار شمرده می‌شد و NDCG اشتباه محاسبه می‌شد.

Source ranking باید قبل از metric deduplicate شود.

درس: Evaluation code همان‌قدر که production code نیاز به test دارد.

۵. Page Ingestion مسیر Security Scanner را دور می‌زد

ورودی text مستقیم scan می‌شد، اما یک ingestion path دیگر برای pageها از scanner عبور نمی‌کرد.

این یک کلاس رایج bug است: security control وجود دارد، ولی coverage همه pathها کامل نیست.

درس: Security را به شکل invariant تست کنید، نه وجود یک function.

۶. Blank PDF Page

صفحه بدون متن نباید ingestion کل document را fail کند.

ممکن است:

  • صفحه واقعاً خالی باشد
  • image-only باشد
  • OCR لازم داشته باشد

Pipeline باید این حالت را صریح مدیریت کند.

درس: Document ingestion باید برای داده ناقص طراحی شود، نه فایل آزمایشگاهی تمیز.

۷. Parent ناقص در Composite Chunking

در یک نسخه، parent یک mixed section عملاً فقط متن child اول را نمایندگی می‌کرد.

این باعث می‌شد context expansion تصویری ناقص از section بدهد.

راه‌حل، ساخت parent از کل section بود.

درس: Hierarchy باید semantic containment واقعی را منعکس کند.

۸. Parent می‌توانست Hit اصلی را از Context بیرون بیندازد

Context builder token budget محدود داشت. اگر parent بزرگ ابتدا اضافه می‌شد، بودجه پر می‌شد و خود retrieved hit حذف می‌شد.

Policy اصلاح شد:

Hit اصلی اول وارد context شود؛ parent و neighbor بعد از آن.

درس: Retrieval evidence باید در context packing اولویت تضمین‌شده داشته باشد.

۹. Reranker می‌توانست Source Trust را overwrite کند

اگر source trust قبل از reranker اعمال شود و reranker score جدید تولید کند، policy multiplier از بین می‌رود.

Trust به مرحله بعد از learned/callable reranking منتقل شد.

درس: ترتیب stageهای ranking بخشی از policy correctness است.

۱۰. Cross Encoder و Double Activation

بعضی مدل‌ها raw logits می‌دهند، بعضی output فعال‌شده. اگر pipeline بی‌اطلاع دوباره sigmoid بزند، score calibration خراب می‌شود.

Contract صریح برای activation اضافه شد:

model
sigmoid
identity

درس: Adapterهای ML باید contract score واضح داشته باشند.

۱۱. Chunk Size بزرگ‌تر از Input Model

Chunker می‌توانست Chunk 1000-token بسازد ولی embedding model فقط 512 token بپذیرد.

اگر model truncate کند، نیمه دوم Chunk عملاً index نمی‌شود.

راه‌حل: model-aware chunk budget clamp.

درس: Configuration اجزای pipeline باید constraint یکدیگر را بدانند.

۱۲. Versioning فقط یک شماره نیست

اگر embedding model یا vectorizer عوض شود، vectorهای قدیمی ممکن است با Query جدید ناسازگار شوند.

Signatureهای قوی‌تر برای embedding/vectorizer/contextualizer اضافه شدند تا incompatibility قابل تشخیص باشد.

درس: Retrieval index یک artifact versioned است.

۱۳. HNSW داشت عملاً Exact Scan می‌شد

وجود ANN به معنی استفاده واقعی از ANN نیست.

در یک مسیر، engine همیشه predicate filter می‌فرستاد و adapter HNSW در presence فیلتر به exact scan fallback می‌کرد.

از بیرون سیستم HNSW «فعال» بود، اما performance benefit عملاً از بین رفته بود.

Filtered HNSW traversal و named representation indexes اضافه شدند.

درس: Feature flag را با behavior benchmark کنید، نه با اسم class.

۱۴. Qdrant فقط یک callback کافی نبود

Generic adapter خوب است، اما backend production نیاز به semantics مشخص دارد:

  • named vectors
  • sparse vector optional
  • payload ACL filters
  • source-scoped replacement
  • delete-by-source

در v4 QdrantSemanticIndex به‌عنوان adapter واقعی‌تر اضافه شد.

درس: Integration production باید contract عملیاتی داشته باشد، نه فقط interface نظری.

۱۵. BM25 ساده به BM25F تبدیل شد

با multi-representation document، occurrence در title با occurrence در body ارزش یکسانی ندارد.

BM25F field-aware scoring این تفاوت را مدل کرد.

درس: وقتی schema غنی می‌شود، lexical ranking هم باید schema-aware شود.

۱۶. Sparse Encoding باید Batch باشد

Encode کردن تک‌به‌تک documentها throughput را خراب می‌کند.

Batch API برای sparse encoder اهمیت پیدا کرد.

درس: Performance اغلب از interface design شروع می‌شود.

۱۷. Multimodal نباید جزیره جدا باشد

اگر visual retriever صفحه مرتبط پیدا کند ولی نتیجه‌اش وارد fusion اصلی نشود، دو سیستم search جدا داریم.

Visual candidateها وارد همان fusion/ranking pipeline شدند و ACL نیز روی آن‌ها اعمال می‌شود.

درس: Retrieval channel جدید باید وارد policy و ranking مشترک شود.

۱۸. Late Interaction واقعی با whitespace approximation فرق دارد

یک MaxSim ساده روی tokenهای تقریبی می‌تواند مفید باشد، اما ColBERT-style late interaction به token matrix واقعی نیاز دارد.

Contract جدا برای encoder چندبرداری اضافه شد.

درس: نام الگوریتم نباید capability را بیش از حد ادعا کند؛ approximation و implementation واقعی را جدا کنید.

۱۹. Graph Routing باید Mode را واقعاً منتقل کند

وجود labelهای local/global کافی نیست؛ mode باید تا graph retriever پایین‌دست propagate شود.

یک bug routing این مسیر را اصلاح کرد.

درس: End-to-end behavior را تست کنید، نه فقط تصمیم Router را.

۲۰. Feedback بدون Persistence نصف قابلیت است

اگر کاربر feedback بدهد و بعد restart همه چیز از بین برود، ranker در محیط واقعی قابل استفاده نیست.

Feedback store durable شد و persistence failure قبل از mutation حافظه مدیریت شد.

درس: Learning signal باید lifecycle قابل اتکا داشته باشد.

۲۱. Fail-open باید Observable باشد

Optional stage مثل reranker یا graph retriever ممکن است fail شود. گاهی منطقی است search ادامه پیدا کند.

اما silent except Exception خطرناک است؛ سیستم ظاهراً سالم است ولی quality افت کرده.

Diagnostics و strict mode اضافه شدند.

درس: Graceful degradation بدون observability تبدیل به silent degradation می‌شود.

۲۲. HTML Hidden DOM نباید Index شود

Hidden content هم search noise است و هم attack surface.

HTML loader اصلاح شد تا scripts/styles و hidden DOM را کنار بگذارد.

درس: Parser کیفیت و امنیت را هم‌زمان تعیین می‌کند.

۲۳. No-Evidence باید Empty باشد

یکی از بدترین fallbackها این است که وقتی هیچ evidence واقعی نداریم، چند candidate arbitrary برگردانیم تا result خالی نباشد.

این رفتار حذف شد.

درس: «نمی‌دانم» در retrieval بهتر از evidence ساختگی است.

۲۴. Delete و Rollback فقط Happy Path نیستند

Source delete ممکن است وسط چند storage/index fail شود. بدون snapshot و recovery، state ناقص می‌ماند.

Source snapshot recovery و restore مستقل اضافه شد.

درس: Mutation چندمرحله‌ای به recovery plan نیاز دارد.

۲۵. Feedback باید Durable-First باشد

اگر memory اول update شود و disk write fail کند، state حافظه و persistence از هم جدا می‌شوند.

Ordering اصلاح شد تا durability مقدم باشد.

درس: ترتیب mutation در consistency مهم است.

۲۶. Page Hash باید Source-wide باشد

Hash cumulative برای هر page identity source را ناسازگار می‌کرد.

یک hash برای source کامل استفاده شد.

درس: تعریف identity باید از ابتدا روشن باشد.

۲۷. Router فقط Heuristic نماند

Heuristic سریع و قابل explain است، اما برای queryهای پیچیده محدودیت دارد.

Classifier اختیاری با confidence threshold و heuristic fallback اضافه شد.

درس: ML را جایی اضافه کنید که fallback deterministic دارید.

۲۸. Pairwise Ranking

Pointwise score همیشه ordering نسبی بهترین را نمی‌گیرد. یک pairwise linear ranker سبک اضافه شد تا contract LTR گسترده‌تر شود.

درس: معماری extensible اجازه می‌دهد مدل ranking بدون بازنویسی engine عوض شود.

۲۹. OCR Hook باید واقعاً در مسیر Loader استفاده شود

داشتن parameter در API کافی نیست. در audit مشخص شد باید مطمئن شویم OCR path واقعاً invoke می‌شود.

درس: Public API promise باید integration test داشته باشد.

۳۰. Documentation هم بخشی از Release است

در پایان فقط test suite کافی نبود. Release شامل این‌ها شد:

  • README
  • Architecture
  • Feature Matrix
  • Migration
  • Verification
  • Release Audit
  • Changelog
  • Wheel
  • Checksums

درس: Production readiness فقط source code نیست؛ قابلیت نصب، بررسی و عملیات هم بخشی از محصول است.

نتیجه تست‌های Release چه بود؟

در release نهایی core gates این نتایج را داشتند:

  • 63 test پاس
  • compileall موفق
  • wheel build موفق
  • clean venv install موفق
  • persistence/restart smoke موفق
  • async concurrency stress موفق در workload تست‌شده
  • PDF/DOCX/OpenTelemetry smoke برای dependencyهای موجود

در عین حال external runtimeهایی مثل Qdrant server و بعضی model packageها در همان build environment live test نشده بودند؛ بنابراین release audit صریحاً از ادعای «همه محیط‌ها بدون خطا» خودداری می‌کند.

این نوع مرزبندی برای گزارش فنی بسیار مهم است.

چیزی که در پایان فهمیدیم

فاصله prototype تا production با اضافه کردن یک مدل بزرگ‌تر پر نمی‌شود.

بخش عمده کار مربوط است به:

  • correctness
  • identity
  • versioning
  • security
  • persistence
  • failure recovery
  • observability
  • evaluation
  • operational boundaries

در واقع هرچه retrieval model بهتر می‌شود، اهمیت engineering اطراف آن بیشتر دیده می‌شود.

آیا همه این پیچیدگی برای هر پروژه لازم است؟

خیر.

برای MVP، معماری ساده بهترین انتخاب است. اشتباه این است که MVP را بدون hardening همان‌طور وارد محیط حساس کنیم.

یک مسیر منطقی:

Prototype
-> Evaluation dataset
-> Hybrid retrieval
-> ACL
-> Persistence
-> Observability
-> Failure tests
-> Production backend
-> Load/security testing

هر مرحله باید براساس نیاز واقعی اضافه شود.

جمع‌بندی

مهم‌ترین درس پروژه برای ما این بود:

ساخت RAG آسان است؛ ساخت retrieval قابل اعتماد یک مسئله مهندسی سیستم است.

اگر قرار باشد فقط یک بخش از این تجربه را به تیم‌هایی که تازه شروع می‌کنند منتقل کنیم، این است که از روز اول یک evaluation set واقعی بسازند و invariantهای امنیتی و داده‌ای را مشخص کنند.

مدل و vector database مهم‌اند، اما باگ‌های production معمولاً در فاصله بین اجزا ظاهر می‌شوند.

به‌روزرسانی:

همهٔ نوشته‌ها

نوشته‌های مرتبط