Notion AI 接口出现响应超时,很多时候不是模型推理速度慢,而是数据准备链路把时间耗光了。一次典型的调用可能需要先从数据库读取Notion页面内容、再组装上下文、最后请求AI服务,中间任何一环出现慢查询或限频重试,都会让整个请求超过网关或客户端设置的超时阈值。优化这类问题,关键是压缩数据库耗时,并把外部API请求控制在一个可预期、可重试的范围内。

一、先定位超时发生在哪一层
Notion AI 调用链通常包含三个主要节点:应用服务、Notion API 以及最终的数据存储层。请求进入应用服务后,服务端一般要先查询数据库,拿到之前同步过来的页面块、属性值或对话历史,再把它们拼接成AI上下文。如果这个查询耗时从几十毫秒膨胀到几秒,后面的AI调用即使正常返回,整体响应也已经超时。
另一个经常被忽略的点是 Notion API 自身的频率限制。Notion 对不同套餐的每秒请求数有明确约束,如果在短时间内集中调用页面读取、搜索或更新接口,服务端会返回 429 Too Many Requests。很多团队在遇到 429 后会立即无差别重试,结果进一步加剧限频,最终把一次普通请求拖成超时。因此定位问题不能只看AI接口本身,要结合慢查询日志、应用追踪和API响应码一起判断。
建议先在应用日志中记录三个时间点:数据库查询开始与结束时间、Notion API 调用开始与结束时间、AI推理耗时。哪一段占比最高,就从哪一段开始优化。如果数据库查询超过 500 毫秒,优先处理索引;如果 429 出现频率高,优先处理限流和重试策略。
二、数据库索引优化:从慢查询到毫秒级返回
假设我们用一张表存储从 Notion 同步过来的页面块,表结构类似下面这样。每次请求AI前,需要按页面ID和块类型拉取最近的正文内容:
CREATE TABLE notion_blocks (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
page_id VARCHAR(64) NOT NULL,
type VARCHAR(32) NOT NULL,
content TEXT,
created_time DATETIME NOT NULL,
updated_time DATETIME NOT NULL
);
业务查询经常写成:
SELECT id, type, content FROM notion_blocks WHERE page_id = 'a1b2c3d4' AND type = 'paragraph' ORDER BY created_time DESC LIMIT 50;
如果只在 page_id 上建了单列索引,数据库会先通过索引查到该页面的所有块,再按 type 过滤,最后对 created_time 做排序。当页面块数量很多时,排序操作会消耗大量内存和CPU,查询延迟明显上升。这时应该使用联合索引,把过滤和排序都交给索引完成:
CREATE INDEX idx_page_type_created ON notion_blocks(page_id, type, created_time DESC);
联合索引遵循最左前缀原则,上面的索引可以同时覆盖 page_id 等值查询、type 等值查询以及 created_time 排序。执行计划中如果看到 type=ref、rows 很小,且 Extra 没有出现 Using filesort,说明索引已经生效。
还可以进一步使用覆盖索引减少回表。把查询需要的列加入索引并不总是合理,因为索引过大同样影响写入性能。但如果查询只返回少量字段,例如 id 和 type,可以针对高频查询创建 (page_id, type, created_time, id) 这样的索引,让数据库直接从索引中取数据,无需再回表读 content 列。实际项目里要先通过 EXPLAIN 或 EXPLAIN ANALYZE 确认执行计划,再决定是否需要覆盖索引。
另外要避免在索引列上使用函数或隐式类型转换。比如 WHERE LOWER(type) = 'paragraph' 会导致索引失效,数据库只能扫描全表。分页深翻也不建议使用大偏移量 OFFSET,可以采用游标分页,基于上一页最后一条的 created_time 和 id 继续查询,保持每次扫描范围稳定。
三、Notion API频率限制与重试策略
Notion API 在请求过频时会返回 429 状态码,并在响应头中给出 Retry-After 提示。很多实现会直接捕获异常后立即重试,这种策略在轻度限频时还能工作,一旦遇到突发流量,短时间内大量重试反而会触发更严格的限频,甚至导致账号临时被封禁。
更稳妥的做法是在应用内部实现令牌桶限流,让发往 Notion 的请求保持均匀节奏。下面是一个简单的Python令牌桶示例:
import time
class TokenBucket:
def __init__(self, rate, capacity):
self.rate = rate
self.capacity = capacity
self.tokens = float(capacity)
self.last_time = time.monotonic()
def consume(self, tokens=1):
now = time.monotonic()
elapsed = now - self.last_time
self.last_time = now
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
if self.tokens < tokens:
return False
self.tokens -= tokens
return True
发请求前先通过 consume() 获取令牌,拿不到就等待或放入队列,避免瞬间打满 Notion 的请求额度。对于仍然出现的 429 响应,需要按照 Retry-After 的值进行指数退避,而不是固定间隔重试:
import time
import requests
def call_notion_api(url, headers, max_retries=5):
for attempt in range(max_retries):
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200:
return response.json()
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", "2"))
sleep_time = retry_after + 2 ** attempt
time.sleep(sleep_time)
continue
if response.status_code >= 500:
time.sleep(1 + attempt)
continue
return None
return None
这段代码把 429 的等待时间交给服务端返回值和本地尝试次数共同决定,既尊重 Notion 的限流要求,又能在服务端给出明确等待时间时快速恢复。对于 5xx 错误,指数退避可以避免在服务不稳定时反复冲击接口。
还有一种常见优化是减少不必要的请求。Notion 页面内容通常会包含多个块,应该尽量使用分页参数一次拉取更多结果,而不是在循环里逐块请求。对于已经同步过的页面,可以设置本地缓存,只有在内容变化时才重新调用 Notion API。这样既能降低限频概率,也能显著缩短数据准备阶段的时间。
四、结合缓存和异步拆分的工程落地
数据库索引优化解决的是查询快慢问题,限流重试解决的是API调用成功率问题,但要让整个系统稳定,还需要在架构上把AI推理和无状态数据准备拆开。同步等待数据库查询、Notion API 调用、AI推理三步串行执行,任何一步抖动都会拉高整体延迟。
建议把页面内容同步和上下文组装做成异步任务。当页面发生变化时,Webhook 或定时任务触发同步,将最新块内容写入数据库和缓存。AI请求到来时,应用服务直接读取本地缓存或数据库,不再实时请求 Notion。这样AI主流程只依赖数据库和缓存,延迟可预测,也避免把 Notion 限频问题带进用户请求链路。
缓存可以放在 Redis 等内存存储中,键设计为 page:{page_id}:blocks,值保存JSON序列化后的上下文片段。设置合理的过期时间,并根据 Notion 内容更新频率调整。如果担心缓存穿透,可以在数据库查询不到数据时也写入空值并设置短过期时间。缓存击穿则可以通过互斥锁或提前重建来处理。
异步拆分还有一个好处是可以对队列进行削峰。当大量AI请求同时到达时,数据准备可以并行完成,但 Notion API 请求通过令牌桶串行化,数据库查询通过连接池和索引承担并发,AI推理则根据服务能力设置并发上限。三层之间通过队列和缓存缓冲,整体吞吐反而更高。
五、监控指标与优化验证
优化完成后,需要持续观察几个关键指标:数据库慢查询数量、查询平均耗时、Notion API 429 次数、重试率、AI端到端响应时间和超时率。慢查询日志可以帮我们发现索引遗漏,应用追踪可以展示每次请求在各阶段的耗时占比。
如果数据库查询仍然缓慢,可以通过 EXPLAIN ANALYZE 查看实际执行时间,并观察索引是否被选择、扫描行数是否下降。索引没有生效的常见原因包括:联合索引顺序错误、查询条件包含函数或类型转换、优化器估算偏差等。必要时可以使用 FORCE INDEX 强制指定索引,但要谨慎评估通用性。
对于API限频,监控需要区分主动限流和被动受限。主动限流是通过令牌桶控制发出的请求数,被动受限是收到429后重试的次数。如果被动受限次数持续增加,说明令牌速率设置仍然过高,需要调低。如果请求队列积压严重,则可能是业务增长导致Notion套餐能力不足,这时需要升级套餐或进一步增加本地缓存命中率。
最终目标不是让所有请求都不超时,而是把超时率控制在一个可接受范围内,并在超时发生时给出明确降级结果。比如返回最近一次缓存的AI结果,或者提示用户稍后重试,而不是让请求挂起直到网关强制断开。数据库索引优化解决了取数慢的问题,API限流与重试解决了取数不稳定的问题,缓存和异步拆分则让整个链路具备更强的抗抖动能力。三者结合,才能有效解决 Notion AI 响应超时。