创建时间: 2026-08-25最后更新: 2026-08-27

1. 路由

路由解决的是「某个 HTTP 请求应该由哪个函数处理」。FastAPI 使用路径和 HTTP 方法共同匹配请求:

routes.py
01
from fastapi import FastAPI
02
03
app = FastAPI()
04
05
@app.get("/conversations")
06
async def list_conversations():
07
return []
08
09
@app.post("/conversations")
10
async def create_conversation():
11
return {"id": "conversation-1"}

两个接口路径相同,但方法不同,所以是两个独立的路径操作。浏览器地址栏通常发送 GET,而创建数据一般使用 POST。

2. 方法

常见 HTTP 方法可以先这样理解:

方法常见语义示例
GET读取资源,不应改变业务状态查询会话
POST创建资源或触发一次处理创建会话、发送消息
PUT完整替换指定资源替换会话设置
PATCH修改资源的部分字段修改会话标题
DELETE删除资源删除会话

这些不是纯粹的命名偏好。浏览器、代理、缓存和接口调用方都会根据 HTTP 语义处理请求。

GET 应该是安全方法,也就是读取时不产生用户可见的业务修改。PUT 和 DELETE 通常应该具备幂等性:同一个请求执行多次,最终状态与执行一次相同。POST 通常不保证幂等,支付或任务创建等接口需要额外设计幂等键。

3. 路径参数

路径中的花括号声明动态参数:

path-parameter.py
1
from fastapi import FastAPI
2
3
app = FastAPI()
4
5
@app.get("/conversations/{conversation_id}")
6
async def get_conversation(conversation_id: str):
7
return {"id": conversation_id}

请求 /conversations/abc 时,conversation_id 的值是 "abc"。

类型提示会参与转换和校验:

integer-path.py
1
@app.get("/messages/{message_id}")
2
async def get_message(message_id: int):
3
return {"id": message_id}

请求 /messages/42 时,函数收到整数 42,不是字符串。请求 /messages/hello 时,FastAPI 无法转换,会直接返回校验错误。

路径参数是 URL 的组成部分,适合表达「要操作哪个资源」。如果一个值只是筛选条件,不决定资源身份,就更适合放在查询参数中。

4. 枚举

有些路径参数只能从有限选项中选择,例如模型供应商。此时可以使用 Python 的 Enum,不要接收任意字符串后再手动写一串判断:

path-enum.py
01
from enum import Enum
02
03
from fastapi import FastAPI
04
05
app = FastAPI()
06
07
class Provider(str, Enum):
08
deepseek = "deepseek"
09
openai = "openai"
10
11
@app.get("/providers/{provider}")
12
async def get_provider(provider: Provider):
13
return {"provider": provider}

请求 /providers/deepseek 时,函数收到的是 Provider.deepseek;传入未声明的值则会得到校验错误。可选值还会进入 OpenAPI,交互文档可以直接显示选项。

继承 str 可以让枚举值按字符串参与 JSON 序列化。对仅用于类型检查的简单字符串选项,也可以使用 Literal;需要在业务代码中反复比较、复用时,Enum 通常更清楚。

5. 查询参数

路径函数中没有出现在 URL 路径里的简单参数,通常会被识别为查询参数:

query-parameters.py
1
@app.get("/conversations")
2
async def list_conversations(
3
offset: int = 0,
4
limit: int = 20,
5
):
6
return {"offset": offset, "limit": limit}

对应请求:

code.ts
1
GET /conversations?offset=20&limit=10

带默认值的参数是可选参数。不提供时,函数使用默认值。没有默认值的查询参数是必填参数:

required-query.py
1
@app.get("/search")
2
async def search(q: str):
3
return {"query": q}

查询参数是 URL 中 ? 后面的键值。它们原本都是文本,FastAPI 会根据类型提示完成转换。例如 ?limit=10 会得到整数 10,?enabled=true 会得到布尔值 True。转换失败时,路径函数不会执行。

6. Annotated

