上周三晚上,我经历了入职以来最难忘的一次线上Bug排查。

那天晚上十点多,我正准备睡觉,手机突然响了。是运维同事打来的,说线上的AI Agent服务出问题了,大量请求报错,用户反馈很强烈。我赶紧打开电脑,开始排查。

没想到,这一排就是一整夜。从晚上十点到第二天早上八点,整整十个小时,我终于找到了问题的根源,修复了Bug。

这篇文章,我想分享一下这次线上Bug的排查过程和解决经验。从问题发现、日志分析到根因定位和修复,聊聊复杂系统中排查Bug的思路和方法。如果你也做后端开发,或者遇到过类似的线上问题,希望这篇文章能给你一些参考。

先说明一下,为了保密,文中涉及的具体业务和技术细节做了一些脱敏处理,但排查的思路和方法是真实的。

问题发现

先说说问题是怎么发现的。

我们的AI Agent服务,是基于一个开源的Agent框架开发的,主要用来处理用户的自然语言请求,调用各种工具,完成复杂的任务。这个服务已经上线运行了几个月,一直比较稳定。

那天晚上,运维同事发现,服务的错误率突然飙升,从平时的0.1%升到了30%以上。大量用户反馈,说Agent没有响应,或者返回了错误的结果。

运维同事第一时间尝试了重启服务,但重启之后,错误率还是很高。而且,错误不是持续的,而是间歇性的,有时候正常,有时候报错,很难定位。

我接到电话之后,赶紧打开电脑,连上VPN,开始查看监控和日志。

首先看监控。服务的CPU、内存、网络都正常,没有明显的异常。错误率确实很高,而且主要集中在Agent的执行环节。

然后看日志。日志里有大量的错误信息,但错误信息很模糊,都是类似"Agent execution failed"、"Unexpected error"这样的通用错误,没有具体的错误原因和堆栈信息。

这是第一个难题:错误信息太模糊,无法直接定位问题。

第一步:复现问题

排查Bug的第一步,是复现问题。只有能稳定复现,才能调试和验证修复。

我先在测试环境尝试复现。用同样的请求参数,在测试环境调用Agent服务,结果测试环境一切正常,没有报错。我试了很多次,都没有复现。

这是第二个难题:测试环境复现不了,只有线上环境报错。

这种情况很常见,线上环境和测试环境的配置、数据量、并发量都不一样,有些问题只有在特定的条件下才会出现。

我开始分析线上环境和测试环境的区别。线上环境的并发量更高,数据量更大,而且用的是生产级的配置。测试环境的并发量低,数据量小,配置也比较简单。

我猜测,问题可能和并发或者数据量有关。于是,我在测试环境用压测工具,模拟高并发的请求,看看能不能复现。

压测了一会儿,果然,测试环境也开始报错了。错误信息和线上的一样,都是模糊的通用错误。

问题能复现了,这是排查的关键一步。能复现,就能调试,就能找到问题的根源。

第二步:开启详细日志

能复现之后,下一步是获取更详细的错误信息。

原来的日志级别是INFO,只记录了关键的操作和错误,没有详细的调试信息。我把日志级别改成了DEBUG,重新运行压测,看看详细的日志。

开启DEBUG日志之后,日志量大大增加,每次请求都有大量的调试信息。我仔细查看了报错请求的日志,发现了一个异常的地方。

在Agent执行的过程中,有一个步骤是调用大模型API,让大模型决定下一步该做什么。正常情况下,大模型会返回一个结构化的结果,包含要调用的工具和参数。但在报错的请求中,大模型返回的结果格式不对,解析失败了。

具体来说,大模型有时候会返回非JSON格式的文本,或者返回的JSON缺少必要的字段。Agent框架在解析这些结果的时候,抛出了异常,但异常被框架捕获了,只记录了通用的错误信息,没有记录具体的异常。

看起来,问题的直接原因是大模型返回的结果格式不对,导致解析失败。

但这只是表面原因。为什么大模型会返回格式不对的结果呢?是大模型本身的问题,还是我们的提示词有问题,还是框架的问题?

我继续分析。

第三步:分析大模型返回的结果

我在日志里加了更详细的记录,把大模型返回的原始结果都记录下来。然后运行压测,收集报错请求的大模型返回结果。

收集了几十个报错请求的结果之后,我发现了一个规律。

这些报错的请求,大模型返回的结果都有一个共同的特点:结果被截断了。也就是说,大模型还没生成完完整的结果,就被截断了,导致返回的JSON不完整,解析失败。

