编码母题 / P1 / Agent 运行时 / 04
上下文预算与消息裁剪器
先读清题意与边界,独立尝试,再展开思路并对照可运行的参考代码。
重要程度高价值难度困难预计完成45–60 分钟
题目是什么
在 tokenBudget 内选择系统消息和最近对话。工具调用与响应必须成组保留;过大的工具结果可压缩,不能留下孤立响应。
输入输出示例
预算不足以同时容纳一次 assistant tool_calls 和对应 tool 回复时,应整组舍弃。
练习起手式与验证
复制代码后补全空方法,再运行下方三组验证;复杂题所需的辅助类型与输入准备一并放在代码块中。
/*
* 题目:上下文预算与消息裁剪器
* 要求:在 tokenBudget 内选择系统消息和最近对话。工具调用与响应必须成组保留;
* 过大的工具结果可压缩,不能留下孤立响应。
* 示例:预算不足以同时容纳一次 assistant tool_calls 和对应 tool
* 回复时,应整组舍弃。
*/
function groupMessages(messages) {}
function buildContext(messages, { tokenBudget, tokenCounter, compactTool = (m) => m }) {}
const count = (m) => String(m.content || "").length + (m.toolCalls ? 1 : 0);
const messages = [
{ role: "system", content: "sys" },
{ role: "user", content: "oldold" },
{ role: "assistant", content: "call", toolCalls: [{ id: "1" }] },
{ role: "tool", toolCallId: "1", content: "result" },
{ role: "user", content: "new" },
];
const context = buildContext(messages, {
tokenBudget: 18,
tokenCounter: count,
});
console.log(context.messages.map((m) => m.content)); // 预期:["sys", "call", "result", "new"]
console.log(context.usedTokens <= 18); // 预期:true
console.log((() => { try { (() => buildContext([{ role: "tool", toolCallId: "1" }], {
tokenBudget: 10,
tokenCounter: count,
}))(); return false; } catch { return true; } })()); // 预期:true(抛出异常)
/*
* 题目:上下文预算与消息裁剪器
* 要求:在 tokenBudget 内选择系统消息和最近对话。工具调用与响应必须成组保留;
* 过大的工具结果可压缩,不能留下孤立响应。
* 示例:预算不足以同时容纳一次 assistant tool_calls 和对应 tool
* 回复时,应整组舍弃。
*/
function groupMessages(messages) {}
function buildContext(messages, { tokenBudget, tokenCounter, compactTool = (m) => m }) {}
const count = (m) => String(m.content || "").length + (m.toolCalls ? 1 : 0);
const messages = [
{ role: "system", content: "sys" },
{ role: "user", content: "oldold" },
{ role: "assistant", content: "call", toolCalls: [{ id: "1" }] },
{ role: "tool", toolCallId: "1", content: "result" },
{ role: "user", content: "new" },
];
const context = buildContext(messages, {
tokenBudget: 18,
tokenCounter: count,
});
console.log(context.messages.map((m) => m.content)); // 预期:["sys", "call", "result", "new"]
console.log(context.usedTokens <= 18); // 预期:true
console.log((() => { try { (() => buildContext([{ role: "tool", toolCallId: "1" }], {
tokenBudget: 10,
tokenCounter: count,
}))(); return false; } catch { return true; } })()); // 预期:true(抛出异常)解题思路
展开思路、不变量与常见边界
关键切入点:先把消息构造成原子组,再从最新向旧选择;系统消息始终保留且计入预算。
核心模型:先把消息切成原子组,再从后向前选择;最后恢复原顺序。不变量:总 Token 不超预算;系统消息顺序不变;孤立、重复或缺失响应的 tool 调用组直接报错。 生产系统可在丢弃 内容前生成摘要,但摘要本身也必须计入预算。
参考代码与验证
“关键机制”注释用于解释实现,下载链接提供未经页面注释修改的原始 JS 文件。
// 关键机制:先把消息构造成原子组,再从最新向旧选择;系统消息始终保留且计入预算。
function groupMessages(messages) {
const groups = [];
for (let i = 0; i < messages.length; i += 1) {
const message = messages[i];
if (message.role === "assistant" && message.toolCalls?.length) {
const ids = new Set(message.toolCalls.map((c) => c.id));
if (ids.size !== message.toolCalls.length)
throw new Error("duplicate tool call id");
const group = [message];
while (
i + 1 < messages.length &&
messages[i + 1].role === "tool" &&
ids.has(messages[i + 1].toolCallId)
)
group.push(messages[++i]);
const received = group.slice(1).map((m) => m.toolCallId);
if (received.length !== ids.size || new Set(received).size !== ids.size)
throw new Error("incomplete tool call group");
groups.push(group);
} else if (message.role === "tool")
throw new Error("orphan tool message");
else groups.push([message]);
}
return groups;
}
function buildContext(
messages,
{ tokenBudget, tokenCounter, compactTool = (m) => m },
) {
if (!(tokenBudget >= 0) || typeof tokenCounter !== "function")
throw new TypeError("invalid options");
const normalized = messages.map((m) =>
m.role === "tool" ? compactTool(m) : m,
);
const systems = normalized.filter((m) => m.role === "system");
const rest = normalized.filter((m) => m.role !== "system");
let used = systems.reduce((sum, m) => sum + tokenCounter(m), 0);
if (used > tokenBudget)
throw new Error("system messages exceed token budget");
const selected = [];
const groups = groupMessages(rest);
for (let i = groups.length - 1; i >= 0; i -= 1) {
const cost = groups[i].reduce((sum, m) => sum + tokenCounter(m), 0);
if (used + cost <= tokenBudget) {
selected.push(groups[i]);
used += cost;
}
}
return {
messages: [...systems, ...selected.reverse().flat()],
usedTokens: used,
};
}
// 三组以上可直接运行的验证。
const count = (m) => String(m.content || "").length + (m.toolCalls ? 1 : 0);
const messages = [
{ role: "system", content: "sys" },
{ role: "user", content: "oldold" },
{ role: "assistant", content: "call", toolCalls: [{ id: "1" }] },
{ role: "tool", toolCallId: "1", content: "result" },
{ role: "user", content: "new" },
];
const context = buildContext(messages, {
tokenBudget: 18,
tokenCounter: count,
});
console.log(context.messages.map((m) => m.content)); // 预期:["sys", "call", "result", "new"]
console.log(context.usedTokens <= 18); // 预期:true
console.log((() => { try { (() => buildContext([{ role: "tool", toolCallId: "1" }], {
tokenBudget: 10,
tokenCounter: count,
}))(); return false; } catch { return true; } })()); // 预期:true(抛出异常)
console.log((() => { try { (() => groupMessages([{ role: "assistant", toolCalls: [{ id: "1" }] }]))(); return false; } catch { return true; } })()); // 预期:true(抛出异常)
// 关键机制:先把消息构造成原子组,再从最新向旧选择;系统消息始终保留且计入预算。
function groupMessages(messages) {
const groups = [];
for (let i = 0; i < messages.length; i += 1) {
const message = messages[i];
if (message.role === "assistant" && message.toolCalls?.length) {
const ids = new Set(message.toolCalls.map((c) => c.id));
if (ids.size !== message.toolCalls.length)
throw new Error("duplicate tool call id");
const group = [message];
while (
i + 1 < messages.length &&
messages[i + 1].role === "tool" &&
ids.has(messages[i + 1].toolCallId)
)
group.push(messages[++i]);
const received = group.slice(1).map((m) => m.toolCallId);
if (received.length !== ids.size || new Set(received).size !== ids.size)
throw new Error("incomplete tool call group");
groups.push(group);
} else if (message.role === "tool")
throw new Error("orphan tool message");
else groups.push([message]);
}
return groups;
}
function buildContext(
messages,
{ tokenBudget, tokenCounter, compactTool = (m) => m },
) {
if (!(tokenBudget >= 0) || typeof tokenCounter !== "function")
throw new TypeError("invalid options");
const normalized = messages.map((m) =>
m.role === "tool" ? compactTool(m) : m,
);
const systems = normalized.filter((m) => m.role === "system");
const rest = normalized.filter((m) => m.role !== "system");
let used = systems.reduce((sum, m) => sum + tokenCounter(m), 0);
if (used > tokenBudget)
throw new Error("system messages exceed token budget");
const selected = [];
const groups = groupMessages(rest);
for (let i = groups.length - 1; i >= 0; i -= 1) {
const cost = groups[i].reduce((sum, m) => sum + tokenCounter(m), 0);
if (used + cost <= tokenBudget) {
selected.push(groups[i]);
used += cost;
}
}
return {
messages: [...systems, ...selected.reverse().flat()],
usedTokens: used,
};
}
// 三组以上可直接运行的验证。
const count = (m) => String(m.content || "").length + (m.toolCalls ? 1 : 0);
const messages = [
{ role: "system", content: "sys" },
{ role: "user", content: "oldold" },
{ role: "assistant", content: "call", toolCalls: [{ id: "1" }] },
{ role: "tool", toolCallId: "1", content: "result" },
{ role: "user", content: "new" },
];
const context = buildContext(messages, {
tokenBudget: 18,
tokenCounter: count,
});
console.log(context.messages.map((m) => m.content)); // 预期:["sys", "call", "result", "new"]
console.log(context.usedTokens <= 18); // 预期:true
console.log((() => { try { (() => buildContext([{ role: "tool", toolCallId: "1" }], {
tokenBudget: 10,
tokenCounter: count,
}))(); return false; } catch { return true; } })()); // 预期:true(抛出异常)
console.log((() => { try { (() => groupMessages([{ role: "assistant", toolCalls: [{ id: "1" }] }]))(); return false; } catch { return true; } })()); // 预期:true(抛出异常)
复杂度分析
时间复杂度:O(M+C)。线性分组 M 条消息并处理 C 个工具调用 id。
空间复杂度:O(M+C)。规范化消息、分组和选择结果最多与输入规模相当。