درس ۶ از ۱۱

RAG and Agentic Search

تا اینجا، tool use به Claude اجازه داد action بگیرد. اما در بسیاری از کاربردهای دنیای واقعی، چیزی که نیاز داریم action نیست — knowledge است. کاربر سوالی درباره‌ی سیاست‌های شرکت، یک مقاله‌ی فنی، یا یک گزارش مالی می‌پرسد، و Claude باید بر اساس متن داخلی شما پاسخ دهد، نه بر اساس داده‌ی pre-train. این الگو همان RAG (Retrieval-Augmented Generation) است: ابتدا قطعه‌های مرتبط را از یک corpus جست‌وجو می‌کنیم، سپس آن‌ها را به prompt اضافه می‌کنیم.

این ماژول هفت لکچر دارد و کل pipeline را پوشش می‌دهد: chunking → embedding → جریان کامل RAG → BM25 → multi-index → reranking → contextual retrieval.


۶.۱ Chunking

هدف یادگیری: تقسیم سند به قطعه‌های قابل بازیابی با اندازه و overlap منطقی.

مفاهیم کلیدی: fixed-size chunking، sentence chunking، paragraph chunking، semantic chunking، overlap (۱۰ تا ۲۰ درصد)، token-aware splitting، markdown-section chunking، splitter‌های LangChain / LlamaIndex.

مدل‌های embedding ورودی محدود دارند (مثلاً ۳۲k token برای Voyage 4). سندهای طولانی باید تقسیم شوند. سه خانواده‌ی استراتژی وجود دارد: (۱) fixed-size — مثلاً پنجره‌ی ۸۰۰ token با overlap ۱۰۰ token: ساده، مقاوم، مستقل از زبان. (۲) structural — تقسیم بر اساس heading، paragraph، list item، code block (برای markdown عالی است). (۳) semantic — تقسیم در نقاطی که فاصله‌ی embedding بین جمله‌ها جهش می‌کند (انسجام موضوعی را بهتر حفظ می‌کند، اما گران‌تر است).

راهنمای contextual retrieval در Anthropic توصیه می‌کند که chunk‌های کوچک‌تر (۲۰۰ تا ۵۰۰ token) همراه با context پیش‌فرستاده (لکچر ۶.۷) را به chunk‌های بزرگ ترجیح دهید، چون دقت retrieval با بزرگ‌تر شدن chunk افت می‌کند.

نمونه کد:

def chunk_text(text, chunk_size=600, overlap=100):
    words = text.split()
    chunks = []
    for i in range(0, len(words), chunk_size - overlap):
        chunks.append(" ".join(words[i:i + chunk_size]))
        if i + chunk_size >= len(words): break
    return chunks

اشتباهات رایج: نداشتن overlap (query‌هایی که در مرز chunk افتاده‌اند گم می‌شوند)؛ chunk بسیار بزرگ (retrieval تبدیل می‌شود به «پیدا کردن سند» نه پیدا کردن پاسخ)؛ حفظ نکردن heading در متن chunk (افت context).


۶.۲ Embeddings

هدف یادگیری: تولید بردارهای dense برای جست‌وجوی معنایی با Voyage AI.

مفاهیم کلیدی: embedding، dense vector، cosine similarity، مدل‌های voyage-4، voyage-4-large، voyage-code-3، voyage-finance-2، پارامتر input_type (query در برابر document)، embedding dimension، normalization.

Anthropic مدل embedding خود را عرضه نمی‌کند؛ مستندات صراحتاً Voyage AI را پیشنهاد می‌کنند. مدل پرچم‌دار voyage-4 است (به‌طور پیش‌فرض ۱۰۲۴ بُعد؛ ابعاد ۲۵۶، ۵۱۲، ۲۰۴۸ هم در دسترس‌اند). مدل‌های تخصصی برای code (voyage-code-3)، finance (voyage-finance-2) و legal (voyage-law-2) موجود است. برای embedding کردن corpus حتماً input_type="document" بگذارید و برای embedding کردن query، input_type="query" — چون Voyage prompt متفاوتی به هر کدام می‌چسباند که retrieval را قابل توجه بهتر می‌کند.

embedding‌های Voyage در L2 نرمال‌اند، پس dot product برابر cosine similarity است. برای ذخیره‌سازی، بین float (دقت بالا) و quantization به int8 یا binary (به ترتیب چهار و ۳۲ برابر کوچک‌تر، کاهش جزئی recall) انتخاب کنید.

