news 2026/11/3 2:25:49

HarmonyOS 端侧智能体长对话压缩:摘要版本、证据索引与安全回退

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyOS 端侧智能体长对话压缩:摘要版本、证据索引与安全回退

HarmonyOS 端侧智能体长对话压缩:摘要版本、证据索引与安全回退

端侧智能体对话变长以后,最常见的故障不是模型“忘了上一句”,而是应用压缩上下文时把正在等待确认的操作、用户刚补充的约束或关键证据一起删掉。另一类问题出现在摘要已经生成、原始消息却继续变化时:旧摘要仍被当成最新状态,后续回答便建立在过期事实之上。

长对话压缩应是有版本的派生视图,而不是覆盖原始记录的一次性改写。本文用一套应用侧模型说明如何划分可压缩前缀、保留保护消息和最近尾部、为摘要绑定来源索引,并在摘要过期或不完整时回退到原始消息。示例只覆盖可独立运行的 TypeScript 纯逻辑,不依赖 HarmonyOS API,也不代表完成 ArkTS 编译、模型 SDK 联调或真机验收。

1. 先定义压缩边界:摘要不能取代对话账本

假设用户先要求“只读本地资料”,随后发起一个需要确认的写入操作,又补充“只对选中的文件生效”。如果压缩器把前面两轮合成“用户希望处理文件”,操作范围和确认状态都可能丢失。压缩质量不能只看文本变短了多少,还要看约束、待办、拒绝理由和证据引用是否继续可追溯。

因此,原始消息是事实账本,摘要是带来源区间的派生记录。摘要可以用于构造模型上下文,但不能删除原始消息,也不应直接承担权限判断。写入、调用工具或读取敏感数据仍要读取结构化状态和授权记录,而不是从自然语言摘要里反推“用户已经同意”。

边界可以先分成三类:明确标记为不可压缩的保护消息、允许被摘要覆盖的旧前缀,以及无条件保留的最近消息尾部。一个消息可能同时落在保护集合和最近尾部中,但最终上下文只能出现一次,去重键应使用稳定消息 ID。

数据类别原始账本模型上下文压缩处理
活动约束、待确认动作保留原文保留永不纳入摘要覆盖集合
可压缩的旧前缀保留通过摘要引用校验摘要版本和源消息指纹
最近消息尾部保留原文保留不参与本轮压缩
过期或无法验证的摘要保留诊断记录不使用回退可读取原文

2. 每条消息携带稳定身份和结构化保护标记

不要靠文本关键词判断是否保留一条消息。类似“确认”“不要”“必须”等词可能出现在引用资料或工具回包里,不代表它们就是当前控制约束。更稳妥的方式是在消息进入会话账本时,由应用业务逻辑写入角色、来源、顺序号和保护原因;压缩器只消费这些结构化字段。

type Turn = { id: string; sequence: number; role: 'user' | 'assistant' | 'tool'; body: string; protected: boolean; protectionReason?: 'pending-confirmation' | 'active-constraint' | 'audit-evidence'; }; type Conversation = { id: string; revision: number; turns: readonly Turn[]; };

消息 ID 用于引用和去重,sequence用于稳定排序,revision表示账本变化。保护标记必须由应用状态机设置,而不是让模型在总结阶段自己宣告“这条很重要”。若历史数据缺少字段,迁移阶段应保守处理:无法判断角色或来源的记录先保留原文,不要悄悄纳入可压缩集合。

3. 压缩只挑旧前缀,尾部保留规则保持确定

若每次压缩都从全量消息里任意挑选,调用结果会随执行顺序和遍历细节变化,排查也很困难。可以先按sequence校验账本,冻结当前 revision,然后从尾部预留最近 N 条;剩余部分作为候选前缀。候选前缀中的保护消息继续留在原文区,未保护记录才进入摘要输入。

type CompactionPlan = { planId: string; conversationId: string; sourceRevision: number; compactedIds: string[]; preservedIds: string[]; throughSequence: number; }; function planCompaction( conversation: Conversation, latestTail: number, planId: string ): { ok: true; plan: CompactionPlan } | { ok: false; reason: string } { if (!isValidConversation(conversation) || !Number.isInteger(latestTail) || latestTail < 1 || planId.trim() === '') { return { ok: false, reason: 'invalid-input' }; } const ordered = [...conversation.turns].sort((a, b) => a.sequence - b.sequence); const prefix = ordered.slice(0, Math.max(0, ordered.length - latestTail)); const compacted = prefix.filter((turn) => !turn.protected); const preserved = [ ...prefix.filter((turn) => turn.protected), ...ordered.slice(Math.max(0, ordered.length - latestTail)) ]; return { ok: true, plan: { planId, conversationId: conversation.id, sourceRevision: conversation.revision, compactedIds: compacted.map((turn) => turn.id), preservedIds: [...new Set(preserved.map((turn) => turn.id))], throughSequence: compacted.at(-1)?.sequence ?? 0 } }; }

