Menu

上下文工程是什么:模型看到了什么,为什么

上下文工程就是决定每次调用时放进模型上下文窗口的所有内容:指令、文档、工具结果、记忆和对话历史,以及它们的先后顺序。你输入的提示词只是其中一部分。

下面的每条提示词都可以编辑:改好后,在 ChatGPT、Claude 或其他 AI 应用中打开。

上下文工程就是决定语言模型回答时能看到的一切:不只是你输入的问题,还有问题周围的指令、插入其中的文档和工具结果、关于用户的已保存记忆,以及到目前为止的对话。所有这些内容共享同一个上下文窗口,模型只根据这些文本作答,别无其他。随着越来越多的 AI 产品变成能自动组装大部分上下文的 Agent,这个术语在 2025 年流行起来。

提示词工程主要关心一个请求该怎么措辞。上下文工程关心的是每次调用时模型面前应该有什么,以及按什么顺序排列。

从提示词到上下文

在聊天应用里,大部分上下文是你自己写的:应用加上系统提示词和历史记录,其余的由你补充。在应用程序中,情况正好相反。用户只输入一句话,模型周围的代码会加上指令、用户资料、搜索找到的三篇帮助文章、可用工具列表,以及上一次工具调用的输出。用户的那句话可能只占模型读到内容的一小部分。

这样的系统回答得不好时,问题很少出在措辞上。常见的原因是模型拿到了错误的材料:缺少某个事实、工具结果已经过时,或者某篇文档在搜索看来相关,实际上却毫不相干。

上下文窗口里有什么

AI 应用中一次典型的调用会包含以下部分或全部内容,大致按这个顺序排列:

  • 系统指令:角色、规则和输出格式,通常对整个应用都是固定的。见系统提示词。
  • 工具定义:模型可以调用的工具的名称、描述和参数。
  • 示例:几组示例输入和输出,用来展示期望的行为。
  • 记忆:从之前的会话中保存下来的事实,比如用户的套餐、语言或偏好。
  • 检索到的文档:针对这个问题在知识库中搜索到的段落(检索增强生成,即 RAG)。
  • 对话历史:之前的几轮对话,可以是原文,也可以是总结。
  • 工具结果:在这个任务中执行搜索、运行代码或调用 API 得到的输出,就像 ReAct 循环那样。
  • 当前消息:用户刚刚问的问题。

下面是为一个客服助手组装好的上下文。逐个关掉各个部分试试。没有“上下文”部分,模型就无从得知客户的套餐;没有“输入”部分,它就没有任何产品信息,而它的限制条件要求它直接说明,而不是去猜。

客服助手的上下文
Parts
你是笔记应用 Acme Notes 的客服助手。
只根据下面的文档回答。如果文档没有涵盖这个问题,就直接说明,并提出帮客户转接人工客服。绝不猜测功能或价格。
客户记忆:名字 Dana,免费套餐,使用 Android 应用。
<document source="help/offline-mode"> 离线模式仅在 Pro 套餐中提供。离线时创建的笔记会在设备重新联网后同步。大于 20 MB 的附件无法离线使用。 </document> <document source="help/plans"> 免费套餐:最多 3 台设备。Pro 套餐:设备数量不限、离线模式、30 天版本历史。 </document>
用通俗的语言写两到四句话。称呼客户的名字。
客户问题:没有网络的时候,我能在飞机上用这个应用吗?
Try it
Example replyReplies vary between models and runs.

Dana 你好,离线模式是 Pro 套餐的功能,而你的账号目前是免费套餐,所以暂时还用不了。升级到 Pro 后,你在飞行途中创建的笔记会在手机重新联网时自动同步。还有一个限制需要了解:大于 20 MB 的附件无法离线使用。

注意,正确的答案需要把两个来源结合起来:记忆(免费套餐)和一篇文档(离线模式仅限 Pro)。单靠其中任何一个都不够,这种情况很典型。上下文工程的很大一部分工作,就是确保相互依赖的信息能同时送到模型面前。

