第 9 章 · transformContext 上下文变换
本章目标:掌握消息处理流水线的完整顺序,用
transformContext实现历史截断与上下文压缩策略。
9.1 完整流水线
每次 LLM 调用前,消息经过两道加工:
text
AgentMessage[]
│
▼
transformContext(messages, signal) ← 异步、可截断/压缩/注入
│ 返回 AgentMessage[](应用层形态)
▼
convertToLlm(messages) ← 过滤自定义类型、转成标准格式
│ 返回 Message[](LLM 层形态)
▼
LLM 请求两者职责不同:transformContext 管「给模型看多少」(量的问题),convertToLlm 管「以什么形态看」(形的问题)。
9.2 基础用法:保留最近 N 轮
typescript
const agent = new Agent({
streamFn: models.streamSimple.bind(models),
initialState: { systemPrompt: "...", model },
// 异步签名,可接收 AbortSignal
transformContext: async (messages, signal) => {
const MAX = 40; // 保留最近 40 条
if (messages.length <= MAX) return messages;
// 简单滑动窗口:丢掉最老的消息
// 注意:toolResult 不能脱离其对应的 assistant 工具调用单独存在,
// 截断点要避开「工具对」的中间位置
return messages.slice(-MAX);
},
});截断的坑:工具对完整性
一条 assistant 消息可能包含 toolCall,紧随其后必有对应 toolResult。若从中间切断,LLM 会收到「孤立的工具结果」而报错。安全做法是按回合边界(user 消息处)切割。
9.3 按回合边界的安全截断
typescript
// 以 user 消息为界分组,再保留最近 N 个完整回合
function truncateByTurns(messages, maxTurns = 10) {
// 找到每条 user 消息的下标作为回合起点
const starts = messages
.map((m, i) => (m.role === "user" ? i : -1))
.filter((i) => i >= 0);
if (starts.length <= maxTurns) return messages;
const cutFrom = starts[starts.length - maxTurns];
return messages.slice(cutFrom);
}
const agent2 = new Agent({
transformContext: async (msgs) => truncateByTurns(msgs, 8),
initialState: { systemPrompt: "...", model },
streamFn: models.streamSimple.bind(models),
});9.4 进阶:压缩(Compaction)策略
比丢弃更聪明的是摘要替换——把老对话压成一段总结:
typescript
// 思路示意:超过阈值时,把老回合交给便宜模型压缩成摘要
transformContext: async (messages) => {
if (estimateTokens(messages) < 50_000) return messages;
const [oldPart, recentPart] =
splitAtTurnBoundary(messages, keepRecentTurns = 6);
// 用小模型生成摘要(复用同一个 models 集合)
const summaryMsg = await models.completeSimple(cheapModel, {
systemPrompt: "把以下对话压缩成要点摘要,保留关键事实与未完成任务。",
messages: toLlmShape(oldPart),
});
// 摘要以 user 消息形式注入开头
return [
{
role: "user",
content: `[历史摘要] ${summaryMsg.content[0].text}`,
timestamp: Date.now(),
},
...recentPart,
];
},生产级实现还应缓存摘要结果(同一批老消息只压一次),并配合 shouldStopAfterTurn 钩子在回合间隙做压缩决策。
9.5 其他用途:注入外部上下文
typescript
// 除了裁剪,还能注入:比如实时检索到的资料
transformContext: async (messages, signal) => {
const lastUser = [...messages].reverse().find((m) => m.role === "user");
const docs = await retrieve(lastUser?.content ?? "", signal); // RAG 检索
return [
...messages,
{
role: "user",
content: `[参考资料]\n${docs}`,
timestamp: Date.now(),
},
];
};9.6 本章小结
- 流水线顺序固定:
transformContext → convertToLlm → LLM; - transformContext 解决「量」,convertToLlm 解决「形」;
- 截断必须在回合边界进行,避免拆散 assistant(toolCall)+toolResult 对;
- 压缩优于丢弃;配合 shouldStopAfterTurn 可在回合间隙从容执行;
- 注入 RAG 资料、动态系统上下文也是它的合法用途。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. transformContext 与 convertToLlm 的职责划分是?
2. 为什么简单按条数 slice 截断可能出问题?
3. 相比直接丢弃老消息,「压缩」策略的核心优势是?
4. transformContext 的函数签名是什么?
🛠️ 动手实践
- 实现「保留最近 8 回合 + 更早内容压缩为摘要」的组合策略,并用长对话实测 token 变化。
- 给你的压缩逻辑加缓存:同一段老消息第二次触发时直接复用已有摘要。
- 利用 transformContext 注入当前日期与用户时区,验证模型回答「今天几号」的正确性。
下一章第 10 章:API Key 从哪里来?深入认证解析与凭据管理。