WangYu::Space

cat /dev/mind

AI Agent 原理与实现(五):上下文

分类:机器学习标签: LLMAI Agent创建时间:2026-07-31 19:29:00修改时间:2026-08-13 20:40:00

在使用 AI 对话助手时,它常常能引用之前的对话内容,我们容易误以为模型有记忆能力,能记住之前的对话。然而,了解过 LLM 的工作原理后会发现,模型并没有记忆能力:它每次生成回复时,都会把对话历史和新输入一起作为上下文,再根据这些上下文生成回复。

在使用基于 LLM 的应用时,我们输入的只是一段相对简短的问题或指令,而 LLM 实际接收到的内容,包含系统提示词、历史消息、新的用户输入等。模型接收到的这些内容统称为上下文 (Context)。这篇文章将详细剖析 Agent 中上下文的构成,让你明白我们输入的指令和模型实际接收到的上下文之间的差异。此外,本文还会介绍上下文窗口的限制、为什么上下文越多不一定越好,以及压缩工具返回结果、上下文压缩等技术。

Agent 的能力不仅取决于模型有多强,也取决于运行时给模型准备了什么上下文。

什么是上下文

上下文就是模型在本轮推理中能够看到的内容,包括系统提示词、对话历史、新的用户输入等,模型根据这些上下文来决定生成的内容。

因为模型是无状态的,要想让它做出正确的决策,就必须提供所有必要的信息。Agent 的任务通常需要几十轮甚至上百轮对话才能完成,每一轮的目标不同,所需的信息也不同。在每一轮推理中把当前步骤真正需要的信息交给模型,正是 Agent 的职责之一。

这里以“订咖啡”任务为例。当用户输入“帮我订一杯咖啡”后,Agent 已经执行了几轮思考和行动:模型问用户想喝什么口味,用户回答“美式”,模型调用地图工具搜索了附近的咖啡店。此时,Agent 发送给模型的上下文大致如下:

# System Prompt
你是一个能调用工具完成任务的智能助手。
信息不足时先询问用户,不要自行猜测。

可用工具:
- ask_user(question):向用户提问
- map_search(keyword, location):搜索附近门店,返回名称、评分、距离
- order(shop, item):在配送平台下单

# 消息历史
用户:帮我订一杯咖啡
助手:我需要先确认用户的口味偏好。
  工具调用:ask_user(question="你喜欢喝哪种口味?")
工具返回:美式
助手:用户想要美式。接下来搜索附近的咖啡店。
  工具调用:map_search(keyword="咖啡", location="公司附近")
工具返回:1. 星巴克(幸福里店)评分 4.8,距离 350 米;2. 瑞幸(金融街店)评分 4.5,距离 500 米
助手:我已经搜索到附近的咖啡店。接下来选择评分更高的星巴克下单,但需要确认用户是否同意。
  工具调用:ask_user(question="我搜索到附近评分更高的星巴克,是否帮你下单?")
工具返回:同意

模型会完整看到上面的内容,并据此决定下一步动作:选择评分更高的星巴克,调用 order 工具下单。

上下文的组成

模型在每一轮推理中看到的上下文,主要包含三个部分:System Prompt、消息历史、工具返回值或用户的新输入,下面分别介绍这三部分的内容和作用。

System Prompt

系统提示词 (System Prompt) 是上下文的第一部分,通常在整个任务中保持不变。它回答四个问题:模型是谁、能做什么、要遵守什么规则、不能做什么。

系统提示词中的内容通常包括:

  1. 角色定义:描述模型的身份、能力和职责。
  2. 行为规则:描述模型执行任务时需要遵守的规则和约束,比如“不能自行猜测用户意图”、“信息不足时先询问用户”等。
  3. 工具描述:描述 LLM 可调用的工具,包括工具名称、用途、参数格式等。
  4. skill 描述:描述 LLM 可调用的 skill,包括 skill 名称、用途、参数格式等。

系统提示词在每一轮推理中都会完整出现。

下面以 opencode 为例,展示它的系统提示词:

You are opencode, an agent - please keep going until the user’s query is completely resolved, before ending your turn and yielding back to the user.

Your thinking should be thorough and so it's fine if it's very long. However, avoid unnecessary repetition and verbosity. You should be concise, but thorough.

You MUST iterate and keep going until the problem is solved.

You have everything you need to resolve this problem. I want you to fully solve this autonomously before coming back to me.

...(略)

完整的系统提示词可以在 Phistory 上查看,或者点击此链接查看 opencode 的系统提示词。

opencode 的系统提示词开头定义了模型的身份和职责:它是一个 Agent,必须持续迭代,直到用户的查询完全解决,才结束本轮并把控制权交还给用户。接着,它要求思考过程全面且可以很长,同时避免不必要的重复和冗长。最后,它明确模型已经拥有解决问题所需的一切资源,要求在返回给用户之前完全自主地解决问题。

系统提示词还定义了 Agent 的行为规则:

## Workflow
1. Fetch any URL's provided by the user using the `webfetch` tool.
2. Understand the problem deeply. Carefully read the issue and think critically about what is required. Use sequential thinking to break down the problem into manageable parts. Consider the following:
   - What is the expected behavior?
   - What are the edge cases?
   - What are the potential pitfalls?
   - How does this fit into the larger context of the codebase?
   - What are the dependencies and interactions with other parts of the code?
3. Investigate the codebase. Explore relevant files, search for key functions, and gather context.
4. Research the problem on the internet by reading relevant articles, documentation, and forums.
5. Develop a clear, step-by-step plan. Break down the fix into manageable, incremental steps. Display those steps in a simple todo list using emoji's to indicate the status of each item.
6. Implement the fix incrementally. Make small, testable code changes.
7. Debug as needed. Use debugging techniques to isolate and resolve issues.
8. Test frequently. Run tests after each change to verify correctness.
9. Iterate until the root cause is fixed and all tests pass.
10. Reflect and validate comprehensively. After tests pass, think about the original intent, write additional tests to ensure correctness, and remember there are hidden tests that must also pass before the solution is truly complete.

Refer to the detailed sections below for more information on each step.

### 1. Fetch Provided URLs

...(略)

这段系统提示词定义了 Agent 解决问题的工作流程:用户提供 URL 时,先用 webfetch 工具获取网页内容;然后深入理解问题,用顺序思维把问题分解成可管理的若干部分。提示词还对每个步骤做了详细说明,确保模型执行任务时有明确指导。

此外,为了让 LLM 了解当前的工作环境,系统提示词还会包含环境信息,包含当前工作的目录、是否是 git 仓库、操作系统平台、当前日期等。示例如下:

Here is some useful information about the environment you are running in:

<env>
 Working directory: /Users/me/projects/my-app
 Is directory a git repo: yes
 Platform: darwin
 Today's date: Saturday, January 15, 2025
</env>

系统提示词还会包含 LLM 可以使用的工具和 skill 的描述,示例如下:

Skills provide specialized instructions and workflows for specific tasks.
Use the skill tool to load a skill when a task matches its description.
<available_skills>
  <skill>
    <name>customize-opencode</name>
    <description>Use ONLY when the user is editing or creating opencode's own configuration: opencode.json, opencode.jsonc, files under .opencode/, or files under ~/.config/opencode/. Also use when creating or fixing opencode agents, subagents, skills, plugins, MCP servers, or permission rules. Do not use for the user's own application code, or for any project that is not configuring opencode itself.</description>
    <location>&lt;built-in&gt;</location>
  </skill>
</available_skills>

# Tools

## bash

Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.

Be aware: OS: linux, Shell: bash

All commands run in the current working directory by default. Use the `workdir` parameter if you need to run a command in a different directory. AVOID using `cd <directory> && <command>` patterns - use `workdir` instead.

...(略)

不同的 Agent 系统会有不同的系统提示词,其设计取决于 Agent 的设计目标和适用场景。

消息历史

