语音代理的长对话遗忘问题与Inner Monologue解决方案

语音代理的长对话遗忘问题与Inner Monologue解决方案 很多做语音交互的开发者都有一个体会语音代理voice agent在对话超过十轮之后特别容易“断片”。用户明明在前面说过“明天从北京飞上海”中间插了两句别的代理转头就反问“请问您飞往哪个城市”。这个问题在纯文本聊天里不算明显因为用户能往上翻历史但在语音环境里对话是线性的用户看不见任何记录代理一旦忘记关键信息整段交互就会陷入反复确认的死循环。本文要展开的就是解决这个问题的思路给 voice agent 增加一层 inner monologue也就是“内心独白”。让代理在每轮开口之前先生成一段用户听不到的内部思考用来整理任务状态、滚动刷新记忆、规划下一步动作然后再给出最终回复。通过这种机制代理可以在十几轮甚至几十轮的长对话中依然记得用户最初交代的目标。文章会围绕这块内容展开阅读下来你可以掌握几个东西语音代理为什么会遗忘常见遗忘场景有哪些什么是 Inner Monologue它和思维链Chain of Thought、传统 System Prompt 有什么区别如何设计提示词让模型输出结构化的内心独白如何用 Python 写一个带内心独白的语音代理核心流程代码可以直接跑以及工程落地时要注意的延迟、成本和可观测性问题。如果你正在做智能客服、语音助手、口语对话系统或者单纯对 Agent 的记忆机制感兴趣这篇文章应该能给你一个可落地的参考方案。1. 语音代理为什么会遗忘1.1 语音交互与文本交互的本质差异先想一个最简单的对比。你在微信上跟客服聊天前面说了什么屏幕上有记录哪怕对方忘了你也能截个图、复制一段话发过去。但在电话客服、智能音箱、语音助手这些场景里用户身边没有一块“对话历史屏幕”用户只能说“我刚才说过了呀”或者“算了重新说一遍”。语音交互的这种特殊性直接决定了 voice agent 的记忆压力比普通聊天机器人更大。它必须在对话过程中持续维护一份“内部档案”记录任务目标、已经收集到的关键信息、还缺什么信息、下一步要做什么。这份档案如果只放在大模型的上下文窗口里就会受到 token 数量限制而且随着对话轮次增加早期信息会被逐渐挤出窗口于是出现“遗忘”。另外语音对话通常还伴随噪声、同音词、口语省略等问题。比如用户说“我要订个票明天早上”如果没有上下文记忆代理很难知道“票”是什么票“明天”有没有具体日期。所以对语音代理来说记忆不是锦上添花而是能不能正常完成任务的先决条件。1.2 上下文遗忘的三种典型场景具体来说语音代理的遗忘通常出现在下面三种场景。场景一长对话导致上下文窗口溢出。这是最直接的原因。大模型上下文窗口有限早期轮次被截断后模型的局部注意力只会集中在最近几轮。用户第 1 轮交代的出发地、目的地到第 8 轮可能已经不完整了。场景二任务切换后忘记原始目标。用户在订机票的过程中突然问了一句“我上个月的报销怎么还没到账”。代理如果顺着用户回答很容易把机票任务整体丢出上下文。等用户再回来说“刚才那个航班帮我查一下”代理已经不知道“刚才那个航班”是哪一趟了。这是语音客服里非常常见的断点。场景三信息分散在不同轮次没有被主动合并。用户第 1 轮说“北京到上海”第 5 轮说“明天”第 7 轮说“经济舱”。如果没有一个显式的状态聚合机制模型很容易只记住最近一轮的“经济舱”而忘记前面的北京、上海和明天。因为这些信息分散在不同的 user input 中模型并没有被明确要求把它们合并到同一张“表单”里。1.3 传统方案的局限在哪里针对上述问题业界其实已经有不少尝试但它们各自有短板。第一种是简单拼接历史消息。把所有对话记录一股脑塞进 prompt。这种方式实现简单但 token 成本高而且超出窗口后依然会丢信息本质上是把问题延后并没有解决。第二种是叫 Dialogue State TrackingDST也就是对话状态跟踪。系统定义一个固定 schema比如出发地、目的地、日期、舱位每轮从用户文本里抽取槽位值更新到数据库。这种方案优点是精确但缺点也很明显需要预先定义好所有可能的槽位遇到开放域任务时难以穷举。用户说一个不在 schema 里的需求系统就识别不了。第三种是向量记忆库。把历史对话编码成向量存起来需要时做相似度召回。这适合长期记忆场景但粒度不够精确而且召回结果不一定能直接驱动当前的对话策略对实时性要求高的语音场景来说延迟也不友好。第四种是微调模型。直接让模型学会“记性更好”成本高、周期长业务频繁变化时基本不可行。而 Inner Monologue 这种方案是在每一轮推理时增加一个显式的“内部状态输出”环节。它不依赖固定 schema也不需要专门的向量库只是通过提示词引导模型先梳理状态、再生成回复。优点是通用、轻量、可插拔能和 DST、向量记忆等方法组合使用。2. 带 Inner Monologue 的语音代理架构2.1 什么是 Inner Monologue用大白话说Inner Monologue 就是让代理在“开口”之前先在内部写一张只有自己能看的便签。便签上写清楚三件事我今天这通电话的目标是什么我已经问到了哪些重要信息接下来我还需要问什么。在技术实现上这个“便签”会作为一段不可见的中间输出由大模型生成。它既不是最终回复也不是简单的日志而是模型对自己当前工作记忆的一次显式整理。通过让模型把“还记得什么”以文字方式写出来它会在生成回复之前主动回顾历史从而减少遗忘。这个概念在很多语音 agent 项目中已经实践过效果比较明显。2.2 与思维链和 System Prompt 的区别有读者可能会问这不就是思维链Chain of Thought吗其实二者有联系但侧重点不同。Chain of Thought 的核心目标是提升推理准确性。处理复杂逻辑题时让模型先输出一步步推导过程再给结论把隐式推理变成显式推理。Inner Monologue 的核心目标则是维护对话状态和记忆。它不一定要做复杂推理重点是把“当前任务是什么、已经确认了什么、还缺什么”这些记忆型信息固化成结构化输出。另外还要区分它是和 System Prompt 的关系。System Prompt 是静态的角色设定比如“你是客服助手语气友好”它不会随对话轮次变化。而 Inner Monologue 是动态生成的每一轮都会根据最新对话重新整理一次。所以可以理解为System Prompt 是代理的性格基线Inner Monologue 是代理的临时工作记忆。维度Inner MonologueChain of Thought传统 System Prompt核心目的记忆维护与状态管理提升推理准确率定义角色、语气与行为是否每轮生成通常每轮生成按需使用固定不变是否用户可见不可见可能不可见不可见输出格式结构化 JSON 或固定模板自然语言推理步骤静态自然语言动态程度动态随对话更新动态随问题变化静态2.3 系统整体架构一个带 Inner Monologue 的语音代理整体流程可以用下面这张文字图表示用户语音 → ASR语音识别把语音转成文本 → 状态读取模块读取当前会话状态任务、槽位、历史摘要 → LLM 引擎 第一步基于系统提示词 当前状态 用户输入生成 inner monologue 第二步基于 inner monologue 生成最终对用户说的 reply → 状态更新模块从 inner monologue 中解析关键信息更新会话状态 → TTS语音合成把 reply 转成语音播放给用户这里有一点要注意ASR 和 TTS 只是语音链路两端决定代理“记不记得住”的核心逻辑在中间的 LLM 引擎和状态更新模块。所以本文的实战代码也主要围绕中间这部分展开。3. 环境准备与项目结构3.1 运行环境说明本文示例使用 Python 编写建议使用 Python 3.10 及以上版本。因为示例中我用到了较新的类型注解写法低版本可能需要做调整。操作系统Windows / macOS / Linux 均可解释器Python 3.10依赖默认零第三方依赖即可运行因为示例内置了一个 mock LLM接入真实模型如果想把 mock 替换成真实 LLM需要安装 openai 等 SDK并配置 API Key。为了让没有 API Key 的读者也能完整跑通流程实战部分的call_llm函数会提供一个模拟实现。这个模拟实现只用来演示“内心独白 状态更新”的完整链路实际生产环境需要替换成真实模型调用。3.2 项目目录结构建议按下面的结构组织代码方便后续扩展voice-agent-inner-monologue/ ├── prompts.py # 系统提示词模板 ├── llm_client.py # LLM 调用层包含 mock 实现 ├── voice_agent.py # 核心代理类负责状态更新与对话循环 ├── demo.py # 演示脚本 └── requirements.txt # 依赖说明可选其中voice_agent.py是核心它负责把提示词、LLM 调用、状态更新串起来。llm_client.py可以理解为一个适配层以后换模型或者接本地推理服务只需要改这一个文件。3.3 依赖安装如果只是体验 mock 流程不需要安装任何第三方库python demo.py如果要把 mock 替换成真实大模型则按需安装依赖pip install openai python-dotenv然后在项目根目录创建.env文件配置 API KeyOPENAI_API_KEYsk-xxxx需要注意实际开发中应根据你使用的模型服务商选择对应的 SDK不要照搬本文的配置。4. 核心原理拆解4.1 提示词设计如何让模型“先想后说”Inner Monologue 能否生效最关键的一步就是提示词设计。系统提示词要明确告诉模型两件事第一你需要生成一段用户看不见的内心独白第二你最终输出的回复必须基于这段内心独白产生。下面是一个适合航班预订场景的提示词模板我会在它后面解释为什么每一条约束都很重要。# 文件路径prompts.py INNER_MONOLOGUE_SYSTEM_PROMPT 你是一个语音客服代理正在和用户进行语音对话。 你有两个特殊的工作要求 1. 在回复用户之前你必须先生成一段内心独白inner monologue。 2. 内心独白用户看不见它是你的内部工作记忆用来避免你遗忘已确认的信息和当前任务进度。 你必须严格按下面的 JSON 格式输出 { inner_monologue: { current_task: 当前正在执行的任务名称, slots: { 关键信息槽位1: 值1, 关键信息槽位2: 值2 }, missing_slots: [还缺少的关键信息], plan: 接下来你打算做什么 }, reply: 你要对用户说出的最终回复 } 约束 - slots 中必须包含对话中出现过的所有关键信息不要遗漏。 - missing_slots 列出完成任务还缺少的字段如果已经齐全就放空数组。 - reply 必须口语化适合 TTS 朗读不要使用 Markdown 标记、表情符号或缩写。 - 如果用户临时切换话题请在 current_task 中保留原任务并在最终回复里先简短回应临时话题再把话题拉回主任务。 这段提示词的几个关键点第一明确输出格式为 JSON。结构化输出方便代码解析能直接把内层状态写入代理的记忆模块而不是让状态隐藏在自然语言里。第二要求slots必须包含所有出现的“历史关键信息”。这句话其实是在强化模型的前置回顾行为等于逼着模型在生成回复前从头到尾检查一遍对话历史。第三对reply的口语化要求很重要。语音合成的目标和文本阅读不同用户听到的句子不能带表格、不能带 AI 痕迹所以提示词里要显式要求它生成适合朗读的内容。第四针对任务切换的场景提示词里明确要求“先保留原任务”。很多遗忘问题并不是模型没有记忆能力而是没有“主动维持旧任务”的意识。加一句“在 current_task 中保留原任务”就能显著改善。4.2 解析内心独白模型输出的内容不一定严格是 JSON可能是带解释的文本也可能被 Markdown 代码块包裹。所以在代码里不能无脑json.loads需要先做一步健壮的提取。方法分两层如果内容被json ... 包裹先去掉外层围栏然后找到文本中第一个{和最后一个}截取这段内容再解析。下面是一个通用提取函数# 文件路径llm_client.py import json def extract_json_block(text: str) - str: 从 LLM 输出中提取 JSON 字符串兼容 json 包裹的情况。 if not text: return text text text.strip() if text.startswith(json): text text[len(json):].strip() elif text.startswith(): text text[3:].strip() if text.endswith(): text text[:-3].strip() start text.find({) end text.rfind(}) if start ! -1 and end ! -1 and end start: return text[start:end 1] return text有些模型即使你要求输出 JSON也可能会在前后追加一句话。这个函数通过find和rfind把 JSON 主体截出来能规避大部分格式问题。解析之后的返回结构再交给状态更新模块处理。4.3 状态更新与记忆维护解析出inner_monologue之后代理需要把它写回自己的状态对象。状态对象本质上是 Python 字典我们可以把current_task、slots、missing_slots都更新进去。更新时需要注意一个问题不能整块覆盖。因为模型某一次的slots可能只包含了最近几轮的信息如果直接整体覆盖反而会把之前的槽位弄丢。正确做法是使用update合并def merge_state(state: dict, inner_monologue: dict) - None: 把 inner_monologue 解析出的状态合并进全局状态。 if not inner_monologue: return if inner_monologue.get(current_task): state[current_task] inner_monologue[current_task] new_slots inner_monologue.get(slots) or {} state[slots].update(new_slots) missing inner_monologue.get(missing_slots) or [] state[missing_slots] missing这里的update是关键。它能保证新信息不断追加同时旧信息不会在模型注意力偏移时被意外丢弃。比如用户在第 3 轮已经说了“北京”第 10 轮的内心独白里可能只剩“经济舱”但因为 state 里早就存了“北京”合并且不会被覆盖。5. 完整实战案例5.1 编写 LLM 调用层为了让示例开箱即用我在llm_client.py里内置了一个 mock LLM。它的职责是根据用户输入中的关键词动态更新航班预订的槽位信息然后返回一段符合前面 JSON 格式的结果。生产环境中你可以把call_llm的实现替换成 OpenAI SDK 或者其他模型服务的调用代码。# 文件路径llm_client.py import json import re from prompts import INNER_MONOLOGUE_SYSTEM_PROMPT def mock_llm(messages) - str: 本地模拟 LLM 实现。 仅用于演示不依赖任何外部 API。 真实环境中请把 call_llm 替换为实际模型调用。 user_content messages[-1][content] user_part user_content.split(用户最新语音识别文本】)[-1].strip() # 从用户文本中提取航班信息 slots {} if 北京 in user_part: slots[from] 北京 if 上海 in user_part: slots[to] 上海 if 明天 in user_part: slots[date] 明天 if 经济舱 in user_part: slots[class] 经济舱 elif 商务舱 in user_part: slots[class] 商务舱 name_match re.search(r我叫([\u4e00-\u9fa5]{2,4}), user_part) if name_match: slots[passenger_name] name_match.group(1) # 缺失字段优先级 priority [from, to, date, class, passenger_name] missing [k for