FastAPI入门到实战:五个Demo从零搭建接口并接入DeepSeek大模型
1. 从一条路由接到 DeepSeek为什么我只写五个 Demo如果你和我一样第一次打开 FastAPI 的官方文档时被里面异步、Pydantic、依赖注入、WebSocket 这些概念砸得有点懵那么这篇文章就是写给你的。我的入门路线很简单不要一上来就啃完整个框架而是老老实实写 5 个 demo从一条最基础的路由开始一路接到 DeepSeek 大模型接口。走完这条线你会发现自己已经能独立搭出一个带参数校验、带数据库、能调用大模型接口的真实服务。这套路线图不是给已经用过 Flask 或 Spring 的人看的而是给真正“从零”开始的人。五个 demo 分别是第一个是只返回一句话的最小接口第二个是路径参数、查询参数和第一个 POST 请求第三个是用 Query、Path 和 Pydantic 把输入边界立起来第四个是把内存数据换成 SQLite 并理解同步与异步的混用第五个是封装一个 /chat 接口接到 DeepSeek。每一步都建立在前一步之上不会突然冒出一个你完全没见过的新概念。我见过很多新手卡在同一个地方文档读了不少但真正动手时不知道先写哪行代码。原因很简单FastAPI 的“所有功能都很强”恰恰是学习时最大的干扰项。你不需要一开始就理解依赖注入能解耦什么也不需要背出所有中间件用法你只需要先看到自己的代码能跑起来、能在浏览器里打开一个 JSON。这个“最小闭环”一旦建立后续所有功能都只是在同一个闭环上做加法。1.1 第一个 Demo安装、启动、拿到第一段 JSON先建一个空目录里面只放一个 main.py内容少到不能再少from fastapi import FastAPI app FastAPI() app.get(/) def read_root(): return {message: hello fastapi}然后安装两个依赖pip install fastapi uvicorn启动命令也只有一行uvicorn main:app --reload打开浏览器访问 http://127.0.0.1:8000你会看到{message:hello fastapi}。看到这段 JSON你的第一个 FastAPI 服务就跑通了。--reload是开发模式下的热重载每次改代码保存后服务会自动重启不用手动关掉再起。这里有个很多人会问的问题为什么我用的是def而不是async def我的回答是在这个阶段两者没有任何区别。FastAPI 允许你混用同步和异步函数普通def路由会被自动放到线程池里执行不会阻塞事件循环。先记住这个结论第四个 Demo 里我会专门展开。1.2 自动文档页面新手最容易忽略的免费调试台跑通第一个路由后你至少应该花十分钟做一件事打开 http://127.0.0.1:8000/docs。你会看到一个 Swagger UI 页面里面自动列出了你刚才写的接口还能直接点击“Try it out”发送请求、查看响应。这是 FastAPI 最让我惊喜的地方你不需要额外写一行文档代码它完全根据函数签名和类型注解生成 OpenAPI schema。除了/docs/redoc还会生成另一种风格的 API 文档。很多新手觉得这两个页面只是“好看”但实际上它们是开发阶段最高效的调试台。你不需要打开 Postman 或 curl直接在页面上点按钮就能测试 GET、POST、传参数、看返回结构。第一个 Demo 做完你应该已经体会到 FastAPI 和很多传统 Web 框架的本质区别类型注解不只是给人看的而是真正驱动请求解析、数据校验、接口文档三件事。2. 参数、请求体与校验第二个和第三个 Demo 解决的核心问题把第一个 Demo 跑通之后接下来的重点从“能返回 JSON”变成“能接收各种输入”。这里最核心的认知转变是FastAPI 不是一个简单的路由分发器它在拿到请求的那一刻就已经根据你的类型注解完成了大量工作。输入越规范接口越健壮。2.1 第二个 Demo路径参数、查询参数和第一个 POST 接口先说路径参数。假设你要通过/items/1这种 URL 访问某个商品代码可以写成app.get(/items/{item_id}) def get_item(item_id: int): return {item_id: item_id}注意这里我把item_id标注成了int。如果不小心把 URL 写成/items/abcFastAPI 会直接返回 422 错误而不是把字符串abc原样传进函数。这是很多新手第一次感受到 FastAPI 强硬的地方类型不对请求根本进不到函数体里。对比一下如果你在 Flask 里用request.view_args拿到的值往往要先自己手动int()再处理异常FastAPI 把这个环节直接内置了。接下来是查询参数。最常见的场景是列表接口带搜索关键词或分页参数fake_db {} app.get(/items/{item_id}) def get_item(item_id: int, q: str | None None): if item_id not in fake_db: return {error: not found} item fake_db[item_id] if q: item[matched_q] q return item这里的q: str | None None表示q是可选参数不传也能访问。FastAPI 会自动把它识别成 URL 里的?qxxx查询参数。如果你把默认值去掉q就变成必填查询参数不传直接 422。这个设计很有意思一个默认值就决定了参数的“可选与必填”语义。接着我们加一个 POST 接口用来创建商品。这是整个入门路线里第一次出现请求体也是 Pydantic 正式登场的地方from pydantic import BaseModel class Item(BaseModel): name: str price: float is_offer: bool False app.post(/items/, status_code201) def create_item(item: Item): item_id len(fake_db) 1 fake_db[item_id] item.model_dump() return {id: item_id, **item.model_dump()}请求体就是一个 JSONFastAPI 会把它解析成Item这个 Pydantic 模型对象。price: float这个注解意味着如果客户端传price: abc请求在进入函数前就会被拒绝返回一个非常清晰的 422 错误说明哪个字段、什么类型、为什么失败。这就是 FastAPI 的核心体验你定义的“数据形状”就是接口真正执行的“数据契约”。2.2 第三个 Demo用 Query 和 Path 给接口加上规则边界第二个 Demo 已经能接收路径参数、查询参数和 JSON 请求体但仅仅“能接收”还不够很多时候我们需要更细的约束。比如商品 ID 必须是正整数搜索关键词最长不能超过 50 个字符分页条数不能超过 100。FastAPI 提供了Query和Path两个工具给参数声明附加规则from fastapi import FastAPI, Query, Path, HTTPException app.get(/items/{item_id}) def get_item( item_id: int Path(..., ge1), q: str | None Query(None, max_length50), ): if item_id not in fake_db: raise HTTPException(status_code404, detailItem not found) return fake_db[item_id]Path(..., ge1)表示这个路径参数必填且必须大于等于 1。...是 Python 的 Ellipsis在 FastAPI 里专门用来表示“必填但没有默认值”。Query(None, max_length50)表示如果传了q长度不能超过 50如果不传就默认是None。注意不要小看这些规则它们不只是“防御性编程”更是在接口还没执行任何业务逻辑之前就把非法输入挡在门外。这里我需要强调一个新手容易混淆的地方参数校验失败和业务找不到数据是完全两回事。路径参数不合法返回 422让客户端知道“你请求的格式不对”而 ID 合法但数据库里没有返回 404表示“请求本身没问题但资源不存在”。这两种错误如果混在一起前端和调用方都会非常难做判断。2.3 FastAPI 为什么默认返回 422一条必须理解的规则很多从 Flask 转过来的开发者第一次看到 422 会愣一下因为这个状态码不常出现在传统 Web 开发里。FastAPI 遵循的是 OpenAPI 规范校验失败统一返回 422 Unprocessable Entity。响应体结构大致长这样{ detail: [ { type: missing, loc: [body, price], msg: Field required, input: {} } ] }这个结构非常关键。loc告诉你是路径参数、查询参数还是请求体里的哪个字段出了问题msg是具体原因input是这次传入的非法值。我在实际联调时前端同事看到 422 直接就能定位到自己的请求漏了哪个字段根本不需要来问我。所以第三个 Demo 给你的经验是不要随便把所有错误都返回 400。系统层面的输入校验交给 FastAPI 默认的 422业务层面的资源不存在用 404权限不足用 403服务器内部错误用 500。规则一旦统一接口的排错成本会直线下降。3. 第四个 Demo持久化到 SQLite顺便把异步机制用明白第四个 Demo 开始我们的服务不再只是“能跑的玩具”而是真正有数据落地的应用。这一步的关键不是 SQL而是理解 FastAPI 的线程池机制。很多教程只教你怎么写async def却不解释什么时候该写、什么时候不该写结果新手遇到同步数据库操作放进异步函数里反而把性能写崩了。3.1 为什么选 SQLite零配置、单文件、够用学习阶段没必要直接上 PostgreSQL 或 MySQL。SQLite 最大的优势是零配置Python 内置sqlite3模块不用装数据库服务不用维护连接池一个.db文件就搞定了。对 Demo 来说这能让注意力集中在 FastAPI 本身。先定义初始化逻辑。FastAPI 现在推荐用 lifespan 来代替旧版的app.on_event(startup)from contextlib import asynccontextmanager import sqlite3 from fastapi import FastAPI def init_db(): conn sqlite3.connect(demo.db) conn.execute( CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, price REAL NOT NULL ) ) conn.close() asynccontextmanager async def lifespan(app: FastAPI): init_db() yield app FastAPI(lifespanlifespan)yield之前的部分是启动时执行yield之后的部分是关闭时执行。如果你有数据库连接资源需要释放写在yield后面即可。为什么新版推荐 lifespan因为on_event是 FastAPI 早期设计职责比较单一而 lifespan 是一个标准异步上下文管理器语义清晰也能更好地和其他框架集成。3.2 同步 sqlite3 和异步接口如何共存FastAPI 的线程池机制很多新手看到 FastAPI 支持异步就把所有接口都写成async def然后在里面调用同步的sqlite3操作。这其实是个误区。同步sqlite3操作本身是阻塞的如果把它放在async def函数里它会直接卡住事件循环导致整个服务的其他请求都跟着变慢。正确做法是调用同步阻塞库时就用普通def写接口。FastAPI 会自动把普通函数丢进线程池执行从而避免阻塞事件循环。代码如下app.post(/items/) def create_item(item: Item): conn sqlite3.connect(demo.db) try: cur conn.execute( INSERT INTO items(name, price) VALUES(?, ?), (item.name, item.price), ) conn.commit() item_id cur.lastrowid finally: conn.close() return {id: item_id, name: item.name, price: item.price}同样的道理如果需要用到asyncio.sleep、httpx.AsyncClient这类真正的异步操作就应该写在async def里。为了直观感受两者的差别你可以写一个对比实验import time import asyncio app.get(/fast) def fast(): return {msg: fast} app.get(/slow-async) async def slow_async(): await asyncio.sleep(3) return {msg: done} app.get(/slow-sync) def slow_sync(): time.sleep(3) return {msg: done}同时请求/fast和/slow-async你会看到/fast马上返回但如果请求/slow-sync的同时请求/fast/fast会明显变慢。这个实验做完你就真正理解了“异步不是让单次请求变快而是让服务在等待 I/O 时不阻塞其他请求”。3.3 用一条中间件把 Demo 变得更像真实服务第四个 Demo 还适合顺手加一个东西HTTP 中间件。我通常会在自己的小项目里加一个记录处理时间的中间件用X-Process-Time响应头暴露出来方便排查慢接口。app FastAPI(lifespanlifespan) app.middleware(http) async def add_process_time_header(request, call_next): start time.time() response await call_next(request) response.headers[X-Process-Time] f{time.time() - start:.4f} return response中间件可以理解为每个请求必经的一层“关卡”。它先记录开始时间调用call_next让请求继续走路由逻辑拿到响应后再附加一个响应头。你还可以在这里做请求日志、简单的 IP 访问统计、全局异常兜底等。这个 Demo 之后你的服务已经具备持久化、异步处理、中间件三个能力离真实项目只差最后一块拼图接入外部 API。4. 第五个 Demo把 FastAPI 接到 DeepSeek代码比你想象中短接下来就是很多人最期待的部分让 FastAPI 调用 DeepSeek。其实这一节要讲的核心并不是“AI 原理”而是如何把一个外部 HTTP API 优雅地封装成自己的接口。只要理解了请求参数、响应解析、错误处理这三个环节接任何大模型 API 都是同一套思路。4.1 准备工作API Key、依赖包和调用方式首先去 DeepSeek 开放平台注册账号创建 API Key。拿到一串以sk-开头的密钥后把它放到环境变量里不要硬编码在代码中export DEEPSEEK_API_KEYsk-你的密钥然后安装 openai 这个 Python 包。DeepSeek 的接口兼容 OpenAI 的请求格式所以可以直接用openaiSDK只要把base_url指向 DeepSeek 的接口地址就行。这里我用的是 DeepSeek 官方 API 的域名调用模型名一般为deepseek-chat。from openai import OpenAI client OpenAI( api_keyos.getenv(DEEPSEEK_API_KEY), base_urlhttps://api.deepseek.com, ) resp client.chat.completions.create( modeldeepseek-chat, messages[ {role: user, content: 用一句话介绍 FastAPI} ], ) print(resp.choices[0].message.content)这段代码单独跑起来就能在命令行里看到 DeepSeek 返回的文本。注意messages是一个列表里面的每条消息都带role可以是system、user、assistant。system消息用于设定角色比如“你是一个严谨的技术助手”如果不需要留空即可。4.2 封装成 FastAPI 的 /chat 接口现在把上面的调用逻辑搬进 FastAPI。我会定义两个 Pydantic 模型ChatRequest控制客户端传什么ChatResponse控制服务端返回什么。这样客户端看到文档就能立刻知道接口契约。import os from openai import OpenAI from fastapi import FastAPI, HTTPException from pydantic import BaseModel app FastAPI() class ChatRequest(BaseModel): prompt: str system: str | None None temperature: float 0.7 max_tokens: int 1024 class ChatResponse(BaseModel): reply: str model: str usage: dict app.post(/chat, response_modelChatResponse) def chat(req: ChatRequest): api_key os.getenv(DEEPSEEK_API_KEY) if not api_key: raise HTTPException(status_code500, detailDEEPSEEK_API_KEY not set) client OpenAI(api_keyapi_key, base_urlhttps://api.deepseek.com) messages [] if req.system: messages.append({role: system, content: req.system}) messages.append({role: user, content: req.prompt}) try: resp client.chat.completions.create( modeldeepseek-chat, messagesmessages, temperaturereq.temperature, max_tokensreq.max_tokens, ) except Exception as e: raise HTTPException(status_code502, detailfDeepSeek API error: {e}) return ChatResponse( replyresp.choices[0].message.content, modelresp.model, usageresp.usage.model_dump() if resp.usage else {}, )这是一个非常典型的“外部 API 封装”写法入口接收自己的业务参数出口返回自己的业务结构中间把第三方调用包在 try-except 里转成统一的 HTTP 错误。为什么这里用普通def而不是async def因为openaiSDK 默认的同步调用是网络阻塞操作放在普通函数里由 FastAPI 线程池处理更简单如果你希望更高的并发可以换用AsyncOpenAI原理也是一样的。4.3 实测一下再加一点真实项目才用得到的考量启动服务后用 curl 调一下curl -X POST http://127.0.0.1:8000/chat \ -H Content-Type: application/json \ -d {prompt: 用一句话介绍 FastAPI, system: 你是一个技术作家}正常返回的 JSON 结构大概是这样{ reply: FastAPI 是一个基于 Python 类型注解的现代 Web 框架强调高性能与自动生成 API 文档。, model: deepseek-chat, usage: { prompt_tokens: 20, completion_tokens: 30, total_tokens: 50 } }到这一步你已经把一条最基础的路由扩展成了一个能调用大模型的后端接口。接下来如果要继续往生产靠至少还有三个点值得做第一超时控制。大模型接口响应不稳定最好设置一个超时时间避免客户端长时间挂着。第二上下文管理。现在客户端每次只传一个prompt但真实聊天需要把历史消息传进来让模型有上下文。第三流式输出。大模型逐字生成时用StreamingResponse可以把内容边生成边返回用户体验会好很多。这些都不是 FastAPI 特有的而是所有大模型 API 集成都要面对的通用问题。5. 跑完五个 Demo 后我最想提醒你的几件事到这里五个 Demo 全部走完了。代码层面该有的基础都有但我更想分享的是实践中的一些体会。这些内容不会出现在大多数教程的“快速入门”部分却能帮你少踩不少坑。5.1 先想清楚输入输出边界再写业务代码我第一次写 FastAPI 接口时习惯先写业务逻辑再回头补参数校验和响应模型结果经常返工。后来我改成“契约先行”先写函数签名再定义BaseModel最后才写函数体。这样做的好处是你会在写业务逻辑之前就把所有非法输入拦截方案想清楚后续调试成本会低非常多。对一个成熟接口来说“接口长什么样”往往比“内部怎么实现”更值得先设计。5.2 同步与异步混用不是玄学是性能分配的常识FastAPI 的线程池机制很好用但它不是万能的。线程池有默认大小上限如果所有接口都是同步阻塞操作流量一大线程池就被占满整个服务响应都会变慢。我自己的原则是能异步就异步但只有await的时候才写async def如果函数里没有真正的异步 I/O写了async def反而可能因为没有在事件循环里让出而拖慢服务。最简单的判断标准是遇到网络请求、磁盘读写、数据库查询优先考虑异步方案遇到纯 CPU 计算用普通def就行。5.3 自动文档的隐形价值团队协作的“唯一真相”很多人只把/docs当作一个“能点击调用的页面”但在我实际协作中发现它更像团队之间的接口契约。前端同事可以直接读文档确认字段类型测试同事可以直接在文档里造请求新同事接手项目时可以从文档反推系统全貌。尤其是当接口比较多以后文档里展示的response_model结构比口头沟通要准确得多。所以每写一个接口我都会认真写类型注解和BaseModel因为这不是给 Python 解释器看的是给未来所有使用这个接口的人看的。5.4 密钥管理和错误处理demo 转生产最重要的三级跳第五个 Demo 里API Key 是从环境变量读的这已经比硬编码安全很多。但真实项目里还要考虑密钥权限隔离、调用频率限制、多模型负载均衡、失败重试策略。错误处理也一样demo 阶段返回一个 502 足够但生产环境最好能把error_type、request_id、trace_id这类信息全部结构化返回方便排查问题。很多人学到这里会只关注“模型回复了什么”忽略了接口稳定性而稳定性才是一个大模型后端服务能不能扛住真实流量的关键。5.5 最后分享一个小技巧给所有外部 API 调用加日志在接入 DeepSeek 的接口里我强烈建议你至少在调试阶段打一条日志记录请求的 prompt、模型、耗时、token 用量和最终状态。大模型的输出不稳定有时用户反馈“答案不对劲”如果没有日志你根本无法复现场景。我在自己的 demo 里会用 Python 的logging简单记录logger.info( chat request | prompt%s | model%s | tokens%s | cost%.2fs, req.prompt[:50], resp.model, resp.usage.total_tokens if resp.usage else 0, time.time() - start, )这个小习惯能让你在后续调优时省下大量时间。五个 Demo 走完你已经有了一条从“返回 hello world”到“调用大模型”的完整路径。接下来无论你是想加深理解、加功能还是往生产方向打磨这条路径都已经立起来了。按这个节奏继续写就好。