מבוא: מעבר להנדסת פרומפטים — עידן הנדסת ההקשר
אם עקבתם אחרי ההתפתחויות בתעשיית ה-AI ב-2024-2025, ודאי שמתם לב למונח שחזר שוב ושוב: "הנדסת פרומפטים" (Prompt Engineering). היה נראה שזה הכישור החם ביותר — משרות שהציעו 300 אלף דולר בשנה רק על כתיבת פרומפטים טובים. אבל ב-2026? הדברים השתנו. בצורה דרמטית.
לפי סקר של Fast Company ממאי 2025, תפקיד "מהנדס פרומפטים" כמשרה עצמאית כמעט נעלם לחלוטין. סקר של מיקרוסופט על 31,000 עובדים דירג את התפקיד במקום הלפני-אחרון ברשימת המשרות שחברות מתכננות להוסיף. אבל — וזה ה"אבל" החשוב — הכישור עצמו לא נעלם. הוא פשוט התפתח למשהו הרבה יותר מקיף: הנדסת הקשר (Context Engineering).
אז מה בדיוק קרה? למה "לכתוב פרומפט טוב" כבר לא מספיק? והכי חשוב — איך בונים מערכות AI מבוססות הנדסת הקשר שבאמת עובדות בייצור?
אם קראתם את המאמרים הקודמים שלנו על שרתי MCP, צינורות RAG, ומערכות מרובות-סוכנים — אתם כבר מכירים את אבני הבניין. עכשיו הגיע הזמן לחבר את הכל יחד לדיסציפלינה אחת שלמה.
מהי הנדסת הקשר? וכמה היא באמת שונה מהנדסת פרומפטים?
הנדסת הקשר היא הדיסציפלינה של תכנון ובניית מערכות דינמיות שמספקות למודל שפה גדול (LLM) את כל המידע הנכון, בפורמט הנכון, בזמן הנכון — כדי שיוכל לבצע את המשימה בהצלחה. זה הרבה מעבר לכתיבת הוראה טקסטואלית טובה.
אנדריי קרפאתי (Andrej Karpathy), חוקר AI ומייסד שותף של OpenAI, הגדיר את זה בצורה שאני חושב שפשוט מסמרת: "האמנות והמדע העדינים של מילוי חלון ההקשר בדיוק במידע הנכון."
ההבדל בין שתי הגישות פשוט אבל קריטי:
- הנדסת פרומפטים שואלת: "איך אני מנסח את השאלה הזו?"
- הנדסת הקשר שואלת: "איזה מידע המודל צריך כדי להצליח, ואיך אני מספק אותו?"
חשבו על זה ככה: לבקש מ-ChatGPT לכתוב מייל מקצועי — זו הנדסת פרומפטים. לבנות פלטפורמת שירות לקוחות שמנהלת היסטוריית שיחות לאורך מספר סשנים, ניגשת לפרטי החשבון של המשתמש, זוכרת טיקטים קודמים, ומשתמשת בכלים חיצוניים — זו הנדסת הקשר. הפער בין השניים הוא עצום.
לפי דו"ח State of Agent Engineering של LangChain לשנת 2025, 57% מהארגונים כבר מפעילים סוכני AI בייצור. אבל 32% מציינים את האיכות כמכשול המרכזי. ורוב הכשלים? הם בכלל לא כשלים של המודל — הם כשלים של הקשר. המודל קיבל מידע חסר, לא עדכני, או רועש, ובמקום לומר "אני לא יודע", הוא מילא את הפערים בניחושים. (מכירים את זה, נכון?)
טבלת השוואה מהירה
| מימד | הנדסת פרומפטים | הנדסת הקשר |
|---|---|---|
| היקף | קלט-פלט בודד | כל מה שהמודל "רואה" — זיכרון, היסטוריה, כלים, מסמכים |
| גישה | ניסוח הוראות ברורות | תכנון ארכיטקטורת מידע שלמה |
| סקלביליות | קורסת כשגדלים — יותר משתמשים = יותר מקרי קצה | מתוכננת לגודל מהיום הראשון |
| דיבאגינג | שינוי ניסוח וניחוש מה השתבש | בדיקת כל חלון ההקשר, slot-ים של זיכרון, ותנועת טוקנים |
| כלים נדרשים | תיבת פרומפט בלבד | מודולי זיכרון, מערכות RAG, שרשרת API-ים, תיאום backend |
| חזרתיות | משתנה — דורש התאמות ידניות | מתוכננת לעקביות ושימוש חוזר |
נקודה חשובה שכדאי להדגיש: הנדסת פרומפטים היא תת-קבוצה של הנדסת הקשר, לא מתחרה שלה. הפרומפט עצמו עדיין חלק חשוב — אבל הוא רק אחד מ-6 שכבות שצריך לנהל.
אנטומיה של חלון ההקשר: 6 השכבות
כדי להבין הנדסת הקשר ברמה מעשית, צריך קודם כל להבין מה בעצם נכנס לחלון ההקשר של LLM. כל קריאה למודל מרכיבה את חלון ההקשר מעד 6 שכבות:
- System Prompt — ההוראות הבסיסיות שמגדירות את התפקיד, ההתנהגות, והכללים של המודל. זה החלק ה"קבוע" יחסית.
- היסטוריית שיחה — ההודעות הקודמות בשיחה הנוכחית. בגלל שמודלים הם stateless (לא זוכרים בין קריאות), צריך לשלוח את כל ההיסטוריה בכל פעם מחדש.
- מסמכים שנשלפו (RAG) — מידע רלוונטי שנמשך ממאגרי ידע, מסדי נתונים וקטוריים, או מקורות חיצוניים.
- זיכרון ארוך טווח — עובדות, העדפות, ותובנות שנצברו לאורך אינטראקציות קודמות.
- הגדרות כלים (Tool Definitions) — תיאורי ה-API-ים, הפונקציות, והכלים שהמודל יכול להפעיל.
- קלט המשתמש — השאלה או המשימה הנוכחית.
כל שכבה צורכת טוקנים יקרים מחלון ההקשר המוגבל. וכאן מתחיל האתגר האמיתי: איך מנהלים את כל השכבות האלה בתוך מגבלת הטוקנים מבלי לוותר על מידע קריטי?
ארבע האסטרטגיות המרכזיות: כתוב, בחר, דחוס, בודד
LangChain, שהפכו לסטנדרט דה-פקטו בתעשייה לפיתוח אפליקציות LLM, פורמליזו ארבע אסטרטגיות מרכזיות להנדסת הקשר. אז בואו נפרק כל אחת מהן — עם דוגמאות קוד מעשיות שאפשר להריץ.
1. כתוב (Write) — שמירת הקשר לאחסון חיצוני
במקום לדחוס הכל לתוך חלון ההקשר, שומרים מידע חשוב מחוץ למודל — במסד נתונים, מערכת קבצים, או מאגר וקטורי. ככה המודל יכול "לזכור" מבלי לבזבז טוקנים יקרים.
from langgraph.graph import StateGraph, START, END
from typing import TypedDict, Annotated
import operator
class AgentState(TypedDict):
"""State that persists across agent steps"""
messages: Annotated[list, operator.add]
user_preferences: dict # Long-term user data
retrieved_docs: list # RAG results
tool_results: list # Tool execution outputs
context_summary: str # Compressed history
def save_to_memory(state: AgentState) -> AgentState:
"""Extract and persist key facts from conversation"""
messages = state["messages"]
# Extract facts worth remembering
facts = extract_facts(messages[-1])
# Save to external vector store
memory_store.add_documents(facts)
return {"messages": [{"role": "system",
"content": f"Saved {len(facts)} facts to memory"}]}
graph = StateGraph(AgentState)
graph.add_node("save_memory", save_to_memory)
היתרון הגדול כאן: הזיכרון חי מחוץ למודל ולא מוגבל בגודל חלון ההקשר. אפשר לצבור ידע לאורך מאות שיחות בלי לחשוש מגלישת טוקנים.
2. בחר (Select) — שליפת מידע רלוונטי בלבד
זו בעצם ליבת ה-RAG. הרעיון פשוט: במקום לשלוח את כל המידע הקיים, בוחרים רק את מה שרלוונטי לשאילתה הנוכחית. אבל המפתח הוא לא רק "מה לשלוף" אלא גם "כמה לשלוף" — ופה הרבה צוותים טועים.
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
# Step 1: Chunk documents intelligently
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
chunk_size=1500, # Balance between precision and context
chunk_overlap=100 # Preserve cross-chunk context
)
chunks = text_splitter.split_documents(raw_docs)
# Step 2: Index in vector store
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
vectorstore = FAISS.from_documents(chunks, embeddings)
# Step 3: Retrieve with relevance threshold
retriever = vectorstore.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={
"score_threshold": 0.75, # Only high-relevance docs
"k": 5 # Max 5 chunks
}
)
def select_context(state: AgentState) -> AgentState:
"""Retrieve only relevant documents for current query"""
query = state["messages"][-1]["content"]
docs = retriever.invoke(query)
return {"retrieved_docs": docs}
מחקרים מראים שהגבלת כלי השלוף ל-30 כלים או פחות מניבה דיוק גבוה פי 3 בבחירת הכלי הנכון, ושומרת על פרומפטים קצרים בהרבה. מספר שצריך לזכור.
3. דחוס (Compress) — כיווץ מידע לחיסכון בטוקנים
כשהיסטוריית השיחה גדלה או שיש יותר מדי מסמכים רלוונטיים, צריך לדחוס את המידע מבלי לאבד את הנקודות החשובות. זה כמו לסכם פגישה של שעה לשלוש נקודות מפתח.
from langchain.memory import ConversationSummaryBufferMemory
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# Keep recent messages verbatim, summarize older ones
memory = ConversationSummaryBufferMemory(
llm=llm,
max_token_limit=2000, # Summarize when exceeding this
return_messages=True
)
# Alternative: Middleware-based approach in LangGraph
def compress_context(state: AgentState) -> AgentState:
"""Summarize old messages to free up token budget"""
messages = state["messages"]
if count_tokens(messages) > 6000:
old_messages = messages[:-4] # Keep last 4 verbatim
recent_messages = messages[-4:]
summary = llm.invoke(
f"Summarize this conversation concisely:\n"
f"{format_messages(old_messages)}"
)
return {
"messages": [
{"role": "system", "content": f"Summary: {summary}"},
*recent_messages
],
"context_summary": summary.content
}
return state
יש שלוש גישות עיקריות לדחיסה, כל אחת עם ה-tradeoff שלה:
- חיתוך (Truncation) — מהיר אבל מאבד מידע. פשוט שומרים את N ההודעות האחרונות ומקווים לטוב.
- חלון נגלל (Sliding Window) — מאוזן יותר. שומרים את K ההודעות האחרונות ומוחקים את הישנות.
- סיכום רקורסיבי — יקר (דורש קריאת LLM נוספת) אבל שומר על המשמעות. מסכמים הודעות ישנות לפרגרף אחד.
4. בודד (Isolate) — הפרדת הקשרים בין סוכנים
במערכות מרובות-סוכנים, כל סוכן צריך הקשר שונה. זה הגיוני כשחושבים על זה — סוכן שמנתח קוד לא צריך לראות היסטוריית שיחות שירות לקוחות, וסוכן שמחפש מידע באינטרנט לא צריך גישה לפרטי חשבון הבנק של המשתמש.
from langgraph.graph import StateGraph
# Each agent gets its own isolated context
class CodeReviewState(TypedDict):
code_diff: str
review_guidelines: str
messages: list
class CustomerSupportState(TypedDict):
customer_id: str
ticket_history: list
messages: list
# Supervisor delegates with filtered context
def route_to_agent(supervisor_state):
task_type = classify_task(supervisor_state["messages"][-1])
if task_type == "code_review":
return CodeReviewState(
code_diff=supervisor_state["current_diff"],
review_guidelines=load_guidelines(),
messages=[] # Fresh context for code agent
)
elif task_type == "support":
return CustomerSupportState(
customer_id=supervisor_state["customer_id"],
ticket_history=fetch_tickets(
supervisor_state["customer_id"]
),
messages=[] # Isolated from code context
)
בידוד הקשר מונע שני דברים מסוכנים: דליפת מידע בין סוכנים (קריטי מבחינת אבטחה ופרטיות), ועומס קוגניטיבי שגורם למודל לבלבל בין הקשרים. ראיתי מערכות שהתמוטטו רק בגלל שסוכנים שיתפו הקשר שלא לצורך.
ניהול חלון ההקשר בייצור: פה רוב המערכות נכשלות
בואו נדבר תכלס. ניהול חלון ההקשר זה המקום שבו התיאוריה פוגשת את המציאות — ורוב מערכות ה-AI נכשלות בייצור בדיוק פה.
העיקרון המנחה: פחות זה יותר (ברצינות)
מחקר של NoLiMa (benchmark להערכת הקשרים ארוכים) גילה ממצא שהפתיע הרבה אנשים בתעשייה: ב-32,000 טוקנים, 11 מתוך 12 מודלים שנבדקו ירדו מתחת ל-50% מהביצועים שלהם בהקשרים קצרים. כלומר, המטרה היא לא "לנצל את מקסימום חלון ההקשר", אלא "להשתמש במינימום ההקשר הנדרש לתשובה איכותית".
ויש עוד: מחקר של Levy, Jacoby ו-Goldberg מ-2024 מצא שביצועי ההסקה של LLM-ים מתחילים להידרדר כבר מסביב ל-3,000 טוקנים. זה הרבה מתחת למגבלות הטכניות שאנחנו רגילים לשמוע עליהן. ה-sweet spot למרבית המשימות? 150-300 מילים.
ספירת טוקנים מדויקת — הבסיס לכל השאר
לפני שמתחילים לנהל, צריך למדוד. מודלים שונים משתמשים בטוקנייזרים שונים, אז ספירה גנרית לא תעבוד — צריך ספירה ספציפית למודל:
import tiktoken
def count_tokens(text: str, model: str = "gpt-4o") -> int:
"""Accurate token counting per model"""
encoding = tiktoken.encoding_for_model(model)
return len(encoding.encode(text))
def budget_context_window(
system_prompt: str,
conversation_history: list,
rag_docs: list,
tool_definitions: list,
max_tokens: int = 128000, # GPT-4o limit
reserve_for_output: int = 4096
) -> dict:
"""Calculate token budget per context layer"""
available = max_tokens - reserve_for_output
budget = {
"system": count_tokens(system_prompt),
"tools": sum(count_tokens(str(t)) for t in tool_definitions),
"history": sum(count_tokens(str(m)) for m in conversation_history),
"rag": sum(count_tokens(d.page_content) for d in rag_docs),
}
total_used = sum(budget.values())
budget["remaining"] = available - total_used
budget["utilization"] = f"{(total_used / available) * 100:.1f}%"
return budget
# Example usage
budget = budget_context_window(
system_prompt=system_msg,
conversation_history=messages,
rag_docs=retrieved_docs,
tool_definitions=tools,
)
print(budget)
# {'system': 850, 'tools': 2400, 'history': 3200,
# 'rag': 4500, 'remaining': 113046, 'utilization': '8.9%'}
אסטרטגיית ניהול הקשר דינמית
הנה פטרן מלא לניהול הקשר דינמי שמשלב את כל הטכניקות שדיברנו עליהן. זה הקוד שהייתי רוצה שמישהו היה נותן לי כשהתחלתי:
class ContextManager:
"""Production context management for LLM applications"""
def __init__(self, max_tokens=128000, output_reserve=4096):
self.max_tokens = max_tokens
self.output_reserve = output_reserve
self.available = max_tokens - output_reserve
# Priority tiers for context elements
PRIORITY = {
"system_prompt": 1, # Never trim
"tool_definitions": 2, # Rarely trim
"user_query": 3, # Never trim
"recent_messages": 4, # Keep last 3-4
"rag_documents": 5, # Trim by relevance
"older_messages": 6, # Summarize first
"user_preferences": 7, # Include if space
}
def assemble_context(self, components: dict) -> list:
"""Assemble context respecting token budget"""
result = []
tokens_used = 0
# Sort by priority (lower number = higher priority)
sorted_items = sorted(
components.items(),
key=lambda x: self.PRIORITY.get(x[0], 99)
)
for name, content in sorted_items:
content_tokens = count_tokens(str(content))
if tokens_used + content_tokens <= self.available:
result.append(content)
tokens_used += content_tokens
else:
# Try to compress instead of dropping
compressed = self._compress(content,
budget=self.available - tokens_used)
if compressed:
result.append(compressed)
tokens_used += count_tokens(str(compressed))
# Else: drop this component entirely
return result
def _compress(self, content, budget):
"""Compress content to fit within budget"""
if count_tokens(str(content)) <= budget:
return content
# Use LLM to summarize if budget allows
if budget > 200:
summary = summarize(content, max_tokens=budget)
return summary
return None # Cannot fit
Caching סמנטי — איך חוסכים 50-90% בעלויות API
אחת הטכניקות הכי אפקטיביות שנתקלתי בהן בייצור היא caching סמנטי. הרעיון פשוט למדי: במקום לשלוח כל שאילתה ישירות ל-LLM (שעולה כסף ולוקח זמן), בודקים אם שאלה דומה כבר נשאלה בעבר.
from langchain_community.cache import RedisSemanticCache
from langchain_openai import OpenAIEmbeddings
import langchain
# Configure semantic caching
langchain.llm_cache = RedisSemanticCache(
redis_url="redis://localhost:6379",
embedding=OpenAIEmbeddings(model="text-embedding-3-small"),
score_threshold=0.92 # High similarity required
)
# Now identical or near-identical queries hit cache
# "What is context engineering?" and
# "Explain context engineering to me" ->
# Same cached result (saves API call + latency)
מה שיפה ב-caching סמנטי זה שהוא מזהה כששאילתות שונות בניסוח אבל זהות במשמעות. במקום התאמה מילולית (שמחמיצה את רוב המקרים), הוא משתמש ב-embeddings כדי למצוא דמיון סמנטי. ארגונים שהטמיעו את זה מדווחים על חיסכון של 50-90% בעלויות API ושיפור משמעותי בזמני תגובה. זה ממש low-hanging fruit.
Memory Blocks — זיכרון ארוך טווח למערכות סוכניות
אחד החידושים המשמעותיים ביותר בהנדסת הקשר ב-2026 הוא הקונספט של Memory Blocks, שהוצג לראשונה על ידי MemGPT. הרעיון: להקצות אזורים שמורים בתוך חלון ההקשר עבור זיכרון פרסיסטנטי — כמו "מדפים" קבועים שהסוכן יכול לכתוב אליהם ולקרוא מהם.
# Memory Block architecture concept
class MemoryBlock:
"""Reserved context window section for persistent memory"""
def __init__(self, name: str, max_tokens: int):
self.name = name
self.max_tokens = max_tokens
self.content = ""
def update(self, new_info: str, llm):
"""Intelligently update memory block"""
prompt = f"""Current memory block ({self.name}):
{self.content}
New information to integrate:
{new_info}
Update the memory block by:
1. Adding new relevant facts
2. Removing outdated information
3. Keeping total under {self.max_tokens} tokens
4. Prioritizing the most actionable information"""
self.content = llm.invoke(prompt).content
# Example: Agent with structured memory blocks
class AgentMemory:
def __init__(self):
self.core_memory = MemoryBlock(
"core_facts", max_tokens=500) # Key user info
self.working_memory = MemoryBlock(
"current_task", max_tokens=1000) # Active task
self.archival = VectorStore() # Unlimited external
def get_context(self) -> str:
return f"""## Core Memory
{self.core_memory.content}
## Working Memory
{self.working_memory.content}
## Retrieved from Archive
{self.archival.search(current_query, k=3)}"""
ההבדל העיקרי בין Memory Blocks לזיכרון רגיל: הסוכן עצמו יכול להחליט מה לזכור ומתי לשלוף. זה לא עוד pipeline קבוע של RAG — זו אוטונומיה קוגניטיבית אמיתית. הסוכן מנהל את הזיכרון שלו כמו שאנחנו מנהלים פתקים — כותב דברים חשובים, מוחק מה שלא רלוונטי, ומחפש כשצריך.
הנדסת הגדרות כלים: מה שרוב אנשים מפספסים
כשמדברים על הנדסת הקשר, רבים מתמקדים ב-RAG ובזיכרון ושוכחים שכבה קריטית: הגדרות הכלים. כל כלי שהמודל יכול להפעיל (API, פונקציה, חיפוש) צורך טוקנים מחלון ההקשר.
וכאן הנקודה המפתיעה: ככל שיש יותר כלים זמינים, כך המודל פחות מדויק בבחירת הנכון. מחקר מ-2025 הראה שהגבלת מספר הכלים ל-30 או פחות מניבה דיוק גבוה פי שלוש. הפתרון? להפעיל RAG גם על הגדרות הכלים:
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
class DynamicToolSelector:
"""Select relevant tools dynamically using RAG"""
def __init__(self, all_tools: list):
# Store tool definitions in vector store
tool_docs = [
f"Tool: {t.name}\nDescription: {t.description}\n"
f"Use when: {t.use_cases}"
for t in all_tools
]
self.tool_index = FAISS.from_texts(
tool_docs,
OpenAIEmbeddings(model="text-embedding-3-small")
)
self.tool_map = {t.name: t for t in all_tools}
def select_tools(self, query: str, max_tools: int = 8):
"""Return only relevant tools for the current query"""
results = self.tool_index.similarity_search(
query, k=max_tools)
selected_names = [
r.page_content.split("\n")[0].replace("Tool: ", "")
for r in results
]
return [self.tool_map[n] for n in selected_names
if n in self.tool_map]
# Usage: Instead of giving the LLM 100+ tools...
selector = DynamicToolSelector(all_available_tools)
relevant_tools = selector.select_tools(
"Calculate quarterly revenue from sales data"
)
# Returns only 5-8 relevant tools instead of 100+
הגישה הזו מקטינה באופן דרמטי את כמות הטוקנים שמוקדשת להגדרות כלים, ובמקביל משפרת את הדיוק כי המודל לא צריך "לבחור" מתוך רשימה ארוכה מדי. פחות אופציות = החלטות טובות יותר. (נכון גם לבני אדם, אגב.)
פטרן Context Orchestrator — חיבור הכל יחד
עכשיו מגיע החלק הכיפי. בואו נראה איך מחברים את כל הטכניקות לפטרן ייצור מלא. הרעיון: Context Orchestrator — שכבת תיאום מרכזית שמרכיבה את ההקשר הדינמי לפני כל קריאה ל-LLM:
from dataclasses import dataclass
from langchain_openai import ChatOpenAI
@dataclass
class ContextConfig:
max_total_tokens: int = 128000
output_reserve: int = 4096
system_prompt_budget: int = 2000
tool_budget: int = 3000
rag_budget: int = 8000
history_budget: int = 6000
memory_budget: int = 2000
class ContextOrchestrator:
"""Central orchestrator for dynamic context assembly"""
def __init__(self, config: ContextConfig):
self.config = config
self.context_manager = ContextManager(
config.max_total_tokens,
config.output_reserve
)
self.memory = AgentMemory()
self.tool_selector = DynamicToolSelector(all_tools)
self.retriever = create_retriever()
self.cache = SemanticCache()
async def build_context(self, user_query: str,
conversation_history: list
) -> dict:
"""Assemble optimized context for LLM call"""
# 1. Check semantic cache first
cached = await self.cache.lookup(user_query)
if cached:
return {"response": cached, "from_cache": True}
# 2. Parallel context assembly
import asyncio
rag_task = asyncio.create_task(
self.retriever.ainvoke(user_query))
tools_task = asyncio.create_task(
asyncio.to_thread(
self.tool_selector.select_tools, user_query))
memory_task = asyncio.create_task(
asyncio.to_thread(
self.memory.get_context))
rag_docs, tools, memory_context = await asyncio.gather(
rag_task, tools_task, memory_task)
# 3. Compress history if needed
compressed_history = self._manage_history(
conversation_history)
# 4. Assemble with priority-based budget
context = self.context_manager.assemble_context({
"system_prompt": self._get_system_prompt(),
"tool_definitions": tools,
"user_query": user_query,
"recent_messages": compressed_history,
"rag_documents": rag_docs,
"user_preferences": memory_context,
})
return {
"context": context,
"tools": tools,
"from_cache": False,
"token_budget": self.context_manager.last_budget
}
שימו לב לשלושה דברים חשובים בדפוס הזה:
- בדיקת cache סמנטי לפני כל עבודה אחרת — חוסך עלויות וזמן
- שליפה מקבילית של RAG, כלים, וזיכרון — מקצר latency משמעותית
- הרכבה מבוססת עדיפויות — רכיבים קריטיים תמיד נכנסים, אופציונליים נדחסים או נפלטים לפי הצורך
מלכודות נפוצות ואיך להימנע מהן
אחרי שבניתי (ולפעמים גם שברתי) עשרות מערכות מבוססות LLM בייצור, הנה הטעויות שאני רואה שוב ושוב:
1. "יותר הקשר = תשובות טובות יותר"
זו כנראה הטעות הנפוצה ביותר. אינטואיטיבית, מרגיש נכון לתת למודל כמה שיותר מידע. אבל המציאות הפוכה: הצפת חלון ההקשר פוגעת בביצועים, מעלה עלויות, ומגדילה latency.
תנו למודל את המינימום הנדרש, לא את המקסימום האפשרי.
2. התעלמות מסדר המידע בחלון ההקשר
למודלים יש הטיה לחדשנות (recency bias) — הם נותנים יותר משקל למידע שמופיע בסוף חלון ההקשר. מחקרים מראים ששאילתות בסוף הפרומפט משפרות את איכות התשובה ב-עד 30%. כלל אצבע פשוט: מסמכים ארוכים למעלה, הוראות ושאילתות למטה.
3. חוסר ניטור שימוש בטוקנים
כל טוקן עולה כסף. ללא מעקב, הוצאות ה-API יכולות להתפוצץ מהר מאוד — ראיתי צוותים שגילו חשבונות של אלפי דולרים כי לא הגדירו alerts. בנו ניטור עלויות לכל שלב בפייפליין.
4. שכחת מקרי קצה של Structured Output
אם אתם מצפים ל-JSON מובנה מהמודל, טפלו תמיד ב: סירובים (refusals), חיתוך (truncation), מערכים ריקים, ובלבול enum. בנו שרשרת fallback — אף ספק לא אמין ב-100%, לא משנה מה כתוב בדוקומנטציה.
5. אי-בידוד הקשר במערכות מרובות-סוכנים
כשכל הסוכנים חולקים את אותו הקשר, אתם מסתכנים בדליפת מידע (סוכן אחד רואה נתונים שלא אמור לראות) וזיהום הקשר (מידע מסוכן אחד מבלבל סוכן אחר). תמיד בודדו הקשרים.
כלים וספריות מומלצים ב-2026
| כלי | שימוש | הערות |
|---|---|---|
| LangChain / LangGraph | אורקסטרציה מלאה של הקשר | הסטנדרט בתעשייה לניהול state ו-context |
| LlamaIndex | אינדוקס ושליפת מסמכים | מצוין ל-RAG מתקדם |
| Pydantic / Zod | ולידציה של פלט מובנה | חובה — גם כשה-API מבטיח schema compliance |
| Instructor | הפקת פלט מובנה מ-LLM | תומך ב-15+ ספקי LLM עם API אחיד |
| Redis + Embeddings | Caching סמנטי | חיסכון של 50-90% בעלויות API |
| LangSmith | Tracing ודיבאגינג | Observability מלא על כל שלב בפייפליין |
| MemGPT / Letta | Memory Blocks | זיכרון ארוך טווח אוטונומי לסוכנים |
שאלות נפוצות
מה ההבדל בין הנדסת הקשר להנדסת פרומפטים?
הנדסת פרומפטים מתמקדת בניסוח ההוראה הטקסטואלית שנשלחת למודל — "איך לשאול". הנדסת הקשר היא דיסציפלינה רחבה יותר שמתכננת את כל סביבת המידע שהמודל רואה: זיכרון, מסמכים שנשלפו, הגדרות כלים, היסטוריית שיחה, והפרומפט עצמו. בקיצור — הנדסת פרומפטים היא תת-קבוצה של הנדסת הקשר.
האם הנדסת הקשר רלוונטית רק למערכות גדולות?
בכלל לא. גם אפליקציה פשוטה שמשתמשת ב-RAG כבר עושה הנדסת הקשר — היא בוחרת אילו מסמכים להכניס לחלון ההקשר. ככל שהמערכת מורכבת יותר (סוכנים, כלים, זיכרון), הנדסת הקשר הופכת קריטית יותר. אבל העקרונות הבסיסיים — כמו "מינימום הקשר הנדרש" ו"סדר חשוב" — רלוונטיים לכל גודל של מערכת.
כמה טוקנים צריך להקצות לכל שכבה בחלון ההקשר?
אין כלל אחד שמתאים לכולם, אבל הנה נקודת התחלה טובה למודלים עם חלון של 128K טוקנים: System Prompt — 1,000-2,000 טוקנים, הגדרות כלים — 2,000-4,000, היסטוריית שיחה — 4,000-8,000, מסמכי RAG — 4,000-10,000, זיכרון — 1,000-2,000, ושמירת 4,096 לפחות לפלט. אבל חשוב — מדדו, אל תנחשו. עקבו אחרי שימוש בטוקנים בייצור ואופטימזו לפי מה שרואים בפועל.
איך הנדסת הקשר משתלבת עם RAG?
RAG הוא אחד ממרכיבי הליבה של הנדסת הקשר — ספציפית, הוא מיישם את אסטרטגיית ה"בחר" (Select). אבל RAG לבדו לא מספיק. הנדסת הקשר מוסיפה שכבות של דחיסה (סיכום מסמכים ארוכים), עדיפויות (אילו chunks חשובים יותר), בידוד (לא כל סוכן צריך את אותם מסמכים), ו-caching סמנטי (חיסכון בשליפות חוזרות).
מה הטעות הנפוצה ביותר בהנדסת הקשר?
הטעות מספר 1 היא להניח ש"יותר הקשר = תשובות טובות יותר". מחקרים מראים שביצועי LLM-ים יורדים כשחלון ההקשר מוצף במידע. מודלים מתבלבלים, מתעלמים ממידע חשוב שנמצא באמצע (ה"אפקט של האגם האבוד"), ומייצרים hallucinations. הכלל: תנו למודל את המינימום הנדרש עם האות-לרעש הגבוה ביותר.