ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

实时AI客服背后:文本工作流与RelayRouter如何撑起Gemini Live Avatar

实时AI客服背后:文本工作流与RelayRouter如何撑起Gemini Live Avatar

前几周我把一套基于 Gemini Live Avatar 的实时客服原型推给运营团队看,他们第一反应几乎都是:“这不就是把聊天框升级成了视频通话吗?”我不能说这个判断完全错,但它让我意识到一个很普遍的误解——实时AI的价值正在被“视频”这个表象带偏。真正把这个原型扛住的,根本不是哪张虚拟人脸,而是背后一条看不见的文本工作流。用户屏幕上看到的每一个点头、每一次停顿、每一句插话,都是后端某个文本事件在特定时刻触发的结果。那个时刻里,究竟是谁接收文本?谁做判断?谁调用工具?谁把结果拼成下一句话?这些问题最终都指向一个常被忽略的角色——RelayRouter。

我把 RelayRouter 放在 Gemini Live Avatar 和业务系统之间,作为一个轻量的消息路由层。这篇文章会拆开聊聊:Live Avatar 在实时链路里到底承担什么,为什么说文本工作流才是它的主心骨,以及我在接入 Gemini 实时能力过程中踩过的账号资格、上下文管理、状态一致性这些坑。无论你是准备做语音客服、实时教育助手,还是想给现有产品加一个“会说话的入口”,这篇内容的思路都可以直接复用。

1. Live Avatar不是“视频聊天”,是“有表情的文本管道”

很多人会把 Live Avatar 理解成“一个会说话的数字人”。从体验上看确实如此,但从工程链路看,它更接近一个表现层组件:接收已经产生好的文本或多模态内容,把内容变成表情、嘴型和语音。一句话,Avatar 负责的是“表演”,真正决定说什么、做什么、查什么的,是上游的文本工作流。把这个关系搞反了,后面所有架构都会跟着歪。

1.1 我最早的理解错在哪

我们第一版 Demo 的代码非常直接,就是把用户语音转成文本,丢给 Gemini 模型,拿到回复再交给 Avatar 说出来。这样做在演示环境里完全能跑通,可一旦进入真实业务场景就立刻露馅。

  • 用户问“这个品种有现货吗”,模型会一本正经地回答“请查看官网了解可用性”,因为它根本不知道我们的库存数据。
  • 用户连续追问细节时,模型把前几轮提到的商品参数忘得一干二净,因为我们只是简单地把历史消息拼接在一起。
  • Avatar 在模型思考的 5 秒里完全没有反应,只能干瞪眼,用户还以为系统卡死了。

这三个问题没有任何一个跟“视频聊天”有关系,全部是文本工作流的缺陷。我们做的所谓“实时”,只是把对话的传输协议变成了流式,并没有让业务逻辑在流式过程中实时流转。说白了,我实现了“实时传输”,但没有实现“实时处理”。

1.2 Live Avatar链路里的文本状态才是主心骨

如果我们把一次 Live Avatar 会话拆成端到端的消息链路,会发现它其实是一个不断循环的文本—多模态事件管道:

  1. 用户语音进入音频输入流。
  2. 语音被转写或语义理解,变成一段文本事件。
  3. 业务层结合上下文、工具结果、知识库,生成下一段回复内容。
  4. 回复内容再转成语音事件和表情事件,交给 Avatar 渲染。

在这个链路里,第 1、2、4 步都可以由现成的多模态能力覆盖,唯独第 3 步是一个纯粹的文本加结构化数据处理过程。Live Avatar 可以把表情做得再自然,也没法让一个不知道库存数据的 Gemini 准确说出“还有 3 台现货”。库存信息必须由文本工作流在恰当的时间点注入到模型上下文里。所以我的结论是:Live Avatar 提供的不是“智能”,而是“智能的表现形式”。智能本身必须有一个文本工作流去承接,而这个工作流里最关键的调度者,就是 RelayRouter。

