Learn-Agentic-AI的API设计:错误处理与状态码规范最佳实践

Learn-Agentic-AI的API设计:错误处理与状态码规范最佳实践

【免费下载链接】learn-agentic-ai Learn Agentic AI using Dapr Agentic Cloud Ascent (DACA) Design Pattern: OpenAI Agents SDK, Memory, MCP, Knowledge Graphs, Docker, Docker Compose, and Kubernetes. 【免费下载链接】learn-agentic-ai 项目地址: https://gitcode.com/GitHub_Trending/le/learn-agentic-ai

引言:API错误处理的重要性

在构建AI代理系统时,API(应用程序编程接口)是连接不同组件的关键桥梁。良好的错误处理机制和清晰的状态码规范能够显著提升系统的可靠性和可维护性。本文将深入探讨Learn-Agentic-AI项目中API设计的错误处理策略和状态码规范,帮助开发者构建更加健壮的AI代理应用。

API错误处理框架概述

MCP协议中的错误处理架构

Learn-Agentic-AI项目采用了MCP(Modular Communication Protocol)协议作为AI代理之间的通信标准。MCP协议定义了一套完整的错误处理机制,确保代理之间的通信可靠且可预测。

在MCP协议中,错误处理主要通过以下几个组件实现:

  1. 错误类型定义:预定义了常见的错误类型,如认证错误、资源不存在、参数无效等。
  2. 错误响应格式:统一的错误响应结构,包含错误代码、描述信息和详细上下文。
  3. 错误传播机制:确保错误信息能够正确地在代理之间传递和处理。

MCP协议架构

相关模块路径:

错误处理流程

MCP协议中的错误处理流程可以概括为以下几个步骤:

  1. 错误检测:在API调用过程中检测到错误情况。
  2. 错误封装:将错误信息按照预定义格式进行封装。
  3. 错误返回:通过API响应返回错误信息。
  4. 错误处理:客户端根据错误信息进行相应的处理。

下面是一个错误处理的示例代码:

async def call_tool(self, tool_name: str, tool_input: dict) -> types.CallToolResult | None:
    try:
        # 尝试调用工具
        response = await self.session.post(
            f"{self.server_url}/tools/{tool_name}",
            json={"input": tool_input}
        )
        response.raise_for_status()  # 如果状态码 >= 400,抛出异常
        return await response.json()
    except httpx.HTTPError as e:
        # 处理HTTP错误
        if e.response.status_code == 404:
            return types.CallToolResult(
                success=False,
                error=types.ErrorData(
                    code="TOOL_NOT_FOUND",
                    message=f"Tool '{tool_name}' not found",
                    details=str(e)
                )
            )
        elif e.response.status_code == 400:
            return types.CallToolResult(
                success=False,
                error=types.ErrorData(
                    code="INVALID_INPUT",
                    message="Invalid input parameters",
                    details=await e.response.json()
                )
            )
        else:
            return types.CallToolResult(
                success=False,
                error=types.ErrorData(
                    code="SERVER_ERROR",
                    message=f"Server error: {e.response.status_code}",
                    details=str(e)
                )
            )

状态码规范详解

HTTP状态码在MCP协议中的应用

MCP协议基于HTTP协议构建,因此充分利用了HTTP状态码来表示API调用的结果。以下是MCP协议中常用的HTTP状态码及其含义:

状态码类别描述示例场景
200成功请求成功处理工具调用成功
201成功资源创建成功创建新的AI代理会话
400客户端错误请求参数无效工具调用参数缺失
401客户端错误未授权缺少API密钥
403客户端错误禁止访问权限不足
404客户端错误资源不存在请求的工具不存在
429客户端错误请求过于频繁API调用频率超限
500服务器错误服务器内部错误AI模型处理失败
503服务器错误服务不可用AI代理暂时下线维护

自定义错误码体系

除了HTTP状态码,MCP协议还定义了一套自定义错误码体系,用于更精确地描述错误类型。自定义错误码通常与HTTP状态码结合使用,提供更丰富的错误信息。

