ソースを参照

使用WsgiToAsgi/uvicorn代替WSGI,切换到异步操作,貌似测试成果了。

wangxq 1 ヶ月 前
コミット
dfe57a4995

+ 170 - 0
test/custom_react_agent/ASGI_启动说明.md

@@ -0,0 +1,170 @@
+# ASGI模式启动说明
+
+## 概述
+
+为了解决Flask与LangGraph异步事件循环冲突问题("Event loop is closed"错误),我们将Flask应用改为使用ASGI适配器启动。这样可以获得真正的异步支持,允许LangGraph的checkpoint保存在请求完成后继续执行。
+
+## 问题背景
+
+原本的错误:
+```
+redisvl.exceptions.RedisSearchError: Unexpected error while searching: Event loop is closed
+```
+
+这个错误发生在Flask路由处理完成后,LangGraph尝试异步保存checkpoint时事件循环已被关闭。
+
+## 解决方案
+
+使用ASGI适配器(`WsgiToAsgi`)将Flask WSGI应用包装为ASGI应用,然后使用uvicorn ASGI服务器运行,获得持久化事件循环支持。
+
+## 安装依赖
+
+```bash
+# 进入项目目录
+cd test/custom_react_agent
+
+# 安装ASGI依赖
+pip install uvicorn asgiref
+
+# 或者安装所有依赖
+pip install -r requirements.txt
+```
+
+## 启动方式
+
+### 方式1:直接运行api.py(推荐)
+
+```bash
+cd test/custom_react_agent
+python api.py
+```
+
+**说明**:
+- 会自动尝试使用ASGI模式启动
+- 如果缺少依赖,会fallback到传统Flask模式
+- 支持Ctrl+C优雅关闭
+
+**启动日志示例**:
+```
+🚀 使用ASGI模式启动异步Flask应用...
+   这将解决事件循环冲突问题,支持LangGraph异步checkpoint保存
+INFO:     Started server process [12345]
+INFO:     Waiting for application startup.
+INFO:     Application startup complete.
+INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
+```
+
+### 方式2:使用uvicorn命令行启动
+
+```bash
+cd test/custom_react_agent
+
+# 启动ASGI应用
+uvicorn asgi_app:asgi_app --host 0.0.0.0 --port 8000
+
+# 开发模式(自动重载)
+uvicorn asgi_app:asgi_app --host 0.0.0.0 --port 8000 --reload
+
+# 生产模式(多worker)
+uvicorn asgi_app:asgi_app --host 0.0.0.0 --port 8000 --workers 4
+```
+
+### 方式3:传统Flask模式(不推荐)
+
+如果ASGI依赖安装失败,会自动fallback到传统Flask模式:
+
+```
+⚠️ ASGI依赖缺失,使用传统Flask模式启动
+   建议安装: pip install uvicorn asgiref
+   传统模式可能存在异步事件循环冲突问题
+```
+
+## 测试API
+
+启动后,可以测试API:
+
+```bash
+# 测试健康检查
+curl http://localhost:8000/health
+
+# 测试聊天API
+curl -X POST http://localhost:8000/api/chat \
+  -H "Content-Type: application/json" \
+  -d '{
+    "user_id": "test_user",
+    "question": "请问下一届足球世界杯在哪里举行?"
+  }'
+```
+
+## 预期效果
+
+1. **完全解决"Event loop is closed"错误**
+2. **LangGraph checkpoint正常保存**
+3. **支持连续多次API调用**
+4. **保持所有现有功能**
+
+## 验证成功
+
+如果修复成功,您应该看到:
+- API响应正常
+- 日志中没有"Event loop is closed"错误
+- 对话状态正确保存
+- 连续请求都能正常处理
+
+## 故障排除
+
+### 如果依然出现事件循环错误:
+
+1. **确认使用ASGI模式**:检查启动日志是否显示"使用ASGI模式启动"
+2. **检查依赖版本**:确保uvicorn和asgiref版本符合要求
+3. **重启服务**:完全重启API服务
+4. **检查Redis连接**:确保Redis服务正常运行
+
+### 常见问题:
+
+**Q: ImportError: No module named 'uvicorn'**
+A: 运行 `pip install uvicorn asgiref`
+
+**Q: 启动时提示权限错误**
+A: 尝试更换端口:`python api.py` 或在代码中修改端口号
+
+**Q: 仍然出现异步错误**
+A: 检查是否真的使用了ASGI模式,查看启动日志确认
+
+## 技术说明
+
+### ASGI vs WSGI
+
+- **WSGI(原来)**:同步协议,每个请求结束后关闭事件循环
+- **ASGI(现在)**:异步协议,保持事件循环活跃,支持异步任务
+
+### WsgiToAsgi适配器
+
+- 无缝将Flask WSGI应用转换为ASGI兼容
+- 保持所有Flask代码不变
+- 获得真正的异步支持
+
+## 文件说明
+
+- `api.py`:主要API文件,包含自动ASGI启动逻辑
+- `asgi_app.py`:独立ASGI应用文件,用于uvicorn命令行启动
+- `requirements.txt`:所需依赖列表
+- `ASGI_启动说明.md`:本文档
+
+## 成功案例
+
+修复后的API调用应该像这样工作:
+
+```bash
+# 第一次请求
+curl -X POST http://localhost:8000/api/chat -H "Content-Type: application/json" -d '{"user_id":"polo","question":"你好"}'
+# ✅ 成功响应
+
+# 第二次请求(之前会失败)
+curl -X POST http://localhost:8000/api/chat -H "Content-Type: application/json" -d '{"user_id":"polo","question":"请问下一届足球世界杯在哪里举行?"}'
+# ✅ 成功响应,无事件循环错误
+```
+
+---
+
+如有问题,请检查启动日志和错误信息。