1.3 视频和语音只是前端渲染,背后是双向文本流

工程实现上,一次实时会话并不是简单的 HTTP 请求—响应,而是一条持续存在的双向流。客户端不停发送事件:文本帧、音频帧、工具调用结果;服务端也不停返回事件:中间思考、文本增量、语音合成、表情控制。Avatar 前端只是按事件去更新画面,它本身不参与任何业务判断。

这带来一个关键约束:你不能等整段话生成完再一次性推给 Avatar,那样延迟会让用户看到一张“嘴型对不上”的脸。你必须把文本拆成更小的事件单元,按顺序分批次送进去。而“拆事件、排事件、把业务返回放到事件流正确位置”这个动作,正是文本工作流的核心。RelayRouter 在这个位置扮演的,就是整个事件流的调度中枢。

2. Gemini实时能力的接入门槛:账号资格、登录和第一个文本会话

前面讲的是架构认知,很多开发者也认同,但实际动手第一步就被卡住。我一开始在 MacBook 上装好开发环境,第一次用开发者后台去启用 Gemini 相关能力,就撞上了网上大量出现的提示:your account is not eligible for Gemini Code Assist for individuals at this time。这种报错非常磨人,因为它发生在“你明明能登录 Gemini,却没有权限调 API”的中间地带。

2.1 还没开始写代码,卡在“not eligible”

我一开始以为是自己的账号余额或区域问题,反复检查才发现,Gemini 登录态和开发者计划的资格是两套东西。这里分享一个我认为最实用的排查顺序,按这个顺序查,基本十分钟内能定位:

  • 先确认登录的账号类型。普通用户账号、个人开发者账号、工作区账号的权限边界完全不同,拿普通 Gemini App 的账号去访问 Code Assist 或 Live API 是经常出现的误区。
  • 再确认产品是否对个人版开放。Gemini Code Assist for individuals 有自己的支持范围,它和你是否能用 Gemini Live API 并不共享同一套资格判断。
  • 最后去官方开发者控制台看配额和权限列表,那里会清楚列出当前账号可以启用哪些 API、哪些接口显示不可用。

这个过程很无趣,但值得认真走一遍。否则你后面调接口时遇到 403 或 quota exceeded,第一反应会去怀疑 Token 或代码,实际却是账号层的问题。还有一个容易忽略的细节:在 MacBook 上安装 Gemini 客户端时如果反复登录失败、转圈、报错,先检查下载版本和官方安装说明,很多跟账号区域支持有关,别急着在代码里找原因。

提示:所有“账号明明存在,就是没权限”的报错,优先检查开发者计划、产品支持范围、API 配额这三项。通常答案就在其中一项。

2.2 真正的实时会话从建立双向流开始

账号问题解决后,我写了第一个实时文本会话。和传统补全接口最大的区别是:Gemini Live API 不是发一次请求收一次响应,而是先建立一个长连接,再在上面持续收发事件。如果你还停留在“post 一个 prompt、等一个 response”的思维,实时场景会非常别扭。

下面是我在路由层里的一个示意结构,真实项目在此基础上做了精简:

# 示意代码:RelayRouter 创建实时文本会话 from relay_router import RelayRouter router = RelayRouter() def on_text(text: str): print("avatar intermediate:", text) def on_tool_call(call): result = execute_business_tool(call.payload) session.send_tool_result(call.id, result) session = router.create_live_session( model="gemini-live", system_prompt="你是售前助手,回答前先判断是否需要查询库存工具。", handlers={"on_text": on_text, "on_tool_call": on_tool_call} ) session.connect() session.send_text("我想知道 A 款无人机现有库存") # 后续事件会通过 on_text / on_tool_call 回流

这段代码是我基于实际项目简化出来的,不是官方 SDK 的完整调用,但业务形态是共通的:会话建立后,所有内容都变成事件流。你不需要等待“完整回复”,而是需要在事件流里判断什么时候该说话、什么时候该调用工具、什么时候该保持安静。这些判断如果全堆在业务代码里,代码会快速膨胀到没法维护。这也是我后来把 RelayRouter 独立出来的直接原因。