前面的函数只使用了 q: str、limit: int 这样的类型提示。它们可以告诉 FastAPI 参数应该转换成什么类型,却没有说明参数来自路径、查询字符串还是请求体,也无法表达长度和数值范围。

Annotated 用来在保留原始类型的同时,给这个类型附加额外信息。它的基本语法是:

annotated-syntax.py
1
Annotated[原始类型, 元数据1, 元数据2, ...]

Annotated 从 Python 3.9 开始可以直接从 typing 导入。更早的 Python 项目可能会从 typing_extensions 导入,但本小册使用的新版本 Python 直接写 from typing import Annotated 即可。

第一个位置必须是类型,后面可以放一项或多项元数据。例如:

annotated-limit.py
1
limit: Annotated[int, Query(ge=1, le=100)] = 20

这行代码包含四层信息:

部分含义
limitPython 函数参数名,也是默认的查询参数名
int函数最终接收到的值应该是整数
Query(ge=1, le=100)参数来自查询字符串,并且值必须在 1 到 100 之间
= 20客户端不传 limit 时,函数使用默认值 20

对于 Python 的类型检查器来说,这个参数的核心类型仍然是 int。FastAPI 在运行时读取 Query(...) 这项元数据,再完成参数来源判断、字符串到整数的转换、范围校验和 OpenAPI 文档生成。不了解这项元数据的普通工具可以忽略它,继续把参数当成 int。

需要特别注意,Annotated 本身不会执行类型转换,也不会在普通 Python 函数调用中拦截错误数据。如果我们绕过 HTTP 请求,直接在 Python 代码中调用函数并传入错误类型,Python 不会因为写了 Annotated 就自动报错。这里真正执行校验的是 FastAPI 背后的参数解析与 Pydantic 校验机制。

现在回到一个完整的路由:

constrained-parameters.py
01
from typing import Annotated
02
03
from fastapi import FastAPI, Path, Query
04
05
app = FastAPI()
06
07
@app.get("/users/{user_id}/messages")
08
async def list_messages(
09
user_id: Annotated[int, Path(gt=0)],
10
q: Annotated[str | None, Query(max_length=50)] = None,
11
limit: Annotated[int, Query(ge=1, le=100)] = 20,
12
):
13
return {
14
"user_id": user_id,
15
"query": q,
16
"limit": limit,
17
}

三个参数分别表达了不同规则:

  • user_id 出现在路径模板中,Path(gt=0) 明确它是路径参数,并要求整数值大于 0;
  • q 使用 str | None,表示函数可能收到字符串或 None。Query(max_length=50) 将搜索词限制在 50 个字符以内,外面的 = None 表示客户端可以不传;
  • limit 是整数查询参数,范围为 1 到 100,省略时使用默认值 20。

gt 是 greater than,表示严格大于;ge 是 greater than or equal,表示大于等于;le 是 less than or equal,表示小于等于。这些约束不仅影响运行时校验,也会写进 OpenAPI,让接口文档和客户端生成工具获得相同规则。

默认值的位置

使用 Annotated 时,默认值应该继续写在 Python 参数的等号右边,而不是放进 Query():

annotated-default.py
1
# 推荐:默认值只有一个来源
2
limit: Annotated[int, Query(ge=1, le=100)] = 20
3
4
# 错误:FastAPI 无法判断应该使用 10 还是 20
5
limit: Annotated[int, Query(default=10, ge=1, le=100)] = 20

第二种写法会产生冲突,因此 FastAPI 不允许在 Annotated 内的 Query() 中再声明 default。默认值放在等号右边还有一个好处:即使这个函数脱离 FastAPI 被普通 Python 代码调用,它仍然遵循正常的函数默认值规则。

你可能会在旧项目中看到下面这种写法:

legacy-query.py
1
limit: int = Query(default=20, ge=1, le=100)

这种语法仍然可以使用,但当前 FastAPI 更推荐 Annotated。旧写法把 FastAPI 的 Query 对象占用了 Python 默认值的位置;新写法则把 int、FastAPI 元数据和真实默认值分别放在各自的位置,编辑器和类型检查器也更容易理解。

