GeekAlerts

جایی برای گیک‌ها

مهندسی کانتکست (Context Engineering) در LLM، فراتر از پرامپت‌نویسی ساده

مهندسی کانتکست (Context Engineering) در LLM، فراتر از پرامپت‌نویسی ساده

شاید فکر کنی نوشتن یه دستور یا پرامپت ساده برای مدل‌های زبانی بزرگ (LLM) مثل چت جی‌پی‌تی آخر خطه، اما واقعیت اینه که یه دنیای فاصله بین این کار و ساختن یه اپلیکیشن درست و حسابی و کارآمد وجود داره. اینجاست که مفهومی به اسم «مهندسی کانتکست» (Context Engineering) وارد میدون میشه. این عبارت یه جورایی یه چتره که زیرش کلی هنر و علم ریز و درشت برای جا دادن اطلاعات توی پنجره کانتکست یه مدل زبانی قرار میگیره؛ یعنی همون فضایی که مدل برای انجام دادن یه کار ازش استفاده میکنه.

مرز دقیق اینکه مهندسی کانتکست از کجا شروع میشه و کجا تموم میشه، جای بحث داره. اما اگه به یکی از توییت‌های «آندری کارپاتی» (Andrej Karpathy) نگاه کنیم، میشه چند تا نکته کلیدی رو از دلش بیرون کشید:

  • این مفهوم فقط به معنی مهندسی پرامپت‌های تکی و اتمی نیست؛ یعنی اون حالتی که شما یه سوال از مدل میپرسی و یه جواب میگیری، فراتر از این حرف‌هاست.
  • یه رویکرد کاملا جامع و کلیه که یه مسئله بزرگ رو به چند تا مسئله کوچیک‌تر میشکنه.
  • این مسئله‌های کوچیک‌تر رو میشه با چند تا مدل زبانی (یا به قول معروف، ایجنت) به صورت جداگونه حل کرد. به هر کدوم از این ایجنت‌ها، کانتکست یا زمینه اطلاعاتی مناسب برای انجام وظیفه‌شون داده میشه.
  • هر ایجنت میتونه بسته به پیچیدگی کارش، توانایی و اندازه متفاوتی داشته باشه. مثلا برای یه کار ساده از یه مدل کوچیک‌تر و برای یه کار پیچیده از یه مدل قوی‌تر استفاده میشه.
  • مراحل میانی‌ای وجود داره که هر ایجنت برای تموم کردن کارش طی میکنه. کانتکست فقط اطلاعاتی نیست که ما به سیستم میدیم؛ بلکه شامل توکن‌های میانی هم میشه که مدل‌های زبانی موقع تولید جواب میبینن (مثل مراحل استدلال، نتایج ابزارها و غیره).
  • این ایجنت‌ها با جریان‌های کنترلی به هم وصل شدن و ما دقیقا مشخص می‌کنیم که اطلاعات چطوری توی سیستم ما جریان پیدا کنه.
  • اطلاعاتی که در اختیار ایجنت‌ها قرار میگیره، میتونه از منابع مختلفی بیاد؛ مثل دیتابیس‌های خارجی با روش «تولید مبتنی بر بازیابی اطلاعات» (RAG)، فراخوانی ابزارها (مثل جستجوی وب)، سیستم‌های حافظه، یا مثال‌های چند شاتی کلاسیک.
  • ایحنت‌ها میتونن موقع تولید جواب، دست به کار بشن و اقداماتی انجام بدن. هر اقدامی که یه ایجنت میتونه انجام بده باید خیلی خوب تعریف شده باشه تا مدل زبانی بتونه از طریق استدلال و عمل باهاش تعامل کنه.
  • علاوه بر همه اینها، سیستم‌ها باید با معیارهای مشخصی ارزیابی بشن و با قابلیت مشاهده‌پذیری (observability) نگهداری بشن. نظارت روی مصرف توکن، تاخیر (latency)، هزینه و کیفیت خروجی یه نکته کلیدی به حساب میاد.

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

چرا همه چیز رو همینطوری به مدل زبانی ندیم؟

