Day 12:AI 应用后端结构设计
学习目标
前四天你分别学会了 API 调用、结构化输出、Function Calling 和 Prompt 模板管理。今天要把这些散件组装成一个完整的后端服务。
很多学 AI 的人会停留在”写脚本”阶段——一个 Python 文件里塞了 API 调用、Prompt、数据处理,跑起来能出结果就觉得够了。这在演示阶段没问题,但一旦需要给别人用、需要稳定运行、需要持续迭代,脚本就撑不住了。你需要一个结构清晰的后端服务。
今天要学的不是某个框架的高级用法,而是”怎么把 AI 能力组织成一个可维护的后端系统”。路由怎么设计、请求和响应怎么定义、服务层怎么拆分、配置怎么管理、日志怎么打、错误怎么返回、项目目录怎么组织——这些都是工程问题,和 AI 本身无关,但决定了你的 AI 能力能不能被可靠地交付。
完成今天的学习后,你应该能搭建一个结构清晰的 FastAPI 项目,设计规范的 API 接口,把 LLM Client、结构化输出、Prompt 模板整合到一个统一的架构中。
核心概念
一、FastAPI 基础
FastAPI 是 Python 生态中构建 API 服务的首选框架。它快(基于异步 ASGI)、简洁(用类型注解自动生成文档)、好用(自动数据校验和序列化)。对于 AI 应用后端来说,FastAPI 几乎是最合适的选择。
为什么选 FastAPI 而不是 Flask 或 Django?Flask 是同步框架,AI 应用经常需要等待模型响应(可能几十秒),同步框架在等待期间会阻塞线程,影响并发能力。FastAPI 是异步框架,等待模型响应期间可以处理其他请求,并发能力更强。Django 太重了,它内置了 ORM、模板引擎、管理后台等很多 AI 应用用不到的东西,学习成本和复杂度都不划算。
FastAPI 的核心概念不多,但对于 AI 应用后端来说,需要重点理解以下几个:
路由(Router)。路由决定了什么 URL 路径对应什么处理函数。比如 POST /analyze/industry 对应行业分析处理函数。
依赖注入(Dependency Injection)。FastAPI 支持依赖注入,可以在处理函数中自动获取数据库连接、配置对象等依赖。这避免了在函数内部手动创建这些对象。
请求体(Request Body)。用 Pydantic 模型定义请求体的结构,FastAPI 自动解析和校验。非法的请求会在进入处理函数之前就被拦截。
响应模型(Response Model)。用 Pydantic 模型定义响应的结构,FastAPI 自动序列化和校验。保证你的 API 返回的数据格式是可预期的。
中间件(Middleware)。中间件是在请求处理之前或之后执行的逻辑。常见的用途包括日志记录、错误处理、认证鉴权。
FastAPI 还有一个被低估的优势:它自动生成交互式 API 文档。你定义好路由和请求/响应模型后,访问 /docs 路径就能看到一个可测试的 API 文档页面。这在开发阶段非常方便,也方便后续给其他人看你的接口设计。
二、路由设计
路由设计是 API 后端的骨架。好的路由设计让使用者直觉地知道怎么调用你的接口。
路由设计的第一原则是 RESTful 风格。虽然 AI 应用不一定要严格遵守 REST 规范,但遵循一些基本约定能让 API 更易理解:
用名词表示资源,用 HTTP 方法表示操作。/industries 表示行业资源,POST /industries/analyze 表示分析行业的操作。
用路径参数表示具体的资源实例。/reports/{report_id} 表示获取某个具体的报告。
用查询参数表示过滤条件。/reports?type=industry&status=completed 表示获取已完成类型的行业分析报告。
对于 AI 应用来说,路由通常分为以下几组:
分析类接口。/analyze/industry(行业分析)、/analyze/role(岗位分析)、/analyze/process(流程分析)。这类接口接收分析请求,调用模型,返回结构化的分析结果。
报告类接口。/reports(报告列表)、/reports/{id}(获取某个报告)、/reports/generate(生成新报告)。这类接口管理分析报告的生命周期。
评估类接口。/evaluate/opportunity(AI 机会评估)、/evaluate/risk(风险评估)。这类接口对特定场景做量化评估。
系统接口。/health(健康检查)、/stats(使用统计)。这类接口用于运维和监控。
路由设计的第二原则是版本化。在路由路径中加入版本号(比如 /api/v1/analyze/industry),这样当你需要对接口做不兼容的修改时,可以发布新版本(/api/v2/…)而不影响使用旧版本的客户端。
路由设计的第三原则是命名一致性。要么全用复数(/industries、/roles、/reports),要么全用单数(/industry、/role、/report),不要混用。HTTP 方法也要一致——查询用 GET,创建用 POST,更新用 PUT,删除用 DELETE。
三、请求体设计
请求体是客户端发给你的数据。在 AI 应用中,请求体通常包含:用户要分析的对象(行业名称、岗位名称等)、可选的分析参数(深度、维度、格式偏好等)、可选的上下文信息(之前的相关分析结果、用户的特殊要求等)。
请求体设计的关键是用 Pydantic 模型来定义。这不仅让 FastAPI 自动校验参数,还让接口文档自动生成。
以行业分析接口为例,请求体应该包含:
industry_name(行业名称)。必填字符串。这是最核心的参数,没有它分析无法进行。
analysis_depth(分析深度)。选填枚举,可选值为”概览”、“标准”、“深入”,默认”标准”。不同的深度对应不同的 Prompt 模板和输出详细程度。
focus_areas(重点关注领域)。选填字符串数组。比如用户特别关心”竞争格局”和”AI 机会”,可以在请求中指定,系统会针对性地加强这些维度的分析。
output_format(输出格式)。选填枚举,可选值为”json”、“markdown”,默认”json”。控制返回结果的格式。
请求体设计要避免过度设计。初学者常犯的错误是把所有可能的参数都塞进请求体,导致请求结构复杂且难以使用。遵循”必填最少、选填合理”的原则——核心参数必填,扩展参数选填,每个选填参数都有明确的默认值。
另一个容易忽略的问题是输入长度限制。对于文本类型的参数,要设置最大长度。比如用户输入的”特殊要求”字段,如果不限制长度,用户可能粘贴一整篇文档进来,导致 Prompt 过长、Token 消耗暴增、模型处理超时。
四、响应体设计
响应体是你的接口返回给客户端的数据。好的响应体设计让客户端能高效地处理结果。
响应体的设计原则:
结构统一。所有接口的响应都应该遵循统一的结构格式。一个常见的格式是:code(状态码)、message(状态描述)、data(具体数据)。
状态码规范。成功返回 0 或 200,业务错误返回对应错误码(比如 1001 表示”行业名称不能为空”、1002 表示”分析超时”),系统错误返回 5000。错误码要有文档说明。
数据字段完整。以行业分析结果为例,data 字段应该包含:analysis_id(分析结果的唯一标识)、industry_name(分析的行业名称)、result(结构化的分析结果)、metadata(元信息:使用的模型、Token 消耗、耗时、版本号)。
响应体的设计还要考虑分页(列表类接口需要)、缓存(同一查询是否需要重新分析)、异步(耗时长的分析是否需要返回任务 ID 然后轮询结果)。
对于 AI 应用,有一个特殊的设计考量:模型生成可能需要很长时间(10-60 秒)。如果接口同步等待模型返回,客户端可能因为超时而失败。两种解决方案:
同步模式。适用于快速分析(10 秒以内能完成)。客户端发送请求,等待响应。简单直接,但受限于模型响应速度。
异步模式。适用于深度分析(可能需要 30 秒以上)。客户端发送请求,接口立即返回一个任务 ID。客户端通过轮询或 WebSocket 获取结果。复杂但更健壮。
在实际项目中,建议两种模式都支持——通过请求参数中的 mode 字段来选择。简单查询用同步模式,复杂分析用异步模式。
五、服务层拆分
如果你的所有业务逻辑都写在路由处理函数里,代码很快就会变得臃肿且难以维护。服务层拆分就是把不同的职责分到不同的模块中。
AI 应用后端通常包含以下服务层:
API 层(路由层)。负责接收请求、调用服务层、返回响应。这一层只做”搬运”——把请求参数传给服务层,把服务层的结果包装成响应返回。不包含任何业务逻辑。
业务服务层。包含具体的业务逻辑。比如行业分析服务(IndustryAnalysisService)负责:选择合适的 Prompt 模板、填充参数、调用模型客户端、校验输出、保存结果。这一层是业务逻辑的核心。
模型调用层。就是 Day 8 设计的 LLM Client。负责和模型 API 交互,处理错误重试、日志记录、Token 统计。业务服务层通过这一层调用模型,不直接和模型 API 打交道。
Prompt 管理层。就是 Day 11 设计的 Prompt 模板库。负责模板的加载、参数填充、版本管理。业务服务层通过这一层获取完整的 Prompt,不直接拼接 Prompt 文本。
数据访问层。负责数据库操作——保存分析结果、查询历史记录、管理报告。业务服务层通过这一层读写数据,不直接写 SQL。
各层之间的依赖关系是单向的:API 层依赖业务服务层,业务服务层依赖模型调用层、Prompt 管理层和数据访问层。下层不依赖上层。
这种分层设计的好处:
关注点分离。每层只关心自己的职责。修改 Prompt 不影响路由代码,修改数据库不影响业务逻辑。
可测试性。每层可以独立测试。测试业务服务层时可以 mock 模型调用层,不需要真的调模型 API。
可替换性。如果要从 OpenAI 切换到 Claude,只需要改模型调用层,其他层不受影响。
六、配置管理
配置管理是把”会变化的东西”从代码中分离出来。包括:API Key、模型名称、默认参数、数据库连接字符串、日志级别、超时时间等。
配置管理的原则:
环境区分。开发环境、测试环境、生产环境使用不同的配置。不要在代码里写 if env == “production”。
敏感信息隔离。API Key、数据库密码等敏感信息存在环境变量或密钥管理服务中,不放在配置文件里。
配置文件格式。推荐使用 YAML 或 TOML 格式。比 JSON 更易读(支持注释),比 .env 更灵活(支持嵌套结构)。
配置加载。应用启动时加载配置,运行时通过配置对象访问。不要到处散落 os.getenv() 调用——集中在一个配置模块中管理。
默认值。每个配置项都应该有合理的默认值。这样即使配置文件缺失某项,应用也能正常运行。
配置管理的一个实用模式是”分层覆盖”:代码中有硬编码的默认值 → 配置文件可以覆盖默认值 → 环境变量可以覆盖配置文件的值。优先级从低到高:默认值 < 配置文件 < 环境变量。这样在开发时用默认值就够了,部署时通过环境变量覆盖敏感信息。
七、日志中间件
日志中间件是在每个请求处理前后自动记录日志的组件。
为什么用中间件而不是在每个处理函数里手动记录?因为手动记录容易遗漏——你可能忘了在某个接口里加日志。中间件是全局的,所有请求都会经过它,不会有遗漏。
日志中间件应该记录:
请求信息。时间戳、请求方法(GET/POST)、请求路径、请求参数(脱敏后)。
处理信息。调用模型了几次、用了什么 Prompt 模板、Token 消耗多少、耗时多少。
响应信息。状态码、响应数据大小、是否成功。
错误信息。如果处理过程中出错,记录完整的错误堆栈。
日志中间件的实现方式是利用 FastAPI 的中间件机制。在请求进入处理函数之前记录开始时间和请求信息,在请求返回之后计算耗时并记录响应信息。
日志的级别要根据场景选择:正常请求用 INFO,慢请求(超过阈值)用 WARNING,错误请求用 ERROR。这样在查看日志时可以快速筛选出需要关注的内容。
八、错误返回规范
错误返回要统一。不能有的接口返回纯文本错误信息,有的返回 JSON,有的返回 HTML。
统一的错误返回格式:
error_code(错误码)。一个数字,标识错误类型。业务错误用 1xxx(比如 1001 参数缺失、1002 分析失败),系统错误用 5xxx(比如 5001 数据库错误、5002 模型调用超时)。
error_message(错误信息)。一段人类可读的错误描述。比如”行业名称不能为空”或”模型调用超时,请稍后重试”。
detail(详细信息)。可选,包含更具体的调试信息。生产环境可以不返回这个字段(避免暴露内部实现细节)。
timestamp(时间戳)。错误发生的时间。
request_id(请求 ID)。用于在日志中追踪这个请求的完整处理过程。
错误返回的分级原则:
对客户端暴露的信息要简洁友好。“模型调用超时”比”openai.APITimeoutError: Request timed out after 30.0s”更容易理解。
不要暴露内部实现细节。不要在错误信息中包含数据库表名、文件路径、API Key 的部分内容等。
对日志记录要详细。日志中的错误信息要包含完整的堆栈和上下文,方便排查问题。但这个详细信息不要返回给客户端。
九、项目目录结构
目录结构是项目组织的物理体现。好的目录结构让新加入的人能快速理解项目组成。
AI 应用后端的推荐目录结构:
project/
|-- app/
| |-- main.py # 应用入口,FastAPI 实例
| |-- config.py # 配置管理
| |-- routers/ # 路由定义
| | |-- analyze.py # 分析类接口
| | |-- report.py # 报告类接口
| | |-- evaluate.py # 评估类接口
| |-- services/ # 业务服务层
| | |-- industry_service.py
| | |-- role_service.py
| | |-- report_service.py
| |-- clients/ # 外部调用客户端
| | |-- llm_client.py # 模型调用客户端
| |-- prompts/ # Prompt 模板库
| | |-- templates/ # 模板文件
| | |-- prompt_manager.py # 模板管理器
| |-- models/ # 数据模型
| | |-- schemas.py # Pydantic 模型定义
| | |-- database.py # 数据库模型
| |-- middleware/ # 中间件
| | |-- logging.py # 日志中间件
| | |-- error_handler.py # 错误处理中间件
| |-- utils/ # 工具函数
|-- tests/ # 测试
|-- docs/ # 文档
|-- requirements.txt # 依赖
|-- .env # 环境变量(不提交到 Git)
|-- config.yaml # 配置文件
这个结构的逻辑是:routers 只负责接收请求和返回响应,services 包含业务逻辑,clients 封装外部调用,prompts 管理 Prompt 模板,models 定义数据结构,middleware 处理横切关注点(日志、错误)。
每个文件的职责要单一。不要在 router 文件里写业务逻辑,不要在 service 文件里直接调用模型 API。职责越单一,代码越容易理解、测试和修改。
概念关系图
+---------------------------+
| 客户端请求 |
+-------------+-------------+
|
v
+---------------------------+
| FastAPI 应用入口 |
| +-- 中间件(日志/错误)--+
+-------------+-------------+
|
v
+---------------------------+
| 路由层 (Routers) |
| /analyze/industry |
| /analyze/role |
| /reports/generate |
+-------------+-------------+
|
v
+---------------------------+
| 业务服务层 (Services) |
| IndustryService |
| RoleService |
| ReportService |
+------+------+------+------+
| | |
v v v
+------+ +--+--+ +-------+
|LLM | |Prompt| |数据 |
|Client| |管理器| |访问层 |
+------+ +--+--+ +-------+
| | |
v v v
+------+ +------+ +-------+
|模型 | |模板 | |数据库 |
|API | |文件 | | |
+------+ +------+ +-------+
实战分析
任务一:搭建 FastAPI 项目
按照上面推荐的目录结构,创建项目骨架。不需要写完整的业务逻辑,但要确保项目能启动、路由能访问、中间件能工作。
关键步骤:
创建项目目录和各层文件夹。在 app/main.py 中创建 FastAPI 实例。在 app/config.py 中实现配置加载。在 app/middleware/ 中实现日志中间件和错误处理中间件。注册路由和中间件。
任务二:设计 /analyze_industry 接口
行业分析接口的设计涉及请求体、响应体、服务层三个层面。
请求体设计。定义 IndustryAnalysisRequest 模型,包含 industry_name(必填)、analysis_depth(选填,默认”标准”)、focus_areas(选填)。
响应体设计。定义 AnalysisResponse 模型,包含 code(0 表示成功)、message(“分析完成”)、data(包含 analysis_id、industry_name、result、metadata)。
服务层设计。IndustryAnalysisService 接收请求参数,调用 PromptManager 获取行业分析模板,填充参数后调用 LLMClient 获取模型输出,通过结构化输出机制校验结果,保存到数据库,返回响应。
任务三:设计 /analyze_role 接口
岗位分析接口的设计思路和行业分析类似,但分析对象不同。
请求体设计。定义 RoleAnalysisRequest 模型,包含 role_name(岗位名称,必填)、industry_name(所属行业,选填)、company_type(公司类型,选填)。
响应体设计。和行业分析接口使用统一的响应格式,只是 data 中的 result 字段结构不同——岗位分析结果包含岗位概述、核心职责、KPI、任务列表、AI 机会等。
任务四:设计 /generate_report 接口
报告生成接口的特点是它依赖之前的分析结果。
请求体设计。定义 ReportGenerateRequest 模型,包含 analysis_ids(分析结果 ID 列表,必填)、report_type(报告类型,必填枚举)、format_style(格式风格,选填)。
服务层设计。ReportService 先从数据库查询指定的分析结果,拼接成报告素材,调用 PromptManager 获取报告生成模板,调用 LLMClient 生成报告文本,格式化后返回。
任务五:接入 LLMClient
在服务层中注入 LLMClient 实例。通过 FastAPI 的依赖注入机制,在应用启动时创建 LLMClient 单例,所有服务共享同一个实例。
这样做的好处是:LLMClient 的配置只需要在一处初始化,所有服务使用统一的模型调用行为(错误处理、日志记录、Token 统计)。
当日产物说明
FastAPI 项目骨架
一个可以启动运行的 FastAPI 项目。包含完整的目录结构、配置管理、日志中间件、错误处理中间件。所有路由都定义好了(虽然业务逻辑可能还是占位实现)。启动后访问 /docs 可以看到自动生成的 API 文档。
API 路由设计文档
一份文档,列出所有接口的设计。每个接口包含:URL 路径、HTTP 方法、请求体结构(字段名、类型、是否必填、描述)、响应体结构、错误码列表、使用示例。
这份文档应该让一个不熟悉项目的人能快速理解你的接口设计。
项目目录结构说明
一份说明文档,解释每个目录和文件的职责。包含:目录树展示、每个目录的作用、文件之间的依赖关系、新增功能时应该在哪个目录添加文件。
常见误区与避坑
误区一:所有代码写在一个文件里
一个 main.py 文件里塞了 2000 行代码,路由、业务逻辑、数据库操作、Prompt 全在里面。修改一个功能要在 2000 行代码中找到对应的位置。加一个功能文件又膨胀 200 行。正确做法是按职责分层,每个文件不超过 200 行。
误区二:路由层包含业务逻辑
在路由处理函数里直接调用模型 API、拼接 Prompt、处理结果。这导致路由函数又长又复杂,难以测试和复用。路由层应该只做参数校验和结果包装,业务逻辑放在服务层。
误区三:配置散落各处
API Key 在一个文件里、数据库密码在另一个文件里、模型名称硬编码在第三个文件里。要改一个配置要翻遍整个项目。正确做法是集中在一个配置模块中管理,通过环境变量覆盖敏感信息。
误区四:错误处理不统一
有的接口返回 HTML 错误页面,有的返回纯文本,有的返回 JSON 但格式各不相同。客户端需要针对不同的接口写不同的错误处理逻辑。正确做法是使用统一的错误返回格式,通过中间件统一处理。
误区五:没有请求 ID 的概念
日志里没有请求 ID,当多个请求同时处理时,无法区分哪条日志属于哪个请求。正确做法是每个请求生成一个唯一 ID,所有相关日志都带上这个 ID。
延伸思考
今天的后端结构设计是一个”最小可用”的架构。它足以支撑你后续几周的 Demo 和 MVP 开发,但随着系统复杂度的增加,你会需要引入更多组件。
Week 5 的工程化阶段会在这个基础上增加:数据库接入(从文件存储升级到关系型数据库)、缓存层(相同查询不重复调用模型)、异步任务队列(耗时的分析任务放到后台执行)、监控告警(系统异常时自动通知)。
从架构演进的角度看,今天的分层设计为未来扩展留好了空间。API 层、服务层、客户端层的分层是稳定的,不会因为增加新功能而大改。新增功能通常只需要:加一个路由、加一个服务、加一个 Prompt 模板。
从更商业化的角度看,一个结构清晰的后端意味着你可以快速响应客户需求。客户说”我需要一个岗位分析接口”,你只需要定义请求/响应模型、写一个服务、选一个 Prompt 模板,半天就能交付。如果代码是一团乱麻,哪怕是一个简单的接口也可能改三天。
自测问题
- 为什么 AI 应用后端推荐用 FastAPI 而不是 Flask?
- 路由设计为什么要版本化(比如 /api/v1/)?如果不版本化会有什么问题?
- 请求体设计中,为什么每个文本类型的参数都要设置最大长度?
- 对于可能耗时很长的分析请求,同步模式和异步模式各有什么优缺点?
- 服务层拆分的原则是什么?API 层应该包含业务逻辑吗?
- 配置管理中,“分层覆盖”模式的三层优先级分别是什么?
- 日志中间件比手动记录日志好在哪里?
- 错误返回为什么要统一格式?暴露内部实现细节有什么风险?
- 如果要在项目中新增一个”竞品分析”接口,你需要修改哪些文件?
- 为什么说今天的分层设计为未来扩展留好了空间?
关键词
- FastAPI:Python 异步 Web 框架,适合构建 AI 应用后端 API 服务
- 路由设计:定义 URL 路径和 HTTP 方法到处理函数的映射关系
- 请求体:客户端发送给 API 的数据,用 Pydantic 模型定义和校验
- 响应体:API 返回给客户端的数据,包含状态码、消息和业务数据
- 服务层拆分:将路由、业务逻辑、外部调用、数据访问按职责分层
- 配置管理:集中管理所有可变配置,支持环境区分和敏感信息隔离
- 日志中间件:全局自动记录每个请求处理过程的组件
- 错误返回规范:统一的错误响应格式,包含错误码、信息和请求 ID
- 项目目录结构:按职责组织代码文件的物理结构
- 依赖注入:通过框架自动提供依赖对象的机制,降低组件间耦合