端侧 Agent 上下文治理全景:从内存管理、Token 预算到安全沙箱的工程闭环
大模型技术在前端应用落地的过程中,不少团队习惯将服务端原生的 Agent 编排模式直接照搬到端侧。然而,浏览器和移动客户端的运行环境存在严格的内存上限、高昂的网络延迟敏感度以及不可控的用户输入安全隐患。
如果端侧 Agent 在多轮交互中继续采用无脑的全量消息追加(Full Context Append),不仅会导致会话后期的 Token 消耗呈现几何级数上升,还会由于上下文窗口挤占导致首字响应时间(TTFT)大幅增加,甚至因客户端内存溢出(OOM)引发页面卡死崩溃。
一套工业级的端侧 Agent 上下文治理体系,必须将“状态切片”、“Token 预算分配”、“滑动窗口衰减”、“时间旅行记忆回溯”与“端侧执行沙箱”形成严密的工程闭环。
端侧上下文的切片状态模型
管理 Agent 的对话历史不能简单等同于操作一个无序的Message[]数组。在复杂交互中,上下文由多种具有不同生命周期与更新频率的信息片段组成。我们可以基于类 Zustand/Redux 的集中式状态切片思想,将上下文划分为四个核心图层:
- System 冻结区(System Invariant Layer):定义 Agent 的基础人格、安全约束与输出 Schema。该区域具有最高优先级,严禁被动态淘汰。
- 短期对话窗口(Short-term Sliding Window):最近 $N$ 轮的用户输入与 Assistant 响应,保留完整的交互细节与 Tool Call 参数。
- 长期语义记忆摘要(Long-term Semantic Summary):对被淘汰的历史对话进行递归压缩提炼生成的实体关系与关键事实。
- 运行时工作记忆(Ephemeral Working Scratchpad):当前任务单次推理过程中产生的中间推理步骤(Thoughts)、临时变量及工具返回值。任务完成后即刻重置,不进入持久化历史。
export interface TokenBudgetConfig { maxContextTokens: number; systemReservedTokens: number; outputReservedTokens: number; summaryBudgetTokens: number; slidingWindowBudgetTokens: number; } export interface ContextChunk { id: string; role: 'system' | 'user' | 'assistant' | 'tool'; content: string; tokenCount: number; timestamp: number; isPinned?: boolean; metadata?: Record<string, unknown>; } export interface AgentContextSnapshot { sessionId: string; systemPrompt: ContextChunk; summary: string; slidingWindow: ContextChunk[]; totalTokens: number; version: number; }Token 预算动态调配与滑动窗口裁剪
在端侧,我们必须引入即时 Token 估算器(如基于 GPT-Tokenizer 或轻量级 WebAssembly 字节对编码分词器)。每次触发模型调用前,上下文管理器必须计算当前总 Token 量,并在预算溢出时执行确定性的渐进淘汰策略。
淘汰算法不能粗暴地从头部直接切断,必须遵循以下优先级顺序:
- 阶段 1:清理冗余工具载荷。历史 Tool Call 的大体积 JSON 返回值通常只在当轮推理中有价值,归档后应裁剪为关键状态摘要。
- 阶段 2:滑动窗口后入先出衰减。从短期窗口最久远的交互对开始移出,并将其归档至待摘要队列。
- 阶段 3:递归滚动摘要。当待摘要内容达到阈值时,在后台异步调用轻量级模型或端侧小型 LLM(如 Gemma-2B、Qwen-1.5B ONNX)更新长期语义摘要。
export class ContextBudgetGovernor { private budget: TokenBudgetConfig; private history: ContextChunk[] = []; private systemChunk: ContextChunk; private summaryChunk: string = ''; constructor(systemPrompt: string, budget: TokenBudgetConfig, estimateFn: (text: string) => number) { this.budget = budget; this.systemChunk = { id: 'system_core', role: 'system', content: systemPrompt, tokenCount: estimateFn(systemPrompt), timestamp: Date.now(), isPinned: true, }; } public appendMessage(chunk: Omit<ContextChunk, 'id' | 'timestamp'>): void { const newChunk: ContextChunk = { ...chunk, id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`, timestamp: Date.now(), }; this.history.push(newChunk); this.reconcile(); } private reconcile(): void { const availableWindowBudget = this.budget.maxContextTokens - this.budget.systemReservedTokens - this.budget.outputReservedTokens - this.budget.summaryBudgetTokens; let currentWindowTokens = this.history.reduce((sum, item) => sum + item.tokenCount, 0); // 触发窗口自适应收缩 while (currentWindowTokens > availableWindowBudget && this.history.length > 1) { const oldestUnpinnedIndex = this.history.findIndex(item => !item.isPinned); if (oldestUnpinnedIndex === -1) break; const [evicted] = this.history.splice(oldestUnpinnedIndex, 1); currentWindowTokens -= evicted.tokenCount; // 触发后台异步摘要归档 this.enqueueForSummarization(evicted); } } private enqueueForSummarization(evicted: ContextChunk): void { // 写入 IndexedDB 临时流,由 Web Worker 异步提炼实体并更新 summaryChunk } public assembleContext(): Array<{ role: string; content: string }> { const payload: Array<{ role: string; content: string }> = [ { role: this.systemChunk.role, content: this.systemChunk.content } ]; if (this.summaryChunk) { payload.push({ role: 'system', content: `[Historical Context Summary]: ${this.summaryChunk}` }); } for (const msg of this.history) { payload.push({ role: msg.role, content: msg.content }); } return payload; } }客户端记忆持久化与时间旅行回溯
在长链路 Agent 执行过程中,工具执行错误、网络超时或模型输出格式校验失败是常态。如果直接修改底层历史记录,重试逻辑将极其混乱。
我们需要为上下文仓库实现快照捕获(Snapshot)与回滚(Rollback)机制,配合 IndexedDB 进行异步冷热分层持久化:
export class ContextHistoryReplayer { private snapshots: AgentContextSnapshot[] = []; private maxSnapshots: number = 20; public saveSnapshot(snapshot: AgentContextSnapshot): void { if (this.snapshots.length >= this.maxSnapshots) { this.snapshots.shift(); } this.snapshots.push(structuredClone(snapshot)); } public rollbackToVersion(targetVersion: number): AgentContextSnapshot { const index = this.snapshots.findIndex(s => s.version === targetVersion); if (index === -1) { throw new Error(`Target snapshot version ${targetVersion} not found in memory`); } // 裁剪后续分支快照,恢复确定性时间线 const targetSnapshot = this.snapshots[index]; this.snapshots = this.snapshots.slice(0, index + 1); return structuredClone(targetSnapshot); } }持久化设计中,必须使用idb-keyval或纯原生 IndexedDB 对会话数据进行差量存储。短期热数据常驻内存,冷会话及淘汰的原始长文本只在数据库中存储引用。当用户重新唤醒旧会话时,通过版本快照以最小 I/O 成本重建上下文树。
端侧安全沙箱与敏感信息治理
端侧 Agent 拥有直接操作 DOM、读取本地存储及调用宿主 Native API 的能力。一旦上下文遭遇恶意注入(Prompt Injection)或混入未经脱敏的敏感个人信息(PII),危害将直接穿透至用户设备。
在上下文进入组装与发送管线前,必须经过两道确定性防线:
export class ContextSecuritySanitizer { private static PII_PATTERNS = [ { type: 'ID_CARD', regex: /\b\d{17}[\dXx]\b/g, replacement: '[MASKED_ID]' }, { type: 'PHONE', regex: /\b1[3-9]\d{9}\b/g, replacement: '[MASKED_PHONE]' }, { type: 'ACCESS_TOKEN', regex: /(bearer\s+|token=)[\w-]{20,}/gi, replacement: '$1[MASKED_TOKEN]' }, ]; private static INJECTION_PATTERNS = [ /ignore\s+all\s+previous\s+instructions/i, /system\s+override\s+mode/i, /<\|im_start\|>/i, /<\|im_end\|>/i, ]; public static sanitizeText(raw: string): string { let clean = raw; // 1. PII 动态脱敏 for (const pattern of this.PII_PATTERNS) { clean = clean.replace(pattern.regex, pattern.replacement); } // 2. 特殊 Token 与元提示词注入防护 for (const injection of this.INJECTION_PATTERNS) { clean = clean.replace(injection, '[BLOCKED_INJECTION_SIGNAL]'); } return clean; } }对于端侧 Tool Call 的执行,所有在浏览器端运行的代码解释器(如 WebAssembly 版 QuickJS 或 Pyodide)必须运行在隔离的 Web Worker 中,并彻底禁用eval、Function构造器以及跨域网络访问权限,通过严格定义的消息通信通道进行输入输出校验。
生产环境治理指标度量
将端侧 Agent 推向生产环境后,必须建立完善的上下文监控大盘,重点跟踪以下四项核心指标:
- Context Token Saturation Rate(上下文饱和度):当前会话 Token 占模型最大窗口预算的比例,监控滑动窗口淘汰机制是否正常触发。
- Context Hit Ratio(上下文有效利用率):模型生成的有效答案与上下文中注入实体信息的关联度,防止无效长文本污染。
- Eviction Re-fetch Rate(淘汰重查率):用户在多轮对话中被迫重复提及已被淘汰信息的频率,用以动态调优滑动窗口容量。
- Client Memory Footprint(内存水线):IndexedDB 存储与前端 Store 对象占用的堆内存大小,严格压制在 50MB 警戒线以内。
通过在客户端建立类型安全的状态切片、严苛的 Token 预算调度、确定的时间旅行快照以及内存与安全双沙箱,我们才能在资源受限的前端宿主中构建出高可用、低延迟且坚固稳定的企业级 Agent 应用。