欢迎光临
我们一直在努力

我用声网对话式 AI 引擎和 Flutter 做了一个课后英语语音家教

前言

从小学到大学,课后学习有一个相似的空白:题目和资料并不少,但真正开口说英语的机会却很有限。

我记得小学学完一篇英语短文后,往往就放下了,几乎没有机会和别人对话;初高中记住了不少句型,却很难把它们说进一段自然的对话;大学虽然有英语角,也要等到固定时间,未必随时都能找到合适的人陪练。

说实话,现在的英语学习 App 已经不只有背单词和句子跟读了。多邻国、ELSA 和 Speak 等产品都提供了 AI 对话、角色扮演或即时反馈,它们在课程体系、学习路径和长期评估上做得很完整。

但我体验到的这类产品,更多面向自主学习的成人用户。围绕普通学生的课后内容做语音陪练,或许还有空间。

我想做的是把课堂上学过的内容,放到课后通过 AI 智能体的语音对话再练一遍,让学生从“学过”走到“能开口用”。后续如果接入不同地区的教材知识库,还可以围绕具体课文做复习和预习。

下图是我希望知识库接入后实现的课后练习流程,不是本文第一版 Demo 已具备的能力:

在这里插入图片描述 地区、时间和教材版本不能由模型自行猜测。真正接入时,需要由学生、家长、学校或老师提供明确资料,并在获得授权后作为知识库检索条件。

面对一年级学生,实际对话可能会中英文混杂,避免一开始就堆太多陌生单词;高年级学生则可以逐步增加英文比例。

于是我开始做这个 App,给它起名叫《随口练》。

一、先确定技术栈

本来想用微信小程序开发,但小程序的 live-player 和 live-pusher 涉及相应资质;而语音对话离不开这类能力,所以我先放弃了这条路。

于是我改用 Flutter 做 App。原生 App 对麦克风等权限的支持更直接,Flutter 也能用一套代码覆盖 Android 和 iOS。

ASR A语音识别由 声网 提供,LLM 大模型和 TTS 语音合成则需接第三方,只是在控制台里统一配置,不用自己搭编排链路。当然,ASR 也可以切换成别家的,BYOK 的好处是给了开发者更多的选择,在面对不同的场景时,选择不同的模型,不用被一家所捆绑。

VAD(语音活动检测)负责判断用户何时开始或结束说话;配合实时字幕,页面能看到边说边出的文字。声网的实时链路也支持全双工和打断,用户不用每次都等智能体把一整段话说完。

二、先梳理一下我的 APP 要做什么

第一版先做一个最小 MVP,只验证实时语音对话这条链路;知识库和工具调用后面再补。

页面我想做成类似豆包的空白聊天页。课后复习、情景练习、模拟表达这些方向,不做成一上来就要填写的表单,而是让学生直接通过语音告诉 Mia 想练什么。

要做到这一点,智能体的角色和行为规则要先配置好。

要做到不同年级可以使用同一条实时链路,只调整题目和说话节奏:小学更适合短句、跟读和鼓励;初高中可以加入情景表达和复述;大学则可以练课堂展示或面试表达。

无论哪个学段,都给智能体留同一条边界:一次最多纠正一个最需要改的点,不把一轮口语练习变成冗长的语法讲解。

三、先在声网创建英语陪练智能体

在 声网 进行注册,然后在控制台创建一个项目,选择通用项目、1V1语音。

在这里插入图片描述

图1 创建项目

项目创建后需要记录 App ID 和 App 证书,另外还要在控制台"设置 → RESTful API"里生成一组客户 ID 和客户密钥。App ID 用于客户端加入 RTC 和 RTM;App Certificate 用于在 FastAPI 服务端签发短期 Token,客户 ID 和客户密钥用于服务端调用声网接口启动智能体,后两者都只留在服务端,不能写进 App。

在这里插入图片描述

图2 项目创建完毕后

接着从侧边栏菜单中的“全部产品”进入“对话式 AI 引擎”,并且切换到刚创建的项目《随口练》,开通对话式 AI 引擎服务。

注意:没有开通服务时,即使前后端代码都正常,智能体也无法启动。

在这里插入图片描述

图3 开启对话式 AI

进入智能体页面,新增智能体。默认会有一个英语陪练的模板,不妨就用它当做我们的初始配置,选择它进入智能体配置页面。

在这里插入图片描述

图4 创建智能体

在这里插入图片描述

图5 智能体编辑页

配置 Mia 的角色和对话规则

进入编辑页后,我没有急着把“课本知识库”“学习档案”都塞进去。它们确实是后面想做的能力,但第一版只想先验证一件事:学生开口后,能不能得到一段不打断节奏、又不太像标准答案的回应。

