news 2026/9/28 2:47:48

Operit 对话摘要分节覆盖(Summary Section Overrides):从字符串切片到结构化提示词构建的实战改造

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Operit 对话摘要分节覆盖(Summary Section Overrides):从字符串切片到结构化提示词构建的实战改造
  • AI Agent
  • 人工智能
  • 大模型
  • AI 应用
  • 工具调用
  • 本地部署
  • MCP Clients
  • Agent 记忆

【免费下载链接】Operit

The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent

项目地址:https://gitcode.com/gh_mirrors/op/Operit
点击查看免费下载

本篇技术指南以 Operit 仓库中的 summary-section-overrides 设计方案 为主体,讲解 Operit 如何在 Android 端 AI 对话应用中实现"摘要分节可编辑"能力:让用户在不改动默认摘要提示词的前提下,分别控制【核心任务状态】【互动情节与设定】【对话历程与概要】【关键信息与上下文】四个内置分节的启用、标题与指令。读完本文,你将掌握 Operit 用"结构化分节 + 差异持久化 + 防抖自动保存"替代"字符串搜索替换"的完整改造思路,并能在源码与测试中定位每一处关键实现。

背景:字符串切片方案的边界丢失缺陷

Operit 的对话长记忆依赖"压缩历史"机制:当对话超过上下文阈值(默认summaryTokenThreshold = 0.70f,即上下文占用 70%)或消息数量超过阈值(默认 16 条)时,应用会调用 AI 服务生成一份结构化摘要,用于后续轮次继承上下文。

摘要生成使用一份固定提示词(见 FunctionalPrompts.kt 的SUMMARY_PROMPT与英文版SUMMARY_PROMPT_EN)。PR #1098 首次引入了"可编辑摘要分节"能力,其实现方式是在这份可变提示词字符串中做搜索与替换:找到某个分节标题(如【互动情节与设定】)后替换其文本块。

该方案存在一个关键缺陷(方案文档 index.md 的 Current State 一节明确记录):

当后面的分节在更早分节被替换之前被禁用时,替换会丢失结束标记并移除后续分节。

原因在于:字符串切片依赖"下一个分节标题"作为当前分节的结束边界。如果先禁用位于后方的分节,其标题已被删除,更早分节的替换操作就找不到结束标记,从而把后面所有分节一并吞掉。也就是说,分节之间的"边界"是隐式的、脆弱的,任何组合操作(禁用 + 编辑 + 重排)都可能产生不可预期的结果。

改造目标:把内置分节当作数据而非字符串

设计文档明确了本次改造的 Intent 与 Scope:

  • 保持四个内置摘要分节可配置,同时不改变没有覆盖项用户的默认提示词;
  • 基于结构化分节构建配置后的提示词,使"启用状态、标题、指令"的任意组合变化都具有确定性边界;
  • 向模型配置增加字段级摘要分节覆盖(field-level summary-section overrides);
  • 用可编辑分节渲染现有的上下文摘要设置界面;
  • 手动与自动摘要生成两条路径都传递覆盖项;
  • 用单元测试覆盖"默认行为、组合的禁用+编辑、持久化差异"三类场景。

三个子文档分别对应三条实现主线:01-structured-summary-prompt.md(结构化提示词)、02-config-and-settings.md(配置与设置界面)、03-summary-callers-and-tests.md(调用链与测试),均已标记[DONE],在dev分支合入。

数据结构:覆盖项与默认分节的分层建模

改造的第一步是把"摘要分节"从提示词字符串中抽离为显式数据模型,定义在 ModelConfigData.kt:

// 分节的实际配置(含默认值) data class SummarySectionConfig( val id: String, val enabled: Boolean = true, val title: String = "", val instruction: String = "" ) // 只记录与默认值不同的覆盖项,三个字段均可为空表示"未覆盖" @Serializable data class SummarySectionOverride( val id: String, val enabled: Boolean? = null, val title: String? = null, val instruction: String? = null ) // 汇总后的对话摘要配置,挂在模型配置上 data class ConversationSummaryConfig( val globalRules: String? = null, val sectionOverrides: List<SummarySectionOverride> = emptyList(), val dialogueReviewEnabled: Boolean = true, val dialogueReviewTitle: String = "" )

关键设计点在于SummarySectionOverride的三字段全部为可空:它只表达"相对默认值的差异",这与"只持久化有变化的字段"(差异持久化)的目标一一对应。模型配置ModelConfigData上新增summarySectionOverrides: List<SummarySectionOverride> = emptyList()(ModelConfigData.kt),与原有的summaryCustomRules(全局自定义规则)并存。