تحقیقات نشون داده که چپوندن هر تیکه اطلاعاتی توی کانتکست یه مدل زبانی، اصلا کار ایده‌آلی نیست. با اینکه خیلی از مدل‌های پیشرفته ادعا میکنن که از پنجره‌های کانتکست طولانی پشتیبانی میکنن، اما هنوزم با مشکلاتی مثل «مسمومیت کانتکست» (context poisoning) یا «پوسیدگی کانتکست» (context rot) دست و پنجه نرم میکنن.

وقتی اطلاعات غیرضروری زیادی توی کانتکست یه مدل زبانی باشه، میتونه درک مدل رو آلوده کنه، باعث بشه مدل چیزهایی رو از خودش دربیاره که واقعیت ندارن (hallucination)، و در نهایت عملکرد ضعیفی از خودش نشون بده.

دقیقا به همین دلیله که فقط داشتن یه پنجره کانتکست بزرگ کافی نیست. ما به رویکردهای سیستماتیک برای مهندسی کانتکست نیاز داریم.

چرا DSPy؟

برای این آموزش، فریمورک DSPy انتخاب شده. دلیل این انتخاب به زودی توضیح داده میشه، اما این اطمینان وجود داره که مفاهیمی که اینجا ارائه میشه، تقریبا برای هر فریمورک پرامپت‌نویسی دیگه‌ای، حتی نوشتن پرامپت به زبان ساده انگلیسی، کاربرد داره.

DSPy یه فریمورک «تعریفی» (declarative) برای ساختن نرم‌افزارهای هوش مصنوعی ماژولاره. این فریمورک تونسته دو جنبه کلیدی هر وظیفه مربوط به مدل‌های زبانی رو به خوبی از هم جدا کنه:
الف) قراردادهای ورودی و خروجی که به یه ماژول داده میشه.
ب) منطقی که نحوه جریان اطلاعات رو کنترل میکنه.

بیا یه مثال ببینیم!
فرض کن میخوایم از یه مدل زبانی برای نوشتن یه جوک استفاده کنیم. به طور مشخص، میخوایم که یه مقدمه (setup)، یه بخش اصلی و خنده‌دار (punchline)، و اجرای کامل جوک با صدای یه کمدین رو برامون تولید کنه.
یه چیز دیگه هم میخوایم: خروجی باید در فرمت JSON باشه تا بعدا بتونیم فیلدهای مختلف اون دیکشنری رو پردازش کنیم. مثلا شاید بخوایم پانچ‌لاین جوک رو روی یه تیشرت چاپ کنیم (فرض کن یه نفر قبلا یه تابع راحت برای این کار نوشته).

با روش‌های معمولی، کد ما میتونه یه چیزی شبیه این باشه:

