如何在 pipecat 运行时在多个 LLM provider 之间切换(LLMSwitcher)?
【免费下载链接】pipecatOpen Source framework for voice agents, multimodal apps, and realtime AI. Maintained by Daily and the community.项目地址: https://gitcode.com/GitHub_Trending/pi/pipecat
在基于 pipecat 构建的语音 Agent 中,如果对话中途需要更换 LLM provider——比如从一个供应商切换到另一个,或者主供应商故障时自动降级——不需要重建 pipeline,pipecat 提供了 LLMSwitcher:把多个LLMService实例放进同一个切换器组件,在运行中通过控制帧或故障策略切换当前生效的 LLM。本文基于仓库中的两个示例 features-service-switcher.py 和 llm_switching.py 说明如何搭建、触发切换并验证结果。适用前提:Python 3.11+(仓库 README 建议 3.12+),并已按仓库说明配置好本地开发环境。
准备环境
- 安装 pipecat 框架(仓库 README.md 给出的方式):
uv add pipecat-ai- 复制环境变量模板并填入你计划使用的服务的 API key(仓库根目录的 env.example):
cp env.example .env两个示例用到的 key 不同:
- features-service-switcher.py 使用
OPENAI_API_KEY、GOOGLE_API_KEY、CARTESIA_API_KEY、DEEPGRAM_API_KEY(示例中通过os.environ读取,缺失会直接报错); - llm_switching.py 的 docstring 明确列出需要
CARTESIA_API_KEY(TTS)、DEEPGRAM_API_KEY(STT)、OPENAI_API_KEY、GOOGLE_API_KEY、ANTHROPIC_API_KEY,以及 AWS 凭据(AWS Bedrock LLM),DAILY_API_KEY为 transport 可选。
- 运行示例时默认使用 SmallWebRTC transport,浏览器打开 http://localhost:7860/client/ 点击 "Connect" 即可与 bot 通话。示例也支持
-t daily或-t twilio -x NGROK_HOST_NAME等其他 transport(见 examples/README.md)。
LLMSwitcher 是什么
LLMSwitcher是 ServiceSwitcher 的 LLM 专用封装(源码位于 src/pipecat/pipeline/llm_switcher.py),本质是一条并行 pipeline:每个成员 LLM 被一对过滤器包住,只有"当前激活"的那个 LLM 会收到帧。关键事实(来自源码 docstring):
- 构造签名:
LLMSwitcher(llms=[...], strategy_type=...),strategy_type默认是ServiceSwitcherStrategyManual; - 列表中的第一个 LLM 是初始激活的 LLM;
- 通过
active_llm属性读取当前激活的 LLM; - 切换由
ServiceSwitcherFrame派生的控制帧驱动,手动切换使用的帧是 ManuallySwitchServiceFrame(service=指定目标服务); - 通过
on_service_switched事件回调感知切换发生。
切换会被拒绝的两种情况(ServiceSwitcher._set_active_if_available):目标服务不在列表中,或者目标服务is_usable为 False(此时日志会提示 "Not switching to ...: it can no longer do its job",需要先恢复该服务)。
最短主路径:手动切换 LLM
下面摘录自 examples/features/features-service-switcher.py 的关键部分(完整可运行版本在该文件中)。它把 STT、LLM、TTS 三个环节都做成了切换器,这里只看 LLM 部分:
from pipecat.frames.frames import LLMRunFrame, ManuallySwitchServiceFrame from pipecat.pipeline.llm_switcher import LLMSwitcher from pipecat.pipeline.pipeline import Pipeline from pipecat.services.google.llm import GoogleLLMService from pipecat.services.openai.llm import OpenAILLMService system_prompt = "You are a helpful assistant in a voice conversation. ..." llm_openai = OpenAILLMService( api_key=os.environ["OPENAI_API_KEY"], settings=OpenAILLMService.Settings(system_instruction=system_prompt), ) llm_google = GoogleLLMService( api_key=os.environ["GOOGLE_API_KEY"], settings=GoogleLLMService.Settings(system_instruction=system_prompt), ) # 默认使用 ServiceSwitcherStrategyManual llm_switcher = LLMSwitcher(llms=[llm_openai, llm_google]) pipeline = Pipeline( [ transport.input(), # Transport user input stt_switcher, user_aggregator, # User responses llm_switcher, # LLM tts_switcher, # TTS transport.output(), # Transport bot output assistant_aggregator, # Assistant spoken responses ] )pipeline 位置固定在 user aggregator 之后、TTS 之前。llms=[llm_openai, llm_google]决定了初始激活的是 OpenAI;列表顺序也决定了 failover 策略的备用顺序(见下文)。
触发切换
在 transport 事件回调里,把ManuallySwitchServiceFrame塞进 worker 队列即可切换(以下片段摘自同一示例的on_client_connected处理):
@transport.event_handler("on_client_connected") async def on_client_connected(transport, client): # ... 开始对话 ... await worker.queue_frames([LLMRunFrame()]) await asyncio.sleep(15) print(f"Switching to {llm_google}") await worker.queue_frames([ManuallySwitchServiceFrame(service=llm_google)])queue_frames之后帧沿 pipeline 下行到达llm_switcher,ServiceSwitcherStrategyManual收到ManuallySwitchServiceFrame后把激活服务切换成frame.service。运行这个示例后,客户端连接 15 秒终端会打印Switching to ...(示例代码自身的 print 语句),随后 bot 的回复改由对应 LLM 生成。
可选分支:在对话中通过工具切换
如果切换时机由用户指令决定("换到 Google"),而不是代码定时触发,参考 examples/flows/python/llm_switching.py。它把 4 个 LLM(OpenAI、Google、Anthropic、AWS Bedrock)放进一个切换器:
llm_switcher = LLMSwitcher( llms=[llm_openai, llm_google, llm_anthropic, llm_aws], strategy_type=ServiceSwitcherStrategyManual, ) # FlowManager 直接使用 llm_switcher 作为 llm flow_manager = FlowManager( worker=worker, llm=llm_switcher, context_aggregator=context_aggregator, )切换逻辑写成一个对话工具,用户说话时由 LLM 调用:
async def switch_llm(flow_manager: FlowManager, llm: str) -> tuple[SwitchLLMResult, None]: """Switch the current LLM service. Args: llm: The name of the LLM service to switch to. Must be one of "OpenAI", "Google", "Anthropic", or "AWS". """ # 按名称映射到对应的 LLM 实例 ... if llm_switcher.active_llm == new_llm: return SwitchLLMResult(status="success", message=f"Already using {llm} LLM service."), None # 在工具调用里,把切换帧从 aggregator 向上游推, # 保证切换在 LLM 用工具结果继续推理之前发生 await context_aggregator.assistant().push_frame( ManuallySwitchServiceFrame(service=new_llm), FrameDirection.UPSTREAM ) return SwitchLLMResult(status="success", message=f"Switched to {llm} LLM service."), None注意它与定时切换写法的一个差异:这里切换帧是从 assistant context aggregator向上游push_frame的。示例注释解释了原因——工具调用会触发 aggregator 的上游更新,从 aggregator 推上游可以保证切换先于"带工具结果再次运行 LLM"发生。切换成功后工具返回status="success"以及"Switched to {llm} LLM service."之类的消息(已在当前 LLM 上时返回"Already using {llm} LLM service."),这些字符串由代码直接构造,可以作为判断切换意图是否被执行的依据。
可选分支:故障时自动 failover
手动策略只响应显式的ManuallySwitchServiceFrame。如果希望某个 provider 挂掉时自动切到下一个,把策略换成ServiceSwitcherStrategyFailover(定义在 src/pipecat/pipeline/service_switcher.py):
llm_switcher = LLMSwitcher( llms=[llm_openai, llm_google], strategy_type=ServiceSwitcherStrategyFailover, ) @llm_switcher.strategy.event_handler("on_service_switched") async def on_switched(strategy, service): # 应用自己决定何时/如何恢复失败的 LLM ...该策略的行为(源码 docstring 描述):
- 只有当激活的 LLM 报出一个让它
is_usable=False的错误(即"再也做不了这份工作")才切换;它能扛过去的错误不会触发 failover; - 按构造时列表的顺序取下一个仍然可用的 LLM,从头尾回绕;
- 失败的 LLM 仍保留在列表里,之后用
FrameProcessor.set_usable把它恢复后就能再切回来; - 恢复/回退策略留给你通过
on_service_switched事件自己实现。
验证切换是否生效
文档给出的可核对点有以下几处:
active_llm属性:任何时候llm_switcher.active_llm返回当前生效的 LLM 实例,llm_switching 示例正是用它判断"是否已经在目标 LLM 上";on_service_switched事件:注册在 strategy 上的事件处理器在激活服务变化时被调用,这是代码层面确认切换发生的通知点;- 日志与返回消息:手动示例在每次切换前打印
Switching to ...;failover 路径下,切走时日志会输出Service {name} reported an error: ...,找不到可用备用时输出No other service available to switch to; - 单元测试:仓库自带 tests/test_llm_switcher.py,覆盖"direct function 会注册到每个成员 LLM""设置更新会到达未激活的成员"等契约。按 README.md 的运行方式只跑这个测试套件:
uv run pytest tests/test_llm_switcher.py切换过程中工具与设置的保持
跨 provider 切换最容易踩的坑是 function calling:LLMSwitcher对工具注册做了专门的广播,源码中有两个事实:
LLMContext(tools=[...])中列出的 direct functions 会在每个成员 LLM 上注册——无论激活与否。原因是成员 LLM 各自被分支过滤器挡住,运行时只有激活的 LLM 能收到LLMContextFrame;切换器在收到上下文帧时主动把所有成员的 tool handler 同步一遍(llm_switcher.py 的process_frame),保证切换后工具继续可用。features-service-switcher 示例就是靠这一机制让get_current_weather等工具在切换前后都工作;llm_switcher.register_function(...)同样会转发到所有成员 LLM,cancel_on_interruption、timeout_secs等参数按"显式参数 >@tool_options装饰器 > 默认值"在每个成员上解析;LLMSwitcher.register_direct_function自 1.4.0 起被标记为 deprecated(计划 2.0.0 移除),官方建议改用LLMContext(tools=[...]),或推LLMSetToolsFrame在会话中途变更工具;- 运行时更新 LLM 设置(如
LLMUpdateSettingsFrame)带reach_inactive_services=True时,更新会同时送达未激活的成员,避免某个 LLM 被激活后缺了配置(tests/test_llm_switcher.py 中有对应测试)。
限制与边界
- 目标 LLM 必须在传给
LLMSwitcher的列表里,且is_usable为 True,否则切换被忽略(目标不在列表时"可能本意是给 pipeline 里另一个 switcher 的",直接被忽略); - 切换器自身不接受
set_usable:它报告的可用性是所有成员 LLM 的"读数",把某个成员恢复为 usable 即可带动整个切换器恢复; - 非激活 LLM 的错误会被切换器就地吸收,不会再向上传播;只有激活 LLM 的错误会交给策略决定是否切换,切换成功后该错误也不再向上传播;
- 两个示例的切换都是"会话内"行为:LLM 实例在启动时就全部构造好(API key 需提前配齐),运行中新增成员 LLM 的路径文档未提供,需要自行处理。
完整代码以仓库内文件为准:examples/features/features-service-switcher.py(三环节手动切换的最小示例)与 examples/flows/python/llm_switching.py(对话工具驱动切换 + Flows 集成),Flows 示例的运行方式见 examples/flows/README.md。
【免费下载链接】pipecatOpen Source framework for voice agents, multimodal apps, and realtime AI. Maintained by Daily and the community.项目地址: https://gitcode.com/GitHub_Trending/pi/pipecat
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考