上下文出错的四种情况

  1. 信息缺失。 模型会用看似合理的猜测来填补空白,很多幻觉就是这样产生的。补上这个事实,或者告诉模型缺少信息时该怎么做。
  2. 材料太多。 每一段无关的内容都会消耗 token,并争夺注意力。Liu 等人 2023 年的论文《Lost in the Middle: How Language Models Use Long Contexts》发现,他们测试的模型对长输入开头或结尾的信息利用得更可靠,对中间的信息则较差。较新的模型处理长输入的能力更强,但只发送能回答问题的几段内容,仍然比粘贴整本手册更便宜,模型也更容易用好。
  3. 信息过时。 十步之前的工具结果描述的文件或余额,可能早已发生变化。如果模型同时看到新旧两个版本,它可能会用旧的那个。
  4. 相互冲突。 两篇文档说法不一,或者记忆里是一种说法,用户说的是另一种。告诉模型以哪个来源为准,比如“用户的最新消息优先于已保存的记忆”。

给上下文排序

顺序既影响结果,也影响成本。

  • 稳定的部分放前面。 系统指令、工具定义和固定的参考资料在多次调用之间很少变化。好几家 API 提供商都支持提示词缓存,可以复用输入中相同开头部分的处理结果,所以不变的前缀能让重复调用更便宜、更快。
  • 长材料放在问题前面。 对于长文档或大量段落,先放材料,再放问题和最终指令。例如 Anthropic 的提示词指南就建议长输入采用这种顺序,让问题紧挨着模型开始输出的位置。
  • 给每一块内容加标签。 用 <document>、<memory> 或 <tool_result> 这样的标签包住每个来源,并写上来源名称。标签能让模型分清数据和指令,也方便你要求它注明答案出自哪里。分隔符和 XML 标签介绍了各种格式。

精简过长的上下文

聊天的每一轮都会重新发送全部历史,所以长会话会不断变长,直到不得不删掉一些内容。不同的应用可能会总结或丢弃较早的消息,或者让你开一个新对话。有意识地主动精简,效果会更好。

  • 最近几轮保留原文,较早的部分换成总结。
  • 工具结果用过之后,换成一行说明,写明它显示了什么。
  • 每份总结都要保留精确的标识符:文件名、函数名、ID、错误信息和数字。如果历史记录里写的是 settings.prod.yaml,总结里却写成“配置文件”,那就丢掉了唯一要紧的细节。
  • 在聊天应用中,对话跑偏时,带着一份交接说明开一个新对话。下面的提示词就能写出这样一份说明。
把长对话压缩成一个新起点
Fill in
这段对话越来越长了。写一份交接说明,让我粘贴到新对话中,使它不需要历史记录也能继续这项工作。包括: 1. 注册表单重构的目标,用一句话说明。 2. 我们做出的决定,以及每个决定的理由。 3. 当前状态:哪些已经可用,哪些还没完成。 4. 重要的确切名称:文件、函数、错误信息、数字。原样照抄。 5. 待解决的问题和下一步。 除非能解释某个决定,否则不要写走不通的思路。不超过 200 字。
Try it
Example replyReplies vary between models and runs.

目标: 把注册表单里手写的校验逻辑,替换成浏览器和服务器共用的一个校验 schema。

决定

  1. 只用一个 schema 文件 signupSchema.ts,由 SignupForm.tsx 和 api/signup.ts 共同导入,这样两套规则就不会逐渐出现差异。
  2. 错误信息一字不改,因为帮助中心引用了这些原文。

当前状态

  1. 浏览器端的校验已经使用 schema,SignupForm.test.tsx 测试通过。
  2. 服务器端仍在调用 api/signup.ts 中旧的 validateSignup()。

确切细节: 密码至少 8 个字符,且包含一个数字。邮箱错误提示文本:“Please enter a valid email address.”