system_prompt = """
You are a comedian who tells jokes, you are always funny.
Generate the setup, punchline, and full delivery in the comedian's voice.
Output in the following JSON format:
{
"setup": <str>,
"punchline": <str>,
"delivery": <str>
}
Your response should be parsable withou errors in Python using json.loads().
"""
client = openai.Client()
response = client.chat.completions.create(
    model="gpt-4o-mini",
    temperature = 1,
    messages=[
        {"role": "system", "content": system_prompt,
        {"role": "user", "content": "Write a joke about AI"}
    ]
)
joke = json.loads(response.choices[0].message.content) # Hope for the best
print_on_a_tshirt(joke["punchline"])

توجه کردی که ما چطوری داریم جواب مدل زبانی رو پردازش می‌کنیم تا دیکشنری رو ازش استخراج کنیم؟ چی میشه اگه یه اتفاق «بد» بیفته، مثلا مدل نتونه جواب رو توی فرمت درخواستی ما تولید کنه؟ کل کد ما به مشکل میخوره و دیگه هیچ چاپی روی هیچ تیشرتی انجام نمیشه!

گسترش دادن کد بالا هم کار سختیه. مثلا اگه میخواستیم مدل قبل از تولید جواب، استدلال زنجیره‌ای (chain of thought) انجام بده، باید یه منطق اضافه برای پردازش درست اون متن استدلال مینوشتیم.
علاوه بر این، نگاه کردن به پرامپت‌های ساده انگلیسی مثل این و فهمیدن اینکه ورودی‌ها و خروجی‌های این سیستم‌ها چی هستن، میتونه سخت باشه. DSPy همه این مشکلات رو حل میکنه. بیا همین مثال رو با استفاده از DSPy بنویسیم.

class JokeGenerator(dspy.Signature):
    """You're a comedian who tells jokes. You're always funny."""
    query: str = dspy.InputField()
    setup: str = dspy.OutputField()
    punchline: str = dspy.OutputField()
    delivery: str = dspy.OutputField()

joke_gen = dspy.Predict(JokeGenerator)
joke_gen.set_lm(lm=dspy.LM("openai/gpt-4.1-mini", temperature=1))
result = joke_gen(query="Write a joke about AI")

print(result)
print_on_a_tshirt(result.punchline)

این رویکرد به شما خروجی‌های ساختاریافته و قابل پیش‌بینی میده که میتونید به صورت برنامه‌نویسی باهاشون کار کنید و دیگه نیازی به پردازش با عبارات منظم (regex) یا دستکاری رشته‌های مستعد خطا ندارید.

«امضا»های (Signatures) فریمورک DSPy به صراحت شما رو مجبور میکنن که تعریف کنید ورودی‌های سیستم چی هستن (توی مثال بالا «query»)، و خروجی‌های سیستم چی هستن (setup، punchline و delivery) و نوع داده‌شون چیه. این امضاها حتی به مدل زبانی میگن که شما میخواید این خروجی‌ها به چه ترتیبی تولید بشن.

اون بخش dspy.Predict یه مثال از یه ماژول (Module) توی DSPy هست. با ماژول‌ها، شما تعریف می‌کنید که مدل زبانی چطوری ورودی‌ها رو به خروجی‌ها تبدیل میکنه. dspy.Predict ساده‌ترین نوع ماژوله. شما میتونید کوئری رو بهش پاس بدید، مثل joke_gen(query="Write a joke about AI") و اون یه پرامپت ساده برای فرستادن به مدل زبانی درست میکنه. در پشت صحنه، DSPy فقط یه پرامپت میسازه که در ادامه میبینید.

وقتی مدل زبانی جواب میده، DSPy آبجکت‌هایی از نوع Pydantic BaseModel میسازه که به صورت خودکار اعتبار‌سنجی اسکما رو انجام میدن و خروجی رو برمیگردونن. اگه موقع این اعتبارسنجی خطایی پیش بیاد، DSPy به صورت خودکار سعی میکنه با فرستادن دوباره پرامپت به مدل، اونها رو برطرف کنه و به این ترتیب ریسک از کار افتادن برنامه رو به شکل چشمگیری کاهش میده.

یه موضوع رایج دیگه توی مهندسی کانتکست، «زنجیره فکر» (Chain of Thought) هست. اینجا ما از مدل زبانی میخوایم که قبل از ارائه جواب نهایی، متن استدلال خودش رو تولید کنه. این کار باعث میشه کانتکست مدل با استدلال خودساخته‌اش پر بشه، قبل از اینکه توکن‌های جواب نهایی رو تولید کنه.

برای انجام این کار، کافیه توی مثال بالا dspy.Predict رو با dspy.ChainOfThought جایگزین کنی. بقیه کد دقیقا همون باقی میمونه. حالا میتونی ببینی که مدل زبانی قبل از فیلدهای خروجی تعریف شده، یه بخش استدلال هم تولید میکنه.

تعاملات چند مرحله‌ای و جریان‌های کاری ایجنتی

بهترین بخش رویکرد DSPy اینه که چطوری وابستگی‌های سیستم (یعنی همون امضاها یا Signatures) رو از جریان‌های کنترلی (یعنی ماژول‌ها یا Modules) جدا میکنه. این ویژگی نوشتن کد برای تعاملات چند مرحله‌ای رو خیلی راحت (و حتی جالب) میکنه. توی این بخش، میبینیم که چطوری میشه چند تا جریان کاری ایجنتی ساده ساخت.

پردازش متوالی (Sequential Processing)

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

این یک رویکرد جامع است که یک مسئله بزرگ را به چندین مسئله کوچکتر تقسیم می‌کند.

بیا با همون مثال تولید جوک ادامه بدیم. ما میتونیم به راحتی دو تا زیرمسئله ازش جدا کنیم. تولید ایده یه کاره، و ساختن جوک یه کار دیگه.
پس بیا دو تا ایجنت داشته باشیم. ایجنت اول از یه کوئری، یه ایده جوک (شامل مقدمه و پانچ‌لاین) تولید میکنه. بعد یه ایجنت دوم از روی این ایده، خود جوک رو میسازه.

هر ایجنت می‌تواند بسته به پیچیدگی وظیفه، قابلیت و اندازه مناسبی داشته باشد.

ما اینجا ایجنت اول رو با مدل gpt-4.1-mini و ایجنت دوم رو با مدل قوی‌تر gpt-4.1 اجرا می‌کنیم.
توجه کن که ما ماژول dspy.Module خودمون رو به اسم JokeGenerator نوشتیم. اینجا از دو تا ماژول جداگونه dspy استفاده می‌کنیم: query_to_idea و idea_to_joke تا کوئری اصلی ما رو اول به یه JokeIdea و بعد در نهایت به یه جوک تبدیل کنن (همونطور که تو تصویر نشون داده شده).

class JokeIdea(BaseModel):
    setup: str
    contradiction: str
    punchline: str

class QueryToIdea(dspy.Signature):
    """Generate a joke idea with setup, contradiction, and punchline."""
    query = dspy.InputField()
    joke_idea: JokeIdea = dspy.OutputField()

class IdeaToJoke(dspy.Signature):
    """Convert a joke idea into a full comedian delivery."""
    joke_idea: JokeIdea = dspy.InputField()
    joke = dspy.OutputField()

class JokeGenerator(dspy.Module):
    def __init__(self):
        self.query_to_idea = dspy.Predict(QueryToIdea)
        self.idea_to_joke = dspy.Predict(IdeaToJoke)
        self.query_to_idea.set_lm(lm=dspy.LM("openai/gpt-4.1-mini"))
        self.idea_to_joke.set_lm(lm=dspy.LM("openai/gpt-4.1"))

    def forward(self, query):
        idea = self.query_to_idea(query=query)
        joke = self.idea_to_joke(joke_idea=idea.joke_idea)
        return joke

بهبود تکراری (Iterative Refinement)

شما همچنین میتونید بهبود تکراری رو پیاده‌سازی کنید؛ جایی که مدل زبانی روی خروجی‌های خودش فکر میکنه و اونها رو اصلاح میکنه. برای مثال، ما میتونیم یه ماژول اصلاح بنویسیم که کانتکستش، خروجی یه مدل زبانی قبلی باشه و باید به عنوان یه ارائه‌دهنده بازخورد عمل کنه. بعد مدل اول میتونه این بازخورد رو به عنوان ورودی بگیره و به صورت تکراری جوابش رو بهتر کنه.

انشعاب شرطی و سیستم‌های چند خروجی

ایجنت‌ها با جریان‌های کنترلی به هم متصل هستند و ما دقیقا نحوه جریان اطلاعات در سیستم خود را تنظیم می‌کنیم.

گاهی وقتا شما میخوای که ایجنتت چند تا نسخه مختلف از یه چیز رو تولید کنه و بعد بهترینش رو از بین اونها انتخاب کنه. بیا یه مثال از این حالت ببینیم.
اینجا ما اول یه «داور جوک» تعریف کردیم. این داور چند تا ایده جوک رو به عنوان ورودی میگیره و بعد ایندکس بهترین جوک رو انتخاب میکنه. این جوک انتخاب شده بعدا به بخش بعدی پاس داده میشه.

num_samples = 5

class JokeJudge(dspy.Signature):
    """Given a list of joke ideas, you must pick the best joke"""
    joke_ideas: list[JokeIdeas] = dspy.InputField()
    best_idx: int = dspy.OutputField(
        le=num_samples,
        ge=1,
        description="The index of the funniest joke")

class ConditionalJokeGenerator(dspy.Module):
    def __init__(self):
        self.query_to_idea = dspy.ChainOfThought(QueryToIdea)
        self.judge = dspy.ChainOfThought(JokeJudge)
        self.idea_to_joke = dspy.ChainOfThought(IdeaToJoke)

    async def forward(self, query):
        # Generate multiple ideas in parallel
        ideas = await asyncio.gather(*[
            self.query_to_idea.acall(query=query)
            for _ in range(num_samples)
        ])
        
        # Judge and rank ideas
        best_idx = (await self.judge.acall(joke_ideas=ideas)).best_idx
        
        # Select best idea and generate final joke
        best_idea = ideas[best_idx]
        
        # Convert from idea to joke
        return await self.idea_to_joke.acall(joke_idea=best_idea)

فراخوانی ابزار (Tool Calling)

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

  • یه توضیح که بگه این تابع چیکار میکنه.
  • یه لیست از ورودی‌ها و نوع داده‌هاشون.

بیا یه مثال از گرفتن اخبار ببینیم. ما اول یه تابع ساده پایتون مینویسیم که توش از Tavily استفاده می‌کنیم. این تابع یه کوئری جستجو رو به عنوان ورودی میگیره و مقالات خبری اخیر از ۷ روز گذشته رو پیدا میکنه.

client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))

