巨坑!解决 Windows 上 FastAPI/Asyncio 子进程 `NotImplementedError` 问题File “C:\Python310\lib\asyncio\base_even

巨坑!解决 Windows 上 FastAPI/Asyncio 子进程 NotImplementedError

今天我想造一个自己的MCP客户端,查阅了许多文档、攻略后,找到了最适合langchain项目的MCP库。
我一比一地复制人家文档给的示例,报了一个非常奇怪的错误,Claude、Gemini都搞不定!

...File "C:\Python310\lib\asyncio\base_events.py", line 498, in _make_subprocess_transport
    raise NotImplementedError
NotImplementedError

苦苦求索,终于寻得一民间偏方–> 在FastApi中运行Asyncio子进程导致NotImplementedError错误的解决方法 - 应龙笔记
方法是指定了ProactorEventLoop作为Windows下asyncio的事件循环,解决了Windows特有的问题。

下面请看解析(by AI)

在使用 Python 的 asyncio 库进行异步编程,尤其是在 Windows 操作系统上与子进程交互时,你可能会遇到一个令人困惑的 NotImplementedError。这种情况经常发生在使用像 FastAPI + Uvicorn 这样的 Web 框架,或者直接运行需要通过 stdio (标准输入/输出) 与子进程通信的异步脚本时(例如,使用 playwright.async_apimcp-client 或直接调用 asyncio.create_subprocess_exec)。本文将深入探讨这个问题的原因,并提供在不同场景下的解决方案。
问题剖析:为何出现 NotImplementedError
当你尝试在 Windows 上运行类似下面涉及子进程创建的代码时:

# 示例:可能触发错误的代码片段
import asyncio
async def run_subprocess():
    process = await asyncio.create_subprocess_exec(
        'cmd', '/c', 'echo Hello from subprocess',
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE
    )
    stdout, stderr = await process.communicate()
    print(f"Subprocess output: {stdout.decode()}")
# 在 Windows 默认事件循环下运行可能报错
# asyncio.run(run_subprocess())

你可能会看到如下的错误堆栈:

Traceback (most recent call last):
  ...
  File "C:\Python310\lib\asyncio\base_events.py", line 1667, in subprocess_exec
    transport = await self._make_subprocess_transport(
  File "C:\Python310\lib\asyncio\base_events.py", line 498, in _make_subprocess_transport
    raise NotImplementedError
NotImplementedError

这个错误的根源在于 Windows 上 asyncio默认事件循环

  • SelectorEventLoop: 这是 asyncio 在 Windows 上的默认事件循环。它试图提供一个类似 POSIX (Linux/macOS) 的 select() 调用接口。然而,在 Windows 上,SelectorEventLoop 对于某些高级功能,特别是与子进程管道传输 (_make_subprocess_transport) 相关的部分,支持并不完善。

  • ProactorEventLoop: 这是 asyncio 提供的另一个专门为 Windows 设计的事件循环。它基于 Windows 高性能的 I/O 模型——IOCP (I/O Completion Ports)ProactorEventLoop 能够正确且高效地处理 Windows 上的异步文件 I/O 和子进程通信。

NotImplementedError 正是因为默认的 SelectorEventLoop 无法完成 subprocess_exec 所需的传输创建。
解决方案:使用 ProactorEventLoopPolicy
解决之道就是显式地告诉 asyncio 在 Windows 上使用 ProactorEventLoop。这可以通过设置事件循环策略 (EventLoopPolicy) 来实现。
实施方案
根据你的应用场景,有两种主要的实施方式:
场景一:直接运行独立的 Asyncio 脚本
如果你的代码是一个独立的 Python 脚本,通过 python your_script.py 直接运行,你可以在脚本的入口点(if __name__ == "__main__":)处设置策略:

# file: src/utils/mcp_agent_single.py (示例结构)
import asyncio
import sys
# ... 其他导入 ...
from mcp.client.stdio import stdio_client # 假设使用了需要子进程的库
from mcp import ClientSession, StdioServerParameters
# ... (你的模型、参数等定义) ...
async def main():
    """主异步函数"""
    print("Running main async function...")
    # 示例: 假设这里会间接调用 asyncio.create_subprocess_exec
    server_params = StdioServerParameters(command="python", args=["some_server.py"])
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            # ... 你的 Agent 或其他逻辑 ...
            print("Async operation successful.")
            # ...
    print("Finished main async function.")
if __name__ == "__main__":
    # --- 关键代码开始 ---
    # 检查是否为 Windows 平台
    if sys.platform == "win32":
        print(f"Platform is Windows ({sys.platform}), applying WindowsProactorEventLoopPolicy.")
        # 设置 ProactorEventLoop 策略
        asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
    # --- 关键代码结束 ---
    print("Starting asyncio event loop...")
    try:
        asyncio.run(main())
        print("Script finished successfully.")
    except Exception as e:
        print(f"Script failed with error: {e}")
        import traceback
        traceback.print_exc()

关键点:

  1. 导入 asynciosys

  2. asyncio.run() 调用之前,检查 sys.platform == "win32"

  3. 如果是 Windows,调用 asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())