常见的自定义错误码类别包括:

  1. 认证错误:与身份验证相关的错误,如无效令牌、令牌过期等。
  2. 授权错误:与权限相关的错误,如禁止访问、操作权限不足等。
  3. 资源错误:与资源操作相关的错误,如资源不存在、资源已存在等。
  4. 参数错误:与请求参数相关的错误,如参数缺失、参数格式错误等。
  5. 业务逻辑错误:与业务规则相关的错误,如操作不允许、状态无效等。
  6. 系统错误:与系统组件相关的错误,如数据库连接失败、服务超时等。

下面是一个自定义错误码的示例实现:

class MCPErrorCodes:
    # 认证错误
    INVALID_TOKEN = "AUTH_001"
    TOKEN_EXPIRED = "AUTH_002"
    MISSING_TOKEN = "AUTH_003"
    
    # 授权错误
    FORBIDDEN = "AUTH_004"
    PERMISSION_DENIED = "AUTH_005"
    
    # 资源错误
    RESOURCE_NOT_FOUND = "RES_001"
    RESOURCE_ALREADY_EXISTS = "RES_002"
    RESOURCE_LOCKED = "RES_003"
    
    # 参数错误
    PARAM_MISSING = "PARAM_001"
    PARAM_INVALID = "PARAM_002"
    PARAM_OUT_OF_RANGE = "PARAM_003"
    
    # 业务逻辑错误
    OPERATION_NOT_ALLOWED = "BUS_001"
    INVALID_STATE = "BUS_002"
    RATE_LIMIT_EXCEEDED = "BUS_003"
    
    # 系统错误
    DATABASE_ERROR = "SYS_001"
    SERVICE_TIMEOUT = "SYS_002"
    EXTERNAL_SERVICE_ERROR = "SYS_003"

错误响应格式标准

标准错误响应结构

MCP协议定义了统一的错误响应格式,确保客户端能够一致地解析错误信息。标准错误响应格式如下:

{
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable error message",
    "details": {
      // 错误相关的详细信息
    },
    "request_id": "unique-request-identifier",
    "timestamp": "2023-10-05T12:34:56Z"
  }
}

各字段说明:

  • code: 自定义错误码,如AUTH_001RES_001等。
  • message: 人类可读的错误描述。
  • details: 包含错误相关的详细信息,如无效参数列表、堆栈跟踪等。
  • request_id: 请求的唯一标识符,用于问题排查。
  • timestamp: 错误发生的时间戳。

错误响应示例

以下是几个常见错误场景的响应示例:

  1. 无效令牌错误(401 Unauthorized):
{
  "error": {
    "code": "AUTH_001",
    "message": "Invalid authentication token",
    "details": {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
    },
    "request_id": "req-123456",
    "timestamp": "2023-10-05T12:34:56Z"
  }
}
  1. 资源不存在错误(404 Not Found):
{
  "error": {
    "code": "RES_001",
    "message": "AI agent not found",
    "details": {
      "agent_id": "agent-456"
    },
    "request_id": "req-789012",
    "timestamp": "2023-10-05T12:35:10Z"
  }
}
  1. 参数无效错误(400 Bad Request):
{
  "error": {
    "code": "PARAM_002",
    "message": "Invalid parameter value",
    "details": {
      "parameter": "temperature",
      "value": "high",
      "expected_type": "number",
      "allowed_range": [0.0, 1.0]
    },
    "request_id": "req-345678",
    "timestamp": "2023-10-05T12:36:22Z"
  }
}

相关代码实现:03_ai_protocols/01_mcp/extra/11_SSE/python_sse/python_sse_server.py

@app.post("/send_message")
async def send_message(session_id: str = Query(...), message: str = Query(...)):
    """Receive client message and push SSE confirmation."""
    if session_id not in sessions:
        return Response(
            content=json.dumps({
                "error": {
                    "code": "RES_001",
                    "message": "Invalid session ID",
                    "details": {"session_id": session_id},
                    "request_id": str(uuid.uuid4()),
                    "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
                }
            }), 
            status_code=404,
            media_type="application/json"
        )
    # ... 正常处理逻辑 ...

实践案例:错误处理实现

认证错误处理

认证是API安全的第一道防线,MCP协议提供了完善的认证错误处理机制。以下是一个认证中间件的示例实现:

async def auth_middleware(request: Request, call_next):
    # 跳过不需要认证的路由
    if request.url.path in ["/health", "/auth/login"]:
        return await call_next(request)
        
    # 获取Authorization头
    auth_header = request.headers.get("Authorization")
    if not auth_header:
        return JSONResponse(
            content={
                "error": {
                    "code": "AUTH_003",
                    "message": "Missing authentication token",
                    "details": {"required_header": "Authorization"},
                    "request_id": str(uuid.uuid4()),
                    "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
                }
            },
            status_code=401
        )
        
    # 验证token格式
    if not auth_header.startswith("Bearer "):
        return JSONResponse(
            content={
                "error": {
                    "code": "AUTH_001",
                    "message": "Invalid authentication token format",
                    "details": {"expected_format": "Bearer <token>"},
                    "request_id": str(uuid.uuid4()),
                    "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
                }
            },
            status_code=401
        )
        
    # 验证token有效性
    token = auth_header.split(" ")[1]
    try:
        # 验证token
        payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
        request.state.user_id = payload.get("sub")
        return await call_next(request)
    except jwt.ExpiredSignatureError:
        return JSONResponse(
            content={
                "error": {
                    "code": "AUTH_002",
                    "message": "Authentication token expired",
                    "details": {"expired_at": payload.get("exp")},
                    "request_id": str(uuid.uuid4()),
                    "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
                }
            },
            status_code=401
        )
    except jwt.InvalidTokenError:
        return JSONResponse(
            content={
                "error": {
                    "code": "AUTH_001",
                    "message": "Invalid authentication token",
                    "details": {"error": "Invalid signature or claims"},
                    "request_id": str(uuid.uuid4()),
                    "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
                }
            },
            status_code=401
        )

业务逻辑错误处理

业务逻辑错误是API使用过程中常见的错误类型,以下是一个工具调用的错误处理示例:

@app.post("/tools/{tool_name}")
async def call_tool(tool_name: str, request: Request):
    try:
        # 获取工具定义
        tool = get_tool_by_name(tool_name)
        if not tool:
            return JSONResponse(
                content={
                    "error": {
                        "code": "RES_001",
                        "message": f"Tool '{tool_name}' not found",
                        "details": {"available_tools": get_available_tools()},
                        "request_id": str(uuid.uuid4()),
                        "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
                    }
                },
                status_code=404
            )
            
        # 解析请求参数
        try:
            tool_input = await request.json()
        except json.JSONDecodeError:
            return JSONResponse(
                content={
                    "error": {
                        "code": "PARAM_002",
                        "message": "Invalid JSON format in request body",
                        "details": {"error": "Invalid JSON"},
                        "request_id": str(uuid.uuid4()),
                        "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
                    }
                },
                status_code=400
            )
            
        # 验证请求参数
        validation_result = tool.validate_input(tool_input)
        if not validation_result.is_valid:
            return JSONResponse(
                content={
                    "error": {
                        "code": "PARAM_001",
                        "message": "Invalid tool input parameters",
                        "details": {
                            "errors": validation_result.errors,
                            "schema": tool.input_schema
                        },
                        "request_id": str(uuid.uuid4()),
                        "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
                    }
                },
                status_code=400
            )
            
        # 调用工具
        try:
            result = await tool.execute(tool_input)
            return JSONResponse(content={"result": result})
        except ToolExecutionError as e:
            return JSONResponse(
                content={
                    "error": {
                        "code": "BUS_001",
                        "message": f"Tool execution failed: {str(e)}",
                        "details": {"tool_name": tool_name, "error": str(e)},
                        "request_id": str(uuid.uuid4()),
                        "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
                    }
                },
                status_code=500
            )
            
    except Exception as e:
        # 捕获未预料到的异常
        logger.error(f"Unexpected error in tool call: {str(e)}", exc_info=True)
        return JSONResponse(
            content={
                "error": {
                    "code": "SYS_001",
                    "message": "An unexpected error occurred",
                    "details": {"error_id": str(uuid.uuid4())},
                    "request_id": str(uuid.uuid4()),
                    "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
                }
            },
            status_code=500
        )

客户端错误处理示例

在客户端,正确处理API错误同样重要。以下是一个MCP客户端的错误处理示例:

async def main():
    """A simple client to demonstrate receiving MCP logging notifications."""
    server_url = "http://localhost:8000/mcp/"
    print(f"🚀 Connecting to MCP server at {server_url}")

    try:
        async with streamablehttp_client(server_url) as (read_stream, write_stream, get_session_id):
            async with ClientSession(read_stream, write_stream, logging_callback=log_handler) as session:
                print("✅ Connected. Initializing session...")
                await session.initialize()
                print("🛠️ Session initialized.")

                print("\nSCENARIO 1: Successful processing")
                print("-" * 40)
                result = await session.call_tool("process_item", {"item_id": "item-123", "should_fail": False})
                if result.success:
                    print(f"✅ Result: {result.content[0].text}")
                else:
                    print(f"❌ Error: {result.error.code} - {result.error.message}")
                    print(f"   Details: {result.error.details}")

                await asyncio.sleep(1)

                print("\nSCENARIO 2: Processing with failure")
                print("-" * 40)
                result = await session.call_tool("process_item", {"item_id": "item-456", "should_fail": True})
                if result.success:
                    print(f"✅ Result: {result.content[0].text}")
                else:
                    print(f"❌ Error: {result.error.code} - {result.error.message}")
                    print(f"   Details: {result.error.details}")

    except AuthenticationError as e:
        print(f"\n🔒 Authentication Error: {e.code} - {e.message}")
        print("   Please check your credentials and try again.")
    except AuthorizationError as e:
        print(f"\n🚫 Authorization Error: {e.code} - {e.message}")
        print("   You don't have permission to perform this action.")
    except ResourceNotFoundError as e:
        print(f"\n🔍 Resource Not Found: {e.code} - {e.message}")
        print(f"   Details: {e.details}")
    except ClientError as e:
        print(f"\n⚠️ Client Error: {e.code} - {e.message}")
        print(f"   Please check your request and try again.")
    except ServerError as e:
        print(f"\n💥 Server Error: {e.code} - {e.message}")
        print(f"   Request ID: {e.request_id}")
        print("   Please contact support with the request ID for assistance.")
    except Exception as e:
        print(f"\n❌ An unexpected error occurred: {str(e)}")
        print("💡 Make sure the server is running.")

    print("\n🎉 Demo finished.")

相关模块路径:

总结与最佳实践建议

错误处理最佳实践总结

  1. 统一错误格式:始终使用统一的错误响应格式,确保客户端能够一致地解析错误信息。
  2. 精确错误分类:使用HTTP状态码和自定义错误码的组合,提供精确的错误分类。
  3. 提供有用信息:在错误响应中包含足够的细节,帮助开发者诊断和解决问题。
  4. 安全考虑:避免在生产环境中返回敏感信息,如堆栈跟踪、数据库连接字符串等。
  5. 日志记录:对所有错误进行详细日志记录,包括请求ID、时间戳和上下文信息,便于问题排查。
  6. 友好提示:为终端用户提供友好的错误提示,同时为开发者提供详细的技术信息。
  7. 错误处理一致性:在整个API中保持错误处理策略的一致性,避免给开发者带来困惑。

状态码使用建议

  1. 遵循HTTP标准:尽量遵循HTTP状态码的标准含义,避免自定义状态码。
  2. 避免过度使用500错误:500错误表示服务器内部错误,应仅用于未预料到的异常。对于可预见的错误,应使用更具体的状态码。
  3. 正确使用4xx错误:4xx错误表示客户端问题,应明确指出客户端需要如何修正请求。
  4. 使用1xx状态码表示中间状态:对于长时间运行的操作,可以使用1xx状态码表示操作正在进行中。

持续改进建议

  1. 错误监控:实现错误监控系统,跟踪常见错误类型和频率,优先解决高频错误。
  2. 错误文档:为所有可能的错误码提供详细文档,包括错误原因、解决方法和示例。
  3. 客户端SDK:提供客户端SDK,封装错误处理逻辑,简化开发者使用API的难度。
  4. 定期审计:定期审计API错误日志,识别潜在问题,持续改进错误处理机制。

参考资源

【免费下载链接】learn-agentic-ai Learn Agentic AI using Dapr Agentic Cloud Ascent (DACA) Design Pattern: OpenAI Agents SDK, Memory, MCP, Knowledge Graphs, Docker, Docker Compose, and Kubernetes. 【免费下载链接】learn-agentic-ai 项目地址: https://gitcode.com/GitHub_Trending/le/learn-agentic-ai

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

抵扣说明:

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

余额充值