生产级MCP Server手写指南:鉴权、流式传输与状态管理全解析
发布时间:2026/9/29 6:32:43来源:尧图网络
手写 MCP Server 这件事听着像造轮子可一旦贴上“生产级”三个字难度直接翻倍。同样一个工具调用demo 里只要能跑通生产环境就得同时解决鉴权、流式传输、状态管理还要面对日志、超时、并发、部署、安全边界这些“看不见的坑”。这篇文章我会从零开始拆把 MCP Server 拆成协议层、安全层、传输层、状态层逐步落地并结合我在实际运维中踩过的坑给出一套可以照着抄的实现方案。适合正在做 AI Agent、IDE 插件、自动化工具接入的开发者也适合那些刚把 MCP 跑通、正打算把它往生产环境推的朋友。1. 为什么还要从零手写 MCP Server先想清楚这四件事1.1 MCP 到底是什么以及生产级指的是什么MCP全称 Model Context Protocol是一套让 AI 应用和外部工具之间进行标准化通信的协议。你可以把它理解为“AI 世界的 USB 接口”模型不需要知道你背后是数据库、文件系统还是某个线上服务只要都实现了 MCP 协议就可以统一调用。一个 MCP Server 的核心职责就是把自己的能力暴露成工具列表然后接收模型发来的调用请求执行后把结果返回。但“生产级”这三个字比“能响应请求”复杂得多。生产环境意味着会有真实用户、真实数据、真实故障。鉴权不过关别人就能绕过你的工具直接调接口流式传输做不好AI 对话就会一直转圈状态管理没设计好用户聊到一半上下文就乱了。我之前见过不少团队demo 阶段只用一个月就把 MCP Server 写完了结果上生产前光补鉴权和超时处理又花了两周。所以我建议动手之前先想清楚下面几件事。1.2 技术选型用框架还是写裸协议先解决一个现实问题是直接用现成框架还是从底层协议手写现成框架比如 Python 生态的 FastMCP、官方的 SDK能帮你把工具注册、参数校验、请求分发这些琐碎事情都封装好。如果你只是要把三五个工具暴露给 LLM 调用用框架足够而且 SDK 对协议的兼容性有保障可以减少很多你自己造轮子可能引入的协议细节错误。但框架也有代价。比如鉴权逻辑SDK 往往默认“服务端是可信的”或者只提供一层很薄的路由级拦截你需要额外包一层网关或者自定义 Middleware。再比如状态管理很多框架支持全局上下文但跨请求、跨会话的隔离还是要你自己设计。我的建议是生产环境不要排斥框架但一定要在框架外面套一层你自己的服务边界。换句话说协议解析可以用框架做鉴权、限流、审计、状态容器这些必须自己掌控。如果你追求极致可控或者需要定制协议行为那再从传输层手写也不迟。1.3 功能拆分鉴权、流式传输、状态管理分别解决什么问题这三个词不是并列的功能列表而是三个层次的问题。鉴权解决的是“你是谁、能不能用”。MCP Server 暴露的不只是数据还有操作能力如果鉴权做不好任何人都能操纵你的工具去读写你的业务数据。流式传输解决的是“结果怎么回来”。LLM 调用工具时用户看到的是流式的思考过程但 MCP Server 返回工具结果也可以流式。尤其当工具执行时间较长比如查一组大报表、调用外部 API如果还是一次性全量返回体验会非常差也容易被网关超时打断。状态管理解决的是“上下文怎么记住”。工具调用之间是有依赖的比如用户先让你查一个订单再让你基于这个订单做分析。如果每次请求都无状态你就得让用户在对话里反复重复同一个上下文这就违反了 AI 应用的本意。所以生产级状态管理的核心是把对话级、会话级、任务级的上下文用可持久化、可隔离、可过期的方式存下来。1.4 目录结构先行避免后期返工写生产级代码最忌讳的就是“先把功能跑通再回来加安全”。因为后期加鉴权会把每个接口都改一遍后期加状态管理又会牵扯到所有工具函数的入参。我建议一开始就按下面的结构组织mcp-server/ ├── main.py # 服务入口 ├── config.py # 配置加载 ├── auth/ # 鉴权相关 │ ├── token.py # token 校验与刷新 │ ├── middleware.py # 网关层拦截 │ └── audit.py # 审计日志 ├── transport/ # 传输层 │ ├── sse.py # SSE 流式实现 │ └── http.py # HTTP 入口 ├── state/ # 状态管理 │ ├── session.py # 会话生命周期 │ ├── store.py # 状态存储抽象 │ └── memory.py # 内存/Redis 实现 ├── tools/ # 具体工具实现 ├── logging_conf.py # 自定义日志管理 └── tests/这样做的核心原因只有一个把“协议相关”和“业务相关”彻底隔离。鉴权、状态、日志属于横切能力如果散落在每个工具函数里后面维护就是一场灾难。2. 鉴权体系设计不只是一个 API Key 那么简单2.1 传输层和业务层分别怎么防很多人做鉴权只在接口入口校验一个 token然后就觉得万事大吉。这远远不够。生产级的鉴权至少要分两层。第一层是传输层鉴权也就是网关层。这一层负责校验调用方身份拦截那些没有携带有效凭证的请求。常见的做法是 API Key 或 Bearer Token配在 HTTP Header 里由 Nginx 或 API 网关统一校验。这一层解决的是“门外的人别进来”。第二层是业务层鉴权也叫授权。身份通过了不代表你有权调用某个敏感工具。比如同一个 MCP Server 上挂了一个“读取公开报表”的工具还挂了一个“删除线上数据”的工具后者的权限级别就必须更高。业务层的校验不能只看 token还要看 token 携带的角色、scope 是否覆盖当前操作。这里补一句很实在的经验MCP Server 往往是给 AI Agent 用的而 Agent 的权限边界本质上等于它能看到和能调用的工具边界。所以如果不做业务层鉴权一旦 token 泄露Agent 就等于被完全操纵了。宁可把 scope 粒度拆细也不要图省事给一个全局管理员 token。2.2 基于 Bearer Token 的鉴权流程落地具体实现上我用一个轻量方案签发短期访问 token 刷新 token访问 token 有效期设为 15 分钟刷新 token 有效期设为 7 天。这样即使访问 token 泄露影响窗口也有限。# auth/token.py import time import hmac import hashlib import base64 import json def sign_token(user_id: str, scope: str, expires_in: int, secret: str) - str: header base64.urlsafe_b64encode(json.dumps({ alg: HS256, typ: JWT }).encode()).rstrip(b) payload base64.urlsafe_b64encode(json.dumps({ user_id: user_id, scope: scope, exp: int(time.time()) expires_in, iat: int(time.time()) }).encode()).rstrip(b) message header b. payload signature hmac.new(secret.encode(), message, hashlib.sha256).digest() sig_b64 base64.urlsafe_b64encode(signature).rstrip(b) return (message b. sig_b64).decode() def verify_token(token: str, secret: str) - dict: header, payload, signature token.split(.) message (header . payload).encode() expected hmac.new(secret.encode(), message, hashlib.sha256).digest() sig_b64 base64.urlsafe_b64encode(expected).rstrip(b) if sig_b64.decode() ! signature: raise PermissionError(signature mismatch) payload_data json.loads(base64.urlsafe_b64decode(payload )) if payload_data[exp] time.time(): raise PermissionError(token expired) return payload_data这里没有依赖第三方 JWT 库逻辑很直白方便你理解核心机制。生产环境我建议直接用 PyJWT 或 Node 的 jsonwebtoken自己写只是为了避免封装太深导致安全隐患。注意一个细节签名用的 secret 一定不能放在代码仓库里建议从环境变量或者密钥管理服务加载并且定期轮换。鉴权中间件会在网关层做一次统一校验把解析出的 user_id 和 scope 塞进请求上下文后面所有工具函数都能直接取用不需要每个工具各自做一遍 token 解析。# auth/middleware.py from functools import wraps from .token import verify_token def require_scope(scope: str): def decorator(func): wraps(func) def wrapper(request, *args, **kwargs): ctx request.context if scope not in ctx[scopes]: raise PermissionError(fmissing scope: {scope}) return func(request, *args, **kwargs) return wrapper return decorator2.3 密钥管理与刷新策略密钥管理这块我再多说几句。生产环境最怕的不是算法被破解而是密钥硬编码在代码里。我见过有人把 secret 直接写在 main.py 顶部然后代码推到公共仓库结果就是整个服务等于裸奔。正确的做法是三层分离本地开发用.env文件测试环境用独立密钥生产环境用密钥管理服务或 K8s Secret 注入。刷新策略上建议两条一是访问 token 短时效二是刷新 token 必须支持吊销。吊销是很多系统最容易被忽略的部分用户退出、token 泄露、权限变更时刷新 token 要能立刻失效。生产环境里我会把吊销的 token 放进 Redis用 SETNX 写入TTL 设成剩余有效时间校验时先看吊销列表。2.4 日志与审计自定义日志管理别只把 print 改成 logger日志在鉴权体系里扮演的角色比很多人想象得重要。审计日志不只是排查问题用的更是合规和追溯的依据。MCP Server 的日志有个特殊性它既要记录协议层面的调用又要记录业务侧的工具执行结果还要避免把敏感数据刷进去。我自己会单独写一个logging_conf.py对日志做三通道设计访问日志、业务日志、审计日志。访问日志记录每次 HTTP 请求的方法、路径、状态码、耗时业务日志记录工具调用的入参、出参、异常审计日志记录“谁在什么时间通过哪个 token 调用了哪个工具”其中 token 只记录前几位和后几位完整凭证绝不落盘。# logging_conf.py import logging class RedactFilter(logging.Filter): def filter(self, record): msg record.getMessage() # 脱敏把 token 字段替换成前8位后4位 if token in msg: record.msg msg.replace(record.token_display, xxxxxx) return True def setup_logging(): formatter logging.Formatter( %(asctime)s | %(levelname)s | %(name)s | %(message)s ) access logging.getLogger(mcp.access) biz logging.getLogger(mcp.biz) audit logging.getLogger(mcp.audit) for logger, filename in [ (access, logs/access.log), (biz, logs/biz.log), (audit, logs/audit.log), ]: handler logging.FileHandler(filename) handler.setFormatter(formatter) logger.addHandler(handler) logger.setLevel(logging.INFO)这里踩过一个坑用 Python 的默认 logger多个 handler 会重复打印到 stdout导致日志文件里出现双份数据。所以我给每个 logger 只挂一个 FileHandlerpropagate必须设为 False不然日志还会继续往父 logger 冒泡。自定义日志管理这件事说到底就是“分类、脱敏、隔离存储”别偷懒。3. 流式传输让工具调用像对话一样逐步返回3.1 MCP 的流式机制到底长什么样MCP 的流式传输底层是 SSEServer-Sent Events。它和 WebSocket 的区别在于SSE 是单向的服务端可以持续推数据而客户端用普通 HTTP 就能接收WebSocket 是双向的适合实时交互。在 MCP 的典型场景里模型先发一个请求服务端执行一段时间然后逐步返回结果SSE 天然匹配这个模型。生产级流式要解决的核心问题是不要让调用方一直短时间内重试也不要让用户等在一个“假死”的请求上。实现上需要三类机制增量事件、心跳、结束标志。增量事件负责把大结果拆成小块心跳负责告诉客户端“我还活着”结束标志负责告诉客户端“这次调用已经完成可以处理结果了”。3.2 基于 SSE 的服务端实现用 Python 实现 SSE 端点可以借助异步生成器。核心思路是把工具执行过程改造成一个异步生成器每产出一个中间结果就 yield 一次SSE 端点再把每次 yield 包装成data:事件。# transport/sse.py import asyncio import json from fastapi import Request from fastapi.responses import StreamingResponse async def event_stream(executor, context): try: async for chunk in executor.run(context): yield fdata: {json.dumps({type: chunk, data: chunk})}\n\n yield fdata: {json.dumps({type: done})}\n\n except Exception as exc: yield fdata: {json.dumps({type: error, message: str(exc)})}\n\n app.post(/mcp/stream) async def mcp_stream(request: Request): auth_result check_auth(request) if not auth_result.valid: return JSONResponse(status_code401, content{detail: unauthorized}) body await request.json() executor build_executor(body[tool], body[args]) return StreamingResponse( event_stream(executor, auth_result.context), media_typetext/event-stream, headers{ Cache-Control: no-cache, X-Accel-Buffering: no, } )注意两个坑。第一X-Accel-Buffering: no必须加否则 Nginx 默认开缓冲会把流式内容全部攒到一起才发出去SSE 就变成了假流式。第二SSE 响应不能走默认的 gzip 压缩压缩会让客户端迟迟等不到断句所以要在网关层对text/event-stream关闭压缩。3.3 心跳、超时与断线重连SSE 的一个隐患是连接看起来没断但服务端可能已经因为某个阻塞任务卡死了。客户端那边通常会等 idle 太久就自动重连但如果服务端没有心跳机制客户端根本区分不了“暂时没消息”和“已经挂了”。我的做法是在事件流里每 15 秒推一个: keep-alive注释行。SSE 规范里以冒号开头的行是注释客户端不会把它当成 data但能刷新连接状态。同时服务端要设置写超时如果往连接里写数据超过某个阈值还没写完强制断开这个连接让客户端走重连逻辑。async def with_heartbeat(gen, interval15): while True: try: chunk await asyncio.wait_for(gen.__anext__(), timeoutinterval) yield chunk except asyncio.TimeoutError: yield : keep-alive\n\n断线重连这一层客户端服务端要配合好。服务端在每次工具调用的开始就分配一个 request_id客户端重连后带上这个 ID服务端可以查询当前执行状态而不是重新跑一遍工具。这一点和状态管理强相关后面细说。3.4 自定义日志与流式输出的合理配合很多人在做流式日志时容易把服务端内部日志和客户端收到的流式消息混为一谈。其实这两者信息密级完全不同。客户端收到的流式消息是处理后的业务结果可能包含中间状态比如“正在读取数据库”“正在调用外部 API”“正在生成最终结果”服务端内部日志则是技术细节比如“连接建立”“第 3 个 chunk 已发送”“GC 暂停了 120ms”。生产环境里我会在自定义日志模块里给流式事件单独开一个 logger。每个 chunk 只记录 chunk 序号、字节数、耗时不记录具体内容。这样既不泄露业务数据又能排查“最后一个 chunk 是不是丢了”。这块经验我是在一次线上故障里学到的当时用户反馈 AI 回答总被截断排查了很久才发现是某个中间结果里包含了特殊字符把 SSE 的\n\n分隔符破坏掉了。后来所有 chunk 出去之前都要做转义这个坑才彻底堵上。4. 状态管理从无状态到有上下文的演化4.1 会话状态与对话状态的边界状态管理最容易犯的错是把所有状态都塞进一个全局 dict。刚开始请求量小觉得没问题等到多用户并发就发现用户 A 的上下文被用户 B 覆盖了。要避免这种情况先把边界划清楚。我一般分两层会话状态Session对应一次连接的生命周期对话状态Conversation对应一次完整任务或对话的生命周期。会话状态存连接级的信息比如当前用的 token、传输类型、最后活跃时间对话状态存业务上下文比如用户查过的订单、引用的报表、已经确认过的参数。在这两层之上还要区分一个“请求级石头”。每一条工具调用都应该有独立的 request_id上下文不能跨请求自动泄露。即使是同一次对话工具 A 产生的内部中间值也不应该默认成为工具 B 的输入除非你显式做上下文传递。4.2 用 Redis 实现会话级状态存储状态如果只放内存进程一重启就全没了这对生产级是不可接受的。我会用 Redis 做会话与对话状态的存储内存做热缓存。Redis 的数据结构里String 适合存 session 元信息Hash 适合存对话上下文ZSet 可以用来做状态过期扫描。# state/store.py import redis import json class RedisStateStore: def __init__(self, redis_url): self.r redis.from_url(redis_url) def save_session(self, session_id, data, ttl3600): key fsession:{session_id} self.r.hset(key, mappingdata) self.r.expire(key, ttl) def get_session(self, session_id): key fsession:{session_id} raw self.r.hgetall(key) if not raw: return None return {k.decode(): v.decode() if isinstance(v, bytes) else v for k, v in raw.items()} def append_conversation(self, conversation_id, message, max_len50): key fconv:{conversation_id} self.r.rpush(key, json.dumps(message)) self.r.ltrim(key, -max_len, -1) self.r.expire(key, 86400) def get_conversation(self, conversation_id): key fconv:{conversation_id} return [json.loads(item) for item in self.r.lrange(key, 0, -1)]注意 Redis 的hgetall返回的是 bytes转回字符串时要做类型判断不然存一个中文字符串会变成乱码。对话上下文用 List 存配合 LTRIM 控制窗口长度只保留最近 50 条这比每次都把整个上下文发回给模型要节约大量 token。4.3 幂等、并发与资源释放状态管理里最难的不是存而是“别把状态弄脏”。生产环境一定会出现重复请求。客户端重试、网络抖动、断线重连都可能导致同一个 request 被发送两次。如果执行的是一个“查询”工具幂等问题不大如果执行的是“创建订单”“调用外部写接口”重复执行就是事故。我的做法是每个工具调用进来先检查这个 request_id 是否执行过。用一个幂等键存到 RedisSET NX 语义成功写入的才继续执行写入失败的直接返回上次的结果。def check_idempotent(request_id): key fidempotent:{request_id} if r.set(key, running, nxTrue, ex600): return True # 第一次请求继续执行 return False # 重复请求直接返回上一次结果执行完成的工具调用要把结果回写到幂等键下面并标记状态为 completed。这样即使客户端重试也能拿到上一次的完整结果而不是重新跑一遍。资源释放也一样容易忘尤其是长连接和异步任务会话断开时要主动清理状态里的临时文件句柄、数据库连接、外部 API 会话不然状态存储会越积越大。4.4 状态隔离的多租户考虑如果你的 MCP Server 要同时服务多个团队、多个应用就一定要考虑状态隔离。最简单的隔离粒度是 token 中的 user_id 或 app_id所有 Redis key 都拼上这个前缀。更严格一点数据库连接、工具可执行范围、上下文的可见性都要按租户隔离。我建议在状态存储层就抽象出一个 TenantStore 接口每个租户的请求上下文都必须显式传递 tenant_id不允许工具函数里直接用“全局共享 store”。这也是生产级代码和 demo 的一个重要分水岭任何状态在代码层面的可见性都要受限。宁可多写几行参数传递也不要让一个全局管理器默默成为数据的横切入口。5. 生产级最佳实践把代码从“能跑”变成“敢上线”5.1 配置管理、优雅退出与信号处理生产级代码的第一个特征是“应用可以被配置而不是被改代码”。同一套 MCP Server 代码在本地、测试、生产三个环境要能跑出不同配置。配置项至少包括密钥、Redis 地址、日志级别、token 有效期、限流阈值、超时时间。配置加载我习惯用一个 dataclass 统一管理启动时校验必填项缺配置直接 fail fast不要等服务跑到一半才发现没有 Redis。优雅退出这块很多人忽略。MCP Server 往往挂着长连接、处理异步任务如果直接 kill正在执行的工具调用会被打断状态也来不及落盘。生产里应该捕获 SIGTERM先把健康检查端点切到不可用状态然后停止接收新连接等待正在执行的调用超时或完成最后再释放状态存储连接。import signal def handle_sigterm(signum, frame): logger.info(received SIGTERM, starting graceful shutdown) app.state.healthy False asyncio.create_task(shutdown()) signal.signal(signal.SIGTERM, handle_sigterm)5.2 链路追踪与可观测性一次 MCP 调用链路可能是客户端 - API 网关 - MCP Server - 工具函数 - 外部 API。任何一环慢了用户都会感觉 AI 在“思考”。所以生产级 MCP Server 必须把 trace_id 贯穿整条链路每个日志、每个状态操作、每个外部调用都带上这个 ID。我在配置日志时会强制加一个上下文变量从请求中间件里取出 trace_id然后绑定到 logger 的 context。Python 里可以用 contextvars 实现每个请求的上下文隔离不会串号。此外健康检查端点要单独暴露/healthz和/readyz前者是进程活着后者是依赖都可用K8s 的探针可以分开配。5.3 测试策略单元、集成、契约测试生产级 MCP Server 测试至少要分三层。单元测试覆盖工具函数和状态存储的边界条件集成测试覆盖鉴权、流式、状态三者的配合契约测试覆盖 MCP 协议的数据格式兼容性防止 SDK 升级后请求结构对不上。其中契约测试最容易漏掉。MCP 协议毕竟是个标准客户端 SDK 和服务端框架不是同一套代码两边一起升级时很可能一个字段名变了另一侧还不知道。我会把关键请求和响应的 JSON Schema 固定下来在 CI 里跑 schema 校验确保协议的心脏部分稳定。5.4 部署形态容器、网关与安全边界部署上最常见的形态是容器化一个 MCP Server 一个 Pod前面挂 API 网关。网关这里要特别强调MCP Server 本身不能直接暴露公网必须由网关承担 TLS 终止、限流、WAF、鉴权等边界安全职责。我见过一种危险部署方式把 MCP Server 直接监听在 0.0.0.0 的某个端口然后靠防火墙“应该没事”来保护。这种方式一旦防火墙配置失误整个工具集就暴露了。正确的网络拓扑是客户端到网关走 HTTPS网关到 MCP Server 走内网 HTTPMCP Server 只接受来自网关的转发请求。如果一定要暴露那鉴权就不能只依赖 Bearer token还要加上 IP 白名单、双向 mTLS 这种更强的认证方式。6. 线上踩坑实录与排查速查表6.1 鉴权失败的三个隐蔽原因鉴权失败最表面的原因当然是 token 错误但有几类隐蔽原因排查起来非常费劲。第一个是时钟漂移。有些内部服务用系统时间验证 token 的签发时间和过期时间一旦服务器时间不准明明刚签发的 token 会因为和服务端时间差超过几秒被判为无效。解决办法是启用 NTP 同步并且 token 校验时允许一个小的 leeway 窗口。第二个是代理服务器改了 Header。Nginx 转发时如果没有把 Authorization Header 透传后端的 MCP Server 就接收不到 token。用 curl 直连后端测能通通过网关测就 401多半是这个问题。第三个是刷新 token 被反复使用。客户端在 token 过期前后并发刷新两个请求同时带着同一个旧刷新 token后端没做一次性校验导致用户被挤下线。解决方式是刷新 token 使用一次后立即作废并返回新的刷新 token客户端要做并发去重。6.2 流式传输断流的排查路径流式断流用户侧表现是“回答到一半突然停住”。排查路径我按顺序来先把服务端日志里每个 chunk 的序号、字节数、发送时间拉出来看最后一条是正常结束还是戛然而止然后看网关配置尤其检查X-Accel-Buffering和 proxy_read_timeout。有一个非常隐蔽的问题HTTP 层连接正常但客户端在接收事件时因为部分代理的 chunked 编码兼容性问题把最后几段数据吞了。这时候要抓原始响应而不是依赖客户端 App 的调试工具。另外如果工具内部调外部 API 时出现慢调用SSE 那个线程一直 pending别的请求也会被堵住所以流式执行必须配超时不能无限等。6.3 状态错乱的典型现场状态错乱最常见的问题是“上下文串号”。现象是用户 A 说“帮我查一下刚才那个订单”AI 却查到了用户 B 的订单。排查时会发现状态 key 里漏了 session_id所有用户共用同一个会话上下文。这个坑的根源几乎都是“把状态直接存在了进程内全局变量里”。另一个高频坑是状态过期时间设置不合理。对话上下文设了 30 分钟过期用户聊到 25 分钟时暂停回来继续对话发现 AI 已经完全忘了之前的上下文。这不是 bug是策略但往往没人提前告诉产品经理。生产环境建议把过期策略做成可配置并且在删除状态前先打一条审计日志方便事后追溯。6.4 排查速查表现象可能原因排查动作客户端 401 但 curl 后端正常网关未透传 Authorization检查 Nginx location 的 proxy_set_header流式返回但内容被“攒住”Nginx 缓冲未关闭加X-Accel-Buffering: notoken 刚签发就校验失败时钟漂移或签发秒级精度问题做 NTP 同步校验加 leeway全局状态互相覆盖状态 key 没拼 session_id检查 Redis key 设计重复调用导致多次写库缺少幂等控制用 request_id 做 SET NX日志重复打印logger propagate 未关设置propagateFalse重启后状态全丢用内存存储且未持久化切换到 Redis流式被截断但无报错chunk 里含特殊字符破坏 SSE 格式对 chunk 做转义检查分隔符最后再分享一个我在实际运维里的体会。MCP Server 和生产级这三个字一组合重点从来不是“能不能写出协议解析”而是“能不能在真实流量下不崩、不串、不泄露”。鉴权、流式、状态管理这三件事表面上是三个功能点本质上都是在回答同一个问题当一个外部模型可以操纵你的工具时你的系统边界到底在哪里。按我现在的习惯每次新增一个工具之前都会先问三个问题这个工具需要什么 scope它的执行结果要不要流式返回它需要用到哪一层状态问题过一遍代码写出来的质量会完全不同。希望这篇文章能帮你少踩一些坑也欢迎你在自己的生产环境里继续补全那些我在文里没来得及写的边界情况。
网站建设高端定制企业官网