1. 为什么需要上下文管理?
前面几章中,Agent 把用户的任务交给模型,执行模型提出的工具调用,再把结果带到下一次模型调用中。每次调用时,提供给模型参考的信息,就叫作上下文。它既包括用户要求、历史对话和工具结果,也包括系统指令、项目约定和工具说明。
这些信息怎样帮助模型继续工作?假设我们给 Agent 一个任务:
修复求和函数:sum(2, 3) 返回了 23,预期结果是 5。保持公共接口不变,并运行测试验证。
第一次调用时,模型知道任务要求,但还没看到代码。Agent 根据模型的工具调用读取函数和测试,把文件内容带到下一次调用中。修改代码并运行测试后,普通用例通过了,负数用例却仍然失败;下一次调用还需要带上修改过程和测试结果,模型才能继续排查。
最直接的做法,是每次调用模型时,都把完整历史一起发过去。任务较短时,这样做简单有效:模型既能看到最初“不改公共接口”的要求,也能知道已经改过什么、测试出了什么问题。
但随着任务持续推进,这种做法会遇到两个问题。
首先,模型的上下文容量有限。 模型一次能处理的信息有上限,这个容量叫作“上下文窗口”,通常用 Token 计量。例如,Claude Sonnet 4.5 的上下文窗口为 20 万 Token,Sonnet 4.6 为 100 万 Token。
100 万 Token 大约能装多少内容?Google 将它粗略类比为 5 万行代码,每行约 80 个字符,实际占用会因内容和分词方式而异。
因此,随着任务推进、历史不断增长,Agent 无法一直把全部历史发给模型,需要控制每次提供的信息量。
其次,模型利用长上下文的能力也有限。 即使内容没有超过容量上限,模型也未必能准确找到并使用其中的全部关键信息。研究发现,在所测试的模型和任务中,同一条关键信息放在长输入的不同位置,会影响回答表现。Lost in the Middle
回到修复任务,大量代码和测试日志累积后,模型可能遗漏最初“不改公共接口”的约束,或没有找到与当前错误相关的线索。这不意味着上下文越多,回答就一定越差:补充相关代码和错误信息通常有帮助。需要关注的是,提供的内容能否帮助模型判断下一步该做什么。
因此,Agent 需要随着任务推进,为每次模型调用组织和更新上下文信息,这就是上下文管理。本章要弄明白的是:怎样准备足够、相关且放得下的上下文信息,让模型能够继续完成任务?
2. 上下文怎样组织,并随着任务推进更新
回到修复求和函数的任务。模型要决定下一步做什么,需要知道用户要解决什么、应遵守哪些约束、有哪些工具可用,以及任务进行到了哪里,例如已经读取的代码、完成的修改和最新测试结果。
下面沿着一次模型调用,先看这些内容怎样组成上下文,再看历史增长后怎样检查长度、生成摘要,并用于下一次调用。
2.1 发给模型的上下文有哪些,分别从哪里来?
第 02 章介绍过,模型服务通过 API 接收输入。我们可以把所有内容拼成一段文字,但仍需要说明哪些是行为要求,例如“修改文件前先读取内容”,哪些是用户的任务,哪些是读取文件或运行测试得到的结果。同一句话出现在用户要求中,还是出现在工具读到的文件里,含义可能完全不同。
模型 API 提供消息角色和内容类型,用来标明这些信息的来源和用途;工具定义则通过规定的结构提供名称、用途和参数规则,让模型服务支持相应的工具调用。这样,Agent 可以按接口约定组织输入,不必把所有区别都放在一段文字里解释。
以 OpenAI 的 Chat Completions API 为例,messages 携带消息,通过 role 区分行为要求、用户输入、模型回复和工具结果:行为要求使用 system 或 developer 角色,具体取决于所选模型;用户输入、模型回复和工具结果分别使用 user、assistant 和 tool。工具定义则放在独立的 tools 字段中。OpenAI API 文档
Ink 内部将系统指令单独保存在 system,对话历史放在 messages,可用工具放在 tools。它的 Chat Completions 适配器再把 system 转成一条 role: "system" 的消息,放入发给服务的 messages 中。因此,下面的三个字段是 Ink 的内部组织方式,不是 Chat Completions API 的三个同名顶层字段:
// 文件:src/provider/types.ts
// 模块级接口:LLMRequest
// 节选:省略模型配置、输出上限、思考配置和中断信号。
export interface LLMRequest {
system: string;
messages: AgentMessage[];
tools: Tool[];
}
下面按字段对照 Ink 中这些信息的来源,以及各字段通常保存的内容:
Ink 上下文字段的内容来源与常见内容
| 字段 | 内容来源 | 常见内容 |
|---|---|---|
system | 内置或自定义提示词、项目规则、扩展上下文、Skills 与子代理目录 | 工作要求、工作目录、项目约定,以及可按需使用的技能与角色说明 |
messages | 用户输入、模型回复、工具执行结果,以及历史压缩生成的摘要 | 用户任务、读取到的代码、工具调用记录、测试输出、历史摘要 |
tools | 本次运行注册的工具对象 | 工具名称、用途、参数规则和本地执行函数 |
用户提出的“不改公共接口”保留在用户消息中,也就是 messages;它不会因为是一条约束,就自动移到 system。
Ink 先用 buildSystemPrompt 组装系统指令的主体,再追加项目约定等内容。下面节选 runner.ts 中追加项目约定的部分:
// 文件:src/runner.ts
// 所在函数:RunnerImpl.assembleSystem
// system 已由前面的 buildSystemPrompt 创建;这里节选追加项目约定的部分。
// 省略记录来源与界面提示;escapeXml 是源码导入的辅助函数。
if (!this.options.noContextFiles) {
const context = loadContextFiles(this.options.cwd);
if (context) {
const inner = context.sections
.map(
s =>
`<project_instructions path="${escapeXml(s.path)}">\n${s.content}\n</project_instructions>\n\n`
)
.join("");
system += `\n\n<project_context>\n\nProject-specific instructions and guidelines:\n\n${inner}</project_context>`;
}
}
loadContextFiles 返回的 sections 按文件保存路径 path 和正文 content。上面的代码给每份规则加上带路径的标签,再追加到 system,让模型知道各段约定来自哪里。例如,项目根目录的 AGENTS.md 可以写编码规范和测试命令;加载后,这些正文会直接出现在系统指令中。
查找时,Ink 先读取全局的 ~/.ink/AGENTS.md,再从文件系统根目录到当前工作目录,依次查找沿途的规则文件。每个目录最多采用一份:优先 AGENTS.override.md,其次 AGENTS.md,再其次 CLAUDE.md;后两者也支持 .MD 扩展名。这里的优先级用于选择同一目录里的文件;不同目录的规则仍按从远到近的顺序加入,不会由代码自动合并或消除矛盾。
系统指令还有其他来源。按“放进去的是正文,还是供后续查找的目录”区分,就容易看清它们的作用:
| 来源 | 怎样进入 system |
|---|---|
内置提示词、SYSTEM.md、APPEND_SYSTEM.md | 默认使用内置工作要求与环境信息;选中的 SYSTEM.md 替换默认提示词主体,APPEND_SYSTEM.md 追加正文。替换主体后仍保留工作目录,并继续组装下面的内容 |
| 项目规则文件 | 按上面的方式加入文件路径和正文 |
| 扩展注册的上下文 | 追加扩展提供的说明正文 |
| Skills | 有可用 read 工具时,加入可供模型选择的技能名称、说明和文件位置;完整正文留到需要时读取 |
| 子代理配置 | 加入供模型选择角色的目录;角色的完整指令留到启动对应子代理时使用 |
自定义提示文件分别从项目 .ink/ 和全局 ~/.ink/ 中选择,项目文件需要通过信任检查。项目规则文件的加载与这道检查不同,具体见第 08 篇。Skills 的发现与按需读取留到第 11 篇展开,这里先记住:提供一份目录,不等于已经把目录中每份文件的正文发给模型。
messages 对应 Agent 循环使用的消息历史数组 history。
tools 在调用模型前准备好,工具注册和说明怎样交给模型,已在第 03 章介绍。
随着任务推进,模型回复、读取到的代码和测试输出不断加入 history。这些历史会在后续调用中再次发送给模型,占用的上下文空间也越来越多。前面提到,模型的上下文窗口有限,也未必能充分利用长上下文中的信息。因此,接下来需要解决的是:怎样控制历史长度,同时保留继续完成任务所需的信息?
2.2 历史越来越长,怎样管理?
当前主流的 Coding Agent 通常会结合多种手段管理上下文。例如,Claude Code 按需加载工具说明,并让子代理在独立上下文中工作,减少主对话积累的内容;历史接近上限时,先清理较早的工具输出,必要时再总结对话。Claude Code 官方说明 Gemini CLI 也支持用摘要替换对话历史,减少后续调用占用的 Token。Gemini CLI 官方说明 这些做法的共同目标,是用更少的内容保留任务进展和关键依据,让模型能够继续工作。压缩结果不一定是文字摘要,例如 OpenAI 的压缩接口会返回模型可继续使用的非明文压缩内容。OpenAI 官方说明
这些方案既会控制进入上下文的信息量,也会整理已经积累的历史。对于不断增长的历史,一种常见处理方式是上下文压缩:用更少的内容保留继续任务所需的信息。下面按一次压缩的执行顺序,先看 Ink 什么时候触发压缩,再看怎样生成或更新摘要,最后看压缩后怎样继续调用模型。
什么时候触发压缩
需要先估算当前上下文占用了多少 Token,再判断是否接近预先设定的阈值。历史不能一直增长到窗口上限,因为后续回复和压缩时的摘要生成也需要空间。Ink 在 compaction.ts 中用下面的条件判断是否触发压缩:
// 文件:src/core/compaction.ts
// 省略参数类型。
export function shouldCompact(contextTokens, settings) {
return (
contextTokens >
(settings.triggerTokens ?? settings.contextWindow - settings.reserveTokens)
);
}
contextWindow 是所选模型的窗口配置,triggerTokens 是自动压缩阈值。没有显式阈值时,才使用 contextWindow - reserveTokens。当前 reserveTokens 默认为 32,768;它既参与预留空间的计算,也用于推导摘要请求的输出预算,不等于普通对话每次请求的 maxTokens。
实际运行时,compactionSettingsFor 根据模型窗口设置阈值。对于已知窗口、且窗口大于预留量的模型,取下面两项中较小的一个:
- 窗口的 85%,向下取整。
- 窗口减去
reserveTokens。
这样,大窗口也会按比例提前留下余量。用两个窗口配置举例,预留量均采用当前默认值:
| 窗口配置 | 窗口的 85% | 窗口减去 32,768 | 实际压缩阈值 |
|---|---|---|---|
| 200,000 Token | 170,000 | 167,232 | 167,232 Token |
| 1,000,000 Token | 850,000 | 967,232 | 850,000 Token |
回到运行测试的场景:若窗口是 200,000 Token,测试前估算占用 160,000 Token,不触发压缩;加入约 20,000 Token 的测试输出后,占用变成 180,000 Token,就会在下一次调用前尝试压缩。这里的判断是严格大于阈值,恰好等于阈值时还不会触发。
还有两个边界情况:不知道模型窗口、只能使用兜底值时,仍按“窗口减去预留量”计算;已知窗口不大于预留量时,则使用至少为 1 的 85% 比例阈值,避免得到零或负数。后者只能确定触发时机,并不保证这么小的窗口能装下摘要请求。
contextTokens 由 estimateContextTokens 估算:优先参考最近一次可用的模型用量报告,再估算之后新增消息的长度;没有可用报告时,就根据消息内容估算。压缩后,保留消息里可能还带着压缩前的用量报告,这些报告描述的是已经被替换的旧上下文。因此,调用方通过 estimateFloor 排除这些旧报告,暂时按新历史估算,直到新的模型回复提供用量。
估算并不等于准确计数。当前 Ink 对未测量的文本采用字符数除以 4 的近似方法,图片使用固定估值;首次估算也没有单独计入系统指令和工具说明。因此,提前触发压缩仍不能保证请求一定放得下。若实际模型请求返回上下文超限错误,调用方还有一次“尝试压缩、成功后重新请求”的恢复机会;压缩失败或重试仍超限时,会给出错误提示。
长度检查放在每次模型调用前。Agent 循环通过 onBeforeTurn 留出检查和整理历史的位置;处理完成后,再把当前的 history 交给模型。下面节选自 loop.ts,省略类型和非主线分支,不能单独运行:
// 文件:src/core/loop.ts
// 所在函数:runAgentLoop
// 位于循环内;省略前面的中断检查和用户追加消息处理。
await onBeforeTurn?.(history); // 调用前检查,必要时更新历史。
const assistant = await streamAssistant({
provider,
request: {
system,
messages: history,
tools,
model,
maxTokens,
thinking,
signal,
},
onEvent,
usage,
});
// 后续:保存模型回复,执行工具,再把结果加入 history。
调用方通过 onBeforeTurn 接入自动压缩。下面节选 RunnerImpl.runTurnInner 传给 runAgentLoop 的回调,保留自动压缩开关与触发条件:
// 文件:src/runner.ts
// 所在函数:RunnerImpl.runTurnInner
// 节选 runAgentLoop 的 onBeforeTurn 配置;省略其他配置和错误提示包装。
onBeforeTurn: session
? async (history) => {
if (!this.autoCompact) return;
const est = estimateContextTokens(history, this.estimateFloor);
if (!shouldCompact(est.tokens, settings)) return;
await this.compactAndSplice(provider, settings, model, modelReference);
}
: undefined,
这条自动压缩路径要求有会话且开启自动压缩。没有触发阈值时,历史保持原样;触发后,才进入 compactAndSplice。用户新要求和上一轮工具结果已经加入历史,因此检查针对的是即将用于下一次调用的信息。
怎样压缩历史
Ink 采用的是“较早历史生成摘要、近期消息保留原文”的方式。压缩入口 compactAndSplice 通过 compactSession 调用 compactHistory,由后者完成历史切分和摘要生成。如果当前历史已经以一份压缩摘要开头,还会把旧摘要单独取出,要求模型结合这次需要总结的消息更新它。
切分时要保留一段近期消息的原文,因为其中可能包含正在处理的代码和最新错误。
compactHistory 先用 findCutIndex 决定近期原文从哪里开始保留,再确定这次需要总结的消息:
// 文件:src/core/compaction.ts
// 所在函数:compactHistory
// 节选:settings 已准备好,省略前面的用量估算。
const cut = findCutIndex(args.messages, settings.keepRecentTokens);
if (cut <= 0) return null; // 没有适合移入摘要的旧消息。
// 省略旧摘要的识别代码,下文展开:无旧摘要时 summarizeFrom 为 0,识别到时为 1。
const toSummarize = args.messages.slice(summarizeFrom, cut);
if (toSummarize.length === 0) return null;
const retainedTail = args.messages.slice(cut);
keepRecentTokens 表示希望保留多少近期内容,是近似目标。findCutIndex 从后往前累计长度,达到目标后,继续向前寻找 user 或 assistant 消息作为保留段的起点。
为什么不能停在任意消息上?假设最后两条消息是:
| 消息 | 内容 |
|---|---|
| 模型回复 | 调用 bash 运行测试,调用 ID 为 t7 |
| 工具结果 | t7 的测试输出,负数用例失败 |
如果只保留第二条,就剩下一个失去对应调用的工具结果。切分需要尊重消息关系;这个例子应连同前面的模型回复一起保留。当前实现按消息角色寻找边界,不是在这一步重新判断每条内容的相关性。
切分后,serializeForSummary 把 toSummarize 中的消息整理成带角色说明的文本,compactHistory 再单独调用模型生成摘要。这次调用不提供工具,系统指令要求只输出摘要,不继续执行原任务。
首次压缩时,summarizeFrom 为 0,从历史开头取出切分点之前的消息。摘要要用于后续工作,因此不能只写“读了文件、修改了代码、跑了测试”。它需要交代原任务的要求、已经取得的进展,以及接下来继续处理所需的依据。源码中的 SUMMARIZATION_PROMPT 明确规定了这些内容:
// 文件:src/core/compaction.ts
// 模块级常量:SUMMARIZATION_PROMPT
// 节选:省略开头的任务说明,保留摘要格式与信息保留要求。
const SUMMARIZATION_PROMPT = `Use this EXACT format:
## Goal
[What is the user trying to accomplish? Can be multiple items.]
## Constraints & Preferences
- [Any constraints, preferences, or requirements mentioned by the user]
- [Or "(none)" if none were mentioned]
## Progress
### Done
- [x] [Completed tasks/changes]
### In Progress
- [ ] [Current work]
### Blocked
- [Issues preventing progress, if any]
## Key Decisions
- **[Decision]**: [Brief rationale]
## Next Steps
1. [Ordered list of what should happen next]
## Critical Context
- [Any data, code references, or facts needed to continue]
- [Or "(none)" if not applicable]
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
这里分别要求保留目标、约束、进展、关键决策、下一步和必要上下文。放到修复任务中,“不改公共接口”应进入约束;已经找到的文件和完成的修改属于进展;文件路径、函数名和仍有用的错误信息,需要尽量准确保留。只有操作经过,没有这些细节,模型仍然不知道怎样接着做。
这些要求只作用于送去总结的旧消息,最新测试结果仍保留在近期消息中。提示词不能保证摘要保留了所有必要信息;需要精确使用的代码和错误信息,仍应保留原文,必要时重新读取。
再次压缩时,怎样更新已有摘要
第一次压缩后,历史变成“摘要 + 近期原文”。任务继续推进,这些内容加上后续对话仍可能再次达到压缩阈值。如果每次都把旧摘要当作普通对话重新概括,原有约束和关键细节可能在反复改写中丢失。因此,当前 Ink 为已有摘要准备了更新路径。
它先检查历史的第一条消息:只有这条消息是带有 SUMMARY_MARK 标记、且能分离出摘要正文的文本 user 消息,才取出 previousSummary,把 summarizeFrom 设为 1:
// 文件:src/core/compaction.ts
// 所在函数:compactHistory,确定 cut 之后、切出 toSummarize 之前。
// SUMMARY_MARK 来自 src/core/session/store.ts,用于标记压缩摘要。
const head = args.messages[0];
let previousSummary: string | undefined;
let summarizeFrom = 0;
if (
head !== undefined &&
head.role === "user" &&
typeof head.content === "string" &&
head.content.startsWith(SUMMARY_MARK)
) {
const markerEnd = head.content.indexOf("]\n\n");
if (markerEnd !== -1) {
previousSummary = head.content.slice(markerEnd + 3);
summarizeFrom = 1;
}
}
这样,旧摘要不会混进 serializeForSummary 生成的对话文本,而是单独放入 <previous-summary> 标签。摘要请求同时包含这份旧摘要、本次需要纳入摘要的对话,以及 UPDATE_SUMMARIZATION_PROMPT。这个提示词要求保留已有信息,补充新进展和决策,把已完成事项移入 Done,更新下一步,并允许删除已不再相关的内容。
这里的“新信息”是相对于旧摘要而言的,也包括上次保留原文、这次才移入摘要的消息。沿用第 3 节图中的消息标记,把第一次生成的摘要记为 S1:第一次压缩得到 S1 + M8 + M9,之后又产生 M10、M11。假设第二次在 M10 前切分:
| 当前历史中的内容 | 本次怎样处理 |
|---|---|
| S1:第一次生成的摘要 | 取出正文,作为需要更新的旧摘要 |
| M8、M9:上次保留的测试调用与结果 | 作为本次需要纳入摘要的对话 |
| M10、M11:更近期的消息 | 继续保留原文,不送去生成摘要 |
模型需要生成一份完整的新摘要 S2,更新成功后的历史是 S2 + M10 + M11。例如,S1 中“修改后尚待验证”的进展,现在应结合 M9 更新为“普通用例通过,负数用例仍失败”。这是教学示例,具体措辞仍由模型生成。
如果切分点之前只有 S1,没有任何对话需要加入摘要,toSummarize 就是空数组,函数直接返回 null,不为改写旧摘要再调用一次模型。后续有可总结的消息、又满足压缩条件时,才会请求模型更新摘要。
这条更新路径旨在减少反复概括造成的信息漂移,不能保证摘要无损。它没有重新读取 S1 所替代的全部原始历史;如果旧摘要已经遗漏某个约束,单靠更新提示词也无法恢复它。
摘要生成到一半,达到输出上限怎么办
压缩本身也是一次模型调用。如果摘要只写出了目标和部分进展,还没写完关键决策与下一步,就达到输出上限,这份摘要不能直接替换旧历史,否则后续工作可能丢失重要线索。
Ink 用 summarizerMaxTokens 为摘要请求单独计算输出上限:先取 reserveTokens 的 80% 并向下取整,若已知模型自身的输出上限,再取两者中较小的值。默认预留量为 32,768 时,前者是 26,214 Token;这只是上限,不表示每次都会生成这么多内容。
摘要首次生成时沿用会话的思考档位。对思考内容和正文共享输出预算的模型,较高的思考档位可能挤占摘要正文的空间。summarizeWithRetry 因此对 max_tokens 做了专门处理:
- 先检查是否被中止:如果收到中止信号,就拒绝不完整摘要,不再开始重试。
- 尝试降低思考档位:只有首次返回
max_tokens,且模型支持的最低档位确实低于当前档位时,才在最低档位重试一次。原始待总结内容和输出上限保持不变;有些模型不能关闭思考,最低档位也不一定是off。 - 仍不完整就报错:没有更低档位、重试后仍为
max_tokens,或者最终摘要为空,都不会生成可替换历史的压缩结果。普通请求错误也不会自动进入这条降低思考档位的重试路径。
这次重试针对的是摘要输出被截断。前面提到的上下文超限恢复,针对的是请求输入装不下,触发条件不同。降低思考档位也不能保证摘要一定成功,或保证其中的信息完整准确。
压缩后,怎样继续调用模型
compactHistory 返回摘要和保留段后,compactSession 将它们记录为一次压缩结果。session.buildContext() 据此重新组织消息,compactAndSplice 再更新正在使用的 history:
// 文件:src/runner.ts
// 所在函数:RunnerImpl.compactAndSplice
// 节选:省略 session 的获取与空值检查、模型来源标记和界面提示。
const compacted = await compactSession({
session,
provider,
model,
settings,
signal,
modelMaxTokens: modelMaxTokensFor(this.model),
thinking: this.level,
});
if (compacted) {
this.history.splice(
0,
this.history.length,
...session.buildContext().messages
);
this.syncEstimateFloor(); // 排除保留消息中压缩前的用量报告。
}
return compacted !== null;
只有成功生成摘要并取得压缩结果后,才会记录压缩结果并更新历史;失败时保留原来的消息。压缩最终失败时,当前运行会报错,不会拿着半份摘要继续请求模型。在调用前检查这条路径中,如果只是没有可供压缩的旧消息,函数返回 null,调用方则保留历史继续执行。
splice 在原数组上更新内容,因此前面 Agent 循环中的 messages: history 随后取得的就是摘要和保留的近期消息。系统指令和可用工具仍通过 system、tools 提供,这次历史压缩没有改变它们。
摘要会被包装成带有“这是之前对话的总结”标记的消息,后面接上保留的近期消息。这里变短的是本次模型输入,原始会话记录仍然保留;历史怎样落盘和恢复,第 05 章再展开。
3. 图解:一次历史压缩的过程
回到修复求和函数的任务。Agent 已经读取代码、修改函数并再次运行测试,准备带着最新结果调用模型。假设此时首次触发压缩,并在最新一组工具调用之前切分历史。下面沿着同一组消息,看它怎样变成“摘要 + 近期原文”。
沿着同一组消息,看一次历史压缩
首次压缩的教学示例:消息只展示正文要点,摘要为人工示意。M1–M9 是图中的消息标记,压缩是否触发取决于长度,而不是消息条数。
1 压缩前:9 条历史消息
较早的 7 条 · 待生成摘要
2 在 M8 前切分
假设本次得到 cut = 7
近期的 2 条 · 保留原文
M8 发起测试,M9 返回结果,两条消息一起保留。
3 M1–M7 生成一条摘要
只把这 7 条消息交给模型,要求总结任务、进展和后续工作。
- 目标与约束
- 修复求和函数,保持公共接口不变。
- 已完成
- 已读取 src/sum.ts 与 tests/sum.test.ts,发现字符串拼接问题,并修改了函数。
- 待验证
- 修改后的函数是否通过测试,需要根据后续测试结果判断。
M8、M9 不参与摘要。摘要此时还不知道最新的负数测试失败了。
摘要生成成功,与近期原文重新组合
4 新历史:摘要 + M8 + M9
messages = [S, M8, M9]
system 与 tools 仍按原来的方式提供。
下一次调用时,模型从摘要中知道原任务、接口约束和已完成的修改,再从保留的 M8、M9 中看到最新测试失败的具体信息,接着排查负数用例。较早的读取、测试和修改记录则不再逐条放进这次请求。
4. 这次看什么,与下次怎样恢复
上下文管理贯穿每次模型调用:提前提供必要规则,执行中按需补充信息,历史过长时保留继续工作所需的事实,并将较早的过程整理成摘要。
任务继续推进,模型看到的消息可能已经从完整历史变成了摘要和近期记录。关闭终端后再打开,怎样恢复同样的工作状态?那些没有放进本次上下文的原始消息,又保存在哪里?第 05 章接着看会话与持久化。