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 模板,半天就能交付。如果代码是一团乱麻,哪怕是一个简单的接口也可能改三天。

自测问题

  1. 为什么 AI 应用后端推荐用 FastAPI 而不是 Flask?
  2. 路由设计为什么要版本化(比如 /api/v1/)?如果不版本化会有什么问题?
  3. 请求体设计中,为什么每个文本类型的参数都要设置最大长度?
  4. 对于可能耗时很长的分析请求,同步模式和异步模式各有什么优缺点?
  5. 服务层拆分的原则是什么?API 层应该包含业务逻辑吗?
  6. 配置管理中,“分层覆盖”模式的三层优先级分别是什么?
  7. 日志中间件比手动记录日志好在哪里?
  8. 错误返回为什么要统一格式?暴露内部实现细节有什么风险?
  9. 如果要在项目中新增一个”竞品分析”接口,你需要修改哪些文件?
  10. 为什么说今天的分层设计为未来扩展留好了空间?

关键词

  • FastAPI:Python 异步 Web 框架,适合构建 AI 应用后端 API 服务
  • 路由设计:定义 URL 路径和 HTTP 方法到处理函数的映射关系
  • 请求体:客户端发送给 API 的数据,用 Pydantic 模型定义和校验
  • 响应体:API 返回给客户端的数据,包含状态码、消息和业务数据
  • 服务层拆分:将路由、业务逻辑、外部调用、数据访问按职责分层
  • 配置管理:集中管理所有可变配置,支持环境区分和敏感信息隔离
  • 日志中间件:全局自动记录每个请求处理过程的组件
  • 错误返回规范:统一的错误响应格式,包含错误码、信息和请求 ID
  • 项目目录结构:按职责组织代码文件的物理结构
  • 依赖注入:通过框架自动提供依赖对象的机制,降低组件间耦合