
1. 从一次批量插入后的 CursorNotFound 说起如果你在用 pymongo 往 MongoDB 里灌数据尤其是爬虫抓完一批、准备insert_many()批量落库的时候大概率见过这个报错pymongo.errors.CursorNotFound: Cursor not found。它出现的位置往往很迷惑——不是插入那一步报的而是你插完之后回头去遍历一个find()游标遍历到一半突然断了。很多人第一反应是「MongoDB 挂了」或者「数据没写进去」其实多数情况下是长链接在中间被服务端回收了游标跟着失效。这个场景特别容易出现在本地脚本和 AI 工具混用的开发者身上一边跑着批量插入脚本一边用 AI 编码助手帮你改查询逻辑脚本执行时间被拉长MongoDB 服务端默认的游标空闲超时大约 10 分钟一到游标就被清掉了。你再去for data in datas就炸了。这篇就围绕「mongo 批量插入数据」和「长链接」这两个关键词把 CursorNotFound 的复现、定位、修复讲清楚同时给出用 TaoToken 统一 Key 管理 API 通道的配置骨架让你在本地脚本和 AI 工具之间切换时不用来回改密钥。先说清楚这篇适合谁如果你写过insert_many()、调过orderedFalse、被no_cursor_timeout坑过或者你正在用 AI 助手帮你生成 pymongo 代码、需要一套统一的模型调用配置那这篇的步骤你可以直接照着做。核心检索词就三个——批量插入、长链接超时、CursorNotFound下面逐个拆。2. 先把 TaoToken 的 Key 和通道配好在排查游标问题之前我习惯先把「模型调用」这条链路和「数据库操作」这条链路分开。原因是很多 CursorNotFound 的排查会被 AI 工具打断——你让助手改一段查询它顺手把整个脚本重跑一遍长链接又被拖长。用 TaoToken 统一 Key 的好处是本地脚本、AI 编码工具、命令行助手共用一套 API 通道配置只写一次排查时不会因为密钥散落各处而分心。TaoToken 在这里扮演的是统一 API 入口的角色官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 。你需要先去控制台拿一个 Key控制台入口在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite Key 管理页面在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。拿到之后下面两套配置骨架可以直接抄。第一套是给支持settings.json的编辑器/工具用的比如某些 AI 编码插件{ taotoken: { api_base: https://taotoken.net/api, api_key: sk-你的TaoToken密钥, default_model: claude-sonnet, timeout_seconds: 120, max_retries: 2 }, mongo: { uri: mongodb://127.0.0.1:27017, db: spider_demo, collection: items } }第二套是给命令行工具或 Python 项目用的config.toml[taotoken] api_base https://taotoken.net/api api_key sk-你的TaoToken密钥 default_model claude-sonnet timeout_seconds 120 [mongo] uri mongodb://127.0.0.1:27017 db spider_demo collection items cursor_timeout_ms 600000注意cursor_timeout_ms这一项它对应的是我们后面要处理的游标超时。把它显式写进配置比散落在代码里更容易定位。TaoToken 的接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各语言 SDK 的调用示例配 Key 遇到问题可以先翻这里。提示Key 不要硬编码进提交到仓库的脚本里用环境变量或本地配置文件读取config.toml记得加进.gitignore。3. 可复制的批量插入与游标配置配置好之后进入正题。先看批量插入的正确姿势。很多人写insert_many()时忽略了ordered参数默认是True也就是按序写入一旦某条数据因为唯一索引冲突失败后面的全部不写。改成orderedFalse之后每条插入互不影响失败的那条单独报错其余照常落库。from pymongo import MongoClient, errors client MongoClient(mongodb://127.0.0.1:27017) db client[spider_demo] col db[items] data_list [{url: fhttps://example.com/{i}, title: fitem-{i}} for i in range(5000)] try: result col.insert_many(data_list, orderedFalse) print(inserted:, len(result.inserted_ids)) except errors.BulkWriteError as e: print(部分写入失败失败条数:, len(e.details[writeErrors]))这段跑完数据是进去了。问题出在下一步——你去遍历一个查询游标比如datas col.find({}, {_id: 0}) for data in datas: do_something(data) # 这一步耗时很长如果do_something每条要处理几百毫秒5000 条就是十几分钟远超 MongoDB 服务端默认的游标空闲超时默认 10 分钟。服务端一看这个游标这么久没动静直接回收客户端再next()就抛CursorNotFound。这就是「长链接」问题的本质不是网络断了是游标在服务端被判定为空闲。修复方式有两种按场景选。第一种是给游标设置永不超时用完手动关闭datas col.find({}, {_id: 0}, no_cursor_timeoutTrue) try: for data in datas: do_something(data) finally: datas.close()第二种是分批拉取用batch_size控制每次从服务端取多少减少单次游标存活时间datas col.find({}, {_id: 0}).batch_size(500) for data in datas: do_something(data)两种可以叠加使用。我实测下来no_cursor_timeoutTrue配合finally里close()最稳但要注意游标不主动关闭会一直占服务端资源脚本异常退出时容易留下悬挂游标所以try/finally不能省。4. 复现一次游标失效并验证修复光看代码不够得亲手复现一次才知道问题出在哪。下面这段脚本故意让处理变慢制造游标超时import time from pymongo import MongoClient from pymongo.errors import CursorNotFound client MongoClient(mongodb://127.0.0.1:27017) col client[spider_demo][items] # 复现不设 no_cursor_timeout处理耗时拉长 datas col.find({}, {_id: 0}) count 0 try: for data in datas: time.sleep(0.2) # 模拟耗时处理 count 1 except CursorNotFound as e: print(f游标在第 {count} 条后失效: {e})跑起来后如果数据量够大、处理够慢你会在某个点看到CursorNotFound。这时候把find()改成带no_cursor_timeoutTrue的版本再跑一次同样的time.sleep(0.2)游标不会再断。验证成功的标志是脚本完整遍历完所有数据没有异常抛出。如果你用 TaoToken 的模型对话能力来辅助排查可以把报错原文贴进去让它帮你分析入口在 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 。比如你贴CursorNotFound: Cursor not found, cursor id: 123456它能帮你确认是超时还是集合被 drop 导致的。注意区分如果集合在遍历过程中被drop()或rename()游标也会失效这种情况加no_cursor_timeout没用得从操作顺序上解决。验证修复时建议同时观察 MongoDB 服务端的游标状态。连上 mongo shell 执行db.serverStatus().metrics.cursor看open.total和timedOut两个计数。修复前timedOut会随脚本运行增长修复后应该保持稳定。这一步能把「长链接超时」这个模糊描述定位到具体指标上。5. 本篇常见错误排查排查过程中有几个坑反复出现列出来对照。第一个坑把no_cursor_timeoutTrue当成万能药。它只解决「游标空闲超时」解决不了「集合被删」「连接被防火墙掐断」「副本集主从切换」这几类问题。如果加了它还是报 CursorNotFound先确认集合在遍历期间有没有被其他脚本改动。第二个坑insert_many()的ordered参数和游标超时混为一谈。这两个是独立问题。orderedFalse解决的是批量写入时单条失败拖累整体跟游标超时没关系。有人看到批量插入报错就以为是游标问题方向就偏了。第三个坑TaoToken 的 Key 配错导致 AI 工具反复重试间接拉长脚本运行时间。如果你在settings.json里把api_base写成了带路径的完整地址或者 Key 前后有空格请求会失败重试脚本卡住长链接问题被放大。检查api_base是否就是https://taotoken.net/apiKey 是否从 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 正确复制。第四个坑游标用完不关。no_cursor_timeoutTrue的游标不会自动过期脚本里如果没写close()跑几次之后服务端会堆积一堆打开游标db.serverStatus().metrics.cursor.open.total一直涨最终影响新查询。养成try/finally的习惯。第五个坑把batch_size设得过大或过小。设太大单次网络传输压力大设太小往返次数多整体更慢反而更容易触发超时。500 到 1000 是比较稳的区间具体看单条文档大小。注意如果你在副本集或分片集群上跑游标超时行为可能和单机不同no_cursor_timeout在分片场景下要配合mongos的参数一起看别只改客户端。6. 把配置和排查动作固定下来排查完这一轮我的做法是把「游标超时」相关的参数全部收进config.toml代码里只读配置不写魔法数字。这样下次再遇到 CursorNotFound先看配置里的cursor_timeout_ms和batch_size再决定是调参数还是改代码逻辑。TaoToken 的 Key 也放在同一份配置里本地脚本和 AI 工具共用排查时不会因为密钥不一致产生额外变量。如果你需要长期跑批量任务、还要接 AI 编码助手可以考虑用 TaoToken 的 Coding Plan入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它把模型调用通道固定下来配合上面的settings.json骨架脚本和助手之间切换不用重新配 Key。Claude Code 相关的接入说明在 https://taotoken.net/claude-code?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 如果你用命令行助手改 pymongo 代码可以参考。最后留一个我踩过的坑no_cursor_timeoutTrue的游标在for循环里如果break提前退出finally里的close()一定要执行否则那个游标会一直挂在服务端。我一开始图省事没写finally跑了一晚上批量任务第二天db.serverStatus().metrics.cursor.open.total上千新查询开始变慢排查了半天才发现是游标没关。把close()补上之后同样的任务跑完打开游标数回到个位数。