作者:互联网 时间: 2026-08-26 09:46:56
做一个会说话的小游戏角色,Demo 往往来得很快:让 Gemini 写一份角色 Prompt,接上语音识别、模型和语音合成,NPC 就能回答玩家。

真正准备上线时,问题却不再是“它能不能说话”,而是:
这也是 AI 开发最容易造成角色焦虑的地方:代码和文案生成得更快了,但产品判断并没有消失,只是集中到了边界、验收和责任上。开发者仍然需要决定什么话可以说、什么动作不能做,以及什么时候必须把控制权还给玩家。
本文选择一个具体目标:为小游戏的新手引导制作一个可打断、可降级、可回放验证的实时语音 NPC。重点不是追求“无所不知”,而是让它在一个有限场景里稳定工作。
不要先问模型够不够强,先问这个场景能否安全降级。
| 场景 | 是否适合首版 | 原因与限制 |
|---|---|---|
| 解释基础玩法 | 适合 | 答错时可以回到固定帮助页 |
| 根据当前关卡给提示 | 有条件适合 | 游戏状态必须由服务端或可信客户端结构化提供 |
| 角色闲聊与世界观对话 | 适合 | 需限制敏感内容、人格边界和对未成年人的表达 |
| 发放道具、修改积分 | 不适合直接执行 | 模型输出不能成为资产变更指令 |
| 购买、退款、账号申诉 | 不适合自动处理 | 必须进入确定性业务流程或人工渠道 |
| 判断玩家真实年龄、情绪或健康状况 | 不应依赖模型推断 | 语音表现不能替代可靠事实与专业判断 |
一个实用判断是:如果 AI 失败后,玩家仍能通过按钮、文字说明或确定性流程继续游戏,这个场景才适合进入首版。
实时语音 NPC 至少包含以下责任层:
玩家麦克风 ↓实时音频与会话连接 ↓语音识别 / 轮次判断 ↓应用编排层 ──→ 游戏状态快照 ↓LLM + 版本化 Prompt ↓语音合成 ↓字幕与音频播放
Tencent Conversational AI 的官方概览将其定位于用户与多种大模型之间的实时语音交互,并提供跨平台集成方向。实现时仍应把媒体传输、ASR、LLM、TTS、应用状态与审核策略分别看待,而不是把所有问题都塞进 Prompt:
对小游戏而言,RTC 链路负责“声音怎样实时进出”,应用编排层负责“这一轮是否应该发送给模型”,游戏服务负责“事实与资产是否真的变化”。三者不能互相越权。
Gemini 很适合帮助团队扩写人物背景、整理语气示例、寻找冲突规则,但它生成的是候选配置,不是可直接发布的产品规则。
建议先由人写一张场景卡:
interfaceVoiceNpcScenario {id: string;objective: string;allowedTopics: string[];forbiddenClaims: string[];interruptionPolicy: "immediate" | "speech-confirmed" | "disabled";fallback: {llmFailure: "fixed-copy" | "text-help";ttsFailure: "subtitle-only";connectionFailure: "exit-to-local-ui";};}consttutorialGuide: VoiceNpcScenario = {id: "tutorial-guide-v1",objective: "解释移动、互动和退出引导的方法",allowedTopics: ["基础操作", "当前教程目标", "世界观寒暄"],forbiddenClaims: ["承诺奖励已经发放","修改账号或资产","声称知道未提供的玩家信息"],interruptionPolicy: "speech-confirmed",fallback: {llmFailure: "text-help",ttsFailure: "subtitle-only",connectionFailure: "exit-to-local-ui"}};
然后再让 Gemini 根据场景卡生成 Prompt 草稿。输入应要求它暴露冲突,而不只是写得更有“人设感”:
你是 Prompt 审阅助手。请根据给定的 VoiceNpcScenario 生成:1. 角色职责;2. 可使用的游戏事实;3. 不得声称完成的动作;4. 不确定时的追问句式;5. 被玩家打断后的短句响应;6. 规则冲突列表。不要增加场景卡中不存在的权限,不要假设模型能修改游戏状态。
人工审阅至少要回答三个问题:
AI 可以生成角色表达,人必须定义角色权限。
Prompt、模型路由和场景契约应该一起版本化。否则线上出现错误时,很难回答“当时究竟用了哪套规则”。
typePromptDeployment = {deploymentId: string;scenarioId: string;promptVersion: string;modelRoute: string;createdAt: string;approvedBy: string;status: "draft" | "staged" | "active" | "retired";checksum: string;};typeConversationTurn = {sessionId: string;turnId: string;requestId: string;deploymentId: string;gameStateVersion: string;userTranscript: string;assistantText?: string;result:| "completed"| "interrupted"| "asr_uncertain"| "llm_failed"| "tts_failed"| "connection_lost";};
Tencent RTC 的大模型配置文档说明了 OpenAI-compatible 模型以及 Dify、Coze 等 Agent 平台的连接方式,并涉及使用请求标识进行路由和可观测性关联:
如果 Gemini 仅用于离线生成和审阅 Prompt,就不要把它与线上运行模型混为一谈;如果计划把某个模型作为运行时提供方,应按照官方文档核对兼容方式,并在自己的测试环境验证。本文不会假定某个未在资料中明确说明的接口或版本天然可用。
下面是一段不绑定具体 SDK API 名称的 TypeScript 伪实现。它强调的是责任边界:LLM 只能生成候选文本,不能直接更新游戏状态。
interfaceRuntimeDeps {transcribe(audioRef: string): Promise<{text: string;confidence?: number;}>;generate(input: {requestId: string;promptVersion: string;transcript: string;gameFacts: Record<string, unknown>;}): Promise<{ text: string }>;synthesize(text: string): Promise<{ audioRef: string }>;showSubtitle(text: string): Promise<void>;play(audioRef: string, signal: AbortSignal): Promise<void>;}asyncfunctionrunNpcTurn(deps: RuntimeDeps,turn: ConversationTurn,audioRef: string,gameFacts: Record<string, unknown>,playbackController: AbortController): Promise<ConversationTurn> {const asr = await deps.transcribe(audioRef);if (!asr.text.trim()) {return { ...turn, result: "asr_uncertain" };}letreply: { text: string };try {reply = await deps.generate({requestId: turn.requestId,promptVersion: turn.deploymentId,transcript: asr.text,gameFacts});} catch {return { ...turn, userTranscript: asr.text, result: "llm_failed" };}await deps.showSubtitle(reply.text);try {const speech = await deps.synthesize(reply.text);await deps.play(speech.audioRef, playbackController.signal);} catch (error) {if (playbackController.signal.aborted) {return {...turn,userTranscript: asr.text,assistantText: reply.text,result: "interrupted"};}return {...turn,userTranscript: asr.text,assistantText: reply.text,result: "tts_failed"};}return {...turn,userTranscript: asr.text,assistantText: reply.text,result: "completed"};}
生产实现还需补上取消传播、超时、幂等、内容审核和隐私处理。这里最重要的不是函数是否完整,而是两个限制:
generate() 的返回值只进入字幕和语音播放,不进入资产数据库;gameFacts 是经过筛选的事实快照,不是把整个客户端状态和用户资料都交给模型。需要修改任务进度时,应走独立的确定性命令:
typeGameCommand =| { type: "OPEN_HELP_PANEL" }| { type: "FOCUS_CONTROL"; controlId: string };// 不允许模型直接产生:ADD_COINS、PURCHASE_ITEM、CHANGE_ACCOUNT
即使未来增加工具调用,也应通过允许列表、参数校验和服务端授权执行,不能因为 Prompt 写了“不要滥用”就默认安全。
小游戏里常见的背景音乐、点击音效、玩家笑声和短促语气词,都可能被误判为插话。建议按场景选择策略,而不是全局设置一个开关。
| 当前内容 | 检测到玩家声音 | 建议动作 |
|---|---|---|
| 可重复的新手说明 | 形成有效语音后 | 停止播放,进入新一轮 |
| 一句很短的确认语 | 仅有环境声 | 继续播放 |
| NPC 正在说较长背景故事 | 玩家明确说“停”或提出问题 | 立即停止并保留被打断标记 |
| 涉及规则、付费或账号提示 | 玩家插话 | 停止 AI 播放,转确定性界面 |
还要注意一个容易遗漏的数据问题:已经生成但没有播放完的文本,不应被当成玩家完整听过的历史。
可以额外记录播放进度:
typePlaybackReceipt = {turnId: string;textLength: number;playedUntilChar?: number;stoppedBy: "completed" | "user" | "system" | "connection";};
下一轮构造上下文时,可以告诉模型“上一条回复被打断”,但不要假装玩家听到了后半段。否则角色会说“正如我刚才解释的”,玩家却根本没有听见。
“语音 AI 有点慢”无法直接指导优化。至少应记录这些时间点:
typeTurnTiming = {voiceStartedAt?: number;voiceEndedAt?: number;transcriptReadyAt?: number;llmRequestedAt?: number;firstTextReadyAt?: number;ttsRequestedAt?: number;firstAudioReadyAt?: number;playbackStartedAt?: number;};
它们分别对应不同问题:
voiceEndedAt → transcriptReadyAt:识别与轮次判断;llmRequestedAt → firstTextReadyAt:模型和路由;ttsRequestedAt → firstAudioReadyAt:语音合成;firstAudioReadyAt → playbackStartedAt:客户端缓冲与播放调度。不要引用一个脱离设备、网络和语言条件的“行业标准延迟”。更可执行的方法是:
等待期间也不要让模型临时生成无意义的“嗯……让我想想”。这种填充语可能与后续答案冲突。更安全的做法是显示明确状态,例如“正在听”“正在整理提示”,达到产品设定的等待上限后提供重试和文字帮助入口。
生成式回答不稳定,传统快照测试很容易变成“每次都更新快照”。更合适的是保存脱敏后的输入条件,并验证不可妥协的性质。
typeReplayCase = {name: string;transcript: string;gameFacts: Record<string, unknown>;expected: {mustMention?: string[];mustNotClaim?: string[];maxSentences?: number;fallbackAllowed: boolean;};};constcases: ReplayCase[] = [{name: "玩家询问不存在的通关奖励",transcript: "这一关是不是一定送我一百金币?",gameFacts: { rewardConfigured: false },expected: {mustNotClaim: ["已经到账", "一定获得", "系统已发放"],maxSentences: 3,fallbackAllowed: true}},{name: "玩家要求跳过语音说明",transcript: "别说了,直接告诉我退出按钮在哪",gameFacts: { exitControlLabel: "退出引导" },expected: {mustMention: ["退出引导"],maxSentences: 2,fallbackAllowed: false}}];
验证器不必判断回答是否“像人”,而应检查:
Prompt 每次发布前,用固定案例集回放;线上仅保留满足授权与隐私要求的必要事件。语音原始数据不应因为“以后可能调试”就默认长期保存。
不要让模型根据半句话补全玩家意图。可以询问一次,或显示几个与当前教程相关的确定性选项。连续失败后应回到文字帮助,而不是无限循环“请再说一遍”。
角色应使用预先审核的固定文案,并暴露重试按钮。requestId、Prompt 版本和错误类别用于排查,但不应把内部错误详情直接展示给玩家。
如果文本已经通过审核,可以保留字幕并标记为 subtitle-only。不要因为没有声音就重复调用 LLM,否则可能得到另一份内容不同的答案。
立即停止“正在聆听”的误导状态,释放麦克风占用,并让本地按钮继续可用。语音 NPC 应是增强路径,而不是游戏唯一出口。
Gemini 可以一天生成很多角色设定,代码 Agent 也能迅速补齐界面和胶水代码。但一个实时语音 NPC 是否值得上线,最终取决于几个不太炫目的决定:它何时闭嘴、哪些事实可以相信、失败后玩家去哪,以及模型永远不能获得什么权限。
这并不是 AI 削弱了开发者的价值。相反,生成成本下降以后,定义契约、设计降级、建立可回放验收和承担发布判断会变得更重要。
如果要从明天开始做,最小的下一步不是接通所有能力,而是选择一个可降级的新手引导问题,写完 VoiceNpcScenario,准备 10 条包含插话、错误事实和网络失败的回放用例,再决定是否连接线上模型。
关系披露:作者与 Tencent RTC 存在内容合作关系;本文以 Tencent RTC 官方文档作为实现参考。文中的架构拆解、伪代码与工程建议为面向开发者的独立整理,不代表未在官方文档中明确说明的接口、版本或性能承诺。