3.6 线上问题排查实战:让你的 AI 服务 7x24 小时稳定运行

2 阅读1分钟

导语:欢迎来到第三周的终极实战!我们已经成功地将“旅小智”部署到了云端。但是,部署成功只是一个新的开始。在真实的生产环境中,系统会在你意想不到的时间、以你意想不到的方式出现问题。当凌晨三点,告警短信将你从睡梦中唤醒,告诉你 AI 服务正在大量报错时,你该如何应对?本章将是一次模拟的“线上消防演练”。我们将抛开所有新功能的开发,专注于每一个生产环境工程师都必须具备的核心能力——问题排查(Troubleshooting)。我将带你进入“作战室”,模拟几个最典型的线上故障场景,并像一位经验丰富的老兵一样,一步步带你分析日志、追踪链路、定位根因,并最终解决问题,让你的 AI 服务恢复稳定。

目录

  1. “On-Call”工程师的心态:从慌乱到从容
    • 第一原则:止血优先,恢复服务是最高目标
    • 第二原则:保留现场,先快照后分析
    • 第三原则:由表及里,从监控到日志,再到代码
  2. “作战室”场景一:P0 级告警!API 延迟飙升,用户反馈 AI “卡死了”
    • 步骤 1:看“心电图”——检查应用性能监控 (APM)
      • 打开 Datadog/Langfuse,查看服务 P95/P99 延迟曲线。
      • 定位到是哪个 API 端点变慢(/invoke)。
      • 查看 /invoke 的调用链路(Trace),发现是 LLM 调用耗时极长。
    • 步骤 2:深入分析——检查 LLM 服务商状态
      • 访问 OpenAI/DeepSeek 的 Status 页面,确认是否存在官方故障。
    • 步骤 3:代码侧排查——检查 Token 数量和 Prompt
      • 查看日志,发现某个 thread_id 的请求 messages 列表异常地长。
      • 根因定位:某个用户的对话持续了上百轮,记忆管理策略(如简单截断)失效,导致每次请求都发送了巨大的上下文,超出了 LLM 的最佳处理范围。
    • 步骤 4:解决方案
      • 短期“止血”:在后端紧急加入一个最大 Token 限制的硬编码,对于超长请求直接返回错误,避免影响其他用户。
      • 长期“根治”:优化记忆管理策略,将 TokenTruncationMemory 升级为 SummarizationMemoryVectorMemory
  3. “作战室”场景二:数据库连接池占满,大量请求失败
    • 步骤 1:看“仪表盘”——检查数据库监控
      • 打开 AWS RDS 监控仪表盘,发现“数据库连接数”指标达到上限。
    • 步骤 2:分析日志——寻找线索
      • 应用日志中出现大量 TimeoutError: Can't get a connection from the pool 或类似的数据库连接超时错误。
    • 步骤 3:代码侧排查——检查数据库连接的使用
      • 根因定位:我们的 LangGraph Checkpointer (如 PostgresSaver) 在每次读写状态时,都会创建一个新的数据库连接,并且没有在使用后正确地释放它。在高并发情况下,这些被占用的连接迅速耗尽了连接池。
    • 步骤 4:解决方案
      • 短期“止血”:重启应用服务,释放所有旧的数据库连接。
      • 长期“根治”:修改 Checkpointer 的初始化逻辑。在应用启动时,创建一个全局的、共享的数据库连接池(如使用 SQLAlchemycreate_engine),并确保所有 Checkpointer 实例都从这个共享的连接池中获取和释放连接。
  4. “作战室”场景三:AI 胡言乱语,工具调用逻辑错乱
    • 问题描述:用户反馈,当我问天气时,Agent 却去调用了计算器。
    • 步骤 1:复现与追踪——使用 Langfuse/LangSmith
      • 在可观测性平台中,找到出问题的那个 thread_id 的完整调用链路。
    • 步骤 2:检查 Prompt 和模型输出
      • 查看 Supervisor Agent 的输入(用户问题、历史消息)和输出(tool_calls)。
      • 根因定位 1(Prompt 污染):发现该用户的上一轮对话中包含大量数字和计算公式,这些“上下文”干扰了 LLM 对新问题“天气怎么样”的理解,使其错误地关联到了“计算器”工具。
      • 根因定位 2(工具描述不清):发现我们的“计算器”工具 description 写的是“可以处理任何计算和逻辑”,过于宽泛,导致模型在面对不确定性时倾向于选择它。
    • 步骤 3:解决方案
      • 优化 Prompt:在 Supervisor 的 System Prompt 中加入更强的指令:“你必须根据用户最新的提问来决定使用哪个工具,忽略历史对话中的无关信息。”
      • 优化工具描述:将计算器工具的 description 修改得更具体:“一个只能用于执行数学表达式的计算器,例如 2+2sin(pi/2)。不能用于回答问题。”
      • 引入 Few-Shot 示例:在 Prompt 中加入一个例子,展示当面对天气问题时,应该如何正确地选择 weather 工具。
  5. 线上问题排查的“瑞士军刀”
    • docker logs -f --tail 100 <container>: 实时查看最新的 100 行日志。
    • docker exec -it <container> /bin/sh: 进入容器内部,进行“案发现场”勘查。
    • curl: 在容器内外测试网络连通性和 API 响应。
    • APM/可观测性平台:最重要的工具,提供上帝视角。
  6. 总结:建立“免疫系统”,从救火到防火

1. “On-Call”工程师的心态:从慌乱到从容

当生产环境的告警响起时,保持冷静是第一要务。一个经验丰富的工程师会遵循一套成熟的心法:

  • 第一原则:止血优先 (Stop the Bleeding)
    • 你的首要目标不是立刻找到问题的根源,而是尽快恢复服务的正常运行。这可能意味着一些临时性的、甚至“粗暴”的操作,比如重启服务、回滚到上一个稳定版本、或者暂时禁用某个出问题的功能。
  • 第二原则:保留现场 (Preserve the Scene)
    • 在进行任何恢复操作之前,尽可能地保留“犯罪现场”。截取监控图表、备份关键的日志文件、保存出错请求的快照。这些都是你事后进行根本原因分析 (Root Cause Analysis, RCA) 的宝贵线索。
  • 第三原则:由表及里 (Top-down Approach)
    • 不要一上来就扎进代码里。从最高层的、最宏观的监控指标开始:
      1. 应用监控 (APM):延迟、错误率、吞吐量(RPS)是否异常?
      2. 系统监控:CPU、内存、网络、磁盘 I/O 是否异常?
      3. 依赖服务监控:数据库、缓存、LLM 服务商的 API 是否正常?
    • 通过这些高层指标,逐步缩小问题的范围,最后才定位到具体的日志和代码。

2. “作战室”场景一:P0 级告警!API 延迟飙升

告警描述P99 latency for /invoke API is over 30 seconds. (99% 的 /invoke 请求响应时间超过 30 秒)。用户在前端看到的现象是 AI 一直在“转圈圈”,没有任何响应。

步骤 1:看“心电图”——检查 APM

打开你的 APM 工具(如 Datadog)。

  • 服务概览:确认是 backend 服务的 /invoke 端点延迟急剧上升。错误率可能没有明显变化。
  • 链路追踪 (Trace):随机选择几个耗时超长的请求 Trace。你会清晰地看到,整个请求的耗时几乎全部集中在 LangGraph.astream_events 这个函数调用上,再往下钻取,发现是 ChatOpenAI.invoke 这一步耗时超过了 25 秒。
  • 初步结论:问题出在对 LLM 的调用上。

步骤 2:深入分析——检查 LLM 服务商状态

  • 立即打开 OpenAI Status (status.openai.com) 或你使用的其他 LLM 服务商的状态页面。
  • 情况 A:如果官方页面显示“Major Outage”,那么恭喜你,问题不是你的锅。你可以向业务方通报情况,然后泡杯咖啡,等待官方修复。
  • 情况 B:官方页面显示一切正常。那么问题就在我们自己这边。

步骤 3:代码侧排查——检查 Token 数量和 Prompt

既然是 LLM 调用慢,要么是 LLM 服务商的问题,要么是我们给它的“输入”有问题。

  • 查看结构化日志:筛选出那几个耗时超长的 Trace 对应的日志。在调用 LLM 之前的日志中,打印出 messages 的内容或者其长度和 Token 数量。
  • 发现异常:你注意到,这些慢请求的 messages 列表的 Token 总数都超过了 30,000,甚至更高。而正常的请求通常只有几千 Token。
  • 根因定位:通过 thread_id 关联到具体的用户会话。你发现这是一个“话痨”用户,他已经和“旅小智”连续对话了上百轮。我们的记忆管理策略(比如只是简单的 InMemoryMemory 或没有限制的 SqliteSaver)导致了上下文无限增长。每次请求,我们都把这上百轮的完整对话历史全部塞给了 LLM,导致模型需要处理极长的上下文,性能急剧下降。

步骤 4:解决方案

  • 短期“止血”
    1. app/main.py/invoke 端点,加入一个硬编码的保护逻辑:
      # 伪代码
      total_tokens = count_tokens(request.history)
      if total_tokens > 16000:
          return JSONResponse(status_code=400, content={"error": "Conversation history is too long. Please start a new session."})
      
    2. 部署这个“热修复”(Hotfix),可以立即阻止超长请求拖垮整个服务。
  • 长期“根治”
    1. 召开事后复盘会(Post-mortem)。
    2. 将修复“记忆管理策略”作为一个高优先级任务。
    3. TripState 实现一个更健壮的记忆模块,比如 SummarizationMemory,当对话超过一定轮次后,自动将旧的对话进行总结,而不是无限制地累加。

3. “作战室”场景二:数据库连接池占满

告警描述High DB connection count & API error rate > 5%

步骤 1:看“仪表盘”——检查数据库监控

  • 打开 AWS RDS 监控页,看到 DatabaseConnections 指标已经顶到了设置的 max_connections 上限。这意味着新的应用请求无法从数据库获取到连接,从而导致请求失败。

步骤 2:分析日志——寻找线索

  • backend 服务的日志中,搜索 databaseconnection 相关的错误。你会看到大量的 TimeoutError: Pool timeoutOperationalError: remaining connection slots are reserved for non-replication superuser connections。这确认了应用确实是连接不上数据库了。

步骤 3:代码侧排查——检查数据库连接的使用

  • 审查代码:问题指向了我们使用 SqliteSaver (或 PostgresSaver) 的地方。我们是怎么初始化它的?
    # 可疑代码
    def get_agent_app():
        memory_saver = SqliteSaver.from_conn_string(":memory:")
        app = workflow.compile(checkpointer=memory_saver)
        return app
    
    @app.post("/invoke")
    def invoke_agent(request: ...):
        # 每次请求都重新获取一次 app?
        app = get_agent_app()
        # ...
    
  • 根因定位:经过审查,你发现了一个致命的设计缺陷。代码在每次 API 请求时,都重新创建了一个 SqliteSaver (或 PostgresSaver) 的实例。而每个 Saver 实例在创建时,都会初始化自己的数据库连接池,并从中获取一个或多个连接。这些连接在使用后,因为 Saver 实例本身在请求结束后就被销毁了,导致连接没有被正确地归还到池中或关闭。日积月累,这些被“泄露”的连接占满了数据库的所有可用连接数。

步骤 4:解决方案

  • 短期“止血”
    1. 最快的方式是重启 backend 服务的所有容器实例。重启会断开所有现存的 TCP 连接,从而释放数据库的连接数。服务会立即恢复。
  • 长期“根治”
    1. 遵循单例模式:数据库连接池和 LangGraph 的 app 实例,都应该是全局单例,在应用启动时创建一次,并在所有请求之间共享。
    2. 重构 main.py:
      # main.py (正确的方式)
      
      # 在全局作用域创建 saver 和 app,只在应用启动时执行一次
      memory_saver = SqliteSaver.from_conn_string("sqlite:///data/trip.sqlite")
      app = workflow.compile(checkpointer=memory_saver)
      
      @app.post("/invoke")
      async def invoke_agent(request: ...):
          # 直接使用全局的 app 实例
          async for event in app.astream_events(...):
              ...
      

4. “作战室”场景三:AI 胡言乱语,工具调用逻辑错乱

这是一个更棘手的、与 AI 行为本身相关的问题。

问题描述:用户投诉,他明确地问“厦门有什么好吃的?”,但“旅小智”却回复说“3*5=15”。

步骤 1:复现与追踪——使用 Langfuse/LangSmith

  • 这是 APM 和可观测性平台大放异彩的地方。通过用户提供的 thread_id,你立刻就能在 Langfuse 中找到完整的对话链路。

步骤 2:检查 Prompt 和模型输出

  • Trace 分析
    1. 你看到最新的用户输入确实是“厦门有什么好吃的?”。
    2. 你检查 Supervisor Agent 节点的输入,发现 messages 历史中,上一轮用户问的是“帮我算一下从上海到厦门 800 公里的油费,假设每公里 0.6 元,加上 200 元的过路费,总共多少钱?”。
    3. 你检查 Supervisor Agent 节点的输出,发现它生成的 tool_calls 竟然是调用 python_calculator,参数是 800*0.6+200
  • 根因定位上下文污染 (Context Bleeding)。LLM 在处理新问题“厦门有什么好吃的?”时,受到了上一轮对话中强烈的“计算”意图的干扰。它错误地将新问题与旧上下文结合起来,认为用户仍然想做计算,从而忽略了新问题本身的意图。

步骤 3:解决方案

这是一个典型的 Prompt Engineering 问题。

  1. 优化 Supervisor Prompt:在 System Prompt 中增加更强的指令,来强调对最新问题的关注。
    # Supervisor Prompt 新增指令
    "IMPORTANT: You MUST prioritize the user's LATEST query to decide the next step. While conversation history provides context, your primary decision should be based on the most recent user input."
    
  2. 优化 Few-Shot 示例:如果问题依然存在,可以在 Prompt 中加入一个“反例”作为 Few-Shot 示例,向模型展示如何处理这种情况。
    // Few-Shot Example in Prompt
    {
        "role": "user", "content": "1+1=?"
    },
    {
        "role": "assistant", "content": "2"
    },
    {
        "role": "user", "content": "What's the weather in London?"
    },
    {
        "role": "assistant", "tool_calls": [{"name": "search_weather", ...}] 
    }
    
    这个例子明确地告诉模型,在完成一个计算任务后,下一个完全不同意图的任务应该调用不同的工具,而不是继续“计算”。

5. 线上问题排查的“瑞士军刀”

  • docker logs: 你的第一双眼睛。
  • docker exec: 你的双手,让你能进入“案发现场”。
  • curl / netcat: 你的听诊器,用于探测网络和端口。
  • APM / 可观测性平台: 你的“上帝之眼”,提供全景视图和调用链追踪,是排查复杂 AI 行为问题的必备神器。

6. 总结:建立“免疫系统”,从救火到防火

问题排查不仅仅是“救火”,更重要的是**“防火”**。每一次线上故障,都是一次宝贵的学习机会。

一次优秀的故障处理流程应该是: 告警 -> 止血 -> 定位 -> 根治 -> 复盘 -> 预防

  • 复盘 (Post-mortem):问题解决后,召集相关人员,详细记录问题发生的时间线、影响范围、处理过程和根本原因。不指责,只关注事实。
  • 预防 (Prevention):从根因出发,制定后续的改进措施。
    • “延迟飙升” -> 改进措施:为所有 Agent 添加健壮的记忆管理策略,并增加上下文长度的监控告警。
    • “数据库连接池占满” -> 改进措施:代码审查流程中加入对资源(连接、文件句柄)管理部分的检查,并进行压力测试。
    • “AI 胡言乱语” -> 改进措施:建立 Prompt 的评估和回归测试集,每次修改 Prompt 都需要通过测试。

通过不断地“救火”和“防火”,你的 AI 系统会像人一样,逐渐建立起强大的“免疫系统”,变得越来越稳定、越来越健壮,最终实现 7x24 小时的稳定运行。