【Pipecat】一个能对话、会用工具的语音 Agent,是怎么工作的?

背景

上一篇《【Pipecat】基于 Pipecat 的 Voice Agent 实践》里,介绍了参考 Tana 做会议助手 MVP 的过程。从前端的卡片布局,到让 Agent 听我们说话、看共享屏幕,再接上创建任务和修改文档的工具,先把这几个功能跑了起来。当时也提到,后面会单开一篇文章,讲讲 Pipecat 的核心架构和原理,这篇就接着往下写。

前一篇主要是按照实现过程来讲,很多地方只介绍了怎么接。比如把 STT、LLM、TTS 放进 Pipeline,Agent 就可以和我们对话了,但这些节点到底是怎么配合的?模型还没生成完,为什么就能开始说话?用户中途插一句,已经生成的文字和音频又该怎么处理?这些问题都需要往框架里面看。

所以这篇会从 Pipecat 的 Pipeline、Frame 和 Processor 开始,顺着一段查询任务的对话,看看数据怎么流动,再逐步加入停顿、打断、记忆和工具调用。没有看过前面的实践也没关系,我们先从 Pipecat 是什么开始。

Pipecat 介绍

Pipecat 是什么

官方是这么介绍 Pipecat 的:

Pipecat is an open source Python framework for building voice and multimodal AI agents.

翻译一下,就是一个用来构建语音和多模态 AI Agent 的开源 Python 框架。官方介绍

语音比较好理解,就是我们说话,Agent 通过语音回答。那多模态呢?比如我们共享一个页面,问它"这个页面有什么问题",它就需要结合我们说的话和页面画面来回答。Pipecat 负责把这些输入接进来,交给对应的模型处理,再把结果输出给用户。

源码和示例维护在 GitHub 的 pipecat-ai/pipecat 仓库,使用 BSD 2-Clause 许可证,可以自行部署。几个常用入口放在这里:官网官方文档GitHub 仓库许可证

Pipecat 能做哪些事情

最容易想到的就是语音问答,我们说一句,Agent 回答一句。把这种交互放到具体的业务里,可以做的事情就多了。比如:

  • 电话客服:用户打电话来问"我的订单到哪了",Agent 可以继续询问订单信息,调用查询接口,再把物流进度说给用户听。
  • 会议助手:听大家讨论,回答会议中的问题。接上任务和文档工具后,还可以根据讨论整理待办、提出文档修改建议,交给参会人确认。
  • 车载助手:通过语音查询目的地、查找沿途的充电站,或者控制音乐播放。这些操作需要接入地图、车辆或媒体服务提供的接口。
  • 陪伴对话:日常聊天、讲故事、口语陪练。用户可以接着上一句话追问,也可以在 Agent 说话时打断,换一个话题。

这些场景都可以从语音对话开始,再接入各自需要的数据和工具。Pipecat 提供对话链路,订单查询、会议任务、车载接口这些业务功能还需要我们自己接。官方 Recipes 中可以找到工具调用、打断和视觉输入等基础能力的示例。

与常见方案的对比

比较这些方案,可以先看它们怎么组织一次对话。比如要在识别结果和模型之间加一道过滤,或者在用户插话时停止旧回复,框架把这些处理放在哪里?

方案 主要组织方式 接入时需要考虑的部分
Pipecat 组合 Pipeline、Processor、Service 和 Transport 可以逐段加入处理逻辑;实例运行、资源回收和业务接口需要维护
LiveKit Agents 围绕 Agent、AgentSession 和媒体会话组织 需要结合房间模型理解会话、轮次和工具的组织方式
TEN Framework 通过扩展组件和配置组织实时多模态 Agent 需要理解扩展之间的连接、运行方式与媒体接入
托管对话式引擎 在平台上配置模型、工具和会话 验证自定义流程、日志可见性及与现有媒体的兼容性

语音对话为什么需要一套处理框架

只调几个接口,能不能完成对话

假设用户问:"登录问题现在谁在处理?"麦克风收到的是声音,要让 Agent 回答,需要经过几个环节:

  • STT(Speech-to-Text,语音识别)把音频转成文字。
  • LLM(大语言模型)结合上下文理解问题,需要时调用工具,再组织回答。
  • TTS(Text-to-Speech,语音合成)把回答转成声音。

把这些环节分别组合,叫做级联链路。各段可以选择自己的服务,出了问题也可以分别观察。另一类 Speech-to-Speech 模型直接接收和输出音频,中间未必有独立的 STT、LLM、TTS;本文先沿级联方式展开。

以 WebRTC 接入为例,各部分的关系是这样的:

客户端采集和播放声音,Transport 对接媒体输入输出,Pipeline 内的服务适配连接识别、推理和合成服务,工具调用则由应用执行。

Pipecat 的核心结构

为什么用 Frame 表达数据