为什么会被截断呢?我查看了大模型API的调用参数,发现了问题。

我们调用大模型API的时候,设置了一个max_tokens参数,限制了大模型生成的最大token数。这个参数的值是256,也就是说,大模型最多生成256个token。

正常情况下,大模型返回的结构化结果,token数在100到200之间,256的限制是够用的。但在某些复杂的请求中,大模型需要生成更长的结果,比如需要调用多个工具,或者参数比较复杂,这时候256的token就不够用了,结果就被截断了。

这就解释了为什么问题是间歇性的。简单的请求,token数够用,不会报错;复杂的请求,token数不够,就会报错。线上环境的请求更复杂,所以报错更多;测试环境的请求简单,所以不容易报错。

看起来,问题的根源找到了:max_tokens参数设置得太小,导致复杂请求的结果被截断,解析失败。

第四步:验证假设

找到了可能的原因之后,下一步是验证。

我把max_tokens参数从256改成了1024,然后重新运行压测,看看错误率是不是下降了。

改了之后,压测了一会儿,错误率确实下降了,从30%降到了5%左右。但还是有一些错误,没有完全消失。

这说明,max_tokens太小是一个原因,但不是唯一的原因。还有其他的问题。

我继续查看剩下的错误请求的日志。这些请求的大模型返回结果,token数都在1024以内,没有被截断,但格式还是不对。

仔细分析这些结果,我发现了另一个问题。

有些结果,大模型返回的不是纯JSON,而是在JSON前后加了一些解释性的文字。比如,大模型会先写一段"我需要调用以下工具来完成任务",然后再返回JSON,最后再加一句"以上是我的计划"。

这种结果,用标准的JSON解析器是解析不了的,因为前后有多余的文字。

为什么大模型会返回这样的结果呢?我查看了我们的提示词。提示词里要求大模型返回JSON格式的结果,但没有明确要求只能返回JSON,不能有其他文字。大模型有时候会"画蛇添足",加上一些解释性的文字。

这是第二个原因:提示词不够严格,导致大模型返回的结果格式不统一。

第五步:修复提示词

找到了第二个原因之后,我开始修复提示词。

我修改了提示词,明确要求大模型只能返回JSON格式的结果,不能有任何其他文字。并且,给出了几个正确的示例,让大模型学习正确的格式。

改了提示词之后,重新运行压测,错误率又下降了,从5%降到了1%左右。

但还是有极少数的错误,没有完全消失。

我继续查看这些错误的日志。这些请求的大模型返回结果,格式是对的,JSON也能解析,但解析之后的内容有问题。

具体来说,JSON里的工具名称或者参数名称不对,和我们定义的不一致。比如,我们定义的工具叫"searchweb",大模型返回的是"websearch";我们定义的参数叫"query",大模型返回的是"question"。

这种情况,JSON能解析,但在调用工具的时候,会因为找不到对应的工具或者参数而报错。

这是第三个原因:大模型有时候会"创造"工具名称和参数名称,和我们定义的不一致。

第六步:添加容错处理

找到了第三个原因之后,我开始思考怎么解决。

这个问题,不能完全靠提示词解决,因为大模型总有"不听话"的时候。需要在代码层面添加容错处理。

我做了几个改进:

第一,添加工具名称的模糊匹配。当大模型返回的工具名称和我们定义的不完全一致时,用模糊匹配(比如编辑距离)找到最相似的工具。如果相似度超过阈值,就认为是这个工具。

第二,添加参数名称的映射。对于常见的参数名称变体,建立映射表。比如,"question"映射到"query","keyword"映射到"query"等。

第三,添加结果校验。解析大模型返回的结果之后,校验工具名称和参数是否合法。如果不合法,不要直接报错,而是把错误信息返回给大模型,让它重新生成正确的结果。这就是所谓的"自我修正"机制。

第四,添加重试机制。如果大模型连续几次返回错误的结果,再报错。给大模型几次修正的机会,不要一次错就放弃。

添加了这些容错处理之后,重新运行压测,错误率终于降到了0.1%以下,和平时的水平差不多了。

第七步:根因分析

问题解决了,但排查还没有结束。我需要做根因分析,搞清楚为什么会出现这个问题,以后怎么避免。

回顾整个排查过程,我发现这个问题的根源,不是某一个具体的bug,而是我们对Agent框架的理解不够深入,对大模型的"不可靠性"认识不足。

大模型和传统的程序不一样。传统的程序,输入确定,输出就确定;大模型的输出是概率性的,同样的输入,可能会有不同的输出。而且,大模型有时候会"犯傻",返回格式不对的结果,或者"创造"不存在的工具和参数。

我们在开发Agent服务的时候,用了开源的Agent框架,以为框架已经处理了这些问题。但实际上,框架只处理了最基本的情况,对于边界情况和异常情况,处理得不够完善。

而且,我们在上线之前,测试不够充分。只测试了正常的、简单的请求,没有测试复杂的、异常的请求,也没有做高并发的压测。导致这些问题,到了线上才暴露出来。

根因总结下来,有几点:

第一,对大模型的不可靠性认识不足,没有做充分的容错处理。

第二,对Agent框架的理解不够深入,没有发现框架的局限性。

第三,测试不充分,没有覆盖复杂场景和异常情况。

第四,监控和日志不够完善,错误信息太模糊,不利于排查。

第八步:改进措施

找到了根因之后,我们制定了一系列改进措施,避免类似的问题再次发生。

第一,完善容错处理。在Agent框架的基础上,添加更完善的容错处理,包括结果校验、自我修正、重试机制、模糊匹配等。确保大模型返回异常结果的时候,服务不会直接报错,而是能自动修正或者优雅降级。

第二,加强测试。建立更完善的测试用例,覆盖正常场景、复杂场景、异常场景。用自动化测试工具,模拟各种请求,确保服务的稳定性。上线之前,必须通过完整的测试,包括高并发压测。

第三,完善监控和日志。添加更详细的监控指标,比如大模型返回结果的格式错误率、token使用率、工具调用成功率等。完善日志,记录详细的错误信息和堆栈,便于排查问题。添加告警,当错误率升高的时候,及时通知开发人员。

第四,深入理解框架。组织团队成员,深入学习Agent框架的源码和原理,理解框架的工作机制和局限性。在框架的基础上,做二次开发,补充框架不足的地方。

第五,建立线上问题排查流程。制定线上问题排查的标准流程,包括问题确认、影响评估、复现、定位、修复、验证、根因分析、改进措施等。确保以后遇到线上问题,能高效地排查和解决。

排查Bug的经验总结

这次排查了一夜的Bug,让我积累了很多经验。总结一下,排查复杂系统的Bug,有几个关键点。

第一,保持冷静。线上出问题的时候,很容易紧张和慌乱。但紧张和慌乱解决不了问题,反而会影响判断。保持冷静,有条理地排查,才能高效地解决问题。

第二,先复现,再调试。能复现的Bug,就已经解决了一半。想办法在测试环境复现问题,复现了之后,才能调试和验证。如果只在线上环境出现,就要分析线上和测试环境的区别,找到触发问题的条件。

第三,从日志和监控入手。日志和监控是排查问题的第一手资料。仔细查看错误日志,分析监控数据,找到异常的地方。如果日志不够详细,就开启更详细的日志,或者添加自定义的日志。

第四,逐层排查,缩小范围。复杂系统的Bug,可能出现在任何一个环节。不要一开始就深入细节,而是先从宏观上判断问题可能出在哪一层,然后逐层深入,缩小范围,最后定位到具体的代码。

第五,大胆假设,小心验证。根据现象和经验,提出可能的原因,然后用实验来验证。验证一个假设,要么确认,要么排除。排除了不可能的,剩下的就是真相。

第六,不要只修表面,要找根因。找到问题的直接原因,修复了,还不够。要深入分析,找到问题的根本原因,然后从根本上解决,避免类似的问题再次发生。

第七,做好记录和复盘。排查完Bug之后,做好记录,包括问题现象、排查过程、原因分析、修复方法、改进措施等。然后组织团队复盘,总结经验教训,让团队都能从这次问题中学到东西。

写在最后

这次线上Bug排查,从晚上十点到第二天早上八点,整整十个小时。虽然很累,但收获很大。

不仅解决了线上的问题,还让我对Agent框架和大模型的特性有了更深的理解,也积累了复杂系统线上问题排查的经验。

线上出Bug,是每个开发者都不想遇到的事情。但既然遇到了,就要认真对待,把它当成一次学习和成长的机会。每解决一个复杂的Bug,你的技术能力和排查经验都会提升。

最后用一句话来结束这篇文章:"Bug不可怕,可怕的是不从Bug中学习。"

愿每一个开发者,都能少遇到线上Bug,遇到了也能高效地解决。