نمونه کد:

import voyageai, numpy as np
vo = voyageai.Client()

docs = ["The grass is green.", "The sky is blue.", "Apple's call is Nov 2."]
doc_emb = vo.embed(docs, model="voyage-4", input_type="document").embeddings

q = "When does Apple report?"
q_emb = vo.embed([q], model="voyage-4", input_type="query").embeddings[0]

sims = np.dot(doc_emb, q_emb)        # cosine == dot for normalized vectors
print(docs[int(np.argmax(sims))])    # "Apple's call is Nov 2."

اشتباهات رایج: فراموش کردن input_type (افت ۵٪ در retrieval)؛ ترکیب کردن مدل‌های مختلف برای corpus و query (فضای بُرداری ناسازگار)؛ بازساخت ایندکس برای تغییرات کوچک (به‌جای آن، ایندکس versioned داشته باشید).


۶.۳ Full RAG flow

هدف یادگیری: اتصال chunk → embed → store → retrieve → prompt → answer در یک pipeline end-to-end.

مفاهیم کلیدی: vector store (مثل Pinecone، Weaviate، pgvector، Qdrant، FAISS)، top-k retrieval، prompt-stuffing chunk‌های بازیابی شده، تگ XML از نوع <context>، citation.

یک حلقه‌ی RAG کانونی شش مرحله دارد: (۱) chunk کردن corpus، (۲) embedding هر chunk، (۳) ذخیره‌ی بردار + metadata در یک vector DB، (۴) در زمان query، embedding گرفتن از query و اجرای k-NN search (معمولاً k بین ۵ تا ۲۰)، (۵) تزریق top-k به context Claude به‌عنوان یک block از نوع <context>، (۶) درخواست از Claude برای پاسخ فقط بر اساس context (به همراه citation). همیشه نام فایل / URL مبدا را در metadata نگه دارید تا بتوانید citation نشان دهید.

برای محتوای فارسی، normalization را جدی بگیرید: ی در برابر ي، ک در برابر ك، نیم‌فاصله — اگر در زمان index و query هر دو نرمال نشوند، recall سقوط می‌کند.

نمونه کد:

def rag_answer(question, k=5):
    q_emb = vo.embed([question], model="voyage-4", input_type="query").embeddings[0]
    hits = vector_db.search(q_emb, top_k=k)        # returns [{text, source, score}, ...]

    context = "\n\n".join(
        f"<chunk source=\"{h['source']}\">{h['text']}</chunk>" for h in hits
    )
    resp = client.messages.create(
        model="claude-opus-4-8", max_tokens=1024,
        system="Answer using only the provided context. Cite the source for each claim. If the answer is not in the context, say so.",
        messages=[{"role": "user", "content": f"<context>\n{context}\n</context>\n\nQuestion: {question}"}],
    )
    return resp.content[0].text

اشتباهات رایج: تزریق chunk زیادی (هزینه + lost-in-the-middle)؛ نگفتن صریح به Claude که از سوال خارج از context طفره برود (hallucination)؛ حذف مرحله‌ی citation (کاربر نمی‌تواند پاسخ را راستی‌آزمایی کند).


۶.۴ BM25 (lexical retrieval)

هدف یادگیری: افزودن جست‌وجوی keyword کلاسیک برای پوشش موارد exact-match که vector search از دست می‌دهد.

مفاهیم کلیدی: BM25، term frequency، inverse document frequency، کتابخانه‌ی rank_bm25 در Python، stop word، stemming، exact-match retrieval (کد SKU، شناسه‌های فنی، پیام خطا، شماره‌ی regulation).

vector search در exact-match‌ها ضعیف است: کد SKU، error message، نام function، شماره‌ی قانون. BM25 — الگوریتم کلاسیک IR — این موارد را خوب می‌گیرد. الگوی استاندارد hybrid search است: BM25 و vector search را موازی اجرا کنید، top-k از هر کدام بگیرید، با reciprocal rank fusion (RRF) ترکیب کنید. مقاله‌ی contextual retrieval در Anthropic نشان می‌دهد hybrid به‌تنهایی شکست‌های top-20 retrieval را حدود ۴۹٪ نسبت به vector-only کاهش می‌دهد.

برای فارسی، قبل از feed به BM25 با hazm یا parsivar tokenize کنید (ZWNJ و prefix‌ها را می‌فهمد)؛ tokenizer انگلیسی off-the-shelf morphology فارسی را نابود می‌کند.

نمونه کد:

from rank_bm25 import BM25Okapi
tokenized_corpus = [doc.lower().split() for doc in corpus]
bm25 = BM25Okapi(tokenized_corpus)

def hybrid_search(query, k=10):
    bm25_hits = bm25.get_top_n(query.lower().split(), corpus, n=k)
    vec_hits = [corpus[i] for i in vector_topk(query, k)]
    # Reciprocal rank fusion
    scores = {}
    for rank, doc in enumerate(bm25_hits):
        scores[doc] = scores.get(doc, 0) + 1 / (60 + rank)
    for rank, doc in enumerate(vec_hits):
        scores[doc] = scores.get(doc, 0) + 1 / (60 + rank)
    return sorted(scores, key=scores.get, reverse=True)[:k]

اشتباهات رایج: استفاده از stop-word list انگلیسی روی فارسی؛ به‌جای fusion، فقط concat کردن دو لیست (precision افت می‌کند)؛ به‌روزرسانی نکردن هر دو ایندکس وقتی corpus عوض می‌شود.


۶.۵ Multi-index pipeline

هدف یادگیری: ترکیب BM25 + vector + (اختیاری) فیلتر keyword در یک pipeline orchestrated.

مفاهیم کلیدی: hybrid retrieval، metadata filter، namespace، multi-stage retrieval، اندازه‌ی candidate pool، k1 (اولیه)، k2 (پس از rerank).

RAG محصولاتی به‌ندرت از یک ایندکس استفاده می‌کند. چیدمان معمول: (الف) metadata pre-filter (tenant_id == X AND language == "fa") برای محدود کردن دامنه؛ (ب) BM25 + vector به‌صورت موازی برای candidate pool (k1 بین ۵۰ تا ۱۰۰)؛ (ج) rerank به top-k2 (۵ تا ۱۰)؛ (د) prompt-stuffing. هر مرحله trade-off متفاوتی بین هزینه و کیفیت دارد. ایندکس برداری کوچک و سریع است اما کم‌دقت؛ BM25 روی term دقیق برنده است؛ rerank گران اما باکیفیت‌ترین مرحله است.

multi-tenancy و ACL در metadata filter پیاده می‌شود — هرگز روی این تکیه نکنید که embedding model «بفهمد» چه کسی به چه چیزی دسترسی دارد.

نمونه کد:

def multi_stage(query, tenant_id, k1=80, k2=8):
    # Stage 1: metadata pre-filter + hybrid retrieval
    filters = {"tenant_id": tenant_id, "language": "fa"}
    bm25_hits = bm25_search(query, filters=filters, k=k1)
    vec_hits = vector_search(query, filters=filters, k=k1)
    candidates = rrf_merge(bm25_hits, vec_hits, k=k1)
    # Stage 2: rerank
    return rerank(query, candidates, k=k2)

اشتباهات رایج: حذف metadata filter (نشت داده‌ی tenant)؛ k1 خیلی کوچک (rerank نمی‌تواند جبران کند)؛ k1 خیلی بزرگ (هزینه‌ی rerank منفجر می‌شود).


۶.۶ Reranking

هدف یادگیری: استفاده از یک cross-encoder reranker (مثل Voyage rerank-2.5 یا Cohere rerank-3) برای reorder کردن candidate‌ها بر اساس ارتباط query-document.

مفاهیم کلیدی: cross-encoder، bi-encoder، voyage-rerank-2.5، rerank-3 (Cohere)، scoring جفت query-document، MRR / NDCG، stage-2 retrieval.

embedding‌ها bi-encoder هستند — query و document را جداگانه encode می‌کنند، پس کیفیت retrieval توسط هندسه‌ی فضای مشترک محدود می‌شود. cross-encoder (یا reranker) جفت (query، document) را با هم از یک transformer می‌گذراند و یک score ارتباط تولید می‌کند که تعاملات قابل‌توجهی را ثبت می‌کند که bi-encoder نمی‌تواند. trade-off: چندین مرتبه‌ی بزرگی کندتر برای هر pair، پس فقط top-50 تا ۱۰۰ را rerank می‌کنید نه کل corpus. مطالعه‌ی contextual retrieval در Anthropic نشان می‌دهد reranking کاهش شکست retrieval را از ۴۹٪ (hybrid) به ۶۷٪ می‌رساند.

نمونه کد:

import voyageai
vo = voyageai.Client()