此计划不执行删改,只冻结一组来源 ID。throughSequence表示摘要候选覆盖到的最大顺序号,并不能单独证明前缀连续;真正应用时还要比较精确的 ID 集合和内容指纹。若compactedIds为空,应该跳过摘要请求,避免生成一个没有来源的空摘要。

4. 摘要信封记录它概括了哪些来源

摘要表除了summaryText,至少应保存会话 ID、摘要 ID、策略版本、来源消息 ID 列表、覆盖范围指针和来源指纹。每次摘要生成都拥有单独 ID,不要原地覆盖上一版;旧版本可在保留期内作为派生记录留存,以便发现摘要漂移时回看,不意味着必须永久保存用户内容。

来源指纹用于发现摘要输入变了,不是密码学认证,也不能防止拥有写权限的一方伪造内容。安全责任仍在应用权限与数据完整性机制。若指纹计算成本较高,可在消息入账时同步维护;但任何增量更新实现都需要和完整重算做一致性测试。

type SummaryEnvelope = { id: string; planId: string; conversationId: string; sourceRevision: number; sourceIds: string[]; sourceFingerprint: string; policyVersion: string; text: string; }; function createSummaryEnvelope( plan: CompactionPlan, turns: readonly Turn[], summaryId: string, policyVersion: string, text: string ): SummaryEnvelope | null { const source = turns.filter((turn) => plan.compactedIds.includes(turn.id)); if (source.length !== plan.compactedIds.length || text.trim() === '') return null; return { id: summaryId, planId: plan.planId, conversationId: plan.conversationId, sourceRevision: plan.sourceRevision, sourceIds: [...plan.compactedIds], sourceFingerprint: fingerprintTurns(source), policyVersion, text: text.trim() }; }

生成器返回摘要文本后,应用还应重新读取当前账本并校验来源。不能假设摘要计算期间会话没有被编辑;窗口切换、用户撤回一条消息、工具回包补充事实,都可能让压缩输入发生变化。

5. 并发生成时用计划代次和来源校验,而不是“谁最后返回谁覆盖”

用户连续发送两条消息时,旧摘要任务可能比新任务更晚完成。若直接把“最后完成”的结果写为当前摘要,旧摘要就会覆盖新状态。摘要写入前应验证会话 ID、来源 ID 列表、来源指纹、策略版本和压缩策略仍与当前规则兼容;不满足时把结果标记为stale,保留诊断信息但不进入当前上下文。

账本在摘要完成后只追加新消息时,旧摘要覆盖的前缀仍可能有效;因此不必简单要求整个 conversation revision 完全相等。应验证被摘要覆盖的那组源消息本身未修改,且保护集合和策略版本没有变化。还要比较planId是否仍是当前活动任务,避免两个相同源快照并发计算时,旧任务因为完成更晚而改写摘要指针。若产品实现选择任何 revision 变化都作废,也可以,但必须明确这是保守策略,并评估频繁输入时反复摘要的成本。

function isSummaryCurrent( summary: SummaryEnvelope, conversation: Conversation, activePolicyVersion: string ): boolean { if (summary.conversationId !== conversation.id || summary.policyVersion !== activePolicyVersion || summary.sourceIds.length === 0) return false; const currentSource = conversation.turns .filter((turn) => summary.sourceIds.includes(turn.id)) .sort((a, b) => a.sequence - b.sequence); const currentIds = currentSource.map((turn) => turn.id); return sameOrderedIds(currentIds, summary.sourceIds) && fingerprintTurns(currentSource) === summary.sourceFingerprint; }

这里sameOrderedIds应比较长度和每个位置的 ID,不能只比较集合大小;fingerprintTurns必须使用确定的字段序列化。保护状态也属于源快照的一部分,因而指纹要包含protected、角色、顺序和正文,不能只哈希消息 ID。