场景二:在 FastAPI/Uvicorn 应用中运行
当你的异步代码运行在像 FastAPI 这样的框架内,并由 Uvicorn 启动时,直接在 if __name__ == "__main__": 中设置策略可能不够可靠,因为 Uvicorn 会接管事件循环的管理。更稳妥的方法是创建一个自定义的 Uvicorn 服务器类:

# file: src/utils/mcp_agent_langgraph.py (示例结构)
import asyncio
import sys
import logging
import uvicorn
from fastapi import FastAPI
# ... 其他导入 ...
from langchain_mcp_adapters.client import MultiServerMCPClient # 假设使用了需要子进程的库
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
app = FastAPI()
mcp_config = { # 示例配置
    "some-mcp-service": {"command": "some_command", "args": []}
}
@app.post("/query")
async def query_endpoint(query: str):
    logging.info("Entering query_endpoint...")
    # 假设这里会间接调用 asyncio.create_subprocess_exec
    async with MultiServerMCPClient(mcp_config) as client:
        # ... 你的 Agent 或其他逻辑 ...
        logging.info("Processing query...")
        response = {"result": "some data"} # 示例响应
    logging.info("Exiting query_endpoint.")
    return response
# --- 关键代码:自定义服务器类 ---
class ProactorServer(uvicorn.Server):
    def run(self, sockets=None):
        # 在 Uvicorn 服务器启动前设置事件循环策略 (仅 Windows)
        if sys.platform == "win32":
            print("Setting ProactorEventLoopPolicy for Uvicorn server on Windows.")
            asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
        # 使用 asyncio.run 运行 Uvicorn 的核心服务协程
        asyncio.run(self.serve(sockets=sockets))
if __name__ == "__main__":
    port = 8080
    host = "127.0.0.1"
    print(f"Starting FastAPI server with ProactorServer at http://{host}:{port}")
    # --- 关键代码:修改启动方式 ---
    # 1. 创建 Uvicorn 配置,必须设置 reload=False
    #    因为 reload=True 会干扰自定义事件循环策略
    config = uvicorn.Config(
        app="mcp_agent_langgraph:app", # 指向你的 FastAPI 应用实例
        host=host,
        port=port,
        reload=False # 重要!
    )
    # 2. 实例化自定义服务器
    server = ProactorServer(config=config)
    # 3. 运行自定义服务器
    server.run()
    # --- 旧的启动方式 (注释掉或删除) ---
    # uvicorn.run("mcp_agent_langgraph:app", host=host, port=port, reload=False)

关键点:

  1. 创建 ProactorServer 类继承自 uvicorn.Server

  2. ProactorServer.run 方法内部,设置事件循环策略 (仅 Windows)。

  3. if __name__ == "__main__": 中,使用 uvicorn.Config 创建配置,务必设置 reload=False

  4. 实例化 ProactorServer 并调用其 run() 方法启动服务,而不是直接调用 uvicorn.run()

重要注意事项

  • reload=False: 当使用自定义服务器类时,Uvicorn 的热重载功能 (reload=True) 可能会导致问题,因为它会尝试重新加载代码并可能重置事件循环策略。请务必将其设置为 False

  • 平台特定: 这个解决方案是专门针对 Windows 的。在 Linux 或 macOS 上,默认的事件循环通常能很好地处理子进程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sonetto1999

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值