最近我们的产品接入了Claude 3.5 Sonnet,本来一切顺利,结果线上出了一个诡异的Bug。
部分用户反馈,AI的回复有时候会出现乱码,有时候会重复同一句话,有时候会突然中断。这个Bug没有明显的规律,复现困难,排查起来非常棘手。我排查了整整一夜,终于找到了原因。
这篇文章,我想分享一下这次排查的过程,以及最终的解决方案。希望能给做AI应用的朋友一些参考。
Bug现象
先说说Bug的现象。
我们的产品是一个AI助手,用户输入问题,我们调用Claude 3.5 Sonnet的API,把回复返回给用户。大部分时候,回复是正常的,但偶尔会出现异常。
异常有几种表现:
第一种是,回复中出现乱码。比如,回复中会夹杂一些奇怪的字符,或者出现重复的字、重复的词。有时候是一两个字,有时候是一整句话重复。
第二种是,回复突然中断。回复到一半,突然就停了,没有说完。而且,这种中断不是因为达到了最大token数,而是在句子中间突然停了。
第三种是,回复格式错误。我们要求AI用特定的格式回复,比如JSON格式。但有时候,AI返回的JSON格式不对,有语法错误,导致我们的解析失败。
这些异常都是随机出现的,没有明显的规律。同样的问题,有时候回复正常,有时候回复异常。而且,异常的概率不高,大概5%左右,但因为用户量大,每天还是有不少用户遇到。
这个Bug最让人头疼的地方就是:随机出现,复现困难,而且是AI模型的输出,不像传统程序那样可以打断点调试。
初步排查
拿到Bug之后,我开始了初步排查。
第一步,查看API返回的原始数据。我们的系统会记录每次调用API的请求和响应。我找了几个异常的案例,查看了API返回的原始数据。发现异常确实是API返回的,不是我们后处理造成的。API返回的内容里,确实有乱码、重复和中断。
第二步,检查请求参数。我对比了正常请求和异常请求的参数,发现没有明显的区别。prompt、temperature、maxtokens、topp等参数都是一样的。说明不是参数设置的问题。
第三步,检查网络。我查看了API调用的网络日志,发现没有超时、没有断连、没有重试。HTTP状态码都是200,说明请求是成功的。网络层面没有问题。
第四步,检查是否是速率限制。我查看了API的速率限制情况,发现我们的调用量在限制范围内,没有被限流。而且,如果是限流的话,应该返回429状态码,而不是200加异常内容。
初步排查没有找到原因,问题变得更诡异了。
深入排查
初步排查没有结果,我开始深入排查。
第一步,统计异常的规律。我把最近一周的异常案例都找出来,做了统计分析。发现了几个规律:
- 异常更容易出现在长回复中。回复越长,出现异常的概率越高。短回复几乎不会出现异常。
- 异常更容易出现在特定的时间段。晚上8点到12点,异常的概率明显更高。
- 异常和用户的问题类型没有明显关系。各种类型的问题都可能出现异常。
第二步,怀疑是流式输出的问题。我们用的是Claude的流式输出(streaming),API会一块块地返回内容,我们拼接起来返回给用户。我怀疑是不是流式输出的拼接出了问题。
于是,我把流式输出的每一块都记录下来,然后对比拼接后的结果和原始块。发现拼接是正确的,没有丢块、没有重复块。异常是在某一块内容里就出现了,不是拼接造成的。
第三步,怀疑是上下文长度的问题。Claude 3.5 Sonnet支持200K的上下文窗口。我查看了异常案例的上下文长度,发现大部分异常案例的上下文都比较长,超过了100K token。
我猜测,是不是上下文太长的时候,模型的输出会出现异常?为了验证这个猜测,我做了一个实验:用同样的prompt,但控制上下文长度,看看异常的概率。
实验结果证实了我的猜测:上下文越长,异常的概率越高。上下文在50K以内的时候,几乎没有异常;上下文在100K左右的时候,异常概率约3%;上下文在150K以上的时候,异常概率超过10%。
这是一个重要的线索。
根因分析
找到了上下文长度这个线索之后,我开始分析根因。
为什么上下文长的时候,模型的输出会出现异常呢?
我查了很多资料,也和一些做AI应用的朋友交流,最终找到了原因。
第一个原因是,长上下文中的注意力衰减。大模型的注意力机制,在处理长上下文的时候,对中间部分的注意力会衰减。也就是说,模型会"忘记"上下文中间的内容。这会导致模型在生成回复的时候,逻辑混乱,出现重复、乱码、中断等问题。
虽然Claude 3.5 Sonnet号称支持200K上下文,但实际上,在长上下文的情况下,模型的表现会下降。尤其是在100K以上,下降比较明显。
第二个原因是,长上下文中的信息干扰。如果上下文中有很多不相关的信息,或者有矛盾的信息,模型会被干扰,输出异常。比如,我们的系统会把用户的历史对话都放进上下文,如果历史对话中有矛盾的信息,或者有很多无关的闲聊,模型就会被干扰。
第三个原因是,服务端的负载问题。晚上8点到12点是使用高峰期,Anthropic的服务器负载高。在高负载的情况下,模型的推理质量可能会下降,出现异常的概率增加。这也解释了为什么异常在特定时间段更多。
第四个原因是,tokenizer的边界问题。在流式输出中,每个块可能在token的中间被截断。如果拼接的时候处理不好,就会出现乱码。虽然我们的拼接逻辑是正确的,但在某些极端情况下,比如多字节字符被截断,还是可能出现问题。
综合来看,这个Bug的根本原因是:在长上下文加高负载的情况下,Claude 3.5 Sonnet的输出质量下降,出现异常。这不是我们代码的Bug,而是模型本身的局限。
解决方案
找到了根因之后,我开始寻找解决方案。
第一个方案是,限制上下文长度。不要把所有的历史对话都放进上下文,只保留最近的、相关的内容。我们实现了一个上下文管理机制:自动总结旧的对话,只把总结和最近的几轮对话放进上下文。这样,上下文长度控制在50K以内,异常的概率大大降低。
第二个方案是,优化prompt。在prompt中明确要求模型输出正确的格式,不要重复,不要中断。我们还在prompt中加入了格式示例,让模型更清楚我们的要求。优化prompt之后,格式错误的问题明显减少。
第三个方案是,后处理校验。在把AI的回复返回给用户之前,做一次校验。比如,检查JSON格式是否正确,如果不正确,就让模型重新生成一次,或者用容错的方式解析。对于重复和乱码,我们也做了一些简单的后处理,比如去除连续重复的短语。
第四个方案是,重试机制。如果检测到回复异常(比如格式错误、内容过短、有明显的乱码),就自动重试一次。重试的时候,适当调整temperature,或者简化上下文。我们加了重试机制之后,用户遇到异常的概率又降低了很多。
第五个方案是,错峰调用。对于非实时的任务,我们安排在低峰期调用API,避开高峰期。这样可以降低因为服务器负载高导致的异常。
综合使用这些方案之后,异常的概率从5%降到了0.5%以下,用户基本感知不到了。
排查经验总结
这次排查,花了我整整一夜的时间。总结一下经验:
第一,AI应用的Bug排查,和传统应用不一样。传统应用的Bug,可以打断点、看日志、复现。但AI模型的输出是不确定的,同样的输入可能有不同的输出。排查AI应用的Bug,需要更多的统计分析和实验验证。
第二,要记录完整的请求和响应。排查AI应用的问题,完整的日志非常重要。要记录每次调用的prompt、参数、响应、耗时、状态码等信息。没有这些日志,根本无法排查问题。
第三,关注上下文长度。长上下文是很多AI应用问题的根源。上下文越长,模型的表现越不稳定,成本也越高。要做好上下文管理,不要盲目地把所有内容都放进去。
第四,做好防御性编程。AI模型的输出是不可控的,可能会有各种异常。应用层要做好防御:格式校验、异常重试、后处理、容错解析。不要假设AI的输出永远是正确的。
第五,了解模型的局限。每个模型都有它的局限,包括上下文长度、推理能力、稳定性等。要了解你用的模型的特点,在设计应用的时候考虑到这些局限。
后续改进
这次Bug之后,我们做了一些改进,避免类似问题再次发生。
第一,完善了监控。我们增加了对AI输出质量的监控,包括异常率、平均回复长度、格式错误率等指标。如果异常率突然升高,会立刻告警。
第二,建立了评测体系。我们收集了一批测试用例,每次更新prompt或者更换模型的时候,都跑一遍测试,看看输出质量有没有下降。这样可以在上线之前就发现问题。
第三,多模型备份。我们不再只依赖一个模型,而是接入了多个模型(Claude、GPT、国产模型)。如果一个模型出现问题,可以自动切换到另一个模型。这样既提高了稳定性,也避免了供应商锁定。
第四,持续优化上下文管理。我们在不断优化上下文管理的策略,比如用向量检索相关的历史对话,而不是简单地截断。这样既能保持上下文的相关性,又能控制长度。
写在最后
大模型应用的开发,和传统应用开发有很大的不同。传统应用的行为是确定的,而大模型的行为是概率性的。这给开发和排查都带来了新的挑战。
这次Claude 3.5 Sonnet的Bug排查,让我对大模型应用的开发有了更深的理解。要做好大模型应用,不仅要会调用API,还要理解模型的原理和局限,做好上下文管理、输出校验、异常处理、监控告警等工作。
AI技术发展很快,模型也在不断进步。也许未来的模型会越来越稳定,这些问题都会慢慢解决。但在那之前,我们还是要在应用层做好充分的准备。
希望这篇文章能给做AI应用的朋友一些参考。如果你也有类似的排查经历,欢迎在评论区交流。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录