You need to enable JavaScript to run this app.
文档中心
实时音视频

实时音视频

复制全文
下载 pdf
实践教程
AI 作业辅导
复制全文
下载 pdf
AI 作业辅导
传统搜题产品通常只能返回静态答案,缺少逐步讲解和互动追问;真人辅导虽然效果更好,但成本高,难以长期、低成本地覆盖大量学生。AI 作业辅导能让学生通过拍照或语音提问,与 AI 老师进行实时互动式学习,并结合语音讲解、字幕展示和多轮问答,提供更接近真人家教的辅导体验。​
本文将指导您如何基于火山引擎实时对话式 AI 方案,搭建一个 AI 作业辅导应用。​
适用场景​
​
​
场景​
说明​
AI 拍照讲题​
学生拍照上传题目后,AI 识别题目内容并进行语音讲解,同时展示字幕。​
AI 语音问答辅导​
学生围绕当前题目继续语音追问,AI 结合上下文补充说明。​
课后知识点答疑​
AI 教师采用引导式教学策略,不直接给完整答案,而是通过提问和提示引导学生自主思考和推导,培养独立思考能力。​
​
技术架构​
全链路采用流式处理,各模块并行运作,从学生输入到 AI 开始回复的首包延迟控制在秒级以内。​
  • 客户端:负责音视频采集与播放、图片上传、UI 交互及信令消息处理。通过集成 RTC SDK,实现与 RTC 服务的通信。​
  • 接入层:火山引擎 RTC 服务。提供全球分布式媒体网络,负责低延迟、高可靠的音频流与信令消息传输。​
  • 智能层(AIGC Service):集成了核心的 AI 能力模块:​
  • ASR(语音识别):将学生的语音输入实时转换为文本,支持流式识别。​
  • LLM(大语言模型):执行多模态图片理解、题目分析、生成讲解文案等核心逻辑。支持多轮对话。​
  • TTS (语音合成):将 LLM 生成的文本实时转换为语音流,支持双向流式。​
  • 应用层:开发者的业务服务器。负责处理客户端请求、管理会话生命周期、调用火山引擎 OpenAPI(如启动对话、更新会话等)以及业务鉴权。​
  • 数据层:用于持久化存储用户信息、对话历史、错题记录等业务数据。​