2.3 第一版Demo最大的坑:把互动做成了“你问我答”

第一版 Demo 跑通后,我以为已经完成了 80%。结果运营团队一试用就提了个需求:“用户说‘等一下,我想先看看规格’,然后紧接着又说‘还是直接下单吧’,你们那个 Avatar 居然还在念上一句废话。”

这是个非常典型的实时问题:用户随时会打断、换话题、补条件。如果文本工作流仍然按“用户问题→模型回答”的顺序执行,中间一旦插进一个新事件,整个流程就会错位。正确做法是让事件流支持抢占和插入:用户新开了一句,路由器要把当前正在生成的回答降级为“可取消”,把新问题排到更高优先级,同时更新会话状态。Avatar 此时应该闭嘴听用户说,而不是继续把上一轮还没念完的结果硬念下去。文本工作流必须有一个能接收“打断事件”的组件,这恰恰是普通聊天框架不会给你的东西。

3. RelayRouter在文本工作流里到底扛了哪三件大事

你可能会问:RelayRouter 到底是什么?它不是一个现成的商业产品,也不是某种神秘框架,而是一个思路:在交互层和模型层之间,加一个专门处理文本消息的路由层。这个名字是我在项目里自己起的,核心职责有三件:组织上下文、做路由决策、维护会话状态。这三件事听起来不复杂,但每件事在实时场景里都比普通聊天要麻烦一个量级。

3.1 上下文窗口的组织与压缩

实时会话和普通聊天最大的差异是持续时间。用户可能和 Avatar 聊上 15 分钟,期间产生 40 轮对话、5 次工具调用。Gemini 的输入上下文有限,不可能全量塞进去。早期我试过最粗暴的方案:把历史消息拼成一个大字符串,塞到系统指令里。结果就是第 8 分钟开始,模型突然忘记用户在第 2 分钟提过的偏好。

后来我在 RelayRouter 里做了一套分层上下文管理:

  • 短期记忆:最近 10 轮的消息完整保留,这是当前话题的“现场”。
  • 长期记忆:更早的对话每 10 轮压缩一次,只保留决策和偏好,不保留每句话的原文。
  • 工具结果:不保留原始 JSON,只保留结构化结论,比如“查询订单 #A1001 状态:已发货,预计明天到达”。

实现上大致是这样:

def build_context(session): recent = session.history[-10:] old = compress_old_turns(session.history[:-10]) tool_summary = summarize_tool_results(session.tool_log) return [ {"role": "system", "content": f"早期摘要:{old}"}, {"role": "system", "content": f"工具记录:{tool_summary}"}, *recent, ]

这个设计的本质,是把“模型能看到的文本”看作一条供应链:不是所有货物都堆在货架上,而是把高频货放在最近的位置,把低频货压缩成标签。上下文压缩也不是简单按字数截断,我建议在路由层用一个小模型专门做摘要,让主模型只处理最近上下文。多花一点算力,换来的是长时间的稳定记忆,非常值得。

3.2 多模型路由与工具调用决策

不是所有问题都要请出最强的模型。实时场景里,很大一部分请求是闲聊、FAQ、简单状态查询。如果每次都把完整上下文送给 Gemini 的实时模型,成本和响应时间都会失控。RelayRouter 的做法是加一道轻量前置路由:

  • 用户输入进来后,先用一个快速分类器判断意图。
  • 闲聊类问题直接走轻量回复模板或小模型,不需要动用实时大模型。
  • 涉及业务数据的问题,先触发工具调用,把工具结果合并后交给 Gemini 生成最终话术。
  • 遇到需要多步推理的问题,路由器负责拆解成子任务,逐步喂养给模型。