必填、可选与可空

参数是否必填,主要取决于有没有默认值;类型中是否包含 None,表示允许的值中是否包含空值。这是两个不同的问题:

annotated-required.py
1
# 必填:客户端必须提供字符串
2
q: Annotated[str, Query(min_length=1)]
3
4
# 可选:不提供时使用 "all"
5
q: Annotated[str, Query(min_length=1)] = "all"
6
7
# 可选且允许为空:不提供时得到 None
8
q: Annotated[str | None, Query(max_length=50)] = None

因此,只有 str | None 并不会自动让参数变成可选参数。真正让客户端可以省略它的是 = None 或其他默认值。路径参数则是 URL 结构的一部分,不能通过默认值变成可选;如果路径模板包含 {user_id},请求中就必须出现对应的路径片段。

复用约束

当多个接口使用相同规则时,可以把完整的 Annotated 声明保存成类型别名:

reusable-annotated.py
01
from typing import Annotated
02
03
from fastapi import FastAPI, Query
04
05
app = FastAPI()
06
07
PageSize = Annotated[
08
int,
09
Query(ge=1, le=100, description="每页返回数量"),
10
]
11
12
@app.get("/conversations")
13
async def list_conversations(limit: PageSize = 20):
14
return {"limit": limit}
15
16
@app.get("/memories")
17
async def list_memories(limit: PageSize = 20):
18
return {"limit": limit}

PageSize 对类型检查器来说仍然是 int,但 FastAPI 还能从中读取查询参数约束。这样可以让分页规则在多个路由中保持一致。类型别名适合复用真正相同的接口规则;如果两个参数只是碰巧都叫 limit,但业务范围不同,就不应该为了减少几行代码而强行共用。

除了 Path() 和 Query(),后续还会看到 Body()、Header()、Cookie()、File()、Form() 和 Depends() 出现在 Annotated 中。它们使用的是同一种思路:第一个位置描述 Python 值的类型,后面的元数据告诉 FastAPI 应该从哪里取得这个值,以及应该如何处理它。

7. 顺序

FastAPI 底层使用 Starlette 的路由系统。收到请求后,路由器会按照路径操作的注册顺序逐个尝试匹配,找到第一个同时符合 HTTP 方法和路径模式的路由后,就进入这个路由的参数解析和处理流程。

它不会先把所有路由收集起来,再自动判断哪一个更具体。因此,静态路径要放在可能覆盖它的动态路径之前:

route-order.py
1
@app.get("/users/me")
2
async def get_current_user():
3
return {"id": "current"}
4
5
@app.get("/users/{user_id}")
6
async def get_user(user_id: str):
7
return {"id": user_id}

请求 /users/me 时,第一个路由精确匹配;请求 /users/42 时,第一个路由不匹配,路由器才会继续尝试 /users/{user_id}。

如果把顺序调换,/users/{user_id} 会先把 me 当成 user_id。即使把参数写成 user_id: int 也不能解决顺序问题:路由系统会先根据 URL 形状选中动态路由,然后 FastAPI 才校验参数类型。此时 /users/me 得到的是整数转换错误,而不是回退到后面的静态路由。

通配路径放在最后

{file_path:path} 可以匹配包含 / 的剩余路径,覆盖范围比普通路径参数更大:

catch-all-route.py
1
@app.get("/files/public")
2
async def get_public_file():
3
return {"file": "public"}
4
5
@app.get("/files/{file_path:path}")
6
async def get_file(file_path: str):
7
return {"file": file_path}

/files/public 必须先声明,否则后面的通配路由会把 public 当成 file_path。同理,固定路径应放在普通动态路径之前,普通动态路径再放在能吞掉多段 URL 的通配路径之前。

聚合路由也有顺序

使用 include_router() 拆分文件后,路由仍然会按照被接入应用的顺序参与匹配。如果两个 APIRouter 最终生成了可能重叠的路径,更具体的路由应该先接入。不过更稳妥的做法,是为不同业务分配清晰且不重叠的前缀,例如 /conversations 和 /memories,不要长期依赖导入顺序解决业务边界冲突。