+ 50 - 16
test/custom_react_agent/api.py

@@ -849,19 +849,53 @@ def get_conversation_summary_api(thread_id: str):
 
 # 为了支持独立运行
 if __name__ == "__main__":
-    # 使用Flask 3.x原生异步支持启动
-    logger.info("🚀 使用Flask内置异步支持启动...")
-    
-    # 信号处理
-    import signal
-    
-    def signal_handler(signum, frame):
-        logger.info("🛑 收到关闭信号,开始清理...")
-        print("正在关闭服务...")
-        exit(0)
-    
-    signal.signal(signal.SIGINT, signal_handler)
-    signal.signal(signal.SIGTERM, signal_handler)
-    
-    # 启动Flask应用
-    app.run(host="0.0.0.0", port=8000, debug=False, threaded=True) 
+    try:
+        # 尝试使用ASGI模式启动(推荐)
+        import uvicorn
+        from asgiref.wsgi import WsgiToAsgi
+        
+        logger.info("🚀 使用ASGI模式启动异步Flask应用...")
+        logger.info("   这将解决事件循环冲突问题,支持LangGraph异步checkpoint保存")
+        
+        # 将Flask WSGI应用转换为ASGI应用
+        asgi_app = WsgiToAsgi(app)
+        
+        # 信号处理
+        import signal
+        
+        def signal_handler(signum, frame):
+            logger.info("🛑 收到关闭信号,开始清理...")
+            print("正在关闭服务...")
+            exit(0)
+        
+        signal.signal(signal.SIGINT, signal_handler)
+        signal.signal(signal.SIGTERM, signal_handler)
+        
+        # 使用uvicorn启动ASGI应用
+        uvicorn.run(
+            asgi_app,
+            host="0.0.0.0",
+            port=8000,
+            log_level="info",
+            access_log=True
+        )
+        
+    except ImportError as e:
+        # 如果缺少ASGI依赖,fallback到传统Flask模式
+        logger.warning("⚠️ ASGI依赖缺失,使用传统Flask模式启动")
+        logger.warning("   建议安装: pip install uvicorn asgiref")
+        logger.warning("   传统模式可能存在异步事件循环冲突问题")
+        
+        # 信号处理
+        import signal
+        
+        def signal_handler(signum, frame):
+            logger.info("🛑 收到关闭信号,开始清理...")
+            print("正在关闭服务...")
+            exit(0)
+        
+        signal.signal(signal.SIGINT, signal_handler)
+        signal.signal(signal.SIGTERM, signal_handler)
+        
+        # 启动传统Flask应用
+        app.run(host="0.0.0.0", port=8000, debug=False, threaded=True) 