我给智能体取名 Mia,提示词没有写得特别花哨,主要是几条实际会影响体验的规则:

你是一位耐心的英语口语陪练 Mia。

开始时,先问学生想复习当天课程、练习一个生活场景,还是做一段模拟表达。学生如果已经直接说了课程内容或场景,就直接开始,不重复询问。
学生说英语时,先理解意思,再用简单英语回应。
如果有明显错误,每次只指出一个,并给出更自然的说法。
回答后只追问一个简单问题,给学生继续说的机会。
学生说“再说一遍”“慢一点”或“换一个话题”时,直接照做。
学生使用中文时,可以先帮助他组织成一小句英语,不批评,也不要一次讲太多语法。
不询问姓名、住址、学校等个人信息;不用于正式考试评分。

开场白我写的是:Hello, I am Mia. Would you like to review a lesson, practice a situation, or try a short talk? 学生也可以不回答选项,直接说“今天学了什么”或“我想练点餐”。

前面提到的“根据年级、教材和所在地匹配内容”,在这一版里还是下一步的规划。真正接入时,教材版本和课程进度应该来自学校、老师或用户自己选择的资料,不能让模型凭一句话去猜。我先把语音对话跑顺,再接知识库。

四、参考官方 Flutter Demo,拆开 App 与服务端

声网的 Recipes 中有 Flutter Quickstart。我先把它当作接入路线图,而不是直接把示例页面搬过来。第一版只保留最小闭环:Flutter 把麦克风声音送进 RTC 频道;英语家教进入同一频道后回传语音;字幕通过 RTM 回到手机页面。

在这里插入图片描述

图6 Flutter Quickstart

本机准备好 Flutter SDK 后,我先运行 flutter doctor 检查 iOS 工具链,再创建 flutter-speaking-coach 工程。这个步骤看着简单,实际能提前发现 Xcode、模拟器或 CocoaPods 没配好的问题。

在这里插入图片描述

图7 Flutter 环境检查

项目我分成两部分:lib/ 是 Flutter 页面、RTC 音频和 RTM 字幕;server/ 是 FastAPI 服务。这样拆不是为了显得复杂,而是因为 App Certificate、启动智能体的鉴权信息都不能放进手机端。Flutter 只拿一次会话需要的频道、UID 和短期 Token;服务端保存证书,负责创建会话并启动智能体。

flowchart LR
A[随口练 Flutter App] –>|申请一轮练习| B[FastAPI 服务端]
B –>|频道、UID、短期 Token| A
B –>|启动英语家教| C[声网对话式 AI 引擎]
A <–>|RTC 实时音频| C
C –>|RTM 字幕和状态| A

服务端创建会话时,先生成一个新的频道和两个 UID,再签发短期 Token:学生端用自己的 uid 签,智能体那个用 agent_uid 签,随启动接口一起传给声网。核心逻辑如下:

@app.post("/api/coach/sessions", response_model=SessionResponse, status_code=201)
def create_session(payload: CreateSessionRequest) -> SessionResponse:
channel = f"speaking-{int(time.time())}-{secrets.randbelow(9000) + 1000}"
user_uid = secrets.randbelow(8_000_000) + 1_000
session = Session(
session_id=secrets.token_urlsafe(12),
channel=channel,
user_uid=user_uid,
agent_uid=secrets.randbelow(8_000_000) + 8_001_000,
token=make_token(channel, user_uid),
topic=payload.topic,
)
sessions[session.session_id] = session
return SessionResponse(
sessionId=session.session_id, appId=required_env("AGORA_APP_ID"),
channel=session.channel, uid=session.user_uid,
token=session.token, agentUid=session.agent_uid,
)

用户点击“开始练习”后,Flutter 再请求启动接口。FastAPI 使用保存在服务端的凭证调用声网接口,让 Mia 加入同一个频道。App 端从头到尾都不接触 App Certificate 和智能体配置密钥。

当前 Demo 启动的是已发布的 Pipeline,所以线上运行的人设和开场白以声网对话式 AI 引擎控制台里已发布的智能体配置为准。本地的 prompts/mia.md 用来维护同一份提示词文本;修改后,还要同步到控制台并发布新版本,线上智能体才会采用新规则。

Flutter APP 端需要接入官方的 agora_rtc_engine、agora_rtm SDK,然后在代码中调用,我封装了一个 AgoraCallService 用于声网 SDK API 调用。

dependencies:
flutter:
sdk: flutter

# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
agora_rtc_engine: ^6.6.3
agora_rtm: ^2.2.6
http: ^1.6.0
permission_handler: ^13.0.0

在这里插入图片描述

图8 Flutter RTC 接入

五、把真实语音和字幕接进 Flutter

