第9期给机器人装上prompt注入防护之后,我以为能消停一阵。结果上周三半夜,主模型供应商挂了四十分钟,机器人跟着一起瘫。用户问什么都返回500,缓存只能顶住重复问题,新问题一个都答不上来。那一刻我才反应过来:第4期做了熔断,第7期做了限流,第8期做了缓存,全是机器人自己不出问题的前提下才管用。模型供应商一挂,这些全白搭。

问题就一个:单点依赖。机器人从头到尾只认一个模型,这个模型就是整条链上最脆的一环。它挂了,你的代码写得再稳也没用。解决办法也不复杂:再备一个模型,主模型不行就自动切备用,主模型好了再切回来。整个逻辑就三件套:主备两个客户端、一个路由、一个健康探测。

环境还是老样子,Python 3.10以上,标准库就够。下面这段代码直接能跑,MockClient模拟两个模型,主模型前3次调用故意失败,备用模型一直正常:

import time

class ModelDown(Exception):
    pass

class MockClient:
    def __init__(self, name, fail_times=0, latency=0.05):
        self.name, self.fail_times, self.latency = name, fail_times, latency
        self.calls = 0
    def chat(self, q):
        self.calls += 1
        time.sleep(self.latency)
        if self.calls <= self.fail_times:
            raise ModelDown(f"{self.name} 第{self.calls}次调用失败")
        return f"[{self.name}] {q} 的答案"

class ModelRouter:
    def __init__(self, primary, fallback, max_fail=2):
        self.primary, self.fallback = primary, fallback
        self.max_fail, self.fail_count = max_fail, 0
        self.primary_down = False
    def chat(self, q):
        if not self.primary_down:
            try:
                ans = self.primary.chat(q)
                self.fail_count = 0
                return ans
            except ModelDown as e:
                self.fail_count += 1
                print("主模型异常:", e)
                if self.fail_count >= self.max_fail:
                    self.primary_down = True
                    print("主模型连续失败,切换到备用模型")
        return self.fallback.chat(q)
    def probe(self):
        # 每隔一段时间探一次主模型,好了就切回来
        try:
            self.primary.chat("ping")
            self.primary_down = False
            self.fail_count = 0
            print("主模型恢复,切回主模型")
        except ModelDown:
            print("主模型还没好,继续用备用")

if __name__ == "__main__":
    main = MockClient("主模型A", fail_times=3)
    back = MockClient("备用模型B")
    router = ModelRouter(main, back)
    for q in ["退货运费谁承担", "怎么查订单", "能开发票吗", "明天几点开门"]:
        print("问:", q, "->", router.chat(q))
    print("调用统计:主模型", main.calls, "次,备用模型", back.calls, "次")
    router.probe()
    print("恢复后:", router.chat("退货要运费吗"))

跑一遍,结果很直观:主模型前两次调用失败,连续失败满2次就切走,后面4个问题全是备用模型答的。调用统计主模型2次、备用模型4次。收尾时再probe一次,主模型第3次调用还是失败,继续用备用,没切回去。这个行为就是想要的:切走要果断,回来要确认。

这里有个数字要讲清楚:连续失败阈值我设的是2次,不是1次。模型接口偶尔抖一下很正常,超时一次就切走,切回来又抖,来回横跳比直接扛着更糟。2到3次是个实在的数:一次是抖动,两次以上大概率是真挂了。切走之后也别再调主模型了,每次请求都先撞一次失败再走备用,等于每次请求白白多一次超时,延迟直接翻倍。

第一个坑:阈值设成1。我第一版就是这么写的,结果主模型一次超时就切备用,备用刚接手两分钟,主模型好了,probe切回来,刚切回来又抖一次,又切走。一晚上来回切了十几回,日志里全是切换记录,用户倒没感觉,账单上两个模型都在计费。改成连续失败2次才切,横跳就没了。

第二个坑:切到备用就忘了回来。很多降级代码只写了切走,没写回来。主模型半夜挂了,早上八点就好了,你的服务还在用备用模型扛,备用一般比主模型贵或者弱,扛一天账单先翻倍。probe就是干这个的:定时发个ping探一次,通了就切回来。探活间隔别太密,5分钟一次够了,探太密等于拿ping去压一个刚恢复的服务。

第三个坑:备用模型能力弱一截,还偷偷降级。备用一般是便宜的小模型,回答质量差一档。用户问个复杂问题,主模型能答好,备用答得磕磕绊绊,用户不会知道你降级了,只会觉得你变笨了。降级的时候打一条日志,顺手记个指标,监控上能看到"当前在备用",出了问题先查这一条。能告诉用户更好,一句"当前使用备用模型,回答可能慢一点"顶十句事后解释。

这篇建议收藏。主模型挂掉不是会不会发生的问题,是什么时候发生的问题。40行路由加两个客户端,半小时就能接进第8期的机器人里。下次半夜供应商挂了,你的机器人还能站着回答问题。

下期是Python实战第11期,讲知识库热更新:不用重启服务就能往机器人里加新文档,顺带把第1期的检索引擎改成能动态加载的版本。这个系列从第1期检索引擎开始,把问答机器人从0搭到上线再到防攻击、做降级,一期接一期,都收在专栏《Python实战》里。关注之后去专栏里能连着看,省得一篇篇翻。

你上次服务挂掉,是模型供应商挂了,还是自己代码出bug?修了多久?

Cyrus

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