+ 14 - 0
test/custom_react_agent/asgi_app.py

@@ -0,0 +1,14 @@
+"""
+ASGI应用启动文件
+提供独立的ASGI启动选项,用于生产环境或uvicorn命令行启动
+"""
+from asgiref.wsgi import WsgiToAsgi
+from api import app
+
+# 将Flask WSGI应用转换为ASGI应用
+asgi_app = WsgiToAsgi(app)
+
+# 这个文件可以通过以下方式启动:
+# uvicorn asgi_app:asgi_app --host 0.0.0.0 --port 8000
+# 或
+# uvicorn asgi_app:asgi_app --host 0.0.0.0 --port 8000 --reload

+ 19 - 0
test/custom_react_agent/requirements.txt

@@ -0,0 +1,19 @@
+# ASGI支持依赖(解决事件循环冲突问题)
+uvicorn>=0.24.0
+asgiref>=3.7.0
+
+# Flask相关
+flask>=3.0.0
+redis>=4.0.0
+
+# LangChain和LangGraph
+langchain>=0.1.0
+langchain-openai>=0.0.5
+langgraph>=0.0.28
+
+# 数据处理
+pandas>=1.5.0
+
+# 其他可能需要的依赖
+requests>=2.28.0
+python-dotenv>=0.19.0

+ 97 - 0
test/custom_react_agent/test_asgi_setup.py

@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+"""
+测试ASGI设置是否正确
+"""
+import sys
+import importlib.util
+
+def test_asgi_dependencies():
+    """测试ASGI依赖是否可用"""
+    print("🧪 测试ASGI依赖...")
+    
+    # 测试uvicorn
+    try:
+        import uvicorn
+        print(f"   ✅ uvicorn: {uvicorn.__version__}")
+    except ImportError:
+        print("   ❌ uvicorn: 未安装")
+        print("      安装命令: pip install uvicorn")
+        return False
+    
+    # 测试asgiref
+    try:
+        import asgiref
+        print(f"   ✅ asgiref: {asgiref.__version__}")
+    except ImportError:
+        print("   ❌ asgiref: 未安装")
+        print("      安装命令: pip install asgiref")
+        return False
+    
+    # 测试WsgiToAsgi
+    try:
+        from asgiref.wsgi import WsgiToAsgi
+        print("   ✅ WsgiToAsgi: 可用")
+    except ImportError:
+        print("   ❌ WsgiToAsgi: 不可用")
+        return False
+    
+    return True
+
+def test_api_import():
+    """测试API模块是否可以正常导入"""
+    print("\n🧪 测试API模块导入...")
+    
+    try:
+        from api import app
+        print("   ✅ Flask应用导入成功")
+        return True
+    except ImportError as e:
+        print(f"   ❌ Flask应用导入失败: {e}")
+        return False
+
+def test_asgi_conversion():
+    """测试ASGI转换是否工作"""
+    print("\n🧪 测试ASGI转换...")
+    
+    try:
+        from asgiref.wsgi import WsgiToAsgi
+        from api import app
+        
+        asgi_app = WsgiToAsgi(app)
+        print("   ✅ WSGI到ASGI转换成功")
+        return True
+    except Exception as e:
+        print(f"   ❌ ASGI转换失败: {e}")
+        return False
+
+def main():
+    """主测试函数"""
+    print("=" * 50)
+    print("🚀 ASGI设置测试")
+    print("=" * 50)
+    
+    success = True
+    
+    # 测试依赖
+    if not test_asgi_dependencies():
+        success = False
+    
+    # 测试API导入
+    if not test_api_import():
+        success = False
+    
+    # 测试ASGI转换
+    if success and not test_asgi_conversion():
+        success = False
+    
+    print("\n" + "=" * 50)
+    if success:
+        print("✅ 所有测试通过!可以使用ASGI模式启动")
+        print("💡 启动命令: python api.py")
+    else:
+        print("❌ 测试失败,请检查依赖安装")
+        print("💡 安装命令: pip install uvicorn asgiref")
+    print("=" * 50)
+
+if __name__ == "__main__":
+    main()