“随口练”的首页没有文本输入框,也没有要先填写的模式表单,只有“开始练习”“静音”“结束”三个操作。连接后,学生可以直接说“我想复习今天的课”或“我想练点餐”,由 Mia 在对话里确认方向。

在这里插入图片描述

图9 首页

点击“开始练习”后,页面会依次显示“申请权限”“正在加入频道”“陪练已连接”。这些状态我没有省掉:学生拒绝麦克风权限时,要知道去哪里打开;Token 过期或入会失败时,也不能只看到一个一直转的圆圈。

RTC 的关键代码放在 AgoraCallService 中。服务端把本次会话的参数返回给 App 后,Flutter 负责加入频道、发布本地麦克风,并订阅 Mia 的远端音频:

await engine.initialize(RtcEngineContext(
appId: session.appId,
channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
));

await engine.joinChannel(
token: session.token,
channelId: session.channel,
uid: session.uid,
options: const ChannelMediaOptions(
clientRoleType: ClientRoleType.clientRoleBroadcaster,
publishMicrophoneTrack: true,
autoSubscribeAudio: true,
),
);

这段代码只做三件事:进频道、把学生的声音发出去、把英语家教的声音接回来。Mia 说话时,手机播放的是远端实时音频,不是等待一整段音频合成完后再下载。

我还接了 RTM 做字幕。前提是启动智能体时传了 advanced_features.enable_rtm = true 和 parameters.data_channel = "rtm"(两者默认都关着,字幕默认走 RTC DataStream),且服务端签的 Token 同时带 RTC 和 RTM 权限。之后就用这个 Token 登录、订阅当前频道,收到消息按来源显示成"我"或"英语家教"。还接了 RTM 做字幕。它同样使用这次会话的短期 Token 登录,订阅当前频道;收到消息后再按来源显示成“我”或“英语家教”。

final (login, _) = await client.login(session.token);
if (login.error) throw StateError('RTM 登录失败:${login.reason}');

final (subscribed, _) = await client.subscribe(
session.channel,
withMessage: true,
);
if (subscribed.error) {
throw StateError('RTM 字幕订阅失败:${subscribed.reason}');
}

在这里插入图片描述

图10 RTM 字幕代码

调试时我才发现,声音和字幕不能当成同一件事看。能听到 Mia、字幕却不更新,先看 RTM 是否登录和订阅成功;字幕有了、但听不到回复,再检查 RTC 的远端音频和手机当前的播放设备。把两条链路拆开,定位问题会快很多。

在这里插入图片描述

图11 麦克风授权

六、启动项目,做一轮真实的课后练习

先启动 FastAPI 服务端:

cd flutter-speaking-coach/server
python -m uvicorn src.server:app –host 0.0.0.0 –port 8001

再启动 Flutter。iOS 模拟器访问电脑上的服务时可以写 127.0.0.1;换成真机后,需要改成电脑在同一局域网内的 IP:

flutter run -d <你的设备ID> \\
–dart-define=COACH_API_BASE_URL=http://127.0.0.1:8001

这一轮我没有测试复杂的发音评分,而是先验证最基本的课后对话:点击开始练习后,直接说:Today we learned about animals. Mia 会接着问一个简单问题。学生如果没听清,就说:Please say that again.;如果想换方向,就直接说:Let us talk about food.

我主要确认四件事:麦克风权限是否正常弹出;学生的话能否出现在字幕中;Mia 的语音和字幕是否都能回来;她还没说完时,新的输入能不能把这一轮打断。前面三项决定“能不能用”,最后一项决定它像不像一次正常交流。

在这里插入图片描述

图12 IOS模拟器对话

打断这一点对学生挺重要。练习时常见的不是完整地听完一段回答,而是中途没听清、想改口,或者突然想到另一个说法。如果每次都必须等智能体讲完,反而会把人又推回“听录音”的感觉里。

在这里插入图片描述

图13 打断日志

小结:先把开口这件事变简单

现在的“随口练”仍然只是第一版。它没有课程体系、学校教材知识库、错题本、学习报告,也不适合拿来做正式考试评分。弱网、耳机切换、进入后台后能否恢复等细节,也都还要继续测。

不过它已经把我最开始想做的事情跑通了:学生不用先想好一大段话,只要从“今天学了什么”开始开口;Flutter 承接手机上的练习页面,FastAPI 管理会话和密钥,声网对话式 AI 引擎把实时音频、智能体和字幕连在一起。

后续我会再把教材和课程进度作为明确的数据源接进来,让 Mia 能围绕真实的学习内容陪练。但在那之前,先让学生能随时开口、随时打断、随时再说一遍,这才更像一次能随时开始的课后英语陪练。

赞(0)
未经允许不得转载:171主机测评 » 我用声网对话式 AI 引擎和 Flutter 做了一个课后英语语音家教
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址