为什么提示词工程已经"不够用"了?
如果你在 2024 年搭建过 LLM 应用,大概率写过不少精心打磨的提示词(Prompt)。那时候,一个好的 System Prompt 加上几个 Few-shot 示例,就能让模型表现得相当不错。
但到了 2026 年,事情变了——而且变得挺彻底的。
Fast Company 在 2025 年 5 月的报道显示,68% 的企业已经把提示词工程纳入了全员基础培训——它不再是一项稀缺技能。不过更关键的变化是,我们构建的 AI 系统已经从"单轮对话"演进到了"长时间运行的自主智能体"。说实话,一个在生产环境中连续工作数小时的 AI Agent,靠一段写得漂亮的提示词是远远不够的。它需要的是一个完整的信息供给体系。
这就是上下文工程(Context Engineering)诞生的背景。
Andrej Karpathy 给出了一个非常精准的类比:把 LLM 想象成 CPU,上下文窗口就是 RAM。而上下文工程就是这个"AI 操作系统"的内存管理器——它决定在每个"时钟周期"里,哪些数据该加载、哪些该换出、哪些该优先处理。这个比喻我觉得是目前最直观的解释了。
Anthropic 的工程团队说得更直白:"Claude 已经足够聪明了,智能不是瓶颈,上下文才是。"
Phil Schmid(Hugging Face)也指出了同样的问题:现在大部分 Agent 失败案例已经不是模型能力问题,而是上下文失败。要么检索了错误的文档,要么往上下文窗口塞了太多历史记录,要么忘了包含工具定义。提示词本身没问题,喂给模型的信息出了问题。
所以这篇文章会用 LangGraph + LangChain 手把手带你实现上下文工程的四大核心策略:Write(写入)、Select(选择)、Compress(压缩)、Isolate(隔离)。每个策略都有完整可运行的 Python 代码、生产环境的最佳实践,以及——你一定会踩的坑。
上下文工程 vs 提示词工程:本质区别
先把概念搞清楚。
很多人以为上下文工程就是"更高级的提示词工程"——其实不是,它们是包含关系。我最初也有这个误解,直到真正在生产环境中踩了坑才理解两者的差异。
| 维度 | 提示词工程 | 上下文工程 |
|---|---|---|
| 关注点 | 单条指令的措辞和格式 | 整个信息流的生命周期管理 |
| 上下文类型 | 静态(手写提示词) | 动态(实时检索 + 历史状态 + 工具接口) |
| 时间维度 | 单次推理 | 跨多轮交互甚至跨 Session |
| 核心挑战 | 如何写清楚指令 | 如何在有限窗口中最大化信息密度 |
| 适用场景 | 简单问答、内容生成 | 长时间运行的 Agent、多步推理、复杂工作流 |
更准确地说,提示词工程是上下文工程的一个子集,主要负责优化"指导性上下文"。而上下文工程还要管理"信息性上下文"(通过 RAG 等技术动态注入的知识)和"工具性上下文"(Agent 可调用的工具定义和调用结果)。
三种典型的上下文失败模式
在深入四大策略之前,先认识三个你一定会遇到的坑。这些不是理论上的可能性,是实际项目中大概率会碰到的问题。
上下文中毒(Context Poisoning)
当一个错误的信息(通常是幻觉)进入上下文后,它会被后续交互反复引用和强化。这个过程有点像谣言传播——一旦进入系统就很难清除。
Google Gemini 团队在用 Agent 玩宝可梦时就遇到过这种情况。Agent 幻觉自己拥有某个道具,这个错误信念被写入了上下文的"目标"部分,导致 Agent 花了好几个小时试图使用一个根本不存在的物品。听起来有点好笑,但在生产环境中这可就不好笑了。
上下文干扰(Context Distraction)
上下文中包含了太多不相关的信息,"淹没"了真正重要的内容。
斯坦福和 UC Berkeley 的研究发现,即使模型声称支持超长上下文窗口,准确率在超过约 32,000 Token 后就开始显著下降——这就是著名的"中间迷失"效应。简单来说,模型只关注开头和结尾,中间的信息基本被忽略。所以并不是窗口越大越好,质量永远比数量重要。
上下文混淆(Context Confusion)
不同类型的上下文相互干扰。比如,一个 Agent 同时处理"规划"和"编码"任务时,规划阶段的抽象讨论可能会影响编码阶段的具体实现。这也是为什么 Anthropic 建议把不同类型的工作放在不同的聊天会话中——隔离是有道理的。
环境准备
我们的代码基于 2026 年 3 月的最新稳定版本。如果你跟着做的话,先确保环境设置好:
# 创建虚拟环境
python -m venv context-eng-env
source context-eng-env/bin/activate
# 安装核心依赖
pip install langgraph>=1.0.0 langchain>=0.3.0 langchain-openai>=0.3.0
pip install langmem>=0.1.0 tiktoken>=0.8.0
pip install chromadb>=0.6.0 langchain-chroma>=0.2.0
# 设置 API Key
export OPENAI_API_KEY="your-api-key-here"
策略一:Write(写入)—— 把记忆搬到上下文之外
核心思想其实很朴素:LLM 生成的内容是"转瞬即逝"的——一旦上下文被截断或压缩,这些信息就丢了。Write 策略通过把关键信息持久化到外部存储(文件、数据库、状态对象),确保重要信息不会随上下文窗口的滑动而消失。
你可以把它理解成人类做笔记的行为。开会的时候不记笔记,两小时后你还能记住多少?
实现方式:Scratchpad(草稿板)
草稿板是最直观的 Write 策略实现。Agent 在执行复杂任务时,把中间结果、关键发现、决策记录写到一个独立的存储区域。Claude Code 的 auto memory 就是这个模式的典型应用——如果你用过 Claude Code,应该对 MEMORY.md 不陌生。
from typing import TypedDict, Annotated, List
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_openai import ChatOpenAI
import json
llm = ChatOpenAI(model="gpt-4.1", temperature=0)
class AgentState(TypedDict):
messages: Annotated[List[BaseMessage], add_messages]
scratchpad: str # 持久化的草稿板
task_plan: str # 持久化的任务计划
def planning_node(state: AgentState) -> dict:
"""规划节点:制定计划并写入草稿板"""
messages = state["messages"]
response = llm.invoke([
{"role": "system", "content": (
"你是一个任务规划器。分析用户的请求,制定一个分步计划。\n"
"用 JSON 格式输出计划,包含 steps 数组。"
)},
*messages
])
# 关键:把计划写入持久化的 task_plan 字段
# 即使上下文被压缩,计划依然可用
return {
"messages": [response],
"task_plan": response.content,
"scratchpad": "[规划完成] 已制定任务计划"
}
def execution_node(state: AgentState) -> dict:
"""执行节点:读取草稿板中的计划来执行任务"""
plan = state.get("task_plan", "无计划")
scratchpad = state.get("scratchpad", "")
response = llm.invoke([
{"role": "system", "content": (
f"你是一个任务执行器。\n"
f"当前计划:{plan}\n"
f"草稿板记录:{scratchpad}\n"
"根据计划执行下一步,并报告结果。"
)},
*state["messages"][-3:] # 只取最近 3 条消息,节省 Token
])
# 更新草稿板,记录执行进度
updated_scratchpad = scratchpad + f"\n[执行中] {response.content[:100]}..."
return {
"messages": [response],
"scratchpad": updated_scratchpad
}
# 构建图
graph = StateGraph(AgentState)
graph.add_node("plan", planning_node)
graph.add_node("execute", execution_node)
graph.add_edge(START, "plan")
graph.add_edge("plan", "execute")
graph.add_edge("execute", END)
app = graph.compile()
# 运行
result = app.invoke({
"messages": [HumanMessage(content="帮我分析这个 CSV 数据并生成报告")],
"scratchpad": "",
"task_plan": ""
})
print("草稿板内容:", result["scratchpad"])
Claude Code 的 LeadResearcher 就是这个模式的升级版——它在开始研究前先把计划写入 Memory,因为一旦上下文窗口超过 200,000 Token 就会被截断,但 Memory 里的计划不会丢失。这个设计相当巧妙。
策略二:Select(选择)—— 按需加载上下文
核心思想:不要把所有可能相关的信息一次性塞进上下文窗口。相反,在需要的时候,用检索工具动态加载最相关的信息。
说白了,RAG 本质上就是一种 Select 策略。不过 Select 的范围比 RAG 更广。
即时上下文加载(Just-in-Time Context Loading)
这是 Anthropic 在 Claude Code 中大量使用的策略。Claude Code 不会预先索引整个代码库(那样太慢、太占空间),而是用 glob 和 grep 工具在需要时按需搜索和加载文件内容。CLAUDE.md 这种静态上下文在启动时加载,而代码文件这种动态上下文则即时检索。
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_core.documents import Document
from langchain_core.tools import tool
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# 模拟一个企业知识库
docs = [
Document(
page_content="退款政策:购买后 30 天内可全额退款,需提供订单号。",
metadata={"type": "policy", "topic": "refund"}
),
Document(
page_content="产品 A 技术规格:支持 API v2.0,最大并发 1000。",
metadata={"type": "tech", "topic": "product-a"}
),
Document(
page_content="如果 API 返回 429 错误,说明触发了限流。请降低请求频率。",
metadata={"type": "troubleshooting", "topic": "api"}
),
Document(
page_content="VIP 客户权益:专属客服、优先响应(1 小时内)、季度回顾。",
metadata={"type": "policy", "topic": "vip"}
),
]
vectorstore = Chroma.from_documents(
docs, embeddings, collection_name="knowledge_base"
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
@tool
def search_knowledge_base(query: str) -> str:
"""搜索企业知识库,获取与用户问题相关的信息。"""
results = retriever.invoke(query)
if not results:
return "未找到相关信息。"
return "\n\n".join([
f"[来源: {doc.metadata.get('type', 'unknown')}] {doc.page_content}"
for doc in results
])
@tool
def get_customer_info(customer_id: str) -> str:
"""根据客户 ID 获取客户信息(模拟)。"""
mock_data = {
"C001": {"name": "张三", "level": "VIP", "orders": 42},
"C002": {"name": "李四", "level": "Standard", "orders": 3},
}
info = mock_data.get(customer_id)
if info:
return json.dumps(info, ensure_ascii=False)
return f"未找到客户 {customer_id}"
这种模式的优势在于:Agent 的上下文窗口始终保持精简。它不会一开始就加载整个知识库的 50 万 Token 内容,而是只在回答特定问题时,加载那 200 Token 真正相关的策略文档。这个差距是巨大的。
策略三:Compress(压缩)—— Token 预算管理
核心思想:上下文窗口是有限资源。即使模型声称支持百万 Token,实际使用中你也不想(也不应该)把窗口填满。压缩策略的目标是在有限的 Token 预算内最大化信息密度。
坦白说,这是四大策略中技术含量最高的一个,也是最容易做错的一个。让我们看三种不同粒度的压缩方法。
方法一:消息修剪(Message Trimming)
最简单但也最有效的压缩方式——直接丢弃旧消息,只保留最近的对话。简单粗暴?是的。但很多时候够用了。
from langchain_core.messages import trim_messages
import tiktoken
def count_tokens(messages: list) -> int:
"""精确计算消息列表的 Token 数"""
enc = tiktoken.encoding_for_model("gpt-4")
total = 0
for msg in messages:
content = msg.content if hasattr(msg, "content") else str(msg)
total += len(enc.encode(content))
return total
def smart_trim(messages: list, max_tokens: int = 4000) -> list:
"""智能修剪:保留系统消息和最近的对话"""
trimmed = trim_messages(
messages,
max_tokens=max_tokens,
token_counter=len,
strategy="last", # 保留最新的消息
start_on="human", # 确保从人类消息开始
allow_partial=False # 不截断单条消息
)
return trimmed
方法二:对话摘要(Conversation Summarization)
用 LLM 把长对话压缩成一段简洁的摘要,保留关键信息,丢弃冗余细节。这个方法比消息修剪更智能(不会丢失早期的重要信息),但也更贵,因为每次压缩本身就要消耗 Token。
from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.checkpoint.memory import InMemorySaver
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4.1", temperature=0)
summary_model = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
class ConversationState(MessagesState):
summary: str # 历史对话的摘要
token_count: int # 当前 Token 计数
TOKEN_THRESHOLD = 8000
def should_compress(state: ConversationState) -> str:
"""判断是否需要压缩"""
if state.get("token_count", 0) > TOKEN_THRESHOLD:
return "compress"
return "respond"
def compress_node(state: ConversationState) -> dict:
"""压缩节点:将对话历史摘要化"""
messages = state["messages"]
old_summary = state.get("summary", "")
summary_prompt = (
f"以下是之前对话的摘要:\n{old_summary}\n\n"
f"以下是最近的对话内容:\n"
)
for msg in messages[:-4]:
role = "用户" if isinstance(msg, HumanMessage) else "助手"
summary_prompt += f"{role}: {msg.content[:500]}\n"
summary_prompt += (
"\n请生成一份简洁的摘要,"
"保留所有关键信息(用户意图、重要决策、未解决的问题)。"
)
summary_response = summary_model.invoke([
{"role": "user", "content": summary_prompt}
])
return {
"messages": messages[-4:],
"summary": summary_response.content,
"token_count": count_tokens(messages[-4:])
}
def respond_node(state: ConversationState) -> dict:
"""正常响应节点"""
summary = state.get("summary", "")
system_msg = "你是一个有帮助的 AI 助手。"
if summary:
system_msg += f"\n\n之前对话的摘要:\n{summary}"
response = model.invoke([
{"role": "system", "content": system_msg},
*state["messages"]
])
new_count = state.get("token_count", 0) + len(response.content)
return {
"messages": [response],
"token_count": new_count
}
方法三:工具输出卸载(Tool Output Offloading)
当工具返回超大响应(比如读取一个大文件),把响应卸载到文件系统,只在上下文中保留文件路径和前几行预览。这个方法经常被忽略,但在实际应用中效果特别好。
import os
import hashlib
def offload_large_response(response: str, threshold: int = 5000) -> str:
"""如果工具响应超过阈值,卸载到文件"""
if len(response) <= threshold:
return response
file_hash = hashlib.md5(response[:200].encode()).hexdigest()[:8]
file_path = f"/tmp/agent_context/{file_hash}.txt"
os.makedirs("/tmp/agent_context", exist_ok=True)
with open(file_path, "w") as f:
f.write(response)
preview_lines = response.split("\n")[:10]
preview = "\n".join(preview_lines)
return (
f"[响应已卸载到文件: {file_path}]\n"
f"[总长度: {len(response)} 字符]\n"
f"[前 10 行预览]:\n{preview}\n"
f"[如需完整内容,请使用 read_file 工具读取]"
)
Claude Code 在实际运行中就使用了类似策略——当上下文使用率超过 95% 时自动触发 auto-compact,对完整的交互轨迹进行摘要压缩。压缩时保留架构决策、未解决的 Bug 和实现细节,丢弃冗余的工具输出。
LangChain 最新发布的 Deep Agents SDK 更进了一步:把压缩决策权交给 Agent 自己。为什么这样做?因为固定阈值触发压缩并不理想——在复杂重构过程中触发压缩会丢失关键上下文,而在开始新任务时触发则正合适。让 Agent 自己决定何时压缩,能做到更智能的上下文管理。
策略四:Isolate(隔离)—— 分而治之
核心思想:与其让一个 Agent 的上下文窗口承载所有信息,不如把上下文拆分到多个独立的执行空间中。每个子 Agent 只处理与自己职责相关的信息,最后汇总结果。
如果你管理过团队就会理解——让一个人同时做五件事,不如让五个人各做一件事。Agent 的上下文管理也是同样的道理。
多 Agent 上下文隔离
这是 OpenAI Swarm 和 Anthropic 多 Agent 研究系统背后的核心设计思想。Lead Agent 生成多个子 Agent 并行处理不同的子任务,每个子 Agent 有独立的上下文窗口——只包含与其具体任务相关的信息。整个系统的"有效上下文"远大于任何单个模型的窗口限制。
from langgraph.graph import StateGraph, START, END
from typing import TypedDict, List
class ResearchState(TypedDict):
query: str
sub_queries: List[str]
sub_results: List[str]
final_report: str
def decompose_query(state: ResearchState) -> dict:
"""将复杂查询分解为子查询"""
response = llm.invoke([
{"role": "system", "content": (
"将用户的复杂问题分解为 2-3 个独立的子问题。\n"
"每个子问题应该可以独立研究。\n"
"用 JSON 数组格式输出。"
)},
{"role": "user", "content": state["query"]}
])
sub_queries = json.loads(response.content)
return {"sub_queries": sub_queries}
def research_sub_query(sub_query: str) -> str:
"""独立的子 Agent:有自己隔离的上下文"""
response = llm.invoke([
{"role": "system", "content": (
"你是一个专注的研究员。只回答给定的问题。\n"
"提供详细、准确的信息。"
)},
{"role": "user", "content": sub_query}
])
return response.content
def parallel_research(state: ResearchState) -> dict:
"""并行执行子查询——每个在隔离的上下文中"""
results = []
for sq in state["sub_queries"]:
result = research_sub_query(sq)
results.append(result)
return {"sub_results": results}
def synthesize(state: ResearchState) -> dict:
"""汇总节点:只接收精炼后的子结果"""
synthesis_input = ""
for q, r in zip(state["sub_queries"], state["sub_results"]):
synthesis_input += f"### 子问题:{q}\n{r}\n\n"
response = llm.invoke([
{"role": "system", "content": (
"根据各子问题的研究结果,生成完整的综合报告。\n"
"确保信息连贯,消除重复,突出关键发现。"
)},
{"role": "user", "content": synthesis_input}
])
return {"final_report": response.content}
# 构建研究图
research_graph = StateGraph(ResearchState)
research_graph.add_node("decompose", decompose_query)
research_graph.add_node("research", parallel_research)
research_graph.add_node("synthesize", synthesize)
research_graph.add_edge(START, "decompose")
research_graph.add_edge("decompose", "research")
research_graph.add_edge("research", "synthesize")
research_graph.add_edge("synthesize", END)
researcher = research_graph.compile()
result = researcher.invoke({
"query": "对比 2026 年主流向量数据库的性能、成本和可扩展性",
"sub_queries": [],
"sub_results": [],
"final_report": ""
})
print(result["final_report"])
LangGraph 的 State 设计天然支持上下文隔离——你可以为不同的处理阶段定义不同的状态 Schema,Agent 在某个节点只能访问该节点需要的状态字段,而不是整个上下文。这种"最小权限"原则用在上下文管理上,效果出奇地好。
四大策略的组合:生产环境架构
在实际生产中,这四大策略很少单独使用。一个成熟的上下文管理系统通常是这样的:
用户查询
│
▼
[Select] 从知识库即时检索相关文档
│
▼
[Write] 将检索结果写入草稿板持久化
│
▼
[Isolate] 将复杂任务分发给多个子 Agent
│ (每个子 Agent 有独立上下文)
▼
[Compress] 对子 Agent 返回的结果进行摘要压缩
│
▼
[Write] 将最终结论写入长期记忆
│
▼
输出结果
LangChain 官方的 context_engineering 仓库提供了完整的 Notebook 示例,实测中把 Token 用量从 115k 降到了 60k——节省了近一半的开销。这个数字还是挺可观的。
生产环境最佳实践
1. 渐进式上下文构建
从最少的上下文开始,根据 Agent 的实际表现逐步添加。避免一开始就把所有信息塞进去——上下文过载比上下文不足更难调试。我个人的经验是,90% 的情况下你给的上下文都太多了。
2. 分层压缩策略
组合使用不同粒度的压缩:
- 85% 阈值:当上下文使用率达到 85%,截断较旧的工具调用输出,替换为文件指针
- 95% 阈值:触发全量摘要压缩(如 Claude Code 的 auto-compact)
- 自主压缩:更优做法是让 Agent 自己决定何时压缩(参考 LangChain Deep Agents 的设计)
3. 保持上下文新鲜度
定期审计项目文档和提示词模板。过时的上下文比没有上下文更危险——它会导致 Agent 基于错误的假设做决策。这一点真的怎么强调都不过分。
4. 建立评估流水线
上下文工程需要严格的评估机制。不要凭直觉判断上下文质量,用数据说话。LangSmith 提供了 Token 用量和任务成功率的追踪工具,建议从项目第一天就接入。
5. 上下文类型隔离
把不同类型的工作(规划、编码、测试、调试)放在不同的会话中。多 Agent 系统中,每个 Agent 应该有:
- 部分隔离的上下文,避免混淆和冲突
- 标准化的 Agent 间通信协议,高效共享压缩后的上下文
- 上下文检查点,管理 Token 消耗和复用相关记忆
常见问题(FAQ)
上下文工程和 RAG 是什么关系?
RAG(检索增强生成)是上下文工程中"Select(选择)"策略的一种核心实现方式。它通过向量检索将外部知识动态注入上下文窗口。但上下文工程的范围远大于 RAG——它还包括上下文的写入、压缩、隔离和全生命周期管理。可以说 RAG 是上下文工程的一个重要子集,但远不是全部。
上下文窗口越大,是不是就不需要上下文工程了?
恰恰相反。斯坦福和 UC Berkeley 的研究表明,即使模型支持百万级 Token 窗口,实际准确率在 32,000 Token 后就开始下降。更大的窗口带来更高的成本和延迟,而且会加剧"中间迷失"效应。上下文工程的核心价值不是把窗口填满,而是确保每个 Token 都有价值。
Gartner 说的"上下文工程负责人"是什么角色?
Gartner 在 2026 年建议企业设立专门的上下文工程负责人或团队,与 AI 工程和运营治理团队协同工作。这个角色负责制定上下文策略、建立治理流程、管理上下文质量评估体系。随着 AI 系统进入生产环境,上下文管理已经从"工程师的副业"变成了需要专人负责的系统性工作。老实说,如果你的团队在认真做 AI 产品,这个角色(即使不是全职)也该提上日程了。
如何判断上下文工程策略是否有效?
关注三个核心指标:一是Token 效率(同样的任务消耗了多少 Token);二是任务成功率(Agent 准确完成任务的比例是否提升);三是幻觉率(生成内容中无法被上下文信息支持的比例是否降低)。LangSmith 提供了这些指标的追踪工具,值得花时间配置。
小团队该从哪个策略开始?
建议按照 Select → Compress → Write → Isolate 的顺序渐进实施。首先用 RAG 实现按需检索(Select),解决"信息不足"的问题。然后实现消息修剪和摘要(Compress),控制 Token 成本。接着引入草稿板(Write),让 Agent 在长任务中保持记忆。最后当系统复杂度需要时,再引入多 Agent 隔离(Isolate)。
不要一开始就追求完整架构,先解决最痛的问题。好的工程总是从最简单能工作的方案开始的。