不要把类型当成路由重载

同一个路径和方法也不要重复声明。即使 Python 允许定义两个不同函数,客户端也无法可靠选择应该执行哪一个。

路由匹配只看 HTTP 方法和 URL 路径,不会根据函数参数的 Python 类型选择重载。下面这种思路无法区分两个接口:

invalid-overload.py
1
@app.get("/users/{user_id}")
2
async def get_user_by_id(user_id: int):
3
return {"id": user_id}
4
5
@app.get("/users/{username}")
6
async def get_user_by_name(username: str):
7
return {"username": username}

对路由器来说,这两个路径模式都是 /users/{某个值},参数名和 Python 类型不会参与选择。可以把它们改成 /users/by-id/{user_id} 与 /users/by-name/{username},也可以统一成一个稳定标识后在业务层查询。

同一路径使用不同 HTTP 方法则没有问题,例如 GET /conversations 和 POST /conversations 是两条独立的路径操作。真正需要避免的是相同方法和相同路径模式重复注册,否则运行时命中的处理函数和 OpenAPI 中展示的接口可能难以对应。

可以把顺序规则归纳为:精确静态路径在前,普通动态路径居中,通配路径在后;业务路由尽量通过前缀消除重叠,而不是依赖声明顺序维持正确结果。

8. 路径规范

路径规范有两类:一类是 FastAPI 对路径和 prefix 的语法要求,另一类是团队为了让接口稳定、可预测而制定的命名约定。框架不会阻止你写出 /getUserData,但这不代表它适合作为长期维护的 API。

前缀与斜杠

最终注册到 FastAPI 应用的路径以 / 开头。APIRouter 的 prefix 同样以 / 开头,但末尾不写 /,避免与子路径拼接出重复斜杠:

route-prefix.py
01
from fastapi import APIRouter
02
03
router = APIRouter(prefix="/conversations")
04
05
@router.get("")
06
async def list_conversations():
07
return []
08
09
@router.get("/{conversation_id}")
10
async def get_conversation(conversation_id: str):
11
return {"id": conversation_id}

上面两个最终路径分别是 /conversations 和 /conversations/{conversation_id}。集合接口使用空的子路径,是为了遵守本文统一不保留末尾斜杠的约定。

/users 与 /users/ 在路由系统中是两个不同路径。默认情况下,只注册其中一个时,FastAPI 通常会把另一种形式重定向到已注册路径。重定向意味着客户端会多发送一次请求,而且某些代理、签名请求或不自动跟随重定向的客户端可能产生不同表现。项目应选定一种风格并保持一致,不要把自动重定向当作路径设计的一部分。

资源命名

业务 API 通常使用小写英文名词表示资源,集合使用复数形式。HTTP 方法已经表达了读取、创建、修改和删除,因此路径中一般不再重复动作动词:

推荐不推荐原因
GET /conversationsGET /getConversationsGET 已经表达读取
POST /conversationsPOST /createConversationPOST 与集合路径共同表达创建
DELETE /conversations/{id}POST /deleteConversation/{id}应使用对应 HTTP 方法
POST /conversations/{id}/messagesGET /sendMessage发送消息会创建状态,不应使用 GET

路径名称不是数据库表名的简单复制。它描述的是对外公开的资源模型,因此应该稳定、易读,并与业务语言一致。多个单词可以使用连字符,例如 /model-providers;不要在同一套 API 中混用 camelCase、下划线和连字符。

层级与查询

路径层级适合表达明确的归属关系,例如 /conversations/{conversation_id}/messages 表示消息属于某个会话。但层级不是越深越准确。如果消息 ID 已经可以唯一定位资源,读取详情时可以直接使用 /messages/{message_id},不必要求客户端重复提供整条父级路径。

筛选、排序和分页不改变资源身份,更适合使用查询参数:

route-query.txt
1
GET /api/v1/conversations?status=active&limit=20&offset=0