def fetch_recent_news(query: str) -> str:
    """Inputs a query string, searches for news and returns top results."""
    response = tavily_client.search(query, search_depth="advanced",
                                    topic="news", days=7, max_results=3)
    return [x["content"] for x in response["results"]]

حالا بیا از dspy.ReAct (یا همون REasoning and ACTing به معنی استدلال و عمل) استفاده کنیم. این ماژول به صورت خودکار در مورد کوئری کاربر استدلال میکنه، تصمیم میگیره که کی و کدوم ابزار رو فراخوانی کنه و نتایج ابزار رو توی جواب نهایی جا میده. انجام این کار خیلی راحته:

class HaikuGenerator(dspy.Signature):
    """
    Generates a haiku about the latest news on the query.
    Also create a simple file where you save the final summary.
    """
    query = dspy.InputField()
    summary = dspy.OutputField(desc="A summary of the latest news")
    haiku = dspy.OutputField()

program = dspy.ReAct(signature=HaikuGenerator,
                     tools=[fetch_recent_news],
                     max_iters=2)

program.set_lm(lm=dspy.LM("openai/gpt-4.1", temperature=0.7))
pred = program(query="OpenAI")

وقتی کد بالا اجرا میشه، مدل زبانی اول در مورد چیزی که کاربر میخواد و اینکه کدوم ابزار رو (اگه نیازه) فراخوانی کنه، استدلال میکنه. بعد اسم تابع و آرگومان‌های لازم برای فراخوانی اون تابع رو تولید میکنه.
ما تابع خبر رو با آرگومان‌های تولید شده فراخوانی می‌کنیم، تابع رو اجرا می‌کنیم تا داده‌های خبری تولید بشن. این اطلاعات دوباره به مدل زبانی پاس داده میشه. مدل زبانی تصمیم میگیره که آیا ابزارهای بیشتری رو فراخوانی کنه یا کار رو «تموم» کنه. اگه مدل استدلال کنه که اطلاعات کافی برای جواب دادن به درخواست اصلی کاربر رو داره، گزینه تموم کردن رو انتخاب میکنه و جواب رو تولید میکنه.

