先看结论与判断条件
- 系统策略、工具权限和必需业务约束属于不可删除层,超预算时应拒绝请求或切换受控模型档位,而不是裁掉规则。
- 上下文预算要使用目标模型实际 tokenizer、模板包裹和预留输出计算,字符数或平均 token 估算不能作为最终门禁。
- 历史消息按完整 turn 处理,不能留下助手回复却删除其用户问题,也不能在摘要后打乱原始 sequence。
- 摘要是版本化数据资产,必须保留约束 ID、未决事项、来源范围和覆盖消息区间,不能只生成一段流畅文字。
- RAG 片段按当前问题相关性、来源权限与时效进入可淘汰层,文档中的指令不能提升自身优先级。
- 每次截断记录 policyVersion、modelBundleId、generation、保留项、移除项和 token 统计,以便复现实际模型输入。
先定义不可删除、可摘要和可淘汰三类上下文
上下文截断不是普通文本压缩,而是一次策略决策。系统策略、工具 allowlist、数据范围、输出 schema 和当前请求中的必需参数构成 immutable 层;旧对话和已确认事实进入 summarizable 层;低优先级检索片段、重复解释和过期候选进入 evictable 层。每个块在创建时就带 layer,不能等超预算后让模型猜重要性。
immutable 不代表永远无限增长。系统与业务团队要把规则拆成稳定 ID,删除重复表述,模板发布时计算固定成本。若不可删除层加当前请求已经超过模型输入上限,正确终态是 context_policy_overflow,提示缩小任务或采用已审核的更大上下文档位;不能为了请求成功悄悄裁掉授权或输出约束。
层级来自受控装配器,不接受用户、检索文档或历史摘要自报优先级。用户可以标记业务偏好,但它仍属于 request 层;检索片段即便写着“必须永久保留”也只是数据。优先级与消息 role、来源和 policyVersion 绑定,从结构上阻止低信任内容挤占系统预算。
| 层级 | 典型内容 | 允许动作 | 超预算终态 |
|---|---|---|---|
| immutable_policy | 系统策略、工具与数据范围 | 仅模板版本替换 | 拒绝请求 |
| required_request | 当前问题与必需参数 | 用户显式修改 | 要求缩小输入 |
| conversation_turn | 历史问答轮次 | 按完整 turn 摘要或移除 | 记录覆盖区间 |
| retrieval_data | RAG 事实片段 | 按相关性和权限淘汰 | 减少引用 |
| diagnostic_context | 非关键调试说明 | 优先移除 | 不进入模型 |
预算必须包含模板包裹、工具声明和输出预留
模型输入成本不只来自可见消息。system instruction、消息角色标记、模板分隔符、工具 schema、结构化输出 schema 和运行时包装都会占用 token。规划器要针对 modelBundleId 使用实际 tokenizer 编码最终消息对象,并预留最大允许输出和安全余量;不能把中文字符数乘固定系数当作发布门禁。
工具声明可能比历史对话更大,且不同任务使用的 allowlist 不同。预算事件要分别记录 policyTokens、toolTokens、requestTokens、historyTokens、retrievalTokens、reservedOutputTokens 和 framingTokens。只有总和低于 inputLimit 才可执行,统计缺少任一项就拒绝,避免 SDK 隐式包装在最后一步溢出。
端侧模型、云模型和 LoRA 组合可能使用不同 tokenizer、最大上下文与模板格式。modelBundleId 要绑定基础模型、适配器、tokenizer 和模板版本。切换后必须重新规划,不能复用旧 token 计数。某个 Android 推理 API 的模型与后端约束也不能外推给其他运行时,配置按实际依赖版本审查。
| 预算项 | 计算来源 | 能否截断 | 证据 |
|---|---|---|---|
| policyTokens | 发布模板实际编码 | 否 | policyVersion |
| toolTokens | 当前 allowlist schema | 通常否 | toolSetVersion |
| requestTokens | 当前用户请求 | 有限重述 | requestId |
| historyTokens | 完整历史轮次 | 可摘要或淘汰 | sequence 区间 |
| retrievalTokens | 授权检索片段 | 可淘汰 | citationId |
| reservedOutputTokens | 产品输出上限 | 按档位固定 | outputPolicy |
| framingTokens | SDK 与模板包裹 | 否 | runtimeVersion |
历史按完整轮次和 sequence 处理
最简单的“保留最后 N 条消息”会破坏成对语义。若删除用户问题却保留助手答案,模型无法知道答案针对什么;若只删除工具请求却保留工具结果,结果可能被当成无来源事实。conversation turn 应包含用户消息、助手候选、工具调用与结果的完整关系,截断以 turn 为最小单位。
每条消息带单调 sequence,会话保存 generation。规划器只能保留原有相对顺序,摘要块放在其覆盖区间的逻辑位置,并声明 coversFrom 与 coversTo。页面切换、重新发送或分支对话产生新 generation,旧规划结果不得写回新会话;迟到的 token 统计同样按 generation 拒绝。
当前未完成轮次通常不可被摘要,因为工具结果或用户确认还没到齐。状态机区分 open、completed、cancelled 和 superseded;只有 completed turn 能进入摘要队列。cancelled 可以按审计策略保留最小终态,不能把半个助手流式回复汇入长期记忆,避免未确认内容被后续当作事实。
| turn 状态 | 可否摘要 | 可否淘汰 | 必须保留 |
|---|---|---|---|
| open | 否 | 否 | 当前请求与已到达工具结果 |
| completed | 是 | 摘要后可淘汰原文 | sequence 与约束 |
| cancelled | 按策略仅终态 | 可以 | 取消原因摘要 |
| superseded | 否 | 可以 | 代际切换记录 |
| tool_pending | 否 | 否 | 调用与授权上下文 |
| policy_blocked | 否 | 可保留最小回执 | 阻止原因 |
摘要必须保留约束清单、未决事项和来源
自由文本摘要容易把否定条件、数字单位、责任主体和未决问题压掉。摘要 schema 应包含 summaryText、requiredConstraintIds、openItems、confirmedFacts、citationIds、coversFrom、coversTo、summaryVersion 和 sourceGeneration。文本帮助模型理解,结构字段用于装配器确定性验证,两者任一缺失都不替换原历史。
requiredConstraintIds 来自原 turn 中标记的业务约束,不由摘要模型自行创造。生成后,门禁比较覆盖区间内所有 mustPreserve ID 是否都出现在摘要中;多出未知 ID 也应拒绝,因为它可能把普通陈述升级为约束。confirmedFacts 必须携带来源或用户确认,不把助手猜测写入长期记忆。
摘要本身也会老化。后续用户撤销偏好、权限变化或业务事实更新时,新 turn 生成 supersedes 关系,下一次规划排除已撤销约束。不能在旧摘要文本上直接字符串替换;要基于结构化事件重建新的 summaryVersion,并保存旧摘要的覆盖与失效回执。
| 字段 | 回答问题 | 验证规则 | 失败处理 |
|---|---|---|---|
| coversFrom、coversTo | 覆盖哪些消息 | 区间有效且连续 | 拒绝摘要 |
| requiredConstraintIds | 哪些约束必须留下 | 与源区间集合一致 | 保留原 turn |
| openItems | 哪些事项未完成 | 状态仍为 open | 不当作事实 |
| confirmedFacts | 哪些事实已确认 | 携带来源或确认 ID | 拒绝无来源项 |
| citationIds | 事实来自哪里 | 属于源上下文 | 删除越界引用 |
| sourceGeneration | 属于哪个会话代际 | 等于当前 generation | 拒绝旧摘要 |
检索片段先按权限过滤,再按价值淘汰
RAG 片段进入预算前先做租户、数据集、时效和来源校验。越权或过期内容不是低优先级,而是根本不得进入上下文。通过授权后,才按当前问题相关性、来源等级、重复度和引用必要性排序。淘汰回执保存 citationId 与 reason,不复制敏感原文。
检索内容只提供事实,不能修改 layer、toolTokens 或 policyTokens。文档中的祈使句和伪 system 标签仍在 retrieval_data 层。装配器将片段放入固定引用区,超预算时先移除重复和低价值片段;若回答必须引用的证据全部被移除,终态改为 insufficient_evidence,而不是生成无来源答案。
历史摘要与当前检索可能冲突。规划器不能把“更短”作为唯一选择,而要让新鲜权威来源进入上下文,并标记旧 confirmedFact 为 disputed,交给业务规则或用户确认。冲突本身占用预算,需要预留说明空间;若无法在限制内表达来源与不确定性,应拒绝高影响回答。
模板和模型切换必须重新计算并验证策略
服务端模板入口、模板版本、应用版本和回退值需要可追溯。模板可能同时改变 system instructions、输入 schema、工具声明和正文,这些变化都会改变 token 成本与不可删除字段。远程模板激活前应在目标 App 能力矩阵计算预算;获取失败或版本越界时使用应用内已审核默认模板。
system instruction 能影响模型行为,却不承担权限与截断安全。即使完整保留系统文字,工具网关仍要独立校验授权,响应仍要执行 schema 和敏感字段检查。上下文规划器的完成条件是结构与预算正确,不是模型一定遵循每条规则;输出行为需单独测试。
instrumented test 覆盖端侧 tokenizer、模板包装、进程重建、旋转、后台恢复和离线模型切换。运行时或模型包变化后,用新 modelBundleId 重新编码所有预算样本。单一设备通过不代表全部系统、ABI 与厂商,报告保留未覆盖项,不将模拟 token 计数冒充真实执行。
| 变化 | 必须重算 | 必须重测 | 旧回执能否沿用 |
|---|---|---|---|
| system instruction | policyTokens | 约束保留 | 否 |
| 工具 schema | toolTokens | 授权与装配 | 否 |
| modelBundleId | 全部 token | 端侧执行 | 否 |
| templateVersion | framing 与输入 | 回退矩阵 | 否 |
| 检索策略 | retrievalTokens | 来源与淘汰 | 仅历史参考 |
| App 加固候选 | 运行行为 | 生命周期与缓存 | 否 |
用预算规划器拒绝删除策略和打乱顺序
公开安全的计划文件可以包含 maxInputTokens、reservedOutputTokens、generation、requiredPolicyIds、items 和 removed。每个 item 声明 id、layer、sequence、tokens 与 constraintIds,不含真实对话内容。门禁先检查预算正数、generation 一致和 sequence 单调,再确认所有必需策略与约束仍在保留集合。
下面的 Python 校验器从命令行读取计划。输入缺失时直接非零退出;它拒绝删除 immutable_policy、重复或逆序 sequence、预算总和越界、摘要遗漏必需约束,以及 removed 中仍出现在保留集合的 ID。代码不调用模型、不接触用户文本,只验证截断结果的结构和证据关系。
真实系统还需用实际 tokenizer 产生 tokens,并验证 summary 覆盖区间、citation 权限和工具 schema。结构门禁通过不证明摘要语义完整,也不证明模型会遵守策略。完成证据要结合摘要测试集、端侧运行回执、工具网关拒绝和输出校验。
import json
import sys
from pathlib import Path
IMMUTABLE = {"immutable_policy", "required_request"}
def stop(message):
print(f"context plan rejected: {message}", file=sys.stderr)
raise SystemExit(2)
def load_plan(path_text):
path = Path(path_text)
if not path.is_file():
print("context plan rejected: plan file is missing", file=sys.stderr)
raise SystemExit(2)
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
stop(f"cannot parse plan: {exc}")
def validate(data):
limit = data.get("maxInputTokens")
reserved = data.get("reservedOutputTokens")
items = data.get("items")
removed = set(data.get("removedIds", []))
required_policy = set(data.get("requiredPolicyIds", []))
required_constraints = set(data.get("requiredConstraintIds", []))
if type(limit) is not int or type(reserved) is not int or limit <= reserved:
stop("token limits are invalid")
if not isinstance(items, list) or not items:
stop("items must be a non-empty list")
ids = [item.get("id") for item in items]
if None in ids or len(ids) != len(set(ids)):
stop("item ids must be present and unique")
if removed.intersection(ids):
stop("removed items still appear in the retained plan")
if any(item.get("layer") in IMMUTABLE and item.get("id") in removed for item in items):
stop("an immutable item was removed")
sequences = [item.get("sequence") for item in items if item.get("sequence") is not None]
if any(type(value) is not int for value in sequences) or sequences != sorted(sequences):
stop("message sequence is invalid or reordered")
retained_policy = {item["id"] for item in items if item.get("layer") == "immutable_policy"}
if not required_policy.issubset(retained_policy):
stop("required policy items are missing")
retained_constraints = set().union(*(set(item.get("constraintIds", [])) for item in items))
if not required_constraints.issubset(retained_constraints):
stop("a summary lost required constraints")
total = sum(item.get("tokens", 0) for item in items) + reserved
if total > limit:
stop("retained context exceeds the input budget")
print(f"context plan accepted: {len(items)} items")
if __name__ == "__main__":
if len(sys.argv) != 2:
stop("usage: validate_context_plan.py plan.json")
validate(load_plan(sys.argv[1]))发布门禁要能复现最终模型输入
每次请求的回执记录 modelBundleId、templateVersion、policyVersion、generation、token 分项、摘要版本、保留 ID、移除 ID 和最终输入摘要。发生问题时,可以重建结构相同的模型输入并确认哪一层被压缩;普通日志不保存完整敏感对话,原文按授权在独立数据层管理。
候选 App、模型、模板、tokenizer、工具集合、摘要 schema 或加固配置变化后,要重跑超预算、轮次截断、旧摘要、权限撤销、检索冲突和生命周期用例。旧 token 回执不能替新 modelBundleId 签字。任何 immutable 缺失或约束丢失都阻止发布,不通过扩大输入上限掩盖规划错误。
站内的 RAG 指令边界文章可继续检查低信任片段不能提升权限;准备评估商业 App 的端侧 LLM、上下文管理和加固范围时,可通过页面行动按钮进入御盾中央平台提交模板、模型包和预算策略。提交只启动评估,策略保留结论仍以同一候选回执为准。
- 系统策略和必需请求项标为不可删除层。
- 使用目标 modelBundleId 的实际 tokenizer 计算全部预算。
- 历史按完整 turn 截断并保持 sequence 单调。
- 摘要保留约束 ID、未决事项、来源和覆盖区间。
- 检索片段先做权限校验再按价值淘汰。
- 每次规划保存 generation、保留项、移除项和 token 回执。
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 端侧模型与 LoRA 路径存在具体模型和后端约束,切换模型包需重算上下文。 | Google AI Edge LLM Inference for Android 说明基础模型、LoRA 与后端适用条件。 | 该 API 已是 maintenance-only,其约束不能外推为所有端侧运行时标准。 |
| 系统指令应完整保留,但不能单独承担工具和输出安全。 | Firebase AI Logic system instructions 说明系统指令用于影响模型行为。 | 模型仍可能不遵循,应用要独立控制输入、输出和工具权限。 |
| 模板入口要与模板版本、应用版本和回退值建立追踪关系。 | Firebase server prompt templates get started 说明应用通过模板 ID 获取服务端配置。 | 获取模板不证明来源、兼容和失败回退已经正确治理。 |
| 模板的模型配置、系统指令、输入 schema、工具声明和正文应同版预算。 | Firebase server prompt template syntax 展示这些字段共同构成模板资产。 | 模板语法不保证模型行为,也不替代客户端和服务端授权。 |
| 端侧 tokenizer、生命周期和离线切换应在 Android 设备回归。 | Android instrumented tests 说明设备端测试可访问应用上下文和平台 API。 | 单一设备通过不能覆盖完整 API、ABI、厂商和模型组合。 |
| 上下文策略变更需要保留来源、构建、验证和变更证据。 | NIST SP 800-218 SSDF 提供安全开发与供应链治理实践。 | 组织级框架不定义具体截断算法,也不证明候选通过。 |
| 不可删除层超出预算时应拒绝请求,而不是裁掉系统规则。 | 工程判断:删除授权和输出边界会使后续模型输入失去必要约束。 | 可采用的更大模型档位必须预先审核,不能临时任意切换。 |
| 当前不能声称目标 App 的截断策略已保留全部关键约束。 | 项目证据尚未接入;需要模板、tokenizer、摘要样本与设备回执。 | 文章提供结构和门禁代码,不替代真实会话验证。 |
工程常见问题
从最旧消息开始删除是不是最合理的截断方法?
不一定。最旧轮次可能包含仍有效的业务约束,必须先按层级和完整 turn 判断,并通过摘要保留约束 ID 与来源。
字符数能否近似代替 token 预算?
只能做早期估算。最终门禁要用目标 modelBundleId 的实际 tokenizer,并包含模板包装、工具 schema 和输出预留。
系统提示词完整保留是否就不会越权?
不能保证。系统指令影响模型行为,工具权限、数据范围和输出校验仍要由确定性应用与服务端边界执行。
让模型自由总结旧对话有什么风险?
它可能丢失否定条件、未决事项、来源或约束。摘要需采用结构 schema,并检查 requiredConstraintIds 和覆盖区间。
检索片段写着必须保留时能否提升优先级?
不能。优先级来自受控装配器和来源通道,文档内容始终属于 retrieval_data,只能提供事实引用。
App 加固后为什么要重测上下文截断?
加固可能影响模板资源、tokenizer、缓存、序列化和生命周期。候选身份变化后,旧预算与摘要回执不能证明新包行为。