不要为每种筛选条件继续创建 /active-conversations、/recent-conversations 等大量近似路径。只有当它们代表真正不同的资源或业务能力时,才值得设计独立路径。

版本边界

/api/v1 通常放在聚合路由的上层前缀中,而不是由每个业务文件重复书写。只有发生不兼容的接口契约变化时才需要新的大版本;增加可选字段或新增接口通常不需要立刻创建 /v2。健康检查等面向部署平台的系统接口可以保持为 /health,不一定纳入业务 API 版本。

特殊字符与敏感信息

路径中的空格、中文和特殊字符会经过 URL 编码。资源 ID 最好使用稳定、适合 URL 的值,例如整数、UUID 或短字符串;不要把整段提示词、文件路径或敏感数据直接塞进路径。

普通的 {value} 只匹配一个路径片段。如果确实需要接收包含 / 的文件路径,可以使用 {file_path:path},但它会扩大匹配范围,而且 OpenAPI 无法完整描述“参数内部还包含路径”的语义,应谨慎使用并放在更具体路由之后。

一个容易被忽略的边界是:路径不应该包含访问凭证。URL 可能出现在浏览器历史、服务器日志、代理日志和分析系统中,API Key 应放在请求头。

9. 状态码

创建资源时,可以在装饰器中声明成功状态码:

route-status.py
01
from fastapi import FastAPI, status
02
03
app = FastAPI()
04
05
@app.post(
06
"/conversations",
07
status_code=status.HTTP_201_CREATED,
08
)
09
async def create_conversation():
10
return {"id": "conversation-1"}

使用 status.HTTP_201_CREATED 比直接写 201 更容易读懂。常见状态码包括:

状态码含义
200请求成功
201资源创建成功
202请求已接受,工作尚未完成
204成功但没有响应体
400请求语义或格式不符合业务要求
401没有有效身份凭证
403已识别身份,但没有权限
404资源不存在
409当前状态与操作发生冲突
422请求数据无法通过校验

状态码是接口契约的一部分。不要让所有失败都返回 200,再把错误藏在 JSON 字段中。

10. 元数据

路径装饰器除了声明方法和 URL,还可以描述这条接口在 OpenAPI 中应该如何展示。Swagger UI、ReDoc、接口测试工具和客户端代码生成器都会读取这些信息,因此元数据也是接口契约的一部分。

先来看一个较完整的配置:

route-metadata.py
01
from fastapi import FastAPI, HTTPException
02
from pydantic import BaseModel
03
04
app = FastAPI()
05
06
class ErrorResponse(BaseModel):
07
detail: str
08
09
@app.get(
10
"/conversations/{conversation_id}",
11
tags=["conversations"],
12
summary="查询会话",
13
response_description="会话详情",
14
operation_id="get_conversation",
15
responses={
16
404: {
17
"model": ErrorResponse,
18
"description": "会话不存在",
19
},
20
},
21
)
22
async def get_conversation(conversation_id: str):
23
"""
24
根据会话 ID 返回会话详情。
25
26
- 包含会话的基础信息
27
- 不返回已经删除的会话
28
"""
29
if conversation_id != "conversation-1":
30
raise HTTPException(status_code=404, detail="会话不存在")
31
32
return {"id": conversation_id, "title": "第一次对话"}

这些配置各自负责不同的信息:

配置作用
tags在文档中对接口分组,可以在 APIRouter 上统一设置
summary接口列表中显示的简短标题
description 或函数文档字符串解释接口行为,支持 Markdown
response_description描述主要成功响应;不填写时 FastAPI 会生成默认说明
responses在 OpenAPI 中声明额外状态码、模型和媒体类型
operation_idOpenAPI 中这次操作的唯一标识,客户端生成工具可能用它生成方法名

response_description 描述的是响应结果,函数文档字符串描述的是整个接口行为,两者不要混为一段重复文字。简短信息放在 summary,参数含义、权限前提和边界行为可以写进文档字符串。说明应该补充接口契约,不能用长篇描述掩盖模糊的路径和字段命名。

