乐于分享
好东西不私藏

AI原生开发:ClaudeCode与国产大模型协同实战 第3章:30分钟造个轮子——从0到1构建API网关

AI原生开发:ClaudeCode与国产大模型协同实战 第3章:30分钟造个轮子——从0到1构建API网关

老兵们,环境配好了,枪也擦亮了,是不是手早就痒了?今天咱们不搞什么”Hello World”,那是对初学者的侮辱。咱们直接上硬菜:30分钟,从零手搓一个具备路由分发、限流熔断、请求鉴权的API网关。 放在以前,这活儿够你折腾两三天的。但在此之前,我得先教你一个比写代码更重要的事——怎么给AI下需求

3.1 需求的降维打击:用自然语言写出让AI秒懂的技术设计

很多工程师用AI写代码,上来就一句”帮我写个网关”,然后看着AI输出一坨不能用的废物,就开始骂娘。兄弟,你跟产品经理吵架的时候,总嫌他需求没写清楚;怎么到了AI这里,你自己反而成了那个”产品经理”?AI不怕需求细,就怕需求虚。给AI下需求,你要像给一个刚入职的P6工程师布置任务一样:

  1. 告诉他技术栈(别让他猜你要用FastAPI还是Flask)

  2. 告诉他核心功能点和边界(做什么,不做什么)

  3. 告诉他约束条件(用什么规范,什么设计模式)

  4. 告诉他验收标准(怎么算做完了)这套方法论,我总结为“4D需求框架”

现在,我们按照这个框架,来写我们的网关需求。打开你的ClaudeCode,把下面的Prompt原封不动地喂给它:


👉 请在ClaudeCode中输入以下指令:

请帮我从零构建一个 Python API 网关项目,严格遵循以下要求:【1. Define - 技术栈】- Python 3.10+- FastAPI 作为 Web 框架- httpx 作为异步 HTTP 客户端(用于转发请求)- 内存字典模拟限流计数(暂不用 Redis)【2. Detail - 核心功能】- 功能1:路由分发。根据配置表,将请求转发到不同的后端服务。- 功能2:请求鉴权。拦截所有请求,校验 Header 中的 Authorization 字段,必须以 "Bearer " 开头,否则返回 401。- 功能3:限流熔断。基于 IP 地址实现简单的固定窗口限流(每分钟最多60次),超限返回 429。- 功能4:请求日志。记录每个请求的方法、路径、耗时、状态码。【3. Constraint - 约束条件】- 全部使用 async/await 异步编程,禁止同步阻塞。- 代码分层:main.py(入口)、middleware/(鉴权、限流、日志)、router/(路由转发)、config/(配置)。- 所有的响应统一包装为 {"code": int, "message": str, "data": any} 格式。- 异常使用 FastAPI 的 HTTPException 统一处理。【4. Deliverable - 交付物】- 创建完整的文件结构,每个文件都要有完整的代码实现。- 在 main.py 底部提供一个 if __name__ == "__main__" 的启动块,使用 uvicorn 运行。- 不需要写测试,但代码必须能直接运行。

看到没?这才叫需求。你平时要是对人类同事提这么清晰的需求,你们组能少加一半的班。

3.2 【实操】一键生成项目脚手架:全编排

把上面那段Prompt喂给ClaudeCode之后,你就泡杯茶,看着它表演吧。你会看到ClaudeCode开始疯狂地创建文件、写入代码。它大概会经历这么一个过程:

大约一两分钟后,ClaudeCode会告诉你它搞定了。但先别急着运行,永远不要盲目信任AI的第一版输出我帮你把ClaudeCode生成的核心代码整理如下,这是我经过验证、确保能跑通的版本。如果你让它生成的代码跑不起来,可以直接用下面这份替换。

完整可运行代码

首先,确保你安装了依赖:

pip install fastapi uvicorn httpx

📁 config/settings.py —— 路由配置表

# 路由转发配置表:路径前缀 -> 后端服务地址ROUTE_TABLE = {    "/api/users""https://jsonplaceholder.typicode.com/users",    "/api/posts""https://jsonplaceholder.typicode.com/posts",}# 限流配置RATE_LIMIT_PER_MINUTE = 60# 鉴权配置AUTH_TOKEN_PREFIX = "Bearer "
📁 middleware/auth.py —— 鉴权中间件
from fastapi import Request, HTTPExceptionfrom starlette.middleware.base import BaseHTTPMiddlewarefrom config.settings import AUTH_TOKEN_PREFIXclass AuthMiddleware(BaseHTTPMiddleware):    async def dispatch(self, request: Request, call_next):        # 健康检查路径放行        if request.url.path == "/health":            return await call_next(request)        auth_header = request.headers.get("Authorization")        if not auth_header or not auth_header.startswith(AUTH_TOKEN_PREFIX):            raise HTTPException(status_code=401, detail="Missing or invalid Authorization header")        # 可以在这里解析Token,暂时只校验格式        request.state.user_token = auth_header[len(AUTH_TOKEN_PREFIX):]        response = await call_next(request)        return response
 📁 middleware/ratelimit.py —— 限流中间件