def rerank(query, candidates, k=8):
    documents = [c["text"] for c in candidates]
    res = vo.rerank(
        query=query, documents=documents, model="voyage-rerank-2.5", top_k=k,
    )
    return [candidates[r.index] for r in res.results]

اشتباهات رایج: rerank کردن کل corpus (انفجار هزینه)؛ pin نکردن snapshot مدل rerank؛ نادیده گرفتن خود score (می‌توانید با threshold، نتایج کم‌ربط را فیلتر کنید).


۶.۷ Contextual retrieval

هدف یادگیری: prepend کردن context اختصاصی هر chunk (تولید شده توسط Claude) قبل از embedding، که کیفیت retrieval را به‌شدت بالا می‌برد.

مفاهیم کلیدی: contextual chunk، contextual embeddings، contextual BM25، prompt caching برای تولید context، کاهش شکست ۴۹٪ / ۶۷٪، هزینه‌ی حدود ۱.۰۲ دلار به ازای هر یک میلیون token با cache.

یک chunk برهنه مثل «درآمد شرکت ۳٪ نسبت به سال قبل رشد کرد» تقریباً غیرقابل بازیابی است — هیچ سیگنالی از کدام شرکت یا کدام سال در آن نیست. روش contextual retrieval در Anthropic چنین است: برای هر chunk، از Claude بخواهید یک preamble ۵۰ تا ۱۰۰ token بسازد که chunk را در سند جا می‌اندازد، و آن را قبل از embedding (و قبل از index در BM25) می‌چسبانید. Anthropic prompt کانونی این کار را منتشر کرده:

«Here is the chunk we want to situate within the whole document … Please give a short succinct context to situate this chunk within the overall document for the purposes of improving search retrieval of the chunk.»

نقطه‌ی break-through اینجاست که با prompt caching کل سند را یک‌بار cache می‌کنید و بعد روی chunk‌ها iterate می‌کنید؛ هزینه به حدود ۱.۰۲ دلار به ازای هر یک میلیون token سند می‌رسد. نتایج ترکیبی: contextual embeddings به‌تنهایی ۳۵٪ کاهش شکست در top-20 می‌دهد؛ به‌علاوه‌ی contextual BM25 → ۴۹٪؛ به‌علاوه‌ی reranking → ۶۷٪.

نمونه کد:

CONTEXTUALIZE_PROMPT = """\
<document>
{full_document}
</document>

Here is the chunk we want to situate within the whole document:
<chunk>
{chunk}
</chunk>

Please give a short succinct context to situate this chunk within the overall document
for the purposes of improving search retrieval of the chunk. Answer only with the succinct
context and nothing else."""

def contextualize_chunk(full_doc, chunk):
    resp = client.messages.create(
        model="claude-haiku-4-5", max_tokens=200,
        system=[{
            "type": "text",
            "text": CONTEXTUALIZE_PROMPT.format(full_document=full_doc, chunk=chunk),
            "cache_control": {"type": "ephemeral"},   # cache the document
        }],
        messages=[{"role": "user", "content": "Generate context."}],
    )
    return resp.content[0].text

contextual_chunk = contextualize_chunk(doc, chunk) + "\n\n" + chunk
embedding = vo.embed([contextual_chunk], model="voyage-4", input_type="document").embeddings[0]

اشتباهات رایج: فراموش کردن cache_control (هزینه از یک به ۲۰ دلار به ازای هر یک میلیون token سند می‌رود)؛ استفاده از متن contextual فقط برای embedding (آن را به BM25 هم بدهید — همان جاست که contextual BM25 کمک می‌کند)؛ regenerate کردن context برای هر chunk در هر re-index (نرخ cache hit مهم است).

فصل ۴ — بخش دوم: ساخت با Claude API (Modules 7–11)

پیش‌نیاز: بخش اول این فصل (Modules 1–6) — Authentication، Messages API، Prompt Engineering، Evaluation، Tool use، RAG. مدت تخمینی این بخش: ۵ تا ۶ ساعت مخاطب: Developer / Tech Lead / AI engineer

این بخش دوم فصل ۴ است. بخش اول (در فایل 04-building-claude-api-fa-part1.md) تا پایان Module 6 را پوشش می‌دهد. در ادامه از Module 7 آغاز می‌کنیم — یعنی نقطه‌ای که از «ابزارهای پایه» عبور می‌کنیم و وارد قابلیت‌های پیشرفته‌ی Claude، پروتکل MCP، اپ‌های رسمی Anthropic، الگوهای agents و workflows، و در نهایت یک چک‌لیست عملی production می‌شویم.