额外响应只是声明

responses={404: ...} 会把 404 响应模型加入 OpenAPI,但它不会自动判断会话是否存在,也不会自动抛出 404。示例中的 HTTPException 才是真正的运行时行为。文档声明和代码实现必须保持一致,否则调用方会看到一份无法兑现的接口契约。

稳定的 operationId

函数名不会决定 URL,但默认会成为路径操作名称的一部分,并参与 OpenAPI operationId 的生成。生成客户端代码时,稳定且不重复的函数名会更容易维护;确实需要固定标识时,也可以在装饰器中显式设置 operation_id。

手动设置的 operation_id 必须在整个应用中唯一。修改它不会影响服务端 URL,却可能让自动生成的前端或 Python 客户端出现方法重命名,因此已经对外使用后应把它当成接口契约维护。没有客户端生成需求时,可以继续使用 FastAPI 的默认生成规则,不必为每个接口手工命名。

弃用与隐藏

旧接口需要保留一段迁移时间时,可以使用 deprecated=True:

deprecated-route.py
1
@app.get("/v1/conversations", deprecated=True)
2
async def list_legacy_conversations():
3
return []

它只会在 OpenAPI 文档中把接口标记为已弃用,路由仍然可以正常调用。真正下线接口还需要迁移计划、调用量观察和最终删除,不能只增加一个标记。

如果某个内部接口不希望出现在 OpenAPI 中,可以设置 include_in_schema=False:

hidden-route.py
1
@app.get("/internal/ping", include_in_schema=False)
2
async def internal_ping():
3
return {"status": "ok"}

隐藏文档不等于保护接口。知道 URL 的客户端仍然能够访问它,身份认证和权限控制必须通过依赖或中间件实现。

标签说明

当标签越来越多时,可以在创建应用时集中补充标签说明和显示顺序:

tag-metadata.py
01
tags_metadata = [
02
{
03
"name": "conversations",
04
"description": "管理会话与会话中的消息。",
05
},
06
{
07
"name": "memories",
08
"description": "查询和管理用户的长期记忆。",
09
},
10
]
11
12
app = FastAPI(openapi_tags=tags_metadata)

openapi_tags 中的排列顺序也会影响标签在文档中的显示顺序。业务路由仍然通过 tags=["conversations"] 引用这些标签;规模较大时还可以使用 Enum 统一标签名称,避免不同文件出现拼写不一致。

openapi_extra 可以继续扩展底层 OpenAPI Operation Object,但它属于较低层的扩展入口。普通项目优先使用 tags、summary、responses 等明确参数,只有对接自定义 OpenAPI 扩展或特殊工具时才需要操作 openapi_extra。

11. 拆分

前面的案例都直接使用 app.get()、app.post() 声明路由,这种写法适合学习语法,却不适合承载不断增长的业务。假设项目同时包含会话、长期记忆、用户和健康检查,把它们全部写进 main.py,入口文件很快就会变成一个难以维护的大文件。

实际项目通常不会按照 GET、POST 这样的 HTTP 方法拆文件,而是按照业务领域拆分。例如,会话相关接口放在 conversations.py,记忆相关接口放在 memories.py。每个业务模块创建自己的 APIRouter,再由上层路由逐级聚合。

一个适合当前本地 AI 助手的起步结构如下:

__init__.py声明 routes 为 Python 包
health.py健康检查
conversations.py会话与消息
memories.py长期记忆
__init__.py声明 api 为 Python 包
router.py聚合业务路由和版本前缀
__init__.py声明 app 为 Python 包
main.py创建并导出 FastAPI 应用
pyproject.toml项目与依赖配置

routes 中的每个文件只声明当前领域的路径。以会话业务为例:

app/api/routes/conversations.py
01
from fastapi import APIRouter, status
02
03
router = APIRouter(
04
prefix="/conversations",
05
tags=["conversations"],
06
)
07
08
@router.get("")
09
async def list_conversations():
10
return []
11
12
@router.post("", status_code=status.HTTP_201_CREATED)
13
async def create_conversation():
14
return {"id": "conversation-1"}
15
16
@router.post("/{conversation_id}/messages")
17
async def create_message(conversation_id: str):
18
return {
19
"conversation_id": conversation_id,
20
"message_id": "message-1",
21
}

这里使用的是 router.get(),而不是 app.get()。可以把 APIRouter 理解成一组尚未接入主应用的路径操作。prefix="/conversations" 会应用到当前模块的全部接口,tags 则让这些接口在 OpenAPI 文档中归到同一组。

健康检查和长期记忆也分别拥有自己的路由:

app/api/routes/health.py
1
from fastapi import APIRouter
2
3
router = APIRouter(tags=["system"])
4
5
@router.get("/health")
6
async def health():
7
return {"status": "ok"}
app/api/routes/memories.py
01
from fastapi import APIRouter
02
03
router = APIRouter(
04
prefix="/memories",
05
tags=["memories"],
06
)
07
08
@router.get("")
09
async def list_memories(q: str | None = None):
10
return {"query": q, "items": []}

这样拆分后,业务文件只关心自己的相对路径,不需要每个接口都重复写 /api/v1。版本前缀和不同业务路由由 app/api/router.py 统一聚合:

app/api/router.py
01
from fastapi import APIRouter
02
03
from app.api.routes import conversations, health, memories
04
05
api_v1_router = APIRouter(prefix="/api/v1")
06
api_v1_router.include_router(conversations.router)
07
api_v1_router.include_router(memories.router)
08
09
router = APIRouter()
10
router.include_router(health.router)
11
router.include_router(api_v1_router)

这里有两层聚合。api_v1_router 负责所有需要版本管理的业务接口,最外层的 router 同时接入不需要版本号的健康检查。于是最终路径分别是:

路由模块模块前缀上层前缀最终路径示例
health.py无无/health
conversations.py/conversations/api/v1/api/v1/conversations
memories.py/memories/api/v1/api/v1/memories

最后,main.py 只创建应用并接入聚合后的总路由:

app/main.py
1
from fastapi import FastAPI
2
3
from app.api.router import router as api_router
4
5
app = FastAPI(title="Local Agent API")
6
app.include_router(api_router)

main.py 不需要知道每个业务有哪些具体接口。以后新增用户业务时,只需要创建 app/api/routes/users.py,再在 app/api/router.py 中调用一次 include_router(),应用入口不必跟着堆积业务细节。

这几层的职责可以这样区分:

层级职责
路径操作处理某个具体的 HTTP 方法和相对路径
业务 APIRouter组织同一领域的路径、标签和公共依赖
聚合 APIRouter组合多个业务路由,统一版本前缀
main.py创建 FastAPI 应用并接入总路由

include_router() 不是启动一个新的子服务,而是把路由中的路径操作纳入同一个 FastAPI 应用和 OpenAPI 文档。路由文件也不应该继续膨胀为新的 main.py:数据库访问、LangChain 调用和复杂业务判断可以交给后续的服务层,路由函数主要负责接收请求、调用业务逻辑和组织响应。

12. 设计

我们可以为本地项目设计一组路由:

routes.txt
1
GET /health
2
GET /api/v1/conversations
3
POST /api/v1/conversations
4
GET /api/v1/conversations/{conversation_id}
5
PATCH /api/v1/conversations/{conversation_id}
6
DELETE /api/v1/conversations/{conversation_id}
7
POST /api/v1/conversations/{conversation_id}/messages

这里使用名词复数表示资源集合,用路径层级表达消息属于某个会话。/api/v1 为未来不兼容升级预留版本边界,但不要因为用了版本号就随意破坏现有契约。

发送消息使用 POST,因为它会创建消息并触发模型调用。不要设计成类似 /getAnswer 的 GET 接口:模型请求通常包含请求体,也会产生新状态,不符合 GET 的语义。

正在验证登录状态
请稍候,验证完成后将继续显示文章内容