6. 组装上下文时恢复原顺序并消除重复

上下文组装不应依赖数据库返回顺序或对象插入顺序。先创建候选项:有效摘要作为覆盖区间的派生节点,未压缩的保护消息与最近尾部作为原文节点。接着按覆盖顺序插入摘要,再把被保留原文按sequence排好;最后基于稳定 ID 去重并校验顺序没有倒退。

type ContextItem = | { kind: 'summary'; id: string; text: string; throughSequence: number } | { kind: 'turn'; id: string; role: Turn['role']; text: string; sequence: number }; function assembleContext( conversation: Conversation, summary: SummaryEnvelope | undefined, preservedIds: readonly string[], policyVersion: string ): ContextItem[] { if (!summary || !isSummaryCurrent(summary, conversation, policyVersion)) { return conversation.turns.map(toContextTurn).sort(compareContext); } const preserved = conversation.turns .filter((turn) => preservedIds.includes(turn.id)) .map(toContextTurn); const items: ContextItem[] = [ { kind: 'summary', id: summary.id, text: summary.text, throughSequence: maxSourceSequence(summary, conversation) }, ...preserved ]; return uniqueById(items).sort(compareContext); }

这段函数里的辅助方法必须遵守相同的身份规则:toContextTurn复制允许进入模型上下文的字段,不携带存储私有字段;uniqueById对重复 ID 报错或隔离,不能静默选第一条;compareContext把摘要放在其覆盖区间之后、保留消息之前。若发现保留 ID 已从账本删除,则整份摘要布局失效并走原始记录回退。

注意摘要节点与源消息使用不同类型。不要把摘要伪装成新的用户消息或系统指令,否则模型容易把派生说明误当成用户原话。上下文协议应能表达“这是应用生成的摘要,来源是这些消息”,并由模型适配层转换为受控数据槽。

7. 账本提交和压缩结果保存应分开事务

压缩服务的计算通常耗时,但账本修改和摘要保存需要明确的一致性边界。创建摘要前冻结来源计划;模型返回后重新校验源消息;校验通过再写入不可变摘要版本。账本并不因压缩成功而删除消息,直到单独的数据保留策略确认可以裁剪或归档。

type SummaryWrite = { accepted: true; summary: SummaryEnvelope } | { accepted: false; reason: 'stale-source' | 'invalid-summary' }; function acceptSummaryResult( candidate: SummaryEnvelope | null, conversation: Conversation, policyVersion: string, activePlanId: string ): SummaryWrite { if (!candidate) return { accepted: false, reason: 'invalid-summary' }; if (candidate.planId !== activePlanId || !isSummaryCurrent(candidate, conversation, policyVersion)) { return { accepted: false, reason: 'stale-source' }; } return { accepted: true, summary: candidate }; }

真正写入数据库时,摘要版本和索引要么一起提交,要么都不提交。若设备空间不足、事务失败或页面退出,应保留之前仍有效的摘要和原始消息,并把新版本标记为失败;不要先删旧摘要,再尝试保存新摘要。应用页面只应拿到“当前摘要版本 ID”和构造上下文所需的投影,底层持久化通过项目自己的 repository/service 边界完成。

8. 失效回退要清楚,不能把坏摘要当最后防线

遇到策略版本过期、源消息缺失、指纹不匹配、摘要为空或上下文组装失败时,首选回退到仍可读取的原始消息,而不是继续使用最后一份摘要。原文量超过模型输入预算时,可暂停当前调用、提示用户缩小范围,或按更保守策略重新压缩;不能静默删除保护消息来凑预算。

如果原始数据已按保留政策清理,则必须明确记录无法恢复的原因和可用历史边界。界面可提示“当前只基于保留的最近消息继续”,避免把不完整上下文包装成完整恢复。与安全相关的待确认动作、用户拒绝和授权撤销事件不应因为摘要过期而重新推断为“已允许”。

现象先检查安全处置
摘要突然不再使用会话 ID、策略版本、源 ID 顺序回退原文并记录失效原因
旧结果覆盖新摘要任务代次与源指纹拒收迟到版本,不覆盖当前摘要
上下文里出现重复消息稳定 ID 与去重结果整体校验失败后重组,不随意丢一条
压缩后确认状态丢失保护标记由谁维护恢复结构化状态,不从摘要猜授权
超预算时保护消息被删除预算策略和保护集合拒绝调用或请求用户缩小范围

