1. 路由
路由解决的是「某个 HTTP 请求应该由哪个函数处理」。FastAPI 使用路径和 HTTP 方法共同匹配请求:
01from fastapi import FastAPI0203app = FastAPI()0405@app.get("/conversations")06async def list_conversations():07return []0809@app.post("/conversations")10async def create_conversation():11return {"id": "conversation-1"}
两个接口路径相同,但方法不同,所以是两个独立的路径操作。浏览器地址栏通常发送 GET,而创建数据一般使用 POST。
2. 方法
常见 HTTP 方法可以先这样理解:
| 方法 | 常见语义 | 示例 |
|---|---|---|
| GET | 读取资源,不应改变业务状态 | 查询会话 |
| POST | 创建资源或触发一次处理 | 创建会话、发送消息 |
| PUT | 完整替换指定资源 | 替换会话设置 |
| PATCH | 修改资源的部分字段 | 修改会话标题 |
| DELETE | 删除资源 | 删除会话 |
这些不是纯粹的命名偏好。浏览器、代理、缓存和接口调用方都会根据 HTTP 语义处理请求。
GET 应该是安全方法,也就是读取时不产生用户可见的业务修改。PUT 和 DELETE 通常应该具备幂等性:同一个请求执行多次,最终状态与执行一次相同。POST 通常不保证幂等,支付或任务创建等接口需要额外设计幂等键。
3. 路径参数
路径中的花括号声明动态参数:
1from fastapi import FastAPI23app = FastAPI()45@app.get("/conversations/{conversation_id}")6async def get_conversation(conversation_id: str):7return {"id": conversation_id}
请求 /conversations/abc 时,conversation_id 的值是 "abc"。
类型提示会参与转换和校验:
1@app.get("/messages/{message_id}")2async def get_message(message_id: int):3return {"id": message_id}
请求 /messages/42 时,函数收到整数 42,不是字符串。请求 /messages/hello 时,FastAPI 无法转换,会直接返回校验错误。
路径参数是 URL 的组成部分,适合表达「要操作哪个资源」。如果一个值只是筛选条件,不决定资源身份,就更适合放在查询参数中。
4. 枚举
有些路径参数只能从有限选项中选择,例如模型供应商。此时可以使用 Python 的 Enum,不要接收任意字符串后再手动写一串判断:
01from enum import Enum0203from fastapi import FastAPI0405app = FastAPI()0607class Provider(str, Enum):08deepseek = "deepseek"09openai = "openai"1011@app.get("/providers/{provider}")12async def get_provider(provider: Provider):13return {"provider": provider}
请求 /providers/deepseek 时,函数收到的是 Provider.deepseek;传入未声明的值则会得到校验错误。可选值还会进入 OpenAPI,交互文档可以直接显示选项。
继承 str 可以让枚举值按字符串参与 JSON 序列化。对仅用于类型检查的简单字符串选项,也可以使用 Literal;需要在业务代码中反复比较、复用时,Enum 通常更清楚。
5. 查询参数
路径函数中没有出现在 URL 路径里的简单参数,通常会被识别为查询参数:
1@app.get("/conversations")2async def list_conversations(3offset: int = 0,4limit: int = 20,5):6return {"offset": offset, "limit": limit}
对应请求:
1GET /conversations?offset=20&limit=10
带默认值的参数是可选参数。不提供时,函数使用默认值。没有默认值的查询参数是必填参数:
1@app.get("/search")2async def search(q: str):3return {"query": q}
查询参数是 URL 中 ? 后面的键值。它们原本都是文本,FastAPI 会根据类型提示完成转换。例如 ?limit=10 会得到整数 10,?enabled=true 会得到布尔值 True。转换失败时,路径函数不会执行。
6. Annotated
前面的函数只使用了 q: str、limit: int 这样的类型提示。它们可以告诉 FastAPI 参数应该转换成什么类型,却没有说明参数来自路径、查询字符串还是请求体,也无法表达长度和数值范围。
Annotated 用来在保留原始类型的同时,给这个类型附加额外信息。它的基本语法是:
1Annotated[原始类型, 元数据1, 元数据2, ...]
Annotated 从 Python 3.9 开始可以直接从 typing 导入。更早的 Python 项目可能会从 typing_extensions 导入,但本小册使用的新版本 Python 直接写 from typing import Annotated 即可。
第一个位置必须是类型,后面可以放一项或多项元数据。例如:
1limit: Annotated[int, Query(ge=1, le=100)] = 20
这行代码包含四层信息:
| 部分 | 含义 |
|---|---|
limit | Python 函数参数名,也是默认的查询参数名 |
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 校验机制。
现在回到一个完整的路由:
01from typing import Annotated0203from fastapi import FastAPI, Path, Query0405app = FastAPI()0607@app.get("/users/{user_id}/messages")08async def list_messages(09user_id: Annotated[int, Path(gt=0)],10q: Annotated[str | None, Query(max_length=50)] = None,11limit: Annotated[int, Query(ge=1, le=100)] = 20,12):13return {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():
1# 推荐:默认值只有一个来源2limit: Annotated[int, Query(ge=1, le=100)] = 2034# 错误:FastAPI 无法判断应该使用 10 还是 205limit: Annotated[int, Query(default=10, ge=1, le=100)] = 20
第二种写法会产生冲突,因此 FastAPI 不允许在 Annotated 内的 Query() 中再声明 default。默认值放在等号右边还有一个好处:即使这个函数脱离 FastAPI 被普通 Python 代码调用,它仍然遵循正常的函数默认值规则。
你可能会在旧项目中看到下面这种写法:
1limit: int = Query(default=20, ge=1, le=100)
这种语法仍然可以使用,但当前 FastAPI 更推荐 Annotated。旧写法把 FastAPI 的 Query 对象占用了 Python 默认值的位置;新写法则把 int、FastAPI 元数据和真实默认值分别放在各自的位置,编辑器和类型检查器也更容易理解。
必填、可选与可空
参数是否必填,主要取决于有没有默认值;类型中是否包含 None,表示允许的值中是否包含空值。这是两个不同的问题:
1# 必填:客户端必须提供字符串2q: Annotated[str, Query(min_length=1)]34# 可选:不提供时使用 "all"5q: Annotated[str, Query(min_length=1)] = "all"67# 可选且允许为空:不提供时得到 None8q: Annotated[str | None, Query(max_length=50)] = None
因此,只有 str | None 并不会自动让参数变成可选参数。真正让客户端可以省略它的是 = None 或其他默认值。路径参数则是 URL 结构的一部分,不能通过默认值变成可选;如果路径模板包含 {user_id},请求中就必须出现对应的路径片段。
复用约束
当多个接口使用相同规则时,可以把完整的 Annotated 声明保存成类型别名:
01from typing import Annotated0203from fastapi import FastAPI, Query0405app = FastAPI()0607PageSize = Annotated[08int,09Query(ge=1, le=100, description="每页返回数量"),10]1112@app.get("/conversations")13async def list_conversations(limit: PageSize = 20):14return {"limit": limit}1516@app.get("/memories")17async def list_memories(limit: PageSize = 20):18return {"limit": limit}
PageSize 对类型检查器来说仍然是 int,但 FastAPI 还能从中读取查询参数约束。这样可以让分页规则在多个路由中保持一致。类型别名适合复用真正相同的接口规则;如果两个参数只是碰巧都叫 limit,但业务范围不同,就不应该为了减少几行代码而强行共用。
除了 Path() 和 Query(),后续还会看到 Body()、Header()、Cookie()、File()、Form() 和 Depends() 出现在 Annotated 中。它们使用的是同一种思路:第一个位置描述 Python 值的类型,后面的元数据告诉 FastAPI 应该从哪里取得这个值,以及应该如何处理它。
7. 顺序
FastAPI 底层使用 Starlette 的路由系统。收到请求后,路由器会按照路径操作的注册顺序逐个尝试匹配,找到第一个同时符合 HTTP 方法和路径模式的路由后,就进入这个路由的参数解析和处理流程。
它不会先把所有路由收集起来,再自动判断哪一个更具体。因此,静态路径要放在可能覆盖它的动态路径之前:
1@app.get("/users/me")2async def get_current_user():3return {"id": "current"}45@app.get("/users/{user_id}")6async def get_user(user_id: str):7return {"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} 可以匹配包含 / 的剩余路径,覆盖范围比普通路径参数更大:
1@app.get("/files/public")2async def get_public_file():3return {"file": "public"}45@app.get("/files/{file_path:path}")6async def get_file(file_path: str):7return {"file": file_path}
/files/public 必须先声明,否则后面的通配路由会把 public 当成 file_path。同理,固定路径应放在普通动态路径之前,普通动态路径再放在能吞掉多段 URL 的通配路径之前。
聚合路由也有顺序
使用 include_router() 拆分文件后,路由仍然会按照被接入应用的顺序参与匹配。如果两个 APIRouter 最终生成了可能重叠的路径,更具体的路由应该先接入。不过更稳妥的做法,是为不同业务分配清晰且不重叠的前缀,例如 /conversations 和 /memories,不要长期依赖导入顺序解决业务边界冲突。
不要把类型当成路由重载
同一个路径和方法也不要重复声明。即使 Python 允许定义两个不同函数,客户端也无法可靠选择应该执行哪一个。
路由匹配只看 HTTP 方法和 URL 路径,不会根据函数参数的 Python 类型选择重载。下面这种思路无法区分两个接口:
1@app.get("/users/{user_id}")2async def get_user_by_id(user_id: int):3return {"id": user_id}45@app.get("/users/{username}")6async def get_user_by_name(username: str):7return {"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 同样以 / 开头,但末尾不写 /,避免与子路径拼接出重复斜杠:
01from fastapi import APIRouter0203router = APIRouter(prefix="/conversations")0405@router.get("")06async def list_conversations():07return []0809@router.get("/{conversation_id}")10async def get_conversation(conversation_id: str):11return {"id": conversation_id}
上面两个最终路径分别是 /conversations 和 /conversations/{conversation_id}。集合接口使用空的子路径,是为了遵守本文统一不保留末尾斜杠的约定。
/users 与 /users/ 在路由系统中是两个不同路径。默认情况下,只注册其中一个时,FastAPI 通常会把另一种形式重定向到已注册路径。重定向意味着客户端会多发送一次请求,而且某些代理、签名请求或不自动跟随重定向的客户端可能产生不同表现。项目应选定一种风格并保持一致,不要把自动重定向当作路径设计的一部分。
资源命名
业务 API 通常使用小写英文名词表示资源,集合使用复数形式。HTTP 方法已经表达了读取、创建、修改和删除,因此路径中一般不再重复动作动词:
| 推荐 | 不推荐 | 原因 |
|---|---|---|
GET /conversations | GET /getConversations | GET 已经表达读取 |
POST /conversations | POST /createConversation | POST 与集合路径共同表达创建 |
DELETE /conversations/{id} | POST /deleteConversation/{id} | 应使用对应 HTTP 方法 |
POST /conversations/{id}/messages | GET /sendMessage | 发送消息会创建状态,不应使用 GET |
路径名称不是数据库表名的简单复制。它描述的是对外公开的资源模型,因此应该稳定、易读,并与业务语言一致。多个单词可以使用连字符,例如 /model-providers;不要在同一套 API 中混用 camelCase、下划线和连字符。
层级与查询
路径层级适合表达明确的归属关系,例如 /conversations/{conversation_id}/messages 表示消息属于某个会话。但层级不是越深越准确。如果消息 ID 已经可以唯一定位资源,读取详情时可以直接使用 /messages/{message_id},不必要求客户端重复提供整条父级路径。
筛选、排序和分页不改变资源身份,更适合使用查询参数:
1GET /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. 状态码
创建资源时,可以在装饰器中声明成功状态码:
01from fastapi import FastAPI, status0203app = FastAPI()0405@app.post(06"/conversations",07status_code=status.HTTP_201_CREATED,08)09async def create_conversation():10return {"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、接口测试工具和客户端代码生成器都会读取这些信息,因此元数据也是接口契约的一部分。
先来看一个较完整的配置:
01from fastapi import FastAPI, HTTPException02from pydantic import BaseModel0304app = FastAPI()0506class ErrorResponse(BaseModel):07detail: str0809@app.get(10"/conversations/{conversation_id}",11tags=["conversations"],12summary="查询会话",13response_description="会话详情",14operation_id="get_conversation",15responses={16404: {17"model": ErrorResponse,18"description": "会话不存在",19},20},21)22async def get_conversation(conversation_id: str):23"""24根据会话 ID 返回会话详情。2526- 包含会话的基础信息27- 不返回已经删除的会话28"""29if conversation_id != "conversation-1":30raise HTTPException(status_code=404, detail="会话不存在")3132return {"id": conversation_id, "title": "第一次对话"}
这些配置各自负责不同的信息:
| 配置 | 作用 |
|---|---|
tags | 在文档中对接口分组,可以在 APIRouter 上统一设置 |
summary | 接口列表中显示的简短标题 |
description 或函数文档字符串 | 解释接口行为,支持 Markdown |
response_description | 描述主要成功响应;不填写时 FastAPI 会生成默认说明 |
responses | 在 OpenAPI 中声明额外状态码、模型和媒体类型 |
operation_id | OpenAPI 中这次操作的唯一标识,客户端生成工具可能用它生成方法名 |
response_description 描述的是响应结果,函数文档字符串描述的是整个接口行为,两者不要混为一段重复文字。简短信息放在 summary,参数含义、权限前提和边界行为可以写进文档字符串。说明应该补充接口契约,不能用长篇描述掩盖模糊的路径和字段命名。
额外响应只是声明
responses={404: ...} 会把 404 响应模型加入 OpenAPI,但它不会自动判断会话是否存在,也不会自动抛出 404。示例中的 HTTPException 才是真正的运行时行为。文档声明和代码实现必须保持一致,否则调用方会看到一份无法兑现的接口契约。
稳定的 operationId
函数名不会决定 URL,但默认会成为路径操作名称的一部分,并参与 OpenAPI operationId 的生成。生成客户端代码时,稳定且不重复的函数名会更容易维护;确实需要固定标识时,也可以在装饰器中显式设置 operation_id。
手动设置的 operation_id 必须在整个应用中唯一。修改它不会影响服务端 URL,却可能让自动生成的前端或 Python 客户端出现方法重命名,因此已经对外使用后应把它当成接口契约维护。没有客户端生成需求时,可以继续使用 FastAPI 的默认生成规则,不必为每个接口手工命名。
弃用与隐藏
旧接口需要保留一段迁移时间时,可以使用 deprecated=True:
1@app.get("/v1/conversations", deprecated=True)2async def list_legacy_conversations():3return []
它只会在 OpenAPI 文档中把接口标记为已弃用,路由仍然可以正常调用。真正下线接口还需要迁移计划、调用量观察和最终删除,不能只增加一个标记。
如果某个内部接口不希望出现在 OpenAPI 中,可以设置 include_in_schema=False:
1@app.get("/internal/ping", include_in_schema=False)2async def internal_ping():3return {"status": "ok"}
隐藏文档不等于保护接口。知道 URL 的客户端仍然能够访问它,身份认证和权限控制必须通过依赖或中间件实现。
标签说明
当标签越来越多时,可以在创建应用时集中补充标签说明和显示顺序:
01tags_metadata = [02{03"name": "conversations",04"description": "管理会话与会话中的消息。",05},06{07"name": "memories",08"description": "查询和管理用户的长期记忆。",09},10]1112app = 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 助手的起步结构如下:
routes 中的每个文件只声明当前领域的路径。以会话业务为例:
01from fastapi import APIRouter, status0203router = APIRouter(04prefix="/conversations",05tags=["conversations"],06)0708@router.get("")09async def list_conversations():10return []1112@router.post("", status_code=status.HTTP_201_CREATED)13async def create_conversation():14return {"id": "conversation-1"}1516@router.post("/{conversation_id}/messages")17async def create_message(conversation_id: str):18return {19"conversation_id": conversation_id,20"message_id": "message-1",21}
这里使用的是 router.get(),而不是 app.get()。可以把 APIRouter 理解成一组尚未接入主应用的路径操作。prefix="/conversations" 会应用到当前模块的全部接口,tags 则让这些接口在 OpenAPI 文档中归到同一组。
健康检查和长期记忆也分别拥有自己的路由:
1from fastapi import APIRouter23router = APIRouter(tags=["system"])45@router.get("/health")6async def health():7return {"status": "ok"}
01from fastapi import APIRouter0203router = APIRouter(04prefix="/memories",05tags=["memories"],06)0708@router.get("")09async def list_memories(q: str | None = None):10return {"query": q, "items": []}
这样拆分后,业务文件只关心自己的相对路径,不需要每个接口都重复写 /api/v1。版本前缀和不同业务路由由 app/api/router.py 统一聚合:
01from fastapi import APIRouter0203from app.api.routes import conversations, health, memories0405api_v1_router = APIRouter(prefix="/api/v1")06api_v1_router.include_router(conversations.router)07api_v1_router.include_router(memories.router)0809router = APIRouter()10router.include_router(health.router)11router.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 只创建应用并接入聚合后的总路由:
1from fastapi import FastAPI23from app.api.router import router as api_router45app = FastAPI(title="Local Agent API")6app.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. 设计
我们可以为本地项目设计一组路由:
1GET /health2GET /api/v1/conversations3POST /api/v1/conversations4GET /api/v1/conversations/{conversation_id}5PATCH /api/v1/conversations/{conversation_id}6DELETE /api/v1/conversations/{conversation_id}7POST /api/v1/conversations/{conversation_id}/messages
这里使用名词复数表示资源集合,用路径层级表达消息属于某个会话。/api/v1 为未来不兼容升级预留版本边界,但不要因为用了版本号就随意破坏现有契约。
发送消息使用 POST,因为它会创建消息并触发模型调用。不要设计成类似 /getAnswer 的 GET 接口:模型请求通常包含请求体,也会产生新状态,不符合 GET 的语义。