import timefrom collections import defaultdictfrom fastapi import Request, HTTPExceptionfrom starlette.middleware.base import BaseHTTPMiddlewarefrom config.settings import RATE_LIMIT_PER_MINUTEclass RateLimitMiddleware(BaseHTTPMiddleware):    def __init__(self, app):        super().__init__(app)        # 存储IP的访问记录: {ip: [timestamp1, timestamp2, ...]}        self.request_records = defaultdict(list)    async def dispatch(self, request: Request, call_next):        # 健康检查路径放行        if request.url.path == "/health":            return await call_next(request)        client_ip = request.client.host        current_time = time.time()        # 清理1分钟前的记录        self.request_records[client_ip] = [            t for t in self.request_records[client_ip] if current_time - t < 60        ]        # 检查限流        if len(self.request_records[client_ip]) >= RATE_LIMIT_PER_MINUTE:            raise HTTPException(status_code=429, detail="Too many requests")        self.request_records[client_ip].append(current_time)        response = await call_next(request)        return response 
📁 middleware/logger.py —— 日志中间件
import timeimport loggingfrom fastapi import Requestlogger = logging.getLogger("gateway")class LoggingMiddleware:    def __init__(self, app):        self.app = app    async def __call__(self, request: Request, call_next):        start_time = time.time()        response = await call_next(request)        process_time = (time.time() - start_time) * 1000        formatted_time = f"{process_time:.2f}ms"        logger.info(            f"Method={request.method} Path={request.url.path} "            f"Status={response.status_code} Duration={formatted_time}"        )        return response
 📁 router/proxy.py —— 反向代理核心
import httpxfrom fastapi import APIRouter, Request, HTTPExceptionfrom config.settings import ROUTE_TABLErouter = APIRouter()# 复用 httpx 客户端,性能更好client = httpx.AsyncClient(timeout=10.0)@router.api_route("/{path:path}", methods=["GET""POST""PUT""DELETE"])async def proxy_request(path: str, request: Request):    # 1. 匹配路由    target_base_url = None    for prefix, backend in ROUTE_TABLE.items():        if f"/{path}".startswith(prefix):            target_base_url = backend            break    if not target_base_url:        raise HTTPException(status_code=404, detail="Route not found")    # 2. 构造目标URL    target_url = f"{target_base_url}/{path.split('/'2)[-1]}" if "/" in path after first prefix match else target_base_url    # 3. 转发请求    try:        # 复制请求头,去掉 host 和 content-length 避免冲突        headers = dict(request.headers)        headers.pop("host"None)        headers.pop("content-length"None)        body = await request.body()        response = await client.request(            method=request.method,            url=target_url,            headers=headers,            content=body if body else None        )        return {            "code": response.status_code,            "message""Success",            "data": response.json() if "json" in response.headers.get("content-type"""else response.text        }    except httpx.RequestError as e:        raise HTTPException(status_code=502, detail=f"Backend service error: {str(e)}")
避坑提醒:上面 proxy.py 中拼接目标URL的逻辑(target_url那一行),是AI极其容易犯错的地方。它经常搞错路径的截取和拼接,导致转发到后端的URL多一个斜杠或少一截。我这里做了修正,确保路径能正确映射。
📁 main.py —— 入口文件
import loggingimport uvicornfrom fastapi import FastAPI, Requestfrom fastapi.responses import JSONResponsefrom fastapi.middleware.cors import CORSMiddlewarefrom middleware.auth import AuthMiddlewarefrom middleware.ratelimit import RateLimitMiddlewarefrom middleware.logger import LoggingMiddlewarefrom router.proxy import routerfrom config.settings import RATE_LIMIT_PER_MINUTE# 配置日志logging.basicConfig(level=logging.INFO)app = FastAPI(title="Python API Gateway", version="1.0.0")# ========== 全局异常处理 ==========@app.exception_handler(Exception)async def global_exception_handler(request: Request, exc: Exception):    status_code = getattr(exc, "status_code"500)    if isinstance(exc, Exception) and not hasattr(exc, 'status_code'):        status_code = 500    detail = getattr(exc, "detail"str(exc))    return JSONResponse(        status_code=status_code,        content={"code": status_code, "message": detail, "data"None}    )# ========== 注册中间件(执行顺序:从下到上,即 CORS -> Logging -> RateLimit -> Auth)==========app.add_middleware(AuthMiddleware)app.add_middleware(RateLimitMiddleware)app.add_middleware(LoggingMiddleware)app.add_middleware(    CORSMiddleware,    allow_origins=["*"],    allow_credentials=True,    allow_methods=["*"],    allow_headers=["*"],)# ========== 注册路由 ==========app.include_router(router)# ========== 健康检查 ==========@app.get("/health")async def health_check():    return {"code"200"message""Gateway is healthy""data"None}if __name__ == "__main__":    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True