System Prompt 之后是用户的消息和模型的回复,它们共同构成消息历史。用户的目标可能在第一条消息里就表达清楚,也可能在几轮对话中逐步细化。上面的例子中,用户的初始指令只有“帮我订一杯咖啡”七个字,几轮对话之后,目标细化成了“在附近评分高的门店订一杯美式”。

消息历史通常是上下文中占比最大的部分,它包含三类内容:

消息历史是模型“记住”之前发生了什么的方式。模型自身没有记忆,每一轮推理都要重新看到完整的消息历史,Agent 通过维护消息历史来提供这些上下文。

LLM 发起工具调用后,Agent 会把工具返回值写入消息历史。工具返回值可能是一个文件的完整内容、网页解析后的文本、数据库查询的结果集等,这些内容会原样进入上下文。比如,模型调用 read_file 工具读取一个 1,200 行的源码文件,上下文就多出 1,200 行。如果它再读 5 个类似的文件,上下文增加 6,000 行以上,但其中大部分内容在后续推理中可能再也不会用到。工具返回值是上下文增长最快的来源,也是优化收益最大的部分。后文会介绍几种压缩工具返回值的方法,它们都是上下文工程的重要组成部分。

上下文窗口

模型的输入长度有一个上限,叫做上下文窗口。窗口大小以 token 数衡量,目前常见商业模型的上下文窗口为 128,000 或 200,000 个 token,部分开源模型已达到 1,000,000 个 token。

上下文窗口大小主要受模型训练时的最大序列长度限制,位置编码器 (Positional Encoder) 决定了模型能处理的最大 token 数。另外,模型的推理耗时和成本也会随着上下文长度线性增长。

因为 LLM 是无状态的,每一轮推理都要把完整的上下文发送给模型。假设系统提示词包含 10,000 个 token,之后每一轮对话新增 5,000 个 token,那么第一轮推理的上下文长度是 10,000 个 token,第二轮是 15,000 个 token,第三轮是 20,000 个 token……到第 30 轮时,上下文长度达到 155,000 个 token。假设模型的上下文窗口是 128,000 个 token,那么到第 25 轮时,上下文长度就超过了窗口上限,模型无法处理完整的上下文。

上下文越长,模型利用关键信息的能力反而越差,因为上下文中包含了很多无用信息。这很好理解:一个人面对一堆杂乱的文件,想找关键的那一页,文件越多反而越难找到目标。模型也是一样,大量无关信息会分散它的注意力,让它更难抓住关键信息。

所以,上下文窗口是容量上限,不是使用目标。上下文管理的目标不是把窗口用满,而是让上下文中尽量只包含当前步骤真正需要的信息,在多轮对话中不要让上下文增加得太快。

控制上下文长度的方法

人与人交流时,只要双方愿意,就可以持续很长时间,不会因为内容太多而“聊不下去”。这是因为人类只记关键内容,还能对过往内容进行总结,忘掉不重要的部分。用户使用 Agent 时同样可以进行长时间对话,对话历史会随轮数增加而越来越长,但上下文窗口是有限的,Agent 需要采用一些策略来控制上下文长度。

下面是几种常见的控制上下文长度的方法:

按需供给信息

一种避免上下文快速增长的方法是按需供给信息:只把当前步骤真正需要的信息放进上下文,而不是把所有信息都塞进去。按需供给信息有三种常见做法:

  1. 只取需要的部分。读文件时,只读相关片段,不读整个文件,或者使用关键词搜索,只读取和关键词直接相关的内容;查数据库时,用 WHERE 条件过滤,不整表扫描。模型通常不需要整个文件,只需要与当前步骤相关的部分。

  2. 按需检索。相关知识远超上下文容量时,不要全部放进上下文,而是先存进向量数据库,每轮检索出最相关的几条,只把这几条放进上下文。这就是常见的 RAG(Retrieval-Augmented Generation,检索增强生成)做法。RAG 与 Agent 记忆的区别,会在后续讲记忆的文章中展开。

  3. 延迟加载。类似第三篇介绍的 skill:上下文里先只保留名称和描述,等任务真正需要时,再把完整内容加载进上下文。按需供给信息适用于各个层面,从文件到知识,再到技能。

