第5章 构建 API 服务:用 FastAPI 封装 ChatBot 为接口
目标概述
在本章,我们将把上一章的 CLI ChatBot 逻辑封装成一个 HTTP API 服务。这样做的意义是,其他程序(例如网页前端、手机App)都可以通过网络请求调用我们的ChatBot,获取回答。这也是现代软件的常见架构:后端提供API接口,前端通过接口获取数据。我们将使用流行的 Python Web 框架 FastAPI 来快速构建此服务,并学习基本的 Web API 开发知识,如定义请求路径、处理请求和响应 JSON 数据格式、测试接口等。完成本章后,你将能够运行一个本地 Web 服务,提供 ChatBot 的问答接口(比如 GET /chat?question=你好 返回回答),为下一章前端集成打基础。
主要知识点
-
API 与 Web 服务:API(应用程序编程接口)指的是供程序调用的接口,本章重点是RESTful风格HTTP API,客户端通过HTTP请求特定URL获取数据。我们的ChatBot API将设计为一个HTTP接口(比如 /ask),客户端发送用户问题,服务端返回AI回答,以JSON格式承载。[61][62]
-
FastAPI 框架:一个用于快速构建API的现代框架,基于Python 3的类型提示,性能高效且易用[63]。其特点包括:简单的路由定义、自动生成交互式文档界面[64]、内置数据验证等。我们将学习使用 FastAPI 定义一个路径操作(route)并指定HTTP方法(GET/POST)、请求参数和返回值。
-
路由与请求方法:在 FastAPI 中,使用装饰器如 @app.get("/") 定义GET请求的处理函数,@app.post("/chat") 定义POST处理[63]。GET 通常用于获取数据,我们可用于实现 /chat 接受查询参数问答;POST 一般用于提交数据,如果问题文本较长或希望使用JSON请求,可以POST。我们需要决定接口规范并实现对应方法。
-
请求参数和验证:FastAPI能自动解析查询参数、请求体等。例如定义函数参数 question: str 则 /chat?question=xxx 的参数会自动解析为字符串传入函数[63]。我们也可以使用 Pydantic 模型定义请求体数据结构,框架会自动校验字段类型并提供错误响应[65]。出于简单,本章我们以查询参数方式获取问题文本。
-
返回响应:FastAPI 可以直接返回Python字典或 Pydantic 模型对象,框架会自动转换为 JSON 响应。[66] 例如我们返回 {"answer": "...文本..."},客户端就能得到 JSON 数据。了解JSON(JavaScript Object Notation)是常用的轻量数据格式,键值对形式,本例中非常适合传输问答内容。
-
CORS:如果前端网页要调用后端API,涉及跨域资源共享(CORS)问题。浏览器默认出于安全限制,不允许从一个域名的网页脚本调用另一个域名的API,除非后端明确允许。[67]我们本地开发时,前端可能从 file:// 或 localhost:3000 调用后端 localhost:8000,属跨域。我们需要在FastAPI中启用 CORS 中间件,允许我们的前端来源以避免被浏览器拦截[68]。
-
运行与测试:学习使用 Uvicorn 或自带命令运行FastAPI服务,测试API是否正常。FastAPI自带交互式文档UI(Swagger),在浏览器打开 http://127.0.0.1:8000/docs 就能可视化地尝试调用API[64]。掌握这个调试利器。在正式环境下,也了解基础的部署思路,如使用Gunicorn+Uvicorn Workers或ASGI服务器。
完整代码
新建文件 api_service.py:
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
import os, openai
# 与之前类似,配置 OpenAI API 密钥
openai.api_key = os.getenv("OPENAI_API_KEY")
if openai.api_key is None:
raise RuntimeError("需要设置 OPENAI_API_KEY 环境变量")
# 初始化 FastAPI 应用
app = FastAPI(title="ChatBot API", description="一个简单的ChatBot问答接口", version="1.0")
# 配置 CORS 中间件,允许来自所有来源的请求(开发阶段开放,生产可收紧)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 允许任意源(在生产环境应指定域名)
allow_methods=["*"], # 允许所有HTTP方法
allow_headers=["*"] # 允许所有请求头
)
# 定义帮助函数:获取回答(可直接用前一章的get_answer逻辑)
def get_answer_from_openai(question: str) -> str:
try:
resp = openai.Completion.create(
engine="text-davinci-003",
prompt=question,
max_tokens=1024,
temperature=0.7,
n=1,
stop=None
)
answer = resp.choices[0].text.strip()
return answer
except Exception as e:
# 打印错误日志,抛出 HTTP 异常
print("OpenAI API 调用失败:", e)
# 返回HTTP 500错误响应
raise HTTPException(status_code=500, detail="调用AI服务出错")
# 定义 API 路由:GET 方法,请求参数 question
@app.get("/chat")
def chat(question: str):
"""
接收用户问题question,返回回答answer。
"""
# 基础校验:问题不能为空
q = question.strip()
if q == "":
raise HTTPException(status_code=400, detail="问题内容不能为空")
answer = get_answer_from_openai(q)
# 返回JSON
return {"question": question, "answer": answer}
代码讲解:
- 创建 FastAPI 应用实例时,可以传入 title、description 等元数据,用于自动文档显示[69]。我们简单注明服务名称和版本。
- CORS 设置:使用 app.add_middleware 添加 CORSMiddleware[67]。这里我们暂时允许所有来源 allow_origins=[""],这是开发阶段为方便前端调试而采取的开放策略。在生产中,最好限定 allow_origins 只包含你的前端应用的域名,例如 ["https://myapp.com"]。allow_methods 和 allow_headers 也设为 "" 以允许常见请求。配置完CORS,中间件会自动为响应加上 Access-Control-Allow-Origin 等头,前端浏览器就能放行跨域请求了[68]。
- get_answer_from_openai 函数:和 CLI 版本类似,调用 OpenAI API 获取回答[66]。区别是我们在这里把异常转换成 HTTPException 抛出[70]。HTTPException 是FastAPI提供的,用于返回 HTTP 错误响应而非简单崩溃。比如若 OpenAI 调用失败,我们返回状态码500(内部服务器错误),detail消息里给出简短说明。FastAPI接收到这个异常会自动生成适当的JSON错误响应,例如:{"detail": "调用AI服务出错"},HTTP状态500。这样API使用者就能明确请求失败及原因。
- 路由定义:使用 @app.get("/chat") 装饰器[63]将 chat() 函数映射到 GET /chat URL。当有 GET 请求到 /chat 时,FastAPI会调用此函数。函数参数 question: str 表示这个接口需要一个名为 question 的字符串参数。FastAPI会自动从查询参数(URL中的?question=xxx)读取到并传给我们。如果未提供 question 参数,FastAPI会返回400错误并提示缺少参数。
- 逻辑处理:进入 chat() 函数后,我们做了一个简单校验:如果 question 去掉空白后为空,则抛出 400 错误[65](HTTP 400 Bad Request)。否则调用前面的 get_answer_from_openai 来获取答案。如果 OpenAI 那边报错,在 get_answer_from_openai 里已经抛 HTTPException 500,这里不用再处理,它会冒泡给FastAPI处理。
- 返回结果:最后我们返回一个 Python 字典 {"question": question, "answer": answer}。FastAPI会将它自动转换为 JSON 格式并返回HTTP响应,状态码默认为200 OK。
运行服务:使用 Uvicorn 启动。Uvicorn 是一个ASGI服务器,FastAPI推荐使用它来跑服务。在Cursor终端执行:
uvicorn api_service:app --reload
解释:api_service:app 表示从 api_service.py 中导入名为 app 的应用对象。--reload 表示代码改动自动重启服务(开发时方便)。你会看到输出类似:
Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
表示服务已启动监听8000端口。
测试接口:打开浏览器访问 http://127.0.0.1:8000/docs,可看到自动生成的Swagger界面[64]。其中列出了 /chat GET 接口,点选可以试用。在“question”栏输入一个问题(比如 "你好,今天星期几?"),点“Execute”,右侧会显示请求和响应示例。响应应该是 status 200,body 是 {"question": "...", "answer": "..."} 形式,answer里是ChatBot返回的答复。如果出现错误,Swagger界面也会显示错误状态和信息(比如缺参数会返回400 detail说明,500错误会返回我们自定义的信息)。
当然也可以使用命令行工具测试,例如:
curl "http://127.0.0.1:8000/chat?question=OpenAI+API+%E6%80%8E%E4%B9%88%E7%94%A8%EF%BC%9F"
这将发送一个GET请求,URL里包含url编码后的中文参数,应该能得到相应的JSON回复。
成功的话,恭喜!你已经将聊天机器人功能提供成一个Web服务接口了。这样设计后的好处是:前端或任何语言的客户端,只要会发HTTP请求,就能集成这个AI问答功能,而不需要关心内部如何调用OpenAI。这也是后端服务化的价值所在。
操作截图或流程
-
Swagger 文档界面:下图展示 FastAPI 自动生成的Swagger UI[64]。可以看到 /chat 路由在列表中,点击可展开详情,包括描述、参数、响应格式等。图中显示如何通过 UI 输入参数并查看返回结果。这对于开发调试非常方便,不需要另写测试页面。
-
调用成功示例:使用 Swagger 输入 "Hello" 提问,执行后右侧HTTP 200响应中 "answer" 字段包含了ChatBot的回答,比如 "Hi there!"。这个接口返回JSON的截图表明,我们的FastAPI成功地将OpenAI回答包装成了API输出。
-
错误处理示例:如果不带参数直接访问 /chat,Swagger会显示响应400以及错误详情"错误":"问题内容不能为空"或者 "Missing query parameter 'question'"。或者如果OpenAI服务调用异常,我们设计返回500错误。下面截图演示人为暂停计算结果,在代码中引发错误,Swagger捕获500状态并显示 detail 信息 "调用AI服务出错"。这体现了我们的HTTPException错误处理有效起作用。
-
CORS 效果:这一点从前端测试才能看出。我们后面在构建前端页面时,若没有正确设置CORS,中浏览器控制台会报跨域错误阻止请求。有了上面的CORS中间件,前端调用将正常。这里可放一张前端JS调用成功的网络请求截图(将在第6章实现),验证响应头含有 Access-Control-Allow-Origin: * 等。
(建议你亲自打开 http://127.0.0.1:8000/docs 操作一下,加深理解。截图可省略。)
常见错误
-
端口被占用:如果8000端口被别的进程占用,Uvicorn会启动失败报错“Address already in use”。解决:换端口,例如 uvicorn api_service:app --port 8001,或结束占用进程。开发中IDE或调试器有时会残留进程,要注意释放。
-
未安装 FastAPI/Uvicorn:如果前面没安装,ModuleNotFoundError: fastapi。解决:pip install fastapi uvicorn。注意:要安装 fastapi[all] 以包含自动文档需要的依赖(如 pyyaml 等),或者按需安装 pip install fastapi uvicorn[standard]。
-
路径/方法错误:比如前端以POST方式请求我们只定义了GET的接口,会返回405 Method Not Allowed。解决:确认前端请求方法和后端定义一致。我们这里GET适合直接浏览器试,但用JS fetch可能更习惯POST发送JSON,那也可以将接口改为 @app.post("/chat") 并从 request body 读取问题。选择哪种都行,只要前后统一即可。
-
未启动或URL错:如果请求 http://127.0.0.1:8000/chat 显示无法连接,检查服务是否在运行,或者是否请求了错误地址(比如忘记/chat)。
-
OpenAI 调用异常:如果 environment没设置导致第一次调用就HTTPException 500,被FastAPI捕获转成 JSON 输出。虽不至于崩溃服务,但控制台会打印我们print的错误供调试。如果这种错误频繁发生,要考虑加日志系统或重试机制。但在我们的简单场景,只提醒用户稍后再试即可。
-
JSON 编码问题:FastAPI会尝试将返回值转换JSON,但某些Python对象(如Datetime未处理)会失败。在我们例子中答案是字符串,安全无虞。不过要注意返回的数据尽量是基本类型(dict/list/str/number等)或可json化的Pydantic模型。
-
CORS 设置过于宽松:虽然在开发期不是报错,但提醒:allow_origins=["*"] 会允许任意网站调用你的API,如果部署上线应改为特定域名清单。同时切记不要在响应里泄露敏感信息,因为任何来源都能访问。安全起见,可以设置 allow_credentials=True 且受信源,保护cookie等敏感信息。在我们无状态API中暂不涉及Cookie。
延伸思考
-
API 设计优化:我们当前接口很简单。思考如果要支持连续对话,该如何设计API?可以让请求携带一个会话ID或上下文列表,服务器负责维护对话状态,或者干脆把上下文也由客户端传每次请求。不同设计权衡在于服务器无状态性和实现复杂度。另一个改进是Rate Limiting(限流):防止滥用。例如在实际服务中,可以针对IP或API Key设置调用频率上限。FastAPI可通过Depends注入节流逻辑实现。
-
部署和并发:用 Uvicorn 启动开发方便,但生产环境通常会用更健壮的ASGI服务器或者进程管理。如使用Gunicorn搭配多Uvicorn worker处理高并发,或者干脆使用Serverless(后面第7章会提及 Vercel等无服务器托管)。FastAPI 的高性能基于Starlette和uvloop,能应对大量并发请求。不过OpenAI调用较慢是瓶颈,可考虑异步调用提高吞吐(openai库目前必须在线程池,否则会阻塞)。这些属于高级优化范畴,可以在以后学习。
-
错误处理与监控:我们的API对已知问题做了一些HTTPException处理。但生产服务需要更完善的错误监控。可以集成Logging模块输出日志,或者用FastAPI的中间件捕获未处理异常统一上报。此外,像OpenAI API调用这样外部依赖,要考虑它的错误(超时、500)如何反馈给前端。目前我们简单地500返回,让前端“稍后再试”。如需更友好体验,可实现重试或fallback(比如换个模型尝试)。这些都是构建健壮AI服务需要考虑的。
现在,我们已经有了后端API,可以为前端提供服务。下一章让我们构建一个简单网页,调用这个API,实现浏览器上的聊天对话界面!