​​​
Demo 体验​
实现方案(Web)​
步骤一:集成 AI 音视频互动方案​
具体集成方式,参见集成到软件应用、集成到嵌入式硬件。​
  • 模型选型建议。​
  • ​
  • ​
    模型​
    针对作业辅导场景,选型建议​
    ASR​
    可开通豆包语音识别大模型 2.0。该模型识别准确率更高,适用于对识别准确率要求较高的场景。​
    LLM​
    基础方案可选择视觉理解系列模型。若业务还需要回答天气、新闻、百科、知识点背景等强时效性问题,可结合联网问答 Agent 使用。若需图搜,还需要开启视觉理解能力。支持视频理解的方舟模型,请参见视觉理解能力。​
    TTS​
    可开通豆包语音合成大模型 2.0。音色可选择教育场景音色(如zh_female_yingyujiaoxue_uranus_bigtts)。​
    ​
  • StartVoiceChat 接口,需开启以下核心配置以适配辅导场景:​
  • 开启视觉理解:将 VisionConfig.Enable 设为 true,以识别图片。​
  • 配置系统提示词:在 LLMConfig.SystemMessages 中通过 Prompt 设定,约定讲题风格(如严禁直接给答案、多用启发式提问),并要求输出数学公式时使用 LaTeX 格式。​
  • 开启公式转读:将 TTSConfig 的 disable_markdown_filter 和 enable_latex_tn 均设置为 true,将 LaTeX 公式转译为适合语音播报的自然语言文本后再进行语音合成(例如将 a^2 读作“a 的平方”)。​
  • (建议)接入联网问答 Agent:如果希望 AI 在作业辅导过程中回答天气、新闻、百科、知识点背景等强时效性问题,或者结合当前题目图片做联网检索,可以接入联网问答 Agent。建议控制联网返回内容的长度,避免语音播报过长影响互动节奏;如果是图搜场景,需提前规划好图片发送和缓存策略,避免把无关图片带入检索。具体实现方式,参见接入联网问答 Agent。​
  • 完整的 StartVoiceChat 调用示例​
  • AppId、RoomId、AgentConfig.TargetUserId:替换为在生成 RTC Token 时使用的 AppId、 RoomId、和真人用户 UserId。​
  • TaskId:由您自行定义,用于唯一标识该 AI 对话任务。​
  • AgentConfig.UserId:AI 的标识 ID,由您自定义。​
  • <your_asr_app_id>、<your_asr_token>、your_tts_app_id、your_tts_token:替换为开通 ASR、TTS 服务后获得的应用 ID 和访问令牌。​
  • your_agent_api_key、your_agent_id:替换为联网问答 Agent 的 API Key 和 Agent ID。​
  • ​
    {​
    "AppId": "<YOUR_AppId>",​
    "RoomId": "<YOUR_RoomId>",​
    "TaskId": "<YOUR_TaskId>",​
    "Config": {​
    "ASRConfig": {​
    "Provider": "volcano",​
    "ProviderParams": {​
    "Mode": "bigmodel", // 豆包语音识别大模型​
    "AppId": "<your_asr_app_id>",​
    "AccessToken": "<your_asr_token>",​
    "ApiResourceId": "volc.seedasr.sauc.duration" // ASR 2.0 版本接入点​
    }​
    },​
    "LLMConfig": {​
    "Mode": "ArkV3",​
    "ModelName": "ep-xxxxxx-vision-pro", // 填入你的视觉大模型接入点 ID 或模型 ID​
    "APIKey": "ark-xxxxxx", // 填入你的火山方舟 API Key​
    "SystemMessages": [​
    "你是一位耐心的小学数学老师。当学生上传题目时,请遵循以下教学策略:",​
    "1. 严禁直接给出最终答案;",​
    "2. 先分析已知条件,然后通过提问引导学生思考下一步;",​
    "3. 所有的数学内容必须使用 $ 符号包裹,使用 LaTeX 格式输出。",​
    "4. 讲解要口语化,多用‘我们来看看’、‘想一想这意味着什么’等词汇。",​
    "5. 当学生询问天气、新闻、百科、知识点背景等强时效性信息,或需要结合当前图片补充外部知识时,调用 `webSearch`。"​
    ],​
    "VisionConfig": {​
    "Enable": true // 核心:开启视觉理解能力​
    }​
    },​
    "WebSearchAgentConfig": {​
    "Enable": true, // 开启联网问答 Agent​
    "APIKey": "your_agent_api_key", // 填入你的联网问答 Agent API Key​
    "ParamsString": "{\"bot_id\":\"your_agent_id\",\"stream\":true}", // 填入你的 Agent ID​
    "FunctionName": "webSearch",​
    "FunctionDescription": "查询时效性的信息,或结合当前图片补充外部知识。",​
    "DisableImageSearch": false, // 允许结合图片进行联网图搜​
    "ComfortWords": "正在帮你查找相关资料,请稍等片刻。"​
    },​
    "TTSConfig": {​
    "Provider": "volcano_bidirection", // 使用双向流式,支持语音打断​
    "ProviderParams": {​
    "ResourceId": "seed-tts-2.0", // 使用语音合成大模型 2.0​
    "app": {​
    "appid": "your_tts_app_id",​
    "token": "your_tts_token"​
    },​
    "audio": {​
    "voice_type": "zh_female_yingyujiaoxue_uranus_bigtts" // 教育场景音色​
    },​
    "additions": {​
    "enable_latex_tn": true, // 核心:开启公式转读​
    "disable_markdown_filter": true // 核心:过滤 Markdown 防止朗读特殊符号​
    }​
    }​
    }​
    },​
    "AgentConfig": {​
    "TargetUserId": ["student_user_001"], // 房间内的真人学生 ID​
    "UserId": "AI_Teacher_001", // 当前 AI 老师的 ID​
    "WelcomeMessage": "你好!我是你的AI老师,把题目拍照发给我,我来给你讲解。"​
    }​
    }​
    ​