下一步: 用 schema 替换 validateSignup(),然后运行 API 测试。

待解决问题: 已注册的邮箱应该返回 409 还是 400?

跨会话的记忆

记忆是比单次对话活得更久的上下文:在一次会话结束时写入存储的事实,会在下一次会话中加载进来。聊天应用也提供了类似的功能,比如已保存的记忆,或者会添加到某个项目每个对话中的项目指令。在你自己的应用里,记忆就是一张表或一个笔记文件,由你的代码读取并插入上下文。让记忆保持有用有两条规则:保存长期成立的事实(套餐、语言、偏好的技术栈),而不是对话记录;只加载与当前任务相关的内容,因为记忆和其他所有内容争夺的是同一块空间。

用代码组装上下文

在应用程序中,上下文工程就是普通的代码。下面这段使用 Anthropic Python SDK 的示例,把固定规则和记忆放进系统提示词,只保留最近的历史,并把带标签的文档放在问题前面。

import anthropic

client = anthropic.Anthropic()
MODEL = "your-model-id"  # e.g. from your provider's model list

def build_context(question, docs, history, memory, max_messages=6):
    documents = "\n".join(
        f'<document source="{d["source"]}">\n{d["text"]}\n</document>' for d in docs
    )
    system = (
        "You are the support assistant for Acme Notes. Answer only from the documents. "
        "If they do not cover the question, say so.\n"
        f"<memory>\n{memory}\n</memory>"
    )
    # history holds complete user/assistant pairs, so an even slice starts with a user turn
    recent = history[-max_messages:]
    user = f"<documents>\n{documents}\n</documents>\n\n{question}"
    return system, recent + [{"role": "user", "content": user}]

# question, docs, history and memory come from your application
system, messages = build_context(question, docs, history, memory)
response = client.messages.create(model=MODEL, max_tokens=1024, system=system, messages=messages)
print(response.content[0].text)

这个函数里的每一个决定(用哪些文档、保留多少条消息、记忆放在哪里)都是上下文工程的选择,每一个都值得像测试措辞改动那样,用真实的问题来测试。

常见问题

上下文工程是什么?

上下文工程是指对语言模型每次调用收到的所有内容进行挑选、排序和精简:系统指令、示例、检索到的文档、工具定义和工具结果、保存的记忆、对话历史以及用户的消息。模型只根据这些文本作答,所以里面有什么、缺了什么,决定了回答的质量。

上下文工程和提示词工程有什么区别?

提示词工程主要关心指令怎么措辞。上下文工程涵盖整个输入,其中很多内容是由代码组装的,而不是人手动输入的:检索哪些文档、保留哪些工具结果、包含多少历史、按什么顺序排列。在聊天中,大部分上下文是你自己写的;在应用或 Agent 中,大部分上下文是由模型周围的系统挑选的。

上下文是不是越多越好?

不是。无关或过时的材料会和真正重要的部分争夺注意力,消耗 token,还可能与当前状态相矛盾。关于长输入的研究发现,模型可能会漏掉放在长上下文中间的信息。只放任务需要的内容,给它加上标签,并删掉已经不再需要的部分。

为什么对话越长,效果越差?

每一轮都会把整段对话重新发送一遍,所以旧的错误、放弃的想法和被替换的代码都留在上下文里,持续影响回复。一旦对话超出上下文窗口,应用就只能丢弃或总结较早的消息。用一份简短的总结(写明已做的决定和当前状态)开一个新对话,往往比继续聊下去效果更好。

上下文工程中的 RAG 是什么?

RAG(检索增强生成)是指在模型回答之前,先在你自己的文档中搜索与问题相关的段落,再把它们插入上下文。它是上下文工程的主要工具之一:模型能拿到训练时不可能知道的最新、具体的事实,你也可以要求它只根据这些段落回答。

Coddy 编程语言插图

和 Coddy 一起学编程

开始