ایجنت‌ها می‌توانند در حین تولید پاسخ، اقداماتی انجام دهند. هر اقدامی که ایجنت می‌تواند انجام دهد باید به خوبی تعریف شود تا مدل زبانی بتواند از طریق استدلال و عمل با آن تعامل کند.

استفاده پیشرفته از ابزار — Scratchpad و ورودی/خروجی فایل

یه استاندارد در حال تحول برای اپلیکیشن‌های مدرن اینه که به مدل‌های زبانی اجازه دسترسی به فایل سیستم داده بشه. اینطوری اونها میتونن فایل‌ها رو بخونن و بنویسن، بین دایرکتوری‌ها جابجا بشن (البته با محدودیت‌های مناسب)، متن رو داخل فایل‌ها جستجو کنن (grep) و حتی دستورات ترمینال رو اجرا کنن!
این الگو کلی امکانات جدید باز میکنه. این کار مدل زبانی رو از یه تولیدکننده متن منفعل به یه ایجنت فعال تبدیل میکنه که میتونه کارهای پیچیده و چند مرحله‌ای رو مستقیما توی محیط کاربر انجام بده. برای مثال، فقط نمایش لیست ابزارهای موجود برای Gemini CLI یه مجموعه کوتاه اما فوق‌العاده قدرتمند از ابزارها رو نشون میده.