比如"登录问题现在谁在处理"这句话,是用户的提问;"由小林处理"是模型的回答。它们都是字符串,接下来的处理却不同:前者要交给模型,后者要拿去朗读。

再加上声音、图片和 "停止上一轮" 这样的通知,只靠内容本身就很难判断该做什么。

Pipecat 用 Frame 给信息加上类型。音频、识别文字、模型回复、上下文和打断事件,都可以用不同的 Frame 表达。各类 Frame 根据需要携带采样率、说话人、时间等字段,并非每种类型都有同样的字段。

Frame 是链路里传递的带类型消息,通常表现为 Python 内存对象。 这里的"帧"不只指一张视频画面,也不要求与一个网络包对应。

Frame 的粒度由类型和服务适配决定:一段音频可以拆成很多 Frame,完整上下文也可以放在一个 Frame 中;模型输出的文本 Frame 则可能只有几个字。Frame 定义

Frame 统一了节点之间的数据格式,减少更换服务对其他节点的影响。 比如更换语音识别服务,Service 适配层仍然输出约定的转写 Frame,后面的聚合器就可以继续处理。

Processor 负责处理,Pipeline 负责连接

现在能区分数据了,接下来是谁处理、先交给谁?

Pipecat 把处理拆成 Processor。STT 节点负责识别音频,TTS 节点负责合成声音;需要把零散信息攒起来的地方,再交给聚合器。

节点收到 Frame 后,可以继续转发、积累内容、产生新的 Frame,也可以按过滤规则停止某类内容继续传播。比如多个音频 Frame 可能得到一段识别文字,一段回答又可以产生很多音频 Frame。输入和输出无需一一对应。

Pipeline 把这些节点按需要连接起来。下面是一条普通语音问答链路,图里的英文名称对应各个处理节点:

节点位置决定它能处理什么。检查用户说了什么,适合放在 STT 后;调整要读出来的文字,适合放在 LLM 和 TTS 之间。每个节点遵循相同的 Frame 传递方式,就可以在原有链路中加入处理,而不用让所有模型服务互相认识。Pipeline 与 Frame Processing

Frame 是怎么流式处理的

Frame 交给下游以后,节点怎样继续运行

Pipeline 把处理节点连起来了,这又引发了一个问题:前一个节点交出 Frame 后,需要等下游全部处理完吗?

在默认队列模式下,一个 Processor 把 Frame 推给相邻节点,先进入对方的输入队列,再由对方的处理任务继续执行。交付完成,只说明 Frame 已经进入下游,不能当作下游已经处理完成。

比如第一段音频交给 STT 后,Transport 还能继续接收第二段;STT 等待远端识别时,其他处理任务也有机会运行。Pipecat 通过异步任务和队列,把交付与处理分开。源码也提供直接处理模式,这里讨论的是默认队列路径。FrameProcessor 源码

并发运行时,顺序要分层看:

观察范围 处理顺序与并发关系
单个 Processor 的普通处理通道 当前 Frame 的处理返回后,再取下一个
不同 Processor 之间 各自推进,等待异步 I/O 时让出执行机会,处理进度可以重叠
Processor 另外启动的后台请求 当前 Frame 处理返回,不代表请求已经结束;结果顺序需要服务适配或应用协调

所以看耗时时,需要把"交出 Frame"和"完成处理"分开记录。只看交付速度,容易漏掉下游队列里的等待。

输入侧:音频块怎样变成一次发言

我们说"登录问题现在谁在处理",采集端提供的是连续音频块。支持流式识别的 STT 逐步返回文字,再由用户聚合器整理成发言:

音频块 → 识别片段 → 用户聚合器 → 交给 LLM 的用户消息

这些片段未必是新增内容,识别服务还可能修订之前的结果。

流式转写为什么不能收到一段就追加

假设用户说"登录问题由小林处理",识别服务陆续返回下面的结果:

返回次序 识别文字 结果性质
第一次 登录问题由小李 中间结果,可能继续修订
第二次 登录问题由小林 修订同一片段,把"小李"改为"小林"
第三次 登录问题由小林处理 该片段的正式结果

这是同一片段的三个版本。全部追加进 Context,就会留下两个负责人,还把同一句话重复几遍。

Pipecat 用 InterimTranscriptionFrame 表达中间结果,用 TranscriptionFrame 表达正式转写。用户聚合器只将正式转写追加到发言文本。 自定义 STT 适配需要正确区分这两类结果,否则下游仍然会重复拼接。用户聚合器源码

正式转写只说明这段识别结果已经确定,用户是否说完,还需要轮次判断。

输出侧:模型还没写完,能不能先说第一句

模型开始回答后,输出侧同样要处理零散片段。假设它准备回答:

查到了登录后反复退出这个任务。负责人是小林。

