Files
story/docs/elder-story-companion-change-plan.md
T

221 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 老人自然陪聊与人生故事:代码更改计划
日期:2026-09-06。规划基线:`029a609`;已保留的第一版里程碑:`第一版`(`0c2151a`)。P0 与 P1 已完成代码实现并发布服务端和版本化人设;真机固件是否已烧录及听测结果以同日基线记录为准。P2 至 P5 仍是后续计划。
## 1. 推荐方案与范围
保留 EchoEar → 火山硬件智能体的实时音频连接,继续使用 S2S 处理自然语音。平台在会话开始前提供简短、可追溯的陪聊背景;会话内首先由 S2S 顺着老人当下的话接续;后台从最终字幕整理故事线索,供下次聊天使用。Viking 继续承担历史记忆检索,平台数据库负责原话、故事版本和续聊状态。
第一阶段不要求每一句回复都等待平台另一个大模型做决定。“陪聊导演”先实现为会话前的背景选择器与会话后的话题整理器。只有实测需要更细控制、且火山当前传输模式支持安全更新时,再增加会话中引导。
交付目标:能自然接住老人当下的话,在对方愿意时聊深一件亲身经历;隔天或重启后能接续,已回答的问题和明确不愿聊的话题不反复开启。故事完整度是结果,不是要求老人完成的任务。
## 2. 已有代码与需要补齐的部分
以下按源码核对,旧设计文档与实际实现不一致时,以源码和运行验证为准。
| 已有模块 | 当前实现 | 本次扩展 |
| --- | --- | --- |
| `internal/orchestration/hardware_agent.go` | 创建会话、确定绑定长者、下发原生 Viking 过滤配置 | 加入陪聊背景选择,保持人物与授权复核 |
| `internal/orchestration/repository.go` | 读取已发布 PERSONA、档案、最近三次摘要和人工确认记忆,冻结会话配置 | 纳入开放话题、减少机械截断、保存本次选题依据 |
| `internal/extraction/generator.go` | 判断内容是否有价值、是否值得成故事,生成摘要、`open_topic` 和故事正文 | 提取具体事件、待续问题及其原话依据;允许没有后续问题 |
| `internal/extraction/processor.go` | 消费结束会话的最终字幕,保存故事候选、版本和来源 | 更新可跨会话的故事线索,精确关联字段与原话 |
| `internal/vikingmemory/` | AddSession 异步写入、事件/画像检索、按租户和长者生成稳定 user_id | 与故事线索保持一致;补齐自动记忆修正与删除的对账方案 |
| `internal/saas/content_service.go` | 故事查看、修订、确认、拒绝及记忆管理 | 复用这些流程,增加故事线索查询及暂停/关闭 |
| `web/src/pages/SaaSWorkspace.tsx` | 聊天记录、故事与记忆界面 | 展示未完故事、出处、继续/暂停状态 |
| `internal/opsconfig/`、`web/src/pages/OpsConfigWorkspace.tsx` | 人设与配置发布、评测、设备分组和回滚 | 发布陪聊人设及版本化策略,支持单设备试运行 |
| 设备与 Windows 模拟器 | 把会话背景和人设映射到 S2S 与 LLM,上传最终字幕 | 两端统一表达规则和新配置映射 |
已经确认的不足:
- `selectRecentConversationSummaries()` 只读 `summary_type='conversation_summary'`,未读已有的 `open_topic`。
- `buildContextSummary()` 将每条已确认记忆压缩到约 30 字,摘要压到约 140 字;关键人物和事件结果可能被截掉。
- `open_topic` 目前是一段模型文本,没有已回答、已拒绝、冷却、已完成等状态,无法可靠避免重复。
- 现有 `story_draft` 以通话和生成版本组织,缺少把多次通话串成同一故事的上层实体。
- 抽取器实际读取双方带角色的字幕;落库来源当前关联长者话轮,但不足以证明每条生成事实确实来自对应原话,不能仅凭角色标记防止模型补写。
- 固件与模拟器的提示词拼装不完全一致:固件有防止朗读舞台描述的规则,模拟器还将对话 ID 放入提示词正文。
- Viking 自动提取结果与平台人工确认记忆是两条路径;当前本地记忆删除不会自动删除 Viking 独立抽取的内容,不能把本地记忆页描述为完整 Viking 管理页。
## 3. 阶段 P0:记录基线、验证实际链路
在改体验前,保存当前 PERSONA/AGENT_BUNDLE、火山 Bot 配置和固件版本;秘密字段留在既有服务器秘密文件中,配置报告只展示非秘密字段。保留“第一版”标签,另建本次开发前的可恢复检查点。
新增 `docs/elder-story-companion-baseline.md`,记录以下实测:
1. 核验实际使用的 Bot、S2S 模式、声音、判停参数和动态人设。现有客户端允许初始更新发出后的 `session.created` 作为握手成功条件,不能仅由此证明 S2S、人设和记忆字段全部生效。
2. 做一次干净会话的 Viking 召回测试:所问事实只存在于 Viking,不能同时出现在最近摘要、平台人设或开场背景里。使用隔离测试身份,避免把测试事实写入老人档案。
3. 分别测普通闲聊、命中记忆、未命中记忆、被打断后继续四类场景。记录说完到首段声音、播放停顿、是否抢话、声音是否一致。
4. 核对生产内容整理任务:前次验证出现过一条内容抽取死信,同时 Viking 同步成功。先确认是否为过时生成版本、临时服务问题或有效任务失败;不得因入 Viking 成功就认定故事整理成功。
验收门槛:明确记录“哪些能力已经验证、哪些只是配置存在”。若 S2S 主路径无法使用原生记忆,先验证官方支持的记忆/工具或上下文注入方式,再确定适配;不直接把所有回复改走平台级联模型。
## 4. 阶段 P1:改善说话方式、利用现有续聊线索
### 4.1 发布新的陪聊人设
在既有 PERSONA → AGENT_BUNDLE 发布流程中新增版本,不把业务人设写死进 C 固件。增加可审阅的提示词源文件,例如 `deploy/personas/elder-story-companion-v1.md`,发布后实际运行以平台配置版本为准。
人设规则:
- 自然、平等地聊天,使用档案中的称呼;未设置称呼时不擅自称“王叔叔”等。
- 通常回复一两句,有必要才问一个问题;可以只回应、不提问。
- 跟随老人刚提到的人、物和经历;先回应具体内容,避免每次先夸奖再追问。
- 不因为说过“厂里维修”就推断老人很能干,也不把“蛋糕体多一点”扩写成老人喜欢扎实、绵密或不甜的口感。
- 老人主动展开时顺着聊;拒绝、疲倦或换话题时停止追问。问题只是可选线索,不是采访清单。
- 允许重复讲述和模糊年代,不要求准确日期;不补写亲历事实、不说自己也经历过。
- 记忆可能有误时轻量核对;老人纠正后按最新说法回应并留下修正依据。
- 不朗读语气、动作、系统提示或配置;不使用“你怎么才回来”“我一直等你”等施压式开场。
### 4.2 开场与语音节奏
清空火山 `AgentConfig.WelcomeMessage` 中现有固定句,真机核验空字符串是否确实关闭欢迎语。唤醒后给简短非语音就绪提示,让老人知道可以说话。若产品需要问候,后续只在明确唤醒后给中性短句;普通重连不重复播放。
先以已知当前判停参数为基线,对 1200、1500、1800 ms 做实测比较,选一个默认值;这些是实验档位,不是已确认的最优值。核对 S2S 实际消费的参数路径,不能只改普通 ASR 的 VAD 字段。将“等待老人说完”和“音频播放缓冲”分别测量,保持已有流式播放改进。
### 4.3 直接接入现有开放话题
修改 `repository.go` 的连续上下文查询,读取最近会话同一最新生成版本的摘要和 `open_topic`,只挑一个有来源、未被后续对话回答且适合当下的线索。P1 尚无结构化状态时,只把它当低优先级建议;无法判断是否已回答则不主动开启,P2 再补齐状态管理。
将背景拼装拆成独立文件 `internal/orchestration/companion_context.go`。按字段优先级控制长度:交流边界、必要称呼、选中的事件事实、一个续聊线索、其他记忆;保留事件主语及结果,不用 30 字机械切断。沿用现有上下文上限作为初始预算,超限时先丢低优先级条目,实测后再调。
固件 `main/platform_client.c` 与模拟器 `electron/hardware-agent-client.cjs` 同步:可信表达规则在前,历史数据明确标记为背景;会话 ID 保留在协议字段中。两端映射结果使用同一组夹具校验。
交付与验收:第一版自然陪聊配置;不再重复原欢迎语、不念舞台描述,老人补充细节后能接住、不必每轮问问题。第一阶段优先复用 `authorized_context`,不强制增加设备接口或大幅修改音频链路。
## 5. 阶段 P2:持久化“未完故事”和陪聊背景
### 5.1 新增轻量陪聊模块
新增 `internal/companion/`:
| 拟新增文件 | 职责 |
| --- | --- |
| `types.go` | `CompanionBrief`、话题状态、引用和策略版本 |
| `service.go` | 根据当前绑定与授权读取候选,组装本次背景 |
| `selector.go` | 排除不愿聊/已回答话题,优先选择相关未完故事,控制重复 |
| `repository.go` | 固定租户和长者范围的读写,版本检查 |
| `render.go` | 将结构化背景渲染为短上下文,按字段预算取舍 |
`CompanionBrief` 建议包含:称呼、当前交流偏好、一个可选故事线索、最多三条有来源的事实、已回答或不宜再问的事项、来源版本、策略版本与生成时间。事实与“可选追问”必须分开,候选问题不能作为已发生事实保存。
会话前只做数据库选择和渲染;需要模型理解的工作在后台提前完成。缓存未命中时退化为现有档案和就绪摘要。新背景冻结进 `context_snapshot`,引用/版本纳入 `content_digest` 和硬件 `config_digest`;旧设备仍可读取文本形式的 `authorized_context`。
### 5.2 数据库增量
从下一空闲迁移编号开始;当前最新为 000052,可暂拟 `000053_companion_story_threads`,实施时重新核对。
| 拟新增表/字段 | 内容 |
| --- | --- |
| `story_thread` | 租户、长者、标题、主题类别、`OPEN/PAUSED/CLOSED`、版本、最后聊到时间、下次可主动提及时间 |
| `story_thread_source` | 将同一故事关联多次会话、原话和既有故事版本,引用均检查相同租户/长者 |
| `story_thread_question` | 问题、提出依据、`PROPOSED/ASKED/ANSWERED/DECLINED`、提问及回答的源话轮 |
| `context_snapshot` 扩展 | 本次背景 JSON、策略版本、实际选择的线索及引用,供回放和排查 |
`PAUSED` 表示不主动开启;老人主动提起时按当下意愿回应。`DECLINED` 的问题不能换个说法反复追问。`CLOSED` 表示当前故事暂告一段落,不等于删除,也不表示以后禁止重讲。
数据库以租户+长者为记忆范围,不以设备作为唯一记忆身份。保留现有 `u_=SHA256(tenant:subject)` 的稳定映射,不因为人设升级更换 assistant_id。多台设备绑定同一长者共享该长者的记忆;绑定不同长者严格隔离。
接入位置:`internal/app/app.go` 装配模块;`hardware_agent.go` 准备背景;`repository.go` 在短事务中重新核对绑定、授权、版本并冻结。模型请求和 Viking 请求不能置于数据库长事务内。
验收:相同用户重复建立会话不反复问已回答的问题;“这个不想聊”会暂停主动提及;换绑另一个长者不读取原长者线索;后台不可用时仍能基本聊天。
## 6. 阶段 P3:把聊天整理成可回访、可修订的故事
扩展现有 `GenerationResult`,增加可选的事件要素、人物、时间原文、地点、已回答问题和后续问题。全部为版本化结构化结果,兼容旧数据,不要求一次聊天把所有字段填满。
必须让每个事实字段包含真实的 `source_turn_ids`,必要时包含文本偏移。双方字幕可帮助解释“蛋糕多一点的”这样的回答,但助手自己的叙述不能成为老人经历的事实来源。年份不确定则保留“刚参加工作那会儿”,不推算为确定年份;模型推测的情绪不得保存为老人自述。
代码任务:
1. `internal/extraction/generator.go` 升级输出结构和提示词;支持有效摘要但没有故事、也没有开放话题的结果。
2. `internal/extraction/processor.go` 校验来源引用、生成版本和当前授权;保存既有摘要/故事修订,并发布 `conversation.companion_update_requested`。
3. `internal/companion/processor.go` 后台归并相同故事、更新问题状态。按会话+生成版本去重,避免迟到字幕和重试造成重复故事;不覆盖人工修订。
4. 需要时用下一迁移扩展 `story_revision` 的结构化要素和来源;继续复用 `story_draft/story_revision/story_source`,不另建一套重复的故事正文系统。
断电路径保留“启动时补报旧会话”和服务端超时恢复。最终字幕先可靠保存;故事和 Viking 抽取异步完成。入库尚未就绪时,下一会话使用已保存且授权允许的近期上下文,不能承诺断电后一秒内所有新记忆都已可检索。
验收:蓝莓蛋糕偏好只生成偏好/摘要,不硬凑一篇人生故事;同一工厂经历跨三次聊天归入同一线索;“我刚说错了”留下修订关系,历史原话可追溯;重放及迟到事件不重复创建内容。
## 7. 阶段 P4:Viking、平台界面与修正保持一致
Viking 的事件/画像继续用于检索,故事的状态、版本与原话出处以平台数据库为准。初期使用既有 `event_v1/profile_v1`;平台自己保存续聊状态,不把所有内部问题列表都写进可召回记忆,避免模型把“计划问什么”误认为老人说过什么。
新增规则或同步故事摘要前,先测试抽取覆盖、重复与删除语义。现有普通用户原话 → Viking AddSession 路径和人工确认记忆路径要明确标识;同一段故事避免同时多次写入。
当前 Viking 与本地记忆删除不同步,需增加 `provider_ref`/来源会话映射以及修订、停用、删除任务。调查 Viking 的单条修订/删除接口,核验是否返回稳定引用;如果只能按会话重建,必须先设计排除与重建策略。无法可靠同步时,界面明确显示尚未完成,不宣称外部记忆已删除;老人纠正的内容也不能继续被旧自动记忆覆盖。
复用 `internal/saas/content_service.go` 和既有故事审核权限。建议新增:
- `GET /api/v1/subjects/{id}/story-threads`:未完/暂停/已结束线索。
- `GET /api/v1/story-threads/{id}`:事件要素、已聊内容、待续问题、原话来源。
- `PATCH /api/v1/story-threads/{id}`:修正标题、暂停或关闭;沿用 ETag、原因与现有权限模型。
以上为拟新增接口,统一在 `internal/httpapi/` 与 `openapi/openapi.yaml` 定义,生成 `web/src/api/schema.d.ts`,不手改生成文件。
前端在 `SaaSWorkspace.tsx` 现有故事区域增加“可以接着聊”“暂时不主动聊”“整理好的故事”;将新增部分拆到独立组件,避免继续扩大单文件。展示每张卡片的来源和是否经确认,家属查看及分享沿用已有授权规则。Viking 同步状态属于管理侧诊断信息,不进入老人语音表达。
## 8. 阶段 P5:需要时再做会话内策略更新
P1–P4 稳定后再决定是否需要。只在老人换话题、明确拒绝或讲完一个片段时触发,后台分析最终字幕,生成有有效期的简短提示;不以固定每句调用一个模型为默认方案。
前置验证:当前 low_load + S2S 是否支持会话中更新、哪些字段可变、会不会重置对话或影响音频。需要修改时关注:
- `firmware/echoear-platform-agent/components/volc_conv_ai/src/transports/low_load/src/volc_ws.c`
- `firmware/echoear-platform-agent/components/application/service/conv_service/src/volc_conv.c`
- `internal/orchestration/transcript_batch.go`、`native_subtitle.go` 和硬件会话事件接口。
策略消息带 conversation_id、revision、based_on_turn_sequence、过期时间和确认结果;在音频回调外处理。过时消息不能覆盖新话题。若当前供应商协议不支持合适的更新,保留会话前背景+会话后整理,记录限制;不得用重新建连模拟逐轮控制。
## 9. 测试、发布与回滚
### 自动验证
- 运行受影响 Go 模块的单元测试;涉及来源、幂等、状态及授权的场景必须接真实隔离 PostgreSQL/Redis,记录跳过的测试,不能把跳过算通过。
- 固件和模拟器用同一配置夹具校验 S2S/LLM 规则一致、秘密不入响应、长背景可控、旧设备兼容;新增字段才需要重新编译及烧录。
- 数据库迁移在独立测试库验证升降级;生产采用可兼容旧代码的增量迁移。
- 扩展 `testdata/agent-eval-suite.v1.json` 或另建 v2,复用 `internal/agenteval/`。语音自然度必须真机听测,不能只检查提示词或生成的文本。
### 真机验收用例
| 场景 | 合格行为 |
| --- | --- |
| “今天没什么事,就坐了一会儿” | 能平常回应,不强行提取故事、连续追问 |
| “我以前在厂里修过机器” | 跟住具体话题,每次最多一个自然追问,允许没有追问 |
| 说到一半停顿、接着说 | 在选定判停档位下不过早抢话;误判可被打断 |
| “这个不想聊,换一个” | 当轮转向,下次也不主动重开该线索 |
| 上次讲修车床,下次又见面 | 能接续有来源的具体细节,不假装知道未讲完的内容 |
| “不是师傅,是我同事” | 后续使用修正说法,不把旧记忆重新当事实 |
| 一次闲聊中断电并恢复 | 已收到的字幕保留,后台收尾,记忆就绪后可再次检索 |
| 更换绑定长者 | 不串人物、偏好或故事 |
| 抽取或 Viking 暂时不可用 | 实时聊天继续,后台可重试,缺少记忆时不编造 |
先录制相同脚本的基线与新版对照。普通接话、故事接续、记忆检索分别统计首段声音 P50/P95;另记播放空洞、抢话、重复问题、无来源事实和用户主动讲述的片段数。建议以 20 组脚本作首轮检查:跨人记忆、忽视明确拒绝、测试中的虚构个人事实须为零;语音延迟与连续性不得明显差于基线。主观自然度由实际听测反馈决定,不以“说满三分钟”强迫老人配合。
### 发布顺序
1. P0 基线验证。
2. P1 人设、开场、现有 open_topic 接入,先针对当前 EchoEar;称为“自然陪聊试用版”。
3. P2 数据结构与背景选择器 → P3 后台故事线索更新;称为“故事续聊版”。
4. P4 平台管理、Viking 修正对账;再交付“故事整理版”。
5. P5 按测得的问题决定,不作为首版前置条件。
利用现有配置版本和设备组发布能力。每批保存 API/worker 镜像、固件二进制、Bot 非秘密配置报告及受保护的完整备份。新逻辑通过版本化策略启停,读写保持向后兼容;回滚应用不回滚生产内容数据。第一版里程碑保留不动。
## 10. 首批建议实际改动清单
本计划执行时,第一批只落地 P0+P1,下列任务可形成一个可审阅提交组:
1. 新增基线记录与陪聊人设源文件,通过既有人设版本发布。
2. 修改 `repository.go` 并提取 `companion_context.go`,接入现有 `open_topic`、改进背景选择与截断。
3. 同步固件和模拟器的提示词构造;核验欢迎语关闭和判停设置。
4. 添加有意义的回归用例:开放话题来自同一长者和同一最新摘要版本、来源为助手的事实不能当用户经历、超长背景保留关键字段、撤回授权后不注入。
5. 受影响测试通过后,部署当前设备试用,用同一组语音脚本比较体验,再推进结构化故事线索。
## 官方依据与待验证项
- [AibotQuery:S2SConfig 与 MemoryConfig](https://www.volcengine.com/docs/6348/1807452?lang=zh):确认可配置字段与模式定义。配置存在不等于当前真实链路全部生效,P0 以实测补齐。
- [RTC 接入 Viking 长期记忆](https://docs.volcengine.com/docs/84313/1928352?lang=en):检索结果用于增强模型上下文;故事续聊状态仍需要平台维护。
- [Viking AddSession](https://www.volcengine.com/docs/84313/1783353?lang=zh):提交会话消息后按抽取配置形成事件/画像;提交成功与可检索就绪分开验收。
本文中的 P2 至 P5 新表、接口和发布阶段仍是建议设计,尚未实施。P0/P1 的实际结果见 `docs/elder-story-companion-baseline.md`。