9. 用纯逻辑测试锁住顺序、来源和失效条件

不用模型服务也能测试计划和校验逻辑。最小测试集应覆盖乱序输入归一化、重复 ID、非连续 sequence、最近尾部边界、保护消息留存、会话切换、来源内容修改、只追加新消息、策略升级、空摘要和失效回退。调用模型的集成测试另行验证摘要质量,但不能代替应用侧来源检查。

以下示例表达几个最关键的不变量。工程测试可以再加属性式生成:任意合法账本都不能让一条消息同时以原文和摘要来源重复进入上下文;修改任一被摘要字段后,旧摘要都不能通过校验。

const plan = planCompaction(conversation, 3, 'compact-18'); assert.equal(plan.ok, true); assert.ok(plan.plan.preservedIds.includes('pending-confirm')); assert.ok(!plan.plan.compactedIds.includes('pending-confirm')); assert.equal(isSummaryCurrent(oldSummary, conversation, 'policy-v2'), false); assert.equal(acceptSummaryResult(oldSummary, changedConversation, 'policy-v2', 'compact-18').accepted, false);

测试还应明确“追加消息”和“修改旧源”不是同一种变更:若源前缀完全不变,追加新尾部不必废弃已完成摘要;若源中任何正文、角色、保护位或顺序变化,则必须拒收。这样既减少不必要重算,也不会为了省成本而容忍陈旧摘要。

10. 落地检查表:只压缩可证明可恢复的部分

上线前逐项确认:原始账本与派生摘要分开保存;每条消息的 ID 和顺序稳定;保护规则由应用侧结构化维护;压缩前冻结源 ID 清单;摘要有策略版本和源指纹;生成返回时再次读取并验证;组装上下文按覆盖区间稳定排序;缺来源、版本过期或校验失败时回退原文;失败重试不会删除上一份可用摘要;日志不复制完整敏感消息;数据清理期限和恢复边界对用户可解释。

实际验收可以按下面的放行清单逐项打勾:

  • 同一输入账本和策略生成相同的来源指纹。
  • 被保护消息从计划生成到上下文组装都保留一次。
  • 摘要任务结束时会重新验证活动planId和源指纹。
  • 消息追加但源前缀未变时可按策略复用;源消息变化时必须失效。
  • 摘要缺失、重复 ID 或策略版本不匹配会走原文回退。
  • 保存失败时不删除上一份可用摘要或原始账本。

如果其中任何一项无法满足,就先让压缩保持只读试验,不要覆盖会话账本。最稳的实现顺序是:先新增派生摘要表和纯逻辑校验,再接入模型摘要生成,最后由 UI 提供压缩状态和恢复提示。每一步都能独立回退,且不会把生成质量问题变成不可恢复的数据丢失。

本文没有运行 HarmonyOS 工程构建或目标设备验收。接入时需再核对项目所用模型接口、ArkTS 类型约束、存储事务能力和数据保留要求;平台运行行为应以具体版本官方文档及项目验证结果为准。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 2:24:03

串行通信入门之 UART 通信原理与实际使用

前言随着嵌入式系统与物联网技术的快速发展&#xff0c;设备之间的数据交互越来越依赖串行通信。UART 作为最基础、最常用的异步串行通信协议&#xff0c;是每一位嵌入式开发者必须掌握的核心技能。一、UART 是什么&#xff1f;UART&#xff08;Universal Asynchronous Receive…

作者头像 李华
网站建设 2026/11/3 2:23:50

蛋糕销售系统|基于springboot + vue蛋糕销售系统(源码+数据库+文档)

蛋糕销售系统 目录 基于springboot vue蛋糕销售系统 一、前言 二、系统功能演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 基于springboot vue蛋糕销售系统 一、前言 博主介绍&#xff1a;✌…

作者头像 李华
网站建设 2026/11/3 2:23:06

Mysql学习(1):揭秘数据库:十大核心优势与架构全解析

&#x1f525;keyipatience:个人主页 &#x1f3ac;作者简介&#xff1a;C/C后端开发学习者 &#x1f31f;专栏传送门&#xff1a;《c》《linux》《c高阶数据结构》《c数据结构与算法》《前端学习》 《Git》《Mysql》 ⭐️patience is key in life 什么是数据库 数据库是20世纪…

作者头像 李华
网站建设 2026/11/3 2:22:41

01 检索下载文献并生成 MD

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华