如果等两句全部生成完,再合成整段音频,前面已经准备好的内容也得跟着等。第一句到达可合成的边界后,就可以先交给 TTS;模型继续生成第二句,输出端发送陆续返回的音频。

开始播放时,TTS 还在合成它,LLM 已经在生成绿色的第二段。图中只示意相对先后,条块长度不代表实测耗时,重叠能力取决于服务适配。模型生成结束、语音合成结束、客户端播放结束,是三个不同的时刻。 TTS 处理与音频队列

这里还有一个选择:文本攒到什么程度再合成?

模型返回的片段由生成接口决定,未必符合朗读习惯。把"负责""人是""小林"分别拿去合成,可能增加请求开销,也可能让语气和停顿不自然。Pipecat 提供文本聚合机制,可以按句子等边界组织合成;支持流式文本输入的 TTS 也可以采用更细的输入方式。TTS 文档

聚合粒度越大,一次合成能拿到的文字越多,开口前的等待也可能增加。我更在意等待和朗读是否连贯这两件事,单纯把片段分细,不一定让对话更自然。

模型正常结束时,缓冲区里没带句号的尾部文字也要交给 TTS;用户打断时则要清掉。这两种收尾,需要后面的控制事件来区分。

第一句合成慢,第二句能先播吗

如果合成请求并行执行,短句可能先返回。沿用刚才的回答,结果可能是:

文本顺序:① 查到了登录后反复退出这个任务。② 负责人是小林。

音频返回:② 先回来,① 后回来。

播放需要遵循文本顺序,不能直接采用请求的返回顺序。 可以先登记句段顺序,将第二段音频暂存,等第一段输出结束后再播放。第一段内部仍然可以边接收边输出,无需等整段下载完。

Pipecat 中,使用音频上下文管理的 TTS 路径将顺序控制拆成三个动作:

  1. 登记上下文: 先将音频上下文标识放进串行队列,确定消费顺序。
  2. 接收音频: 返回的音频进入对应上下文,不因先返回就越过前面的内容。
  3. 按序输出: 输出任务按登记顺序消费,一个上下文结束后再处理下一个。

这些动作可以交错运行,例如前一个上下文正在输出,后一个已经收到音频。同一上下文里的音频块,仍依赖服务适配提供正确顺序;并且这条路径并非所有 TTS 适配都会经过。TTS 处理与音频队列

如果应用自己增加并行合成,需要分别保留两个标识:回复标识 用于打断后判断结果是否失效,段序号用于正常播放时排序。自建请求需要自己接上这些处理,不能默认框架已经覆盖。

下游处理太慢,Frame 会不会堆起来

模型生成文字的速度,通常比语音播放快。如果持续生成,来不及播放的内容就会积压在队列里,占用内存,也让后面的回答等得更久。

队列可以暂存数据,但不能解决生产和消费速度不匹配的问题。 当积压过多时,需要让上游减慢或暂停生产,这种机制叫做背压。

Pipecat 的基础 Frame 队列默认不设容量上限,因此应用还需要根据待播时长等信息,限制后续生成或合成的内容,不能一直往队列里塞。FrameQueue 实现

待播内容越多,用户插话时要清理的旧工作也越多。如果"停止"消息也堵在同一个长队列里,限流并不能解决打断问题。这就需要区分普通数据和控制事件。

普通数据和控制事件为什么分开处理

打断不能排在旧回答后面

沿着刚才的积压继续看。用户说"等等",如果打断消息也排在那些待合成文字后面,等轮到它,旧回答可能已经说完了。

Pipecat 将 SystemFrame 放在高优先级输入通道中处理,普通数据和有序控制帧则进入普通处理队列。两边有各自的处理任务,打断因此有机会在普通任务等待时得到处理。

这里要区分"装了什么数据"和"按什么方式调度"。输入音频携带声音,但它也属于 SystemFrame;ControlFrame 虽然名字里有控制,却和普通数据在同一通道保持顺序,例如正常结束消息。分类要看 Frame 的定义。Frame 定义

高优先级也要有机会运行

把打断放到高优先级通道,并不代表它在任何时候都能立刻运行。它仍要遵守自身通道的顺序,也需要事件循环给它执行机会。

如果某个处理器执行耗时的同步请求,占住了事件循环,输入任务也没法凭空获得执行时间。高优先级解决排队顺序,协作调度决定任务什么时候有机会运行,两者需要一起看。FrameProcessor 源码

Frame 还可以向上游传播。下游检测到事件时,有时需要通知前面的节点停止工作;方向表示 Pipeline 内的传播关系,与网络的客户端、服务端方向没有固定对应关系。

正常结束和立即取消要区分

既然高优先级能少排队,结束消息也放进去行不行?

假设 Agent 需要先说"再见",再关闭连接。结束消息如果越过待播音频,告别语就可能没机会说完。用户中途取消则相反,我们希望它尽快停止。

