上下文管理
长时运行的智能体很快就会触及输入 token 上限。在 AgentConfig 上设置 contextStrategy,控制对话在变长时如何收缩:
const agent: AgentConfig = { name: 'long-runner', model: 'claude-sonnet-4-6', // Pick one: contextStrategy: { type: 'sliding-window', maxTurns: 20 }, // contextStrategy: { type: 'summarize', maxTokens: 80_000, summaryModel: 'claude-haiku-4-5' }, // contextStrategy: { type: 'compact', maxTokens: 100_000, preserveRecentTurns: 4 }, // contextStrategy: { type: 'custom', compress: (messages, estimatedTokens) => ... },}| Strategy | 何时选用 |
|---|---|
sliding-window | 开销最低。保留最近 N 轮,其余丢弃。 |
summarize | 把旧轮次发送给一个摘要模型;用摘要替换原始内容。 |
compact | 基于规则:截断大段 assistant 文本和工具结果,最近轮次原样保留。不额外调用 LLM。 |
custom | 提供你自己的 compress(messages, estimatedTokens) 函数。 |
审计策略替换了什么
Section titled “审计策略替换了什么”上面每种策略都会破坏性地重写对话:它产出的消息就是下一次请求所携带的内容,而原始内容已从工作对话中消失。启用运行事件日志后,每次应用策略还会发出一个 context/replace 事件,记录移除了什么、以及取而代之的是什么,因此模型实际看到的对话在事后依然可以重建。
const journal = new InMemoryRunJournal()await orchestrator.runTasks(team, tasks, { journal })
for (const event of await journal.readFrom(0)) { if (event.type !== 'context/replace') continue console.log(event.strategy, event.replacements.length, event.dropped?.sourceEventSeqs)}| 策略 | strategy | 该事件记录了什么 |
|---|---|---|
sliding-window | 'sliding-window' | dropped 点名每个被移除轮次的每个块;一条替换项承载截断提示,其来源是被丢弃的那些轮次以及它被并入的那条消息。detail: { droppedTurns }。 |
summarize | 'summarize' | 一条替换项承载 [Conversation summary] 块,其来源是被它压缩的那些旧轮次。detail 点名摘要模型及其用量。摘要模型自身的调用不会作为一轮被记入日志——它是这次重写的实现细节。 |
compact | 'compact' | 每个被重写的块一条替换项(截断后的文本、[Image compacted]、[Tool result: …, compacted]),每条只点名它所替换的那一个块。未被触碰的块保留各自的来源。 |
compressToolResults | 'compress-tool-results' | 每个新近被压缩的结果一条替换项。已压缩过的标记会被跳过,因此一次长时运行是每个结果一个事件,而不是每次请求一个。 |
custom | 'custom' | 你的函数新造出来的任何块,都会原样存储,并以整个输入对话作为其来源。你按引用透传的块保留各自的来源。 |
每个派生块的来源,就是承载它的那个事件的单一序号;而且该块按原样存储,而不是存成「如何重建它」的描述——这正是 enforceLineage: true 能在每种内置策略下都通过的原因。一次什么都没有改变的处理不会发出任何事件;summarize 的备忘缓存会复用首次记录该摘要的那个事件,而不是再写一遍。
压缩工具结果
Section titled “压缩工具结果”工具输出会跨轮次留在对话历史中,即便智能体已经据此行动过。在长时运行中,这会占用相当大一部分上下文预算。
compressToolResults 会在每次新的 LLM 调用前,把已消费的工具结果(即后面跟着一条 assistant 回复的那些)替换成一个简短标记:
const agent: AgentConfig = { name: 'long-runner', model: 'claude-sonnet-4-6', // Enable with the default threshold (500 chars): compressToolResults: true, // Or only compress results longer than N characters: // compressToolResults: { minChars: 2000 },}| Value | 行为 |
|---|---|
true | 压缩长度超过 500 字符的结果(默认阈值) |
{ minChars: N } | 压缩长度超过 N 字符的结果 |
false / undefined | 禁用(默认) |
说明:
- 错误类工具结果永不压缩。
- 委派的
tool_result块(来自delegate_to_agent)豁免——父智能体始终保留子智能体的完整输出。 - 富媒体的图像/文件结果按估算大小来判断阈值。已消费的富结果可能变成一个文本标记;最新的那个富结果保持完整。
summarize策略在请求摘要模型压缩旧轮次之前,会先把富媒体的字节与 URL 替换成描述性占位符。- 与
contextStrategy协同工作;两者结合可获得最大的上下文余量。
截断工具输出
Section titled “截断工具输出”maxToolOutputChars 为隐式的字符串型工具结果限制原始输出长度。超过限制的输出会被截断成「头部 + 尾部」摘录,中间放一个标记。显式的富 modelOutput 不会被重写;请在工具内部调整其尺寸或为它设限。这发生在执行时、结果进入对话之前。
const agent: AgentConfig = { name: 'long-runner', model: 'claude-sonnet-4-6', maxToolOutputChars: 10_000, // truncate any single tool output to 10 k chars}按工具设置的 maxOutputChars(设在 ToolDefinition 上)优先于智能体级的 maxToolOutputChars。
跨提供方保留推理
Section titled “跨提供方保留推理”推理模型(OpenAI o 系列、DeepSeek reasoner、Anthropic 扩展思考、Gemini 思考摘要)会产出中间推理,框架将其提取为 ReasoningBlock,并带一个 provenance 字段标明产出它的适配器。默认情况下,只有带有效签名的同提供方块会被回传;其余的在出站转换时被静默丢弃,以免接收方模型拒绝一个未签名的思考块,或为了让 prompt 体积可预测。
preserveReasoningAsText 选择性启用一个 <thinking>...</thinking> 文本兜底:每当出站转换遇到一个目标适配器无法原生回传的推理块时,该块会被降级为行内文本,并拼接到下一条 assistant 消息前:
const agent: AgentConfig = { name: 'cross-provider', model: 'gpt-5', provider: 'openai', // Enable text fallback for reasoning blocks the target adapter can't echo. preserveReasoningAsText: true, // Defaults to ON when preserveReasoningAsText is true; head+tail truncate // each block to 1200 chars. Override to tune, or set false to disable. // compressReasoningText: { minChars: 4000 },}兜底触发时:
| Source provenance | Target adapter capability | preserveReasoningAsText: true 下的行为 |
|---|---|---|
与目标匹配('anthropic' → Anthropic)且有签名 | 'own-issued' | 原生回传(不变) |
| 与目标匹配但无签名 | 'own-issued' | 文本兜底 |
外来(如 'openai' → Anthropic) | 'own-issued' | 文本兜底 |
在工具调用对话中与目标匹配('deepseek' → DeepSeek) | 'tool-use-only' | 原生 reasoning_content 回传(依 DeepSeek V4 规范,见 #251) |
与目标匹配但历史中任何位置都没有 tool_use | 'tool-use-only' | 丢弃(DeepSeek 规范忽略非工具推理) |
外来(如 'openai' → DeepSeek) | 'tool-use-only' | 文本兜底 |
与目标匹配('bedrock' → Bedrock)且有签名或已脱敏 | 'own-issued' | 经 reasoningContent.{reasoningText,redactedContent} 原生回传(见 #223) |
| 任意 | 'never'(OpenAI、Azure、Copilot、AI SDK 等) | 文本兜底 |
被脱敏的推理(Anthropic 安全过滤)会产出占位符 <thinking>[redacted]</thinking>,用以表明发生过推理,同时不泄露那段不透明的负载。
说明:
- 默认禁用,以免静默膨胀 prompt token。
- 默认开启的截断(
compressReasoningText)在长链式思考上是强制的安全措施;只在调试时才关闭。 - 某些本地 OpenAI 兼容模型可能把
<thinking>文本重新输出到自己的 assistant 回复中,这可能触发循环检测器。失败模式与缓解办法见examples/patterns/cross-provider-reasoning.ts。 - Bedrock 的
capabilities.echoesReasoning === 'own-issued':带签名的推理块(reasoningContent.reasoningText.signature)和脱敏块(reasoningContent.redactedContent)在chat()和stream()上都能原生往返,入站提取与出站序列化两侧皆然(见 #223)。 'tool-use-only'(DeepSeek V4)是唯一一种无需用户启用preserveReasoningAsText就能做同提供方回传的能力——它在内部被强制开启,因为 DeepSeek API 要求如此。