یه توضیح کوتاه در مورد سرورهای MCP

یه پارادایم جدید دیگه تو فضای سیستم‌های ایجنتی، سرورهای MCP هستن. MCPها به یه مقاله جدا برای خودشون نیاز دارن، پس اینجا خیلی وارد جزئیاتشون نمیشیم.
این روش به سرعت به یه راه استاندارد صنعتی برای ارائه ابزارهای تخصصی به مدل‌های زبانی تبدیل شده. این روش از معماری کلاسیک کلاینت-سرور پیروی میکنه که توش مدل زبانی (کلاینت) یه درخواست به سرور MCP میفرسته، و سرور MCP کار درخواستی رو انجام میده و نتیجه رو برای پردازش‌های بعدی به مدل زبانی برمیگردونه. MCPها برای مهندسی کانتکست مثال‌های خاص عالی هستن، چون شما میتونید فرمت‌های پرامپت سیستمی، منابع، دسترسی محدود به دیتابیس و غیره رو برای اپلیکیشن‌تون تعریف کنید.
یه ریپازیتوری وجود داره که لیست خوبی از سرورهای MCP رو داره و میتونید برای اتصال اپلیکیشن‌های مدل زبانی خودتون به طیف گسترده‌ای از برنامه‌ها، اونها رو مطالعه کنید.

تولید مبتنی بر بازیابی اطلاعات (RAG)

تولید مبتنی بر بازیابی اطلاعات یا RAG (Retrieval Augmented Generation) به یکی از پایه‌های اصلی توسعه اپلیکیشن‌های هوش مصنوعی مدرن تبدیل شده. این یه رویکرد معماریه که اطلاعات خارجی، مرتبط و به‌روز رو به مدل‌های زبانی بزرگ تزریق میکنه؛ اطلاعاتی که به صورت کانتکستی به کوئری کاربر مرتبطه.
پایپ‌لاین‌های RAG از دو فاز تشکیل شدن: پیش‌پردازش و فاز استنتاج. موقع پیش‌پردازش، ما مجموعه داده‌های مرجع رو پردازش می‌کنیم و اون رو در یه فرمت قابل کوئری گرفتن ذخیره می‌کنیم. توی فاز استنتاج، ما کوئری کاربر رو پردازش می‌کنیم، اسناد مرتبط رو از دیتابیسمون بازیابی می‌کنیم و اونها رو به مدل زبانی میدیم تا جواب رو تولید کنه.

اطلاعات در دسترس ایجنت‌ها می‌تواند از منابع متعددی باشد – پایگاه داده خارجی با تولید مبتنی بر بازیابی اطلاعات (RAG)، فراخوانی ابزار (مانند جستجوی وب)، سیستم‌های حافظه، یا مثال‌های کلاسیک چند شاتی.

ساختن سیستم‌های RAG کار پیچیده‌ایه و کلی تحقیق و بهینه‌سازی مهندسی عالی انجام شده که زندگی رو راحت‌تر کرده.

چند نکته عملی برای یه RAG خوب

  • موقع پیش‌پردازش، برای هر تکه (chunk) از داده، متادیتا (metadata) اضافی تولید کنید. این میتونه به سادگی «سوالاتی که این تکه جواب میده» باشه. وقتی تکه‌ها رو توی دیتابیس‌تون ذخیره می‌کنید، این متادیتای تولید شده رو هم ذخیره کنید!