压缩工具返回结果

工具返回值是上下文增长最快的来源,也是压缩的第一目标。思路是把“工具实际做了什么”压缩成“模型下一步需要什么”。常见做法有四类。

  1. 截断。给工具返回值设置长度上限。例如,read_file 工具默认只返回前 200 行,超出时在末尾追加“文件共 1,200 行,第 201 行至第 1,200 行已省略”。模型需要更多内容时,可以再次请求读取指定范围。
  2. 过滤。返回值里只保留任务需要的字段。搜索门店时,工具返回 20 家门店,每家 15 个字段,模型真正需要的只有名称、评分和距离,Agent 在写入上下文前把其余 12 个字段去掉。
  3. 摘要。返回值很长、结构复杂时,先生成一份摘要,将摘要写入上下文,完整内容存到文件里。模型之后需要细节时,再读文件。
  4. 卸载。把完整的工具返回值写入外部存储(文件或数据库),上下文里只留一个引用。上下文保持很小,数据也没有丢。

以“卸载”为例,压缩前后上下文中的内容对比如下。压缩前:

工具返回:query_database("SELECT * FROM orders WHERE month = '2026-06'")
# 8,431 行,每行包含 15 个字段……

压缩后:

工具返回:query_database("SELECT * FROM orders WHERE month = '2026-06'")
查询返回 8,431 行,完整结果已保存到 /tmp/orders_202606.csv。
汇总统计:共 8,431 行,总金额 1,234,567.89 元,前三名城市为上海、北京、杭州。
如需查看行级明细,可以读取上面的 CSV 文件。

模型要回答“6 月生意怎么样”,通常只需要汇总统计,不需要逐行查看 8,431 条记录。当它确实需要某条订单明细时,可以再读一次 CSV:信息没有丢,只是从上下文里移出去了。

上下文压缩

上面的两种方法只能减缓上下文增长速度,但增长再慢,最终还是会超过上下文窗口上限。上下文接近窗口上限时,Agent 必须腾出空间,通常的做法是对对话历史做压缩,以下两种做法通常组合使用。

1. 截断

只保留最近 N 轮消息,丢弃最早的消息。这种方式简单,但早期信息会全部丢失,任务目标、初始约束这类重要信息也可能因此丢失。因为通常第一条用户消息里就包含了任务目标和约束,所以截断一般配合保留策略使用:System Prompt 和第一条用户消息永远保留,只截断中间的过程消息。

2. 摘要

用模型对早期对话做摘要,用摘要替换原始消息,最近几轮消息保留原文。摘要保留关键事实,如用户目标、已确认的决策、任务进度等,去掉过程细节。例如,把 50 轮对话(约 250,000 个 token)压缩成一段摘要(约 100 个 token),再保留最近 5 轮原文(约 25,000 个 token)。上下文从约 250,000 个 token 降到约 25,000 个 token,模型仍然知道目标是什么、已经做了什么、接下来做什么。

摘要一般由 LLM 生成:Agent 只需要新开一个对话,修改系统提示词,让模型对输入的对话历史做摘要。这种给对话历史做摘要的方式通常称为 compact,它能在保留关键信息的同时,显著减少上下文长度。

总结

上下文是模型看到世界的唯一窗口,它的内容由 Agent 程序决定。这篇文章介绍了上下文的三个组成部分:System Prompt、消息历史、工具返回值或用户的新输入。上下文窗口设定了上下文长度的上限,但上下文越长不一定越好:无关信息越多,模型的注意力越容易分散,对关键信息的处理也就越差。

因此,正确的做法是上下文工程:按需供给信息、压缩工具返回结果、在上下文过长时压缩整个上下文,让模型每一轮只拿到完成当前步骤真正需要的信息。Agent 的能力不仅取决于模型有多强,也取决于运行时给模型准备了什么上下文。

评论 (评论内容仅博主可见,不会公开显示)