尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

做饭时手上沾满油,怎么问?我给厨房装了个数字人厨师长:TaoToken 统一 Key 接入 Agent 语音交互实战

发布时间:2026/9/28 18:59:03

资讯中心
01
ARTICLE

做饭时手上沾满油,怎么问?我给厨房装了个数字人厨师长:TaoToken 统一 Key 接入 Agent 语音交互实战

做饭时手上沾满油,怎么问?我给厨房装了个数字人厨师长:TaoToken 统一 Key 接入 Agent 语音交互实战
1. 做饭时手上沾满油为什么还要跟 AI 较劲场景很具体锅里油温七成手上全是油手机在客厅充电平板挂在墙上但不想用油手去点屏幕。这时候你只想问一句“糖醋排骨先放糖还是先放醋”然后有人回答你不用碰任何设备。这就是具身交互要解决的问题。纯文本 Agent 在厨房场景几乎无法落地——颠勺做菜的时候根本腾不出双手敲键盘、触摸屏。Agent 的认知能力理解、推理、记忆、工具调用已经足够成熟但输出如果只能通过文字它就永远被困在“需要你主动去用”的工具定位里。我这次要做的是把数字人厨师长部署在厨房壁挂屏上直接语音提问即可解答烹饪步骤、支持多轮追问、烹饪定时提醒还可以结合冰箱现有食材自动推荐菜谱。整套链路用 Vue SDK 搭建通过 TaoToken 统一 Key/API 通道接入 Agent 语音问答目标让开发者快速跑通油手免触交互 Demo。适合谁看做过 Vue 项目、想给 Agent 加一层“身体”的前端或全栈开发者正在做智能硬件/厨房终端/展厅交互的产品同学以及被“聊天框 Agent”困住、想找具身交互落地场景的人。下面按“原问题 → TaoToken 前置 → 可复制配置 → 验证请求 → 常见错排查 → CTA”的顺序拆每一步都能直接抄。2. TaoToken 前置一把 Key 打通语音问答链路数字人厨师长的核心链路是语音输入 → RAG 检索 大模型推理 → 流式文本 → 数字人 SDK 参数流驱动 → 数字人说话。其中“大模型推理”这一环我选择用 TaoToken 统一 Key 接入。为什么不用各家模型分别申请 Key因为厨房助手这个场景会频繁切换模型菜谱推理用 Qwen Plus意图识别用轻量模型后续可能还要接 Claude 做复杂菜谱规划。如果每个模型一套 Key、一套计费、一套 SDK维护成本会爆炸。TaoToken 提供统一 Key/API 通道一个 Key 走 OpenAI 兼容协议切换模型只改model字段。TaoToken 是什么一个统一的大模型 API 接入通道兼容 OpenAI 协议支持多模型路由。能做什么用一个 Key 调用多家模型适合 Agent、Coding、语音问答等需要多模型协作的场景。适合谁不想在多个模型平台之间反复注册、充值、维护 Key 的开发者。接入前你需要准备一个 TaoToken 账号在控制台创建 API Key一个数字人 SDK 的 App ID / App Secret本文用通用 SDK 占位实际替换为你选用的数字人服务一个 Vue3 TypeScript 项目TaoToken 的 API 地址是https://taotoken.net/api兼容 OpenAI 的/v1/chat/completions路径。也就是说你原来用 OpenAI SDK 写的代码只需要改baseURL和apiKey两个字段就能切到 TaoToken。注意API 地址不要加 UTM 参数直接写https://taotoken.net/api即可。控制台和 API Keys 页面在 deep link 里后面 CTA 会给。3. 可复制配置settings.json 骨架与 Vue 服务层3.1 settings.json 配置骨架先给一份可直接复制的settings.json放在项目根目录或src/config/下。这份配置把 TaoToken 的 Key、模型、数字人 SDK 参数、语音链路参数全部集中管理避免散落在代码里。{ taotoken: { baseURL: https://taotoken.net/api, apiKey: sk-your-taotoken-key, defaultModel: qwen-plus, fallbackModel: qwen-turbo, timeout: 30000, maxRetries: 2 }, avatar: { appId: your_avatar_appid, appSecret: your_avatar_appsecret, containerId: avatar-sdk, gatewayServer: https://your-avatar-gateway/session, hardwareAcceleration: prefer-hardware }, voice: { wakeWord: 大厨, sampleRate: 16000, chunkSize: 6, interruptEnabled: true }, rag: { topK: 3, maxContextLength: 2000 } }字段说明用表格对照更清楚字段作用建议值taotoken.baseURLTaoToken API 入口https://taotoken.net/apitaotoken.defaultModel菜谱推理主模型qwen-plustaotoken.fallbackModel主模型超时降级qwen-turbovoice.chunkSize流式文本攒够几个 chunk 再驱动数字人6voice.interruptEnabled是否允许语音打断truerag.topK知识库检索返回条数33.2 Vue 服务层llm.service.ts在src/services/下新建llm.service.ts负责调用 TaoToken。注意这里用的是 OpenAI 兼容协议baseURL指向 TaoToken。// src/services/llm.service.ts import settings from ../config/settings.json; const SYSTEM_PROMPT 你是厨房AI助手大厨帮用户解决烹饪问题。 你的特点 1. 回答简洁2-3句话因为用户在做菜手忙脚乱没空听长篇大论 2. 步骤清晰用先...再...最后...的句式 3. 记住用户的口味偏好通过RAG注入推荐菜谱时自动规避忌口 4. 涉及油温、火候给具体描述比如筷子放进去周围冒密集小泡就是七成热 5. 用户问到做不出来的菜时给出替代方案 6. 语气热情像厨房里的大厨在教你做菜; export async function streamChat( userMessage: string, onChunk: (text: string) void, context: Array{ role: string; content: string } [] ) { const messages [ { role: system, content: SYSTEM_PROMPT }, ...context.slice(-6), { role: user, content: userMessage }, ]; const response await fetch(${settings.taotoken.baseURL}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${settings.taotoken.apiKey}, }, body: JSON.stringify({ model: settings.taotoken.defaultModel, messages, stream: true, }), }); if (!response.ok) { throw new Error(TaoToken 请求失败: ${response.status}); } const reader response.body!.getReader(); const decoder new TextDecoder(); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n); buffer lines.pop() || ; for (const line of lines) { if (!line.startsWith(data: ) || line data: [DONE]) continue; try { const json JSON.parse(line.slice(6)); const content json.choices?.[0]?.delta?.content; if (content) onChunk(content); } catch { // 忽略解析异常 } } } }3.3 RAG 服务层rag.service.ts光有记忆不够Agent 回答烹饪问题时得能从知识库里检索菜谱信息注入到大模型上下文里避免编菜谱。// src/services/rag.service.ts import { search as searchKB } from ./knowledge-base.service; import { streamChat } from ./llm.service; export async function ragQuery( question: string, onChunk: (text: string) void ) { // 1. 从知识库检索相关信息 const relevant searchKB(question); const context relevant .map((e) [${e.type}] ${e.content}) .join(\n); // 2. 构造增强 prompt let enhancedPrompt question; if (context) { enhancedPrompt 参考以下已知信息回答用户问题。 已知信息 ${context} 用户问题${question}; } // 3. 流式调用大模型 await streamChat(enhancedPrompt, onChunk); }3.4 知识库服务层knowledge-base.service.ts厨房助手需要记住三类信息用户的口味偏好、当前菜谱的进度、常备食材清单。// src/services/knowledge-base.service.ts const STORAGE_KEY kitchen_assistant_kb; interface KB { preferences: Array{ id: number; content: string; createdAt: string }; ingredients: string[]; progress: { dish?: string; step?: number; updatedAt?: string }; } function loadKB(): KB { const raw localStorage.getItem(STORAGE_KEY); if (!raw) return { preferences: [], ingredients: [], progress: {} }; try { return JSON.parse(raw); } catch { return { preferences: [], ingredients: [], progress: {} }; } } function saveKB(kb: KB) { localStorage.setItem(STORAGE_KEY, JSON.stringify(kb)); } export function savePreference(preference: string) { const kb loadKB(); kb.preferences.push({ id: Date.now(), content: preference, createdAt: new Date().toISOString(), }); saveKB(kb); } export function updateIngredients(ingredients: string[]) { const kb loadKB(); kb.ingredients ingredients; saveKB(kb); } export function saveProgress(dish: string, step: number) { const kb loadKB(); kb.progress { dish, step, updatedAt: new Date().toISOString() }; saveKB(kb); } export function search(query: string) { const kb loadKB(); const results: Array{ content: string; type: string } []; kb.preferences.forEach((p) { if (query.includes(p.content.slice(0, 2))) { results.push({ ...p, type: preference }); } }); if (kb.progress.dish query.includes(kb.progress.dish)) { results.push({ content: 当前在做${kb.progress.dish}第${kb.progress.step}步, type: progress, }); } if (query.includes(食材) || query.includes(冰箱)) { results.push({ content: 冰箱里有${kb.ingredients.join(、)}, type: ingredients, }); } return results; } export function getKB() { return loadKB(); }3.5 数字人服务层avatar.service.ts这一层负责数字人渲染、语音合成和动作驱动。不同 SDK 的 API 名称不同这里用通用占位你替换成实际 SDK 的方法名即可。// src/services/avatar.service.ts import settings from ../config/settings.json; let sdkInstance: any null; export function initAvatar() { sdkInstance new (window as any).AvatarSDK({ containerId: settings.avatar.containerId, appId: settings.avatar.appId, appSecret: settings.avatar.appSecret, gatewayServer: settings.avatar.gatewayServer, hardwareAcceleration: settings.avatar.hardwareAcceleration, onMessage: (msg: any) console.log([Avatar], msg), }); sdkInstance.init({ onDownloadProgress: (p: number) console.log([Avatar] 资源加载: ${p}%), }); return sdkInstance; } // 流式驱动is_start / is_end 控制播报节奏 export function speak(text: string, isStart: boolean, isEnd: boolean) { if (!sdkInstance) return; sdkInstance.speak(text, isStart, isEnd); } // 打断当前播报切回倾听状态 export function interruptIdle() { if (!sdkInstance) return; sdkInstance.interactiveidle(); } // SSML 驱动说话 语义化动作 export function speakWithAction(text: string, action: string) { const ssml speak ue4event typeka/type dataaction_semantic${action}/action_semantic/data /ue4event ${text} /speak; speak(ssml, true, true); } export function destroyAvatar() { if (sdkInstance) sdkInstance.destroy(); }两个坑先提醒SDK 只支持localhost或https访问直接用 IP 会报VideoDecoder is not defined频繁刷新会触发房间限流记得在beforeunload调destroyAvatar()释放连接。4. 验证请求从语音输入到数字人开口4.1 主组件 KitchenAssistant.vue把四个 service 的输出缝到数字人表达上。核心链路语音输入 → RAG 检索 大模型推理 → 流式文本 → 数字人 SDK 参数流驱动 → 数字人说话。!-- src/components/KitchenAssistant.vue -- template div classkitchen-container div idavatar-sdk/div div classstatus-bar span v-ifcooking正在做{{ currentDish }} · 第{{ currentStep }}步/span span v-else classidle-hint说大厨唤醒我/span /div /div /template script setup langts import { ref, onMounted, onBeforeUnmount } from vue; import { initAvatar, speak, interruptIdle, speakWithAction, destroyAvatar, } from ../services/avatar.service; import { ragQuery } from ../services/rag.service; import { savePreference, saveProgress, getKB, } from ../services/knowledge-base.service; import settings from ../config/settings.json; const cooking ref(false); const currentDish ref(); const currentStep ref(0); let pendingText: string | null null; function handleVoiceState(res: any) { const state typeof res string ? res : res.state || res.data; if ((state end || state idle) pendingText) { speak(pendingText, true, true); pendingText null; } } function interruptAndSpeak(text: string) { pendingText text; interruptIdle(); } async function processCommand(msg: string) { // 记忆指令 if (msg.startsWith(记住) || msg.startsWith(我不吃)) { const pref msg.replace(/^(记住|我不吃)/, ).trim(); savePreference(pref || msg); speakWithAction(记下了以后做菜不放${pref || msg}。, ThumbsUp); return; } // 进度指令 if (msg.includes(记住进度)) { saveProgress(currentDish.value, currentStep.value); speakWithAction(进度已记住随时可以问我在做到哪一步。, ThumbsUp); return; } // RAG 检索 大模型推理 → 流式驱动 let buf ; let count 0; await ragQuery(msg, (chunk) { buf chunk; count; if (count settings.voice.chunkSize) { speak(buf, buf chunk, false); buf ; count 0; } }); if (buf) speak(buf, false, true); } onMounted(() { initAvatar(); const hour new Date().getHours(); let greeting 我准备好啦今天想做什么菜; if (hour 10) greeting 早上好想做点早餐吃吗煎蛋还是下面条; else if (hour 16) greeting 该准备晚饭啦有什么想吃的吗; const kb getKB(); if (kb.preferences.length 0) { greeting 我记着你吃${kb.preferences[0].content}呢。; } speakWithAction(greeting, Hello); }); onBeforeUnmount(() { destroyAvatar(); }); /script style scoped .kitchen-container { width: 100%; height: 100%; position: relative; } #avatar-sdk { width: 100%; height: 100%; } .status-bar { position: absolute; top: 12px; left: 12px; background: rgba(255, 255, 255, 0.9); padding: 4px 12px; border-radius: 12px; font-size: 13px; color: #333; } .idle-hint { color: #999; } /style4.2 用 curl 验证 TaoToken 通道在跑 Vue 之前先用 curl 确认 TaoToken 通道是通的。这一步能帮你排除 90% 的“Key 没配好”问题。curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-your-taotoken-key \ -d { model: qwen-plus, messages: [ {role: system, content: 你是厨房助手回答简洁。}, {role: user, content: 糖醋排骨先放糖还是先放醋} ], stream: false }成功结果长这样{ id: chatcmpl-xxx, object: chat.completion, choices: [ { index: 0, message: { role: assistant, content: 先放糖。糖在油里炒出焦糖色再放醋酸甜味才挂得住排骨。顺序反了醋会挥发只剩酸味。 }, finish_reason: stop } ] }看到choices[0].message.content有内容说明 TaoToken 通道正常。如果返回 401检查 Key返回 404检查baseURL是不是写成了https://taotoken.net/api/v1正确写法是https://taotoken.net/api路径里再拼/v1/chat/completions。4.3 语音链路验证动作启动 Vue 项目后按这个顺序验证打开http://localhost:5173确认数字人形象加载完成控制台无VideoDecoder报错。对着麦克风说“大厨糖醋排骨怎么做”观察数字人是否开口播报步骤。播报途中说“等一下醋放多少”验证打断是否生效——数字人应立即停止当前播报回应新问题。说“记住我不吃香菜”然后问“推荐个凉菜”验证 RAG 是否把忌口注入上下文推荐结果里不应出现香菜。说“记住进度”然后问“我做到哪了”验证跨会话记忆。实测下来端到端约 500ms 响应意味着你在灶台前喊一声“下一步该干嘛”数字人几乎秒回不会让你等着油烧过头。5. 本篇常见错排查5.1 TaoToken 返回 401 / 403最常见的原因是 Key 没带Bearer前缀或者 Key 复制时带了空格。检查Authorization头是不是Bearer sk-xxx格式。另一个原因是 Key 被禁用或额度耗尽去控制台 API Keys 页面确认状态。5.2 流式返回乱码或截断TextDecoder必须带{ stream: true }否则多字节字符会被截断。另外buffer要保留最后一行不完整的data:等下一个 chunk 拼上再解析。上面llm.service.ts里的写法已经处理了这两点。5.3 数字人 SDK 报 VideoDecoder is not definedSDK 只支持localhost或https访问。如果你用局域网 IP比如192.168.x.x访问浏览器会禁用某些媒体 API。解决办法本地开发用localhost部署时配 https 证书。5.4 频繁刷新触发房间限流数字人 SDK 会占用一个房间连接频繁刷新页面会导致旧连接没释放、新连接被拒。在onBeforeUnmount里调destroyAvatar()并在window.beforeunload里也调一次确保连接释放。5.5 RAG 检索不到知识库内容检查search()里的匹配逻辑。中文分词比较粗糙query.includes(p.content.slice(0, 2))只匹配前两个字。如果用户说“我不吃香菜”存进去问“推荐凉菜”时匹配不到需要把偏好内容也做关键词提取。简单做法存偏好时同时存一个keywords数组检索时遍历匹配。5.6 语音打断不生效打断依赖 SDK 的interactiveidle()方法。如果调用后数字人还在说话检查是不是speak()的is_end参数没传true导致 SDK 认为播报还没结束。另外pendingText的状态管理要确保在idle回调里才触发新播报否则会两个声音叠在一起。6. 跑通之后你可以往哪走这套 Demo 跑通后最直接的下一步是把 TaoToken 的 Key 换成你自己的然后按场景调模型。菜谱推理用qwen-plus意图识别用qwen-turbo降本复杂菜谱规划可以切到更强的模型——只改settings.json里的defaultModel字段代码不用动。如果你要长期做编码类 Agent 或厨房场景的自动化工作流建议看一下 Coding Plan它适合需要持续调用、多模型协作的开发场景。如果你只是想先验证模型对话效果可以直接在模型对话页面试几个菜谱问题确认回答质量再接入。接入过程中遇到 Key 或通道问题去 API Keys 页面和接入文档对照排查比在代码里猜快得多。厨房只是具身交互的一个场景。车间、展厅、门店——任何双手被占用的地方Agent 都需要一个“身体”来表达。大模型让 AI 学会了思考下一步是让 AI 进入终端以具身的方式与人交互。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。