
当我发布第一篇关于 Neuron AI Router 的文章时,我以为大家会问关于路由规则的问题:哪种规则适合结构化输出、如何编写自定义规则、轮询(round robin)在高负载下表现如何。这些问题确实来了,但最常见的问题却不一样,而且其实根本不是关于路由的——而是关于故障:当提供商宕机时,我的 Agent 会怎样?
这是个好问题,如果你刚开始构建 AI 应用,在看任何代码之前都值得先把这个问题讲清楚。
简单版本是这样的:Neuron AI Router 中新的回退(fallback)策略,允许你为 PHP Agent 定义一个有序的 LLM 提供商列表。当一次推理调用因瞬时错误(如限流、超时、服务器过载)失败时,同一个请求会自动重试到列表中的下一个提供商。
故障转移是透明的:Agent 完全察觉不到它发生过,对话继续进行,不丢失任何状态。本文其余部分会解释这个问题为何存在、常见方案为何不够用,以及如何配置它。
LLM 提供商是你通过 HTTP 通信的外部服务。每次你的 Agent "思考",都是在向一台你无法控制、由一家正在为数百万其他请求服务的公司所运营的机器发起网络调用。这些服务会以非常普通的方式失败:流量激增导致你触发限流;提供商流量激增返回"过载"错误;请求超时;他们那边的一次部署导致几分钟的错误率升高。这些都不意味着你做错了什么,也都不罕见。只要你的 Agent 在生产环境运行足够久,这些情况你都会遇到。
在传统 Web 应用中,一次对第三方 API 的失败调用通常只是系统的某个角落:记个日志,也许放到队列里重试,页面其余部分照常工作。但在基于 Agent 的应用中,推理调用不是角落,而是心脏。调用失败,功能就下线了。而 Agent 还以一种微妙的方式让情况更糟:单个用户请求可能触发多次推理调用,因为 Agent 会在工具执行、推理步骤和后续消息之间循环。每次循环迭代都是一次撞上提供商"坏时刻"的机会。你的 Agent 越强大,暴露面就越大。
直觉性的修复是 try/catch 加重试。它对一次偶发超时有用,但有个结构性弱点:当某个提供商正处于糟糕的五分钟时,对同一个提供商重试只是意味着"更慢地失败"。你那一刻真正想要的是换一个提供商。而这正是过去让人难受的地方——切换提供商要么意味着维护第二套 Agent 配置,要么意味着围绕失败在应用代码里做分支。失败发生在 Agent 执行的深处、工具循环的中间,所以在应用层捕获并从头开始,会把 Agent 已经完成的工作全部丢弃。
我把这件事放在 router 包的语境下写,是因为 router 本身就处在恰好能解决它的那一层。RouterProvider 是一个实现了 AIProviderInterface 的代理——与所有 Neuron 提供商遵守同一套契约。它在 Agent 与外部世界之间的边界上拦截每一次 chat()、stream() 和 structured() 调用。如果某个提供商在这个边界上失败,router 是系统中唯一能在出错时捕获、并把完全相同的请求转发给另一个提供商的组件,而 Agent 对发生的一切毫无感知。
这就是新回退策略所做的事。你定义一个有序的提供商列表。router 把请求发给第一个,如果调用因瞬时错误失败,请求就透明地流转到列表中的下一个提供商。Agent 收到响应时就像什么都没发生。工具循环继续跑,对话状态原封不动。
"瞬时"这个词很关键。不是每种错误都值得重试。如果你的 API key 无效或请求格式错误,把同一个请求再发给同一个提供商会以同样的方式失败,router 不会浪费时间假装不是这样。而限流、超时、服务端错误则不同:它们绑定在某个提供商的某个时间点上,同一个请求此刻在别处很有机会成功。回退逻辑只对这第二类错误作出反应。
在这次更新中,路由规则变成可选的。如果你只想要韧性(resilience),配置就是两个提供商加一个回退顺序:
use NeuronAI\Agent\Agent;
use NeuronAI\Router\RouterProvider;
use NeuronAI\Providers\Anthropic\Anthropic;
use NeuronAI\Providers\OpenAI\OpenAI;
use NeuronAI\Providers\AIProviderInterface;
class MyAgent extends Agent
{
protectedfunction provider(): AIProviderInterface
{
return RouterProvider::make()
->addProvider('anthropic', new Anthropic(
key: 'ANTHROPIC_API_KEY',
model: 'claude-sonnet-4-20250514',
))
->addProvider('openai', new OpenAI(
key: 'OPENAI_API_KEY',
model: 'gpt-4o',
))
->setFallback(['anthropic', 'openai']);
}
}
每次调用都走Anthropic。如果Anthropic返回瞬时错误,同一个调用就转到 OpenAI。你的 Agent 代码、工具、指令、RAG 配置——其余一切都不变。这正是 router 从第一天起就具备的"即插即用"特性,只是应用到了一个新问题上。
回退与路由规则还可以组合使用。规则决定在正常情况下请求该去哪里,回退顺序则决定当这个决定撞上一个正在失败的提供商时该怎么办:
use NeuronAI\Router\Rules\MethodRule;
RouterProvider::make()
->addProvider('anthropic', new Anthropic(...))
->addProvider('openai', new OpenAI(...))
->addProvider('gemini', new Gemini(...))
->setRule(
new MethodRule('anthropic')->structured('openai')
)
->setFallback(['anthropic', 'openai', 'gemini']);
这里结构化输出发给 OpenAI,其余都发给 Anthropic;但二者中任何一个临时不可用时,请求会按回退顺序找到可用提供商。这两个机制回答的是两个不同的问题——规则回答"谁应该处理这个?",回退回答"谁现在能处理这个?"。
一个实用提示:静默工作的回退对用户体验很好,却对你的可观测性有风险。如果 Anthropic 已经失败了两个小时、每个请求都悄悄地由 OpenAI 在服务,你会想知道——因为你很可能在付不同的价格、得到不同的模型行为。引入回退后请留意你的监控,把回退激活的激增当作一个值得排查的信号,而不仅仅是"系统已经替我解决了的问题"。
在其他生态中,这件事通常发生在应用之外:你在代码和提供商之间放一个网关服务(比如 OpenRouter),由网关来处理故障转移。这可行,但意味着多一跳网络、多一个供应商、多一个仪表盘,以及多一个你的 prompt 会经过的地方。
而在这里,整套机制是位于你代码库内的一个 composer 包。它随你的应用一起版本化、用纯 PHP 配置、可以用你自己的测试套件来测试。之所以能这样,原因和我在本系列第一篇里描述的一样:统一消息层(Unified Messaging Layer)。
因为每个 Neuron 提供商都知道如何把同一种消息表示翻译成它自己的 API 格式,所以 router 可以把一个刚刚在 Anthropic 上失败的请求直接交给 OpenAI,中间不需要任何翻译工作。回退策略不是硬接到 router 上的功能,而是同一架构特性的第二个必然结果——路由是第一个。
用 composer 更新包:
composer require neuron-core/router
Neuron AI 文档则涵盖了关于提供商和 Agent 的全部内容,适合从零开始的人。如果你在生产环境运行 Agent,并且对"哪些错误该不该触发回退"有自己的看法,欢迎提 issue 或在社区渠道分享你的经验。
这个功能正是来自那些在生产环境运行 Neuron AI 的人们的讨论,我也希望它继续这样演进下去。