工具调用这里有个特别容易踩的坑:不要把工具调用逻辑写在模型返回里。模型返回“我需要调用 get_stock()”,你的代码还要解析函数名、找参数、再执行,这套流程在实时场景里太慢。我的做法是,RelayRouter 在把消息发给模型之前,先根据意图直接执行工具,把结果作为上下文的一部分提供给模型。模型的任务从“想象一个查询并描述”变成“根据已查询到的库存数据来回答”,输出质量会稳定很多。

3.3 会话状态与实时消息一致性

最容易被忽略的是状态管理。WebSocket 长连接在真实网络环境下会断、会重连、会消息乱序。一旦连接断了,模型不知道现在进行到哪一步,用户却以为对话还在继续。RelayRouter 必须用一张会话状态表记录当前阶段、最近一次工具调用 ID、已经发送给 Avatar 的最后事件 ID、等待模型响应的队列。这张表大致长这样:

状态字段作用
session_id一次实时对话的唯一 ID
stage当前阶段:listening / processing / waiting_tool / speaking
last_event_id最近一次发送给 Avatar 的事件 ID,用于去重
pending_tool_calls已触发但结果还没注入上下文的工具调用列表

有了状态表,重连之后就能把未完成的流程继续下去。实际项目里,重连时最容易丢的是工具结果:模型已经调了工具,工具也返回了,但服务端还没来得及把结果注入上下文就断线了。RelayRouter 要做的,是在重连后把未消费的工具结果重新发给模型,并标记为已处理。这类似于数据库里的“事务补偿”,只不过我们补偿的是文本上下文。

4. 从能聊到能用:实时AI接入文本工作流的几个关键优化

“能聊”和“能用”之间,隔着一大堆产品级细节。我挑几个对体验改善最明显的优化点展开说说。

4.1 文本事件不宜用“最终回答”直接驱动Avatar

很多教程会让程序把最终回答字符串整个传给 Live Avatar,结果就是 Avatar 在生成完整句子之前毫无反应,生成后一口气读出来,听起来非常机械。我们后来把所有输出拆成事件类型,路由器和 Avatar 前端约定一套简单协议:

事件作用触发方式
turn_start用户发言开始收到语音或文本输入后立即触发
ack告诉用户“我在听/我收到了”RelayRouter 确认事件进入队列时触发
thinking让 Avatar 做出思考表情模型已连接但还未产出首字时触发
tool_call静默执行工具意图识别命中工具时触发
text_partial文本增量模型流式输出时触发
audio_ready语音合成片段TTS 输出可用时触发

这样做的好处是,实时链路里每个状态变化都能第一时间反映到 Avatar 画面上。尤其是 tool_call 事件,在等待库存接口返回时,可以让 Avatar 低头“看一下系统”,而不是干站着。这个体验差异,是从 Demo 到产品最关键的一步。

4.2 延迟预算拆开算,别只看端到端

做实时 AI 最容易犯的错误,是只盯着“从用户说完到 Avatar 完整回答”的总耗时。总耗时会掩盖真正的问题。我习惯把延迟拆成三段:

  1. 首反馈时间:用户说完后,Avatar 第一次给出确定性反应,哪怕只是点头或说“嗯”。这个时间必须低于 600ms,否则用户会怀疑设备坏了。
  2. 工具结果时间:需要调用业务系统时,从用户提问到工具数据回到路由器的时间。这个时间取决于下游系统,AI 层面能优化的空间很有限。
  3. 完整回答时间:模型流式生成加语音合成的全过程,通常 3 到 8 秒可以接受。

RelayRouter 在这三段里的任务不一样。第一段要尽快发 ack 事件,不让用户空等;第二段要做并发预取,比如用户说“帮我查物流”时,路由器提前判断需要调用物流接口,在语音识别还没完全结束时就开始查询;第三段要让模型增量生成,同时把已完成的句子立刻交给 TTS,不必等整段回答完毕。这套打法可以把“等待 3 秒”变成“1 秒有反应,3 秒有结果”,体验完全不同。

4.3 别所有流量都走实时视频,降级机制很必要