Pipecat 用不同事件表达这些意图,让节点按语义处理剩余工作。否则一个统一的"停止"很容易出现两种问题:该说完的话被截掉,或者该取消的内容还在继续播放。Pipeline 结束机制

用户什么时候算说完

检测到静音,不等于这一轮结束

前面讲了事件怎么传,轮次结束事件又该什么时候发?比如用户这样说:

查一下登录问题......就是反复退出那个。

中间只是停下来想了一下,Agent 如果已经开始回答,就会漏掉后面的补充。要判断什么时候可以回答,先区分三个概念:

能力 回答的问题 不能直接据此得出的结论
VAD(Voice Activity Detection,人声活动检测) 音频中是否检测到人声 检测到静音,不代表用户已经说完
STT(语音识别) 用户说了哪些文字 一段转写确定,不代表用户不会继续补充
轮次检测 用户这轮发言是否可以结束 判断结束时,尾部转写仍可能在路上

VAD 不知道"查一下......"后面是不是还缺一个对象。轮次判断需要结合更多信息:有的策略使用文字,有的模型从音频的语调、节奏等信息预测是否说完,不能把它们都理解成在检查句号。语音输入与轮次检测

轮次判断怎样配合转写收尾

沿用上面的例子,可以把处理过程分开看:

  1. 出现停顿时,先判断是否结束。 VAD 报告停声,轮次策略仍可以继续等待。用户再次开口,就撤销之前待结束的判断,继续收集输入。
  2. 满足结束判断后,检查转写进度。 用户确实说完了,也要考虑最后几个字是否已经返回,否则交给模型的仍然可能只有前半句。

Pipecat 的 TurnAnalyzerUserTurnStopStrategy 在等待转写的模式下,会协调轮次模型的完成判断与转写到达情况。支持结束确认的服务可以通过 finalized 信号推进;没有该信号时,则结合已有文字和 STT 等待超时处理。轮次结束策略源码

上面分开的是两个判断,转写和轮次检测在运行中可以重叠。等待超时给迟到的转写留了时间,但不能保证返回的文字一定完整。

用户聚合器一边收集正式转写,一边跟进轮次状态。结束条件满足后,再将本轮文字写入 Context,交给 LLM。上下文聚合器源码

唤醒决定是否回应当前输入,轮次结束决定什么时候回答;多人场景还需要媒体和转写信息区分说话人。

验证策略时,我会用带自然停顿、临时补充和短回答的录音,先标出用户真正说完的位置,再检查三项结果:

  • 是否抢话:策略提前结束了多少次。
  • 等待多久:用户说完后,又等了多长时间才提交。
  • 文字是否完整:提交给模型时,有没有漏掉尾部内容。

只看响应变快,可能只是把轮次结束得更早。这里给出的是验证思路,本文没有对应的实测结果。

中途打断,旧输出怎么处理

顺着一次插话看处理过程

继续刚才的对话。Agent 正在说:"登录问题由小林处理,另外还有一个......"用户插话:"等等,只说登录这个。"

这时,"只说登录这个"要作为新输入继续处理,前一轮回答也要停下来。只做其中一边,对话都接不上。

在允许打断的配置下,输入端持续接收新音频,轮次策略判断用户开始发言,用户聚合器向上游、下游广播 InterruptionFrame。相关节点分别响应:

LLM 停止本地旧生成流,TTS 停止旧合成并清理待发内容,输出端清理本地未播音频。新的发言仍然继续收集,聚合完成后再发起下一轮推理。各分支可以分别响应,无需等某个服务完全停下后,其他节点才开始处理。打断机制

触发时机由策略决定,不要求先识别出"等等"这个词。"嗯""对"这样的附和是否算插话,则需要单独处理。

为什么取消模型以后,声音还在继续

模型可能生成了三句话,TTS 合成了两句,客户端只播到第一句。后面已经有内容了,模型停下并不会让这些声音跟着消失。

内容位置 可以怎样处理
仍在生成 停止本地等待和消费;远端能否立即取消取决于服务
已生成、还没合成 清理旧轮次待合成文字
已合成、留在服务端输出队列 丢弃旧轮次待播音频
已发到网络或设备缓冲 需要 Transport 和客户端配合停止、清缓冲
已经播放 用户已经听到,无法撤回

所以遇到"取消以后还多说了一小段",我会先看那段声音停留在哪里。服务端清空队列,处理不到设备已经缓存的音频。只盯着模型请求,很容易查错位置。音频输出处理

清理时为什么有些 Frame 不能丢

既然旧内容会残留,把队列全部清掉行不行?

旧音频可以作废,工具结果却可能对应一个已经完成的操作;结束消息还可能承担资源清理。一起丢掉,对话虽然安静了,执行结果和收尾处理也可能没了。

