486 lines
25 KiB
Markdown
Executable File
486 lines
25 KiB
Markdown
Executable File
# 12 · Python 开发规范
|
||
|
||
> 适用:本学习项目(FastAPI Agent 服务骨架 + 各阶段离线 Demo)
|
||
> 目的:统一目录、文件、接口、工具函数与各层抽象的命名与组织方式,让代码层次清晰、可读、可持续演进。
|
||
> 约定本规范**面向本项目当前真实结构编写**,不做超出实际需要的理论堆砌。
|
||
|
||
## 1. 项目根布局
|
||
|
||
```
|
||
/workspace
|
||
├─ app/ # FastAPI 服务骨架(阶段七部署到 API 的核心)
|
||
│ ├─ main.py # 应用实例 + 生命周期 + 全局异常处理器
|
||
│ ├─ config.py # pydantic-settings 配置(读取 .env)
|
||
│ ├─ schemas/ # 请求/响应 Pydantic 模型(common.py 统一响应 + 按域拆分)
|
||
│ ├─ dependencies.py # 可复用依赖(Depends:会话 / 鉴权 / 配置)
|
||
│ ├─ api/ # 路由(routers),按资源拆分
|
||
│ ├─ agents/ # Agent 编排层(阶段七扩展)
|
||
│ ├─ tools/ # 工具服务层(LLM / 检索 / 计算)
|
||
│ └─ storage/ # 存储层(Redis / PG / Qdrant)
|
||
├─ code/ # 各阶段离线可运行 Demo(纯标准库优先)
|
||
├─ docs/ # 学习文档(阶段一 ~ 八 + 配套)
|
||
├─ requirements.txt # 服务侧依赖清单
|
||
├─ .env.example # 环境变量模板(.env 不入库)
|
||
└─ .gitignore # 忽略 .env / __pycache__ 等
|
||
```
|
||
|
||
- 三类代码按「**能否被 API 调用**」区分:`app/` 是服务,`code/` 是脚本型 Demo,`docs/` 非代码。
|
||
|
||
- 新增可复用的非服务工具放 `app/tools/`(详见第 5 节),新增教学示例放对应 `code/阶段N/`。
|
||
|
||
- **FastAPI 目录对齐官方 Bigger Applications 惯例**:路由按资源拆到 `api/` 各文件、`dependencies.py` 集中放 `Depends` 依赖、`schemas/` 目录放模型 [$TRAE\_REF](https://fastapi.tiangolo.com/tutorial/bigger-applications/#apirouter)。
|
||
|
||
## 2. app/ 分层架构与各层抽象
|
||
|
||
`app/` 采用**四层**抽象,每个模块只允许依赖其下层,禁止跨层引用:
|
||
|
||
```
|
||
API 接入层 app/api/ 路由 + 请求响应对象(HTTP / SSE)
|
||
↓
|
||
Agent 编排层 app/agents/ 规划 / 记忆 / 工具调用编排(LangGraph 等)
|
||
↓
|
||
工具服务层 app/tools/ 可复用工具函数(llm / 检索 / 计算 ...)
|
||
↓
|
||
存储层 app/storage/ Redis / PG / Qdrant 访问封装
|
||
```
|
||
|
||
```mermaid
|
||
graph TD
|
||
A[API接入层 app/api] --> B[Agent编排层 app/agents]
|
||
B --> C[工具服务层 app/tools]
|
||
C --> D[存储层 app/storage]
|
||
```
|
||
|
||
当前骨架已具备 `app/main.py`、`app/api/routes.py`、`app/schemas/`;`app/agents` / `app/tools` / `app/storage` 按需新增(阶段七扩展时落地)。
|
||
|
||
### 2.1 分层职责与引用规则
|
||
|
||
| 层 | 目录 | 职责 | 允许引用 |
|
||
| ----- | -------------- | ------------------------ | ----------- |
|
||
| 接入层 | `app/api/` | 路由、入参校验、响应序列化、流式返回 | 编排层、schemas |
|
||
| 编排层 | `app/agents/` | 状态机、规划、记忆、工具调用循环 | 工具服务层、存储层 |
|
||
| 工具服务层 | `app/tools/` | 一次性的可复用能力(LLM 封装、检索、计算) | 存储层 |
|
||
| 存储层 | `app/storage/` | Redis / PG / Qdrant 读写封装 | 仅驱动与配置 |
|
||
|
||
- 接入层**不得直接触碰存储层**;编排层**不得写 HTTP 路由**。
|
||
|
||
- 依赖方向永远是单向的,出现「下层 import 上层」即视为架构违规。
|
||
|
||
## 3. 文件命名规范
|
||
|
||
| 类别 | 规则 | 示例 |
|
||
| -------------- | ----------------------------------------- | ----------------------------- |
|
||
| 模块文件 | 小写下划线 `snake_case.py` | `routes.py` / `llm_client.py` |
|
||
| 包目录 | 小写下划线,含 `__init__.py` | `app/api/` / `app/tools/` |
|
||
| 教学 Demo | 语义化小写,一个文件一个主题 | `react_agent.py` |
|
||
| 数据模型 | `schemas/` 目录按域拆分,`__init__.py` 统一导出 | `app/schemas/chat.py` |
|
||
| 可复用依赖 | `dependencies.py`(放 `Depends` 函数) | `dependencies.py` |
|
||
| 常量/配置 | `config.py`(pydantic-settings `Settings`) | `config.py` |
|
||
| 命名 `.markdown` | 中文语义化 + 阶段编号 | `docs/07-…/01-Agent工程化…md` |
|
||
|
||
- 文件名只做一件事:一眼看出「这是什么」「属于哪层/哪阶段」。
|
||
|
||
- `code/` 下避免用 `test` / `utils` / `common` 这类无意义泛名。
|
||
|
||
## 4. 接口(API)规范
|
||
|
||
### 4.1 通用约定
|
||
|
||
- 路由集中注册:`app/api/` 下 `APIRouter(prefix=..., tags=[...])`,统一在 `main.py` `include_router`。
|
||
|
||
- 请求/响应结构一律用 Pydantic 模型声明(`schemas/` 目录),由 FastAPI 自动校验与生成文档。
|
||
|
||
- 健康检查统一 `GET /health`(`main.py` 内),供 K8s probe 使用,不入业务路由。
|
||
|
||
- **`tags`** **必填**:路由按 `tags` 分组,Swagger 文档据此归类,禁止无 `tags` 裸挂。
|
||
|
||
- **显式** **`status_code`**:每个端点声明响应码,避免默认 200 掩盖语义(创建用 201、删除用 204 等)。
|
||
|
||
```python
|
||
# app/main.py
|
||
from contextlib import asynccontextmanager
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(app: FastAPI): # 生命周期:启动/关闭时的资源管理
|
||
# 启动:连库 / 加载模型 / 初始化配置
|
||
yield
|
||
# 关闭:释放连接 / 落盘状态
|
||
|
||
app = FastAPI(title="Python AI Agent 学习项目", version="0.1.0", lifespan=lifespan)
|
||
app.include_router(router) # 挂载 /agent 路由
|
||
app.add_exception_handler(CustomError, custom_handler) # 全局异常处理器
|
||
|
||
@app.get("/health", tags=["system"])
|
||
async def health() -> dict:
|
||
return {"status": "ok"}
|
||
```
|
||
|
||
### 4.2 REST 语义
|
||
|
||
| 场景 | 方法 / 路径 | `status_code` | 说明 |
|
||
| -- | ------------------------------- | ------------- | ---------------------------------- |
|
||
| 对话 | `POST /agent/chat` | 200 | 请求 `ChatRequest`,返回 `ChatResponse` |
|
||
| 创建 | `POST /agent/tasks` | 201 | 创建长任务,返回任务 ID |
|
||
| 查询 | `GET /agent/tasks/{id}` | 200 | 幂等查询,无副作用 |
|
||
| 状态 | `GET /agent/tasks/{id}/status` | 200 | 查询长任务进度 |
|
||
| 取消 | `POST /agent/tasks/{id}/cancel` | 202 | 接受取消请求,异步生效 |
|
||
| 删除 | `DELETE /agent/tasks/{id}` | 204 | 无返回体 |
|
||
|
||
- 动词进 URL 视为反模式:用 `POST /xx/{id}/cancel`,而非 `POST /xx/cancel_task`。
|
||
|
||
- 必须显式声明 `response_model`,FastAPI 据此生成 OpenAPI 与做响应序列化。
|
||
|
||
### 4.3 请求/响应模型(Pydantic v2)
|
||
|
||
```python
|
||
from pydantic import BaseModel, Field
|
||
|
||
class ChatRequest(BaseModel):
|
||
question: str = Field(..., min_length=1, max_length=2000, description="用户问题")
|
||
session_id: str | None = Field(default=None, description="多轮会话 ID")
|
||
|
||
class ChatResponse(BaseModel):
|
||
answer: str = Field(..., description="回答内容")
|
||
source: str = Field(..., description="来源标识: skeleton / rag / tool ...")
|
||
```
|
||
|
||
- 每条 Field 都要有 `description`;校验约束(`min/max_length`)贴近业务真实边界。
|
||
|
||
- 响应模型独立于请求模型,禁止直接复用请求模型回传多余字段。
|
||
|
||
### 4.4 统一响应封装与业务状态码
|
||
|
||
**所有接口成功响应统一用 `ApiResponse` 包装**,结构固定为:
|
||
|
||
```json
|
||
{"code": 0, "message": "成功", "data": { ... }, "trace_id": "abc123"}
|
||
```
|
||
|
||
- 定义位置:`app/schemas/common.py`(`ApiResponse` 泛型 + `BizCode` 枚举)。
|
||
- `code` 为**业务状态码**(与 HTTP 状态码解耦):`0` 成功,`4xxxx` 客户端侧错误,`5xxxx` 服务端侧错误。
|
||
- `message` 为对调用方可读的状态描述;`data` 成功时携带业务数据、失败时为 `null`;`trace_id` 贯穿日志与存储。
|
||
- 成功用 `ApiResponse.ok(data, trace_id=...)`,失败用 `ApiResponse.fail(BizCode.XXX, ...)`。
|
||
- 业务状态码集中在 `BizCode` 枚举管理,**禁止在代码里散落魔法数字**;新增错误在枚举追加一项。
|
||
|
||
| 业务状态码 | 含义 | 关联异常 |
|
||
|-|-|-|
|
||
| `0` | 成功 | - |
|
||
| `40001` | 参数校验失败 | -(Pydantic 自动 422) |
|
||
| `40101` | 未授权访问 | `UnauthorizedError` |
|
||
| `40301` | 无权限操作 | `ForbiddenError` |
|
||
| `40401` | 资源不存在 | `KnowledgeNotFound` |
|
||
| `42901` | 请求过于频繁 | `RateLimited` |
|
||
| `50201` | 外部依赖失败 | `ToolCallFailed` |
|
||
| `50000` | 服务内部错误 | `AppError` / 兜底 |
|
||
|
||
- 全局异常处理器(`app/errors.py`)已统一输出 `ApiResponse` 结构(`data=null`),HTTP 状态码仍按语义返回(401/403/404/429/502/500)。
|
||
- Pydantic 校验错误(422)保持 FastAPI 默认 `{"detail": ...}` 结构,**不**套业务包装,避免与标准校验语义混淆。
|
||
|
||
### 4.5 流式输出(SSE)——统一协议标准
|
||
|
||
**流式接口与普通接口走两套响应约定**:普通接口统一 `ApiResponse` 包装(见 4.4);流式接口用 SSE 帧,`event` 字段表达消息语义,负载直接放业务数据,不再重复套 `{code, message, data}`。
|
||
|
||
- 定义位置:`app/schemas/sse.py`(事件类型 `SSEEventType` + 负载模型 + 帧构造 `format_sse`);帧构造**一律走 `format_sse()`**,禁止在路由里手拼字符串。
|
||
- 响应头固定 `Content-Type: text/event-stream`,接口示例:`POST /agent/chat/stream`(`app/api/routes.py`)。
|
||
- 每帧格式(两行,空行结尾):
|
||
|
||
```
|
||
event: <事件类型>
|
||
data: <JSON 负载>
|
||
|
||
```
|
||
|
||
- 事件类型与负载模型:
|
||
|
||
| 事件 | 负载字段 | 语义 |
|
||
|-|-|-|
|
||
| `meta` | `session_id` / `model` / `trace_id` | 流开始元信息,固定第一帧 |
|
||
| `message_start` | `{}` | 开始生成回答 |
|
||
| `delta` | `content` | token 增量(按序拼接即完整回答) |
|
||
| `tool_call` | `tool` / `args` | 调用工具 |
|
||
| `tool_result` | `tool` / `result` | 工具返回结果 |
|
||
| `done` | `finish_reason` / `usage` | 正常结束,固定最后一帧 |
|
||
| `error` | `code` / `message` | 中途失败,data 内复用 `BizCode` 的 code/message 语义 |
|
||
| `ping` | `{}` | 心跳保活(应对代理/客户端断流) |
|
||
|
||
- 事件流转约定:`meta → message_start → (delta \| tool_call \| tool_result)* → done`;任一步出错 → 发送 `error` → 再补 `done(finish_reason=error)` 兜底,保证流一定以 `done` 收尾。
|
||
- 长任务(RAG 检索、多步 agent)拆「提交 → 查询进度 → 获取结果」,不用单请求阻塞拖死连接。
|
||
|
||
### 4.6 依赖注入(Depends)——FastAPI 核心机制
|
||
|
||
**凡跨端点复用的横切能力(鉴权、会话、配置、限流),一律用** **`Depends`** **注入,禁止在路由里手动 new 对象**。这是 FastAPI 与普通 Flask/命令式项目的分水岭,也是专业项目结构的基础 [$TRAE\_REF](https://tomodahinata.com/en/blog/fastapi-project-structure-apirouter-dependencies-large-app-guide)。
|
||
|
||
```python
|
||
# app/dependencies.py —— 集中声明可复用依赖
|
||
def get_settings() -> Settings:
|
||
return get_settings.cache # 单例配置注入
|
||
|
||
async def get_tenant(req: Request) -> str:
|
||
"""从 Header/Token 解析租户 ID, 供多租户隔离使用(阶段八 Level5)"""
|
||
token = req.headers.get("X-Tenant-Id")
|
||
if not token:
|
||
raise HTTPException(status_code=401, detail="缺少租户标识")
|
||
return token
|
||
|
||
# app/api/routes.py —— 端点通过 Depends 取依赖
|
||
@router.post("/chat", response_model=ChatResponse, status_code=200)
|
||
async def chat(req: ChatRequest,
|
||
settings: Settings = Depends(get_settings),
|
||
tenant: str = Depends(get_tenant)) -> ChatResponse:
|
||
...
|
||
```
|
||
|
||
- **分层注入**:路由只依赖「抽象依赖函数」;编排层、工具层对象由依赖函数装配,接入层不感知具体实现。
|
||
|
||
- 鉴权、会话、配置、`trace_id` 生成等横切关注点,统一放 `app/dependencies.py`。
|
||
|
||
- 有状态依赖(数据库连接等)用 FastAPI 的依赖缓存(默认每次请求复用同依赖实例)。
|
||
|
||
- **禁止**:在路由函数体内 `Client(...)` 手动构造、绕过 `Depends` 直读全局单例。
|
||
|
||
## 5. 工具函数规范(app/tools)
|
||
|
||
### 5.1 组织原则
|
||
|
||
- 工具按域拆分文件,一个文件一个能力主题:`llm_client.py` / `search.py` / `retriever.py` / `calc.py`。
|
||
|
||
- 工具函数应是**纯函数优先**:输入确定、输出确定、尽可能无隐藏副作用;有外呼(LLM / 外部 API)的单独标注。
|
||
|
||
- 每个工具给**明确的入参、返回类型注解与 docstring**,让 Agent 能可靠调用。
|
||
|
||
```python
|
||
# app/tools/llm_client.py
|
||
import httpx
|
||
|
||
def call_llm(prompt: str, *, base_url: str, api_key: str, timeout: float = 30.0) -> str:
|
||
"""调用 LLM 完成生成。有外呼副作用, 失败抛异常由上层重试。"""
|
||
...
|
||
```
|
||
|
||
### 5.2 工具函数清单(阶段四「工具调用生态」落地)
|
||
|
||
| 工具 | 文件建议 | 输入 → 输出 | 副作用 |
|
||
| ------ | --------------------- | -------------------- | --- |
|
||
| LLM 对话 | `tools/llm_client.py` | `prompt/model → str` | 外呼 |
|
||
| 搜索 | `tools/search.py` | `query → list[dict]` | 外呼 |
|
||
| 检索 | `tools/retriever.py` | `query/向量 → hits` | 读存储 |
|
||
| 计算 | `tools/calc.py` | `expr → number` | 无 |
|
||
| 外部工具 | `tools/*.py` | 各自定义 | 视情况 |
|
||
|
||
- 所有工具函数**统一用关键字参数传可选配置**(`*` 分隔必选与可选),避免位置参数歧义。
|
||
|
||
- docstring 首行一句话说明「它做什么」,第二段提副作用与异常。
|
||
|
||
## 6. 依赖、环境与第三方工具使用
|
||
|
||
### 6.1 依赖与配置
|
||
|
||
- 服务依赖集中在 `requirements.txt`,锁定主版本(如 `fastapi>=0.110`),生产建议 `pip freeze` 出 lock。
|
||
|
||
- `requirements.txt` 是**前瞻声明**:记录服务侧设计需要的依赖,可超前于 `app/` 当前代码的 import;新增依赖先加这里,使用处按阶段落地(`loguru`/`tenacity`/`httpx`/`pydantic-settings` 在阶段四/六/七扩展时启用)。
|
||
|
||
- 密钥与可变配置一律走 `.env`(模板在 `.env.example`);`.env` **强制不入库**。
|
||
|
||
- 配置统一用 **`pydantic-settings`** 的 `BaseSettings` 管理(FastAPI 官方推荐)[$TRAE\_REF](https://fastapi.tiangolo.com/advanced/settings/#use-the-settings),禁止在业务模块散落 `os.getenv`。
|
||
|
||
```python
|
||
# app/config.py —— 唯一配置入口, 启动时读取 .env
|
||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||
|
||
class Settings(BaseSettings):
|
||
app_name: str = "Python AI Agent 学习项目"
|
||
openai_api_key: str = "" # 密钥, 只从 .env 读
|
||
deepseek_api_key: str = ""
|
||
redis_url: str = "redis://localhost:6379/0"
|
||
qdrant_url: str = "http://localhost:6333"
|
||
|
||
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
|
||
|
||
settings = Settings() # 全应用共享单例
|
||
```
|
||
|
||
- 使用方通过 `Depends(get_settings)` 注入 `Settings`,而不是 `from app.config import settings` 到处 import。
|
||
|
||
- 新增配置项 = 在 `Settings` 加一个带默认值的字段 + 在 `.env.example` 补一行,二处同步。
|
||
|
||
### 6.2 标准工具库约定(对应阶段一「核心工具库」)
|
||
|
||
| 库 | 用途 | 本规范约定 |
|
||
| ------------------- | ------- | ------------------------------------- |
|
||
| `pydantic` | 数据模型与校验 | 所有请求/响应收敛到 Pydantic |
|
||
| `pydantic-settings` | 配置管理 | `config.py` 用 `BaseSettings` 读 `.env` |
|
||
| `loguru` | 日志 | 统一 `logger`,按层打 tag,见第 7 节 |
|
||
| `tenacity` | 重试 | 外呼统一用 `@retry` 做指数退避,见第 8 节 |
|
||
| `httpx` | 异步 HTTP | 取代 `requests`;超时必设,见第 8 节 |
|
||
|
||
## 7. 日志规范
|
||
|
||
- 统一 `from loguru import logger`,不散用 `print`(Demo 教学文件除外)。
|
||
|
||
- 关键路径要能还原现场,至少覆盖:请求进入、工具调用(入参/返回)、失败、耗时。
|
||
|
||
- 日志分级对齐:`debug` 明细、`info` 正常生命周期、`warning` 可恢复、`error` 需关注。
|
||
|
||
```python
|
||
from loguru import logger
|
||
|
||
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=0.5))
|
||
def call_llm(...):
|
||
logger.info("llm call start, prompt={}", prompt[:50])
|
||
try:
|
||
r = ...
|
||
logger.info("llm call ok, used_tokens={}", r.usage)
|
||
return r
|
||
except httpx.TimeoutException as e:
|
||
logger.warning("llm timeout: {}", e)
|
||
raise
|
||
```
|
||
|
||
- 生产排障依赖日志可检索:每条日志带稳定标识(`trace_id` / `session_id`),见第 9 节。
|
||
|
||
## 8. 错误处理与可靠性
|
||
|
||
### 8.1 异常策略
|
||
|
||
- 业务方法向上抛「语义化异常」而非裸调库异常;接入层统一捕获转 HTTP 状态码。
|
||
|
||
- **外呼必须设超时**:`httpx` 传 `timeout`,LLM / 外部 API 一律默认 `timeout=30.0`。
|
||
|
||
- **外呼统一重试**:用 `tenacity` 指数退避,默认 `stop_after_attempt(3)`、`wait_exponential(multiplier=0.5)`;幂等操作才重试,写操作重试前判幂等。
|
||
|
||
```python
|
||
from tenacity import retry, stop_after_attempt, wait_exponential
|
||
|
||
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=0.5))
|
||
def fetch(day: str) -> dict: # GET 幂等 → 可安全重试
|
||
...
|
||
```
|
||
|
||
- 时长敏感 / 高风险的调用在编排层用**熔断**(连续失败即快速失败),示例见 `code/阶段七`。
|
||
|
||
- 工具返回空结果必须「如实报空、勿编造」,禁止模型自行补齐不存在的数据。
|
||
|
||
### 8.2 HTTP 错误码映射(接入层)
|
||
|
||
| 异常 | 返回码 |
|
||
| ---------------- | --------- |
|
||
| 参数校验失败(Pydantic) | 422 |
|
||
| 未授权 / 权限不足 | 401 / 403 |
|
||
| 资源不存在(含检索空结果) | 404 |
|
||
| 外部依赖失败(LLM / 存储) | 502 |
|
||
| 请求过于频繁 / 熔断打开 | 429 |
|
||
| 服务内部错误(未分类) | 500 |
|
||
|
||
### 8.3 全局异常处理器(FastAPI 惯用法)
|
||
|
||
- 业务侧抛**自定义异常**(如 `ToolCallFailed`、`KnowledgeNotFound`),接入层用 `@app.exception_handler` 统一转 HTTP 响应,路由内不散落 try/except 转码。
|
||
|
||
- 通用异常处理收敛到 `main.py` 或独立 `app/errors.py`,避免每个路由重复写错误映射。
|
||
|
||
```python
|
||
# app/errors.py —— 集中定义自定义异常与全局处理器
|
||
class AppError(Exception):
|
||
biz_code: BizCode = BizCode.INTERNAL_ERROR # 关联业务状态码
|
||
http_status: int = 500
|
||
|
||
class ToolCallFailed(AppError):
|
||
biz_code = BizCode.TOOL_FAILED # 50201 → HTTP 502
|
||
http_status = 502
|
||
|
||
# main.py 注册(register_exception_handlers 统一注册)
|
||
# 处理器输出统一 ApiResponse 结构,HTTP 状态码按语义返回:
|
||
# {"code": 50201, "message": "外部依赖失败", "data": null, "trace_id": ...}
|
||
```
|
||
|
||
- Pydantic 校验错误由 FastAPI 自动转 422,无需自定义;只对**业务异常**注册处理器。
|
||
|
||
## 9. 可追踪性与并发
|
||
|
||
- 每个请求注入 `trace_id`(如 `uuid4().hex[:8]`),贯穿日志与存储,跨链路可对。
|
||
|
||
- 多租户 / 会话用稳定键区分:`app/agents` 里 `thread_key(<tenant> , <session>)` 组合,见阶段八 Level5。
|
||
|
||
- 异步敏感路径用原生 `async/await`(FastAPI / httpx 天然支持);CPU 密集用 `asyncio.to_thread` 不阻塞事件循环。
|
||
|
||
## 10. 代码质量与提交
|
||
|
||
| 项 | 约定 |
|
||
| --------- | -------------------------------------- |
|
||
| Python 版本 | 3.10+,语法用现代类型注解(`str \| None`) |
|
||
| 格式 | `ruff format` 统一风格 |
|
||
| Lint | `ruff check`,消灭未用变量 / import 与未处理异常 |
|
||
| 类型 | 函数/参数/返回值全部注解 |
|
||
| 文档串 | 每个公开函数有 docstring,首行一句话说明意图 |
|
||
| 测试 | 关键接口配 `pytest` 单测;数据类用 Pydantic 校验即测边界 |
|
||
| 提交信息 | 一句话 present 动作,如 `fix: 修复流式接口内存泄漏` |
|
||
|
||
## 11. 各类代码的定位(避免误用)
|
||
|
||
| 位置 | 定位 | 应避免 |
|
||
| ----------- | ------------- | --------------------- |
|
||
| `app/` | 生产风格服务,遵循以上规范 | 用 `print`、裸读 env、跨层引用 |
|
||
| `code/阶段N/` | 教学 Demo,重可读性 | 上生产的依赖(环境变量强绑定) |
|
||
| `docs/` | 文档,不动代码 | 混入可执行逻辑 |
|
||
|
||
- Demo 的注释可以比生产多(教学),但**命名与分层原则一致**,便于从 Demo 平滑迁移到 `app/`。
|
||
|
||
## 12. 防过度设计(底线准则)
|
||
|
||
**前提**:本仓库是「学习项目 + 生产骨架」的混合体,最大的基建风险是「为将来做准备」而把抽象、分层、依赖做过头。以下为不可逾越的底线,新增任何结构先对照自检。
|
||
|
||
### 12.1 一句话原则
|
||
|
||
> 抽象必须服务当下的真实代码;基础设施按真实的触发时机建,不为「看起来更规范」提前预埋。
|
||
|
||
### 12.2 八条红线
|
||
|
||
1. **抽象必须有当下的受益者。** 每新增一层(service / repository / 中间件 / 基类 / 装饰器)先回答:现在有没有调用方?没有受益者的空壳就是为抽象付费(如尚无业务时的空 `app/services/`)。
|
||
2. **推迟到触发时机,而非提前预埋。** 详见「12.3 推迟清单」:基础设施按真实需求点再建。
|
||
3. **单一职责,但不为细分而细分。** 一个文件承载一个完整职责是允许的;按「读的人会不会迷路」判断是否拆分,而非按行数或「内聚感」。
|
||
4. **分层方向是硬约束,层数是软约束。** 依赖单向(`api → agents → tools → storage`)必须遵守;各层内部是否再分子层,交由实际复杂度决定。
|
||
5. **全局机制给一次、做对。** 统一响应、SSE 协议、异常转码、trace_id 等横切机制每个只实现一次,后续一律复用、禁止绕行。临时造轮子(手拼 SSE 帧、散落 `os.getenv`)是漏不是省。
|
||
6. **依赖与配置从简起步。** 新增依赖必须有当下真实用途;新增配置项 = `Settings` 加字段 + `.env.example` 补一行。
|
||
7. **扩展点「留位置」,不要「做实现」。** 预留好位置(如 SSE 已定义 `tool_call / tool_result / ping` 事件)但不预写将返工的实现。
|
||
8. **怀疑自己的编辑器视野。** 「加一层更规范」的冲动,未必是让此时读者更清楚,可能只是满足外部「最佳实践」印象。
|
||
|
||
### 12.3 本仓库推迟清单(照此执行,暂不实现)
|
||
|
||
| 结构 | 触发时机(再开始建) |
|
||
| ---- | ------------------------------------------------------------ |
|
||
| Alembic 迁移 | 第一张真实业务表出现时 |
|
||
| `/readyz` 就绪探针 | 存储/外部依赖接入后 |
|
||
| API Key 鉴权 Depends | 服务对外开放时 |
|
||
| 会话态持久化(Redis) | 多 worker 真正需要共享状态时 |
|
||
| RAG 相关超出 import 的深层封装 | 阶段四 RAG 落地时 |
|
||
| service / repository 层 | 出现明确的跨用例业务复用点时 |
|
||
| K8s manifests / CI 流水线 | 部署到集群前 |
|
||
|
||
### 12.4 自查三问(新增抽象前)
|
||
|
||
1. 这个抽象现在有没有至少一个真实调用方?
|
||
2. 把它推迟一个月,会有多少返工成本?(< 半天 → 推迟)
|
||
3. 去掉它,谁会读不清代码?
|
||
|
||
三问全过再动手;有一问存疑,先写下意图、引入真实触发时机后再实现。
|
||
|
||
## 13. 小结自检
|
||
|
||
- [ ] 新代码归属明确(`app/` 服务 / `code/` 教学),模块命名一眼可辨
|
||
|
||
- [ ] 接入层不碰存储、编排层不写路由,依赖单向
|
||
|
||
- [ ] 所有接口用 Pydantic 声明请求/响应,字段带 `description`
|
||
|
||
- [ ] 路由带 `tags` 分组 + 显式 `status_code`,`main.py` 只做组装
|
||
|
||
- [ ] 横切能力(鉴权/会话/配置)用 `Depends` 注入,不手动 new 对象
|
||
|
||
- [ ] 配置用 `pydantic-settings`,密钥走 `.env` 不入库
|
||
|
||
- [ ] 业务异常用全局 `exception_handler` 统一转码
|
||
|
||
- [ ] 外呼统一设超时 + `tenacity` 重试,空结果如实报空
|
||
|
||
- [ ] 统一 `loguru` 日志 + `trace_id`
|
||
|