从成本角度看,实时对话会比普通文本对话贵不少。尤其是客服场景里,大量请求是重复 FAQ,完全没有必要动用完整的 Live Avatar。我在 RelayRouter 里加了一条降级策略:

  • 非业务闲聊:普通文本模型足够,不启动多模态会话。
  • 简单查单:启动实时会话,但关闭视频和表情流,只保留语音和文本。
  • 复杂导购或多轮讲解:才完整打开 Avatar 能力。

这条策略还可以写成阈值规则:当当前并发数超过某个值,自动关闭非核心流。实时 AI 不是永远要拉满,能用最低成本完成交互的地方,就不必让用户看到一张会点头的脸。这个判断也由文本工作流来做,因为只有它同时掌握意图和上下文。

4.4 可观测性:把每次实时链路录下来

实时工作流排障非常痛苦,因为问题经常发生在“模型吐到一半、工具返回超时、TTS 队列积压”的交界处。我在第一版就吃过亏:用户反馈“有时候 Avatar 会重复说一句话”,我完全没法复现。后来给 RelayRouter 加了 trace 日志,记录每一条文本事件的产生时间、来源、处理耗时、发送目标,才定位到是重连机制把同一条事件重复推给了 TTS。所以实时链路一定要从第一天就保留文本事件轨迹,不要等出 bug 再补。

5. 把业务接进来:一个售前接待案例和三个踩坑教训

最后用一个实际案例把这些串起来。假设用户问:“我想定制一台无人机,续航要超过 40 分钟。”背后的 RelayRouter 流程大致是:

  1. 用户语音进入实时会话,转写成文本事件。
  2. 路由器做意图分类,识别出“定制咨询”和关键约束“续航大于 40 分钟”。
  3. 查询可选配置和报价,把结果压缩成一段结构化文本。
  4. 把结构化文本注入 Gemini 实时模型上下文,让模型基于真实数据组织话术。
  5. 模型流式生成回答,路由器把增量文本拆成 text_partial 事件推给 Avatar。
  6. Avatar 同步说话、点头、展示表情。

整个过程中,Live Avatar 唯一做的事是第 6 步,前五步全部发生在文本工作流里。这也是我在开篇反复强调的:视频不是那个让实时 AI“能用”的东西,文本工作流才是。

做完这个项目,我总结出三个值得记住的坑。

坑一:上下文压缩不能只按 token 截断。我第一次压缩上下文时直接取最近 N 条,导致模型在某个客服对话里忘了用户是企业客户,报了一个不给批发的价格。后来改成“先摘要旧内容,再保留最近轮次”,才稳定下来。

坑二:不要把音频流和文本流做强同步。早期我想让 Avatar 的嘴型严格跟随文本断句,结果 TTS 延迟一抖动,整个界面卡顿得让人想砸电脑。音频流和文本流是两个独立通道,谁先到就先播谁,慢的那条做淡入,不做强制等待。

坑三:工具结果的注入位置不能乱。有一次我们把工具结果放在用户消息后面,Gemini 把“工具输出”也当成了用户昵称的一部分,回答完全跑偏。工具结果应该单独作为 system 或 function 消息注入,并明确标注来源。

我现在越来越觉得,“实时 AI”这四个字里,真正有挑战的是“实时工作流”,而不是“AI”。Live Avatar 把用户的期待抬得很高,后端却还是一堆散落的数据库、订单接口和文档。RelayRouter 的位置,恰恰就是夹在这两者之间,接住高期待,再把凌乱的后端按正确顺序输送给模型。它不是一个炫酷的前端组件,而是一个不怎么上镜但决定成败的文本调度层。

最后分享一个我至今还在用的小技巧:测试实时 AI 时,别只盯着录屏和对话记录看,一定要给每一条文本事件盖时间戳。一旦用户反馈“某个回答慢了”或“说了重复的话”,你能直接看到是哪一段链路慢、哪一条事件被重发,而不是对着空气猜。这个动作成本极低,但对实时系统的可维护性帮助极大。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进