上下文工程实战指南:用四大策略构建生产级 AI 智能体

掌握上下文工程四大核心策略 Write、Select、Compress、Isolate,用 LangGraph + Python 代码实例构建生产级 AI 智能体。含三种上下文失败模式分析、Token 预算管理实践,以及 Anthropic 和 LangChain 的生产经验总结。

为什么提示词工程已经"不够用"了?

如果你在 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 不会预先索引整个代码库(那样太慢、太占空间),而是用 globgrep 工具在需要时按需搜索和加载文件内容。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)。

不要一开始就追求完整架构,先解决最痛的问题。好的工程总是从最简单能工作的方案开始的。

关于作者 Editorial Team

Our team of expert writers and editors.