
以前写博客总想在页面里放一块“每日一句话”翻遍了各家公共 API要么要注册要么限制调用次数要么返回的内容跟自己的站点气质完全不搭后来干脆决定自己动手搭一个真正属于自己的 API 服务。老实说我不是那种能把框架细节全部背下来的开发者所以这次直接把 AI 当成了核心工具——不会的问它写一半让它接着补报错丢给它解释再不行就让它在原基础上重写。这个选择帮了我大忙。从初始化项目的第一条命令开始到服务真正跑在公网上前前后后只花了一个周末的零碎时间。这篇文章就把当时完整的过程整理出来需求怎么拆、提示词怎么给、哪些代码是 AI 生成的、哪些是我手工改的、上线后又踩了什么坑。如果你也想用 AI 从零搭自己的 API这篇可以直接照着走。1. 这个项目到底要做什么需求拆解和 AI 的角色边界1.1 为什么不愿意再用现成公共 API很多人第一反应是“找个现成接口不就行了”我在决定自建之前也是这么想的。但实际操作下来现成公共 API 通常有三个麻烦一是认证体系复杂有的要申请 AppKey有的要 OAuth 跳转为了在博客侧栏显示一句话搞这么重完全没有必要二是调用次数和并发限制热门接口免费额度低流量稍微大一点就 429而且数据内容、返回结构都是别人定义的想改字段名都没办法三是服务不稳定我之前遇到过接口突然下线的代码还得跟着改。自建 API 的好处在于数据完全在自己手里数据结构、错误码、调用频率都可以自定义想扩展就扩展而且整个链路从数据库到路由到部署都走一遍之后后面再做别的项目心里会非常有底。1.2 把“一个 API 服务”拆成最小可用版本刚开始做项目最容易犯的错是把需求想得太大。我只给自己定了四个接口能跑通就算第一版成功接口动作作用/api/quotes/randomGET随机返回一条句子支持按分类筛选/api/quotesPOST新增一条句子/api/quotesGET按分类分页查询句子列表/api/healthGET健康检查方便部署后确认服务存活这个 MVP 范围刻意排除了用户系统、登录鉴权、前端页面、复杂缓存。原因很简单范围越大AI 生成的代码越不可控出问题的时候你连错误出在哪一层都定位不了。先做一个完整闭环后续再迭代这是我自己做小项目最重要的原则。1.3 AI 能替我写什么不能替我决定什么用好 AI 的前提是搞清楚它的能力边界。对话式大模型本质上是根据你给的上下文一句一句预测最可能的后续内容所以它特别擅长的是模式化很强的代码FastAPI 的路由写法、Pydantic 模型定义、SQLite 的增删改查、pytest 的基础用例这些几乎全网都有类似样本它写出来基本靠谱。但它不擅长替你拍板需求。比如“数据表里到底要存哪些字段”“错误码怎么定义”“要不要做鉴权”“接口返回结构是什么”这些是需要结合业务场景判断的决策AI 只会给你一个平均水平的答案如果你不问它就默认跳过。所以我的用法是AI 负责把代码从 0 写到 80 分我来负责确认这 80 分的方向对不对再把细节补到 95 分。1.4 开工前的另一个准备工作让 AI 工具本身先跑通这个坑很多人会忽略。我用 AI 编程助手的时候第一次调用就报错提示类似no api key for provider route意思是工具默认的模型服务没配好 API Key整条请求直接失败。后来检查发现是环境变量漏配了。所以开工前先把 AI 工具的认证配好用一句最简单的“你好”试通再开始干正事否则你写再多提示词都是白搭。2. 技术选型为什么最后是 FastAPI SQLite Uvicorn2.1 框架对比让 AI 给我各写一个 hello再决定这个项目的核心语言我一开始就定了 Python因为想做 AI 相关扩展的话 Python 生态最顺手。但框架在 Flask 和 FastAPI 之间犹豫了一下最后选择 FastAPI有三个原因类型校验省心。FastAPI 配合 Pydantic请求参数和返回结构都能自动校验前端传错类型直接返回带原因的 422不用自己在代码里写一堆 if。自带交互式文档。启动之后访问/docs就是 Swagger 页面所有接口可以直接在页面上测试前后端联调的时候特别方便。异步支持。虽然这个项目用不太上但后面要接大模型 API、做流式返回时异步就是刚需了。我不太建议凭感觉选框架。当时我分别让 AI 用 Flask 和 FastAPI 写了同一个“接收一条 JSON 并返回”的接口对比之后才发现 FastAPI 的请求体验明显更顺因为 Flask 需要自己解析request.get_json()而 FastAPI 直接在函数参数里就能拿到校验后的对象。这种对比花不了十分钟但能帮你省掉后面几小时的返工。关注点FlaskFastAPI请求参数校验手动写容易漏Pydantic 自动校验接口文档需另装 flasgger自动生成/docs异步支持一般原生支持适合规模小工具、模板页API 服务、前后端分离项目2.2 数据库先 SQLite别急着上 MySQL 或 Redis数据量级取决于这个 API 面向多少人用。一个个人博客的每日一句话接口一天可能几百次访问根本不需要上 MySQL。SQLite 单文件存储、零配置、Python 内置支持直接把数据库文件放到项目目录里就是全部运维。我知道很多人会有一种“生产环境必须上正经数据库”的执念但技术选型永远要匹配场景。SQLite 的瓶颈出现在高并发写入和多实例部署时个人项目短期内碰不到。真到了需要切换的时候把数据导出再换 PostgreSQL 也不难。先跑起来比一步到位更重要。2.3 本地环境虚拟环境和启动命令环境准备其实只有三条命令但每一步都不要跳mkdir quotes-api cd quotes-api python3 -m venv venv source venv/bin/activate pip install fastapi uvicorn[standard] pytest我特意没有用pip install sqlalchemy因为这种体量的项目直接用标准库sqlite3就够少一个 ORM 就少一层 AI 瞎编的空间。启动开发服务器也很简单uvicorn main:app --reload --port 8000加上--reload改代码后服务会自动重启。启动后浏览器打开http://127.0.0.1:8000/docs能看到接口文档说明骨架已经通了。2.4 选什么样的 AI 工具来做这个事我同时用了对话式大模型和 IDE 里的 AI 补全插件。补全插件适合在写代码时即时联想比如敲了def get_random_quote之后它自动补函数体对话式模型适合做整体设计、解释报错和写测试用例。选择时可以关注两点一是各家大模型 API 的免费额度和上下文长度小项目用免费额度完全够了二是注意上下文长度限制我不会一次性把整个项目代码都丢进去让 AI 优化很容易触发类似maximum context length的报错把问题拆细、按文件提问会更稳。3. 实战搭建把需求翻译成代码的关键过程3.1 第一版提示词我是这么写的同样让 AI 写代码不同的人写出来的质量差距可以非常大差距就在提示词。我给 AI 的提示词不是一句“帮我写个 API”而是一段结构化的需求说明你现在是一个 Python 后端工程师。请帮我创建一个 FastAPI 项目命名为 quotes-api。 需求 1. 使用 SQLite 存储句子字段包括 id、content、category、created_at。 2. 提供四个接口 - GET /api/quotes/random 随机返回一句支持可选参数 category 筛选 - POST /api/quotes 新增句子content 必填category 可选默认 default - GET /api/quotes 分页查询page 和 page_size 参数支持 category 筛选 - GET /api/health 返回服务状态。 3. 所有接口返回统一结构{code: 0, message: ok, data: ...}。 4. 请求参数要校验content 长度不超过 500category 不超过 50。 5. 使用标准库 sqlite3不要引入 SQLAlchemy。注意我加了第 5 条明确告诉它不要用什么。这一步非常重要因为 AI 默认倾向选择流行方案如果你不限制它就会给你铺一个 SQLAlchemy 的摊子而对这个项目来说完全不必要。提示词里写得越明确AI 生成的代码就越贴近你的需求。3.2 拿到骨架之后先别急着运行AI 给的第一版代码通常是一个目录结构大概长这样quotes-api/ ├── main.py # 主程序和路由 ├── database.py # 数据库连接和初始化 ├── requirements.txt # 依赖列表 └── test_quotes.py # 测试用例我拿到之后做的第一件事不是直接运行而是逐文件读一遍。原因很简单AI 生成的代码可能存在它“以为存在”的方法直接运行只会看到一个接一个的报错还不如先确认结构。尤其是requirements.txtAI 给出的依赖往往是网上教程里的老版本我会在安装完后立刻执行一次pip freeze requirements.txt把实际版本固化下来避免过两天完全想不起来环境里装的什么。3.3 核心代码长什么样数据库与路由经过我手动修改后的database.py长这样核心是开启 WAL 模式这个后面讲并发坑的时候会再说import sqlite3 DATABASE quotes.db def get_db(): conn sqlite3.connect(DATABASE) conn.row_factory sqlite3.Row conn.execute(PRAGMA journal_modeWAL;) conn.execute(PRAGMA busy_timeout3000;) return conn def init_db(): with get_db() as db: db.execute( CREATE TABLE IF NOT EXISTS quotes ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, category TEXT DEFAULT default, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) db.execute( CREATE INDEX IF NOT EXISTS idx_quotes_category ON quotes(category) )main.py里的核心逻辑同样不复杂但这部分是 AI 生成后我手动改过最多的import random from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field from database import get_db, init_db app FastAPI(titleQuotes API, version0.1.0) class QuoteIn(BaseModel): content: str Field(..., min_length1, max_length500) category: str Field(default, max_length50) app.on_event(startup) def on_startup(): init_db() app.get(/api/health) def health(): return {code: 0, message: ok, data: {status: up}} app.get(/api/quotes/random) def random_quote(category: str | None None): db get_db() if category: row db.execute( SELECT * FROM quotes WHERE category? ORDER BY RANDOM() LIMIT 1, (category,), ).fetchone() else: row db.execute( SELECT * FROM quotes ORDER BY RANDOM() LIMIT 1 ).fetchone() if row is None: raise HTTPException(status_code404, detailNo quote found) return {code: 0, message: ok, data: dict(row)} app.post(/api/quotes) def create_quote(quote: QuoteIn): db get_db() cur db.execute( INSERT INTO quotes (content, category) VALUES (?, ?), (quote.content, quote.category), ) db.commit() return {code: 0, message: ok, data: {id: cur.lastrowid}} app.get(/api/quotes) def list_quotes(category: str | None None, page: int 1, page_size: int 10): db get_db() where WHERE category? if category else params (category,) if category else () total db.execute( fSELECT COUNT(*) FROM quotes {where}, params ).fetchone()[0] rows db.execute( fSELECT * FROM quotes {where} ORDER BY id DESC LIMIT ? OFFSET ?, params (page_size, (page - 1) * page_size), ).fetchall() return { code: 0, message: ok, data: {items: [dict(row) for row in rows], total: total}, }我不建议让 AI 一次生成一个几百行的单文件把所有逻辑塞进去因为它很容易漏掉错误处理而且出现问题后定位困难。拆成database.py和main.py后续还能继续加auth.py、middleware.py可维护性会好很多。3.4 用 pytest 挡住回归再用 curl 做冒烟验证代码写完先别急着部署至少让 AI 写一份基础测试。我会直接丢给它一段话“根据 main.py 生成 pytest 测试覆盖空表、新增成功、按分类筛选、分页边界、content 超长时返回 422。”AI 生成的测试代码大致能覆盖这些场景但我自己会手动补两个最容易被 AI 忽略的 case一是空表时随机接口应该返回 404二是类别不存在时列表接口应该返回空列表而不是报 500。curl -X POST http://127.0.0.1:8000/api/quotes \ -H Content-Type: application/json \ -d {content: 学而时习之不亦说乎, category: 论语} curl http://127.0.0.1:8000/api/quotes/random跑完这几条 curl整个项目的核心闭环就算通了。这也是我后面所有迭代都依赖的基线改动任何代码先跑 pytest再跑 curl。4. AI 代码不是拿来就能跑我实测里遇到的几个真坑4.1 Content-Type 没配对接口直接 422第一个坑发生在刚写完 POST 接口的时候。我用/docs页面测一切正常但换到 curl 就返回 422 Unprocessable Entity。排查过程是这样的先看 FastAPI 返回的校验错误信息提示 content 字段缺失再用curl -v检查实际发送的请求头发现我原以为的 JSON 发送其实Content-Type默认是application/x-www-form-urlencodedPydantic 根本没把请求体解析成 JSON。修复很简单在 curl 里加一个 Headercurl -X POST http://127.0.0.1:8000/api/quotes \ -H Content-Type: application/json \ -d {content: 三人行必有我师, category: 论语}这个坑本身不算难但它提醒我AI 生成的代码默认是正确的真正出错的往往是调用端没有遵循接口约定。以后凡是遇到 422先检查请求头再检查参数名不要一上来就怀疑服务端代码。4.2 SQLite 并发写database is locked第二个坑是上线后才暴露的。一次压测里连续发了多个 POST 请求服务直接报sqlite3.OperationalError: database is locked。原因很简单SQLite 默认在同一时刻只允许一个连接写数据库多个写入请求同时到达时后到的会拿不到写锁。AI 生成的代码没有考虑到并发场景因为它只写了最基础的connect和commit。解决办法是在连接时开启 WAL 模式和忙碌超时这两行就是我上面database.py里加的那两个PRAGMA。用生活化的方式理解WAL 模式相当于让所有人都先在便利贴上写字再统一整理进笔记本而不是每个人拿同一支笔抢着写busy_timeout则是告诉后来的人“等一下前面的人马上写完”。这两个设置对个人项目的并发量完全够用。4.3 AI 会一本正经地编造不存在的方法这是我觉得最值得警惕的坑。有一次让 AI 生成一个 ORM 版本的数据更新逻辑它给出了一个db.quote_record.update_to_db()之类的方法运行之后直接AttributeError。问题是 AI 在生成这段代码时完全不加犹豫看起来就像真有一个官方方法存在一样初学者很容易被这种自信误导。我的应对方法是凡是 AI 代码里出现我不认识的方法或属性先让它解释“这个方法是哪个库提供的官方文档在哪”再让它给出一个不依赖该方法的等价写法。多问两次你会发现有些方法真的是查无此物有些是在旧版本里存在但已经废弃了。想避免这类问题一个可靠的兜底就是跑 pytest能过测试且业务逻辑正确就说明这个方法至少在当前环境下是可用的。4.4 不设输入限制上线几小时就被人刷写第四个坑比较疼。我把 POST 接口部署到公网后几个小时内数据库里多了一堆乱码和广告内容。原因就是我最初图省事没加访问频率控制任何人都可以不限次数地往库里写数据。后来加了两层防护应用层给每个新增接口做了基于 IP 的简单限流比如每个 IP 每分钟最多写 5 次同时把 content 字段的长度校验明确写在 Pydantic 模型里。再后来干脆不在公网开放 POST 接口了只有带管理 token 的请求才能写入查询接口保持公开即可。这个经验教训是只要接口暴露在公网就会被机器人扫描输入校验和限流不是可选项是必选项。4.5 AI 工具自己也会出问题API Key 没配、上下文超限AI 工具本身也不是始终可靠的。我在项目中途想让 AI 对整个 main.py 做一次重构结果直接返回了上下文超长的报错提示类似maximum context length。这是因为我把整个项目文件都粘贴进去了超出了模型单次能处理的最大 token 数。解决方式很简单按文件拆分提问或者先让 AI 总结当前代码结构再针对模块逐段优化。同样常见的是 API Key 问题如果 AI 编程工具依赖的模型服务没有正确配置密钥就会像我开头说的那样所有对话请求直接失败。所以用 AI 辅助开发时别只盯着写代码得先把工具本身的认证、配额、报错信息看明白。4.6 靠测试兜底让 AI 生成 pytest但别盲信说了这么多坑最重要的兜底其实只有一个测试。每让 AI 改完一版代码我都会执行一遍pytest -q如果测试挂了就把失败日志原封不动丢给 AI让它解释并修复。这会形成一个很高效的循环pytest -q测试用例跑得越全AI 后面越不敢乱来。我实测下来只要测试里覆盖了边界情况、非法输入和空数据场景AI 生成的代码整体稳定性会提升一大截。不要嫌写测试麻烦它是你唯一能低成本确认 AI 代码没在骗你的手段。5. 从本机到公网部署上线与基础防护5.1 用 systemd 把服务托管起来开发环境用uvicorn main:app --reload没问题但生产环境不能这样跑因为终端一关服务就没了。我用 systemd 写了一个服务单元让 API 作为系统服务常驻运行[Unit] DescriptionQuotes API Afternetwork.target [Service] Userwww-data WorkingDirectory/srv/quotes-api ExecStart/srv/quotes-api/venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000 Restartalways [Install] WantedBymulti-user.target注意这里--host 127.0.0.1意思是服务只在本机监听不直接暴露到公网后面由 Nginx 统一接收外部流量。保存后执行systemctl enable --now quotes-api服务会在开机时自启并且进程崩溃后自动重启。5.2 Nginx 反向代理 HTTPS用 Nginx 的好处是能统一处理 HTTPS、限流、请求头和安全策略。我的 Nginx 配置很简单server { listen 80; server_name api.example.com; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }HTTPS 直接用 certbot 申请免费证书一条命令就能把证书和自动续期都配置好certbot --nginx -d api.example.com这里不用自己手动折腾证书文件certbot 会修改 Nginx 配置并把 HTTP 流量重定向到 HTTPS。上线后我用curl https://api.example.com/api/health验证了一次确认公网入口完全正常。5.3 至少要做这层防护限流、身份识别、日志给公网 API 做防护我按优先级做了三件事Nginx 层限流对/api/路径做了每秒 10 次的整体限流超出直接返回 503。这能挡住大部分粗暴刷接口的机器人。写接口加 TokenPOST 接口现在必须带Authorization: Bearer TOKEN才能调用查询接口保持公开。Token 通过环境变量注入不写进代码库。日志输出到标准输出不把日志写进文件直接用 systemd 的 journal 收集这样所有日志都在journalctl里排查问题时不用登录服务器去翻文件。5.4 上线后第一件事看日志而不是看页面上线后的第一次检查我会先看日志而不是先调接口。因为接口只验证“服务活着”日志才能告诉你“服务有没有在被乱搞”。journalctl -u quotes-api -f我第一次上线后的日志里全是 404 扫描记录各种随机路径的探测请求说明服务一暴露到公网就会被扫描器盯上。这也是为什么我坚持不在公网开放写接口、必须加限流的原因。日志里如果看到某个 IP 频繁访问直接在 Nginx 层把它禁掉即可不用改动应用代码。6. 如果再让我重做一遍我会提前改掉的三件事6.1 先定统一错误码和返回结构再开始写路由这个项目第一版接口返回结构是完全一致的但错误返回却被我写得比较随意有的接口直接返回 HTTP 422 的 FastAPI 默认结构有的返回 404{detail:...}前端对接时要针对不同接口写两套解析逻辑。后面我统一把返回格式固定为{code: 0, message: ok, data: ...}业务错误也用同一个结构返回。这个教训是返回结构属于接口契约写代码之前就应该定好而不是写完再补。6.2 数据库结构从第一天就交给迁移工具一开始我在init_db()里用CREATE TABLE IF NOT EXISTS建表这种方式对单表项目足够但一旦你想加字段、加索引就会面临“线上库已经建好、测试库还是新结构”的混乱。后来我引入了 Alembic 做数据库迁移每改一次表结构都会生成一个迁移文件团队成员在不同环境下执行同一套迁移数据库结构就不会跑偏。对于个人项目可能不算必须但我建议一旦表结构要变动第二次就值得上一套迁移工具了。6.3 配置和密钥集中管理别写死在代码里上线之后改配置会非常痛苦。最初我把数据库路径、Token、允许的跨域来源都直接写在了main.py里后来要改其中一个就得重新部署一次。正确的做法是用环境变量或.env文件统一管理from pydantic_settings import BaseSettings class Settings(BaseSettings): api_token: str database_path: str quotes.db cors_origins: list[str] [*] class Config: env_file .env settings Settings()这个改动带来的直接好处是改 Token、切数据库路径都只需要改环境变量不用碰代码而且密钥不会因为代码被复制而泄露。如果你也在用 AI 辅助开发有一点需要特别留意AI 默认生成代码时不会主动考虑密钥管理它会很自然地把密钥作为常量写进代码里这正是需要你人工把关的地方。最后再分享一个我个人的固定习惯每次让 AI 生成完代码我不会立刻信任它而是先看一眼依赖和函数名再跑一遍 pytest最后用 curl 打几个“脏数据”——超长字符串、空字符串、不存在的 ID哪个打崩了就让 AI 解释原因。这套流程帮我挡掉了绝大多数上线后才会发现的坑。这个项目做完之后我又用同样的方法搭过两个内部小工具流程基本固定为三步拆需求、把需求变成提示词、用测试验收 AI 的输出。如果你也想试着用 AI 从零搭自己的 API建议从今天这个小项目开始把它当成你带的第一个实习生来用。测试通过的那一刻你会比直接让它给你一个“完整项目”更有成就感。