English · 中文
Position: Layer 2 of the four-layer evolution of LLM application engineering Research date: 2026-05-01
Context engineering was formally named in the second half of 2024 by Karpathy, in his YC AI School talk, and Anthropic [6] positions it as “the natural evolution of prompt engineering.” The core observation: in the era of 100K–1M context windows and agents, what determines how the model performs is no longer “that one prompt,” but everything the model can see at the moment of inference—the system prompt, conversation history, tool definitions, retrieved documents, memory, and planning state.
This chapter covers:
/compact)The pivotal shift: the core difficulty of a production LLM application has moved from “writing the prompt” to “managing context”—and that is the prerequisite for agents (03) and harness engineering (04).
The term “context engineering” began circulating in the community in the second half of 2024, and is generally credited to Karpathy’s YC AI School talk Software Is Changing (Again) [5]. The definition he gave has been quoted repeatedly since:
“In every industrial-strength LLM app, context engineering is the delicate art and science of filling the context window with just the right information for the next step.” (In every industrial-grade LLM application, context engineering is the delicate art and science of filling the context window with exactly the right information, in preparation for the next step of inference.)
Anthropic’s official 2025 article Effective Context Engineering for AI Agents [6] goes further and positions it as “the natural evolution of prompt engineering”:
“What prompt engineering cares about is the sentence you say to the model; what context engineering cares about is all the information the model can see at the moment of inference—the system prompt, conversation history, tool definitions, retrieved documents, memory, planning state, constraints, and policies.”
The LangChain team, in the blog post Context Engineering for Agents [7], gives an engineering definition that is often cited:
“Context engineering is the discipline of designing dynamic systems that, at the right time and in the right format, supply the LLM with the right information and tools, so the model has everything it needs to complete the task.”
Why is “prompt engineering” no longer enough? Three fundamental changes:
By early 2026, the industry consensus is: the core difficulty of a production LLM application has moved from “writing the prompt” to “managing context.”
To understand why “filling the window” is a bad idea, you first have to understand three physical constraints on context:
Back to prompt-engineering.en.md §1.1: standard attention is N² complexity. When context goes from 32K to 128K, attention compute is 16×, not 4×. For an inference service that means two things:
Each attention layer maintains caches of K and V at inference time (to avoid recomputation). For a 70B model at 128K context, the KV cache can occupy 40+ GB, larger than the model weights themselves. That is why techniques that “manage the KV cache,” such as vLLM’s PagedAttention and SGLang’s RadixAttention, have become a core competitive advantage of inference servers (see harness-engineering.en.md §3.9.2).
Liu et al.’s 2024 TACL paper Lost in the Middle: How Language Models Use Long Contexts [2] systematically verified a counterintuitive phenomenon: put the critical information at the start or the end of the prompt and the model uses it well; put it in the middle and the model often fails to see it. Recall as a function of position follows a U-shaped curve—primacy bias at the start, recency bias at the end, and a black hole in the middle.
The curve has been verified on GPT-3.5 / GPT-4 / Claude. Even Gemini 1.5 Pro in 2025 reaches 99.7% recall on a simple needle-in-a-haystack test, but when the task becomes “find 8 relevant facts inside 1M tokens and synthesize them,” average recall drops to ~60% [41].
The RULER benchmark [12] (What’s the Real Context Size of Your Long-Context Language Models?), published in 2024 by NVIDIA and other institutions, upgrades “long-context capability” from the toy needle-in-a-haystack test to a torture suite:
What RULER exposes: almost every model that claims a context of 100K+ has an “effective context” on RULER far smaller than the advertised figure. For example, a model may claim to support 200K, but on RULER accuracy falls below 80% past 50K, and at 128K it is already close to random.
That pulls the industry back to reality: long context is not a replacement for RAG; it is a complement.
In Long-Context Models vs RAG: When the 1M-Token Window Is the Wrong Tool [41], Tianpan gives a decision framework that was mainstream in 2026:
| Dimension | 1M-context model | RAG |
|---|---|---|
| Latency | 30–60× slower (the entire window must be prefilled) | Two stages, retrieval + generation, but the generation-stage input is small |
| Cost per query | ~1250× more expensive | Retrieval is nearly free; generation cost scales with the number of relevant chunks |
| Data updates | Retransmitted on every query | The index is updated once, and every query benefits |
| Explainability | Black box | Retrieved results can be audited and cited |
| Multi-fact recall | ~60% (in practice) | With a reranker, 90%+ is reachable |
| Scenarios that truly suit long context | A one-shot deep analysis (“read this 200-page PDF and summarize the insights”) | Knowledge-base Q&A, customer support, document retrieval |
Practical guidance: 90% of “knowledge retrieval” scenarios should use RAG; only the 10% that are “deep analysis of a single document” should use long context.
RAG (Retrieval-Augmented Generation) is the most mature branch of context engineering, and it has already evolved into a complete sub-discipline. The state of 2025–2026 can be summarized as four generations of evolution [43]:
The sections below unpack the 2026 design space in RAG pipeline order.
Real-world documents are PDFs, slide decks, scans, Excel, and HTML, not plain text. This step is often skipped, and its impact is very large:
pypdf / pdfplumber (fast, but error-prone on tables and multi-column layout)unstructured / LlamaParse (with a layout model that can recognize headings, paragraphs, tables, and lists)PaddleOCR / Tesseract / EasyOCR (scans and images)Best practice: run at least two parsers and compare them, then decide which to use. Tables and formulas must be handled specially (convert to Markdown / LaTeX).
The chunking strategy affects recall quality far more than the choice of embedding model:
General recommendation: a chunk size of 256–512 tokens and an overlap of 50–100 tokens is the sweet spot for most tasks.
The main contenders on the April 2026 MTEB (Massive Text Embedding Benchmark) leaderboard [13]:
| Model | MTEB | Dimensions | Price / 1M tok | Notes |
|---|---|---|---|---|
| OpenAI text-embedding-3-large | 64.6 | 3072 (truncatable) | $0.13 | Established and stable |
| Cohere embed-v4 | 65.2 | 1536 | $0.10 | Current MTEB #1 |
| Voyage 3 large | ~66 | 1024/2048 | $0.18 | On average ~10% higher than OpenAI |
| Google Gemini Embedding 2 | 64+ | 3072 | $0.006 | Native multimodal (text/image/video/audio/PDF), 100+ languages |
| BGE-M3 (BAAI) | 64+ | 1024 | Free (open source) | The same model supports dense / sparse / multi-vector retrieval |
| Jina v5-text-small | 71.7 (v2 leaderboard) | 512 | Free (Apache 2.0) | 677M parameters; best quality-to-size ratio |
Two key trends in 2026:
Measured on Chinese workloads: BGE-M3 is the best free option (open source + strong multilingual performance + the same model supports sparse/dense), and Voyage 3 large is the best paid option (Chinese recall is generally 10%+ higher than OpenAI).
The five mainstream systems in 2026 production deployments [14]: pgvector / Qdrant / Weaviate / Milvus / LanceDB.
Performance benchmark (2026, 128-dimensional embeddings, on the order of 1M vectors) [14]:
| Database | p99 latency | QPS (single node) | Notes |
|---|---|---|---|
| Qdrant | 2 ms | 12,000 | Rust implementation; strongest metadata filtering |
| FAISS (in-memory) | 3 ms | 15,000 | Single-machine performance leader; no service-deployment capability |
| Milvus | 5 ms | 8,000 | Distributed + high availability; version 2.5 hybrid search is 30x faster than ES |
| Pinecone | 8 ms | 5,000 | Fully managed; zero operational burden |
| Weaviate | 10 ms | 4,000 | Built-in GraphQL and knowledge-graph capability |
Selection decisions:
pgvector 0.9 has caught up in performance, avoiding the complexity of operating a second databaseVector retrieval (dense) is good at semantic similarity and weak at exact keyword matching (product IDs, proper nouns, typo tolerance). Production-grade RAG is almost always hybrid search:
Hybrid strategy: merge the sparse and dense result lists with RRF (Reciprocal Rank Fusion). Simple, and no parameter tuning.
The RRF formula:
\[ \text{RRF}(d) = \sum_{r \in R} \frac{1}{k + r(d)} \]
where R is the set of retrievers, r(d) is the rank of document d in that retriever (1, 2, 3, …), and k is a smoothing constant (default 60, Cormack et al. 2009). Intuition: a better rank contributes more, but because the term is a reciprocal the gaps converge quickly, which keeps any one retriever from dominating the list.
Weighted RRF (production practice): default RRF treats every retriever with equal weight. To give dense and sparse different weights:
def weighted_rrf(results_dict, weights, k=60):
# results_dict = {"dense": [doc1, doc2, ...], "sparse": [docA, docB, ...]}
# weights = {"dense": 0.7, "sparse": 0.3}
scores = defaultdict(float)
for retriever, docs in results_dict.items():
w = weights[retriever]
for rank, doc in enumerate(docs, start=1):
scores[doc.id] += w / (k + rank)
return sorted(scores.items(), key=lambda x: -x[1])
RRF vs. other fusion methods:
| Method | Needs tuning? | Score alignment across retrievers | When to use |
|---|---|---|---|
| RRF | No (k=60 is nearly universal) | Uses rank, not score, so alignment is natural | Default first choice |
| CombSUM / Linear | Yes (scores must be normalized) | Hard (BM25 vs. cosine have different units) | Not recommended unless calibration already exists |
| WAND (Weighted AND) | Medium | Used inside sparse retrieval, not across fusion | Speeds up inverted indexes in Lucene / Elasticsearch |
| Convex Combination + ML | High (requires learning-to-rank) | Alignment is trained | Best at large scale, when click data is available |
Empirical result: hybrid search + a reranker improves MRR by +9.3 percentage points over semantic-only, and with Cohere Rerank accuracy can reach 90%+.
After first-stage retrieval (embedding / BM25) recalls the top 100, a more expensive but more accurate cross-encoder reranks the query–document pairs, and the top 5 are given to the LLM. The ROI of this step is extremely high:
| Reranker | Type | ELO | Notes |
|---|---|---|---|
| Zerank 2 | API | 1638 | Current leader |
| Cohere Rerank v4 Pro | API | 1629 | Established |
| Cohere Rerank 3 Nimble | API | — | Speed-optimized version |
| BGE-reranker-v2-m3 | Self-hosted | — | Open source; quality close to Cohere |
| FlashRank | Self-hosted | — | Extremely lightweight; millisecond-scale |
| ColBERT v2 | Self-hosted | — | Late-interaction architecture |
| RankLLM (rerank directly with an LLM) | LLM | — | Most expensive, but the most explainable |
Top rerankers improve precision by 15–40% over embeddings alone. If you are allowed to add only one RAG optimization, adding a reranker is usually the highest ROI.
A user asking “今天天气咋样” and a document that says “current weather conditions” mean the same thing, but the vocabularies do not overlap. Query rewriting processes the query before retrieval:
How to choose (decide by the shape of the query):
| Query shape | Recommended method | Why |
|---|---|---|
| Short query / missing keywords | HyDE / Query2Doc | A short query does not match the length of a document chunk; expanding it into a pseudo-document pulls the embeddings closer |
| Polysemy / synonym problems | Multi-Query | Rewrites cover different phrasings and raise recall |
| A narrow question that depends on background knowledge | Step-Back | Retrieve the broad knowledge first, then focus on the detail |
| Compound / comparison / multi-hop | Sub-Query Decomposition | A single retrieval cannot cover multiple subtopics |
| Ordinary conversation + code / numbers | Do not rewrite | An LLM rewrite can change key terms or numbers, and recall drops |
Pitfalls:
Microsoft GraphRAG (open-sourced in 2024) and community variants such as LightRAG / Nano-GraphRAG extend retrieval from “vector similarity” to “knowledge-graph traversal”:
In some scenarios GraphRAG pushes retrieval precision to 99% (relation-dense domains such as financial compliance, law, and medicine) [43].
Side-by-side comparison of the three:
| Dimension | Self-RAG | CRAG | Agentic RAG |
|---|---|---|---|
| Decides when to retrieve | The model itself (special token [Retrieve]) |
Always retrieves | The agent controller decides |
| Judging retrieval results | The model itself ([IsRel] reflection token) |
An independent retrieval evaluator (a small model; high / medium / low) | An LLM judge or a tool |
| Failure fallback | Stop retrieving + signal uncertainty | Trigger a web search and retrieve again | Rewrite the query / switch data sources / multi-round iteration |
| Training cost | High (requires SFT on the reflection tokens) | Low (plug-and-play) | 0 (runtime orchestration) |
| Latency | Medium (one inference emits extra tokens) | High (always retrieve + evaluate + possibly the web) | Highest (possibly a multi-round loop) |
| Fits | General Q&A where the model’s own knowledge is sufficient | Knowledge bases with poor freshness that need a web fallback | Multi-hop reasoning, research Q&A, Deep Research |
| Mainstream deployments in 2026 | OpenAI gpt-4o web-search mode | The internal architecture of Perplexity / You.com | The research agents in Claude / Cursor / Devin |
Empirical results: in production deployments, Self-RAG can reduce “unnecessary retrieval” by 25–40%—because the model learns not to retrieve for things it already knows. On multi-hop QA (HotpotQA / MuSiQue), Agentic RAG is +15–25 points more accurate than vanilla RAG, but token cost multiplies by 3–10×.
See agent-engineering.en.md §2 on how the pattern evolved, and §5 on the framework comparison.
Indexing schemes that mix images and text:
Layout-aware chunking (a key engineering point, and underestimated):
A traditional RecursiveCharacterTextSplitter works poorly on laid-out documents such as PDF / Word—tables are split in half, figure captions are separated from the figures, and the table of contents is mixed with the body. Production options:
| Tool | Approach | Strength |
|---|---|---|
| Unstructured.io | Rules + ML element classification | General purpose; full coverage of HTML/PDF/PPT/email |
| LlamaParse | LlamaIndex’s own LLM parser | Markdown output; high table fidelity |
| PyMuPDF + pdfplumber | Low-level PDF SDK | Custom layout rules; preserves bbox coordinates |
| Docling (IBM, 2024) | Open-source structured document parsing | Associates tables / formulas / image captions |
| MinerU (OpenDataLab, 2024) | Friendly to Chinese PDFs | Works well on Chinese academic papers and textbooks |
Multimodal RAG selection:
Pitfall: multimodal embeddings are much better at cross-modal retrieval (finding an image from text) than at cross-modal alignment (whether the image and the text are actually about the same thing). The generation stage still needs a VLM to confirm relevance a second time.
Threading the 10 subsections above together, a minimal runnable skeleton of a production-grade RAG pipeline:
# 1. 文档解析
from llama_parse import LlamaParse
parser = LlamaParse(result_type="markdown")
docs = parser.load_data("./knowledge_base/")
# 2. 切分
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=512, chunk_overlap=80,
separators=["\n\n", "\n", "。", "!", "?", ".", " "]
)
chunks = splitter.split_documents(docs)
# 3. 嵌入 + 入向量库
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
from langchain_qdrant import QdrantVectorStore
embeddings = HuggingFaceBgeEmbeddings(model_name="BAAI/bge-m3")
vectorstore = QdrantVectorStore.from_documents(
chunks, embeddings, url="http://localhost:6333", collection_name="kb"
)
# 4. 混合检索(dense + sparse + RRF)
from langchain.retrievers import EnsembleRetriever, BM25Retriever
bm25_retriever = BM25Retriever.from_documents(chunks); bm25_retriever.k = 20
dense_retriever = vectorstore.as_retriever(search_kwargs={"k": 20})
hybrid_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, dense_retriever], weights=[0.4, 0.6]
)
# 5. 查询改写(multi-query + step-back)
from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(model="claude-sonnet-4-6")
multi_q_retriever = MultiQueryRetriever.from_llm(retriever=hybrid_retriever, llm=llm)
# 6. 重排(Cohere Rerank 或 BGE-reranker)
from langchain.retrievers import ContextualCompressionRetriever
from langchain_cohere import CohereRerank
reranker = CohereRerank(model="rerank-v3.5", top_n=5)
final_retriever = ContextualCompressionRetriever(
base_retriever=multi_q_retriever, base_compressor=reranker
)
# 7. 生成(带 citation)
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个严谨的助手。仅根据给定 context 回答,每条事实必须标注 [doc_id]。"),
("human", "Context:\n{context}\n\n问题: {input}"),
])
combine_chain = create_stuff_documents_chain(llm, prompt)
rag_chain = create_retrieval_chain(final_retriever, combine_chain)
answer = rag_chain.invoke({"input": "Transformer 的 attention 复杂度是多少?"})
This skeleton covers the 7 core steps of RAG. Taking it to production still requires adding:
| Resource | Type | Recommended use |
|---|---|---|
NirDiamant/RAG_Techniques |
A collection of advanced-technique notebooks | 30+ advanced RAG techniques, each with a runnable notebook; the most complete |
infiniflow/ragflow |
Open-source RAG engine | When you want an enterprise RAG platform that works out of the box |
HKUDS/LightRAG |
EMNLP 2025 paper + implementation | A reference for a concise, efficient RAG paradigm |
Danielskry/Awesome-RAG |
awesome list | An entry point when looking for resources on a specific topic |
lehoanglong95/rag-all-in-one |
Full-stack guide | When you want to compare tools by component |
pguso/rag-from-scratch |
From-scratch implementation | When you want to understand what is actually happening inside the black box |
microsoft/graphrag |
Official GraphRAG | The reference implementation of entity-relation graph RAG |
abhinav-kimothi/A-Simple-Guide-to-RAG |
Companion to an introductory book | An introduction for complete beginners |
If RAG solves “how to get an external knowledge base into context,” a memory system solves “how to get the agent’s own experience into context.” The two share pieces of engineering (both are retrieval + injection into the prompt), but they differ in kind:
| Dimension | RAG | Memory system |
|---|---|---|
| Data source | External static documents (company wiki, PDFs) | Conversation history, agent behavior, user preferences |
| Write trigger | Once, at index time | Every interaction may trigger an incremental write |
| Data lifecycle | Follows the source document | Has its own “expiration / eviction / summarization” logic |
| Isolation granularity | Global or department-level | Must be per-user / per-agent / per-session |
| Freshness | Usually minutes to days | Can be seconds (“what the user just said”) |
| Consistency | Strong (one ground truth) | Weak (facts change, and there are contradictions) |
“The context window is already 1 M. Do we still need a memory system?” is a real question. Three reasons:
A memory system is, in essence, “turning all past experience into a small retrievable database, and on each inference retrieving only the few most relevant items to inject into context”—replacing “feed everything to the model” with “the model retrieves on demand.”
A classification borrowed from cognitive psychology. Each type corresponds to a different engineering implementation:
| Memory type | Cognitive meaning | Engineering implementation | Storage backend | When written | When retrieved |
|---|---|---|---|---|---|
| Working memory | The “scratch paper” of the current task | The current context window | The LLM’s internal KV cache | Every token | Every token |
| Short-term | The most recent N turns | A rolling message buffer | RAM / Redis | Every turn | Injected in full every turn |
| Episodic | “What happened when” | Timestamped conversation fragments | Vector store + time index | End of session / important events | By topic + time window |
| Semantic | Stable facts about the world / the user | KV pairs or graph nodes | Graph store / KV store / SQL | When a new fact is extracted | By entity name |
| Procedural | “How to do this kind of task” | Executable skills / procedure scripts | Filesystem / skill registry | Summarized after the task succeeds | By task type |
| Long-term | All of the types above, across sessions | The union of the types above | Usually a combination of several backends | Async / background worker | On demand |
Key insight: a production agent’s “memory” is usually not a single system, but some combination of subsets of the 6 types above. Letta (MemGPT) puts short-term + semantic memory in context (Core Memory), episodic memory in a vector store (Recall Memory), and external knowledge in another vector store (Archival Memory)—the classic three layers of an OS-inspired design.
Taken apart, any memory system is these 5 components:
┌──────────────────────────┐
│ Conversation / Action │
│ (raw events) │
└────────────┬─────────────┘
│
┌─────────────────────▼──────────────────────┐
│ ① Extractor(抽取器) │
│ LLM 把原始事件抽成结构化"记忆事实" │
│ e.g. "用户偏好暗色主题" │
└─────────────────────┬──────────────────────┘
│
┌─────────────────────▼──────────────────────┐
│ ② Writer(写入器) │
│ 去重 / merge / conflict resolution │
│ embed + 入库 │
└─────────────────────┬──────────────────────┘
│
┌─────────▼─────────┐
│ Storage Backend │
│ vector / graph / │
│ KV / SQL / file │
└─────────┬─────────┘
│
┌─────────────────────▼──────────────────────┐
│ ③ Retriever(检索器) │
│ query → top-K 相关记忆 │
│ hybrid: semantic + keyword + graph │
└─────────────────────┬──────────────────────┘
│
┌─────────────────────▼──────────────────────┐
│ ④ Forgetter(遗忘器) │
│ 按 LRU / 重要性 / 时间衰减 淘汰 │
└─────────────────────┬──────────────────────┘
│
┌─────────────────────▼──────────────────────┐
│ ⑤ Compressor(压缩器) │
│ 老记忆摘要、冲突合并、知识图重建 │
└────────────────────────────────────────────┘
Each component has its own engineering decision points. §2.5.4–2.5.8 below walks through them one by one.
A memory system’s storage backend is not “pick one” but “pick which ones to combine.” Common options:
| Backend | Strengths | Weaknesses | Typical use |
|---|---|---|---|
| Vector store (Qdrant / Weaviate / Pinecone / pgvector) | Semantic similarity retrieval | Weak at exact match / relationship queries | Primary store for episodic memory |
| Graph store (Neo4j / FalkorDB / Memgraph) | Entity relations / multi-hop queries / time | Complex to deploy / slow retrieval | Semantic memory + relations + time |
| KV store (Redis / DynamoDB) | Extremely low latency / simple | No semantic retrieval | Short-term working memory + cache |
| Relational (Postgres + pgvector / Tiger Data) | Transactional / SQL filters / one-stop | Tuning a large-scale vector index is painful | An integrated stack at small-to-medium scale |
| Filesystem (Anthropic Memory Tool) | Simple / auditable / agent-managed | Retrieval depends on the agent grepping for itself | Native to the Claude ecosystem |
| Dedicated memory SaaS (Mem0 Cloud / Zep Cloud / Supermemory) | Zero operations | Lock-in / data leaves your boundary | Startup MVP |
你的记忆主体是什么?
│
├─ 主要是用户偏好 / 实体事实 → 语义记忆为主
│ ├─ 关系密集("A 是 B 的同事"、"X 公司 2024 收购 Y")→ Zep(Graphiti 图)
│ └─ 关系简单 → Mem0(vector + 可选 graph)
│
├─ 主要是对话历史 + 长跑任务 → 情景记忆为主 + agent 自管
│ └─ → Letta(Core + Recall + Archival 三层)
│
├─ 主要是 agent 行为可审计 / 可回滚
│ └─ → Anthropic Memory Tool(filesystem,可 git 管理)
│
└─ 已用 Redis / Postgres,不想加新栈
├─ 用 Redis Agent Memory Server
└─ 用 Postgres + pgvector + Tiger
Memory is not simply storing the raw conversation. It is “facts” extracted by an LLM. The Mem0 v2 write algorithm (paper [19]), broken down:
1. Retrieve 相关记忆(top-10 semantically similar)
2. LLM 单次调用抽取新事实(Add-only, 不 Update / Delete)
└─ Prompt: "Given new conversation + existing memories,
extract NEW facts that should be remembered.
Return JSON list."
3. Embed + MD5 hash 去重
4. 实体抽取 + 链接(用于 graph memory)
5. 批量写入 vector store + entity collection
Key design choices:
core_memory_replace tool)Write pseudocode (a from-scratch version):
def write_memory(user_id, conversation_chunk):
# 1. 检索相关历史
relevant = vector_store.search(
embed(conversation_chunk), filter={"user_id": user_id}, k=10
)
# 2. LLM 抽取
extracted = llm.extract_facts(
prompt=f"对话:{conversation_chunk}\n现有记忆:{relevant}\n新事实?",
schema={"facts": [{"text": str, "category": str, "confidence": float}]},
)
# 3. 过滤低置信度
facts = [f for f in extracted["facts"] if f["confidence"] > 0.7]
# 4. 去重
new_facts = []
for f in facts:
h = hashlib.md5(f["text"].encode()).hexdigest()
if not vector_store.exists(hash=h, user_id=user_id):
new_facts.append({**f, "hash": h, "user_id": user_id, "ts": now()})
# 5. 写入
vector_store.add_batch(new_facts)
Retrieval by embedding alone will miss things. Production memory systems in 2026 all use multi-signal fusion—the same idea as hybrid retrieval in RAG. Mem0 v2 retrieval:
Query → preprocess(lemmatize + entity 抽取)
↓
┌──┴──────────────────────────┬────────────────────────┐
▼ ▼ ▼
Semantic(vector cosine) BM25 keyword Entity match
│ │ │
└──────────────┬──────────────┴────────────────────────┘
▼
Score Fusion (RRF or weighted)
▼
Top-K Memories → 注入 system prompt
Advanced techniques:
exp(-λ·age); newer memories are preferredMemory explosion is a real problem. The MaRS architecture (arXiv 2603.07670) formalizes 6 mainstream eviction policies:
| Policy | Formula / rule | Fits |
|---|---|---|
| FIFO | By insertion time | The simplest; almost unused (it deletes important old facts) |
| LRU | By last-access time | The default starting point for a desktop agent |
| Priority Decay | score = w₁·prior + w₂·recency + w₃·frequency |
The best general-purpose baseline |
| Reflection-Summary | The old N items → an LLM summarizes them into 1 item | Used together with LRU; essential for a long-running agent |
| Random-Drop | Delete at random | Only as a baseline for comparison |
| Hybrid | LRU + Priority Decay + summary used together | Recommended for production |
A practical recipe (Letta and Mem0 are both close to this idea):
def evict_if_needed(memories, max_count=10000):
if len(memories) <= max_count:
return memories
# 1. 计算每条记忆的"保留分数"
for m in memories:
m.keep_score = (
m.importance * 0.4 # 写入时打的重要性
+ recency_score(m.last_access) * 0.3 # 越近越高
+ access_frequency(m) * 0.2 # 被引用次数
- age_penalty(m.created) * 0.1 # 越老越低
)
# 2. 按分数排序,淘汰末尾 N 条
memories.sort(key=lambda m: m.keep_score, reverse=True)
to_evict = memories[max_count:]
# 3. 老记忆不直接删,先尝试摘要
summary_chunks = group_by_topic(to_evict, n=10) # 10 条一组
for chunk in summary_chunks:
summary = llm.summarize(chunk)
memories.append(Memory(
text=summary, importance=avg([m.importance for m in chunk]),
ts=min([m.created for m in chunk]),
sources=[m.id for m in chunk],
))
# 4. 真正删除原始 N 条
delete_batch([m.id for m in to_evict])
return memories
Rules of thumb:
Architecture: 5 pillars (summarized from paper [19] and the Mem0 docs):
Note: the 5 items above are a checklist arranged by engineering responsibility, not an official architecture diagram from any of Mem0 / Letta / Zep. What the Mem0 paper actually describes is four operations, Add/Update/Delete/NoOp, plus an LLM extraction pipeline; Letta uses three layers, Core/Recall/Archival; Zep uses three node kinds, Episodic/Entity/Edge.
Performance (the v2 algorithm, in the terms reported by the Mem0 paper arXiv:2504.19413): on long-conversation memory benchmarks such as LoCoMo / LongMemEval it is substantially better than the RAG / full-context baselines (the size of the gain varies considerably by subtask and by metric; see Tables 4–5 of the original paper); extraction latency is cut in half.
Complete code example:
from mem0 import Memory
# 初始化(用 Qdrant + Neo4j + OpenAI)
config = {
"vector_store": {
"provider": "qdrant",
"config": {"host": "localhost", "port": 6333},
},
"graph_store": { # 可选
"provider": "neo4j",
"config": {"url": "bolt://localhost:7687",
"username": "neo4j", "password": "..."},
},
"llm": {
"provider": "openai",
"config": {"model": "gpt-4o-mini"},
},
"embedder": {
"provider": "openai",
"config": {"model": "text-embedding-3-small"},
},
}
m = Memory.from_config(config)
# 写入:从对话自动抽事实
m.add(
messages=[
{"role": "user", "content": "我是 Liang,住北京,做 AI 工程师,我喜欢冷咖啡"},
{"role": "assistant", "content": "记下了"},
],
user_id="liang_001",
)
# Mem0 自动:提取 ["用户名是 Liang", "住北京", "职业 AI 工程师", "偏好冷咖啡"]
# 检索
memories = m.search(query="他喝什么咖啡?", user_id="liang_001", limit=3)
# [{"memory": "用户偏好冷咖啡", "score": 0.89}, ...]
# 注入下次 prompt
context = "\n".join([f"- {m['memory']}" for m in memories])
prompt = f"已知用户信息:\n{context}\n\n用户:今天给我推荐什么饮料?"
Production architecture recommended by AWS: Mem0 + ElastiCache for Valkey (short-term + cache) + Neptune Analytics (graph) + OpenSearch (vectors).
Fits: general chatbots, customer support, and personal assistants—the “add memory in 5 lines of code” case.
Architecture: treat the LLM as an OS and divide memory into three layers (the core innovation):
| Layer | Name | Where | Capacity | Who can change it |
|---|---|---|---|---|
| L1 | Core Memory | In-context (inside the system prompt) | Fixed (for example 4 KB) | The agent itself, via a tool |
| L2 | Recall Memory | External DB | The full conversation history | Automatic |
| L3 | Archival Memory | Vector store | Unlimited | The agent itself, via a tool |
Core Memory is the key innovation—it is not a database but part of the system prompt. The agent can change it with the core_memory_append / core_memory_replace tools the way it would edit a file, and on the next inference that content automatically appears at the head of the context. This is equivalent to letting the agent “keep editing its own persona.”
6 built-in tools (the agent manages its own memory):
| Tool | Role |
|---|---|
send_message |
Send a message to the user |
core_memory_append |
Append a new fact to in-context memory |
core_memory_replace |
Replace in-context memory (correct an error / update a preference) |
conversation_search |
Search conversation history (recall memory) |
archival_memory_insert |
Write to the long-term archive |
archival_memory_search |
Retrieve from the long-term archive |
Complete code:
from letta_client import Letta
client = Letta(base_url="http://localhost:8283") # 自托管 server
# 创建 agent,定义 core memory blocks
agent = client.agents.create(
name="my_assistant",
memory_blocks=[
{"label": "human", "value": "用户的名字、偏好、当前正在做的事"},
{"label": "persona", "value": "你是一个简洁、礼貌的助理"},
],
model="anthropic/claude-sonnet-4-6",
embedding="openai/text-embedding-3-small",
)
# 对话(多轮,每次 Letta 自动管理 context)
response = client.agents.messages.create(
agent_id=agent.id,
messages=[{"role": "user", "content": "我叫 Liang,下周要面试"}],
)
# Letta 内部:agent 调用 core_memory_append("human", "用户叫 Liang,下周面试")
# 关掉、重启、明天继续——agent 还记得 Liang 与他的面试
response = client.agents.messages.create(
agent_id=agent.id,
messages=[{"role": "user", "content": "帮我准备一下"}],
)
# agent 看 core memory → 知道 Liang 要面试 → 自动反馈
Fits: long-running agents (days / weeks), products that need a “persistent identity” (a personalized assistant / an AI coach), and an OSS alternative to the Anthropic Memory Tool.
Core innovation: every fact carries two time axes:
Architecture: 3 node kinds + edges
┌────────────────┐ rel ┌────────────────┐
│ Entity Node │◀─────▶│ Entity Node │
│ Alice │ edge │ TechCorp │
└───────┬────────┘ └────────────────┘
│ source ▲
▼ │ joined
┌────────────────┐ │
│ Episodic Node │──────────────────┘
│ "对话片段" │ derives
│ T=2024-03-15 │
└────────────────┘
Key capability: when a fact changes (“Alice left in 2024-06”), Zep does not delete the old fact. It marks the old edge valid_to=2024-06 and the new edge valid_from=2024-06. A query can then ask:
valid_to=NULLvalid_from <= 2024-04 <= valid_toHybrid retrieval: semantic embeddings + BM25 + direct graph traversal—no LLM call, pure index and graph queries, latency 100–500 ms.
Minimal code (Graphiti SDK):
from graphiti_core import Graphiti
from graphiti_core.nodes import EpisodeType
g = Graphiti(uri="bolt://localhost:7687", user="neo4j", password="...")
await g.build_indices_and_constraints()
# 写入(episode = 一段对话或事件)
await g.add_episode(
name="meeting_2026_03",
episode_body="Alice 离开 TechCorp,加入 Anthropic 做 research engineer",
source=EpisodeType.message,
reference_time=datetime.now(),
)
# Graphiti 自动:抽 entities → 建 nodes → 抽 relations → 建 edges → invalid 旧 edges
# 查询
results = await g.search(query="Alice 现在在哪工作?", limit=5)
# [{"fact": "Alice 现在在 Anthropic", "valid_from": "2026-03", "valid_to": None}, ...]
Fits: scenarios where user state changes—CRM, customer support, medical history, compliance audit, and legal fact bases.
Released to public beta on 2026-04-23. Design philosophy: treat memory as files, and let Claude read and write them with the familiar bash + code execution tools.
Architecture:
Claude Agent
│
├─ tool: bash (read/write/grep files)
├─ tool: code execution
└─ tool: memory (mount filesystem)
│
▼
┌───────────────────┐
│ /memories/ │
│ ├ liang.md │ ← user profile
│ ├ tasks/ │
│ │ ├ today.md │
│ │ └ done.md │
│ └ playbooks/ │
│ └ deploy.md │ ← procedural
└───────────────────┘
(any backend:
local fs, S3,
encrypted, ...)
Complete code (Python SDK, public beta):
from anthropic import Anthropic
from anthropic.lib.tools import BetaAbstractMemoryTool
# 选项 1:默认本地 filesystem 后端
client = Anthropic()
response = client.beta.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
tools=[
{"type": "memory_2026_04_23", "name": "memory"},
{"type": "bash_2026_04_23", "name": "bash"},
],
messages=[{"role": "user", "content": "记一下:我下周三要去机场,记得提醒我提前 3 小时出发"}],
extra_headers={"anthropic-beta": "memory-2026-04-23"},
)
# Claude 自动:bash > echo "trip_2026_03_20 ..." > /memories/calendar.md
# 选项 2:自定义后端(S3 / 加密 / 数据库)
class S3MemoryTool(BetaAbstractMemoryTool):
def __init__(self, bucket):
self.bucket = bucket
async def list_files(self, path: str) -> list[str]:
return list_s3_keys(self.bucket, path)
async def read_file(self, path: str) -> str:
return s3_get(self.bucket, path)
async def write_file(self, path: str, content: str):
s3_put(self.bucket, path, content)
# ... delete / move / search 等
Early customer cases (published officially by Anthropic):
Fits: teams already tied to the Claude ecosystem, cases that need “an agent managing its own notebook the way a programmer would,” and cases that need audit / version control (memory files can go into git).
| Dimension | Mem0 | Letta | Zep / Graphiti | Anthropic Memory Tool |
|---|---|---|---|---|
| Data model | facts (vector + graph) | core / recall / archival | temporal KG | filesystem |
| Agent initiative | passive (auto extract) | agent-managed tools | passive | agent-managed tools |
| Multi-model support | 16+ LLMs | Any | Any | Claude only |
| Backend choice | 24+ vector + graph | Postgres / built-in | Neo4j / FalkorDB | Any filesystem |
| Temporal tracking | Weak (timestamp-based) | Weak | Strong (two time axes) | File mtime |
| Relationship modeling | Medium (optional graph) | Weak | Strong (graph-native) | Weak |
| Cost to get started | 5 lines | 50 lines | 30 lines | 10 lines |
| Open source | ✓ | ✓ | ✓ (Graphiti) | × |
| Chinese support | Pair it with a Chinese embedder | Same | Same | Native |
| Papers / numbers | LoCoMo +20pt | MemGPT paper | arXiv 2501.13956 | Netflix error rate -97% |
The pitfall the vast majority of SaaS products will hit: the memory system leaks across users. Two layers of defense:
# 错误示范(会跨用户混)
m.add(messages=msgs) # ❌ 没 user_id
# 正确:强制 user_id 必填
m.add(messages=msgs, user_id=request.user.id)
m.search(query=q, user_id=request.user.id, ...) # 检索也必须带
# 强制约束(包一层):
class TenantSafeMemory:
def __init__(self, m, user_id):
self.m = m; self.uid = user_id
def add(self, messages, **kw):
return self.m.add(messages=messages, user_id=self.uid, **kw)
def search(self, query, **kw):
return self.m.search(query=query, user_id=self.uid, **kw)
| Scheme | Isolation strength | Performance | Scale it fits |
|---|---|---|---|
Shared collection + user_id filter |
Weak (depends on the application layer) | Good | < 1k users |
| One collection per user | Strong | Medium (the number of collections has a ceiling) | 1k–10k |
| One namespace per user (Pinecone / Turbopuffer) | Strong | Good | 10k–1M+ |
| A separate DB instance per tenant | Strongest (physical isolation) | Medium | Large customers / compliance |
Vector stores designed specifically for multi-tenancy, such as Turbopuffer, can support a million namespaces, with each tenant’s data strictly isolated inside.
# 审计:所有跨用户的访问立刻报警
@audit_log(level="critical")
def admin_search_all(query):
return vector_store.search(query=query, no_user_filter=True)
# GDPR "被遗忘权"
def forget_user(user_id):
vector_store.delete_all(user_id=user_id)
graph_store.delete_subgraph(user_id=user_id)
redis.delete_pattern(f"user:{user_id}:*")
audit_log(f"user {user_id} memory wiped at {now()}")
In practice, memory and RAG often coexist, and they do not conflict:
User Query
│
├──▶ RAG(公司文档 / 产品手册 / 政策) → 通用知识
│
└──▶ Memory(用户偏好 / 历史交互 / agent 经验) → 个性化
两路检索结果合并 → 注入 system prompt → LLM
A typical production prompt structure:
你是 ACME 公司客服。
# 用户档案(来自 Memory)
- 姓名:Liang
- 历史订单:3 单(2 单已完成)
- 偏好:电邮通知优先
# 相关知识(来自 RAG)
[政策片段] 30 天内可全额退款
[FAQ 片段] 退款流程通常 3-5 工作日
# 当前对话
用户:我想退订单 #12345
Vector memory / KV memory handles “facts,” but a long-running coding agent / research agent also needs a structured, human-readable, diffable memory of work in progress—that is the progress file (the de facto standard used by Augment Code, Claude Code, and Devin).
A typical schema (Markdown; common names are PROGRESS.md / CONTEXT.md / .agent/state.md):
---
task_id: refactor-auth-2026-05-03
started: 2026-05-03T09:12Z
last_update: 2026-05-03T11:48Z
status: in_progress
parent_branch: main
worktree: /Users/x/dev/repo/.worktrees/refactor-auth
---
## Goal
迁移 session-based auth 到 JWT,保持向前兼容 30 天
## Decisions made
- [2026-05-03 09:30] 采用 RS256 而非 HS256(多服务部署需要公钥分发)— 见 ADR-007
- [2026-05-03 10:15] 旧 session cookie 保留 30 天,双写策略
## Files touched
- `src/auth/jwt.ts` (new)
- `src/auth/middleware.ts` (modified, +120/-45)
- `src/auth/session.ts` (deprecated, kept for 30d)
## Open questions
- [ ] refresh token 是否需要黑名单?需问 @security-team
- [ ] mobile 客户端最低支持版本是否覆盖 JWT 解析?
## Next steps
1. 写 jwt.test.ts 单元测试覆盖签 / 验 / 过期 / tampering
2. 在 staging 跑 24h 双写,看 metric
3. 文档 docs/auth-migration.md
Why this format rather than vector memory:
PROGRESS.md → and immediately knows where it left offKey engineering points:
git status + git log -5 + cat PROGRESS.md. That trio is the lowest-cost way to recover “who I am, where I left off, and what is next.”References: Anthropic’s Memory Tool, the Augment Code engineering blog, Claude Code’s .claude/ directory, Cursor’s .cursorrules, and Codename Goose’s goose hints.
| Failure mode | Symptom | Countermeasure |
|---|---|---|
| Memory explosion | After 1,000 items are stored, retrieval is all noise | LRU + summarization (§2.5.7) |
| Stale memory | “I like cold coffee” has expired but still takes effect | Zep time axes / conflict detection at write time / the user overrides it |
| Hallucinated memory | The LLM extracts as fact something that was never said | A confidence threshold at extraction + the user can audit |
| Cross-user leak | A’s preferences are injected into B’s prompt | Force user_id (§2.5.9) |
| Cold start | A new user’s memory is empty, and the agent seems “unfamiliar” | Explicit onboarding + a generic-profile fallback |
| Privacy / compliance | GDPR / CCPA require deletion of user data | A forget_user API that cascade-deletes across the stack |
| Extraction cost | Calling an LLM to extract facts every turn doubles token cost | An importance gate skips irrelevant conversation + batch every N turns |
| Low retrieval recall | The LLM does not see the critical fact | Add BM25 + entity match (§2.5.6 multi-signal) |
| Memory corruption | See the section below | Periodic consistency checks + ground-truth calibration + a TTL |
“Memory corruption” is a failure mode that inevitably appears after a stateful agent has been deployed for more than a few weeks, and early engineering teams rarely prevent it. Four typical kinds of corruption that the Anthropic / Letta / Mem0 teams have discussed in public:
| Corruption type | Example | Root cause | Defense |
|---|---|---|---|
| Stale fact | “The user is in Hangzhou” → three months later they have moved to Shanghai, but the old memory is still in the top-K | No TTL, and no write-time check of “fact freshness” | Zep-style valid_from / valid_to; at write time, detect old facts about the same entity and mark them invalid |
| Contradiction | One item says “the user is vegetarian,” another says “the user likes steak” | Extraction across multiple sessions has no cross-session consistency check | Before writing, run a contradiction-detection LLM on “this user’s existing facts + the new fact”; or a periodic reconciliation job |
| Hallucinated fact | During extraction the LLM “infers” from the conversation a preference the user never stated | The extraction prompt does not restrict itself to “explicit statements only” | At extraction time, return an evidence span (a quote from the original conversation); do not store anything without evidence |
| Persona drift | Long-running self-updating memory makes the agent’s self-description seriously inconsistent with the initial system prompt | Agent-managed memory + no frozen section | Split core memory into immutable / mutable sections; periodically reset the mutable section to the baseline |
Defensive engineering practice (ordered by ROI):
created_at / valid_from / valid_to, and retrieval filters by the current time(fact, evidence_quote, confidence); confidence < 0.7 is not stored| Resource | Type | Recommended use |
|---|---|---|
mem0ai/mem0 |
Official repository | A general memory layer; integrate in 5 lines |
letta-ai/letta |
Official repository | OS-inspired stateful agent |
getzep/graphiti |
Official repository | Temporal KG engine (Zep’s core) |
getzep/zep |
Official repository | The full Zep service (built on Graphiti) |
redis/agent-memory-server |
Official repository | Redis one-stop agent memory |
topoteretes/cognee |
Official repository | Graph-native + ontology |
supermemoryai/supermemory |
Official repository | Cross-vendor memory SDK |
| Anthropic Memory Tool docs | Official documentation | https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool |
Papers to read:
Mem0 paper — Production-Ready AI Agents with Scalable Long-Term MemoryMemGPT paper — Towards LLMs as Operating SystemsZep paper — A Temporal Knowledge Graph Architecture for Agent MemoryMaRS paper — Memory for Autonomous LLM Agents: Mechanisms, Evaluation, and Emerging Frontiers (a 2026 survey)Forgetful but Faithful — Cognitive Memory Architecture and BenchmarkEven at 1M tokens, the context window is still a scarce resource — for three reasons:
So a production-grade agent does not “fill the 1 M.” It spends tokens deliberately: every token should have a reason to appear in the prompt.
Treat the context window as a fixed space with logical partitions. Each partition has its own priority and its own eviction policy:
┌─────────────────────────────────────────────────────┐
│ ┌─────────────────────────────────────────────────┐ │
│ │ [固定区] System Prompt + Persona │ │ ← 永不淘汰
│ │ - 人设、约束、风格、角色 │ │ ~500-2000 tokens
│ └─────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ [可变高优] Memory(注入的用户/agent 记忆) │ │ ← 检索 + 重排
│ │ - 用户档案、偏好、近期事件 │ │ ~500-3000 tokens
│ └─────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ [可变高优] RAG 检索结果 │ │ ← top-K + reranker
│ │ - 公司知识、文档片段 │ │ ~2000-8000 tokens
│ └─────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ [可变中优] 工具定义 schema │ │ ← 仅放当前可能用到的
│ │ - tool descriptions + signatures │ │ ~500-3000 tokens
│ └─────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ [可变中优] 历史对话 │ │ ← 滑窗 + 摘要
│ │ - 最近 N 轮 + 早期摘要 │ │ 可变
│ └─────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ [低优 / 易淘汰] 工具调用结果 │ │ ← 旧的截断或摘要
│ │ - tool outputs,往往最长 │ │ 可变
│ └─────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ [尾部] 当前 user message │ │ ← 永远是最后
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
头部 尾部
↑ primacy bias recency bias ↑
Lost-in-the-Middle 黑洞
Suggested quota allocation (by product type):
| Product type | System | Memory | RAG | Tools | History | Tool results | User |
|---|---|---|---|---|---|---|---|
| Customer-support chatbot | 10% | 15% | 30% | 5% | 20% | 10% | 10% |
| Coding agent | 5% | 5% | 40% | 10% | 10% | 25% | 5% |
| Research agent | 5% | 10% | 30% | 5% | 10% | 35% | 5% |
| Personal assistant | 15% | 30% | 10% | 10% | 25% | 5% | 5% |
Production experience: any partition above 50% is a signal that the design is wrong.
The splits given by the two authoritative industry sources are not “5.” Anthropic’s Effective Context Engineering [6] gives three strategies: compaction / structured note-taking / sub-agent architectures. LangChain’s Context Engineering for Agents [7] gives four: write / select / compress / isolate. The five actions below merge the two splits and expand them, so an implementation can check them item by item. They are not an industry-standard taxonomy.
Use primacy + recency bias: put the most important content at the head and the tail, and the secondary content in the middle.
def assemble_context(system, memories, rag_chunks, tools, history, current_user_msg):
return [
system, # 头部:永远在前
*high_priority(memories), # 头部 2:用户身份与偏好
*high_priority(rag_chunks, top=3), # 头部 3:最相关的 3 个片段
*medium_priority(history), # 中部:历史对话(可被中部黑洞稀释)
*low_priority(rag_chunks, rest=True),# 中部:剩余 RAG 片段
*truncate(tool_results), # 尾部 -2:截断的工具结果
tools_definitions, # 尾部 -1:tool schema
current_user_msg, # 尾部:当前消息
]
Periodically check token utilization, and trigger when it crosses the threshold:
def trim_if_needed(context, max_tokens=180_000, target=140_000):
current = count_tokens(context)
if current < max_tokens: return context
# 优先淘汰:旧的工具结果
context = truncate_old_tool_results(context, keep_recent=5)
if count_tokens(context) <= target: return context
# 其次:早期对话压缩成摘要
context = summarize_old_history(context, summary_length=500)
if count_tokens(context) <= target: return context
# 再次:删除非 top-K 的 RAG 片段
context = drop_low_score_rag(context, threshold=0.6)
return context
/compactClaude Code’s /compact command compresses the current long conversation into a summary plus the key facts, and reclaims tokens:
def compact(messages, llm):
# 1. 抽取关键事实(不能丢)
facts = llm.invoke(f"""
Extract critical facts from this conversation that MUST be preserved:
{messages}
Return as bullet list. Include: user requirements, decisions made, code locations,
pending tasks, open questions.
""")
# 2. 写一段精简摘要
summary = llm.invoke(f"""
Summarize this conversation in <500 words. Focus on:
- What problem we're solving
- What approach we took
- Current progress
{messages}
""")
# 3. 重置 context
new_context = [
SystemMessage(original_system),
SystemMessage(f"# Compressed conversation context\n\n{summary}\n\n# Key facts\n{facts}"),
# 保留最近 3 轮原文(细节在新对话里有用)
*messages[-3:],
]
return new_context
The key point: the summary must be generated by an LLM (rule-based extraction loses context), but the prompt for the LLM summary itself must explicitly require that critical information such as “code locations / numbers / decisions” be preserved.
Reasoning agents such as ReAct produce a large volume of thought / observation while thinking, and that should not pollute the outward-facing context:
class AgentMemory:
def __init__(self):
self.public_messages = [] # 用户和 agent 互发
self.scratchpad = [] # agent 内部草稿(thought / 中间 tool result)
def get_llm_context(self):
# 给 LLM 看的:public + 最近 5 步 scratchpad(防止失忆)
return self.public_messages + self.scratchpad[-5:]
def get_user_facing(self):
# 给用户看的:仅 public
return self.public_messages
LangGraph’s state mechanism / Letta’s core vs recall are both productizations of this idea.
JSON returned by tools is often explosively long (for example, one SQL query of 1000 rows). Two layers of filtering:
def clip_tool_result(tool_name, result, current_step, total_steps):
# 1. 大结果直接截断
s = str(result)
if len(s) > 4000:
s = s[:2000] + f"\n... [truncated {len(s)-2000} chars] ...\n" + s[-500:]
# 2. 老的 tool result(>3 步前)摘要化
if total_steps - current_step > 3:
s = summarize_tool_result(s, max_length=300)
return s
| Method | Compression ratio | Information loss | Latency | Fit |
|---|---|---|---|---|
| Simple truncation | medium | high | 0 | tool results / fallback |
| Rule-based extraction (regex / structured) | medium | low | 0 | known formats (JSON / tables) |
LLM summary (/compact) |
high | low | 1-3 seconds | long conversations / high-value history |
| Anchored Iterative Summary (2026 SOTA) | high | lowest | 3-10 seconds | extremely long tasks (agents running for hours) |
Compaction is not “compress only once the context is full.” Otherwise the LLM has already been contaminated by noise. Production practice triggers on multiple signals:
| Trigger signal | Threshold (reference) | Where it applies |
|---|---|---|
| Token occupancy | > 70% of max_tokens (for example 180K / 256K = 70%) | general fallback, the most commonly used |
| History turns | > 50 conversation turns | long-conversation customer support / companion products |
| Accumulated tool results | a single-step tool result > 4000 tokens, or a cumulative total > 30K | coding agent / data agent |
| Quality-signal decline | user satisfaction / verifier pass rate over the last 5 turns drops | products that have a reward signal |
| Task boundary | a subtask completes (the agent declares it itself) | long-running multi-step agents |
| Time decay | a historical message goes > 30 minutes with no citation | agents running multiple tasks in parallel |
| Anthropic Auto-Compact signal | Claude API usage.context_window_remaining < 20% |
products on Claude read the official signal directly |
Key principles (Anthropic, Effective Context Engineering, 2026): 1. Compaction is lossy. Avoid it whenever you can (do actions 1-5 of §2.6.3 first). 2. Always keep the last N turns of the original conversation (N=3-5). The LLM needs recent raw signal to judge “what just happened.” 3. The summary should keep pointers — decisions, open questions, unfinished subtasks, code / file paths — rather than tell a story. 4. Immediately after the summary, run an evaluator once and confirm the critical facts were not lost. If they were, roll back to raw and upgrade to a stronger compression strategy. 5. Compaction itself has to be traced — what was dropped, the token diff before and after compression, and whether a rollback fired all go into observability.
class ContextManager:
def __init__(self, max_tokens=180_000, target_tokens=140_000):
self.max = max_tokens
self.target = target_tokens
def assemble(self, system, memories, rag, tools, history, user_msg):
ctx = self._assemble_by_priority(system, memories, rag, tools, history, user_msg)
if count_tokens(ctx) > self.max:
ctx = self._compress(ctx)
return ctx
def _assemble_by_priority(self, ...):
# 见 §2.6.3 动作 1
def _compress(self, ctx):
# 顺序尝试 5 种压缩,越早越轻:
for fn in [
lambda c: drop_low_score_rag(c, threshold=0.6),
lambda c: clip_tool_results(c, keep_recent=5),
lambda c: summarize_old_history(c, length=500),
lambda c: drop_low_priority_memory(c, keep=10),
lambda c: hard_truncate_history(c, keep_recent=3),
]:
ctx = fn(ctx)
if count_tokens(ctx) <= self.target:
return ctx
return ctx # 实在压不下,让 LLM 自己拒绝
def metrics(self, ctx):
# observability:每次返回 dict 让 dashboard 跟踪每分区占比
return {
"total": count_tokens(ctx),
"system_pct": count_tokens(ctx[0]) / count_tokens(ctx),
"rag_pct": ...,
"history_pct": ...,
"tool_results_pct": ...,
}
Wire metrics() into Langfuse / LangSmith, and the dashboard shows “which kind of content is eating the context quota” — the most practical tool for diagnosing agent drift.
Every modern LLM provider supports prompt caching — the “prompt prefix” is cached at the KV cache layer and reused the next time the same prefix appears. This is the single largest cost-optimization opportunity of 2025-2026, with a discount of up to 90%.
| Provider | How it triggers | Hit discount | Minimum length for a hit | TTL | Write |
|---|---|---|---|---|---|
| Anthropic | explicit (cache_control marker) |
90% (cached input $0.30 vs base $3.00 / Sonnet) | 1024 tokens (Sonnet) / 2048 (Haiku) | 5 min (default) / 1h (paid) | 1.25× base |
| OpenAI | automatic (a prefix of ≥1024 tokens is cached automatically) | 50-90% (depends on the model) | 1024 tokens | 5-10 min | 0 (writes are free) |
| Google Gemini | implicit (on by default for 2.5+) + explicit API | implicit about 50% / explicit about 75% | 4096 tokens | default 1h | billed by storage |
| AWS Bedrock | translated for every provider | same as each provider | same | same | same |
Empirical result (public data from ProjectDiscovery): once prompt caching is wired up, the monthly LLM bill drops 59-70%. Academic work (arXiv 2601.06007) on long-horizon agent tasks reports API cost down 41-80% and TTFT down 13-31%.
Score by “reuse frequency × length”:
| Content type | Worth caching? |
|---|---|
| System prompt + persona | ★★★ strongly recommended |
| A large set of few-shot examples | ★★★ |
| Tool-definition schemas (10+ tools) | ★★★ |
| Fixed documents retrieved by RAG (for example an SLA / policy handbook) | ★★ |
| Conversation history that changes | ✗ do not cache (cache breakdown) |
| A user query that differs every time | ✗ |
Rule of thumb: if a prefix’s repeat rate is > 50%, cache it; otherwise do not.
from anthropic import Anthropic
client = Anthropic()
# system + tools + 大段 RAG 都加 cache_control
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=[
{
"type": "text",
"text": "你是 ACME 公司客服,遵循以下规则...", # 长 system
"cache_control": {"type": "ephemeral"}, # ★ 标记为可缓存
},
{
"type": "text",
"text": KB_KNOWLEDGE_BASE, # 100K token 的知识库
"cache_control": {"type": "ephemeral"},
},
],
tools=[
{"name": "search", "description": "...", "input_schema": {...},
"cache_control": {"type": "ephemeral"}},
],
messages=[
{"role": "user", "content": user_query}, # 这部分不缓存
],
)
# 看 cache 命中
print(f"Cache read: {response.usage.cache_read_input_tokens}")
print(f"Cache write: {response.usage.cache_creation_input_tokens}")
print(f"Regular input: {response.usage.input_tokens}")
# Cost 计算
cost = (
response.usage.cache_read_input_tokens * 0.30 / 1e6 # 命中超便宜
+ response.usage.cache_creation_input_tokens * 3.75 / 1e6 # 写入贵 1.25×
+ response.usage.input_tokens * 3.00 / 1e6 # 普通输入
+ response.usage.output_tokens * 15.00 / 1e6 # 输出
)
cache_control marker, the text must be identical — one extra space is a miss.cache_control, and watch the cache hit rate alongside observability.When an agent runs a long-horizon task, the prompt is “a prefix (the system) + conversation history that keeps being appended.” Appending new content does not break the prefix cache, so the prefix portion keeps hitting:
Round 1: [system] [user1] → write cache(system)
Round 2: [system] [user1] [bot1] [user2] → cache HIT 前缀,付钱仅 [bot1][user2]
Round 3: [system] [user1] [bot1] [user2] [bot2] [user3] → cache HIT 更长前缀
...
The reverse: if you insert / modify / delete anything in the middle (for example, a fresh RAG retrieval stuffing different chunks into the system prompt), the cache is invalidated immediately, and the next call is billed in full again.
A hidden best practice of agent design: put “content that changes” at the tail; the prefix is only ever appended to, never modified.
# Langfuse 把 cache metrics 也记下来
@observe(metadata={"cache_strategy": "anthropic_explicit"})
def my_agent_call(query):
response = client.messages.create(...)
langfuse_context.update_current_observation(
usage={
"cache_read": response.usage.cache_read_input_tokens,
"cache_write": response.usage.cache_creation_input_tokens,
"regular_input": response.usage.input_tokens,
"output": response.usage.output_tokens,
},
)
return response
On the dashboard, watch:
cache_read / total_input) — target > 60%Wiring tools and data sources into an LLM has long been dirty work:
The mathematical model: M LLMs × N tools = M·N adapters. 10 LLMs × 50 tools = 500 integration points.
MCP (Model Context Protocol) [9] is an open standard Anthropic released in November 2024. It splits M·N into M+N:
没有 MCP 有 MCP
┌───┬───┬───┐ ┌───┬───┬───┐
│GPT│Cld│Gem│ │GPT│Cld│Gem│
└─┬─┴─┬─┴─┬─┘ └─┬─┴─┬─┴─┬─┘
│ │ │ × N tools └───┼───┘
各连各的 (M·N) │ MCP protocol (JSON-RPC 2.0)
│
┌──────┼──────┐
│ │ │
┌─┴─┬──┬─┴─┬──┬─┴─┐
│GH │DB│Fs │Sl│...│ ← N tools
└───┴──┴───┴──┴───┘
The analogy: before USB-C, every phone had its own cable (M·N); after USB-C, any host connects to any peripheral (M+N). MCP is the USB-C of AI.
The capabilities an MCP server exposes to a client fall into three classes:
| Primitive | Meaning | Example | Who triggers it |
|---|---|---|---|
| Tools | an executable function, with side effects | create_issue, run_sql |
the model decides to call it |
| Resources | read-only data, addressed by URI | file:///doc.md, db://table/users |
the application preloads it |
| Prompts | a template the user can invoke | /explain-code, /summarize |
the user triggers it |
These three classes correspond to the three data flows of an LLM application: the model calls on its own initiative, the application injects, and the user invokes.
| Transport | Where it fits | How it is deployed |
|---|---|---|
| stdio | local inter-process communication (the default for Claude Desktop / Cursor / Cline) | a child process, configured in mcp_servers.json |
| SSE (Server-Sent Events) | a remote service, one-way stream | an HTTP server, long-lived connection |
| Streamable HTTP (the 2025 replacement for SSE) | a remote service, bidirectional stream, more standard | an HTTP server; the de facto standard |
# server.py
from mcp.server.fastmcp import FastMCP
from datetime import datetime
mcp = FastMCP("my-toolkit", version="1.0.0")
# === Tools(有副作用的函数)===
@mcp.tool()
def create_github_issue(repo: str, title: str, body: str) -> dict:
"""在指定 GitHub repo 创建 issue。
Args:
repo: owner/name 格式
title: issue 标题
body: issue 正文(markdown)
Returns:
{"id": int, "url": str}
"""
# 实际调用 GitHub API
response = github.create_issue(repo, title, body)
return {"id": response.id, "url": response.html_url}
@mcp.tool()
async def run_sql(query: str, params: list = None) -> list[dict]:
"""对只读副本执行 SQL 查询。"""
# 安全:用 parameterized query
result = await db.fetch_all(query, params or [])
return [dict(r) for r in result[:1000]] # 上限 1000 行
# === Resources(只读数据)===
@mcp.resource("docs://{topic}")
def get_docs(topic: str) -> str:
"""获取某主题的内部文档。"""
return knowledge_base.get(topic)
@mcp.resource("user://{user_id}/profile")
def get_user_profile(user_id: str) -> dict:
"""获取用户档案(仅 PII redacted 版)。"""
return user_db.get_redacted(user_id)
# === Prompts(用户模板)===
@mcp.prompt()
def code_review(code: str, language: str = "python") -> str:
"""让 AI 做代码审查。"""
return f"""请对以下 {language} 代码做 code review:
- 找出潜在 bug
- 评估可读性
- 建议优化
```{language}
{code}
““”
if name == “main”: mcp.run(transport=“stdio”) # 或 sse / streamable-http
#### TypeScript (`@modelcontextprotocol/sdk`)
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "my-toolkit", version: "1.0.0" });
// Tool
server.tool(
"create_issue",
{ repo: z.string(), title: z.string(), body: z.string() },
async ({ repo, title, body }) => {
const result = await github.createIssue(repo, title, body);
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
// Resource
server.resource(
"user-profile",
"user://{user_id}/profile",
async (uri, { user_id }) => ({
contents: [{ uri: uri.href, mimeType: "application/json",
text: JSON.stringify(await userDb.get(user_id)) }],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
~/.config/claude/mcp_servers.json (Mac/Linux), or a similar configuration in Cursor / Cline:
{
"mcpServers": {
"my-toolkit": {
"command": "python",
"args": ["/abs/path/to/server.py"],
"env": {"GITHUB_TOKEN": "ghp_..."}
},
"github-official": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..."}
},
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem",
"/Users/liang/projects"]
}
}
}
Restart the client, and every tool / resource / prompt is available automatically.
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent
# 一次连多个 MCP server
async with MultiServerMCPClient({
"my-toolkit": {"command": "python", "args": ["./server.py"], "transport": "stdio"},
"github": {"url": "http://localhost:3000/sse", "transport": "sse"},
}) as client:
tools = client.get_tools() # 自动转成 LangChain Tool 列表
agent = create_react_agent("anthropic:claude-sonnet-4-6", tools)
result = await agent.ainvoke({"messages": [("user", "在 acme/api repo 开个 bug issue")]})
Pick by category:
| Category | Repository | Use |
|---|---|---|
| Official reference implementations | modelcontextprotocol/servers |
dozens that run out of the box: filesystem / git / github / postgres / sqlite / slack / google-drive / brave-search / sequential-thinking |
| Awesome list | punkpeye/awesome-mcp-servers |
a community server index (hundreds) |
| Databases | postgres / mysql / sqlite / mongodb / redis each have an official or community MCP | |
| Cloud services | aws-mcp / gcp-mcp / azure-mcp | |
| DevOps | kubernetes / terraform / docker MCP | |
| Productivity | notion / linear / jira / confluence MCP | |
| Browsers | playwright-mcp / browserbase-mcp / Puppeteer MCP | |
| Commercial enterprise | Zapier MCP (5000+ integrations) |
| Time | Event |
|---|---|
| 2024-11 | Anthropic publishes the MCP protocol |
| 2025-Q1 | IDEs such as Cursor / Cline integrate it |
| 2025-Q2 | OpenAI announces support |
| 2025-Q3 | Google / Microsoft / Amazon follow |
| 2025-Q4 | MCP governance moves to an independent MCP Steering Committee (jointly governed by Anthropic + Block + OpenAI + Microsoft and others) |
| 2026-02 | Monthly downloads of the Python + TypeScript SDKs exceed 97 million |
| 2026-Q1 | Nearly every mainstream LLM client supports it natively |
MCP solves communication between an agent and its tools. A2A (Agent-to-Agent) solves communication between agents. This area is still a contest among several protocols:
| Protocol / framework | Origin | Paradigm | Status |
|---|---|---|---|
| Google A2A | Google 2025 | HTTP-based agent discovery + task delegation | early |
| Microsoft Agent Framework Protocol | Microsoft 2026 | successor to AutoGen; a standard for multi-agent conversation | being promoted |
| Anthropic Agent Skills | Anthropic 2026 | Skill package: a reusable unit of agent capability | early |
| MCP for A2A | community | treat “another agent” as an MCP server | in practice |
| Letta multi-agent | Letta | via message passing | inside the framework |
The analogy between MCP and A2A:
“MCP gives an agent a pair of hands (connecting tools / data). A2A lets multiple agents team up and do the work. MCP is vertical integration (a single agent ↔︎ a tool stack). A2A is horizontal collaboration (agent ↔︎ agent network).”
The state of affairs in spring 2026: MCP is already the de facto standard, and A2A is still in its Cambrian explosion. Practical advice: building a single agent now → go all-in on MCP; building multiple agents → use the built-in protocol of a concrete framework (LangGraph / Microsoft Agent Framework), and wait until the A2A standard is clear.
| Trap | Symptom | Countermeasure |
|---|---|---|
| Too many tools | 50+ tools make the LLM’s decision hard | group + route (pick a category first, then a tool) |
| Tool description too short | the model does not know when to use it | write “when to use / when not to use” + 1-2 examples |
| Side effects left undeclared | the model treats the call as read-only and causes an unexpected modification | the description states explicitly that it “will modify X”, and the client side adds deny-first |
| Return value too long | one return of 10K rows blows up the context | server-side cap + pagination + summary |
| Synchronous blocking | a long task freezes the client | use an async transport (streamable-http) |
| Authentication is a mess | each tool handles its own token | the server reads tokens uniformly from env; the client uses an OAuth flow |
Details of tool integration at the agent-implementation layer → agent-engineering.en.md and agent-engineering.en.md; infrastructure at the LLM gateway / observability / inference-server layer → harness-engineering.en.md appendix (note: chapter 04 explicitly marks these 4 kinds as “related but not harness engineering”).
[2] Liu, N. F. et al. (2024). Lost in the Middle: How Language Models Use Long Contexts. TACL. [5] Karpathy, A. Software Is Changing (Again). YC AI School talk. [6] Anthropic. Effective Context Engineering for AI Agents. https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents [7] LangChain. Context Engineering for Agents. https://www.langchain.com/blog/context-engineering-for-agents [9] Wikipedia. Model Context Protocol. https://en.wikipedia.org/wiki/Model_Context_Protocol [12] Hsieh, C. et al. (2024). RULER: What’s the Real Context Size of Your Long-Context Language Models? https://arxiv.org/abs/2404.06654 [13] Cheney Zhang. Embedding Models Benchmark 2026. [14] CallSphere. Vector Database Benchmarks 2026: pgvector / Qdrant / Weaviate / Milvus / LanceDB. [18] Packer, C. et al. (2023). MemGPT: Towards LLMs as Operating Systems. [19] Mem0 Team (2025). Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory. https://arxiv.org/abs/2504.19413 [21] Anthropic Memory Tool docs. https://docs.anthropic.com [41] TianPan. Long-Context Models vs RAG: When the 1M-Token Window Is the Wrong Tool. [43] Squirro. RAG in 2026: Bridging Knowledge and Generative AI.
Additional: - Asai et al. (2023). Self-RAG. - Microsoft GraphRAG. https://github.com/microsoft/graphrag - LightRAG. https://github.com/HKUDS/LightRAG - Edge et al. (2024). From Local to Global: A Graph RAG Approach to Query-Focused Summarization. - BGE-M3. https://huggingface.co/BAAI/bge-m3
Concepts marked
[term]in the answers jump to the matching entry in this table and expand it. The table groups 60+ high-frequency terms under 10 topics and is collapsed by default — expand a term only when you don’t know it, and read the short explanation, so you are not buried in information.
Analyze source without actually running it, and flag potential bugs, unused variables, style violations, and suspicious usage. Common tools:
In agent engineering, lint is often used as a hard constraint in the harness — feeding lint errors back to the model to rewrite the code is much more reliable than merely saying “please follow the coding standards” in the prompt (Augment Code calls this a Constraint Harness).
Make sure types are consistent across assignments and function calls:
For an agent: after the agent finishes writing code, force a run of tsc --noEmit / mypy . / cargo check, and feed type errors back to the model to keep fixing. That is much more reliable than letting the agent “claim it checked the types” — a typical practice of harness engineering.
CI (Continuous Integration): on every commit, automatically run tests, builds, lint, type check, and other checks. CD (Continuous Deployment): after the checks pass, automatically deploy to test / staging / production.
Common tools: GitHub Actions, GitLab CI, Jenkins, CircleCI, Buildkite.
In agent engineering, CI is the most important deterministic feedback source — an LLM’s self-assessment is unreliable, but CI pass/fail is a 0/1 signal and can be used directly as ground truth. Hashimoto’s harness engineering stresses that whether a problem gets solved is strongly tied to whether the agent can verify its own work.
Run untrusted code (user uploads, LLM-generated code) in a restricted environment so it cannot damage the host. Isolation strength, from weak to strong:
An agent that runs user- or LLM-generated code must be sandboxed, with defense in depth on top: a network-egress allowlist + a read-only filesystem + time/memory quotas + no host credentials mounted.
The name comes from using canaries in coal mines to detect poison gas. A typical deploy sequence is: shadow (dual-run, do not return the result) → 1% → 5% → 25% → 50% → 100%, and each stage advances only if the core metrics show no regression.
For LLM applications, a new prompt / new model / new agent harness must go out through a canary. If any key metric (resolution rate, CSAT, refusal rate, p99 latency, cost per request) worsens past a threshold, roll back. The non-determinism of LLM applications makes canaries more important than they are for traditional software.
Hash on user_id to split traffic so a given user always lands in the same bucket (avoiding a flickering experience). What to judge:
Prompt changes, model swaps, and RAG changes in an LLM application should all go through an A/B test. Shipping on offline eval alone often fails in production (the live traffic distribution differs a lot from the gold set).
The metrics the business side watches most closely when deciding whether an agent should launch:
What agent engineering watches: put model / prompt / RAG changes into an A/B test and look at these business metrics, not only offline benchmark scores. It is very common for a change to raise the benchmark while GMV / CSAT fall.
Pause an autonomous agent at a critical node and wait for a human:
Design principle: “Humans steer. Agents execute.” (OpenAI Harness Engineering). An agent does not replace a person; it amplifies a person’s judgment — the person sets the plan, sets the constraints, and reviews the result, and the agent does the execution.
Four terms defined by Google’s SRE practice:
1 - SLO. An SLO of 99.9% means 43.2 minutes of “budget” in 30 days during which the target can be missed. Once the budget is spent, non-essential releases are frozen.How to use this for an agent application:
Rule of thumb: a stricter SLO is not automatically better — 99.99% costs 5–10× more than 99.9%, and it is likely past the threshold users can perceive. Google recommends the level that is just high enough that users don’t complain.
It expires automatically. Common uses:
A rule-of-thumb conversion: 1000 English words ≈ 1300 tokens; 1000 Chinese characters ≈ 1500–2000 tokens.
Why it matters: the context window, API billing, and inference speed are all counted in tokens. Controlling the token count = controlling cost and latency. Anthropic, OpenAI, and Google each have their own tokenizer, and a ±20% difference in token count for the same text is common.
Text → a neural network → a vector of 768 / 1024 / 1536 / 3072 floating-point numbers. Vectors for synonyms or same-topic text sit close together in that space (high cosine similarity).
Main uses:
Mainstream models in 2026: OpenAI text-embedding-3-large, Cohere v4, Voyage-3, BGE-M3 (open-source SOTA; Chinese–English bilingual, combining dense / sparse / multi-vector).
It includes the system prompt, conversation history, user input, and model output; all of them count. Mainstream models in 2026:
Note: the advertised window ≠ the effective window. The RULER benchmark shows that most models’ performance drops sharply beyond 1/4 of the window (Lost in the Middle). Engineering guidance sets the threshold for “what should go into context” at 8–32k; anything longer goes through RAG or hierarchical summarization.
The two phases have different bottlenecks, so the tuning strategies differ too — prefill is optimized with parallelism / FlashAttention, and decode is optimized with continuous batching / speculative decoding. Because the two phases have opposite characteristics, engineering also uses two kinds of scheduling optimization: PD disaggregation (Prefill/Decode Disaggregation, handling the phases on different GPU pools, as in DistServe/Mooncake) and chunked prefill (split a long prefill into chunks and interleave them with decode). See A11 for detail.
Generating token t needs the K/V of every earlier token in the attention computation. Caching them so they are not recomputed every time is the most basic optimization in LLM inference.
Memory use: 2 × num_layers × num_heads × head_dim × seq_len × batch × bytes_per_elem. On a 70B model, the KV cache for a 128k context can reach tens of GB, far more than the model weights themselves.
Further optimizations:
How GPT-style decoder-only models generate: P(x_t | x_1, ..., x_{t-1}). Every generated token runs a full forward pass (even though the KV cache can be reused), so output length directly determines latency.
By comparison: BERT is non-autoregressive (masked LM: it predicts every masked position at once), and diffusion models are also non-autoregressive (they denoise the whole span in one shot).
The cost of autoregression produced a large family of speedups: speculative decoding, parallel decoding, Medusa, EAGLE, and others.
The last decoder layer outputs a logit vector of dimension [vocab_size]; after softmax it is the probability distribution of the next token.
Places engineering can get the logits (or top-k logprobs):
Note: commercial OpenAI / Anthropic APIs return only the sampled result by default; you have to turn the logprobs option on explicitly.
P(token) ∝ exp(logit / T):
Turn it up when: Self-Consistency needs diverse paths, creative writing, brainstorming. Turn it down when: code generation, math reasoning, strict formatted output, T=0.1–0.3.
Trap: T=0 is not fully identical to deterministic — non-determinism of floating-point GPU kernels plus concurrent scheduling can still leave tiny differences for the same prompt.
Xiao et al. 2023 found that during long-sequence inference, later tokens often assign a disproportionate share of attention to the first few tokens of the sequence (even when those tokens are semantically unrelated). Those tokens become a “sink” and absorb the normalization mass that softmax is required to distribute.
Why it matters in engineering:
LayerNorm: y = (x - μ) / σ * g + b, mean and variance taken along the hidden dimension. Independent of the batch, so it naturally supports variable-length sequences and single-example inference — which is why Transformers use LayerNorm rather than BatchNorm.
RMSNorm (Zhang & Sennrich 2019): y = x / sqrt(mean(x²)+ε) * g, dropping the mean subtraction and the bias. Modern LLMs such as Llama, PaLM, and Mistral have almost all switched to RMSNorm, saving 7–64% of normalization time without hurting quality.
Pre-LN vs Post-LN: doing LN before the residual (Pre-LN) trains more stably than doing it after the residual (Post-LN), and warmup can be dropped. From GPT-2 onward, Pre-LN is nearly universal.
Su et al. 2021. Represent position m as a rotation matrix R_m applied to Q/K, so that <R_m·q, R_n·k> = f(q, k, m-n) — the attention score depends only on relative distance.
Advantages: (1) no extra parameters; (2) relative position is encoded naturally; (3) compatible with linear attention. It has become the position-encoding scheme of the great majority of modern LLMs, including Llama, Qwen, and Mistral.
The extrapolation problem: naive extrapolation from 2k to 32k collapses — the rotation angles of high-frequency components fall into regions never seen in training. Mitigations:
Naive extrapolation of RoPE collapses — the rotation angles of high-frequency components fall into regions never seen outside the training context. Two engineering schemes:
Where they are used:
Numbers from practice: YaRN is nearly lossless at a 4× context extension (2k→8k or 32k→128k); beyond 8× there is still a performance drop. Extreme long context (1M+) usually needs retraining rather than YaRN alone.
Standard multi-head attention: each of the H heads has its own Q/K/V.
The gain: the KV cache shrinks to 1/8 ~ 1/H, inference throughput rises substantially, and quality is almost unchanged. Llama-2 70B, Llama-3, Mistral, Qwen2, and others use GQA.
Replace the traditional dense FFN with N parallel expert FFNs plus a router. Each token picks the top-k experts through the router (k is usually 1–2) and only those experts are activated for the compute.
Advantage: a large parameter count with little compute. Mixtral 8x7B has 47B total parameters and only 13B activated; DeepSeek-V3 has 671B total parameters and only 37B activated. At the same inference cost, capacity far exceeds a dense model.
Challenges: (1) load balancing during training (so one or two experts are not overused); (2) memory is counted by total parameters, not activated parameters; (3) communication cost is high (expert parallelism).
Single-head attention produces one attention distribution for each pair of tokens — one “relational viewpoint”. Multiple heads (h=8/32/64/128) let the model learn several relations at once: syntactic heads attend to short-range dependencies, and semantic heads attend to coreference and long-range entities.
Why a single head with a larger dimension does not work: multi-head is a mixture of h low-rank attentions and is strictly more expressive; a single head is one high-rank attention and has fewer degrees of freedom.
A further variant: GQA / MQA drops heads on the KV side to save memory, while the Q side stays multi-head.
Dao 2022. Standard attention uses O(N²) memory (it stores an N×N attention matrix), which bottlenecks long sequences. FlashAttention loads Q/K/V in blocks into GPU SRAM for the compute and does not write out the full attention matrix. Memory drops to O(N), and it is also faster — attention is memory-bound, so reading less HBM makes it faster.
The line of work:
Kwon et al. 2023. It addresses three kinds of fragmentation in the traditional inference KV cache:
Mechanism: cut the KV cache into fixed-size blocks (for example 16 tokens), and each sequence keeps a block table (a logical→physical mapping). Memory utilization rises from ~20% to >96%; multiple samples of the same prompt share prefix blocks; together with continuous batching, throughput is 2–4× HuggingFace TGI and 24× FasterTransformer.
The de facto standard open-source inference server since 2023. Core features:
Compared with: TGI (HuggingFace), TensorRT-LLM (NVIDIA; proprietary and faster), SGLang (more thorough prefix reuse), LMDeploy (from OpenCompass).
Also called in-flight batching, proposed by Yu et al., ORCA, 2022. Compared with static batching (wait until the batch is full and then run together; when short and long requests are mixed, GPU utilization is often <30%), continuous batching:
vLLM’s numbers: throughput is 5–23× static batching. It is now the default in every modern inference engine.
Leviathan et al. 2023. The procedure:
Mathematically this guarantees the output distribution matches the original model exactly (rejection sampling).
When it pays off: (1) the draft and target distributions are similar (acceptance rate >60%); (2) the large model is memory-bandwidth bound, so parallel verification does not add latency. Acceptance rate is 70–90% for code generation and 30–50% for open-ended generation. Variants: EAGLE-2/3, Medusa, Llama 3.1 self-speculative.
The KV cache is reuse within a single request; prefix caching is reuse across requests. When many requests share a system prompt / few-shot examples / agent history, compute that KV once, cache it, and later requests hit the cache and skip prefill.
Effect: in agent / RAG / multi-turn settings, the system prompt is usually 1–4k tokens and shared by almost every request, so TTFT can drop 10–100×.
Implementations: vLLM --enable-prefix-caching, SGLang RadixAttention (a radix tree that automatically manages every historical prefix), the Anthropic prompt caching API (5-minute TTL; the cached portion is priced at 0.1×).
Driven mainly by prefill (processing every input token). One of the core metrics for billing and monitoring.
Ways to lower TTFT:
Companion metric: TPOT (time per output token, the gap between tokens, also called TBT/ITL) — determined by decode.
The other core latency metric, paired with TTFT: TTFT measures “how long until the first character appears” (determined by prefill), and TPOT measures “how fast each following character appears” (determined by decode). Perceived speed of streaming ≈ 1/TPOT (tokens/s).
Mixed-precision training: weights and gradients in BF16/FP16, optimizer state still in FP32.
At deployment, quantize weights to low-bit integers and dequantize back to floating point for inference.
INT8 / INT4 quantization usually needs a calibration set (128–512 samples); AWQ is smarter (it protects the 1% salient channels). For extreme memory budgets, use GGUF (the llama.cpp format) plus 3-bit / 2-bit quantization to run on phones.
Lin et al. 2023. The core observation: protecting only ~1% of the “important weight channels” (identified by activation magnitude) is enough to hold accuracy. Scale those channels up per channel, then quantize.
Compared with GPTQ: simpler to implement, lower perplexity on Llama-2, and now the default in vLLM / TensorRT-LLM.
Frantar 2022. Layer-wise minimization of quantization error, inspired by OBQ, with a small calibration set (128 samples). Pros: early, and the ecosystem is complete (exllama / vllm / TGI all support it). Cons: sensitive to the activation distribution; important channels can be crushed.
Deployment choices in 2026: H100/H200 → FP8; A100/3090 → AWQ INT4; phones/edge → INT4 + GGUF.
The most basic form of instruction tuning: treat (input, desired output) pairs as supervised samples and use cross-entropy loss to teach instruction following.
Data volumes from practice:
The LIMA paper: 1000 curated samples can bring a 65B base close to GPT-4 conversation quality — quality >> quantity.
Hu et al. 2021. Factor the weight update into two low-rank matrices ΔW = BA (r=8/16/64) and train only those matrices, cutting trainable parameters by 100–10000×.
Memory:
Advantages: few trainable parameters / multiple adapters can be hot-swapped / mitigates catastrophic forgetting.
Dettmers et al. 2023. Three key pieces:
QLoRA for a 65B model is about 33GB and fits on one A100-48G or two RTX 3090s. Cost: training is 30–40% slower (dequantization overhead).
Three steps: (1) SFT trains a base policy; (2) humans label (chosen, rejected) preference pairs to train a Reward Model; (3) the RM scores policy outputs, and algorithms such as PPO/RLOO/REINFORCE update the policy.
InstructGPT and ChatGPT brought RLHF into the mainstream. Three major pain points:
This produced simplifications and improvements such as DPO, GRPO, and RLVR.
Trained in step two of RLHF. Input: (prompt, response). Output: a scalar reward. The training loss is Bradley-Terry: make the RM score chosen higher than rejected on (chosen, rejected) pairs.
Common weaknesses:
Alternatives: DPO removes the RM entirely, and RLVR uses a programmatically judged reward.
Rafailov et al. 2023. It proves that under a Bradley-Terry preference model plus a KL constraint, the optimal policy has a closed form, so preference pairs (chosen, rejected) can be used directly as a supervised loss:
L_DPO = -log σ(β(log π(y_w)/π_ref(y_w) - log π(y_l)/π_ref(y_l)))
Advantages: (1) no RM / no online sampling / no critic; only the policy and a reference model; (2) uses the same training stack as SFT; (3) hyperparameters are stable.
Disadvantages: depends heavily on preference-data quality; easy to overfit surface patterns of the chosen answers; high reward-hacking risk.
The main post-training path for Llama-3 and Qwen2 is DPO or a variant (IPO, KTO, SimPO).
Schulman 2017. The actor learns the policy, the critic learns a value function to estimate advantage, and clipping limits how far the policy can move each step so it doesn’t collapse.
RLHF must load four models at once — actor / ref / critic / reward — so memory is ≥4× a single model. Hyperparameters are sensitive (KL coefficient, clip ratio, advantage normalization), and training is unstable.
This produced simplifications such as DPO (drop RL), GRPO (drop the critic), and RLOO (use mean reward as the baseline).
Proposed in DeepSeek-Math 2024 and validated by R1. The core change: drop the critic (the value network). Sample G responses for the same prompt (typically G=64), normalize advantage by the within-group mean and standard deviation of the reward: A_i = (r_i - mean(r)) / std(r), then apply a PPO-like clipped objective plus KL regularization.
Effects:
On tasks that can be checked automatically — math, code, formal logic — use a program as the reward: answer match, unit tests passing, a theorem prover verifying.
Advantages:
DeepSeek-R1 and DAPO (ByteDance’s open-source SOTA) are both in this family. Limitation: it applies only to verifiable tasks; open-ended dialogue still needs RLHF / DPO.
Mechanism: parameters are pulled by the new task’s loss, and directions that encoded general knowledge get overwritten.
A mitigation mix (recommended in production):
Advanced: EWC (Fisher-information-weighted L2 regularization), model souping (average the fine-tuned weights with the original model), and extending MoE routing.
Wei et al. 2022. The simplest form: “Let’s think step by step” (Kojima 2022 zero-shot CoT).
Why it works: (1) more effective compute (more forward steps); (2) it turns “System 1” intuitive answers into “System 2” step-by-step reasoning; (3) it exposes faulty reasoning so self-consistency voting can correct it.
It is an emergent ability and is almost useless on models under 10B. Long CoT (trained with RL in R1 / o1, including backtracking and self-verification, thousands of tokens long) is the new paradigm of 2025.
Yao et al. 2023. CoT is a single linear chain and cannot go back once it is wrong. ToT generates several candidate thoughts at each step, the LLM self-evaluates state value (promising / sure / impossible), and BFS/DFS expansion can backtrack and prune.
On Game of 24, GPT-4 CoT scores 4% vs ToT 74%. Cost: LLM calls rise from 1 to 50–200.
Fits: solution spaces that need explicit search (puzzles, planning, theorem proving) where intermediate states can be scored. Does not fit: chitchat, summarization, translation.
Yao et al. 2022. The structure is a Thought → Action → Observation loop: the model first writes “Thought: I need to look up X”, then outputs “Action: search[X]”, the tool result is fed back into the prompt as “Observation: …”, and the next Thought follows.
Three effects:
It is the conceptual prototype of modern agent frameworks (Claude tool use, OpenAI function calling, LangChain, AutoGPT).
Shinn et al. 2023. After a task fails (an external verifier gives a binary reward), the agent uses an LLM to summarize the cause as a natural-language lesson, stores it in episodic memory, and adds that lesson to the prompt on the next task to change behavior.
vs Self-Refine:
On HumanEval, GPT-4 with Reflexion scores 91%, above the GPT-4 baseline of 80%.
Madaan et al. 2023. Loop N rounds inside one trajectory, with no external reward and no cross-task memory. Average gain of 20% across 7 tasks.
Trap: a weak model’s self-critique is inaccurate and revisions can get worse. Production often combines Self-Refine (within an episode) with Reflexion (across episodes).
Wang et al. 2022. With temperature>0, sample N CoT paths and vote on the final answer. On GSM8K this is about 17 percentage points above greedy CoT (PaLM 55%→74%).
Pays off when: (1) the answer space is discrete (numbers, multiple choice); (2) the model’s error rate is <50%; (3) reasoning paths are diverse (temperature 0.7–1.0). Cost: N× (typically N=5–40). It does not apply to open-ended generation.
When to use few-shot: (1) the task format is novel or the output structure is complex (a JSON schema, a specific markdown layout); (2) a classification task has many labels.
When to use zero-shot: modern models are already saturated on general tasks (summarization, translation, QA), and adding examples can introduce bias.
Traps: example order matters a lot (recency bias); accidental regularities get learned; too many examples run into Lost in the Middle. Keep it to ≤8.
The standard pipeline = Ingestion + Retrieval + Generation:
Every stage has a bottleneck: chunk size, embedding choice, insufficient recall, context order, and the model not staying faithful to the context. Production RAG ≠ “embedding + a vector store + an LLM” — it is an engineering system.
Strategies:
Okapi BM25. Relevance of a document to a query is Σ IDF(qi) · f(qi,d) · (k+1) / (f + k·(1-b+b·|d|/avgdl)). Strengths: exact term hits, high weight on rare terms, proper nouns, numbers. Weakness: synonyms don’t match (“医生” vs “doctor”).
It complements dense embeddings — hybrid search fuses the two, and recall is 5–15 percentage points above dense alone.
Dense embeddings are good at semantic similarity; BM25 is good at exact term match; the two complement each other.
Fusion methods:
score(d) = Σ 1/(k + rank_i(d)), k=60. The production default; the two score streams do not need to be normalizedα·dense + (1-α)·bm25. You have to tune α and normalize the scoresAnthropic’s Contextual Retrieval experiment: embedding alone → + contextual embedding → + BM25 → + rerank, with failure rates 5.7% → 4.0% → 3.7% → 1.9%.
Bi-encoder (embedding): encode the query and the doc separately, then compare by cosine. Fast, but there is no interaction. Cross-encoder (reranker): concatenate query + doc and feed them to a transformer; self-attention at every layer lets them interact. Relevance modeling is 20–40% stronger (NDCG@10), and it is 100× slower.
The production standard: bi-encoder recalls top-100 → cross-encoder reranks to top-10 → feed the LLM.
Mainstream: Cohere Rerank v3/v4, BGE-reranker-v2-m3 (open-source SOTA for Chinese and English), Jina Reranker v2, Mixedbread.
Trap: if the relevant document is not in the recalled top-100, reranking cannot save it — a reranker improves what was already recalled and cannot stand in for recall.
A 2026 comparison of mainstream embedding models:
Selection framework:
Key traps:
Mainstream:
ANN algorithms: HNSW (the most common), IVF, ScaNN (Google). Index time vs retrieval accuracy vs memory is a trade-off.
Naive RAG struggles with cross-document, cross-entity reasoning, unstable recall quality, and multi-hop queries. Four common advanced schemes:
Practical choice:
Es et al. 2023. The four metrics:
Production thresholds: all four ≥0.7 counts as passing; if Faithfulness < 0.7, fix that first; if Context Recall < 0.7, tune the retriever.
Named by Mitchell Hashimoto in 2026: “anytime you find an agent makes a mistake, take the time to engineer a solution such that the agent never makes that mistake again.”
This is not model fine-tuning. It is an engineered environment that makes it physically impossible for the model to fail in the same way again.
Two laws:
OpenAI’s formula: Agent = Model + Harness; “Humans steer. Agents execute.”
A comparison of mainstream frameworks, 2024–2026:
Practice: prototype by calling the SDK directly; consider LangGraph once workflow patterns (routing / parallelization / orchestrator-workers) have settled; consider LlamaIndex for pure RAG; don’t start with the LangChain “full suite”.
Released in 2024-11, and moved to an independent Steering Committee in 2025-Q4 (Anthropic / Block / OpenAI / Microsoft). Three core primitives:
It solves the N×M problem of “writing a new adapter for every new tool”. The single fastest-growing skill in 2026, 97M+ monthly downloads, and already an explicit requirement on the Anthropic Applied AI Engineer JD.
The model outputs {"name": "search", "arguments": {"query": "..."}} JSON; the runtime parses it, actually runs the function, and feeds the result back to the model. Anthropic’s Tool Use API and OpenAI function calling are the same idea.
Design principles (Anthropic’s measurements: “agent performance is proportional to how clear the tool descriptions are”):
Anthropic released Computer Use in 2024-Q4. The loop: screenshot → the LLM looks at the image and reasons → it outputs mouse and keyboard actions (click X,Y / type text / scroll) → the system executes them → screenshot again.
Browser Use (an open-source framework) is similar but limited to the browser, based on Playwright plus DOM augmentation (overlay numbered labels on clickable elements in the screenshot).
Useful for: automating sites with no API, automating desktop software, end-to-end e2e tests. Limits: slow, expensive, weak on security, and poor support for canvas-rendered apps. Production advice: if an API exists, use the API.
The main agent hands “read 30 files and research them” to a sub-agent. The sub-agent has its own context window and returns only the final summary to the main agent — the main context is not polluted by intermediate artifacts.
Claude Code’s Task tool, OpenAI Swarm, CrewAI, and others support this. It is one of the key ways to prevent a Loop of Death and a context explosion.
The agent reads it automatically when it enters the project. It usually contains:
Hashimoto’s view: each line of AGENTS.md ≈ one mistake sealed off. It is the implicit-prompt form of harness engineering.
The permission model Claude Code uses. Bash commands are authorized at verb + path granularity:
git status allowgit push --force deny (destructive)rm -rf pops a user confirmationcat ~/.ssh/* deny (sensitive path)The matching security principle: if any one layer fails, the others still catch it. Combine it with a sandbox, a network-egress allowlist, and a read-only filesystem for defense in depth.
A core design from Anthropic’s Effective Harnesses: a long-running agent will inevitably blow the context window, and LLM context alone cannot remember “which step I am on”.
The fix:
The next session runs the 5-step startup protocol and resumes from the breakpoint. Core insight: the context window is “working memory”; the filesystem is “long-term memory”.
Common patterns:
Defense in depth:
Example: a user jokes “I’m a billionaire”, it is stored in semantic memory, and the agent reasons from that assumption from then on.
Defenses:
“Full-trace plus evaluation” tools for LLM apps and agents. Mainstream in 2026:
Capabilities to line up:
How to choose: prefer Langfuse if you self-host; LangSmith for heavy LangChain users; Phoenix for trace monitoring alone; Patronus for safety / real-time blocking.
Grouped by the capability being measured:
In practice: from a 2026 point of view, no single benchmark is trustworthy anymore — you need three layers: cross-checking several benchmarks, your own gold set, and blind human votes on Arena. See E11 and H3.
Eval for an LLM or agent application is not a single action; it is a system. Common layers:
How the eval tool stack splits:
Eval types by how they score:
Rules of thumb: (1) changing a prompt with no eval is changing it blind; (2) the eval model must be ≥ the model being evaluated (the judge principle); (3) the gold set must be versioned and private; (4) every bad case must be regressed into the gold set — the harness-engineering closed loop.
Model “evaluation” as an LLM task: the judge model reads (prompt, response) and outputs a 0–1 score / pass/fail / which side wins pairwise. It parallelizes at large scale, is cheap, and can be automated in CI.
Three major biases:
The judge principle: the judge model’s capability should be ≥ the model being evaluated, or it cannot tell what is good about a stronger model’s answer. The production standard is three layers: your own gold set + an LLM judge + human calibration.
The Liu et al. 2023 experiment: put the gold passage at the start or the end of the prompt and accuracy is high; put it in the middle and accuracy falls in a U-shape. With 10+ documents, the middle position can score below a no-context baseline.
Engineering mitigations:
It is #1 on the OWASP LLM Top 10. Types:
Simon Willison’s conclusion: prompt injection has no 100% solution, and the system must be designed on the assumption that “the LLM is untrusted”. The dual-LLM pattern = a Planner (sees trusted input and decides) + an Executor (sees untrusted input but has no tool permissions).
Sources:
Debug flow: reproduce it and quantify the trigger rate → attribute the cause (RAG recall / whether the model used the context / tool calls) → fix → add a regression → production monitoring (a faithfulness scorer blocks in real time).
Key point: most hallucinations are defects in the prompt / RAG / context engineering, not “the model is sick”.
NVIDIA NeMo Guardrails defines 5 kinds:
Key point: more rails are not automatically better. Turn on 1–2 high-ROI rails per kind, or latency and cost explode.
Common techniques:
Defense: the vendor’s RLHF safety training + application-layer input/output classifiers (Llama Guard) + a cap on max context (against many-shot) + regular red-team exercises.