class ChunkAnnotator(dspy.Signature):
    chunk: str = dspy.InputField()
    possible_questions: list[str] = dspy.OutputField(
        description="list of questions that this chunk answers"
    )
  • بازنویسی کوئری (Query Rewriting): استفاده مستقیم از کوئری کاربر برای بازیابی اطلاعات توی RAG اغلب ایده بدیه. کاربرا چیزای خیلی تصادفی‌ای مینویسن که ممکنه با توزیع متن توی مجموعه داده شما همخونی نداشته باشه. بازنویسی کوئری همون کاری رو میکنه که از اسمش پیداست: کوئری رو «بازنویسی» میکنه. شاید گرامرش رو درست کنه، غلط‌های املاییش رو بگیره، با توجه به مکالمات قبلی بهش کانتکست بده یا حتی کلمات کلیدی اضافه‌ای بهش اضافه کنه که کوئری زدن رو راحت‌تر کنه.
class QueryRewriting(dspy.Signature):
    user_query: str = dspy.InputField()
    conversation: str = dspy.InputField(
        description="The conversation so far")
    modified_query: str = dspy.OutputField(
        description="a query contextualizing the user query with the conversation's context and optimized for retrieval search"
)
  • HYDE یا Hypothetical Document Embedding یه نوع سیستم بازنویسی کوئریه. توی HYDE، ما یه جواب مصنوعی (یا فرضی) از دانش داخلی مدل زبانی تولید می‌کنیم. این جواب اغلب شامل کلمات کلیدی مهمیه که سعی میکنه مستقیما با دیتابیس جواب‌ها مطابقت پیدا کنه. بازنویسی کوئری معمولی برای جستجو توی یه دیتابیس از سوالات عالیه، و HYDE برای جستجو توی یه دیتابیس از جواب‌ها عالیه.
  • جستجوی ترکیبی (Hybrid search) تقریبا همیشه بهتر از جستجوی صرفا معنایی یا صرفا مبتنی بر کلمات کلیدیه. برای جستجوی معنایی، میشه از جستجوی نزدیک‌ترین همسایه با شباهت کسینوسی و امبدینگ‌های برداری استفاده کرد. و برای جستجوی مبتنی بر کلیدواژه، از BM25 استفاده میشه.
  • RRF: شما میتونید چندین استراتژی برای بازیابی اسناد انتخاب کنید و بعد از «ترکیب رتبه متقابل» یا reciprocal rank fusion (RRF) برای ترکیب اونها در یک لیست واحد استفاده کنید!
  • جستجوی چند مرحله‌ای (Multi-Hop Search) هم اگه بتونید تاخیر اضافی رو تحمل کنید، یه گزینه قابل بررسیه. اینجا، شما اسناد بازیابی شده رو دوباره به مدل زبانی میدید تا کوئری‌های جدیدی تولید کنه، که از اونها برای انجام جستجوهای اضافی روی دیتابیس استفاده میشه.
class MultiHopHyDESearch(dspy.Module):
    def __init__(self, retriever):
        self.generate_queries = dspy.ChainOfThought(QueryGeneration)
        self.retriever = retriever

    def forward(self, query, n_hops=3):
        results = []
        for hop in range(n_hops): # توجه کنید که ما چندین بار حلقه می‌زنیم
            # Generate optimized search queries
            search_queries = self.generate_queries(
                query=query,
                previous_jokes=retrieved_jokes
            )
            # Retrieve using both semantic and keyword search
            semantic_results = self.retriever.semantic_search(
                search_queries.semantic_query
            )
            bm25_results = self.retriever.bm25_search(
                search_queries.bm25_query
            )
            # Fuse results
            hop_results = reciprocal_rank_fusion([
                semantic_results, bm25_results
            ])
            results.extend(hop_results)
        return results
  • ارجاعات (Citations): وقتی از مدل زبانی میخوایم که از روی اسناد بازیابی شده جواب تولید کنه، میتونیم ازش بخوایم که به اسنادی که مفید تشخیص داده هم ارجاع بده. این کار به مدل اجازه میده که اول یه برنامه از اینکه چطوری میخواد از محتوای بازیابی شده استفاده کنه، تولید کنه.
  • حافظه (Memory): اگه دارید یه چت‌بات میسازید، مهمه که به مسئله حافظه فکر کنید. شما میتونید حافظه رو ترکیبی از بازیابی اطلاعات و فراخوانی ابزار تصور کنید. یه سیستم شناخته شده، سیستم Mem0 هست. مدل زبانی داده‌های جدید رو مشاهده میکنه و ابزارهایی رو فراخوانی میکنه تا تصمیم بگیره که آیا لازمه حافظه موجودش رو اضافه یا اصلاح کنه. موقع جواب دادن به سوال، از RAG برای بازیابی خاطرات مرتبط و تولید جواب استفاده میکنه.