Pipecat 用 UninterruptibleFrame 标记需要保留的内容。打断时会清理可打断的排队内容,保留不能丢的 Frame;当前处理能否取消,也要看它的标记。工具结果和 EndFrame 都有相应的保留语义。FrameProcessor 源码

队列清空了,旧请求又返回怎么办

假设用户打断时,旧回复还有一段音频没有返回。服务端清空队列后开始新回复,旧音频却在这时到达,又被回调放进了输出队列。

清空队列只能处理已有内容,迟到的结果还需要按所属回复判断是否有效。 对于应用自己启动的后台请求,可以这样处理:

  1. 标记所属回复:为每次回复分配一个输出标识,文本、合成请求和返回音频都保留这个标识。它标识一次输出,与用户发言轮次、业务操作编号分开。
  2. 打断时失效旧回复:先将旧标识 A 标记为失效,再取消旧请求、清理待发内容。新回复使用新的标识 B。
  3. 拦截迟到结果:回调入队和实际输出前都检查标识。即使 A 的音频在取消后才返回,也不能混进 B 的播放队列。
  4. 处理端侧缓冲:已经发送到客户端的旧音频,需要客户端配合停止播放、清理缓冲,服务端的检查无法撤回这部分数据。

Pipecat 的部分 TTS 路径已有对应机制:打断时重置音频上下文和当前标识,追加结果时检查上下文是否可用,仅允许符合当前轮次条件的上下文重建。具体可以看 TTS 上下文与打断处理。应用自建的请求是否经过这些检查,仍需要单独核对。

这里我的理解是,输出失效和业务结果失效要分开处理:旧音频可以丢弃,但已经创建任务的结果需要保留。取消一次回复,不能把已经发生的业务操作也当成取消成功。

这也带出了下一个问题:声音停在半句,前一轮的对话记录应该保留到哪里?

多轮对话与记忆,为什么要跟输出配合

下一句里的"他"从哪里来

用户接着上一轮追问:

用户:登录问题现在谁在处理?> Agent:登录后反复退出这个任务,由小林处理。> 用户:那他预计什么时候能修好?

只把最后一句交给模型,就缺少"他"的指代。Pipecat 用 Context 保存会话消息、工具描述和调用结果 ,用户与助手聚合器共同更新这份上下文,供下一轮推理读取。上下文管理

查询结果中还要保留任务标识,后续才能继续查同一条任务的截止时间。

模型生成的内容,可以全部记进 Context 吗

模型已经生成"另一个任务由小周处理",用户却在播放前打断了。如果整段文字都存进 Context,下一轮模型就可能认为这条信息已经告诉过用户。

助手聚合器放在输出之后,是为了根据输出进度记录回复。 Pipecat 的 TTS 与输出处理结合文本分段、调度和可用时间戳推进文本,聚合器在打断时收束已收集的内容。文字与音频进度设计

应用侧可以将记录分为四类:

记录 能说明什么 打断以后怎样使用
已生成文本 模型准备说什么 可用于排查,不直接当作已经告诉用户
已发送音频及对应文本 内容推进到输出或传输的哪个位置 用于估计输出范围,不能冒充端侧播放确认
客户端已播放进度(如果具备) 播放器报告处理到了哪个音频位置 结合音频与文本对齐收束记录;仍不能证明用户实际听懂
工具结果与业务状态 操作是否完成、结果是什么 独立保留,不能按语音截断位置删除

例如任务创建成功,但通知还没播出来,就需要同时保留"任务已创建"和"用户可能尚未收到通知"两个状态,避免再次创建。

Pipecat 的输出进度不等于客户端播放确认。截断精度受服务时间戳和端侧缓存影响;应用若有播放回执,也要核对所属回复,避免旧回执更新新记录。

长对话怎么保留信息

按输出进度记下来的内容,继续聊也会越来越多。为了回答一句"截止时间呢",每次都带上很早以前的完整讨论,既花钱,也增加等待。

先保留最近几轮,把更早的内容整理成摘要,就能减少这部分输入。

Pipecat 提供可启用的上下文摘要机制。不过,我不会把任务 ID、用户确认这些信息只留在摘要里。摘要允许概括,业务执行需要准确字段,两者对"少一点细节"的容忍度不同。上下文摘要

原始记录可以另存,需要时再检索相关部分。保存历史和每轮都把全部历史塞给模型,可以分开做。

不过,到目前为止,我们一直假设"任务负责人是小林"已经查到了。这条业务数据究竟从哪里来?

工具调用怎么进入对话

模型提出调用,执行结果再回到模型

模型听懂了"登录问题现在谁在处理",也没法凭这句话知道任务系统里的最新负责人。它需要一次真正的查询。

应用先提供工具描述,说明能查什么、需要什么参数。以一次查询为例,工具调用会形成下面的往返:

模型选择查询工具 → 应用执行查询 → 结果写入 Context → 模型根据结果回答

这里会有前后两次推理:第一次决定查什么,查询返回任务和负责人后,后续推理再组织回答。模型负责提出调用,应用负责真正执行。 Function Calling

请求和结果通过工具调用标识关联。如果同时发起几个查询,返回结果也要分别对应原来的调用,不能随意拼在一起。

Pipecat 用相应 Frame 表达工具开始、执行中和结果返回,让聚合器把工具消息纳入 Context。**工具结果也是后续对话的输入。**用户继续问"什么时候完成",就可以沿用前一轮查到的任务。工具执行流程

MCP 解决工具的接入方式

业务工具既可以直接对接接口,也可以通过 MCP(Model Context Protocol)接入。Pipecat 的 MCP Client 获取工具描述,供模型选择,再向对应服务发起调用并返回结果MCPClient 源码

工具能被模型调用,不代表用户拥有操作权限。业务系统提供调用 Token,服务端根据凭据和资源范围校验权限。工具返回的网页、文档属于数据,不能自动变成系统指令。

对话被打断,已经执行的操作怎么办

用户说"帮我建一个跟进任务",请求发出后又改口"先别创建"。本地可以取消等待,但后端事务可能已经提交。本地取消不代表远端操作回滚。

应用需要在发送前确定业务操作标识,查询和重试同一操作时继续使用它,不能依赖每轮新生成的工具调用 ID。打断后的处理取决于执行状态:

打断时的状态 后续处理
可以确定请求尚未发出 停止发送,记录此次创建已取消
请求已发出,结果未知 用原操作标识查询或继续核查,不宣称取消成功
后端明确返回成功 记录任务 ID;用户仍要取消时,检查是否允许撤销
后端明确返回失败且未产生副作用 告知失败,再根据用户意图决定是否重试

结果未知时,先核查再决定下一步。暂时查不到可能是请求仍在处理或数据尚未可见;重试时,需要后端按幂等标识防止重复执行。

Pipecat 可以配置工具是否随打断取消,这控制的是本地调用生命周期。远端操作的最终状态,以及成功后能否撤销,仍由业务服务确认。工具取消与异步执行

Pipecat Flows 怎么组织分阶段对话

Pipeline 之外,为什么还要有 Flows

创建任务通常要先收集标题、负责人和时间,再确认并执行。只靠一段 Prompt,模型需要每轮判断当前进度,还可能跳过确认。Pipecat Flows 将这些步骤组织成对话阶段:

对比项 Pipeline Flows
组织对象 音频、文本和事件的处理路径 业务对话的阶段与转移
节点职责 识别、聚合、推理、合成等处理 配置当前提示、可用工具和转移规则
创建任务的例子 每轮发言都通过语音链路处理 根据进度收集信息、确认参数或执行创建

以创建任务为例:

收集标题、负责人和时间 → 核对参数并等待确认 → 执行创建

询问负责人时,只提供查询和选择人员的工具;参数确认后,再进入允许创建的阶段。不同阶段复用同一条语音 Pipeline,Flows 调整当前提示、可用工具和转移规则。 Flows 介绍

简单查询可以直接调用工具;有收集、确认、执行等阶段时,再用 Flows 管理。后端的参数和权限校验仍然保留。

改口时,状态怎么跟着变化

阶段拆开了,用户却不会总按顺序说话。假设参数已经确认,创建请求还没有发出,用户又补了一句:

标题改成"跟进登录后反复退出的问题"。

图里的红色分支回到确认阶段。标题变了,旧确认就不能直接使用;这时要更新状态,再让用户核对修改后的内容:

  1. 更新字段。 将新标题写入结构化 state,不受影响的负责人、时间继续保留;Context 记录这次改口的对话。
  2. 重新确认。 回到核对阶段,让用户确认更新后的参数。旧的同意只对应旧内容,不能直接用于新任务。
  3. 确认后再执行。 进入允许创建的阶段,后端仍然校验参数和权限。如果请求已经发出,则按前一节的执行状态核查,不能只退回节点就当作操作撤销了。

**Context 保存对话过程,结构化状态保存当前确定的字段。**切换节点时,可以按需要追加或重置上下文;需要长期保存的业务状态,应由应用持久化。Flows 上下文策略

兼容性范围:Pipecat Flows 面向支持 Function Calling 的文本 LLM;实时 Speech-to-Speech 服务有兼容性限制。接入时仍需核对所选模型与流程层的支持情况。

多模态输入怎样和语音对应

能收到画面,还要知道是哪一刻的画面

前面一直围绕任务系统的记录交流。用户也可能不报任务名称,直接共享页面,说"看一下这里为什么登录失败"。

