最近在做一个小项目,期间最多被问到的一个问题是:都说 AI 能写代码,到底能不能从零搭一个能上线的 API 服务?这周我专门抽时间按这个思路走了一遍,最终交付了一个“文本摘要服务”——对外暴露 REST API,接收一段文本,返回摘要和关键词。整条链路里,从项目骨架、业务代码到测试用例,绝大多数代码都是 AI 先生成、我再 Review 和调整;同时,服务本身也调用了大模型 API 完成文本理解。这篇文章就完整记录这次实战里我的技术选型、Prompt 编写、踩坑经历和上线经验,给想用 AI 快速搭 API 服务的朋友一份可以直接照做的参考。
1. 项目整体设计:先定边界,再让 AI 进场
1.1 需求拆解:这个 API 到底要做什么
很多朋友一上来就让 AI“直接给我写个 API”,结果 AI 吐出一大堆代码,本地却怎么都跑不通。原因很简单:没先定义边界。这次我给自己定的需求是极简的:
- 提供一个
POST /v1/summarize接口,接收{text, max_length},返回{summary, keywords}。 - 提供一个
GET /health用于健康检查,方便后续部署后做探活。 - 服务必须在本地一条命令启动,代码量控制在几百行内,不引入复杂框架。
- 大模型 API 的 key 只能保存在服务端,绝不能暴露给调用方。
这种小范围需求非常适合做第一个 AI 辅助实战。目标太大会浪费大量时间在无关环节上,目标太小又体现不出 AI 的优势。我强烈建议你在让 AI 动手之前,先像这样把范围压到“小到不能再小”,哪怕只是做一个返回固定 JSON 的接口也行。真正的业务逻辑可以在后续迭代里一点点加,但工程骨架第一次就要立住。
1.2 技术选型:为什么选 FastAPI 而不是别的
AI 生成代码比较擅长主流框架,比如 Node.js 的 Express/Fastify、Python 的 FastAPI/Flask。我最终选了 FastAPI,原因有三个:
- FastAPI 自带的 Pydantic 可以很方便地做请求参数校验和响应模型定义,省掉手写参数判断的脏活。
- 它默认生成 OpenAPI 文档,Swagger UI 直接挂在
/docs,联调时非常直观。 - Python 生态里调用大模型 SDK 比较顺手,异步支持也能减少 IO 等待,适合对接外部 API。
我也在 prompt 里要求 AI 严格使用 FastAPI 的依赖注入方式写代码,而不是像 Flask 那样全局变量满天飞。这个决定帮我省了后面大量重构时间。
作为对比,我也给经常纠结的朋友一个参考方向:
| 方案 | 适合场景 | 需要额外注意 |
|---|---|---|
| FastAPI | 快速迭代、需要自动文档、Python 生态 | 需要理解异步与 Pydantic |
| Flask | 老项目、简单的 HTML 服务 | 参数校验和文档要自己搭 |
| Express/Fastify | Node.js 团队、前后端同语言 | 大模型 SDK 不如 Python 丰富 |
选型没有绝对标准,但一旦选定,就要把需求围绕这个框架描述清楚。AI 生成代码时,最好给它一个确定的技术栈范围,别让它自由发挥。
1.3 AI 在方案里扮演什么角色
这次我把 AI 当成“高级结对工程师”,而不是替我做决策的工具。方案、边界、数据结构由我确定,AI 负责把常见框架代码快速生成初稿,我再逐行 Review、运行验证、修 bug。整个过程很像面试官给候选人布置任务,最终代码质量还是要我把关。
实践中你会发现,LLM 对常见框架的代码范式非常熟悉,比如:FastAPI 的应用入口怎么写、CORS 中间件放哪里、环境变量怎么读取。但对你的业务细节基本不了解,如果你不告诉它“我们要调用哪个大模型、超时多少秒、返回什么字段”,它只会给出一堆通用内容和 TODO。所以我对所有项目成员的统一要求是:需求描述写清楚,AI 生成速度才真正快。
2. 用 AI 生成从零开始的 FastAPI 骨架
2.1 写提示词:一份能拿到高质量代码的 Prompt 模板
很多人说 AI 生成代码不够好用,多半是 prompt 太随意。比如“帮我写一个 API”,这个信息量太少。我这次用的模板可以收藏:
你是一名 Python/FastAPI 专家。请生成一个简单的 API 服务,要求如下: 1. 使用 fastapi、uvicorn、pydantic。 2. 提供 GET /health,返回 {"status": "ok"}。 3. 提供 POST /v1/summarize,接收 {"text": str, "max_length": int = 200}。 4. summarize 逻辑:调用 OpenAI SDK 兼容的大模型接口,base_url 和 api_key 从环境变量读取,模型名为 deepseek-chat。 5. 请求校验:text 不能少于 20 个字符,max_length 限制在 20 到 500 之间。 6. 返回 {"summary": str, "keywords": list[str]}。 7. 加入 CORS 中间件、日志 logger。 8. 密钥禁止硬编码,统一通过 dotenv 加载。 9. 给出项目文件结构、main.py、requirements.txt、.env.example。这个 prompt 基本等价于一份需求文档。AI 拿到后生成的代码,绝大多数可以直接用。关键是包含几个要素:角色定义、技术栈、接口定义、参数约束、安全要求、交付物清单。你也可以根据自己的语言习惯写成对话,但信息密度一定要高。
2.2 让 AI 产出项目骨架的实操过程
我让 AI 生成的项目结构大致如下:
project/ ├── main.py ├── services/ │ └── llm_client.py ├── models.py ├── requirements.txt ├── .env.example └── tests/ └── test_main.py核心的main.py大概是这样的(简写):
from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field from dotenv import load_dotenv import os, httpx, logging load_dotenv() logger = logging.getLogger("uvicorn") app = FastAPI(title="Text Summarize API", version="1.0.0") class SummarizeRequest(BaseModel): text: str = Field(..., min_length=20, max_length=8000) max_length: int = Field(200, ge=20, le=500) class SummarizeResponse(BaseModel): summary: str keywords: list[str] @app.get("/health") def health(): return {"status": "ok"} async def call_llm_api(text: str, max_length: int): api_key = os.getenv("LLM_API_KEY") base_url = os.getenv("LLM_BASE_URL") if not api_key: logger.error("LLM_API_KEY not configured") raise HTTPException(status_code=500, detail="LLM_API_KEY not configured") async with httpx.AsyncClient(timeout=30) as client: resp = await client.post( f"{base_url}/chat/completions", headers={"Authorization": f"Bearer {api_key}"}, json={ "model": os.getenv("LLM_MODEL", "deepseek-chat"), "messages": [ {"role": "system", "content": "你是文本摘要助手,输出 JSON 格式的 summary 和 keywords。"}, {"role": "user", "content": text} ], "max_tokens": max_length, "temperature": 0.3, }, ) resp.raise_for_status() data = resp.json() content = data["choices"][0]["message"]["content"] # 在这里把模型输出的 JSON 解析成 Python 对象 return parse_summary(content)这段代码虽然是 AI 生成的,但我做了几个关键调整。最重要的一点是:我要求它把 timeout 显式加到网络请求里。默认情况下,httpx不会等待无上限,但外部模型接口慢起来能让人崩溃,如果不设超时,用户侧会一直挂起。另一点是,模型返回的不一定是标准 JSON,必须有容错处理,后面测试时会说到。
2.3 人工 Review:AI 代码的三处必改项
AI 展开代码后,我做的第一件事不是运行,而是按三条红线检查:
- 密钥管理:搜索代码里有没有
sk-开头或者os.environ["..."]中写死敏感字符串。一旦发现硬编码,立刻改成.env读取,并确认.gitignore里忽略了.env。这个动作不能依赖 AI,它默认不会帮你做安全审计。 - 依赖版本:AI 给的
requirements.txt经常是fastapi、uvicorn这种不带版本号的写法和乱七八糟的过时包。我会快速改成固定版本,至少锁住主版本号,比如fastapi==0.115.*。否则过几天依赖一升级,接口行为就变了。 - 异常处理:AI 生成的代码多半只有成功路径,对超时、模型服务不可用、返回格式异常这些场景处理很粗糙。这些只能靠有经验的开发者补上。如果项目里没有明确的全局异常处理器,建议直接在 prompt 里要求“所有外部调用都进行异常捕获并返回统一的错误信息”。
Review 完毕后再启动。只有一次跑通的代码,才是真正的可交付代码。
3. 数据库、模型路由与接口细节
3.1 数据模型:用 Pydantic 管好出入参
FastAPI 最爽的部分就是用 Pydantic 定义请求和响应。这次需求里的SummarizeRequest和SummarizeResponse不仅能做参数校验,还能作为 OpenAPI 文档的一部分展示给前端同事。
要注意的是,Field里的约束需要根据真实业务调校。我一开始直接把text限制为最短 10 字符,但联调时发现很多短文本实际长度不到 10,就放宽到了 20;同时把max_length的最小值从 10 调到 20,避免摘要比原文还短。这些细节 AI 是不知道的,必须靠你从业务角度校准。
3.2 记录调用日志与请求历史:从 SQLite 开始
虽然摘要服务不是数据密集型应用,但为了后续查看调用量和排查问题,我加了一张request_logs表,记录每次调用的文本长度、耗时、状态和错误信息。本地先用 SQLite,后续数据量上来再平滑切到 PostgreSQL。
这里可以用 AI 生成 SQLAlchemy 模型:
from sqlalchemy import Column, Integer, String, DateTime, Text from sqlalchemy.sql import func class RequestLog(Base): __tablename__ = "request_logs" id = Column(Integer, primary_key=True) text_len = Column(Integer, nullable=False) duration_ms = Column(Integer, nullable=False) status = Column(String(10), nullable=False) error_msg = Column(Text, nullable=True) created_at = Column(DateTime, server_default=func.now())加日志表的好处是:后面设定告警、看免费额度消耗、判断是否有人刷接口,都有数据支撑。代码实现时,我会在路由函数里用中间件或者依赖注入的方式统计耗时,再写库。简单项目直接在函数里start = time.time()也够用。
3.3 模型路由:同一个接口背后接不同大模型 API
很多人的需求不止接一个模型,比如同时接 DeepSeek、智谱、Kimi 等 API。这次我提前做了一个 Provider 抽象,让同一个/v1/summarize接口可以随时切模型:
LLM_PROVIDER=deepseek LLM_API_KEY=sk-xxx LLM_BASE_URL=https://api.deepseek.com LLM_MODEL=deepseek-chat业务代码里只需要读取当前 Provider 配置,让 AI 生成一层适配器就行。实际反馈是,各家 API 虽然都号称“OpenAI 兼容”,但细节总有差异,比如有些厂商的超时参数、响应格式、token 计费方式不同。所以不要天真地认为一套代码通吃所有厂商,至少要留一个provider字段,后续针对特定模型做参数微调。
另外,别把大模型 API 的能力直接暴露成“万能接口”。我见过不少团队把所有用户输入直接转发给大模型,既不过滤也不限制长度,轻则被刷走大量免费额度,重则产生安全风险。服务端做一层校验和缓存很关键。
4. 测试、联调与排坑实录
4.1 本地跑通与 curl 验证
环境准备这部分没什么捷径:
python -m venv venv source venv/bin/activate pip install -r requirements.txt uvicorn main:app --reload --port 8000启动后用浏览器打开http://localhost:8000/docs,Swagger UI 里可以直接试接口。我更喜欢用 curl,方便脚本化:
curl -X POST http://localhost:8000/v1/summarize \ -H "Content-Type: application/json" \ -d '{ "text": "人工智能正在改变软件开发的流程,从代码生成到测试用例编写,AI 工具已经能承担大量重复劳动。", "max_length": 100 }'正常情况下会返回类似:
{ "summary": "AI 工具正在通过代码生成和测试自动化改变软件开发流程。", "keywords": ["人工智能", "软件开发", "代码生成"] }这里有个小技巧:联调前先把大模型 API 的超时调短一点,比如 5 秒。否则真报错时,你要等很久才能看到错误日志。我第一次就把 timeout 设成了 30 秒,结果排查问题时每一步都像卡死一样。
4.2 让 AI 先生成测试用例,再补 Mock
在 prompt 里要求 AI 生成测试用例是个很高效的做法。但必须强调“Mock 掉外部 API”,否则每跑一次测试都会真实调用大模型接口,既烧钱又慢。我的核心测试长这样:
from fastapi.testclient import TestClient from unittest.mock import AsyncMock, patch from main import app client = TestClient(app) def test_health(): resp = client.get("/health") assert resp.status_code == 200 assert resp.json() == {"status": "ok"} @patch("main.call_llm_api", new_callable=AsyncMock) def test_summarize_success(mock_llm): mock_llm.return_value = {"summary": "测试摘要", "keywords": ["AI"]} resp = client.post("/v1/summarize", json={ "text": "这是一段超过二十个字符的测试文本,用于验证接口返回。", "max_length": 100 }) assert resp.status_code == 200 assert resp.json()["summary"] == "测试摘要"Mock 的目的不是糊弄测试,而是隔离外部依赖,让测试结果稳定。真正的大模型 API 验证放到联调环境里做一两次就够了。测试用例生成出来后,我还会追加几个边界用例:过短文本、超过最大长度的文本、缺少字段的请求,确保参数校验能拦住脏数据。
4.3 常见报错速查表
这次实战从开发到上线,我整理了一张高频问题速查表,很多都是热词里大家反复踩坑的内容。
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
no api key for provider route "deepseek-official" | 服务启动时没有读取到环境变量里的 API Key | 检查.env文件是否被正确加载,确认os.getenv("LLM_API_KEY")非空后重启服务 |
api error: 400 this model's maximum context length is ... | 输入文本太常,超过模型上下文窗口限制 | 在接口层增加文本长度校验,或在调用模型前做截断/分段处理 |
PermissionError: [Errno 13] Permission denied | 启动 Docker 或写目录时权限不够 | 检查当前用户是否在docker组中,或给运行目录配置正确的写入权限 |
address already in use | 端口被其他进程占用 | 换端口或关闭旧进程 |
ModuleNotFoundError: pydantic | 依赖安装不完整 | 重新执行pip install -r requirements.txt,并锁定依赖版本 |
| 返回内容不是合法 JSON | 大模型输出了一段 JSON 前后的解释性文字 | 在 prompt 里要求严格 JSON 输出,代码里做容错解析,例如截取第一个大括号到最后一个大括号 |
其中最坑的是“模型上下文长度超限”。大模型 API 的上下文窗口不是无限大的,比如有些模型的 limit 是 1M tokens,但绝大多数情况是用户传入了一篇超长文档。这时候接口层面的前置截断比模型层的兜底更有效率。你可以先用简单规则卡掉超过 8000 字符的文本,然后再交给模型。
4.4 避坑技巧:CORS 和跨域
如果这个 API 要提供给网页前端调用,CORS 是必须处理的。AI 生成的代码有时会默认允许所有来源,这在开发时方便,但上线前要收敛。推荐按环境控制允许的域名列表,示例:
from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=os.getenv("ALLOWED_ORIGINS", "http://localhost:3000").split(","), allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )顺便说一句,很多人分不清 CORS 和服务器跨域的问题:CORS 是浏览器拦截,不是服务端拒绝。哪怕服务端没配 CORS,用 curl 和 Postman 也照样能通。联调时如果前端报错,先确认请求是否真的发出去了,再看 CORS 配置。
5. 部署上线与运行保障
5.1 Docker 化:从 Dockerfile 到一条命令启动
小项目最怕的就是“在我电脑上能跑”,所以一定要容器化。这个项目的 Dockerfile 很简单:
FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]构建并启动:
docker build -t text-summarize-api . docker run -d --name summarize-api --env-file .env -p 8000:8000 text-summarize-api这里有个易错点:--env-file .env必须在docker run时显式指定,否则容器里读不到环境变量,接口就会报 key 未配置。另外,.dockerignore里一定要排除.venv、.env、__pycache__,否则镜像体积会很大,而且可能把密钥打进去。
如果你在配置 Docker 时遇到Permission denied while trying to connect to the docker API,大概率是当前用户不在docker组里。临时方案是sudo执行或改 socket 权限,长期方案是把用户加入 docker 组后重新登录:
sudo usermod -aG docker $USER5.2 用 systemd 管理进程,而不是 Ctrl+C
容器适合开发验证和交付,但如果在虚拟机里长期跑,我更习惯用 systemd 管理进程。把 venv 里的 uvicorn 作为服务进程,开机自启、崩溃自动重启,比手敲命令靠谱得多。写一个text-summarize-api.service:
[Unit] Description=Text Summarize API After=network.target [Service] User=www-data WorkingDirectory=/opt/app EnvironmentFile=/opt/app/.env ExecStart=/opt/app/venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000 Restart=always RestartSec=5 [Install] WantedBy=multi-user.target注意 systemd 的EnvironmentFile不支持export前缀,.env里不能写export LLM_API_KEY=xxx,直接写LLM_API_KEY=xxx即可。启动后用以下命令控制:
sudo systemctl daemon-reload sudo systemctl enable --now text-summarize-api journalctl -u text-summarize-api -f这种方案和 Docker 不冲突,生产环境也可以用 Nginx 反向代理到容器的端口。选择哪种取决于团队运维习惯,不必纠结。
5.3 调用量、免费额度与缓存策略
大模型 API 是按 token 计费的,虽然大家都有一定免费额度,但自己用着玩和上线给用户用完全是两码事。上线前我做了三件事:
- 加缓存:相同
text一天内不做重复调用。简单起见用内存字典加过期时间,后续可以换 Redis。这样重复请求不会再增加调用量。 - 加限流:用 slowapi 或者自己在中间件里做按 IP 的请求频率限制,避免被脚本刷爆。
- 记录 token 用量:从模型返回的
usage字段里取出total_tokens,写进日志。
缓存那一段代码如下:
cache = {} async def get_summary_cached(text: str, max_length: int): key = f"{text}_{max_length}" if key in cache and cache[key]["expire_at"] > time.time(): return cache[key]["data"] data = await call_llm_api(text, max_length) cache[key] = {"data": data, "expire_at": time.time() + 86400} return data这样做的好处非常明显:测试和前端联调时,反复点同一个按钮不会反复消耗大模型 API 额度。以前我没加缓存,光联调一天就跑掉了上万 token,加了缓存之后,本地测试基本零成本。
5.4 安全加固:上线前必做的几件事
最后说安全,因为 AI 生成的代码默认是不考虑安全粒度的。上线前至少完成这四项:
- 启用 HTTPS:Nginx 或网关层配置 TLS 证书,不能用明文 HTTP 传数据。
- 限制请求体大小:FastAPI 里限制
text最大长度,前面已经做了 8000 字符。Nginx 层也可以再设client_max_body_size 512k。 - 接口接入身份校验:哪怕只是简单的 Bearer Token,也能拦住绝大多数扫描流量。
- 密钥隔离:大模型 API key 只存在服务端,调用方永远不接触。
这些不是可选项。很多人觉得“只是一个内部 API”,但暴露在公网后,扫描器和爬虫随时都可能光顾。我在真实项目中见过因为没加请求体大小限制,被一段 10MB 的请求直接打爆内存的例子,代价远远大于提前加一行配置。
跑完这个项目,我最大的感受是:AI 不是银弹,但绝对是提效杠杆。真正有经验的开发者用 AI,会把需求拆得更细、把约束写得清楚,AI 生成的东西质量自然就高。说白了,AI 写的是初稿,做决定和背锅的还是你自己。
这次我踩过最大的坑就是低估了环境变量配置的重要性,服务在本地一切正常,部署后因为忘记读取.env导致所有请求都返回 500。后来我在所有可能启动服务的方式里都验证了一遍,才彻底解决。
下一步我准备在这个小项目上加批量摘要任务、接入更多 Provider,顺便把 Redis 缓存和限流做实。最后分享一个团队内部的小技巧:AI 生成的代码合并前,至少让另一个有经验的人 Review;如果只是个人项目,也务必在本地跑完pytest再上线。这样就算 AI 帮你把代码写得飞快,你还是能守住最基本的工程质量底线。