导语:欢迎来到第三周的终极实战!我们已经成功地将“旅小智”部署到了云端。但是,部署成功只是一个新的开始。在真实的生产环境中,系统会在你意想不到的时间、以你意想不到的方式出现问题。当凌晨三点,告警短信将你从睡梦中唤醒,告诉你 AI 服务正在大量报错时,你该如何应对?本章将是一次模拟的“线上消防演练”。我们将抛开所有新功能的开发,专注于每一个生产环境工程师都必须具备的核心能力——问题排查(Troubleshooting)。我将带你进入“作战室”,模拟几个最典型的线上故障场景,并像一位经验丰富的老兵一样,一步步带你分析日志、追踪链路、定位根因,并最终解决问题,让你的 AI 服务恢复稳定。
目录
- “On-Call”工程师的心态:从慌乱到从容
- 第一原则:止血优先,恢复服务是最高目标
- 第二原则:保留现场,先快照后分析
- 第三原则:由表及里,从监控到日志,再到代码
- “作战室”场景一: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升级为SummarizationMemory或VectorMemory。
- 步骤 1:看“心电图”——检查应用性能监控 (APM)
- “作战室”场景二:数据库连接池占满,大量请求失败
- 步骤 1:看“仪表盘”——检查数据库监控
- 打开 AWS RDS 监控仪表盘,发现“数据库连接数”指标达到上限。
- 步骤 2:分析日志——寻找线索
- 应用日志中出现大量
TimeoutError: Can't get a connection from the pool或类似的数据库连接超时错误。
- 应用日志中出现大量
- 步骤 3:代码侧排查——检查数据库连接的使用
- 根因定位:我们的 LangGraph Checkpointer (如
PostgresSaver) 在每次读写状态时,都会创建一个新的数据库连接,并且没有在使用后正确地释放它。在高并发情况下,这些被占用的连接迅速耗尽了连接池。
- 根因定位:我们的 LangGraph Checkpointer (如
- 步骤 4:解决方案
- 短期“止血”:重启应用服务,释放所有旧的数据库连接。
- 长期“根治”:修改 Checkpointer 的初始化逻辑。在应用启动时,创建一个全局的、共享的数据库连接池(如使用
SQLAlchemy的create_engine),并确保所有 Checkpointer 实例都从这个共享的连接池中获取和释放连接。
- 步骤 1:看“仪表盘”——检查数据库监控
- “作战室”场景三:AI 胡言乱语,工具调用逻辑错乱
- 问题描述:用户反馈,当我问天气时,Agent 却去调用了计算器。
- 步骤 1:复现与追踪——使用 Langfuse/LangSmith
- 在可观测性平台中,找到出问题的那个
thread_id的完整调用链路。
- 在可观测性平台中,找到出问题的那个
- 步骤 2:检查 Prompt 和模型输出
- 查看 Supervisor Agent 的输入(用户问题、历史消息)和输出(
tool_calls)。 - 根因定位 1(Prompt 污染):发现该用户的上一轮对话中包含大量数字和计算公式,这些“上下文”干扰了 LLM 对新问题“天气怎么样”的理解,使其错误地关联到了“计算器”工具。
- 根因定位 2(工具描述不清):发现我们的“计算器”工具
description写的是“可以处理任何计算和逻辑”,过于宽泛,导致模型在面对不确定性时倾向于选择它。
- 查看 Supervisor Agent 的输入(用户问题、历史消息)和输出(
- 步骤 3:解决方案
- 优化 Prompt:在 Supervisor 的 System Prompt 中加入更强的指令:“你必须根据用户最新的提问来决定使用哪个工具,忽略历史对话中的无关信息。”
- 优化工具描述:将计算器工具的
description修改得更具体:“一个只能用于执行数学表达式的计算器,例如2+2或sin(pi/2)。不能用于回答问题。” - 引入 Few-Shot 示例:在 Prompt 中加入一个例子,展示当面对天气问题时,应该如何正确地选择
weather工具。
- 线上问题排查的“瑞士军刀”
docker logs -f --tail 100 <container>: 实时查看最新的 100 行日志。docker exec -it <container> /bin/sh: 进入容器内部,进行“案发现场”勘查。curl: 在容器内外测试网络连通性和 API 响应。- APM/可观测性平台:最重要的工具,提供上帝视角。
- 总结:建立“免疫系统”,从救火到防火
1. “On-Call”工程师的心态:从慌乱到从容
当生产环境的告警响起时,保持冷静是第一要务。一个经验丰富的工程师会遵循一套成熟的心法:
- 第一原则:止血优先 (Stop the Bleeding)
- 你的首要目标不是立刻找到问题的根源,而是尽快恢复服务的正常运行。这可能意味着一些临时性的、甚至“粗暴”的操作,比如重启服务、回滚到上一个稳定版本、或者暂时禁用某个出问题的功能。
- 第二原则:保留现场 (Preserve the Scene)
- 在进行任何恢复操作之前,尽可能地保留“犯罪现场”。截取监控图表、备份关键的日志文件、保存出错请求的快照。这些都是你事后进行根本原因分析 (Root Cause Analysis, RCA) 的宝贵线索。
- 第三原则:由表及里 (Top-down Approach)
- 不要一上来就扎进代码里。从最高层的、最宏观的监控指标开始:
- 应用监控 (APM):延迟、错误率、吞吐量(RPS)是否异常?
- 系统监控:CPU、内存、网络、磁盘 I/O 是否异常?
- 依赖服务监控:数据库、缓存、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:解决方案
- 短期“止血”:
- 在
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."}) - 部署这个“热修复”(Hotfix),可以立即阻止超长请求拖垮整个服务。
- 在
- 长期“根治”:
- 召开事后复盘会(Post-mortem)。
- 将修复“记忆管理策略”作为一个高优先级任务。
- 为
TripState实现一个更健壮的记忆模块,比如SummarizationMemory,当对话超过一定轮次后,自动将旧的对话进行总结,而不是无限制地累加。
3. “作战室”场景二:数据库连接池占满
告警描述:High DB connection count & API error rate > 5%。
步骤 1:看“仪表盘”——检查数据库监控
- 打开 AWS RDS 监控页,看到
DatabaseConnections指标已经顶到了设置的max_connections上限。这意味着新的应用请求无法从数据库获取到连接,从而导致请求失败。
步骤 2:分析日志——寻找线索
- 在
backend服务的日志中,搜索database或connection相关的错误。你会看到大量的TimeoutError: Pool timeout或OperationalError: 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:解决方案
- 短期“止血”:
- 最快的方式是重启
backend服务的所有容器实例。重启会断开所有现存的 TCP 连接,从而释放数据库的连接数。服务会立即恢复。
- 最快的方式是重启
- 长期“根治”:
- 遵循单例模式:数据库连接池和 LangGraph 的
app实例,都应该是全局单例,在应用启动时创建一次,并在所有请求之间共享。 - 重构
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(...): ...
- 遵循单例模式:数据库连接池和 LangGraph 的
4. “作战室”场景三:AI 胡言乱语,工具调用逻辑错乱
这是一个更棘手的、与 AI 行为本身相关的问题。
问题描述:用户投诉,他明确地问“厦门有什么好吃的?”,但“旅小智”却回复说“3*5=15”。
步骤 1:复现与追踪——使用 Langfuse/LangSmith
- 这是 APM 和可观测性平台大放异彩的地方。通过用户提供的
thread_id,你立刻就能在 Langfuse 中找到完整的对话链路。
步骤 2:检查 Prompt 和模型输出
- Trace 分析:
- 你看到最新的用户输入确实是“厦门有什么好吃的?”。
- 你检查 Supervisor Agent 节点的输入,发现
messages历史中,上一轮用户问的是“帮我算一下从上海到厦门 800 公里的油费,假设每公里 0.6 元,加上 200 元的过路费,总共多少钱?”。 - 你检查 Supervisor Agent 节点的输出,发现它生成的
tool_calls竟然是调用python_calculator,参数是800*0.6+200。
- 根因定位:上下文污染 (Context Bleeding)。LLM 在处理新问题“厦门有什么好吃的?”时,受到了上一轮对话中强烈的“计算”意图的干扰。它错误地将新问题与旧上下文结合起来,认为用户仍然想做计算,从而忽略了新问题本身的意图。
步骤 3:解决方案
这是一个典型的 Prompt Engineering 问题。
- 优化 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." - 优化 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 小时的稳定运行。