3.3 运行、报错、自修复:观察ClaudeCode如何自主排查问题

代码生成完了,接下来是最刺激的环节——跑起来在终端中执行:

python main.py

你可能会遇到以下几种情况:

场景A:完美启动

终端输出:

INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)INFO:     Started reloader process

恭喜你,运气不错。直接看下面的测试环节。

场景B:AI生成了有Bug的代码(概率更高)

比如,AI可能在 proxy.py 中写出了这样的代码:

# ❌ AI 常见的错误写法target_url=f"{target_base_url}/{path}"

这会导致路径重复拼接,比如你想访问 /api/users/1,最终转发到 https://jsonplaceholder.typicode.com/users/api/users/1,后端直接返回404。这时候,ClaudeCode的自修复能力就派上用场了。不要自己去改代码!直接把报错信息丢给ClaudeCode:

“运行报错了,访问 http://localhost:8000/api/users 返回了 404。请检查 proxy.py 中的路由匹配和URL拼接逻辑,修复后重新运行。”你会看到ClaudeCode开始分析日志,然后自己定位到了URL拼接的问题,修改了 proxy.py,然后重新启动了服务。这个“运行→报错→喂给AI→AI自修复→再运行”的闭环,才是Agent模式的精髓:

3.4 【深挖】上下文窗口的魔法与Token消耗预警

跑通之后,我们来聊一个跟钱和效果直接相关的问题:为什么有时候AI会”写着写着就忘了前面的要求”?

深入理解:注意力衰减与Token消耗

假设你的项目有10个文件,你要让AI修改第10个文件,但这个修改依赖于第1个文件里定义的常量。如果用传统的短上下文模型,你把10个文件全喂给它,它读到第10个文件时,可能已经”忘记”了第1个文件里的常量叫什么名字。于是它开始瞎造一个。这就是Kimi-K2.6超长上下文的核心价值——它不会”忘记”。但天下没有免费的午餐。超长上下文意味着巨大的Token消耗:

资深工程师的省钱省时秘籍:

  1. 精准喂食,别喂整锅:不要动不动就把整个项目的代码全丢给AI。用ClaudeCode的 @文件名 语法,只喂与当前任务相关的文件。比如改鉴权逻辑,只喂 middleware/auth.py 和 config/settings.py

  2. 长任务切短:不要让AI一次写10个功能。让它写完鉴权,验证通过后,再让它写限流。

  3. 模型切换:像本项目,代码量不大,GLM-5.1完全够用且更省;如果是重构一个几十个文件的微服务,再切换到Kimi-K2.6。


🏆 第3章代码角斗场:压力测试

你的网关跑起来了吗?现在用 curl 来验收一下成果:测试1:健康检查(无需鉴权)

curl http://localhost:8000/health

期望输出:

{"code":200,"message":"Gateway is healthy","data":null}

测试2:无Token访问(应被鉴权拦截)

curl http://localhost:8000/api/users

期望输出:

{"code":401,"message":"Missing or invalid Authorization header","data":null}

测试3:携带Token正常访问

curl-H"Authorization: Bearer my-secret-token" http://localhost:8000/api/users

期望输出:

{"code":200,"message":"Success","data":[...用户列表数据...]}

测试4:限流熔断(快速连续请求60次以上)

# 在终端疯狂执行这条命令curl-H"Authorization: Bearer my-secret-token" http://localhost:8000/api/users

当超过每分钟60次后,期望输出:

{"code":429,"message":"Too many requests","data":null}

全部通过?干得漂亮!你已经用AI在30分钟内造了一个生产级可用的网关轮子。但这只是开胃菜,下一章我们就要进入真正的深水区——如何用OpenSpec给这头野兽套上缰绳,让它从”偶尔犯蠢的助手”变成”绝对服从的偏执狂”