本篇对应仓库
app/main.py、app/config.py与 README「快速开始」。从零开始:装依赖 → 建最小骨架 → 应用工厂 → 第一个接口。
一、拉代码、建虚拟环境
# Windows PowerShell(macOS/Linux 把 \.venv 换成 /)。教程实测环境:Python 3.13,仓库锁定 Python 3.12,两者都能跑通全部测试
git clone https://github.com/lianghui32/sinan-ai-platform.git
cd sinan-ai-platform
python -m venv .venv # 或者: python -m virtualenv .venv(部分 Windows Python 缺 venv 模块时)
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
为什么要虚拟环境:依赖按项目隔离,不污染系统 Python,也不被别的项目污染。作者把 8 个依赖的版本精确锁定在 requirements.txt(如 fastapi==0.141.1),文件头部注释注明这是 pip-audit 审计后整体升级的版本——锁定 = 任何人任何时间装出来都是同一套。
装完验证(教程实测输出):
.\.venv\Scripts\python.exe -m pip list | Select-String "fastapi|numpy|pytest"
# fastapi 0.141.1
# numpy 2.2.0
# pytest 9.1.1
二、先跑起来看看终点长什么样
.\.venv\Scripts\python.exe -m uvicorn app.main:app --host 0.0.0.0 --port 8005
浏览器打开 http://127.0.0.1:8005——登录页已经在了(默认 admin / admin123,首次启动自动建库并写入种子数据:知识库示例文档、3 个岗位助手、2 个演示工作流)。
教程实测记录:本机 Windows + Python 3.13 实际启动成功,
POST /api/auth/login返回 admin 会话,GET /api/assistants列出"亚马逊运营助手 / 产品开发助手 / 数据分析助手"三个种子助手。
我们现在的任务是:忘掉这些成品,从空文件夹重建它。下面每一步都解释"为什么"。
三、最小骨架:三件套
在工作目录新建 app/ 文件夹,先写三个文件。
3.1 app/config.py——配置先行
"""全局配置:端口、数据库路径、环境变量、安全基线参数。"""
import os
from pathlib import Path
# 项目根目录
BASE_DIR = Path(__file__).resolve().parent.parent
# 固定端口 8005
PORT = 8005
# 数据库文件路径:优先环境变量 AIP_DB_PATH,其次默认 data/platform.db
DEFAULT_DB_PATH = BASE_DIR / "data" / "platform.db"
def resolve_db_path(db_path: str | None = None) -> str:
"""确定实际使用的 sqlite 数据库文件路径。"""
path = db_path or os.environ.get("AIP_DB_PATH") or str(DEFAULT_DB_PATH)
p = Path(path)
p.parent.mkdir(parents=True, exist_ok=True)
return str(p)
为什么第一天就单独立配置文件:看 resolve_db_path 的三级回退——函数参数 > 环境变量 > 默认路径。这一个函数同时服务三种人:测试代码传临时路径(每个测试用独立库)、运维用环境变量切库、开发者直接跑用默认库。配置集中 + 回退链,是后面"107 项测试互不干扰"的地基。
3.2 app/db.py(临时最小版)——先有连接,下篇补全
"""SQLite 数据库:连接管理。仅使用标准库 sqlite3。"""
import sqlite3
def get_conn(db_path: str) -> sqlite3.Connection:
conn = sqlite3.connect(db_path, timeout=10)
conn.row_factory = sqlite3.Row # 让查询结果可以按列名访问 r["username"]
conn.execute("PRAGMA foreign_keys = ON")
# 多线程并发读写:等锁而不是立刻抛 database is locked
conn.execute("PRAGMA busy_timeout = 5000")
return conn
两个值得背下来的细节:
- row_factory = sqlite3.Row:不设的话只能 r[0]、r[1] 按下标取,可读性灾难;
- busy_timeout = 5000:SQLite 并发写默认立刻报 database is locked,等待 5 秒让绝大多数"撞锁"自然化解。
3.3 app/main.py——应用工厂
"""FastAPI 应用入口:create_app 工厂 + 静态前端挂载。
启动(端口固定 8005):
python -m uvicorn app.main:app --host 0.0.0.0 --port 8005
"""
from pathlib import Path
from fastapi import FastAPI
from .config import PORT, resolve_db_path
from .db import init_db # 下篇实现;本篇可先写 pass 占位
def create_app(db_path: str | None = None) -> FastAPI:
"""应用工厂:db_path 参数 > 环境变量 AIP_DB_PATH > 默认 data/platform.db。"""
resolved_db_path = resolve_db_path(db_path)
init_db(resolved_db_path)
app = FastAPI(
title="司南 Sinan — 跨境电商企业级AI中台",
version="1.2.0",
)
app.state.db_path = resolved_db_path
def get_db_path(request=None) -> str:
return resolved_db_path
@app.get("/api/health", tags=["meta"])
def health():
return {"status": "ok", "service": "sinan-ai-platform", "port": PORT}
return app
app = create_app()
if __name__ == "__main__":
import uvicorn
uvicorn.run("app.main:app", host="0.0.0.0", port=PORT, reload=False)
四、核心概念:为什么是"应用工厂"而不是模块级 app
新手常见写法是模块顶层 app = FastAPI() 一把梭。司南用 create_app(db_path) 工厂,app = create_app() 只是最后对外的默认实例。区别在哪:
- 测试隔离:
create_app(str(tmp_path / "test.db"))让每个测试用例指向独立临时库。仓库tests/conftest.py里那行app = create_app(str(tmp_path / "test.db"))就是这么用的——107 项测试没有任何一条碰真实数据。 - 同一份代码两种驱动:既能被 uvicorn 起成真实服务,也能被
httpx.ASGITransport直接在进程内驱动(11 篇讲测试时展开)。 - 依赖显式化:路由需要的
get_db_path、权限依赖、限流器都从工厂注入(deps字典),而不是各路由from config import *各自为政。
app = create_app() 里的 init_db 会自动建库建表(首次运行自动创建 data/platform.db 并写入种子数据),所以你什么都不用初始化,直接 uvicorn 就能跑。
五、跑起来
.\.venv\Scripts\python.exe -m uvicorn app.main:app --host 0.0.0.0 --port 8005
# INFO: Uvicorn running on http://0.0.0.0:8005
验证(另开一个终端):
(Invoke-WebRequest http://127.0.0.1:8005/api/health).Content
# {"status":"ok","service":"sinan-ai-platform","port":8005}
再开 http://127.0.0.1:8005/docs——FastAPI 自带的交互式接口文档,后续每个接口都会自动出现在这里。
--host 0.0.0.0 是什么意思:监听所有网卡(局域网内其他设备也能访问);只想本机访问用 127.0.0.1。上线前记得 README 的安全提示:默认口令仅供本地演示。
六、本篇动手清单
- 按第三节亲手建出
config.py/db.py/main.py,让/api/health返回 ok - 把
init_db调用注释掉再跑一次,观察错误——理解"启动时建库"这件事发生在哪 - 试着
create_app("data/test-my.db")起第二个实例,确认两个数据库文件互不影响
常见坑:
- ModuleNotFoundError: No module named 'app'——必须在项目根目录执行 uvicorn(模块路径 app.main:app 是相对根目录的)
- 端口被占用:netstat -ano | findstr 8005 找到 PID,taskkill /PID xxx /F 杀掉
- 部分精简版 Python 报 No module named venv:改用 python -m pip install virtualenv 再 python -m virtualenv .venv(教程实测过这条路径)
下一篇我们把 init_db 从占位变成真家伙:15 张表、幂等建库、种子数据、增量迁移——全程无 ORM。