这次缺少的上下文是一张画面。只有识别出来的文字,模型不知道"这里"指哪块区域。

Pipecat 可以通过图像相关 Frame 传递图片,再结合问题交给支持视觉的模型。采集授权、共享源选择和采样频率,需要媒体层与应用共同决定。视觉输入示例

图片进来了,还得看它是哪一刻的。用户问报错,送给模型的却是几秒前的正常页面,模型仍然会理解错。

按需取图可以围绕当前提问获取最近画面;持续观察则要考虑采样和重复内容。音频和图片都能用 Frame 传递,但时间和来源仍要保留,不能指望统一格式自动解决对齐。

输入也要按问题选择:看页面布局或报错位置,就给截图;查负责人、截止时间等确定字段,就用接口返回的结构化数据。

回头看 Pipecat 的设计

回过头来看,其实我上个月看完 Pipecat 之后,就觉得它和之前学过的 Netty 很像。Netty 用 ChannelPipeline 串起不同的 ChannelHandler,解码、编码和业务处理各自负责一部分,消息和事件沿着链路传递。Pipecat 的 Processor 和 Pipeline,也有这种拆分职责、组合处理的思路。Netty ChannelPipeline 文档

比如要过滤识别结果,可以在 STT 后面加一个 Processor;要调整朗读内容,就在 LLM 和 TTS 之间处理。框架提供处理规则和扩展位置,我们把自己的逻辑放进去。 这样再看 Pipeline,就容易理解它为什么要拆成这些节点。

最近的 DeepSeek Harness 也让我有类似的感觉。它强调"一切皆插件",模型、工具、会话和调度都可以通过插件组合,插件之间通过服务和事件协作。它和 Pipecat 的具体结构不同,但我觉得它们有一个共同点:把变化的能力拆成组件,通过约定好的接口协作,让开发者可以替换其中一部分。

这也是我看完 Pipecat 后比较有收获的地方。做 Voice Agent 时,除了选模型,还要考虑数据怎么传、状态由谁维护、打断以后哪些工作要停下来。这些能力有了明确的分工,后面增加功能、替换服务或者排查问题,才知道应该从哪里下手。

进一步思考的问题

前面介绍了一轮对话中的数据、事件和状态。下面补充一些接入和运行中的问题,区分 Pipecat 已有的能力与应用需要补充的设计。

自定义 STT,怎样处理修订和重连

接入识别服务时,还需要按服务协议处理字幕修订和重连后的重复结果:

  1. 更新字幕 :服务标出稳定片段时,可以保留稳定部分、更新尾部。连续两次文字相同,不代表它不会再变。流式识别中的中间结果与稳定片段
  2. 识别同一片段:按说话人、音频流和片段标识定位记录,新版本替换旧版本,正式结果只提交一次。没有版本号时,按服务提供的顺序或时间范围建立对应关系。
  3. 区分全文和增量:服务返回累计全文时,按协议转换为新增片段。不能只按文字去重,用户也可能真的说了两遍"对"。

这些规则由接入层处理,Frame 类型本身不提供通用修订协议。

用户说"嗯嗯",要不要停下来

同样一句"嗯嗯",放在不同的对话里,含义会变:

Agent 正在介绍任务进展,用户说"嗯嗯":可能只是表示在听。

Agent 问"现在创建吗",用户说"嗯嗯":可能是在确认。

有没有人声,只能回答用户是否发出了声音,不能直接决定要不要打断。 这里还需要结合当前对话场景。

Pipecat 提供按最少词数触发等轮次开始策略,也有可接入的附和判别能力,但短句过滤有代价:"停""别建"都很短,却应该及时处理。打断策略

具体怎么选策略,可以按照这两种方法:

  1. 按场景标注样本。 分清讲解中的附和、确认步骤中的回答、要求停下和背景人声,明确每类样本预期触发什么行为。
  2. 检查漏判和误判。 哪些真插话被漏掉了,哪些附和让 Agent 停了下来,再据此调整策略。中文里的词数还取决于识别和分词方式,不适合直接照搬一个阈值。

还有一种选择是先暂停,判断用户只是在附和后再继续播放。这里要区分暂停取消

处理方式 需要处理的状态 后续怎么继续
暂停播放 保留待播内容和播放位置,限制暂停期间的生成与缓冲 判断可以继续后,从保留的位置恢复
标准打断 取消旧工作,清理旧输出 通常依据已输出记录重新组织回答

暂停需要额外维护状态。是否值得做,我会先看当前场景里误打断有多频繁。

对话很长,摘要里的信息过时了怎么办

前面说过可以保留近期原文、压缩更早的对话。但摘要生成有时间差:

旧摘要里写着"负责人是小林",用户刚说要改成"小周",新摘要还没生成。下一轮到底读哪份?

我的考虑是,将讨论背景和当前业务状态分开保存:

保存位置 在这个例子中记录什么
近期原文 用户提出修改的原话,保留"怎么改的"
历史摘要 早期讨论背景,包括当时为什么选小林
结构化状态 当前选中的负责人是小周,以及这个选择的来源
未决事项 负责人选择发生变化,仍需要确认或执行修改

当前选中了小周,不等于任务系统里的负责人已经改成小周。 已经生效的负责人,需要通过查询结果核对,不能把修改意图记成已完成的事实。

Pipecat 的摘要机制会保留近期消息,并协调摘要请求和结果。上下文摘要提供历史压缩能力,但应用自己的业务状态仍要单独维护。

如果应用另行实现异步摘要,更新过程还需要处理版本问题:

  1. 发起请求时,记录摘要覆盖到哪条消息、基于哪个版本生成。
  2. 结果返回时,检查版本,只替换对应的历史范围,保留请求发出后新增的原文。
  3. 并发请求返回乱序时,拦截过期结果,避免旧摘要覆盖更新的摘要或业务状态。

摘要负责压缩历史。任务 ID、执行结果和待确认项,仍要有结构化记录可以核对。

Flows 有了阶段,为什么还会反复问同一个问题

比如查询"小林"返回两个同名的人,Agent 要求选择,用户回答"就是负责登录那个"。如果下一次查询没有带上这个新条件,流程就可能变成:

查询"小林" → 返回两个候选人 → 用户补充"负责登录" → 再次只查询"小林" → 仍然返回两个候选人

节点执行了,不代表业务有进展。 Flows 可以限定阶段和可用工具,应用还要定义什么情况算推进了流程。这个例子里,可以按下面的顺序处理。

第一步:检查新信息有没有进入状态。

用户补充了"负责登录",就要检查它有没有用于后续查询。判断进展时,可以观察字段是否确定、候选项是否减少,以及用户是否补充了条件。

第二步:识别无进展的重复调用。

结合当前节点、规范化后的工具参数和状态版本判断。状态和结果都没变,就换一个能消除歧义的问题。这里不能禁止所有同参数调用,动态数据可能确实需要刷新。

第三步:设置停止条件。

连续没有进展时,停止自动重试,把缺少的信息告诉用户,让用户选择或退出。同时为一轮自动处理设置工具调用次数、耗时或成本上限,并定义达到上限后的行为。

这些限制需要由应用执行。只在 Prompt 里写"不要死循环",无法保证流程会按预期退出。

用户觉得慢,时间究竟花在哪里

Agent 很快说了一句"我查一下",过了很久才报负责人。

首段音频很快,用户等答案的时间却没有缩短。所以测延迟时,需要把首段音频延迟有效答案延迟分开记录。在这个例子里,后者看的是用户多久听到负责人,不能用"我查一下"的播放时间代替。

第一步:记录同一轮对话的时间点。

环节 需要观察的时间点
用户输入 用户实际说完、轮次被判定结束、可用转写到达
模型与工具 LLM 请求与首段结果、工具请求与返回
语音输出 可合成文本形成、首段音频到达、客户端开始播放

这些记录需要关联到同一轮对话。转写和轮次判断可能重叠,不能把每个环节的耗时直接相加

Pipecat 的服务指标能提供首结果等待、处理耗时及模型用量等信息;是否有某个指标还要看服务支持情况。Metrics 文档里的服务耗时,不能直接等同于用户说完到听见答案的端到端耗时。端侧播放需要另外观测,跨机器的时间戳也要先处理时钟差异。

第二步:沿着关键路径找等待发生在哪里。

尾部转写迟迟没到,就先看识别收尾;大量时间花在文本聚合,就检查句段长度;真正慢的是查任务,换 TTS 帮助也有限。先从记录里找到耗时位置,再决定改哪一段。

第三步:用相同条件比较优化结果。

比较时保留相同的场景、网络和回答长度条件,同时报告样本量、P50 和 P95 等分位数,以及失败和超时情况。

本文没有做这一组实测,这里只介绍排查方式,不给出优化百分比。

参考

相关推荐
Superzhangaa1 小时前
全国服务消费季里的科技新品:太希智能外骨骼持续走红
大数据·人工智能·科技·开源
不悔哥1 小时前
开源 Zephyr:现代嵌入式 RTOS 长什么样
单片机·开源·嵌入式
EAIReport2 小时前
GitHub 超全使用指南|托管、协作、开源实战教程
开源·github
GISMagic2 小时前
2.从零制作第一个天气 Agent:理解 Prompt、Skill 与 LangChain
ai·langchain·prompt·agent
linqiw11 小时前
Embabel 上手:Java Agent 是怎么干活的
agent
X54先生(人文科技)12 小时前
ELR-SELLM Edge 神经元网络架构评估报告
人工智能·深度学习·架构·开源