8.6 KiB
EMAI 助手:端上听写 + 对话 + 合成 —— 设计
版本:v0.2 日期:2026-10-18 状态:设计稿,未写代码。 范围:耳机唤醒和 App 里按住说话,两条入口共用这一套。通话 / 同声 / 面对面 / 音视频不在本文。 对照:科大讯飞SparkChain-全面替换第三方-落地文档 第 5.5 节是「三段接上、先问通」;本文把时间、地点、天气、新闻、个人工具的分工定死。
零、结论
每轮对话都带上手机本地时间和缓存地址,只解决「现在几点、今天星期几、我在哪」。
天气、新闻和其它要联网才知道的问题,模型手里只有这两样事实,答不出来。这两类仍由手机执行工具,把结果喂回模型,再合成。
个人数据(纪要、拾忆、待办)同样是手机拿登录身份去调已有 MCP。对话本身不经过业务接口。
一、时间地址怎么带(相对「原样塞进每句话」改过的地方)
| 做法 | 原因 | |
|---|---|---|
| 时间 | 每一轮用户说完,用手机本地钟重新拼一次 | 会话可能持续十几分钟。「现在几点」不能用开场那一刻 |
| 地址 | 只读 DeviceLocationService 的缓存,这一轮不等 GPS |
定位会拖住开口。缓存规则维持现状:回前台刷新,10 分钟内不重复 |
| 放哪 | 放进本轮系统提示,不拼进用户原话,不写入聊天历史 | 写进原话,模型会当成用户自己说的;写进历史,旧时间会把「现在」答错 |
| 经纬度 | 不进模型。只有天气工具要用时,手机自己附上 | 模型念出坐标没有用,还容易被读出来 |
| 没有位置 | 系统提示写「当前没有位置」,不要写「未知」 | 「未知」会被当成地名 |
| 专问「我在哪」且缓存空 | 只这一问允许再等一次定位,上限 4 秒,到点仍没有就按「当前没有位置」答 | 别的问题(含天气)不为这一问阻塞 |
时间文案用手机时区,带星期和偏移,例如:2026年10月18日 星期日 00:59,东八区(UTC+8)。
地址文案沿用现在能念出来的那串(完整地址,没有再退省市区街道)。念「我在哪」用这串。查天气只用其中的城市名。
二、整轮怎么走
SDK 没有百炼那种边听边说。一轮是说完再答。耳机协议、问候语、打断、后台保活维持第 5.5 节,本文只换「听完之后谁来答」。
麦克风 PCM
→ 讯飞 SDK 实时转写,区域 RegionMSFT,语种 = 下面第三节
→ 一句最终文本
→ 本轮系统提示(新时间 + 缓存地址 + 回答规矩)+ 最近对话 + 工具清单
→ 讯飞 SDK 文本对话 LLM(端上,不经业务服务器)
├─ 直接有文字 → 讯飞 SDK 在线合成,区域 RegionMSFT,音色 = 第三节
└─ 返回 functionCall → 手机执行工具(最多两轮)→ 结果喂回 LLM → 再合成
两条入口(DeviceAiSessionService 耳机唤醒、EmaiController 按住说话)共用这一个编排。PCM 从哪来、喇叭往哪放,仍由各自的会话服务决定。
空识别不调用对话模型。工具失败时合成「暂时查不到」,不让模型编一个天气或一条新闻。工具来回超过两轮就停,用已经拿到的结果回答;还什么都没有就说查不到。
对话模型用 SDK 的文本 LLM(海外 domain=openai,文档点名 gpt-5.3-chat / gpt-5.4)。听写和合成才是 SDK 里的微软区域(RegionMSFT)。会议总结走同一套 OpenAI 文本模型,不走星火,也不拿来做 EMAI 的实时对话。
三、识别语种和合成音色
跟设置里的界面语言走,不跟用户刚说的那一句走。
设置页切换语言时,settings_controller 已经把该语言收成一条 ASR 区域码(跟随系统则用匹配后的系统语言,兜底 en-US)。EMAI 直接用这一条:
| 取值 | |
|---|---|
实时转写 lang |
上面那条区域码,例如界面中文 → zh-CN |
| 合成发音人 | 同一条语言在音色表里的神经语音。性别只有 1 用男声;未设置 0 和女 2 用女声。用户对象还没就绪时用女声 |
| 回答语言 | 与这条界面语言一致,写进系统提示 |
用户用别的语言插一句,本轮仍按界面语言识别和回答。要换语种,去设置里改,不在对话中途猜。
四、问题分成四类
系统提示里写明:时间、星期、日期、地点只用本轮给出的事实,不要调用工具,也不要改口。
| 用户在问 | 谁处理 | 落地 |
|---|---|---|
| 几点、星期几、今天几号 | 模型直接答 | 只用本轮时间。不调工具 |
| 我在哪 | 模型直接答 | 有地址就念地址。没有就说还没有位置,不要编一个城市 |
| 天气 | 手机执行 hefeng_weather |
城市用缓存里的城市名,日期用本轮日期。有城市就不要反问「哪个城市」。没有位置时才问一次城市,问到了再查 |
| 新闻、实时信息 | 手机执行联网搜索 | 搜索词带上本轮日期,避免「今天」落到模型的训练截止日期。海外节点用 tavily_web_search,国内节点用 bocha_web_search,和现有 MCP 分组一致 |
| 我的会议、记忆、待办 | 手机执行个人 MCP | 见第五节。没有的就说没有,不要编 |
天气和搜索的密钥留在现有 MCP 进程里。手机只发起这一次工具调用,不把天气接口嵌进对话链路,也不把密钥放进安装包。
和风天气的入参是城市名加可选日期,不是经纬度。所以坐标只在缓存里,给不了城市名时不能拿坐标假装能查。
五、个人 MCP
手机调用,数据仍在用户自己的服务器上。这一跳是工具,不是把整段对话交给业务接口。
沿用现在这 7 个、且要身份的工具:
search_meeting_notes、get_memory_items、get_memory_stats、get_memory_report、get_user_tasks、add_user_task、cancel_user_task。
身份仍用 user_getmcptoken 的短时令牌,按工具名附在调用上。令牌不写进系统提示,不进历史,不出现在给模型的工具参数里。模型只看到工具名和参数说明。
百炼时期令牌要按工具名嵌进 user_defined_params,是因为第三方云在代调。换成手机自己调之后,手机在发 MCP 请求时带上令牌即可,形状不再跟百炼的嵌套走。
hefeng_weather 不在这 7 个里面。它和联网搜索不需要用户身份,手机调用时不要附登录令牌。
六、和现在百炼的对应
| 百炼现在 | 换成 |
|---|---|
会话开始时把日期、地址放进 user_prompt_params,整场不变 |
每轮重算时间;地址仍只读缓存 |
client_info.location 把经纬度交给云端 |
经纬度不进模型 |
| 云端插件自己搜网页、自己查天气 | 模型只返回要调哪个工具;手机执行,再把结果喂回去 |
| 个人 MCP 由云端代调,令牌按工具名塞进会话 | 手机自己调,令牌只附在那一次请求上 |
| 边听边说 | 做不到。说完一句再答 |
测试服 aiOnlyIflytek 已经让百炼 start 直接返回失败。本文落地前 EMAI 在测试服起不来。不要为了对照把百炼重新打开。
七、明确不做
- 不把对话转发到
user_*业务接口,也不在服务端再包一层「代答」。 - 不把会议总结的星火接口拿来当 EMAI 的对话模型。
- 不在每一轮阻塞等 GPS。
- 不把经纬度、令牌、天气密钥写进提示词。
- 不改耳机协议字节,不改通话 / 同声 / 面对面 / 音视频的选路。
- 第一版不恢复百炼的全双工。工具调用失败不影响「问一句有回答」。
八、验收
- 问「现在几点」「今天星期几」:答的是手机本地时间,和会话开始时的时间可以不同。
- 定位开着时问「我在哪」:念缓存地址,不念经纬度。关掉定位再问:说没有位置,不编城市。
- 有城市缓存时问天气:不反问城市,答案来自工具。把工具结果改成失败时,嘴里是「暂时查不到」,不是编出来的温度。
- 问「今天新闻」:搜索词里带当天日期,回答来自搜索结果。
- 问「我最近的会议 / 待办」:走个人 MCP,换一个账号看不到上一个账号的数据。抓对话请求里看不到令牌。
- 设置改成英语后,识别和回答都是英语,音色是该语言的女声;性别设为男之后换成男声。
- 耳机按键和 App 按住说话,问同一句,走的是同一套编排。