公司动态
语音代理的“内心独白”:用状态管理根治多轮对话失忆
你正在和一个 voice agent 处理一个稍微复杂一点的请求比如“帮我订明天下午三点从上海到北京的高铁尽量靠窗如果没票就改下午四点”。前半段它听得很好也报出了车次。等你说完支付方式它突然问“请问您刚才是要订去北京还是上海”这一刻你已经不太想再重复了。这类问题早期语音助手里几乎天天发生今天也仍然没有彻底消失。很多人会把它归结为上下文窗口不够长或者语音识别不够准。但在我接触过的不少语音代理项目里真正的问题并不是模型记不住而是系统没有为“记住”这件事设计一条独立通道。最近看到一个思路很直接的实现给 voice agent 加一层 inner monologue中文可以理解成“内心独白”。它不是把每一句话都原样存下来而是让代理在内部不断压缩、更新自己对当前任务的理解用这个私有状态来阻止自己忘记关键信息。1. 语音代理为什么总在“关键时刻失忆”1.1 表面现象上下文越长输出越飘如果你用一个很朴素的方式搭建语音代理最常见的做法就是把用户语音转写出来的文本直接追加到对话历史里然后整体发给大模型。开场那几轮效果通常不错因为历史很短干扰项少。可一旦进入了第 5 轮、第 6 轮问题就开始冒出来。典型表现是用户在前面已经明确说过“不要香菜”到后面问“还有什么忌口”代理不但答不上来甚至可能重新问一遍“请问您有什么忌口吗”。用户会觉得这东西像金鱼一样只有七秒钟记忆。我用过一个更让人崩溃的版本。用户说“不要香菜”下一轮问“要不要加辣”代理能答对。可再多聊几轮用户问“那除了不要香菜还有什么忌口”它就开始含糊其辞。原因很简单“不要香菜”在原始对话文本里只是几十个字之一模型在生成回复时并没有把它当成一个需要持续维护的关键约束。这种失忆不是一次性的而是随着对话长度递增的。1.2 深层原因语音交互不是“多打几轮字”文本聊天里用户翻一翻上面的记录就能知道自己说过什么。语音对话里没有这个“外置记忆”用户只能靠自己复述。所以语音代理对内部记忆的依赖天然比文本聊天更强。但语音输入本身又比文本输入更容易打断语义用户经常会修正自己比如“嗯不是我说的是靠窗如果有的话”。如果系统只按时间顺序堆文本这段语义就是碎的。ASR 会带来转写错误尤其是人名、地名、数字、忌口等关键信息。用户没有耐心复述一次失败体验比文本聊天更糟。语音交互的实时性要求很高模型不能无限制地重读超长历史。所以memory 问题在 voice agent 里不是可选项而是体验底线。一个代理如果记不住前面说过的东西后面所有功能都等于白做。2. “内心独白”不是玄学而是代理的私有工作记忆2.1 它和普通对话提示词有什么区别普通对话提示词本质上是一个“一次性打包发送”的请求系统指令、历史对话、用户当前输入全部塞进模型输入里。模型要在这一次生成里自己完成“理解当前状态”和“生成合适回复”两件事。inner monologue 的不同之处在于它把“理解状态”单独拆了出来。代理在面向用户生成回复之前先在一个私有的推理通道里把自己的理解写下来用户目标是什么、已经确认过哪些信息、还有哪些事情没做、有没有不确定的地方。然后再拿着这份私有状态去生成最终回复。你可以把它理解成员工接待客户的方式。普通做法是员工靠脑子记一边听客户说一边直接回答。inner monologue 是让员工每次接待客户前先在工单系统里更新“客户要求、已处理步骤、未解决事项”然后再去回答客户。关键区别在于普通对话记录是给模型“读”的原材料。inner monologue 是系统根据原材料提炼出来的“工作笔记”。它不给用户看甚至不需要出现在公开回复里。2.2 它如何阻止遗忘发生遗忘不是一瞬间发生的而是上下文不断累积早期信息被逐渐稀释。模型没有真正“忘记”某个词而是在生成时注意力被大量新内容挤占。inner monologue 相当于持续把散落在历史里的关键信息压缩成几个稳定字段用户目标、关键偏好、已完成步骤、待确认项。每一次代理准备回复前都先刷新一次这些字段。这样即使后面原始历史被截断模型仍然能通过系统提示里的状态知道该继续做什么。举例来说用户第一轮说“不要香菜”第三轮说“靠窗位置”第五轮可能因为寒暄已经离题很远。如果没有状态压缩模型在第五轮看到的是大量寒暄和琐碎信息。如果有 inner monologue系统提示里会一直保留一行用户偏好不要香菜座位偏好靠窗。模型看到这一行就不会再去问已经确认过的事情。这种机制不是让模型永远记住所有事而是保证它优先记住最重要的事。3. 给语音代理加上 inner monologue 的落地路径3.1 先拆解数据流语音代理的基础链路通常是ASR 语音识别 - 对话管理 - 大模型生成 - TTS 语音合成。要加入 inner monologue通常在“对话管理”和“大模型生成”之间插入一个“状态提炼器”。整体数据流大致如下用户语音 - ASR 转写文本 - 对话历史 当前输入 - 状态提炼器inner monologue - 读取旧状态 - 更新目标 / 进度 / 偏好 / 待确认 - 写入新状态 - 构造 prompt系统指令 当前状态 对话历史 用户输入 - 大模型生成公开回复 - TTS 播放这个流程里用户只感知到最终语音回复看不见中间的状态更新。但状态更新决定了代理接下来会不会说错话。3.2 三个核心模块怎么配合在工程实现上至少需要三个模块第一个是会话状态存储。按 session_id 保存维护一个可读写的状态对象。常用字段包括用户目标已确认信息已完成步骤当前待办不确定/待确认项第二个是状态更新器。它负责把旧状态和新输入合并成新状态。实现方式可以是用大模型做摘要也可以用规则抽取或者两者结合。如果系统本身已经调用了大模型强烈建议把状态更新做成异步或并行不要每次都让用户多等一次完整生成。第三个是 Prompt 注入器。它把最新状态以“内部状态”形式放进系统 prompt让模型在生成公开回复之前能读取到自己的工作笔记。这三个模块配合起来才能形成完整的独白循环。3.3 一个最小实现思路不急着上复杂架构。可以先写一个最简陋但能跑的版本验证机制是否有效。下面是一个偏伪代码的结构真实项目里可以替换成 Redis、数据库或其他存储。class VoiceAgentScratchpad: def __init__(self, session_id: str): self.session_id session_id self.state { goal: None, preferences: [], completed_steps: [], pending_items: [], uncertainties: [], } def load(self): # 从 Redis 或数据库中读取之前保存的状态 pass def save(self): # 写回持久化存储保证会话中断后能恢复 pass def update(self, transcript: str): # 把 transcript 和旧状态合并成新状态 # 可以用一个较轻量的 LLM 调用也可以用规则 pass核心流程是这样每次收到用户输入后先读旧状态再更新状态然后把新状态注入生成回复的 prompt。一个可参考的 prompt 结构[系统] 你是一个语音助手。 在每次回复前先更新内部状态 - 用户目标 - 已确认信息 - 已完成步骤 - 待办事项 - 不确定内容 然后基于内部状态生成公开回复。 公开回复中不要暴露内部状态。实际测试时先准备一个 5 轮以上的任务场景验证代理在加了独白后是否不再重复问用户已经给过的信息。4. 真正会决定成败的是触发时机和上下文边界4.1 什么时候触发独白比独白本身更重要一个很容易踩的坑是把状态更新做成“每轮必做”。这样确实不会遗忘但会让响应变慢成本也会成倍上升。更合理的做法是设计触发策略在关键节点触发用户完成一个子任务、切换话题、确认或否定某个信息时。在系统不确定时触发连续多轮没有更新状态或者模型置信度低。在后台异步触发用户等待 TTS 播放时系统趁机更新状态。从实际调试经验看最容易失败的是“只在开头和结尾更新状态”。因为中间一旦出现打断、反问、纠偏状态就会和实际情况脱节。独白得跟着对话走不能只在固定的两个点更新。4.2 上下文不是越长越好很多人以为 inner monologue 就是把更多历史信息记下来于是把状态字段越写越长最后几乎等于把原文重新抄了一遍。这是明显误区。inner monologue 的价值在于压缩而不是囤积。状态字段里只应该保留对后续行动真正有影响的信息。比如“用户今天心情不错”这种信息除非你的任务是情感陪伴否则不需要占用状态空间。如果状态写得太多它自己也会变成新的长上下文负担最终又回到“注意力被稀释”的老问题。建议是原始对话走短期记忆保留近几轮。状态字段走长期记忆只保留跨轮次需要的事实。状态长度要设上限超出后触发重新提炼而不是追加。4.3 断点续传与恢复语音对话经常会被打断或者用户中途离开、断线重连。如果状态只存在内存里一旦服务重启代理就会失忆所有努力白费。所以 scratchpad 必须持久化。会话恢复时先加载状态再让代理说一句“我们继续刚才的流程”而不是重新问一遍所有信息。建议给状态加上时间戳和版本号。这样排查问题时能看出状态是不是被旧数据覆盖也能知道哪次更新引入了错误信息。注意如果会话跨设备或跨平台一定要考虑状态同步问题。同一个用户今天在手机端、明天在网页端如果两边状态不一致用户会明显感觉到代理“换了一个脑子”。5. 这套方案适合哪些场景不建议哪些场景5.1 适合多轮任务型、客服、外呼、Agent 工作流inner monologue 最适合的场景是那些目标明确、轮次多、信息字段固定的语音对话。典型的包括订票、订餐、预约。售后客服工单。电话外呼回访。企业内部语音助手。任何需要分步收集信息的 Agent 工作流。在这些场景里inner monologue 的价值不只是“不遗忘”而是让状态变得可见。客服主管可以通过日志查看代理内部状态知道它为什么那样回复开发者在调试时也能快速定位是哪个状态字段被写错了。从另一个角度看它很像大模型时代的 DST对话状态追踪。传统 DST 依赖预定义槽位比如“日期”“地点”“人数”。inner monologue 用自然语言状态存储目标、偏好、进度能处理更开放、更复杂的任务。5.2 不建议闲聊型、极低延迟、强隐私场景闲聊型语音对话没有明确目标状态压缩收益很低反而可能让对话变得机械、缓慢。用户只想随便聊聊天系统却在后台写工作笔记这没有必要。极低延迟场景也要慎重。如果每次回复前都增加一次独立的 LLM 调用来更新状态用户会明显感到卡顿。这类场景更适合用规则或小型模型做状态更新把额外耗时压到几十毫秒级别。强隐私场景更要注意。inner monologue 里可能包含用户身份证号、地址、过敏史等敏感信息。如果平台不能满足安全存储和合规要求就不要把这些内容写进独白。即便要写也要先做脱敏和访问控制。6. 从“给代理加记忆”到“让代理掌握自己的状态”6.1 短期记忆和长期记忆的分工加 inner monologue 之后需要重新理解记忆的分工。短期记忆是原始对话历史负责让模型理解当下这句话。它的特点是量大、实时、但容易过期。长期记忆是状态字段负责让代理知道整个任务进行到哪里。它的特点是精炼、稳定、跨轮次有效。inner monologue 就是连接两者的转换器从短期记忆中提炼长期状态再用长期状态指导短期输出。不要把所有希望寄托在“模型自己会记住”上。工程上必须要有状态持久化、版本管理、日志审计。这些东西决定了一个演示项目能不能真正进生产环境。6.2 一个可复用的检查清单三步法如果要在自己的项目里落地可以按这个顺序推进。先跑通单会话定义状态字段。每次回复前更新状态。把状态注入用于生成回复的 prompt。用一个 5 轮任务场景验证不遗忘。再验证多轮稳定性用 8 轮左右、包含打断和纠偏的任务测试。检查状态更新是否正确、是否遗漏关键约束。增加断点续传模拟会话中断后恢复。对比加与不加独白的回复质量。最后考虑成本和工程化把状态更新改成异步触发。设置状态长度上限。增加脱敏、日志和审计。在关键节点用提问确认而不是假设系统完全理解。这套流程不是一次性的。每换一个任务领域都要重新校准状态字段和触发策略。6.3 一些容易忽略的工程细节ASR 转写错误会直接污染状态。比如用户说“不要香菜”ASR 转成“不要香茶”状态里就会多出一条错误偏好。建议状态更新时只抽取置信度较高的信息或者把不确定内容标记为“待确认”在下一次回复里顺口问一句。另外不要把所有负面体验都归罪到模型记性上。很多时候问题出在上游链路ASR 没听清TTS 播放太快或者用户输入本身被打断。先看输入再看状态最后看模型这是排查这类问题的基本顺序。调试时建议同时记录公开回复和内部状态。这样能看到“代理为什么这么答”而不是只看到答案错误。与其追求“更长的上下文窗口”不如先问自己代理有没有一条可靠的路把零星输入固化成自己能读的状态。这比把记忆无限放大更重要。给团队写代码要留注释给 voice agent 配一条内心独白也是类似的工程习惯。