步骤二:构建前端界面​
以下仅为核心逻辑示例,实际请您结合自身业务调整。​
图片上传处理​
用户上传图片后,客户端向业务服务器请求启动 AI 对话,并加入对应的 RTC 房间。​
用户上传图片后,建议先完成 AI 会话创建和 RTC 入房,再根据你的系统架构选择“客户端直传图片”或“服务端中转图片”的实现方式。以下代码示例仅展示建会和入房的公共部分。​
​
// public/app.js — 图片上传​
​
const uploadArea = document.getElementById('uploadArea');​
const imageInput = document.getElementById('imageInput');​
const preview = document.getElementById('preview');​
​
function generateId() {​
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);​
}​
​
const userId = 'user_' + generateId();​
let roomId = '';​
let taskId = '';​
let isSessionActive = false;​
​
uploadArea.addEventListener('click', () => imageInput.click());​
​
imageInput.addEventListener('change', async (e) => {​
const file = e.target.files[0];​
if (!file) return;​
​
if (file.size > 5 * 1024 * 1024) {​
addSystemBubble('图片大小不能超过 5MB');​
return;​
}​
​
// 显示预览​
const reader = new FileReader();​
reader.onload = (ev) => {​
preview.src = ev.target.result;​
preview.style.display = 'block';​
};​
reader.readAsDataURL(file);​
​
addSystemBubble('正在识别题目...');​
​
const base64 = await fileToBase64(file);​
roomId = 'room_' + generateId();​
taskId = 'task_' + generateId();​
​
try {​
// 1. 启动语音对话​
const response = await fetch('/api/start-voice-chat', {​
method: 'POST',​
headers: { 'Content-Type': 'application/json' },​
body: JSON.stringify({ roomId, taskId, userId })​
});​
const session = await response.json();​
isSessionActive = true;​
​
// 2. 加入 RTC 房间​
await joinRTCRoom(session.roomId || roomId, userId, session.rtcToken);​
​
// 3. 发送题目图片给 AI​
// 方式 A:客户端通过 sendUserBinaryMessage 直接发送图片​
// 方式 B:客户端把图片发给业务服务端生成 URL,再由服务端调用 UpdateVoiceChat 发送图片 URL​
// 方式 C:若启用联网问答 Agent,可让 AI 在当前对话中结合图片或视频画面进行联网检索​
​
addSystemBubble('AI 老师开始讲解...');​
} catch (err) {​
console.error('处理失败:', err);​
addSystemBubble('识别失败,请重试');​
}​
​
imageInput.value = '';​
});​
​
function fileToBase64(file) {​
return new Promise((resolve) => {​
const reader = new FileReader();​
reader.onload = () => resolve(reader.result.split(',')[1]);​
reader.readAsDataURL(file);​
});​
}​
​
音频播放与字幕解析​
当 AI 老师加入房间并开始讲话时,前端需要将远端音频流绑定到页面节点上才能听到声音。同时,需要监听房间消息以展示 AI 讲解的字幕和学生自己说话的识别结果。​
​
// 1. 处理音频播放:必须绑定 DOM 节点才能出声​
rtcEngine.on(VERTC.events.onUserPublishStream, async (e) => {​
if (e.mediaType === VERTC.MediaType.AUDIO_AND_VIDEO || e.mediaType === VERTC.MediaType.AUDIO) {​
let audioContainer = document.getElementById('remote-audio');​
if (!audioContainer) {​
audioContainer = document.createElement('div');​
audioContainer.id = 'remote-audio';​
audioContainer.style.display = 'none'; // 隐藏播放器​
document.body.appendChild(audioContainer);​
}​
await rtcEngine.setRemoteVideoPlayer(VERTC.StreamIndex.STREAM_INDEX_MAIN, {​
userId: e.userId,​
renderDom: audioContainer,​
});​
}​
});​
​
// 2. 接收并解析字幕:兼容文本通道与二进制通道​
// 文本通道字幕​
rtcEngine.on(VERTC.events.onRoomMessageReceived, (e) => {​
try {​
const jsonData = JSON.parse(e.message);​
const dataList = Array.isArray(jsonData) ? jsonData : ​
(jsonData.type === 'subtitle' ? [jsonData.data] : [jsonData]);​
renderSubtitles(dataList);​
} catch (err) {}​
});​
​
// 二进制通道字幕(如开启了魔术字)​
rtcEngine.on(VERTC.events.onRoomBinaryMessageReceived, (e) => {​
// 需根据私有协议 unpack 解包为字符串后再解析​
// const messageStr = unpackSubtitle(e.message); ​
// ...​
});​
​
// 渲染字幕到 UI​
function renderSubtitles(dataList) {​
dataList.forEach(item => {​
// paragraph 为最终段落,text 为流式增量​
const content = item.paragraph || item.text;​
if (content) {​
const isAI = item.role === 'ai';​
// 如果是 AI 回复,将包含 LaTeX 的字符串转为前端易读格式​
const displayText = isAI ? latexToDisplayText(content) : content;​
​
// TODO: 将 displayText 更新到你的 UI 聊天列表中​
// item.definite 为 true 时表示这句话识别/生成结束​
console.log(`[${isAI ? 'AI老师' : '学生'}]: ${displayText}`);​
}​
});​
}​
​
LaTeX 前端转换​
通过 RTC SDK 的消息通道接收字幕,并使用自定义函数将 LaTeX 字符串转换为可读数学符号公式,覆盖 K12 数学全部常用符号:​
​
// public/app.js — LaTeX 转换​
​
function latexToDisplayText(text) {​
if (!text) return text;​
let result = text;​
​
// 移除包裹命令​
result = result.replace(/\\boxed\{(.+?)\}/g, '$1');​
result = result.replace(/\\text\{(.+?)\}/g, '$1');​
result = result.replace(/\\mathrm\{(.+?)\}/g, '$1');​
​
// 几何符号​
result = result.replace(/\\triangle/g, '△');​
result = result.replace(/\\angle/g, '∠');​
result = result.replace(/\\perp/g, '⊥');​
result = result.replace(/\\parallel/g, '∥');​
result = result.replace(/\\cong/g, '≅');​
result = result.replace(/\\sim/g, '∽');​
result = result.replace(/\\odot/g, '⊙');​
​
// 逻辑符号​
result = result.replace(/\\because/g, '∵');​
result = result.replace(/\\therefore/g, '∴');​
​
// 运算符​
result = result.replace(/\\times/g, '×');​
result = result.replace(/\\div/g, '÷');​
result = result.replace(/\\pm/g, '±');​
result = result.replace(/\\mp/g, '∓');​
result = result.replace(/\\cdot/g, '·');​
​
// 关系符号​
result = result.replace(/\\neq?/g, '≠');​
result = result.replace(/\\leq?/g, '≤');​
result = result.replace(/\\geq?/g, '≥');​
result = result.replace(/\\approx/g, '≈');​
result = result.replace(/\\equiv/g, '≡');​
result = result.replace(/\\propto/g, '∝');​
​
// 集合​
result = result.replace(/\\in/g, '∈');​
result = result.replace(/\\subset/g, '⊂');​
result = result.replace(/\\cup/g, '∪');​
result = result.replace(/\\cap/g, '∩');​
result = result.replace(/\\emptyset/g, '∅');​
​
// 箭头​
result = result.replace(/\\Rightarrow/g, '⇒');​
result = result.replace(/\\Leftrightarrow/g, '⇔');​
result = result.replace(/\\rightarrow/g, '→');​
result = result.replace(/\\leftarrow/g, '←');​
​
// 希腊字母​
result = result.replace(/\\alpha/g, 'α');​
result = result.replace(/\\beta/g, 'β');​
result = result.replace(/\\gamma/g, 'γ');​
result = result.replace(/\\delta/g, 'δ');​
result = result.replace(/\\theta/g, 'θ');​
result = result.replace(/\\pi/g, 'π');​
result = result.replace(/\\sigma/g, 'σ');​
result = result.replace(/\\omega/g, 'ω');​
result = result.replace(/\\lambda/g, 'λ');​
result = result.replace(/\\mu/g, 'μ');​
​
// 数学函数​
result = result.replace(/\\(sin|cos|tan|log|ln|lim|max|min|sum|prod|int)/g, '$1');​
​
// 复合结构​
result = result.replace(/\\circ/g, '°');​
result = result.replace(/\\frac\{(.+?)\}\{(.+?)\}/g, '($1/$2)');​
result = result.replace(/\^{(.+?)}/g, '$1');​
result = result.replace(/_{(.+?)}/g, '$1');​
result = result.replace(/\\sqrt\{(.+?)\}/g, '√($1)');​
​
// 清理残留​
result = result.replace(/\\[a-zA-Z]+/g, '');​
result = result.replace(/[{}]/g, '');​
result = result.replace(/\$/g, '');​
​
return result;​
}​
​
语音输入(按住说话)​
通过控制 RTC SDK 的音频流发布与停止,实现“按住说话”功能,用于向 AI 追问。​
​
// public/app.js — 语音按钮​
​
const talkBtn = document.getElementById('talkBtn');​
let isRecording = false;​
​
// 鼠标事件​
talkBtn.addEventListener('mousedown', startRecording);​
talkBtn.addEventListener('mouseup', stopRecording);​
talkBtn.addEventListener('mouseleave', stopRecording);​
​
// 触摸事件​
talkBtn.addEventListener('touchstart', (e) => { e.preventDefault(); startRecording(); });​
talkBtn.addEventListener('touchend', (e) => { e.preventDefault(); stopRecording(); });​
​
async function startRecording() {​
if (!isJoined || isRecording) return;​
isRecording = true;​
talkBtn.classList.add('recording');​
​
// 开启麦克风采集并发布音频流​
await rtcEngine.startAudioCapture();​
await rtcEngine.publishStream(VERTC.MediaType.AUDIO);​
}​
​
async function stopRecording() {​
if (!isRecording) return;​
isRecording = false;​
talkBtn.classList.remove('recording');​
​
// 停止发布并关闭采集​
await rtcEngine.unpublishStream(VERTC.MediaType.AUDIO);​
await rtcEngine.stopAudioCapture();​
}​
​
步骤三:实现“拍照传题”(图片理解)​
  • 拍照传题:向 AI 发送一张或多张图片并提问。支持多图上传、图片分片上传。​
  • 客户端发送图片:由客户端通过 sendUserBinaryMessage 发送 Base64 图片,支持多图上传和图片分片上传,适合直接处理本地拍照得到的图片数据。​
  • 服务端发送图片:由客户端先将图片上传到业务服务端或对象存储,再由服务端调用接口将图片 URL 发送给 AI,适合已有统一文件上传和内容审核链路的系统。​
  • 切题与缓存清理(可选) :当学生问完当前题目,准备拍下一道题时,为了防止 AI 把上一题的条件和下一题混淆,客户端还可以发送清理指令,清除当前 GroupID 的图片缓存。​
具体的实现方式,请参见图片理解(发送外部图片问答)。​
相关文档​
​
常见问题​
如何降低对话延迟?​
AI 对话延迟通常由 ASR 判停、LLM 推理 和 TTS 首包生成三个环节导致。具体优化方案,可参考降低对话延迟。​
​
最近更新时间:2026.10.08 11:25:49
这个页面对您有帮助吗?
有用
有用
无用
无用