Supabase Edge Function 调用 LLM 时,最常见的超时场景不是 GPT-4o 返回慢,而是数据库连接池把 150 秒墙钟配额吃掉了。Deno 日志里一条 PostgresError 后紧跟一个未捕获的 timeout,前端只看到 502 Bad Gateway——整个过程里 LLM API 甚至还没收到请求。

这个问题在独立开发者做 AI SaaS 时尤其隐蔽:本地 dev 用 supabase functions serve 不走连接池,一切正常;一部署到生产,Edge Function 走 Supavisor 的 Transaction Mode Pooler,prepared statement 直接被拒绝,客户端反复重试直到超时。

为什么 Transaction Mode 不支持 Prepared Statement?

Supabase 的 Edge Functions 使用连接池而非直接连 PostgreSQL。架构上绕不开:

  1. Edge Function 实例是短生命周期的(Deno isolate 在处理完请求后可能被回收)
  2. 如果每个实例都创建独立的长连接,几千个并发请求会把 PostgreSQL 连接数打满
  3. 所以 Supabase 使用 Supavisor(或 Dedicated Pooler 的 PgBouncer)做连接池代理

Supavisor 连接池在 Transaction Mode 下,一个连接被多个请求复用——请求 A 拿连接→执行 SQL→释放,请求 B 接着用同一个连接。Prepared statement 是绑定在特定连接上的:请求 A 创建的 prepared statement,请求 B 用不了,但客户端不知道,继续用缓存里的 statement name 去调用,就会报错。

错误日志通常是:

PostgresError: prepared statement "s0" does not exist

或者更隐蔽的:

PostgresError: unnamed prepared statement does not exist

后者的触发原因是:postgres.js 客户端默认会隐式创建 unnamed prepared statement 来缓存查询计划。当连接被池回收后,下一个请求带着同一个 statement 名称进来,实际的 PostgreSQL 连接已经换了,语句不存在。

多方案连接配置对比

Edge Function 连数据库有三种路径,各自有不同的适用场景:

方案端口连接模式Prepared Statement适用场景风险
Supavisor Transaction Mode6543池化事务不支持,必须关闭Edge Function 生产默认(推荐)需改客户端配置
Supavisor Session Mode5432池化会话支持长生命周期应用(非 Edge)Edge 下连接泄漏
直连 PostgreSQL5432(非池化)原始连接支持psql、migration、pg_dump连接数快速耗尽

Edge Function 只有一个正确选择:Transaction Mode 端口 6543 + 关闭 prepared statement。

连接字符串(从 Dashboard → Settings → Database → Connection Pooling 获取):

postgresql://postgres.[project-ref]:[password]@aws-0-us-west-1.pooler.supabase.com:6543/postgres

注意区分:端口 6543 是 Pooler,端口 5432 是直连。很多项目在本地开发时用了 5432,部署时没切 6543,导致 Edge Function 打满连接数。

postgres.js 客户端怎么关 Prepared Statement?

独立开发者最常用的 Postgres 客户端是 postgres.js(Deno 生态首选),一行配置即可关闭:

import postgres from 'https://deno.land/x/[email protected]/mod.js'

const sql = postgres(Deno.env.get('DATABASE_URL')!, {
  prepare: false, // Transaction Mode 必须关
  max: 5, // 限制并发连接数,默认 10 在 Edge 下太大
  idle_timeout: 10, // 空闲连接 10 秒回收
})

如果你用 Supabase 的 js 客户端(@supabase/supabase-js),它的内部 query 不会用 prepared statement,不受这个限制——但它的底层连接也是 Pooled,所以批量操作时仍然要注意连接数量上限。

Edge Function 完整写法:

import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import postgres from 'https://deno.land/x/[email protected]/mod.js'

Deno.serve(async (req) => {
  const dbUrl = Deno.env.get('DIRECT_URL')! // 用于直连(非池化,本地 dev 用)
  const poolUrl = Deno.env.get('DATABASE_URL')! // 用于池化(生产用 6543)

  // supabase-js 读写数据
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  )

  // postgres.js 跑复杂查询(必须关 prepare)
  const sql = postgres(poolUrl, { prepare: false })

  const { data: user } = await supabase
    .from('users')
    .select('*')
    .eq('id', req.headers.get('x-user-id'))
    .single()

  if (!user) {
    return new Response(JSON.stringify({ error: 'not found' }), { status: 404 })
  }

  const result = await sql`SELECT * FROM llm_logs WHERE user_id = ${user.id} ORDER BY created_at DESC LIMIT 10`

  return new Response(JSON.stringify(result), {
    headers: { 'Content-Type': 'application/json' },
  })
})

如果按照以上配置后数据库查询恢复正常,但 LLM 调用仍超时,问题就出在 LLM 端。

LLM 调用超过 150 秒怎么分流?

OpenAI 的 GPT-4o、Claude 4.7 Opus 和 DeepSeek-R1 等大模型的推理时间波动很大——高峰期一次 Completion 可能 30-120 秒,加上 prompt 预处理和 response 后处理,很容易逼近 150 秒阈值。

不是在 Edge Function 里加更多超时预算,而是把 LLM 调用从请求链路里拆出来。

方案 A:waitUntil 后台执行(单次 ≤ 400 秒,Pro 套餐)

Deno.serve(async (req) => {
  const { prompt } = await req.json()

  EdgeRuntime.waitUntil(
    (async () => {
      const llmRes = await fetch('https://api.openai.com/v1/chat/completions', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
        },
        body: JSON.stringify({
          model: 'gpt-4o',
          messages: [{ role: 'user', content: prompt }],
        }),
      })
      const data = await llmRes.json()
      // 结果写到数据库,或通过 Supabase Realtime 推送
      const sql = postgres(Deno.env.get('DATABASE_URL')!, { prepare: false })
      await sql`INSERT INTO llm_results (prompt, result) VALUES (${prompt}, ${JSON.stringify(data)})`
    })()
  )

  return new Response(JSON.stringify({ status: 'processing' }), { status: 202 })
})

前端通过 Supabase Realtime 订阅 llm_results 表的变化,或者轮询查询状态。

方案 B:OpenAI background: true(无需管理超时)

const res = await fetch('https://api.openai.com/v1/responses', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    model: 'gpt-4o',
    input: [{ role: 'user', content: prompt }],
    background: true,
  }),
})
const { id } = await res.json()
// OpenAI 完成后 POST 到你提供的 webhook URL
// 把 response_id 存起来,前端轮询或 webhook 回调后通知用户
策略最长耗时复杂度是否需 Pro适用 LLM
直接 Edge 内调用~140 秒Free 够用快模型(gpt-4o-mini)
waitUntil 后台400 秒需要中速模型(gpt-4o)
OpenAI background无硬限制Free 够用OpenAI 生态
独立长期运行服务不限任何套餐所有模型

独立开发者做 AI SaaS 时,建议初期选用方案 A 或 B,避免过早引入额外服务。

如果团队部署涉及 Supabase 到 OpenAI 的网络稳定性,可以把后台 API 调用放在一条固定路由上,并使用独立开发者出海稳定专线承载 Edge Function 到 LLM 的出口流量。

排查三步:先改连接,再拆 LLM

按概率从高到低:

  1. 数据库连接占满超时(概率最高):检查连接字符串端口是否 6543,客户端 prepare: false 是否设置。Edge Function 日志里看到 PostgresError + connection timeout 属于这一类。
  2. LLM I/O 阻塞墙钟(概率次高):用 console.time('llm')console.timeEnd('llm') 在 Edge Function 日志里确认 LLM 调用的实际耗时。如果单次超过 120 秒,改用 waitUntil 或 background 模式。
  3. 冷启动叠加(概率低但影响大):Deno 首次加载依赖可能耗 3-8 秒。如果 LLM 调用 + 冷启动 > 150 秒,用 deno info 检查依赖包大小,或者把常用的 @supabase/supabase-js 和 OpenAI client 拆分导入。