巨坑!解决 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_api
、mcp-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()
关键点:
-
导入
asyncio
和sys
。 -
在
asyncio.run()
调用之前,检查sys.platform == "win32"
。 -
如果是 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)
关键点:
-
创建
ProactorServer
类继承自uvicorn.Server
。 -
在
ProactorServer.run
方法内部,设置事件循环策略 (仅 Windows)。 -
在
if __name__ == "__main__":
中,使用uvicorn.Config
创建配置,务必设置reload=False
。 -
实例化
ProactorServer
并调用其run()
方法启动服务,而不是直接调用uvicorn.run()
。
重要注意事项
-
reload=False
: 当使用自定义服务器类时,Uvicorn 的热重载功能 (reload=True
) 可能会导致问题,因为它会尝试重新加载代码并可能重置事件循环策略。请务必将其设置为False
。 -
平台特定: 这个解决方案是专门针对 Windows 的。在 Linux 或 macOS 上,默认的事件循环通常能很好地处理子进程。