尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

语音代理内部独白:用状态管理根治多轮对话遗忘问题

语音代理内部独白:用状态管理根治多轮对话遗忘问题 语音代理voice agent在多轮对话中反复遗忘用户刚说过的重要内容是这个场景里最常见也最影响体验的问题。用户说出“明天下午三点去机场接王总车型要保持商务车”代理当时回答“好的”五分钟后却问“请问接谁”。问题不在模型“记性差”而在 agent 缺少一条持续更新、可查询、与用户可见输出相分离的状态流。给 voice agent 增加一条内部独白inner monologue让它在每次生成回复前先更新自己的工作笔记再基于这份笔记回答能明显减少关键信息丢失。这篇文章会围绕“内部独白”这个设计思路从原因分析、数据结构、最小代码实现、验证方法、故障排查到生产化建议逐个展开。适合正在做语音助手、客服机器人、口语陪练或智能硬件对话模块的开发者。读完可以自己实现一个带内部状态记忆的语音代理并掌握如何用多轮剧本验证它是否真的不再遗忘。1. 语音代理的“遗忘”不是模型记忆差而是状态管理缺失1.1 语音代理重复问“你刚才说要什么”问题出在哪很多人在第一版语音代理里只做了一件事把用户语音转成文本拼进 LLM 的 messages拿到回答后再转成语音。这种实现单轮效果很好多轮就开始“失忆”。用户说“我明天下午三点去机场接王总车型要保持商务车”代理回答“好的已记下”。下一轮用户说“接机时准备一束花”如果直接把这个文本追加到历史对话里模型理论上还能看到上一轮内容但实际回复时经常只围绕“花”展开把“王总”“商务车”“明天下午三点”这些实体弱化甚至丢掉。原因不只是上下文截断。语音代理和文字聊天不同输入是流式、口语化、有噪声的。用户不会按标准格式补充信息而是不断追加、修正、插入细节。如果系统没有把这些细节显式固化到某个状态对象只靠对话历史推断模型每次生成回答时都要重新“回忆”整段对话一旦历史变长或任务切换关键实体就会被稀释。所以“遗忘”本质上是状态管理缺失。模型不是没有看过那些文本而是没有一个可靠的机制保证每个关键事实都被提取、保存、并在需要时被引用。1.2 三类信息丢失的典型路径第一类是 ASR 层面的丢失。语音识别会把“王总”识别成“王宗”把“商务车”识别成“商用车”。文本已经错了后续 LLM 再聪明也无法恢复正确实体。第二类是上下文窗口截断。对话超过模型长度限制后最早的信息被丢弃如果系统没有做摘要或状态外置关键条件就没了。第三类是长对话中的注意力漂移。即使上下文没被截断多轮后模型也倾向于更关注最近的 user 消息较早的约束条件容易被忽略。这三类丢失的修复方式完全不同丢失类型典型表现解决方向ASR 识别错误人名、地名、数字听错ASR 热词、上下文提示、实体回填上下文窗口截断报 context length 或早期信息消失外部状态存储、滑动窗口、摘要注意力漂移只关注当前句忽略旧约束内部状态显式携带回复 prompt 强制引用内部独白主要解决第二和第三类问题同时能为第一类问题提供实体列表帮助 ASR 做上下文纠偏。1.3 为什么“调大上下文窗口”解决不了根本问题有人会想既然模型容易忘把上下文窗口从 8k 提到 128k 不就行了。实际情况是窗口变大只能让更多 token 存放在上下文里并不能保证模型会正确利用它们。长文本中的关键信息会被噪声埋没用户口语里的重复、修正、语气词也会占据大量 token。成本也会明显上升每轮请求都携带完整历史延迟和费用都会增长。更合理的做法是“压缩状态”每轮只维护一份结构化的工作记忆而不是把原始对话全部塞给模型。内部独白就是这个压缩过程的核心。它让代理在接收新消息后先把新消息里值得记住的内容抽取出来合并进旧状态再基于这个最新状态生成回复。这样无论上下文窗口多大模型看到的都是“当前任务、已知信息、待办事项”的紧凑状态而不是一段未经整理的长对话。2. 内部独白让代理在开口前先写一张工作便签2.1 从人类客服的“边接听边记录”理解内部独白想象一位客服专员接电话。他不会把用户说的每句话都大声重复也不会在回答时把内心分析全部讲给用户听。他会一边听一边在工单系统里记录用户姓名、产品型号、问题描述、已经尝试过的方案。记录内容可能和最终回复完全不同但最终回复必须依赖这份工单。内部独白就是给 voice agent 准备的“工单”。它是一段不会直接展示给用户的内容由模型生成用于持续记录和更新代理对当前会话的理解。用户听到的只有最终回复工作记录则作为内部状态存在系统里。这里要注意区分两个概念用户可见输出面向用户的自然语言回复简洁、自然、符合语音场景。内部独白面向系统自己的状态数据结构化、可覆盖、可追溯不展示给用户。两者的职责分离很重要。如果把内部思考混进用户回复 prompt模型很可能会把“我刚刚更新了内部状态为……”这类话也说出来体验会非常奇怪。2.2 工作流程听写 - 更新独白 - 生成回复带内部独白的语音代理每一轮循环可以拆成四步音频输入经过 ASR得到用户文本。把“旧内部状态 用户最新文本”交给 LLM生成“新内部状态”。把“新内部状态 用户最新文本”交给 LLM生成用户可见回复。回复经过 TTS 输出同时把新状态持久化。核心在第 2 步。模型不直接生成给用户的回答而是先回答“我该记住什么”。这个强制步骤把“记住关键信息”从隐式能力变成显式任务。以接机场景为例初始状态空。第一轮用户说“明天下午三点去机场接王总商务车”。更新后的状态current_task 为“接机”user_profile 包含人物“王总”、车辆要求“商务车”pending_steps 包含“准时到达机场”。模型再基于这个状态生成回复“好的明天下午三点机场接王总我会安排商务车。”第二轮用户说“接机时准备一束花”。状态更新时保留上一轮的王总和商务车追加 pending_steps“准备花”。回复会自然带上“好的接王总时会准备一束花”而不是只回应花。2.3 内部状态的数据结构内部状态要用结构化对象不要用无格式的段落文本。因为状态需要被持续覆盖、追加、检查自由文本很难做到局部更新和稳定读取。一个最小可用的状态对象可以包含这些字段字段类型说明session_idstring会话唯一标识user_profileobject用户提到的个人信息、偏好、固定约束current_taskstring当前正在推进的任务completed_stepslist已经完成的事项pending_stepslist接下来要做的事项last_topicstring当前话题一句话概括updated_atnumber最近一次状态更新时间不要求字段一开始就设计得很全。关键是确定“什么算必须记住的信息”。人名、时间、地点、数字、约束、用户修正这些最容易在口语场景里丢失应该放进 user_profile 或 pending_steps。如果想记住任意信息可以增加一个facts: list[str]专门存放当前任务相关的关键事实。2.4 单次调用还是两次调用的取舍内部独白有两种实现方式一次调用模型先输出内部思考再输出用户回复用特殊分隔符拆分。两次调用先调用一次模型更新状态再调用一次生成回复。一次调用的优点是延迟更低、成本更省。缺点是状态不容易被结构化处理思考内容可能污染回复也难以单独对“记忆质量”做测试和修正。两次调用虽然增加一次 LLM 请求但每条链路职责清晰状态更新可以单独做 JSON 解析和校验回复生成也更容易控制语气。学习阶段推荐两次调用。因为你能清楚看到“状态更新失败”和“回复生成失败”分别是哪一步造成的。当机制稳定后再考虑用一次调用加结构化输出做性能优化。注意内部独白不是模型安全机制它只是工程上的状态管理手段。如果模型能力不支持可靠输出结构化 JSON就需要增加解析容错和重试逻辑。3. 搭建最小 voice agent环境准备与项目骨架3.1 技术选型和运行环境为了把核心机制讲清楚示例采用 Python 项目。LLM 使用兼容 OpenAI Chat Completions 接口的服务也可以替换为其他提供同类型接口的模型服务。ASR 和 TTS 先做成接口学习阶段用文本模拟验证逻辑后再接入真实语音引擎。建议环境组件版本建议Python3.10 及以上LLM 服务任意支持 chat/completions 的模型语音识别Whisper 本地或云端 ASR语音合成任意可返回音频字节的 TTSWeb 框架FastAPI Uvicorn依赖管理pip virtualenv依赖安装示例pip install openai fastapi uvicorn python-dotenv pydantic如果需要跑真实语音识别再额外安装pip install openai-whisper sounddevice soundfile numpy注意Whisper 本地模型安装和首次运行会下载权重网络环境不同耗时差别很大。先把内部独白机制跑通再接入语音是最稳的学习路径。3.2 项目目录与配置项目目录建议如下voice_agent/ ├── config.py ├── agent.py ├── engines.py ├── monologue.py ├── reply.py ├── storage.py ├── main_cli.py ├── server.py └── requirements.txt这个结构把“LLM 引擎”“ASR/TTS 引擎”“状态存储”“内部独白”“回复生成”分开了。每个文件只负责一件事后续排查和替换组件都方便。创建.env文件至少包含OPENAI_API_KEYyour_api_key_here OPENAI_MODELgpt-4o-miniconfig.py读取环境变量import os from dotenv import load_dotenv load_dotenv() OPENAI_API_KEY os.getenv(OPENAI_API_KEY, ) OPENAI_MODEL os.getenv(OPENAI_MODEL, gpt-4o-mini) SESSION_TTL_SECONDS int(os.getenv(SESSION_TTL_SECONDS, 3600))在真实项目中API Key 绝不能写进代码仓库。使用环境变量或密钥管理服务是底线。3.3 定义 LLM、ASR、TTS 引擎接口接口抽象的意义是方便替换。内部独白机制不依赖具体 ASR 和 TTS只依赖文本输入。先定义统一接口后续可以无缝切换。engines.pyclass BaseLLM: def chat(self, messages, temperature0.0) - str: raise NotImplementedError class BaseASR: def transcribe(self, audio_bytes: bytes) - str: raise NotImplementedError class BaseTTS: def synthesize(self, text: str) - bytes: raise NotImplementedError class ConsoleTTS(BaseTTS): def synthesize(self, text: str) - bytes: print(f[TTS] {text}) return text.encode(utf-8) class TextASR(BaseASR): def transcribe(self, audio_bytes: bytes) - str: raise NotImplementedErrorOpenAILLM实现class OpenAILLM(BaseLLM): def __init__(self, modelNone, api_keyNone, base_urlNone): from openai import OpenAI self.model model or gpt-4o-mini kwargs {} if api_key: kwargs[api_key] api_key if base_url: kwargs[base_url] base_url self.client OpenAI(**kwargs) def chat(self, messages, temperature0.0) - str: resp self.client.chat.completions.create( modelself.model, messagesmessages, temperaturetemperature, ) return resp.choices[0].message.content这里没有把所有初始化参数写死而是通过kwargs传入。不同模型服务可能要求不同的base_url由调用方按需传入。3.4 实现内部状态存储学习环境下用内存存储就够了重点放在状态结构上。生产环境再替换为 Redis 或 SQLite。storage.pyimport time from dataclasses import dataclass, field, asdict dataclass class SessionState: session_id: str user_profile: dict field(default_factorydict) current_task: str completed_steps: list field(default_factorylist) pending_steps: list field(default_factorylist) last_topic: str updated_at: float time.time() def to_dict(self): return asdict(self) class MemoryStore: def __init__(self): self._sessions {} def load(self, session_id: str) - SessionState: if session_id not in self._sessions: self._sessions[session_id] SessionState(session_idsession_id) return self._sessions[session_id] def save(self, state: SessionState): self._sessions[state.session_id] state这里的completed_steps和pending_steps使用列表因为同一时间段可能有多件事。user_profile使用字典方便按名保存不同维度的用户信息。3.5 实现内部独白更新与回复生成内部独白更新是核心。关键是 prompt 要明确告诉模型你在维护内部状态用户看不到这些内容输出必须是 JSON。monologue.pyimport json import time from storage import SessionState MONOLOGUE_SYSTEM_PROMPT 你是一个语音助手的内部记忆模块。用户不会直接看到这里的内容。 请根据旧状态和用户最新说的话更新状态 JSON。 只输出 JSON不要输出任何解释。 状态 JSON 的字段 - user_profile: dict记录用户提到的个人信息与固定偏好 - current_task: str当前正在推进的任务没有则为空字符串 - completed_steps: list[str]已经完成的事项 - pending_steps: list[str]接下来要完成的事项 - last_topic: str当前话题的一句话概括 处理规则 1. 如果旧状态与新消息冲突以新消息为准。 2. 不要删除仍然有效的关键信息。 3. 如果新消息没有改变某个字段保留旧值。 def parse_json_response(raw: str) - dict: raw raw.strip() if raw.startswith(): lines raw.splitlines() lines [line for line in lines if not line.startswith()] raw \n.join(lines).strip() return json.loads(raw) def update_monologue(llm, state: SessionState, latest_text: str) - SessionState: content ( 旧状态\n f{json.dumps(state.to_dict(), ensure_asciiFalse, indent2)}\n\n f用户最新说\n{latest_text} ) messages [ {role: system, content: MONOLOGUE_SYSTEM_PROMPT}, {role: user, content: content}, ] raw llm.chat(messages, temperature0.0) data parse_json_response(raw) data[session_id] state.session_id data[updated_at] time.time() return SessionState(**data)需要注意SessionState(**data)要求 JSON 字段与 dataclass 字段一致。如果模型漏了字段可以直接从旧状态补齐避免崩溃for key in state.to_dict(): if key not in data: data[key] getattr(state, key)reply.pyimport json from storage import SessionState REPLY_SYSTEM_PROMPT 你是语音助手。请根据内部状态回应用户。 回复保持在两句话以内语气自然口语化。 不要复述内部思考不要列清单不要输出内部状态细节。 如果需要在回复中引用用户此前提供的信息直接表达。 def generate_reply(llm, state: SessionState, user_text: str) - str: content ( f用户最新指令{user_text}\n\n f当前内部状态{json.dumps(state.to_dict(), ensure_asciiFalse)} ) messages [ {role: system, content: REPLY_SYSTEM_PROMPT}, {role: user, content: content}, ] return llm.chat(messages, temperature0.3)这里刻意把内部状态放在回复 prompt 里让模型在回答前先看到完整的“记忆”。模型不需要从长对话里猜测关键信息因为状态已经显式给出。3.6 用 CLI 完成一轮“文本模拟语音输入 - 语音回复”agent.py把整个流程串起来from engines import BaseLLM, BaseASR, BaseTTS, ConsoleTTS from monologue import update_monologue from reply import generate_reply from storage import MemoryStore, SessionState class VoiceAgent: def __init__(self, llm: BaseLLM, store: MemoryStore, asr: BaseASR None, tts: BaseTTS None): self.llm llm self.store store self.asr asr self.tts tts or ConsoleTTS() def handle_text(self, user_text: str, session_id: str): state self.store.load(session_id) new_state update_monologue(self.llm, state, user_text) self.store.save(new_state) reply generate_reply(self.llm, new_state, user_text) audio self.tts.synthesize(reply) return { session_id: session_id, reply: reply, audio: audio, state: new_state.to_dict(), } def handle_audio(self, audio_bytes: bytes, session_id: str): user_text self.asr.transcribe(audio_bytes) return self.handle_text(user_text, session_id)main_cli.py提供一个交互入口import json import uuid from agent import VoiceAgent from engines import OpenAILLM, ConsoleTTS from storage import MemoryStore def build_agent() - VoiceAgent: llm OpenAILLM() store MemoryStore() return VoiceAgent(llmllm, storestore, ttsConsoleTTS()) def main(): agent build_agent() session_id uuid.uuid4().hex[:8] print(输入 quit 退出) while True: text input(你文本模拟语音: ).strip() if text.lower() in {quit, exit, q}: break result agent.handle_text(text, session_id) print(f助手: {result[reply]}) print(f内部状态: {json.dumps(result[state], ensure_asciiFalse, indent2)}) if __name__ __main__: main()运行方式python main_cli.py运行后会看到每一轮的用户输入、助手回复和内部状态。打印内部状态是学习和调试阶段的关键手段可以直观看到“模型是否记住了信息”。实际产品中不要把这个状态暴露给终端用户。4. 通过 WebSocket 接入实时语音会话4.1 为什么学习阶段优先用 CLI生产阶段再上 WebSocketCLI 版本的优点是运行简单不需要管理连接生命周期适合验证核心机制。但真实语音代理通常需要持续接收音频片段、控制超时、处理断连这时需要 WebSocket 这类长连接协议。WebSocket 可以承载多种消息类型音频帧、文本指令、状态请求、服务端回调。服务端收到音频后可以分片识别也可以整段识别。本文先给出“收到完整音频字节或文本返回完整回复”的最小实现生产再结合 VAD 做流式处理。4.2 WebSocket 服务端实现server.pyimport asyncio import base64 from fastapi import FastAPI, WebSocket, WebSocketDisconnect from agent import VoiceAgent from engines import OpenAILLM, ConsoleTTS from storage import MemoryStore app FastAPI() agent VoiceAgent( llmOpenAILLM(), storeMemoryStore(), ttsConsoleTTS(), ) app.websocket(/ws/{session_id}) async def websocket_endpoint(websocket: WebSocket, session_id: str): await websocket.accept() try: while True: payload await websocket.receive_json() if payload.get(type) text: text payload.get(text, ) result await asyncio.to_thread(agent.handle_text, text, session_id) await websocket.send_json({ type: reply, reply: result[reply], state: result[state], }) elif payload.get(type) audio: audio_bytes base64.b64decode(payload.get(audio, )) result await asyncio.to_thread(agent.handle_audio, audio_bytes, session_id) audio_b64 base64.b64encode(result[audio]).decode() await websocket.send_json({ type: reply, reply: result[reply], state: result[state], audio: audio_b64, }) except WebSocketDisconnect: # 客户端断开清理连接状态 pass启动服务uvicorn server:app --host 0.0.0.0 --port 8000这段代码里asyncio.to_thread很关键。LLM 调用是阻塞操作直接在协程里执行会卡住事件循环导致其他会话无法处理。使用线程池避免阻塞是生产化的基本处理。4.3 客户端发送音频的调用示例以下示例用 Python 的websockets库发送一段音频import asyncio import base64 import json import websockets async def send_audio(): async with websockets.connect(ws://localhost:8000/ws/user-001) as ws: audio_bytes open(sample.wav, rb).read() await ws.send(json.dumps({ type: audio, audio: base64.b64encode(audio_bytes).decode(), })) resp await ws.recv() data json.loads(resp) print(data[reply]) asyncio.run(send_audio())这里假设sample.wav已经录制好。真正终端场景里客户端需要负责麦克风采集、降噪和音频编码服务端不直接接触设备硬件只处理音频字节。4.4 会话生命周期管理WebSocket 连接断开后会话是否还要保留取决于业务。如果用户在手机上短暂锁屏代理通常应该保留会话状态几分钟如果用户明确结束则释放状态。目前MemoryStore是内存实现服务重启后状态全部丢失。要支持长时间会话需要把SessionState持久化到 Redis 或 SQLite并按session_id读取。内存状态只适合单机、短会话、学习场景。注意不要把用户原始录音保存到日志。语音数据可能包含生物特征和个人信息生产环境需要对音频做脱敏或定时清理。5. 如何验证代理真的不遗忘了5.1 设计多轮对话测试剧本内部独白的效果不能靠“感觉回答对了”来验证要用多轮剧本 状态断言来做回归测试。以接机场景为例测试剧本可以设计为轮次用户输入期望状态变化1我明天下午三点去机场接王总车型要保持商务车。current_task 包含“接机”user_profile 包含王总、商务车、时间2接机时准备一束花。pending_steps 增加“准备一束花”上一轮信息仍保留3我刚才说错了是后天下午三点不是明天。user_profile 中的时间更新为“后天下午三点”4接到王总后送他去酒店。pending_steps 增加“送王总去酒店”人物保持为“王总”这里第 3 轮很关键用来验证“修正”能力。没有内部独白的普通多轮对话模型很可能会同时保留“明天”和“后天”然后生成矛盾回复。5.2 用状态快照自动核对关键实体测试脚本可以直接检查handle_text返回的state字典。test_agent.pyfrom agent import VoiceAgent from engines import OpenAILLM from storage import MemoryStore SCENARIOS [ { user: 我明天下午三点去机场接王总车型要保持商务车。, check: lambda state: 王总 in state[user_profile].get(person, ), }, { user: 接机时准备一束花。, check: lambda state: 花 in str(state[pending_steps]), }, { user: 我刚才说错了是后天下午三点不是明天。, check: lambda state: 后天 in str(state[user_profile]), }, ] def test_agent_memory(): agent VoiceAgent(llmOpenAILLM(), storeMemoryStore()) session_id test-session for item in SCENARIOS: result agent.handle_text(item[user], session_id) state result[state] assert item[check](state), f轮次失败状态{state} test_agent_memory()由于 LLM 输出存在不确定性测试断言不要写死完整 JSON而是检查关键字符串或关键字段是否存在。温度设置成 0 能降低随机性但不能完全消除。5.3 结果检查的指标与判定标准除了单轮检查建议统计以下指标指标定义合格标准实体保持率多轮后关键实体仍存在于状态中的比例不低于 90%修正准确率用户修正后旧值被新值替换的比例不低于 80%回复自然度回复是否暴露内部状态、是否过长人工抽样检查上下文超限率测试中触发 context length 的比例为 0这些指标可以放进自动化回归脚本每次修改 prompt 后跑一遍避免“改好 A 问题却弄丢 B 信息”。5.4 把测试脚本接入回归流程日常迭代中prompt 改动最容易影响状态更新效果。比如在内部独白 prompt 里增加一个字段旧状态没有该字段时模型可能填充空值导致原有关键信息被覆盖。因此每次改 prompt 后都要跑多轮剧本。能接入 CI 更好。测试脚本只依赖 LLM 接口和 API Key运行时间不长完全可以作为单独 job 执行。如果担心成本可以先用最小两轮剧本做烟雾测试再每周跑完整场景。6. 常见故障排查6.1 ASR 错词污染了内部状态现象语音输入“明天下午三点去机场接王总”代理回复“好的明天下午三点去接王宗”内部状态里的人也变成了“王宗”。原因ASR 将同音字识别错误。内部独白会把错误文本当作事实写入状态后续所有流程都会被错误状态引导。排查路径查看 ASR 的原始文本确认错词发生在语音识别阶段还是 LLM 阶段。查看内部状态中user_profile的字段确认是否已被错误实体写入。如果 ASR 支持initial_prompt或热词列表把当前会话中出现的高频实体传进去。在内部独白 prompt 中增加规则如果遇到疑似人名或地名尽量保持原样不要随意“纠正”。一个较少人注意的点不要让 LLM 在内部独白里做“润色修正”。LLM 看到“王总”和“接机”很可能推断出某个人名但推断结果不一定对。状态模块的核心职责是抽取和记忆不是纠错。6.2 LLM 返回非法 JSON解析失败现象update_monologue调用json.loads抛JSONDecodeError整个请求中断用户没有收到回复。原因模型没有严格遵守“只输出 JSON”的指令或者输出包含 Markdown 代码块或者字段缺少引号。排查路径打印raw内容确认模型实际返回了什么。检查是否在 prompt 中明确写了“只输出 JSON不要解释”。检查所使用的模型是否支持response_format{type: json_object}如果支持应在调用时加上。增加解析兜底函数先剥离 Markdown 代码块再解析。增加重试逻辑如果第一次解析失败重新带“上次输出格式错误请只输出 JSON”进入第二次调用。代码兜底示例def parse_json_with_retry(llm, messages, retries2): for i in range(retries): raw llm.chat(messages, temperature0.0) try: return parse_json_response(raw) except json.JSONDecodeError: messages messages [ {role: assistant, content: raw}, {role: user, content: 格式错误请只输出 JSON。}, ] raise RuntimeError(内部独白生成失败)6.3 内部状态不断膨胀导致上下文超限现象会话进行到一定轮数后请求报错 “maximum context length exceeded”。排查发现pending_steps和user_profile越来越长每一轮都拼进 prompt最终超出模型限制。原因状态只增不减。模型在更新状态时很少主动删除不再需要的内容历史待办、旧话题、重复描述都会累积。解决方案对pending_steps做数量上限比如只保留最近 5 条超过后让 LLM 合并或丢弃。对completed_steps做摘要保留“已完成接机准备”不再保留每条细节。每 N 轮做一次状态压缩把旧状态摘要成一段结构化描述。如果使用外部存储把状态历史与当前状态分离prompt 只带当前状态不带全部历史。在设计 prompt 时增加“清理规则”也很有效请删除已经完成且不再影响后续对话的事项如果某个信息对当前任务没有作用可以移出 user_profile。6.4 回复带出“思考痕迹”用户体验奇怪现象用户问“接机准备什么”代理回答“根据我的内部状态current_task 是接机pending_steps 包含准备花和商务车”。原因回复生成 prompt 直接把内部状态 JSON 塞了进去但没有告诉模型“这些内容用户不可见”模型把状态里的字段名当成了可说的内容。解决办法在回复 prompt 中明确写“以下是内部状态仅供你参考不要逐字转述”。设置回复长度上限例如“不超过两句话”。TTS 输出前可以做一次文本清洗过滤明显的 JSON 片段或标签。在测试脚本里加入“禁止出现字段名”的断言例如检查回复中是否包含current_task、user_profile等字符串。7. 从学习项目走向生产最佳实践与扩展7.1 生产环境必须补充的五个能力学习项目能跑通内部独白机制后要进入生产还需要补齐五块能力能力具体措施持久化使用 Redis 或 SQLite 保存会话状态服务重启不丢日志记录 ASR 文本、独白更新前后 diff、最终回复方便回查监控统计每轮耗时、token 消耗、状态解析失败率安全对音频和文本做隐私过滤避免记录敏感原始数据回滚状态更新失败时保留上一版状态支持手动修正状态 diff 日志特别有用。它能告诉你“某轮用户说了什么话导致状态从什么变成什么”。很多“突然忘记”的问题都是因为某轮状态被错误覆盖看 diff 能立刻定位。7.2 用记忆分层降低遗忘概率内部独白可以理解为一层“工作记忆”但它不是唯一记忆。更完整的语音代理记忆可以分三层层级内容示例会话工作记忆当前任务、当前步骤、本会话事实明天下午三点接王总长期用户画像跨会话稳定偏好用户喜欢安静行程需要商务车系统知识产品、规则、领域知识机场停车场位置、限行规则每层使用不同存储和更新策略。内部独白主要负责会话工作记忆跨会话信息应该在会话结束后抽取到用户画像系统知识通过 RAG 或知识库维护。这三层不要混在一个大 JSON 里否则更新和检索都会变得复杂。7.3 可复用的排错清单当你发现 voice agent “忘记了”某个信息按以下顺序排查先打开 ASR 日志确认用户输入没有被识别错。再打印内部状态确认信息是否在状态更新阶段丢失。对比上一轮状态和本轮状态的 diff确认是覆盖、删除还是从未写入。检查状态是否超出上下文窗口导致模型读取时被截断。检查 reply prompt确认模型是否被明确要求引用旧状态。如果以上都正常跑一个最小两轮剧本复现问题并打印 LLM 原始返回。这套清单能覆盖绝大多数遗忘问题。核心思想是“先定位信息在哪一步丢了”而不是盲目调整 prompt。7.4 下一步扩展方向内部独白机制最吸引人的地方是它可组合、可扩展。后续可以考虑结构化输出用 JSON Schema 约束状态字段降低模型输出不合法 JSON 的概率。异步更新状态更新不阻塞回复延迟更低但要注意一致性问题。
返回列表