بهترین شیوه‌ها و ملاحظات تولید

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

علاوه بر این، سیستم‌ها باید با معیارها ارزیابی شوند و با قابلیت مشاهده‌پذیری نگهداری شوند. نظارت بر مصرف توکن، تأخیر و هزینه در مقابل کیفیت خروجی یک ملاحظه کلیدی است.

  1. اول ارزیابی را طراحی کنید
    قبل از ساختن فیچرها، تصمیم بگیرید که چطور موفقیت رو اندازه‌گیری می‌کنید. این کار به تعیین محدوده اپلیکیشن شما کمک میکنه و تصمیمات بهینه‌سازی رو هدایت میکنه.
    • اگه بتونید پاداش‌های قابل تایید یا عینی طراحی کنید، این بهترین حالته. (مثال: وظایف طبقه‌بندی که شما یه مجموعه داده اعتبارسنجی دارید).
    • اگه نه، آیا میتونید توابعی تعریف کنید که به صورت اکتشافی (heuristically) جواب‌های مدل زبانی رو برای مورد استفاده شما ارزیابی کنن؟ (مثال: تعداد دفعاتی که یه تکه خاص با توجه به یه سوال بازیابی میشه).
    • اگه نه، آیا میتونید از انسان‌ها برای حاشیه‌نویسی (annotate) جواب‌های مدل زبانی‌تون کمک بگیرید؟
    • اگه هیچکدوم کار نکرد، از یه مدل زبانی به عنوان داور برای ارزیابی جواب‌ها استفاده کنید. در بیشتر موارد، بهتره که وظیفه ارزیابی خودتون رو به عنوان یه مطالعه مقایسه‌ای تنظیم کنید، که توش داور چندین جواب تولید شده با هایپرپارامترها/پرامپت‌های مختلف رو دریافت میکنه و باید رتبه‌بندی کنه که کدوم‌ها بهترینن.
  2. تقریبا همه جا از خروجی‌های ساختاریافته استفاده کنید
    همیشه خروجی‌های ساختاریافته رو به متن آزاد ترجیح بدید. این کار سیستم شما رو قابل اعتمادتر و دیباگ کردنش رو راحت‌تر میکنه. میتونید اعتبارسنجی و تلاش مجدد هم اضافه کنید!
  3. برای شکست طراحی کنید
    موقع طراحی پرامپت‌ها یا ماژول‌های dspy، مطمئن بشید که همیشه در نظر میگیرید «اگه اوضاع خراب شد چی میشه؟». مثل هر نرم‌افزار خوبی، کم کردن حالت‌های خطا و شکست خوردن با برنامه، سناریوی ایده‌آله.
  4. همه چیز را نظارت کنید
    DSPy با MLflow یکپارچه میشه تا موارد زیر رو ردیابی کنه:
    • پرامپت‌های تکی که به مدل زبانی داده میشن و جواب‌هاشون
    • مصرف توکن و هزینه‌ها
    • تاخیر به ازای هر ماژول
    • نرخ‌های موفقیت/شکست
    • عملکرد مدل در طول زمان
    ابزارهایی مثل Langfuse و Logfire هم به همون اندازه جایگزین‌های خوبی هستن.

منابع

  • [۱] Context Engineering — A Comprehensive Hands-On Tutorial with DSPy | Towards Data Science

دیدگاه‌ها

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *