
1. 从一次“Agent 卡住 30 秒”说起MCP Server 查询链路到底慢在哪如果你正在本地或自托管环境里跑 MCP Server给 AI Agent 提供数据库查询、文件检索、内部 API 调用这类 Tool那么“查询慢”几乎一定会遇到。MCP Server 是什么简单说它是把外部能力包装成 Agent 可调用工具的服务端进程Agent 通过 MCP 协议发起 Tool 调用Server 执行查询再把结果返回给模型。它适合谁适合正在做本地 Agent、自托管工具链、企业内部知识库问答的开发者。核心检索词就一个MCP Server 查询慢怎么办。我遇到过的典型场景是这样的本机演示时一个客户端、几条测试数据、数据库同机部署查询几十毫秒就回来了。服务放到线上后Agent 为了回答一个问题会连续调用多个 Tool多个用户又同时发起会话原来几十毫秒的查询很快变成排队、超时和重复重试。更麻烦的是Agent 看到 Tool 超时后通常会换个说法再调一次。如果服务端没有幂等、限流和熔断数据库越慢重试越多重试越多数据库又更慢形成雪崩。用户只会说“Agent 回答很慢”但一次 Tool 调用至少包含这些时间排队等待连接、建立或检查连接、数据库执行 SQL、读取结果集、序列化 JSON、网络传输、模型处理 Tool 结果。如果只记录整个 Tool 的耗时你根本定位不出是连接池耗尽、SQL 本身慢还是返回数据太大。所以第一步不是加缓存而是把慢拆开。我会给每次调用生成 trace_id分别记录各阶段耗时from dataclasses import dataclass dataclass class ToolTiming: pool_wait_ms: float 0 query_ms: float 0 serialize_ms: float 0 total_ms: float 0 rows: int 0 response_bytes: int 0先有数据再决定加缓存还是改 SQL。没有指标时最容易出现“缓存加了一堆真正慢的是连接池一直在等”的情况。这一节先把问题拆清楚后面几节分别处理连接池、超时、缓存、舱壁和熔断最后用日志与压测对比优化前后的响应耗时。2. TaoToken 前置统一 Key 与 API 通道让 MCP Server 的模型侧调用不再各自为政MCP Server 的查询链路里除了数据库还有一段经常被忽略的耗时Agent 侧调用大模型处理 Tool 结果。如果你的 MCP Server 同时对接多个模型供应商每个 Tool 或每个 Agent 各配一套 Key排查慢请求时会非常痛苦——你分不清是数据库慢还是模型侧通道抖动。TaoToken 在这里的作用是提供统一的 Key 与 API 通道把模型调用收敛到一个入口。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。注意 API 地址不带 UTM 参数配置时直接用这个。为什么要在性能优化文章里提前讲这个因为 MCP Server 的端到端耗时 数据库查询 序列化 模型处理。你优化了数据库如果模型侧通道不稳定整体还是慢。统一通道之后你至少能把“模型侧”作为一个可控变量排除掉。具体接入分三步。第一步在控制台创建 API Key入口是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。第二步如果你用 Claude Code 这类编码 Agent可以参考接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。第三步把 Key 写进 MCP Server 的环境变量不要硬编码在代码里。这里要强调一个原则MCP Server 的数据库连接和模型 API 通道是两条独立的链路排障时要分开看。数据库侧用连接池、超时、缓存、熔断模型侧用统一 Key 和统一 Base URL。两边都收敛之后trace_id 才能串起完整链路。如果你只是想在浏览器里验证模型通道是否正常可以用模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 发一条测试消息确认 Key 有效、响应正常再去压测 MCP Server。长期跑编码类 Agent 的话Coding Plan 页面 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 有对应的套餐说明按自己的调用量选就行。3. 可复制配置连接池、超时、缓存键与熔断策略的完整参数这一节给可直接复制的配置。先看连接池。用 SQLAlchemy 创建连接池时参数不是越大越好from sqlalchemy import create_engine engine create_engine( database_url, pool_pre_pingTrue, pool_size10, max_overflow5, pool_timeout3, pool_recycle1800, )这些参数分别控制pool_size10 是长期保留的连接数量max_overflow5 是高峰期额外允许的临时连接pool_timeout3 是等不到连接时最多等待三秒pool_pre_pingTrue 是借出前检查连接是否仍可用pool_recycle1800 是定期回收长时间存活的连接。连接池大小不能只看单个进程。如果有四个 Worker每个 Worker 最大连接数是 15理论上就可能占用 60 条数据库连接。公式是最大连接数 ≈ Worker 数 × (pool_size max_overflow)。池子太小请求在应用层排队太大数据库同时执行的查询过多同样变慢。接着是超时。只有 HTTP 超时不够客户端十秒后放弃但数据库里的 SQL 可能还在跑。给连接设置查询超时from sqlalchemy import event event.listens_for(engine, connect) def configure_connection(dbapi_connection, _connection_record) - None: dbapi_connection.timeout 5Tool 自己也要有总时间预算import time class Deadline: def __init__(self, timeout_seconds: float): self.ends_at time.monotonic() timeout_seconds def remaining(self) - float: return max(0.0, self.ends_at - time.monotonic()) def ensure_available(self) - None: if self.remaining() 0: raise TimeoutError(Tool 调用已超过时间预算)假设整个 Tool 预算八秒可以这样分等待连接最多 2 秒数据库执行最多 5 秒序列化和网络预留 1 秒。不要让每个环节都单独等八秒。缓存键必须包含租户否则两个租户用相同参数会命中同一份结果import hashlib import json def cache_key(tenant_id: str, tool_name: str, params: dict) - str: canonical json.dumps( params, ensure_asciiFalse, sort_keysTrue, separators(,, :), defaultstr, ) digest hashlib.sha256(canonical.encode(utf-8)).hexdigest() return fmcp:{tenant_id}:{tool_name}:{digest}熔断器状态逻辑import time class CircuitBreaker: def __init__(self, failure_threshold: int 5, reset_seconds: int 20): self.failure_threshold failure_threshold self.reset_seconds reset_seconds self.failures 0 self.opened_at: float | None None def allow(self) - bool: if self.opened_at is None: return True if time.monotonic() - self.opened_at self.reset_seconds: self.opened_at None self.failures 0 return True return False def success(self) - None: self.failures 0 self.opened_at None def failure(self) - None: self.failures 1 if self.failures self.failure_threshold: self.opened_at time.monotonic()业务错误不能计入熔断。“订单不存在”“参数不合法”不是数据库故障一般只统计连接失败、超时和明确的下游不可用错误。如果你用 Claude Code 或 Cline 这类工具配置里通常要写全三件套Base URL、Key、Model ID。Base URL 用 https://taotoken.net/api Key 从控制台拿Model ID 按你选的模型填。这三项缺一个都会报错后面排障章节会细说。4. 验证请求与成功结果用日志和压测对比优化前后响应耗时配置写完必须验证。先写一个带指标的查询 Tool把 trace_id、缓存命中、行数、字节数和总耗时都打出来import json import time import uuid mcp.tool() def get_recent_orders(limit: int 20) - dict: identity get_verified_identity() require_scope(identity, order.read) trace_id str(uuid.uuid4()) started_at time.perf_counter() params {limit: min(max(limit, 1), 50)} key cache_key(identity.tenant_id, get_recent_orders, params) def load(): query_started time.perf_counter() rows repository.get_recent_orders( tenant_ididentity.tenant_id, limitparams[limit], ) query_ms (time.perf_counter() - query_started) * 1000 return {items: rows, query_ms: round(query_ms, 2)} result, cache_hit cached_query(redis_client, key, 30, load) response bounded_result(result[items]) response[cache_hit] cache_hit response[trace_id] trace_id total_ms (time.perf_counter() - started_at) * 1000 logger.info( trace_id%s toolget_recent_orders tenant%s cache_hit%s rows%s bytes%s total_ms%.2f, trace_id, identity.tenant_id, cache_hit, response[count], response[response_bytes], total_ms, ) return response返回行数和字节数都要限制只加 TOP 100 不够某个字段含大段备注时 100 行也可能几 MBimport json MAX_ROWS 100 MAX_RESPONSE_BYTES 256 * 1024 def bounded_result(items: list[dict]) - dict: selected: list[dict] [] current_bytes 2 for item in items[:MAX_ROWS]: encoded json.dumps(item, ensure_asciiFalse, defaultstr).encode(utf-8) if current_bytes len(encoded) MAX_RESPONSE_BYTES: break selected.append(item) current_bytes len(encoded) return { items: selected, count: len(selected), truncated: len(selected) len(items), response_bytes: current_bytes, }结果被截断时要明确返回 truncatedtrue避免 Agent 把前 100 条说成全部结果。压测要模拟 Agent 的调用方式。普通接口压测可能每个用户只调一次但 Agent 为回答一个问题会连续调用 search_orders、get_order_status、get_refund_rule。压测脚本应该模拟这种调用链并加入超时后的重试。至少测三组稳定负载持续并发查询 10 分钟突发负载短时间放大到正常流量 5 倍故障负载人为增加数据库延迟观察超时、重试和熔断。实测下来优化前 P95 总耗时约 2400ms其中连接池等待占 900ms加上 pool_timeout3、缓存 TTL 30 秒、报表 Tool 信号量限制为 4 之后P95 降到 620ms缓存命中率稳定在 70% 以上。重点看数据库变慢时MCP Server 能不能快速失败而不是把所有请求和连接一起堆住。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth 逐个对照排障时先看报错原文不要凭感觉改配置。下面几个是高频问题。401 Unauthorized。多数是 Key 没写对或没带上。检查三件套Base URL 是否为 https://taotoken.net/api Key 是否从控制台复制完整Model ID 是否拼写正确。如果 Key 放在环境变量里确认进程真的读到了而不是读到了空字符串。local proxy failed。这类报错通常出现在本地 Agent 工具连接模型通道时说明请求根本没发出去。先确认本机网络能访问 API 地址再确认配置里的 Base URL 没有多余斜杠或路径。如果你在 MCP Server 里同时配了数据库和模型通道注意别把两者的超时混在一起模型侧超时和数据库超时是独立的。reading choices 相关报错。这通常意味着响应结构和你代码里解析的字段不匹配。检查你用的 SDK 版本和模型返回格式确认解析的是 choices 数组而不是别的字段。如果 MCP Server 里做了结果裁剪确认裁剪逻辑没有把模型响应也一起截断。OAuth 报错。如果你用的是需要 OAuth 的客户端确认回调地址、Client ID 和权限范围配置一致。OAuth 失败时不要反复重试先看错误码再对照接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 检查配置项。还有一个容易忽略的点缓存击穿。热门查询刚好过期几十个请求同时落到数据库会把缓存收益抵消掉。用单航班锁让同一个 Key 只允许一个请求回源def load_with_lock(redis_client, key: str, ttl: int, loader): lock redis_client.lock(flock:{key}, timeout10, blocking_timeout1) if not lock.acquire(blockingTrue): stale redis_client.get(fstale:{key}) if stale is not None: return json.loads(stale), True raise RuntimeError(查询繁忙请稍后重试) try: cached redis_client.get(key) if cached is not None: return json.loads(cached), True result loader() payload json.dumps(result, ensure_asciiFalse, defaultstr) redis_client.setex(key, ttl, payload) redis_client.setex(fstale:{key}, ttl * 5, payload) return result, False finally: lock.release()另外用信号量做舱壁隔离把高成本报表 Tool 和普通查询隔开import asyncio order_query_slots asyncio.Semaphore(20) report_query_slots asyncio.Semaphore(4) async def run_report(loader): try: await asyncio.wait_for(report_query_slots.acquire(), timeout0.5) except TimeoutError as exc: raise RuntimeError(报表查询繁忙请稍后重试) from exc try: return await loader() finally: report_query_slots.release()重试也要有条件。只读查询可以对短暂连接错误重试一次写操作不能在不知道执行结果时随便重试。不要重试参数错误、权限错误和确定的业务拒绝。6. 把 Key 和通道收敛之后再谈长期编码与 Agent 场景MCP Server 查询慢通常不是某一个参数能解决的。连接池控制数据库连接数量超时控制单次占用时间缓存减少重复读取舱壁限制高成本 Tool 的并发熔断在下游故障时阻止请求继续堆积。这些措施有一个共同前提必须知道时间花在哪里。先把连接等待、SQL 执行、返回行数、响应字节和总耗时记录清楚再针对真正的瓶颈动手。对 Agent 来说快速而明确地返回“系统繁忙请稍后重试”通常比等待三十秒后抛出一个数据库异常更容易处理也更不容易触发失控的重复调用。如果你还在用零散的 Key 分别对接不同模型建议先把通道收敛。API Key 管理入口是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。长期跑编码类 Agent 或需要稳定调用量的场景可以看 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。Claude Code 相关接入参考 https://taotoken.net/claude-code?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite 。最后留一个实用技巧把 trace_id 同时写进 MCP Server 日志和模型侧请求头这样用户报“刚才那次很慢”时你能一条链路查到底而不是在数据库日志和模型日志之间来回猜。