四个内置分节的固定标识符定义在 FunctionalPrompts.kt:

private val summarySectionIds = listOf("core_task", "interaction", "progress", "key_info") // 中文标题:核心任务状态 / 互动情节与设定 / 对话历程与概要 / 关键信息与上下文 // 英文标题:Core Task Status / Interaction & Scenario / Conversation Progress & Overview / Key Information & Context

中文分节标题在提示词中以【】包裹,英文以[]包裹(summarySectionHeader函数根据useEnglish选择),例如SUMMARY_SECTION_CORE_TASK_CN = "【核心任务状态】"。useEnglish由设备语言环境决定(!LocaleUtils.usesChineseContent(context))。

结构化提示词模板:前缀 + 分节 + 后缀

要获得"确定性边界",Operit 在 FunctionalPrompts.kt 中新增了提示词模板解析逻辑summaryPromptTemplate():把默认提示词一次性解析为三段结构——前缀(prefix,即开头的角色说明与==========对话摘要==========标记)、有序分节列表(sections,每个分节包含SummarySectionConfig与原始文本块block)、后缀(suffix,结尾的等号分隔线)。

private fun summaryPromptTemplate(useEnglish: Boolean): SummaryPromptTemplate { val prompt = summaryPrompt(useEnglish).trimIndent() val titles = summarySectionTitles(useEnglish) val separator = if (useEnglish) "=======================================" else "============================" val starts = titles.map { title -> prompt.indexOf(summarySectionHeader(title, useEnglish)).also { start -> check(start >= 0) { "Summary prompt is missing section: $title" } } } val sections = starts.mapIndexed { index, start -> val end = starts.getOrNull(index + 1) ?: prompt.indexOf(separator, start) ... } return SummaryPromptTemplate( prefix = prompt.substring(0, starts.first()), sections = sections, suffix = prompt.substring(sections.last().block.let { starts.last() + it.length }) ) }

分节边界的判定从"下一个标题的字符串位置"升级为"模板中预先解析好的分节索引"。legacyPromptSections(useEnglish)从模板提取默认分节配置,作为所有后续合并与差异计算的基准。

提示词装配:覆盖项合并与结构化重排

resolveSummarySections()(FunctionalPrompts.kt)负责把覆盖项合并进默认分节,产出完整的分节配置列表,供设置界面回显:

fun resolveSummarySections(overrides: List<SummarySectionOverride>, useEnglish: Boolean): List<SummarySectionConfig> { val overridesById = overrides.associateBy { it.id.trim() } return legacyPromptSections(useEnglish).map { defaultSection -> val override = overridesById[defaultSection.id] ?: return@map defaultSection defaultSection.copy( enabled = override.enabled ?: defaultSection.enabled, title = override.title?.trim()?.takeIf { it.isNotBlank() } ?: defaultSection.title, instruction = override.instruction?.trim()?.takeIf { it.isNotBlank() } ?: defaultSection.instruction ) } }

applySummarySectionOverrides()(FunctionalPrompts.kt)则是最终的提示词装配逻辑,也是修复"边界丢失"缺陷的核心:

return buildString { append(template.prefix) template.sections.forEach { section -> val override = overridesById[section.config.id] if (override?.enabled != false) { // 显式禁用才跳过,其余保留 val title = override?.title?.trim()?.takeIf { it.isNotBlank() } val instruction = override?.instruction?.trim()?.takeIf { it.isNotBlank() } if (title == null && instruction == null) { append(section.block) // 未编辑:原样拼接默认文本块 } else { append(summarySectionHeader(title ?: section.config.title, useEnglish)) append('\n') append(instruction ?: section.config.instruction) append("\n\n") } } } append(template.suffix) }

注意这里的关键行为:对每个分节独立判断 enabled、title、instruction,逐节拼接。禁用某个分节只是"跳过该节",其前后的分节边界由循环结构天然保证,不再依赖字符串中的结束标记——这正是方案文档所要求的"禁用和编辑任意数量的分节,都不会移除无关的已启用分节"。

顶层入口buildSummarySystemPrompt()(FunctionalPrompts.kt)对外提供完整系统提示词:

fun buildSummarySystemPrompt( previousSummary: String?, useEnglish: Boolean, summaryConfig: ConversationSummaryConfig = ConversationSummaryConfig() ): String { var prompt = summaryPrompt(useEnglish).trimIndent() if (summaryConfig.sectionOverrides.isNotEmpty()) { prompt = applySummarySectionOverrides(summaryConfig.sectionOverrides, useEnglish) } val promptWithPreviousSummary = appendPreviousSummary(prompt, previousSummary, useEnglish) return summaryConfig.globalRules?.trim()?.takeIf { it.isNotBlank() }?.let { rules -> "$promptWithPreviousSummary\n\n$rules" } ?: promptWithPreviousSummary }

这段代码有两个"零覆盖兼容"保证:当sectionOverrides为空时,prompt保持SUMMARY_PROMPT.trimIndent()原文不动(测试buildSummarySystemPrompt_withoutOverridesKeepsLegacyPrompt断言了逐字节相等);当globalRules为空时不会追加多余文本。没有覆盖项的用户看到的提示词与旧版本完全一致。

设置界面:四个分节编辑器与防抖自动保存

分节状态回显

ContextSummarySettingsScreen.kt 是"上下文与摘要设置"屏幕。它通过LaunchedEffect(currentConfig?.id, currentConfig?.summarySectionOverrides, useEnglish)监听覆盖项变化,用resolveSummarySections把覆盖项展开成完整分节列表回显到界面(L214-L219):

LaunchedEffect(currentConfig?.id, currentConfig?.summarySectionOverrides, useEnglish) { summarySectionsInput = FunctionalPrompts.resolveSummarySections( currentConfig?.summarySectionOverrides.orEmpty(), useEnglish ) }

在RenderContextSummaryConfigSections中,summarySectionsInput.forEachIndexed为每个分节渲染一个SummarySectionEditor(L815-L829)。SummarySectionEditor(L875-L922)的交互模型是:

  • 折叠头部显示分节标题 + 一个启用开关(Switch),点击头部可展开;
  • 展开后有两个多行文本输入:分节标题(singleLine = true)与分节指令(可进入全屏编辑器);
  • 分节被禁用时(section.enabled = false),标题与指令输入框同时置灰不可编辑(enabled = enabled && section.enabled)。

差异计算与防抖保存

与全局规则、上下文长度等设置一样,分节编辑复用统一的"自动保存"范式:snapshotFlow + drop(1) + debounce(700) + distinctUntilChanged + collectLatest,即 700ms 防抖后仅在值真正变化时写入。分节对应的ContextSummarySectionsAutoSaveEffect(L604-L642)在保存前调用buildSummarySectionOverrides做差异收敛:

snapshotFlow { summarySectionsInputProvider() } .drop(1).debounce(700).distinctUntilChanged() .collectLatest { sections -> val current = latestConfig ?: return@collectLatest if (current.id != configId) return@collectLatest val overrides = FunctionalPrompts.buildSummarySectionOverrides(sections, useEnglish) if (current.summarySectionOverrides == overrides) return@collectLatest try { modelConfigManager.updateSummarySettings( configId = current.id, enableSummary = current.enableSummary, summaryTokenThreshold = current.summaryTokenThreshold, enableSummaryByMessageCount = current.enableSummaryByMessageCount, summaryMessageCountThreshold = current.summaryMessageCountThreshold, summarySectionOverrides = overrides ) ... } }

buildSummarySectionOverrides()(FunctionalPrompts.kt)逐字段对比默认值,只保留真正变化的字段:enabled变化才记录enabled;标题与默认标题不同(且非空白)才记录title;指令同理。三字段均无差异的分节直接产出null被过滤。这样持久化数据里永远不会出现"默认提示词的重复副本",只有增量覆盖。

最终落盘由 ModelConfigManager.updateSummarySettings 完成,该方法已扩展summaryCustomRules与summarySectionOverrides两个可选参数,未传参时保留当前值(?: current.xxx语义)。

调用链:手动与自动总结共用同一份配置

方案要求"手动与自动摘要生成两条路径都传递所选模型配置的覆盖项"。Operit 的摘要调用链如下(详见 03-summary-callers-and-tests.md):

  1. 配置读取:MessageCoordinationDelegate.readSummaryConfig() 通过FunctionalConfigManager找到 CHAT 功能当前绑定的模型配置 ID,再从ModelConfigManager读出该配置,组装为ConversationSummaryConfig:

    ConversationSummaryConfig( globalRules = config.summaryCustomRules.takeIf { it.isNotBlank() }, sectionOverrides = config.summarySectionOverrides, dialogueReviewEnabled = config.enableSummaryDialogueReview, dialogueReviewTitle = config.summaryDialogueReviewTitle )

    读取失败或未绑定配置时回退到ConversationSummaryConfig()(空覆盖项 = 默认提示词)。

  2. 总结触发:summarizeHistory()(MessageCoordinationDelegate.kt)是自动(token/消息数超阈值、发送消息后异步触发)与手动(用户点击"压缩历史")两条路径的共同入口,统一调用AIMessageManager.summarizeMemory(service, currentMessages, autoContinue, effectiveIsGroupChat, summaryConfig),其中summaryConfig = readSummaryConfig()。

  3. 消息准备:AIMessageManager.summarizeMemory() 定位上一次 summary 消息的位置,只对之后的 user/ai 消息做总结;群聊模式则把多角色消息打包为单条用户消息。随后调用enhancedAiService.generateSummary(conversationToSummarize, previousSummary, summaryConfig)(AIMessageManager.kt)。

  4. 提示词构建:EnhancedAIService.generateSummary转发到 ConversationService.generateSummaryFromPromptTurns(),其中核心一行:

    var systemPrompt = FunctionalPrompts.buildSummarySystemPrompt( previousSummary = previousSummary, useEnglish = useEnglish, summaryConfig = summaryConfig )

    覆盖项在此刻被装配进系统提示词;随后还会追加上一次摘要(appendPreviousSummary)与全局自定义规则。

由于手动与自动路径都经过summarizeHistory→readSummaryConfig→buildSummarySystemPrompt,两者拿到的提示词完全一致,且基于当前聊天绑定的模型配置实时读取,切换模型配置后摘要分节设置也会随之切换。

测试覆盖:默认、组合与差异持久化

单元测试集中在 FunctionalPromptsSummaryTest.kt,四个用例恰好对应方案文档 Scope 中要求的三种行为:

测试用例断言目标
buildSummarySystemPrompt_withoutOverridesKeepsLegacyPrompt无覆盖项时,输出与SUMMARY_PROMPT.trimIndent()完全相等,默认提示词不被改动
buildSummarySystemPrompt_disablingMiddleSectionDoesNotRemoveLaterSections同时"编辑 core_task(改为【工程状态】)+ 禁用 interaction"后,【工程状态】与自定义指令出现、【互动情节与设定】消失、而后面的【对话历程与概要】【关键信息与上下文】完整保留——直接回归了字符串切片缺陷
buildSummarySystemPrompt_unknownOverrideKeepsLegacyTemplate未知分节 ID 的覆盖项被忽略,英文默认提示词原样返回
buildSummarySectionOverrides_onlyPersistsChangedFields只改 core_task 标题时,生成的覆盖项列表仅有SummarySectionOverride(id = "core_task", title = "工程状态")一项,验证差异持久化

其中第二个用例是最具代表性的"组合禁用+编辑"回归测试:它证明无论禁用发生在编辑之前还是之后,结构化装配都能保证每个分节独立、边界确定。

小结

Operit 的摘要分节覆盖改造,本质上是一次"从文本处理到结构化数据"的范式迁移:默认提示词被一次性解析为prefix / sections / suffix模板,覆盖项以"可空字段的差异对象"持久化,提示词装配按分节独立迭代完成,设置界面通过防抖自动保存闭环。最终效果与设计文档的 Expected Result 完全一致——用户可逐节自定义摘要结构,而没有任何覆盖项的用户依旧使用未经改动的默认提示词,手动与自动摘要也始终保持配置一致。相关实现可分别在 FunctionalPrompts.kt、ModelConfigData.kt、ContextSummarySettingsScreen.kt 与 FunctionalPromptsSummaryTest.kt 中对照阅读。

  • AI Agent
  • 人工智能
  • 大模型
  • AI 应用
  • 工具调用
  • 本地部署
  • MCP Clients
  • Agent 记忆

【免费下载链接】Operit

The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent

项目地址:https://gitcode.com/gh_mirrors/op/Operit
点击查看免费下载

相关推荐

上一篇:MiniMax-M2.7-MXFP4模型优化指南:从配置文件到推理引擎的全方位调优
下一篇:SAM2 Hiera小型模型性能优化:如何提升图像分割速度的完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

Arm Development Studio安装激活全攻略:从下载到调试一站式实操指南

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

作者头像 李华
网站建设 2026/9/28 2:43:19

RGB-D目标跟踪实战:数据对齐、梯度回传与深度敏感区域优化

简介&#xff1a;这是一份面向计算机视觉初学者与进阶学习者的多模态目标跟踪实践项目&#xff0c;聚焦RGB与Depth双模态融合技术&#xff0c;适用于课程设计、毕业设计及工程实训等场景。项目基于Python实现&#xff0c;采用边缘引导的单目深度估计网络EG-BTS构建COCO2017 RGB…

作者头像 李华