标签 FastAPI 下的文章

是否想设计一套让用户感到公平的 API 限流规则?通过平滑流量,避免随机触发 429 错误,并借助 Redis 与真正的滑动窗口算法,实现足够健壮的限流执行,以适应复杂的生产环境。

如果限流器上线后立刻收到客诉,并非个例。事实上,大多数所谓“简单”的限流方案,其简单程度就如同将折叠椅当作简单梯子来用,平时凑合,但一旦出问题便可能是严重的故障,且往往发生在最不该出错的时刻。

正确的解决方式不是提高限流阈值,而是让限流规则更具公平性。

本文将演示如何为 FastAPI 与 Redis 搭建滑动窗口算法,避免边界峰值问题,减少误判,同时保持足以应对真实流量的性能。

为什么固定窗口会导致误判?
最常见的“固定窗口”算法,比如“每分钟最多 60 次请求”,看似简单有效,却隐藏着一个致命缺陷:
假设一个用户在 12:00:59 这一刻瞬间发出了 60 次请求。
紧接着下一秒 12:01:00,计数器清零重置。
然后他又立刻发出 60 次请求。
结果就是:在短短 1 秒多的时间里,用户实际发出了 120 次请求,而你的限流器却认为完全合规。
更糟糕的是,固定窗口常常会惩罚那些在时间窗口边界附近正常操作的用户。比如用户在某一分钟的最后几秒和下一分钟的开头发送了两小批请求,就很容易被系统标记为“滥用”——即使他的行为完全没有恶意。
滑动窗口算法正是为了解决这个问题而生的。

滑动窗口是怎么工作的

  • 固定窗口问的是:“这个固定的 1 分钟时间段里,有多少请求?”
  • 滑动窗口问的是:“从当前这一刻往前推 60 秒,这滚动的 60 秒里,有多少请求?”
    它没有生硬的“时间桶”概念,也不会在整点时刻突然重置计数器。整个时间窗口是连续滑动的,就像一条移动的时间滑轨。

有几种实现方式,但有一个非常优雅的 Redis 方案:

  1. 存储:为每一个需要限流的对象(如用户ID、IP)创建一个 Redis 有序集合(ZSET),每次请求的时间戳就是集合中的一个成员。
  2. 判断(每次请求时):

    • 清理:移除集合中所有超过窗口时长(比如60秒)的旧时间戳。
    • 计数:统计集合中剩余的时间戳数量(即最近60秒内的请求数)。
    • 裁决:如果数量未超限,则将当前请求的时间戳加入集合。
    • 保洁:为这个集合设置一个过期时间,让不活跃的用户数据自动清理。

    核心架构:如何保证高并发下的准确性?

[客户端请求] --> [FastAPI 应用 (依赖注入/中间件)]
                          |
                          |--- (原子化限流检查) ---|
                          V
                     [Redis 集群]
                   (Key: 用户标识:路由路径)
                    (Value: 有序集合 ZSET)

这里的关键在于,“清理、计数、添加” 这一系列操作必须是原子的。否则,在超高并发下,多个请求可能同时通过检查,导致实际请求数超出限制。因此,我们选择使用 Redis Lua 脚本来保证原子性。

设计限流键:我们要限制“谁”?
在 coding 前,先定义“公平”的含义。

  • 按IP:最简单的方案,但对于公司网关、移动网络(NAT)后的多个真实用户可能不公平。
  • 按用户ID/API密钥:如果你有用户认证体系,这是最精准、最公平的方式。
  • 按端点:可以对不同的端点设置不同的限制,例如 /login 接口比 /public/news 更严格。
  • 复合键:例如 user_id:route,能实现非常精细的“公平使用”策略。

一个推荐的实践策略是:

  1. 首选:已认证用户的 API Key 或 User ID。
  2. 降级:如果未认证,则使用 Client IP。
  3. 增强:可选地结合请求路径,对不同成本的接口实施差异化限流。

Redis Lua脚本(原子滑动窗口)
这个脚本一次性完成了滑动窗口限流的所有逻辑:清理旧数据、判断是否超限、记录新请求。

-- 参数说明:-- KEYS[1]: 限流键,例如 "rate_limit:user_123:/api/search"-- ARGV[1]: 当前时间戳(毫秒)-- ARGV[2]: 窗口大小(毫秒),如 60000-- ARGV[3]: 限制次数,如 60-- ARGV[4]: 键的过期时间(秒),应略大于窗口local current_time = tonumber(ARGV[1])local window_size = tonumber(ARGV[2])local max_requests = tonumber(ARGV[3])local key_ttl = tonumber(ARGV[4])-- 1. 移除窗口之外的所有旧时间戳
redis.call("ZREMRANGEBYSCORE", KEYS[1], 0, current_time - window_size)-- 2. 获取当前窗口内的请求数量local current_count = redis.call("ZCARD", KEYS[1])-- 3. 判断是否超限if current_count >= max_requests then-- 计算还需要多久才能重试(基于窗口内最早的请求)local oldest_request = redis.call("ZRANGE", KEYS[1], 0, 0, "WITHSCORES")local wait_time_ms = 0if oldest_request[2] then
        wait_time_ms = (tonumber(oldest_request[2]) + window_size) - current_time
        if wait_time_ms < 0 then wait_time_ms = 0 endend-- 返回:不允许,当前计数,需等待的毫秒数return {0, current_count, wait_time_ms}end-- 4. 未超限,记录本次请求
redis.call("ZADD", KEYS[1], current_time, tostring(current_time))-- 5. 刷新键的过期时间
redis.call("EXPIRE", KEYS[1], key_ttl)-- 返回:允许,新的计数,无需等待return {1, current_count + 1, 0}

返回结果:

  • allowed:是否允许 (1/0)
  • new_count:当前窗口内的最新请求数
  • retry_after_ms:让我们在 API 响应中提供精确的 Retry-After 头部。

在 FastAPI 中的优雅集成
此示例使用redis-py的异步客户端redis.asyncio,并将限流器作为依赖项应用。

from fastapi import FastAPI, Request, HTTPException, Depends
import time
import redis.asyncio as redis

app = FastAPI(title="带滑动窗口限流的API服务")# 初始化异步Redis客户端
redis_client = redis.Redis(host="localhost", port=6379, decode_responses=False)# 将上面的Lua脚本内容粘贴在这里
LUA_SLIDING_WINDOW_SCRIPT = """
-- ... Lua脚本内容同上 ...
"""
_script_sha1 = None  # 缓存脚本加载后返回的SHA1值# 限流配置
RATE_LIMIT_WINDOW = 60  # 时间窗口:60秒
RATE_LIMIT_MAX_REQS = 60 # 最大请求数:60次
KEY_EXPIRE_BUFFER = 120  # 键的过期时间(稍长于窗口,便于调试)def _get_current_ms():"""获取当前毫秒时间戳"""return int(time.time() * 1000)async def _ensure_script_loaded():"""确保Lua脚本已被加载到Redis服务器"""global _script_sha1
    if _script_sha1 is None:
        _script_sha1 = await redis_client.script_load(LUA_SLIDING_WINDOW_SCRIPT)async def sliding_window_rate_limiter(request: Request):"""
    核心限流依赖项。
    可被用于全局中间件或单个路由的 `dependencies=[Depends(sliding_window_rate_limiter)]`。
    """await _ensure_script_loaded()# 1. 构造限流对象的标识符#    优先使用API Key,否则使用客户端IP(根据你的认证体系调整)
    api_key = request.headers.get("X-API-Key")
    client_identifier = api_key if api_key else request.client.host

    # 2. 可选:将请求路径也作为限流维度的一部分,实现更细粒度控制
    request_path = request.url.path
    redis_key = f"rate_limit:{client_identifier}:{request_path}"# 3. 原子化执行限流逻辑
    result = await redis_client.evalsha(
        _script_sha1,1,  # 表示后面只有一个Key
        redis_key,
        _get_current_ms(),
        RATE_LIMIT_WINDOW * 1000,  # 转为毫秒
        RATE_LIMIT_MAX_REQS,
        KEY_EXPIRE_BUFFER
    )

    allowed, current_count, retry_after_ms = int(result[0]), int(result[1]), int(result[2])# 4. 如果被限流,抛出标准的429错误if not allowed:# 将毫秒转换为秒(向上取整,最少1秒)
        retry_after_seconds = max(1, (retry_after_ms + 999) // 1000)raise HTTPException(
            status_code=429,
            detail={"code": "rate_limit_exceeded","message": "请求过于频繁,请稍后再试。","retry_after": retry_after_seconds,"limit": RATE_LIMIT_MAX_REQS,"window": RATE_LIMIT_WINDOW,},
            headers={"Retry-After": str(retry_after_seconds),"X-RateLimit-Limit": str(RATE_LIMIT_MAX_REQS),"X-RateLimit-Remaining": "0","X-RateLimit-Reset": str(int(time.time()) + retry_after_seconds),})# 5. 请求通过,可以在此处将剩余次数等信息添加到响应头(可选)# response.headers["X-RateLimit-Remaining"] = str(RATE_LIMIT_MAX_REQS - current_count)return True# 在需要限流的路由上使用依赖项
@app.get("/api/v1/search", dependencies=[Depends(sliding_window_rate_limiter)])async def search_products(query: str):"""商品搜索接口,受滑动窗口限流保护。"""# 这里是你的业务逻辑...return {"results": [], "query": query}# 健康检查接口通常不需要限流
@app.get("/health")async def health_check():return {"status": "healthy"}

为什么这种方法能避免误判?

  1. 真正公平:平稳发送请求的用户不会在“59秒”和“00秒”的边界上被误伤。
  2. 精准评估:突发流量会在一个连续滑动的窗口内被评估,而非两个割裂的“时间桶”。
  3. 体验友好:返回的 Retry-After 时间是基于窗口中最早的那个请求计算的,告诉用户一个明确的、合理的重试时间,而不是“请稍后再试”这种模糊提示。

上生产环境前,务必考虑的几点

  1. 使用Redis作为唯一可信源(而非应用内存)
    只要你部署了多个 FastAPI 实例,就必须使用 Redis 这类外部存储来做计数。各个Pod内存里的计数器互不干扰,限流就形同虚设。
  2. 谨慎使用纯IP限流
    除非是面向公众的、最基础的防护,否则尽量结合用户身份。一个公司的出口IP背后可能有成百上千的员工,一人犯错,全员被封,并不是一个合适的方式。
  3. 考虑差异化限流成本
    查询接口 和 数据导出接口 对服务器的压力差别很大。可以为不同接口设置不同的 (窗口, 次数) 组合,甚至引入更高级的 令牌桶算法 来应对复杂成本。
  4. 制定故障降级策略
    如果 Redis 挂了怎么办?

    • 故障开放:对于 查询类、非核心 接口,可以选择暂时放行,保证核心业务可用。
    • 故障关闭:对于 登录、支付、发送验证码 等敏感接口,应该严格失败,防止在缓存失效时被攻击。

小结
一个好的API限流器,不应该让守规矩的用户感到访问如同碰运气一般。通过 FastAPI + Redis + 滑动窗口 这个组合,可以获得的是一个行为可预测、边界处理平滑、反馈信息有用的限流方案。

使用 django 的 command 启动 langGraph Server, 但要求基于 AsyncPostgresSaver,
没有找到相关的可用的代码, 这里记录下 直接抛出代码

"""
Run the LangGraph Agent Server
"""

import asyncio
import uvicorn
from django.core.management.base import BaseCommand
from django.conf import settings
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from langserve import add_routes
from langchain_core.globals import set_verbose
from psycopg_pool import AsyncConnectionPool
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
from apps.ai.graph.app import AsyncGraphApp


class Command(BaseCommand):
    """
    Run the LangGraph Agent Server
    """

    help = "Starts the LangGraph Agent Server"

    def add_arguments(self, parser):
        parser.add_argument("--host", type=str, default="0.0.0.0")
        parser.add_argument("--port", type=int, default=2028)

    def handle(self, *args, **options):
        asyncio.run(self.handle_async(*args, **options))

    async def handle_async(self, *args, **options):
        """启动 Agent Server"""
        host = options["host"]
        port = options["port"]

        self.stdout.write(f"Starting Agent Server at http://{host}:{port}...")

        # Get the LangGraph application
        checkpointer = await self.get_checkpointer()
        graph_app = AsyncGraphApp().compile(checkpointer=checkpointer).app
        print(f"graph_app----------------->: {graph_app}")

        # Initialize FastAPI app
        app = FastAPI(
            title="Baby Consultant Agent",
            version="1.0",
            description="A LangGraph-based agent for baby consultation",
        )

        # Set CORS
        app.add_middleware(
            CORSMiddleware,
            allow_origins=["*"],
            allow_credentials=True,
            allow_methods=["*"],
            allow_headers=["*"],
        )
        set_verbose(True)
        # Add routes using LangServe
        # This exposes the graph at /agent/invoke, /agent/stream, etc.
        add_routes(
            app,
            graph_app,
            path="/agent",
        )

        # Run with Uvicorn
        config = uvicorn.Config(app, host=host, port=port)
        # 基于当前的Async Running Loop 启动unicorn
        server = uvicorn.Server(config)
        await server.serve()

    async def get_checkpointer(self):
        """获取 Checkpointer"""
        # 1. 显式创建连接池 (让它在应用生命周期内一直存活)
        connection_kwargs = {
            "autocommit": True,
            "prepare_threshold": 0,
        }

        # 使用同步的 ConnectionPool
        pool = AsyncConnectionPool(
            conninfo=settings.LANGGRAPH_POSTGRES_CONNECTION_STRING,
            max_size=20,
            kwargs=connection_kwargs,
        )

        # 2. 将连接池传入构造函数
        checkpointer = AsyncPostgresSaver(pool)

        # 3. 初始化数据库表
        await checkpointer.setup()

        return checkpointer

使用 django 的 command 启动 langGraph Server, 但要求基于 AsyncPostgresSaver,
没有找到相关的可用的代码, 这里记录下 直接抛出代码

"""
Run the LangGraph Agent Server
"""

import asyncio
import uvicorn
from django.core.management.base import BaseCommand
from django.conf import settings
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from langserve import add_routes
from langchain_core.globals import set_verbose
from psycopg_pool import AsyncConnectionPool
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
from apps.ai.graph.app import AsyncGraphApp


class Command(BaseCommand):
    """
    Run the LangGraph Agent Server
    """

    help = "Starts the LangGraph Agent Server"

    def add_arguments(self, parser):
        parser.add_argument("--host", type=str, default="0.0.0.0")
        parser.add_argument("--port", type=int, default=2028)

    def handle(self, *args, **options):
        asyncio.run(self.handle_async(*args, **options))

    async def handle_async(self, *args, **options):
        """启动 Agent Server"""
        host = options["host"]
        port = options["port"]

        self.stdout.write(f"Starting Agent Server at http://{host}:{port}...")

        # Get the LangGraph application
        checkpointer = await self.get_checkpointer()
        graph_app = AsyncGraphApp().compile(checkpointer=checkpointer).app
        print(f"graph_app----------------->: {graph_app}")

        # Initialize FastAPI app
        app = FastAPI(
            title="Baby Consultant Agent",
            version="1.0",
            description="A LangGraph-based agent for baby consultation",
        )

        # Set CORS
        app.add_middleware(
            CORSMiddleware,
            allow_origins=["*"],
            allow_credentials=True,
            allow_methods=["*"],
            allow_headers=["*"],
        )
        set_verbose(True)
        # Add routes using LangServe
        # This exposes the graph at /agent/invoke, /agent/stream, etc.
        add_routes(
            app,
            graph_app,
            path="/agent",
        )

        # Run with Uvicorn
        config = uvicorn.Config(app, host=host, port=port)
        # 基于当前的Async Running Loop 启动unicorn
        server = uvicorn.Server(config)
        await server.serve()

    async def get_checkpointer(self):
        """获取 Checkpointer"""
        # 1. 显式创建连接池 (让它在应用生命周期内一直存活)
        connection_kwargs = {
            "autocommit": True,
            "prepare_threshold": 0,
        }

        # 使用同步的 ConnectionPool
        pool = AsyncConnectionPool(
            conninfo=settings.LANGGRAPH_POSTGRES_CONNECTION_STRING,
            max_size=20,
            kwargs=connection_kwargs,
        )

        # 2. 将连接池传入构造函数
        checkpointer = AsyncPostgresSaver(pool)

        # 3. 初始化数据库表
        await checkpointer.setup()

        return checkpointer

我复现一下上面的操作说 zq-platform初始数据写不到数据库

第一次执行

alembic revision --autogenerate -m "init tables"

报错

INFO  [
            alembic.runtime.migration]
           Context impl PostgresqlImpl.INFO  [
            alembic.runtime.migration]
           Will assume transactional DDL.ERROR [
            alembic.util.messaging]
           Target database is not up to date.FAILED: Target database is not up to date.

意思是:你的数据库当前版本 (current) 落后于 Alembic 迁移脚本所定义的最新版本 (head) cnblogs.com+1。这就好比你手里拿着的是第3版的说明书,但产品已经更新到第5版了

要解决这个问题,核心思路就是将数据库的当前版本 (current) 更新到与最新的迁移脚本版本 (head) 一致。

1.查看当前数据库状态:首先,确认一下版本差异。在项目根目录下打开终端,依次运行:

    # 查看数据库当前记录的版本    alembic current    # 查看所有可用的迁移脚本版本(head)    alembic heads

你通常会看到 current 的版本号比 heads 的版本号要旧,或者 heads 显示了多个分支(这通常意味着存在多个分支迁移需要合并)。

分别显示

INFO  [
            alembic.runtime.migration]
           Context impl PostgresqlImpl.INFO  [
            alembic.runtime.migration]
           Will assume transactional DDL.

这证实了问题所在:数据库当前停留在一个空版本,并没有处于最新状态,所以 Alembic 拒绝你生成新的迁移脚本。

请直接运行下面这条命令来解决这个问题:

alembic upgrade head

这个命令会扫描 alembic/versions 文件夹,找到所有脚本,并依次在数据库中执行它们。

执行结果:

INFO  [
            alembic.runtime.migration]
           Context impl PostgresqlImpl.INFO  [
            alembic.runtime.migration]
           Will assume transactional DDL.INFO  [
            alembic.runtime.migration]
           Running upgrade  -> b6a31168d666, init tablesINFO  [
            alembic.runtime.migration]
           Running upgrade b6a31168d666 -> a79453452d83, add page design

数据库已经成功升级到最新版本了。从输出 Running upgrade b6a31168d666 -> a79453452d83 可以看到:数据库已经更新到了 a79453452d83,

打开数据库可以看到当前版本号:

图片

重新生成迁移:现在可以再次尝试运行 alembic revision --autogenerate -m “init tables”。

alembic revision --autogenerate -m "init tables"

显示结果:

INFO  [
            alembic.runtime.migration]
           Context impl PostgresqlImpl.INFO  [
            alembic.runtime.migration]
           Will assume transactional DDL.Generating F:\下载程序与源码\★★★可执行项目收集★★★\zq-platform\backend-fastapi\alembic\versions\588
            bd64ec92e_init_tables.py
           ...  done

输出显示:Generating ... 588bd64ec92e\_init\_tables.py ... done

这表示 Alembic 成功扫描了你的代码和数据库,并发现它们之间的差异,自动生成了一个新的迁移脚本文件: 588bd64ec92e\_init\_tables.py。

接下来的步骤:让修改生效

现在脚本文件只是生成了,但数据库里的表结构还没变。你需要执行最后一步:

1. 查看生成的脚本(可选,但推荐)

你可以打开 alembic\versions\ 9abfcdbaff03\_init\_tables.py 看一眼。检查 upgrade() 函数里是否包含了 op.create\_table(…) 等语句。如果那里是空的,说明 Alembic 没检测到你的模型变化,或者模型没有被正确导入。

2. 更新数据库(必须执行)

在终端运行:

alembic upgrade head

这将会把刚才生成的脚本应用到你的 fastapi\_db 数据库中,创建对应的表结构。

显示

INFO  [
            alembic.runtime.migration]
           Context impl PostgresqlImpl.INFO  [
            alembic.runtime.migration]
           Will assume transactional DDL.INFO  [
            alembic.runtime.migration]
           Running upgrade a79453452d83 -> 588bd64ec92e, init tables

数据库迁移已经完全成功了!🎉

从输出 Running upgrade a79453452d83 -> 588bd64ec92e 可以确认:

你的 fastapi\_db 数据库已经成功应用了 init tables 脚本,所有定义的表结构现在都已经真实存在于数据库中了。

验证成果

如果你想确认表真的创建好了,可以:

1.打开 pgAdmin(如果你安装了的话)。

2.连接到 fastapi\_db。

3.展开 Schemas -> public -> Tables。

4.你应该能看到刚才定义的所有数据表(比如 users 等)。

或者,你可以直接在 Python 代码中试着向数据库写入一条数据,看是否报错。

再执行python scripts/ loaddata.py db\_init.json,导入数据,看到

导入完成:  成功: 38 条  失败: 0 条

先不要激动!!!日志最后一句“导入完成: 成功 38 条 / 失败 0 条”是脚本自己打印的统计,并不真实——

只要发生 ROLLBACK,整个事务就被回滚,数据库里一条新数据也没有写进去。

真正的失败原因就是这一条:


            asyncpg.exceptions.DataError:
            invalid input for query argument $4: '2026-01-11T19:44:39.752685'  (expected a 
            datetime.date
           or 
            datetime.datetime
           instance, got 'str')

也就是 core\_user.last\_login 字段传的是 字符串,而数据库列类型是 timestamp without time zone,异步驱动 asyncpg 不接受字符串隐式转换。

如何修复

def parse_datetime(value):    """解析日期时间字符串"""    if isinstance(value, str):        # 尝试多种日期时间格式        formats = [            "%Y-%m-%dT%H:%M:%S.%f",  # ISO 格式带微秒            "%Y-%m-%dT%H:%M:%S",      # ISO 格式不带微秒            "%Y-%m-%d %H:%M:%S.%f",   # 带微秒的空格分隔格式            "%Y-%m-%d %H:%M:%S",      # 不带微秒的空格分隔格式            "%Y-%m-%d",               # 仅日期格式        ]                for fmt in formats:            try:                return 
            datetime.strptime(value,
           fmt)            except ValueError:                continue                # 如果以上格式都不匹配,尝试 fromisoformat        try:            return 
            datetime.fromisoformat(value.replace(
          "Z", "+00:00"))        except ValueError:            pass                # 如果所有尝试都失败,返回原始值        return value    return value    ......    # 转换日期时间字段                for key, value in 
            fields.items():
                              if isinstance(value, str):                        # 检查是否为日期时间格式的字符串                        parsed_value = parse_datetime(value)                        # 如果成功解析且返回的是 datetime 对象,则替换原值                        if isinstance(parsed_value, datetime):                            fields[key] = parsed_value

再执行python scripts/ loaddata.py db\_init.json,直至这些数据都导入完成。

当看到

从文件导入数据: 
            db_init.json
          读取到 38 条记录2026-01-20 17:07:52,224 INFO 
            sqlalchemy.engine.Engine
           select 
            pg_catalog.version()
          2026-01-20 17:07:52,225 INFO 
            sqlalchemy.engine.Engine
           [raw sql] ()......2026-01-20 17:07:52,276 INFO 
            sqlalchemy.engine.Engine
           COMMIT导入完成:  成功: 38 条  失败: 0 条

·脚本成功读取了 db\_init.json 文件,识别出包含 38 条待导入的记录

·SQLAlchemy 引擎成功连接到 PostgreSQL 数据库(日志中出现 pg\_catalog.version() 是 PostgreSQL 特有的查询)

数据库验证

出现账号数据即为数据导入成功。

图片

启动服务

python main.py或使用 uvicornuvicorn main:app --reload --host 0.0.0.0 --port 8000

这样初始数据写不到数据库问题就可以得到根本解决。

在上一篇《Claude Code × 智谱 BigModel 实战集成指南》中,我们已经完成了一次完整的项目实战。项目可以正常运行,但在后续代码 Review 时,一个问题逐渐暴露出来:

生成的代码虽然能跑,但大量 API 和用法已经过时,与最新官方文档存在明显偏差。

这在 AI 辅助开发中其实非常常见——模型的训练数据更新速度,往往赶不上框架和 SDK 的迭代速度。

正巧这时,一位朋友向我推荐了 Anthropic 最新发布的 Agent Skills,通过 plugins 的方式,让 Claude 在生成代码时 动态读取最新官方文档和工具能力,从而显著降低“写得像,但跑不通”的概率。

本文就是这次探索的完整记录。


一、Agent Skills 是什么?

官方仓库地址:

https://github.com/anthropics/skills

Agent Skills 可以理解为:

一套可插拔的“能力模块”,用于教会 Claude 如何用正确的方法、最新的工具、可重复的流程 来完成特定任务。

在技术层面上:

  • 每个 Skill 本质上是一个文件夹
  • 内部包含:

    • 指令(instructions)
    • 脚本(scripts)
    • 资源文件(resources)
  • Claude Code 会在运行时动态加载这些 Skills

它能解决什么问题?

Agent Skills 的核心价值在于 “降低幻觉 + 提高一致性”,典型应用场景包括:

  • 按公司/团队的编码规范生成代码
  • 按最新官方文档调用 API(而不是靠模型记忆)
  • 执行固定的工程化流程(初始化项目、生成目录结构、部署脚本等)
  • 自动化个人或组织级任务

简单来说:

Skills 不是让模型更聪明,而是让模型更“守规矩”。

二、在 Claude Code 中安装 Agent Skills

在 Claude Code 命令行中执行:

/plugin marketplace add anthropics/skills

安装完成后,你就已经具备了使用官方 Skills 的能力。

这一步相当于为 Claude Code 打开了“官方增强模式”。

PixPin_2026-01-22_10-07-25.png


三、安装 context7 插件(关键步骤)

接下来是本文的重点:context7

1️⃣ 打开插件管理

在 Claude Code 中输入:

/plugins

然后使用键盘 ➡️ 进入 Discover

2️⃣ 搜索并安装 context7

在搜索框中输入 context7,完成安装。

context7 本质上是一个 MCP(Model Context Protocol)插件,
能让 Claude 直接参考并对齐最新的官方文档内容

PixPin_2026-01-22_10-09-22.png


四、使用 context7 生成项目代码

安装完成后,就可以在 Prompt 中显式声明使用 context7

示例 Prompt

---
name: context7
description: 使用 Context7,基于框架最新的官方文档
---

# context7

## 指南
已使用以下技术栈生成企业级项目:
- 使用 Context7,基于最新的官方文档
- FastAPI 0.128.0,带 Token 认证
  - 使用 sqlite 生成 token
  - 不使用 JWT,仅做 Token 校验
- langchain 1.2.6,使用 create_agent
- langchain-ollama 1.0.1
  - model:qwen3-vl:32b
  - embedding:qwen3-embedding:8b
- langgraph 1.0.6
- Milvus(pymilvus)2.6.6
- langfuse 3.12.0

通过这种方式,你是在明确告诉 Claude

不要靠“印象”写代码,而是以当前官方文档为准

PixPin_2026-01-22_10-29-45.png


五、实际体验与问题分析

真实结论只有一句话:

效果明显提升,但依然不能“一次生成直接可用”。

优点

  • API 使用明显更接近最新文档
  • 过时参数、废弃方法显著减少
  • 工程结构更合理,思路更偏向“真实项目”

仍然存在的问题

  • 复杂技术栈组合(LangChain + LangGraph + Milvus + Langfuse)
  • 仍然需要 多轮调试才能完全跑通
  • 某些边界用法依然存在偏差

我的判断

并不是 context7 不行,而是模型生成速度,依然落后于框架演进速度。

context7 做到的是:

  • 让 Claude 看得到 最新文档
  • 但最终“怎么拼起来”,仍然依赖模型本身的推理与代码能力

六、总结

如果你正在使用 Claude Code 做偏工程化、偏企业级的项目开发,我的建议是:

一定要上 Agent Skills

能用 context7 就用 context7

❌ 不要再完全相信“模型记忆里的 API”

但同时也要有一个清醒认知:

AI 辅助开发 = 更快的起点,而不是免调试的终点。

在当前阶段,最理想的模式依然是:

AI 生成 + 人类 Review + 多轮修正

后续我也会继续记录 Claude Code + MCP + 多模型协作 的实践经验,欢迎关注。

大家好,我是 WeAgentChat (唯信) 的开发者。

打开微信,那里是工作群的消息轰炸、亲戚的催婚和半生不熟的社交点赞。
有时候我在想,如果有一个平行的微信,里面所有的“好友”都是 AI ,但他们不仅能陪我聊天,还能像真人一样拥有性格、记得我们的点点滴滴,永远秒回、永远在线、永远站在我这边,那会是什么体验?

于是,我撸出了这个 AI 版微信WeAgentChat (唯信)

👉 Talk is cheap, show me the code: GitHub | 官网 & 预览

🌟 核心定义:你的另外一个微信

WeChat 是给人类朋友的,WeAgentChat 是给 AI 朋友的。

在这个应用里,我不仅刻意复刻了微信经典的 UI 风格和交互习惯(强迫症级别的还原),更试图打破目前 AI 助手“一问一答、用完即走”的工具属性,打造一个有温度的虚拟社交圈

1. 高度人格化的 Agent 矩阵

你可以为每个 AI 好友设定独特的灵魂。他们不是通用的助手,而是拥有特定性格、背景故事甚至怪癖的“数字人类”。

  • 有的可能是你的“毒舌损友”,在你犹豫不决时推你一把;
  • 有的可能是“温和的长辈”,在你压力大时提供情绪价值。

2. 拒绝捏人焦虑:好友库 & 话题寻人

不知道跟谁聊?懒得自己写 Prompt ?

  • 丰富的预设好友库:内置了数十位性格迥异的角色,从二次元老婆到硅谷大佬,一键添加,即刻开聊。
  • 通过话题找名人:这是我最喜欢的功能。想聊“科幻小说”?系统自动为你推荐“刘慈欣”;想聊“烧脑电影”?“诺兰”直接出现在列表里。只需输入感兴趣的话题,系统会通过语义匹配找到最契合的 AI 聊伴。不再尬聊,直奔主题。

3. “双轨”长期记忆:它真的懂你

大多数 AI 聊久了就会“失忆”,这种割裂感非常毁体验。我设计了一套双轨记忆系统:

  • Global Profile:AI 会自动根据聊天内容,实时更新它对你的性格、喜好、现状的认知。
  • Event-Level RAG:每一段深刻的对话都会被蒸馏成“事件卡片”。即使你半年前随口提过一句失眠,今天它可能又会恰到好处地关心你的睡眠质量。

4. 被动会话管理:告别“新建聊天”

我极其讨厌 ChatGPT 那种“手动点 New Chat”的割裂感。
在唯信里,如果你停止聊天超过 30 分钟,系统会自动归档当前会话并提取记忆。下次你再开口时,就像真朋友一样,是一个自然、连贯的新开始。

5. 绝对自由的对话空间 (NSFW Friendly)

我知道很多朋友苦于大厂模型的道德审查。
得益于本地化架构,你可以自由接入无审查模型(如各类 Uncensored 本地模型或 API )。在这里,没有云端审判,你可以聊任何想聊的话题,释放最真实的压力。

🛠️ 硬核技术实现 (V 站惯例)

作为一个本地优先的应用,我选择了最稳健的工具链:

  • Frontend: Vue 3.5 + Vite + Tailwind CSS (UI 高度还原微信风格)。
  • Backend: FastAPI (Python) 异步驱动。
  • Database: SQLite + sqlite-vec (所有的向量存储和关系数据都在本地,隐私第一)。
  • Memory Engine: 嵌入式 Memobase SDK ,处理复杂的事件提取和 RAG 检索。
  • Desktop: Electron 包装,支持一键启动后端服务。

🔒 隐私与安全

这可能是我做这个产品最坚持的一点:所有聊天记录和记忆数据都保存在你本地的 sqilte 数据库中。
你可以连接 OpenAI (兼容) 的 API 。除了 LLM 和向量化的调用,没有任何数据会上传到云端。

💡 开发小花絮:Vibe Coding 时代的产物

说起来,这个项目的诞生还要感谢现在的 AI 编程浪潮。
每天在公司上班,我已经习惯了 Vibe Coding 的节奏:把繁杂的逻辑丢给 AI ,看着它在屏幕上飞速吐代码。
在等待 AI 生成代码的那几十秒、几分钟的“贤者时间”里,我不仅没闲着,反而以此为契机,并行开启了这个 Side Project 。
用 AI 帮我省下的时间,去创造另一个全是 AI 的世界,这大概就是程序员独有的浪漫(摸鱼)吧。

💬 邀请与反馈

目前项目还在活跃开发中,核心的对话流和记忆系统已经跑通。

我想听听大家的看法:

  • 如果拥有这样一个“另外的微信”,你最希望在这里和什么样的 AI 交朋友?
  • 在“人与 AI 深度社交”这个命题下,你最看重的功能是什么?

目前的 UI 预览

主界面

欢迎拍砖,也欢迎给个 Star 鼓励一下社恐开发者的奇思妙想。

Claude Code × 智谱 BigModel 实战集成指南

本文记录一次 Claude Code + 智谱 BigModel(GLM Coding 套餐) 的完整体验,从 CLI 安装、IDE 集成,到使用 Claude Code 零手写代码 搭建一个可运行的 AI 后端工程,并对整体体验做一个总结。


一、什么是 Claude Code?

Claude Code 是 Anthropic 推出的 本地 AI 编码助手(CLI + IDE 插件),核心能力包括:

  • 在本地代码仓库中直接对话式开发
  • 理解项目结构、自动生成/修改代码
  • 支持多种 IDE(VS Code / JetBrains 全家桶)
  • 支持通过 兼容 Anthropic API 的第三方模型 接入(如智谱 GLM)

这意味着:即使不使用 Anthropic 官方模型,也可以完整使用 Claude Code 的工程化能力。


二、Claude Code CLI 安装

macOS / Linux / WSL

curl -fsSL https://claude.ai/install.sh | bash

Windows PowerShell

irm https://claude.ai/install.ps1 | iex

Windows CMD

curl -fsSL https://claude.ai/install.cmd -o install.cmd && install.cmd && del install.cmd

安装完成后,终端中可直接使用:

claude

三、IDE 集成能力

1️⃣ Claude Code Desktop

  • 官方桌面客户端
  • 适合直接在本地项目中进行对话式开发

PixPin_2026-01-19_19-48-04.png

2️⃣ VS Code

  • 官方插件支持
  • 与当前 Workspace 深度绑定

PixPin_2026-01-19_19-43-51.png

3️⃣ JetBrains 系列(官方支持)

  • IntelliJ IDEA
  • PyCharm
  • GoLand
  • WebStorm
  • PhpStorm
  • Android Studio

PixPin_2026-01-19_19-53-17.png

实际体验中,对 多文件工程、后端项目结构 的理解能力非常强。

四、接入智谱 BigModel(GLM Coding 套餐)

Claude Code 可以通过 Anthropic API 兼容协议 接入智谱大模型。

4.1 注册账号

👉 https://www.bigmodel.cn/glm-coding

4.2 创建 API Key

登录后进入:

👉 https://bigmodel.cn/usercenter/proj-mgmt/apikeys

创建新的 API Key 并保存。


4.3 使用官方自动化工具(强烈推荐)

智谱提供了 Coding Tool Helper,可自动完成:

  • Claude Code 安装
  • API Key 配置
  • MCP Server 管理
  • 模型套餐加载
一条命令完成配置
npx @z_ai/coding-helper

按照交互提示操作即可,无需手动修改复杂配置。


4.4 启动 Claude Code

进入任意代码目录,执行:

claude

首次启动时若提示:

Do you want to use this API key?

选择 Yes 即可。


五、模型配置与切换

默认模型映射

ANTHROPIC_DEFAULT_OPUS_MODEL   → GLM-4.7
ANTHROPIC_DEFAULT_SONNET_MODEL → GLM-4.7
ANTHROPIC_DEFAULT_HAIKU_MODEL  → GLM-4.5-Air

手动配置(可选)

编辑文件:

~/.claude/settings.json
{
  "env": {
    "ANTHROPIC_DEFAULT_HAIKU_MODEL": "glm-4.5-air",
    "ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.7",
    "ANTHROPIC_DEFAULT_OPUS_MODEL": "glm-4.7"
  }
}

验证模型状态

重新打开终端并运行:

claude

在 Claude Code 中输入:

/status

即可看到当前模型配置状态。


六、资源包与福利

  • ✅ 注册即送 体验 Token

PixPin_2026-01-19_20-12-56.png

  • ✅ 实名认证赠送 500 万 GLM-4.7 Token

PixPin_2026-01-19_20-13-54.png

👉 资源包管理:
https://bigmodel.cn/finance-center/resource-package/package-mgmt

对于个人开发者和技术验证阶段非常友好。


七、实战体验:零手写代码搭建 AI 后端

在 Claude Code 中直接输入需求:

请帮我集成 FastAPI、LangChain、LangGraph、langchain-ollama、Milvus,并构建好项目结构:

  • FastAPI 接口
  • Token 认证(非 JWT)
  • 使用 SQLite 生成和校验 Token
  • Milvus 作为向量数据库
  • Ollama 作为本地模型推理

PixPin_2026-01-19_20-15-16.png

结果:

  • 一次生成即成功运行
  • ✅ 自动生成项目结构
  • ✅ 自动生成依赖、启动方式、示例接口
  • ✅ Token 认证逻辑清晰、可直接落地

全程未手写一行代码,仅做了运行验证。


八、总结

一句话评价:Claude Code + GLM-4.7 = 当前最强中文友好的工程级 AI 编码体验之一

优点

  • 工程理解能力强(不是“代码片段级”)
  • 对后端框架 / AI 工程非常友好
  • CLI + IDE 双形态,贴近真实开发流
  • 国产模型接入,成本可控、速度稳定

适合人群

  • 后端 / AI 工程师
  • 想快速验证 AI 架构方案的团队
  • 对 Agent / RAG / 工程化落地有需求的开发者
结论
如果你已经在做 AI 工程,而不是只写 Demo,Claude Code 非常值得一试。

基于 Pydantic-Resolve 和 FastAPI-Voyager 的 Clean Architecture 实践

篇幅较长无法粘贴全文,原文链接:
https://github.com/allmonday/A-Python-web-development-methodology-for-complex-business-scenarios/blob/main/README.zh.md

一套面向复杂业务场景的 Python Web 开发方法论

目录


1. 背景与问题

1.1 当前主流做法及其痛点

在 Python Web 开发中,处理复杂业务场景时,开发者通常采用以下几种模式:

模式一:直接使用 ORM (如 SQLAlchemy )

@router.get("/teams/{team_id}", response_model=TeamDetail)
async def get_team(team_id: int, session: AsyncSession = Depends(get_session)):
    # 获取团队基本信息
    team = await session.get(Team, team_id)

    # 获取 Sprint 列表
    sprints = await session.execute(
        select(Sprint).where(Sprint.team_id == team_id)
    )
    team.sprints = sprints.scalars().all()

    # 获取每个 Sprint 的 Story
    for sprint in team.sprints:
        stories = await session.execute(
            select(Story).where(Story.sprint_id == sprint.id)
        )
        sprint.stories = stories.scalars().all()

        # 获取每个 Story 的 Task
        for story in sprint.stories:
            tasks = await session.execute(
                select(Task).where(Task.story_id == story.id)
            )
            story.tasks = tasks.scalars().all()

            # 获取每个 Task 的负责人
            for task in story.tasks:
                task.owner = await session.get(User, task.owner_id)

    return team

这种做法在简单场景下确实很直观,能够快速上手。ORM 的类型安全特性也能在编译时发现一些错误,而且与数据库表结构的一一对应关系让代码容易理解。但当我们面对真正的业务场景时,这种方式的缺陷很快就暴露出来了。

最致命的问题是 N+1 查询。虽然代码看起来很清晰,但执行时会产生大量的数据库查询。每当我们访问一个关联关系时,ORM 就会发起一次新的查询。在深层嵌套的情况下,查询数量会呈指数级增长。更糟糕的是,这种性能问题在开发阶段不容易发现,只有当数据量积累到一定程度后才会显现出来,那时候往往已经太晚了。

代码的组织方式也是个问题。数据获取的逻辑散落在各个嵌套的循环中,业务逻辑和数据获取逻辑混在一起,难以阅读和维护。当需要修改业务规则时,开发者不得不在复杂的嵌套结构中寻找修改点,很容易引入新的 bug 。性能更是不可控,随着数据量的增长,查询效率会急剧下降,而这些性能瓶颈很难在代码层面直接观察到。

此外,相似的数据获取逻辑会在多个 API 中重复出现,导致大量代码冗余。当一个 API 需要获取"团队及其 Sprint",另一个 API 需要"团队及其成员"时,即使它们的查询逻辑非常相似,也不得不重复编写。这违反了 DRY ( Don't Repeat Yourself )原则,增加了维护成本。

模式二:使用 ORM 的 Eager Loading

@router.get("/teams/{team_id}", response_model=TeamDetail)
async def get_team(team_id: int, session: AsyncSession = Depends(get_session)):
    # 使用 joinedload 预加载关联数据
    result = await session.execute(
        select(Team)
        .options(
            joinedload(Team.sprints)
            .joinedload(Sprint.stories)
            .joinedload(Story.tasks)
            .joinedload(Task.owner)
        )
        .where(Team.id == team_id)
    )
    return result.scalar_one()

为了解决 N+1 查询问题,ORM 提供了 Eager Loading 机制,让我们可以通过 joinedloadselectinload 等方式预先加载关联数据。代码变得更简洁了,性能问题也得到了缓解。但这种方案也带来了新的挑战。

最明显的问题是笛卡尔积。当我们使用多层 JOIN 预加载关联数据时,数据库返回的数据量会急剧膨胀。比如一个团队有 10 个 Sprint ,每个 Sprint 有 10 个 Story ,每个 Story 有 10 个 Task ,那么 JOIN 的结果集会包含 1000 行数据,即使每行的数据量不大,也会给网络传输和内存占用带来压力。

更严重的问题是灵活性差。Eager Loading 的策略是在代码中硬编码的,所有使用同一个 Model 的 API 都会执行相同的预加载逻辑。但不同的 API 往往需要不同的数据。比如一个 API 只需要团队的基本信息,另一个 API 需要团队的 Sprint ,还有一个 API 需要团队的成员。如果统一使用 Eager Loading 加载所有关联数据,就会出现过度获取的问题,前端不需要的数据也被查询和传输了,浪费了资源。

配置 Eager Loading 本身就很复杂。开发者需要理解 lazyjoinedloadselectinloadsubquery 等多种加载策略的区别,知道什么时候用哪一种,以及它们各自会有什么副作用。这种配置错误很容易导致性能问题或意外的数据加载行为。而且,这种"一刀切"的配置方式意味着所有 API 都使用相同的加载策略,无法针对特定场景进行优化。

模式三:手动组装数据

@router.get("/teams/{team_id}", response_model=TeamDetail)
async def get_team(team_id: int, session: AsyncSession = Depends(get_session)):
    # 1. 批量获取所有需要的数据
    team = await session.get(Team, team_id)

    sprints_result = await session.execute(
        select(Sprint).where(Sprint.team_id == team_id)
    )
    sprint_ids = [s.id for s in sprints_result.scalars().all()]

    stories_result = await session.execute(
        select(Story).where(Story.sprint_id.in_(sprint_ids))
    )
    story_ids = [s.id for s in stories_result.scalars().all()]

    tasks_result = await session.execute(
        select(Task).where(Story.id.in_(story_ids))
    )
    tasks = tasks_result.scalars().all()

    owner_ids = list(set(t.owner_id for t in tasks))
    owners_result = await session.execute(
        select(User).where(User.id.in_(owner_ids))
    )
    owners = {u.id: u for u in owners_result.scalars().all()}

    # 2. 手动组装数据结构
    sprint_dict = {s.id: s for s in sprints_result.scalars().all()}
    story_dict = {s.id: s for s in stories_result.scalars().all()}

    for story in story_dict.values():
        story.tasks = [t for t in tasks if t.story_id == story.id]
        for task in story.tasks:
            task.owner = owners.get(task.owner_id)

    for sprint in sprint_dict.values():
        sprint.stories = [s for s in story_dict.values() if s.sprint_id == sprint.id]

    team.sprints = list(sprint_dict.values())

    return team

为了获得最优的性能和精确的数据控制,有经验的开发者会选择手动组装数据。这种方式完全掌控查询逻辑,可以精确控制每个查询的 SQL 语句,避免不必要的数据库访问。通过批量查询和智能的数据组装,可以获得最佳的性能,而且没有冗余数据。

但这种方式的代价是代码变得非常冗长。如上面的例子所示,为了获取一个团队的完整信息,我们需要编写多个查询,手动构建数据字典,然后通过嵌套循环组装数据。代码的长度和复杂度都大幅增加,而真正表达业务逻辑的代码反而被淹没在数据组装的细节中。

更容易出错也是个大问题。手动组装数据涉及到大量的索引操作和循环嵌套,很容易出现索引错误、空指针引用等 bug 。而且这些错误往往只有在运行时、特定数据条件下才会暴露,难以在开发阶段发现。

维护成本更是高昂。当业务规则发生变化时(比如需要添加一个新的关联关系),开发者需要在所有相关的 API 中修改数据组装逻辑。如果遗漏了某个地方,就会导致数据不一致。而且,相似的数据组装逻辑会在多个 API 中重复出现,违反了 DRY 原则。

最根本的问题是,这种代码已经变成了纯粹的数据搬运工,看不出任何业务意图。代码中充满了字典操作、循环嵌套、索引查找,而这些都是技术细节,与业务需求毫无关系。新加入的团队成员很难从这些代码中理解业务逻辑,业务知识的传递变得异常困难。

模式四:使用 GraphQL

type Query {
    team(id: ID!): Team
}

type Team {
    id: ID!
    name: String!
    sprints: [Sprint!]!
}

type Sprint {
    id: ID!
    name: String!
    stories: [Story!]!
}

type Story {
    id: ID!
    name: String!
    tasks: [Task!]!
}

type Task {
    id: ID!
    name: String!
    owner: User!
}

GraphQL 确实是一个很有吸引力的方案。前端可以按需获取数据,需要什么字段就查什么字段,不会有过度获取的问题。它提供了类型安全的查询接口,而且通过 DataLoader 可以自动解决 N+1 查询问题。这些特性让 GraphQL 在前端开发中广受欢迎。

但 GraphQL 的学习曲线非常陡峭。开发者需要学习全新的查询语言、Schema 定义、Resolver 编写、DataLoader 配置等一堆概念,这与 REST API 的直观性形成了鲜明对比。更麻烦的是,GraphQL 的过度灵活性给后端带来了巨大的挑战。前端可以构造任意复杂的查询,有些查询甚至可能是开发者没有想到过的,这导致后端很难进行针对性的优化。当一个查询嵌套了 10 层,返回了数百万条数据时,数据库和服务器都会面临巨大的压力。

调试 GraphQL API 也比调试 REST API 复杂得多。当一个 GraphQL 查询出错时,错误信息往往很难定位到具体的问题源头。而且 GraphQL 需要额外的服务器和工具链支持,无法直接利用现有的 FastAPI 生态系统。比如 FastAPI 的依赖注入、中间件、自动文档生成等特性,在 GraphQL 中都无法直接使用。

还有一个更深层次的问题是 ERD 和用例的界限模糊。GraphQL 的 Schema 同时扮演了实体模型和查询接口两个角色。当我们设计一个 GraphQL Schema 时,很难确定应该按照实体来组织(一个 Type 对应一个数据库表),还是按照用例来组织(不同的业务场景需要不同的字段)。这导致最佳实践不清晰,不同的项目、不同的开发者可能有完全不同的组织方式。

而且随着业务增长,所有的用例都会堆砌在同一个 Schema 中,导致 Schema 膨胀,难以维护。权限控制也变得异常复杂。不同的 API 端点可能有不同的权限要求,但它们可能都查询同一个实体(比如 User ),在 GraphQL 中很难针对不同的查询场景应用不同的权限规则。

1.2 问题根源分析

上面我们探讨的所有模式,虽然表面上的问题各不相同,但它们的核心困境其实是一致的。

问题 1:业务模型与数据模型混淆

# SQLAlchemy ORM 同时扮演两个角色:
# 1. 数据模型(如何存储)
# 2. 业务模型(业务概念)

class Team(Base):
    __tablename__ = 'teams'

    id = Column(Integer, primary_key=True)
    name = Column(String)

    # 这是数据库的外键关系,还是业务关系?
    sprints = relationship("Sprint", back_populates="team")

在传统的 ORM 开发中,业务模型和数据模型是混在一起的。看看这个例子,Team 类既表达了业务概念(团队是什么),又承载了数据模型的细节(如何在数据库中存储)。当我们在 sprints 字段上定义 relationship 时,这到底是在描述一个业务关系(团队有多个 Sprint ),还是在声明一个数据库外键约束?这种模糊性会导致很多问题。

数据库的设计约束会直接影响我们的业务建模。比如,如果数据库中的 teams 表没有直接到 users 的外键,而是通过中间表 team_members 关联,那么在 ORM 中我们也必须通过这个中间表来定义关系。这意味着业务模型被迫适应数据库的实现细节,而不是反过来。

更严重的是,这种方式无法表达跨库、跨服务的业务关系。现代系统中,数据可能分布在不同的数据库中,甚至存储在外部服务里。比如用户的基本信息在 PostgreSQL ,而用户的偏好设置在 MongoDB ,用户的实时状态在 Redis 中。ORM 的 relationship 无法跨越这些边界,业务模型因此被限制在了单一数据库的范围内。

问题 2:依赖方向错误

传统架构的依赖方向:
┌─────────────┐
│   API Layer │  ← 依赖于
└──────┬──────┘
       │
       ↓
┌─────────────┐
│ ORM Models  │  ← 依赖于
└──────┬──────┘
       │
       ↓
┌─────────────┐
│  Database   │
└─────────────┘

问题:业务规则依赖于数据库实现!

这违反了 Clean Architecture 的依赖规则。正确的依赖关系应该是:业务规则最稳定,不依赖任何外层;数据库是实现细节,应该依赖业务规则;当数据库变化时,业务规则不应该受影响。但传统架构的依赖方向恰恰相反,业务规则被数据库的实现细节所绑架。

问题 3:缺少业务关系的显式声明

# 传统方式:业务关系隐藏在查询中
async def get_team_tasks(team_id: int):
    # "团队的任务"这个业务概念隐藏在 SQL WHERE 中
    result = await session.execute(
        select(Task)
        .join(Sprint, Sprint.id == Task.sprint_id)
        .where(Sprint.team_id == team_id)
    )
    return result.scalars().all()

业务关系没有被显式声明出来,这是个很隐蔽但危害很大的问题。看看这个例子,"团队的任务"是一个清晰的业务概念,但这个概念被隐藏在 SQL 的 JOIN 和 WHERE 子句中。新加入团队的成员需要阅读大量代码才能理解系统中有哪些业务关系,这些关系是如何定义的。更糟糕的是,没有自动化的方式来检查业务关系的一致性。当需求变化需要修改某个关系时,开发者很难找到所有相关的代码,很容易遗漏某个地方,导致业务逻辑的不一致。

问题 4:中间表的技术暴露

在 SQLAlchemy ORM 中,多对多关系需要显式定义中间表,这导致技术细节泄漏到业务层。

# SQLAlchemy ORM:必须定义中间表
class Team(Base):
    __tablename__ = 'teams'
    id = Column(Integer, primary_key=True)
    name = Column(String)

    # ORM relationship 需要指定中间表
    members = relationship("User",
                          secondary="team_members",  # 必须指定中间表
                          back_populates="teams")

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String)

    teams = relationship("Team",
                        secondary="team_members",  # 必须指定中间表
                        back_populates="members")

# 中间表(技术实现细节)
class TeamMember(Base):
    __tablename__ = 'team_members'
    team_id = Column(Integer, ForeignKey('teams.id'), primary_key=True)
    user_id = Column(Integer, ForeignKey('users.id'), primary_key=True)
    role = Column(String)  # 可能还有额外字段

# 查询时需要关心中间表的存在
@router.get("/teams/{team_id}")
async def get_team_members(team_id: int, session: AsyncSession):
    # 必须通过中间表查询
    result = await session.execute(
        select(User)
        .join(TeamMember, TeamMember.user_id == User.id)  # 中间表暴露
        .where(TeamMember.team_id == team_id)
    )
    return result.scalars().all()

这个问题的根源在于,ORM 的多对多关系需要显式定义中间表,这导致技术细节直接泄漏到业务层代码中。业务代码必须知道 team_members 中间表的存在,查询时也需要显式地 join 这个中间表。这增加了代码复杂度,更重要的是,业务逻辑被数据库的实现细节所绑架。

更深层的问题是业务语义变得模糊。TeamMember 到底是一个有意义的业务概念,还是纯粹的技术实现?如果中间表还有额外的字段(比如 role 表示用户在团队中的角色,joined_at 表示加入时间),这些字段应该被建模为独立的实体吗?不同的开发者可能给出不同的答案,缺乏统一的指导原则。

数据组装也因此变得复杂。查询"团队的所有成员"需要 join 中间表,查询"用户所属的团队"也需要 join 中间表。所有涉及多对多关系的查询都变得冗长和难以理解。当业务规则要求"获取用户在所有团队中的角色"时,情况就更加复杂了。这些技术细节让业务逻辑的实现变得异常沉重。

对比:Pydantic-Resolve ERD 的方式

# ERD:业务概念清晰,无需关心中间表
class TeamEntity(BaseModel, BaseEntity):
    """团队实体 - 业务概念"""
    __relationships__ = [
        # 直接表达"团队有多个成员"的业务关系
        Relationship(
            field='id',
            target_kls=list[UserEntity],
            loader=team_to_users_loader  # loader 内部处理中间表
        ),
    ]
    id: int
    name: str

class UserEntity(BaseModel, BaseEntity):
    """用户实体 - 业务概念"""
    __relationships__ = [
        # 直接表达"用户属于多个团队"的业务关系
        Relationship(
            field='id',
            target_kls=list[TeamEntity],
            loader=user_to_teams_loader
        ),
    ]
    id: int
    name: str

# Loader 实现细节:中间表只在这里出现
async def team_to_users_loader(team_ids: list[int]):
    """加载团队成员 - 内部处理中间表"""
    async with get_session() as session:
        # 只有这里需要知道中间表的存在
        result = await session.execute(
            select(User)
            .join(TeamMember, TeamMember.user_id == User.id)
            .where(TeamMember.team_id.in_(team_ids))
        )
        users = result.scalars().all()

        # 构建映射
        users_by_team = {}
        for user in users:
            for tm in user.team_memberships:
                if tm.team_id not in users_by_team:
                    users_by_team[tm.team_id] = []
                users_by_team[tm.team_id].append(user)

        return [users_by_team.get(tid, []) for tid in team_ids]

关键差异

维度 SQLAlchemy ORM Pydantic-Resolve ERD
中间表位置 暴露在业务层 隐藏在 loader 实现中
业务语义 技术关系 (secondary) 业务关系 (团队包含成员)
查询代码 需要 join 中间表 loader.load(team_id)
代码位置 分散在多处 集中在 loader
测试 依赖数据库表结构 可 mock loader

架构优势

传统方式:
Team → TeamMember (中间表) → User
业务层需要知道中间表的存在

Pydantic-Resolve 方式:
Team → User (业务关系)
中间表是数据层的实现细节,业务层不关心

这意味着:

  1. 业务模型纯净:Team 和 User 的关系直接表达业务语义

  2. 技术细节封装:中间表的存在被封装在 loader 中

  3. 灵活的存储策略


    • 数据库可以用中间表实现
    • 也可以用 JSON 字段存储
    • 甚至可以是外部服务(如 LDAP )
    • 业务层代码无需修改
  4. 易于理解:新人看到 ERD 就能理解业务关系,不需要先学习数据库设计


2. Clean Architecture 思想

2.1 核心原则

Clean Architecture 由 Robert C. Martin (Uncle Bob) 提出,核心思想是:

"Software architecture is the art of drawing lines that I call boundaries."
软件架构的艺术在于画界线。

原则 1:依赖规则

外层依赖内层,内层不依赖外层。

                ↓ 依赖方向
    ┌─────────────────────┐
    │   Frameworks &      │  外层
    │   Drivers           │  (实现细节)
    ├─────────────────────┤
    │   Interface         │
    │   Adapters          │
    ├─────────────────────┤
    │   Use Cases         │
    │   (Application)     │
    ├─────────────────────┤
    │   Entities          │  内层
    │   (Business Rules)  │  (核心)
    └─────────────────────┘

遵循依赖规则有几个关键点需要注意。首先,内层不知道外层的存在,这意味着核心业务逻辑不依赖于任何框架、数据库或 UI 的细节。其次,内层不包含外层的信息,比如业务规则不应该知道数据是用 PostgreSQL 还是 MongoDB 存储的。最后,外层的实现可以随时替换而不影响内层,这意味着我们可以从 SQLAlchemy 切换到 MongoDB ,或者从 FastAPI 切换到 Django ,而业务逻辑代码无需修改。

原则 2:业务规则独立

# ❌ 错误:业务规则依赖数据库
class Task:
    def calculate_priority(self, session):
        # 业务逻辑被数据库实现细节污染
        if self.assignee_id in session.query(TeamMember).filter_by(role='lead'):
            return 'high'

# ✅ 正确:业务规则独立
class Task:
    def calculate_priority(self, assignee_roles):
        # 业务逻辑只依赖业务概念
        if 'lead' in assignee_roles:
            return 'high'

原则 3:跨边界的数据传递

# 内层定义数据结构
class TaskEntity(BaseModel):
    id: int
    name: str
    assignee_id: int

# 外层负责转换
def task_entity_to_orm(entity: TaskEntity) -> Task:
    return Task(
        id=entity.id,
        name=entity.name,
        assignee_id=entity.assignee_id
    )

2.2 依赖规则

在 Web 开发中,依赖规则可以这样理解:

┌────────────────────────────────────────────────────┐
│         Presentation Layer (外层)                   │
│  - FastAPI Routes                                   │
│  - Request/Response Models                          │
│  - 依赖: Application Layer                          │
└────────────────────────────────────────────────────┘
                    ↓
┌────────────────────────────────────────────────────┐
│      Application Layer (Use Cases)                 │
│  - 业务用例(获取用户、创建订单)                    │
│  - 依赖: Domain Layer                               │
└────────────────────────────────────────────────────┘
                    ↓
┌────────────────────────────────────────────────────┐
│           Domain Layer (内层)                      │
│  - Entities (业务实体)                              │
│  - Business Rules (业务规则)                        │
│  - Value Objects (值对象)                           │
│  - 不依赖任何外层                                    │
└────────────────────────────────────────────────────┘
                    ↓
┌────────────────────────────────────────────────────┐
│    Infrastructure Layer (最外层)                   │
│  - Database (SQLAlchemy)                           │
│  - External Services                               │
│  - File System                                     │
└────────────────────────────────────────────────────┘

关键洞察

  • Entities 不应该知道 SQLAlchemy 的存在
  • Business Rules 不应该知道数据库表结构
  • Use Cases 不应该知道 HTTP 协议的细节

2.3 在 Web 开发中的应用

传统架构的问题

# 传统方式:所有层次耦合

# Domain Layer (应该独立,但实际上依赖了 ORM)
class User(Base):  # ← SQLAlchemy Base
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)

# Application Layer (应该只依赖 Domain ,但直接使用了 ORM)
async def create_user(data: dict, session: AsyncSession):
    user = User(**data)  # ← 直接使用 ORM Model
    session.add(user)
    await session.commit()

# Presentation Layer
@router.post("/users")
async def api_create_user(data: dict, session=Depends(get_session)):
    return await create_user(data, session)  # ← 暴露了数据库细节

这段代码暴露了传统架构的核心问题。SQLAlchemy 虽然建立了对象关系映射( ORM ),让数据库表可以通过 Python 对象来操作,但这种映射关系过于紧密。ORM Model 既承担了数据持久化的职责,又要表达业务概念,导致对象无法自由地代表业务模型。业务实体被数据库的实现细节所绑架,每个字段、每个关系都必须与数据库表结构一一对应,完全失去了作为独立业务概念存在的自由。

更深层次的问题包括:

  1. Domain Layer 被 SQLAlchemy 绑定:业务实体继承了 SQLAlchemy 的 Base ,无法独立于数据库存在
  2. 业务逻辑无法脱离数据库测试:编写单元测试时必须启动完整的数据库环境,大大降低了测试效率
  3. 切换数据库需要修改所有层:当从 PostgreSQL 迁移到 MongoDB 时,所有使用 ORM Model 的代码都需要重写


。。。

基于 Pydantic-Resolve 和 FastAPI-Voyager 的 Clean Architecture 实践

篇幅较长无法粘贴全文,原文链接:
https://github.com/allmonday/A-Python-web-development-methodology-for-complex-business-scenarios/blob/main/README.zh.md

一套面向复杂业务场景的 Python Web 开发方法论

目录


1. 背景与问题

1.1 当前主流做法及其痛点

在 Python Web 开发中,处理复杂业务场景时,开发者通常采用以下几种模式:

模式一:直接使用 ORM (如 SQLAlchemy )

@router.get("/teams/{team_id}", response_model=TeamDetail)
async def get_team(team_id: int, session: AsyncSession = Depends(get_session)):
    # 获取团队基本信息
    team = await session.get(Team, team_id)

    # 获取 Sprint 列表
    sprints = await session.execute(
        select(Sprint).where(Sprint.team_id == team_id)
    )
    team.sprints = sprints.scalars().all()

    # 获取每个 Sprint 的 Story
    for sprint in team.sprints:
        stories = await session.execute(
            select(Story).where(Story.sprint_id == sprint.id)
        )
        sprint.stories = stories.scalars().all()

        # 获取每个 Story 的 Task
        for story in sprint.stories:
            tasks = await session.execute(
                select(Task).where(Task.story_id == story.id)
            )
            story.tasks = tasks.scalars().all()

            # 获取每个 Task 的负责人
            for task in story.tasks:
                task.owner = await session.get(User, task.owner_id)

    return team

这种做法在简单场景下确实很直观,能够快速上手。ORM 的类型安全特性也能在编译时发现一些错误,而且与数据库表结构的一一对应关系让代码容易理解。但当我们面对真正的业务场景时,这种方式的缺陷很快就暴露出来了。

最致命的问题是 N+1 查询。虽然代码看起来很清晰,但执行时会产生大量的数据库查询。每当我们访问一个关联关系时,ORM 就会发起一次新的查询。在深层嵌套的情况下,查询数量会呈指数级增长。更糟糕的是,这种性能问题在开发阶段不容易发现,只有当数据量积累到一定程度后才会显现出来,那时候往往已经太晚了。

代码的组织方式也是个问题。数据获取的逻辑散落在各个嵌套的循环中,业务逻辑和数据获取逻辑混在一起,难以阅读和维护。当需要修改业务规则时,开发者不得不在复杂的嵌套结构中寻找修改点,很容易引入新的 bug 。性能更是不可控,随着数据量的增长,查询效率会急剧下降,而这些性能瓶颈很难在代码层面直接观察到。

此外,相似的数据获取逻辑会在多个 API 中重复出现,导致大量代码冗余。当一个 API 需要获取"团队及其 Sprint",另一个 API 需要"团队及其成员"时,即使它们的查询逻辑非常相似,也不得不重复编写。这违反了 DRY ( Don't Repeat Yourself )原则,增加了维护成本。

模式二:使用 ORM 的 Eager Loading

@router.get("/teams/{team_id}", response_model=TeamDetail)
async def get_team(team_id: int, session: AsyncSession = Depends(get_session)):
    # 使用 joinedload 预加载关联数据
    result = await session.execute(
        select(Team)
        .options(
            joinedload(Team.sprints)
            .joinedload(Sprint.stories)
            .joinedload(Story.tasks)
            .joinedload(Task.owner)
        )
        .where(Team.id == team_id)
    )
    return result.scalar_one()

为了解决 N+1 查询问题,ORM 提供了 Eager Loading 机制,让我们可以通过 joinedloadselectinload 等方式预先加载关联数据。代码变得更简洁了,性能问题也得到了缓解。但这种方案也带来了新的挑战。

最明显的问题是笛卡尔积。当我们使用多层 JOIN 预加载关联数据时,数据库返回的数据量会急剧膨胀。比如一个团队有 10 个 Sprint ,每个 Sprint 有 10 个 Story ,每个 Story 有 10 个 Task ,那么 JOIN 的结果集会包含 1000 行数据,即使每行的数据量不大,也会给网络传输和内存占用带来压力。

更严重的问题是灵活性差。Eager Loading 的策略是在代码中硬编码的,所有使用同一个 Model 的 API 都会执行相同的预加载逻辑。但不同的 API 往往需要不同的数据。比如一个 API 只需要团队的基本信息,另一个 API 需要团队的 Sprint ,还有一个 API 需要团队的成员。如果统一使用 Eager Loading 加载所有关联数据,就会出现过度获取的问题,前端不需要的数据也被查询和传输了,浪费了资源。

配置 Eager Loading 本身就很复杂。开发者需要理解 lazyjoinedloadselectinloadsubquery 等多种加载策略的区别,知道什么时候用哪一种,以及它们各自会有什么副作用。这种配置错误很容易导致性能问题或意外的数据加载行为。而且,这种"一刀切"的配置方式意味着所有 API 都使用相同的加载策略,无法针对特定场景进行优化。

模式三:手动组装数据

@router.get("/teams/{team_id}", response_model=TeamDetail)
async def get_team(team_id: int, session: AsyncSession = Depends(get_session)):
    # 1. 批量获取所有需要的数据
    team = await session.get(Team, team_id)

    sprints_result = await session.execute(
        select(Sprint).where(Sprint.team_id == team_id)
    )
    sprint_ids = [s.id for s in sprints_result.scalars().all()]

    stories_result = await session.execute(
        select(Story).where(Story.sprint_id.in_(sprint_ids))
    )
    story_ids = [s.id for s in stories_result.scalars().all()]

    tasks_result = await session.execute(
        select(Task).where(Story.id.in_(story_ids))
    )
    tasks = tasks_result.scalars().all()

    owner_ids = list(set(t.owner_id for t in tasks))
    owners_result = await session.execute(
        select(User).where(User.id.in_(owner_ids))
    )
    owners = {u.id: u for u in owners_result.scalars().all()}

    # 2. 手动组装数据结构
    sprint_dict = {s.id: s for s in sprints_result.scalars().all()}
    story_dict = {s.id: s for s in stories_result.scalars().all()}

    for story in story_dict.values():
        story.tasks = [t for t in tasks if t.story_id == story.id]
        for task in story.tasks:
            task.owner = owners.get(task.owner_id)

    for sprint in sprint_dict.values():
        sprint.stories = [s for s in story_dict.values() if s.sprint_id == sprint.id]

    team.sprints = list(sprint_dict.values())

    return team

为了获得最优的性能和精确的数据控制,有经验的开发者会选择手动组装数据。这种方式完全掌控查询逻辑,可以精确控制每个查询的 SQL 语句,避免不必要的数据库访问。通过批量查询和智能的数据组装,可以获得最佳的性能,而且没有冗余数据。

但这种方式的代价是代码变得非常冗长。如上面的例子所示,为了获取一个团队的完整信息,我们需要编写多个查询,手动构建数据字典,然后通过嵌套循环组装数据。代码的长度和复杂度都大幅增加,而真正表达业务逻辑的代码反而被淹没在数据组装的细节中。

更容易出错也是个大问题。手动组装数据涉及到大量的索引操作和循环嵌套,很容易出现索引错误、空指针引用等 bug 。而且这些错误往往只有在运行时、特定数据条件下才会暴露,难以在开发阶段发现。

维护成本更是高昂。当业务规则发生变化时(比如需要添加一个新的关联关系),开发者需要在所有相关的 API 中修改数据组装逻辑。如果遗漏了某个地方,就会导致数据不一致。而且,相似的数据组装逻辑会在多个 API 中重复出现,违反了 DRY 原则。

最根本的问题是,这种代码已经变成了纯粹的数据搬运工,看不出任何业务意图。代码中充满了字典操作、循环嵌套、索引查找,而这些都是技术细节,与业务需求毫无关系。新加入的团队成员很难从这些代码中理解业务逻辑,业务知识的传递变得异常困难。

模式四:使用 GraphQL

type Query {
    team(id: ID!): Team
}

type Team {
    id: ID!
    name: String!
    sprints: [Sprint!]!
}

type Sprint {
    id: ID!
    name: String!
    stories: [Story!]!
}

type Story {
    id: ID!
    name: String!
    tasks: [Task!]!
}

type Task {
    id: ID!
    name: String!
    owner: User!
}

GraphQL 确实是一个很有吸引力的方案。前端可以按需获取数据,需要什么字段就查什么字段,不会有过度获取的问题。它提供了类型安全的查询接口,而且通过 DataLoader 可以自动解决 N+1 查询问题。这些特性让 GraphQL 在前端开发中广受欢迎。

但 GraphQL 的学习曲线非常陡峭。开发者需要学习全新的查询语言、Schema 定义、Resolver 编写、DataLoader 配置等一堆概念,这与 REST API 的直观性形成了鲜明对比。更麻烦的是,GraphQL 的过度灵活性给后端带来了巨大的挑战。前端可以构造任意复杂的查询,有些查询甚至可能是开发者没有想到过的,这导致后端很难进行针对性的优化。当一个查询嵌套了 10 层,返回了数百万条数据时,数据库和服务器都会面临巨大的压力。

调试 GraphQL API 也比调试 REST API 复杂得多。当一个 GraphQL 查询出错时,错误信息往往很难定位到具体的问题源头。而且 GraphQL 需要额外的服务器和工具链支持,无法直接利用现有的 FastAPI 生态系统。比如 FastAPI 的依赖注入、中间件、自动文档生成等特性,在 GraphQL 中都无法直接使用。

还有一个更深层次的问题是 ERD 和用例的界限模糊。GraphQL 的 Schema 同时扮演了实体模型和查询接口两个角色。当我们设计一个 GraphQL Schema 时,很难确定应该按照实体来组织(一个 Type 对应一个数据库表),还是按照用例来组织(不同的业务场景需要不同的字段)。这导致最佳实践不清晰,不同的项目、不同的开发者可能有完全不同的组织方式。

而且随着业务增长,所有的用例都会堆砌在同一个 Schema 中,导致 Schema 膨胀,难以维护。权限控制也变得异常复杂。不同的 API 端点可能有不同的权限要求,但它们可能都查询同一个实体(比如 User ),在 GraphQL 中很难针对不同的查询场景应用不同的权限规则。

1.2 问题根源分析

上面我们探讨的所有模式,虽然表面上的问题各不相同,但它们的核心困境其实是一致的。

问题 1:业务模型与数据模型混淆

# SQLAlchemy ORM 同时扮演两个角色:
# 1. 数据模型(如何存储)
# 2. 业务模型(业务概念)

class Team(Base):
    __tablename__ = 'teams'

    id = Column(Integer, primary_key=True)
    name = Column(String)

    # 这是数据库的外键关系,还是业务关系?
    sprints = relationship("Sprint", back_populates="team")

在传统的 ORM 开发中,业务模型和数据模型是混在一起的。看看这个例子,Team 类既表达了业务概念(团队是什么),又承载了数据模型的细节(如何在数据库中存储)。当我们在 sprints 字段上定义 relationship 时,这到底是在描述一个业务关系(团队有多个 Sprint ),还是在声明一个数据库外键约束?这种模糊性会导致很多问题。

数据库的设计约束会直接影响我们的业务建模。比如,如果数据库中的 teams 表没有直接到 users 的外键,而是通过中间表 team_members 关联,那么在 ORM 中我们也必须通过这个中间表来定义关系。这意味着业务模型被迫适应数据库的实现细节,而不是反过来。

更严重的是,这种方式无法表达跨库、跨服务的业务关系。现代系统中,数据可能分布在不同的数据库中,甚至存储在外部服务里。比如用户的基本信息在 PostgreSQL ,而用户的偏好设置在 MongoDB ,用户的实时状态在 Redis 中。ORM 的 relationship 无法跨越这些边界,业务模型因此被限制在了单一数据库的范围内。

问题 2:依赖方向错误

传统架构的依赖方向:
┌─────────────┐
│   API Layer │  ← 依赖于
└──────┬──────┘
       │
       ↓
┌─────────────┐
│ ORM Models  │  ← 依赖于
└──────┬──────┘
       │
       ↓
┌─────────────┐
│  Database   │
└─────────────┘

问题:业务规则依赖于数据库实现!

这违反了 Clean Architecture 的依赖规则。正确的依赖关系应该是:业务规则最稳定,不依赖任何外层;数据库是实现细节,应该依赖业务规则;当数据库变化时,业务规则不应该受影响。但传统架构的依赖方向恰恰相反,业务规则被数据库的实现细节所绑架。

问题 3:缺少业务关系的显式声明

# 传统方式:业务关系隐藏在查询中
async def get_team_tasks(team_id: int):
    # "团队的任务"这个业务概念隐藏在 SQL WHERE 中
    result = await session.execute(
        select(Task)
        .join(Sprint, Sprint.id == Task.sprint_id)
        .where(Sprint.team_id == team_id)
    )
    return result.scalars().all()

业务关系没有被显式声明出来,这是个很隐蔽但危害很大的问题。看看这个例子,"团队的任务"是一个清晰的业务概念,但这个概念被隐藏在 SQL 的 JOIN 和 WHERE 子句中。新加入团队的成员需要阅读大量代码才能理解系统中有哪些业务关系,这些关系是如何定义的。更糟糕的是,没有自动化的方式来检查业务关系的一致性。当需求变化需要修改某个关系时,开发者很难找到所有相关的代码,很容易遗漏某个地方,导致业务逻辑的不一致。

问题 4:中间表的技术暴露

在 SQLAlchemy ORM 中,多对多关系需要显式定义中间表,这导致技术细节泄漏到业务层。

# SQLAlchemy ORM:必须定义中间表
class Team(Base):
    __tablename__ = 'teams'
    id = Column(Integer, primary_key=True)
    name = Column(String)

    # ORM relationship 需要指定中间表
    members = relationship("User",
                          secondary="team_members",  # 必须指定中间表
                          back_populates="teams")

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String)

    teams = relationship("Team",
                        secondary="team_members",  # 必须指定中间表
                        back_populates="members")

# 中间表(技术实现细节)
class TeamMember(Base):
    __tablename__ = 'team_members'
    team_id = Column(Integer, ForeignKey('teams.id'), primary_key=True)
    user_id = Column(Integer, ForeignKey('users.id'), primary_key=True)
    role = Column(String)  # 可能还有额外字段

# 查询时需要关心中间表的存在
@router.get("/teams/{team_id}")
async def get_team_members(team_id: int, session: AsyncSession):
    # 必须通过中间表查询
    result = await session.execute(
        select(User)
        .join(TeamMember, TeamMember.user_id == User.id)  # 中间表暴露
        .where(TeamMember.team_id == team_id)
    )
    return result.scalars().all()

这个问题的根源在于,ORM 的多对多关系需要显式定义中间表,这导致技术细节直接泄漏到业务层代码中。业务代码必须知道 team_members 中间表的存在,查询时也需要显式地 join 这个中间表。这增加了代码复杂度,更重要的是,业务逻辑被数据库的实现细节所绑架。

更深层的问题是业务语义变得模糊。TeamMember 到底是一个有意义的业务概念,还是纯粹的技术实现?如果中间表还有额外的字段(比如 role 表示用户在团队中的角色,joined_at 表示加入时间),这些字段应该被建模为独立的实体吗?不同的开发者可能给出不同的答案,缺乏统一的指导原则。

数据组装也因此变得复杂。查询"团队的所有成员"需要 join 中间表,查询"用户所属的团队"也需要 join 中间表。所有涉及多对多关系的查询都变得冗长和难以理解。当业务规则要求"获取用户在所有团队中的角色"时,情况就更加复杂了。这些技术细节让业务逻辑的实现变得异常沉重。

对比:Pydantic-Resolve ERD 的方式

# ERD:业务概念清晰,无需关心中间表
class TeamEntity(BaseModel, BaseEntity):
    """团队实体 - 业务概念"""
    __relationships__ = [
        # 直接表达"团队有多个成员"的业务关系
        Relationship(
            field='id',
            target_kls=list[UserEntity],
            loader=team_to_users_loader  # loader 内部处理中间表
        ),
    ]
    id: int
    name: str

class UserEntity(BaseModel, BaseEntity):
    """用户实体 - 业务概念"""
    __relationships__ = [
        # 直接表达"用户属于多个团队"的业务关系
        Relationship(
            field='id',
            target_kls=list[TeamEntity],
            loader=user_to_teams_loader
        ),
    ]
    id: int
    name: str

# Loader 实现细节:中间表只在这里出现
async def team_to_users_loader(team_ids: list[int]):
    """加载团队成员 - 内部处理中间表"""
    async with get_session() as session:
        # 只有这里需要知道中间表的存在
        result = await session.execute(
            select(User)
            .join(TeamMember, TeamMember.user_id == User.id)
            .where(TeamMember.team_id.in_(team_ids))
        )
        users = result.scalars().all()

        # 构建映射
        users_by_team = {}
        for user in users:
            for tm in user.team_memberships:
                if tm.team_id not in users_by_team:
                    users_by_team[tm.team_id] = []
                users_by_team[tm.team_id].append(user)

        return [users_by_team.get(tid, []) for tid in team_ids]

关键差异

维度 SQLAlchemy ORM Pydantic-Resolve ERD
中间表位置 暴露在业务层 隐藏在 loader 实现中
业务语义 技术关系 (secondary) 业务关系 (团队包含成员)
查询代码 需要 join 中间表 loader.load(team_id)
代码位置 分散在多处 集中在 loader
测试 依赖数据库表结构 可 mock loader

架构优势

传统方式:
Team → TeamMember (中间表) → User
业务层需要知道中间表的存在

Pydantic-Resolve 方式:
Team → User (业务关系)
中间表是数据层的实现细节,业务层不关心

这意味着:

  1. 业务模型纯净:Team 和 User 的关系直接表达业务语义

  2. 技术细节封装:中间表的存在被封装在 loader 中

  3. 灵活的存储策略


    • 数据库可以用中间表实现
    • 也可以用 JSON 字段存储
    • 甚至可以是外部服务(如 LDAP )
    • 业务层代码无需修改
  4. 易于理解:新人看到 ERD 就能理解业务关系,不需要先学习数据库设计


2. Clean Architecture 思想

2.1 核心原则

Clean Architecture 由 Robert C. Martin (Uncle Bob) 提出,核心思想是:

"Software architecture is the art of drawing lines that I call boundaries."
软件架构的艺术在于画界线。

原则 1:依赖规则

外层依赖内层,内层不依赖外层。

                ↓ 依赖方向
    ┌─────────────────────┐
    │   Frameworks &      │  外层
    │   Drivers           │  (实现细节)
    ├─────────────────────┤
    │   Interface         │
    │   Adapters          │
    ├─────────────────────┤
    │   Use Cases         │
    │   (Application)     │
    ├─────────────────────┤
    │   Entities          │  内层
    │   (Business Rules)  │  (核心)
    └─────────────────────┘

遵循依赖规则有几个关键点需要注意。首先,内层不知道外层的存在,这意味着核心业务逻辑不依赖于任何框架、数据库或 UI 的细节。其次,内层不包含外层的信息,比如业务规则不应该知道数据是用 PostgreSQL 还是 MongoDB 存储的。最后,外层的实现可以随时替换而不影响内层,这意味着我们可以从 SQLAlchemy 切换到 MongoDB ,或者从 FastAPI 切换到 Django ,而业务逻辑代码无需修改。

原则 2:业务规则独立

# ❌ 错误:业务规则依赖数据库
class Task:
    def calculate_priority(self, session):
        # 业务逻辑被数据库实现细节污染
        if self.assignee_id in session.query(TeamMember).filter_by(role='lead'):
            return 'high'

# ✅ 正确:业务规则独立
class Task:
    def calculate_priority(self, assignee_roles):
        # 业务逻辑只依赖业务概念
        if 'lead' in assignee_roles:
            return 'high'

原则 3:跨边界的数据传递

# 内层定义数据结构
class TaskEntity(BaseModel):
    id: int
    name: str
    assignee_id: int

# 外层负责转换
def task_entity_to_orm(entity: TaskEntity) -> Task:
    return Task(
        id=entity.id,
        name=entity.name,
        assignee_id=entity.assignee_id
    )

2.2 依赖规则

在 Web 开发中,依赖规则可以这样理解:

┌────────────────────────────────────────────────────┐
│         Presentation Layer (外层)                   │
│  - FastAPI Routes                                   │
│  - Request/Response Models                          │
│  - 依赖: Application Layer                          │
└────────────────────────────────────────────────────┘
                    ↓
┌────────────────────────────────────────────────────┐
│      Application Layer (Use Cases)                 │
│  - 业务用例(获取用户、创建订单)                    │
│  - 依赖: Domain Layer                               │
└────────────────────────────────────────────────────┘
                    ↓
┌────────────────────────────────────────────────────┐
│           Domain Layer (内层)                      │
│  - Entities (业务实体)                              │
│  - Business Rules (业务规则)                        │
│  - Value Objects (值对象)                           │
│  - 不依赖任何外层                                    │
└────────────────────────────────────────────────────┘
                    ↓
┌────────────────────────────────────────────────────┐
│    Infrastructure Layer (最外层)                   │
│  - Database (SQLAlchemy)                           │
│  - External Services                               │
│  - File System                                     │
└────────────────────────────────────────────────────┘

关键洞察

  • Entities 不应该知道 SQLAlchemy 的存在
  • Business Rules 不应该知道数据库表结构
  • Use Cases 不应该知道 HTTP 协议的细节

2.3 在 Web 开发中的应用

传统架构的问题

# 传统方式:所有层次耦合

# Domain Layer (应该独立,但实际上依赖了 ORM)
class User(Base):  # ← SQLAlchemy Base
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)

# Application Layer (应该只依赖 Domain ,但直接使用了 ORM)
async def create_user(data: dict, session: AsyncSession):
    user = User(**data)  # ← 直接使用 ORM Model
    session.add(user)
    await session.commit()

# Presentation Layer
@router.post("/users")
async def api_create_user(data: dict, session=Depends(get_session)):
    return await create_user(data, session)  # ← 暴露了数据库细节

这段代码暴露了传统架构的核心问题。SQLAlchemy 虽然建立了对象关系映射( ORM ),让数据库表可以通过 Python 对象来操作,但这种映射关系过于紧密。ORM Model 既承担了数据持久化的职责,又要表达业务概念,导致对象无法自由地代表业务模型。业务实体被数据库的实现细节所绑架,每个字段、每个关系都必须与数据库表结构一一对应,完全失去了作为独立业务概念存在的自由。

更深层次的问题包括:

  1. Domain Layer 被 SQLAlchemy 绑定:业务实体继承了 SQLAlchemy 的 Base ,无法独立于数据库存在
  2. 业务逻辑无法脱离数据库测试:编写单元测试时必须启动完整的数据库环境,大大降低了测试效率
  3. 切换数据库需要修改所有层:当从 PostgreSQL 迁移到 MongoDB 时,所有使用 ORM Model 的代码都需要重写


。。。

Instant Melon (即刻瓜田)

一个专注于匿名故事分享与互动的开源平台。
“每个人都有故事,这里是你的树洞,也是你的瓜田。”

项目简介

最近闲来无事(忘记在哪刷到了一个类似的平台),就 A 了一个匿名吃瓜平台。
起因是想做一个纯粹的、无压力的吐槽和分享空间。项目目前已经开源,欢迎各位佬品鉴。

给我一些优化建议。如果觉得还不错,求一个宝贵的 Star

GitHub 地址: https://github.com/AIME-JF/Instant-Melon
在线地址:Instant-Melon


核心功能

  • 匿名发布:无需复杂的注册流程(匿名),保护隐私,畅所欲言。
  • 吃瓜互动:支持评论、双击点赞、表情互动,让吃瓜更有趣。
  • 清新 UI:精心设计的界面,适配移动端与桌面端,阅读体验极佳。
  • 内容管理:内置简单的审核机制,防止滥用。

  • 前端框架:Next.js (App Router) / Vue3
  • UI 组件:TailwindCSS / ShadcnUI
  • 后端 / 数据库: PostgreSQL / FastAPI

(PS:现在没啥内容在里面,因为根本没人玩 ,有几个内容是搬站内佬的)

快速开始 / 部署

  1. git clone [https://github.com/AIME-JF/Instant-Melon.git](https://github.com/AIME-JF/Instant-Melon.git)
    

📌 转载信息
转载时间:
2026/1/14 10:59:40

演示站点:https://mirror.dal.ao

项目地址:dalao-org/MirrorOne

老项目重构求点 Star

前言

可能很多人还记得前几年的 Oneinstack/lnmp 脚本被收购后出现供应链投毒的问题。

OneInStack 疑似供应链投毒 Nginx

但是投毒的方式并不是在脚本本身进行了修改,而是在脚本依赖的 oneinstack 托管的软件包里直接进行了投毒。相当于是脚本下载了含木马的软件包。因此,在那之后我就写了一个 Python 脚本来爬取 Oneinstack 全部依赖的软件包的官方下载地址,并通过 GitHub Actions 自动运行,生成 Cloudflare/Netlify 支持的跳转脚本。当 lnmp 这一类脚本请求下载时,会被自动跳转到原始的官方链接。

当时的代码记录

举个例子

当你请求下载 MySQL 8.4.6 的时候,请求镜像地址 https://mirror.dal.ao/src/mysql-8.4.6-linux-glibc2.28-x86_64.tar.xz 会直接 302 跳转到 https://downloads.mysql.com/archives/get/p/23/file/mysql-8.4.6-linux-glibc2.28-x86_64.tar.xz

MirrorOne

这两天因为重新部署服务器,因此重新把这个项目拾起来重构了一番。将原本依赖 GitHub Actions 的运行方式彻底抛弃了,改为 FastAPI 后端 + 定时任务来处理。主要原因有以下几个:

  1. GitHub Actions 访问 PHP 的官方站点频繁出错
  2. Netlify 和 Cloudflare Pages 都有跳转规则数量上限
  3. 自托管可以处理局域网等受限网络的情况

新的特点

  1. 新增了 WebUI 界面,允许管理员进行设置,包括对抓取的软件包版本的限制、版本类型限制。避免了命令行程序设置的麻烦。
  1. 重定向和缓存的双重镜像模式。在默认的重定向模式下,对软件包下载的请求仍然会被重定向到官方下载链接;而缓存模式下,会从托管的服务器下载。但为了避免恶意利用,当系统在缓存模式下时,仍然可以通过 force_redirect=true 请求参数来强制使用重定向模式。
  2. 标准化的爬虫 Scraper 基类,可以更容易地添加更多软件包。

📌 转载信息
转载时间:
2026/1/14 10:53:34

基于 Pydantic-Resolve 和 FastAPI-Voyager 的 Clean Architecture 实践

篇幅较长无法粘贴全文,原文链接:
https://github.com/allmonday/A-Python-web-development-methodology-for-complex-business-scenarios/blob/main/README.zh.md

一套面向复杂业务场景的 Python Web 开发方法论

目录


1. 背景与问题

1.1 当前主流做法及其痛点

在 Python Web 开发中,处理复杂业务场景时,开发者通常采用以下几种模式:

模式一:直接使用 ORM (如 SQLAlchemy )

@router.get("/teams/{team_id}", response_model=TeamDetail)
async def get_team(team_id: int, session: AsyncSession = Depends(get_session)):
    # 获取团队基本信息
    team = await session.get(Team, team_id)

    # 获取 Sprint 列表
    sprints = await session.execute(
        select(Sprint).where(Sprint.team_id == team_id)
    )
    team.sprints = sprints.scalars().all()

    # 获取每个 Sprint 的 Story
    for sprint in team.sprints:
        stories = await session.execute(
            select(Story).where(Story.sprint_id == sprint.id)
        )
        sprint.stories = stories.scalars().all()

        # 获取每个 Story 的 Task
        for story in sprint.stories:
            tasks = await session.execute(
                select(Task).where(Task.story_id == story.id)
            )
            story.tasks = tasks.scalars().all()

            # 获取每个 Task 的负责人
            for task in story.tasks:
                task.owner = await session.get(User, task.owner_id)

    return team

这种做法在简单场景下确实很直观,能够快速上手。ORM 的类型安全特性也能在编译时发现一些错误,而且与数据库表结构的一一对应关系让代码容易理解。但当我们面对真正的业务场景时,这种方式的缺陷很快就暴露出来了。

最致命的问题是 N+1 查询。虽然代码看起来很清晰,但执行时会产生大量的数据库查询。每当我们访问一个关联关系时,ORM 就会发起一次新的查询。在深层嵌套的情况下,查询数量会呈指数级增长。更糟糕的是,这种性能问题在开发阶段不容易发现,只有当数据量积累到一定程度后才会显现出来,那时候往往已经太晚了。

代码的组织方式也是个问题。数据获取的逻辑散落在各个嵌套的循环中,业务逻辑和数据获取逻辑混在一起,难以阅读和维护。当需要修改业务规则时,开发者不得不在复杂的嵌套结构中寻找修改点,很容易引入新的 bug 。性能更是不可控,随着数据量的增长,查询效率会急剧下降,而这些性能瓶颈很难在代码层面直接观察到。

此外,相似的数据获取逻辑会在多个 API 中重复出现,导致大量代码冗余。当一个 API 需要获取"团队及其 Sprint",另一个 API 需要"团队及其成员"时,即使它们的查询逻辑非常相似,也不得不重复编写。这违反了 DRY ( Don't Repeat Yourself )原则,增加了维护成本。

模式二:使用 ORM 的 Eager Loading

@router.get("/teams/{team_id}", response_model=TeamDetail)
async def get_team(team_id: int, session: AsyncSession = Depends(get_session)):
    # 使用 joinedload 预加载关联数据
    result = await session.execute(
        select(Team)
        .options(
            joinedload(Team.sprints)
            .joinedload(Sprint.stories)
            .joinedload(Story.tasks)
            .joinedload(Task.owner)
        )
        .where(Team.id == team_id)
    )
    return result.scalar_one()

为了解决 N+1 查询问题,ORM 提供了 Eager Loading 机制,让我们可以通过 joinedloadselectinload 等方式预先加载关联数据。代码变得更简洁了,性能问题也得到了缓解。但这种方案也带来了新的挑战。

最明显的问题是笛卡尔积。当我们使用多层 JOIN 预加载关联数据时,数据库返回的数据量会急剧膨胀。比如一个团队有 10 个 Sprint ,每个 Sprint 有 10 个 Story ,每个 Story 有 10 个 Task ,那么 JOIN 的结果集会包含 1000 行数据,即使每行的数据量不大,也会给网络传输和内存占用带来压力。

更严重的问题是灵活性差。Eager Loading 的策略是在代码中硬编码的,所有使用同一个 Model 的 API 都会执行相同的预加载逻辑。但不同的 API 往往需要不同的数据。比如一个 API 只需要团队的基本信息,另一个 API 需要团队的 Sprint ,还有一个 API 需要团队的成员。如果统一使用 Eager Loading 加载所有关联数据,就会出现过度获取的问题,前端不需要的数据也被查询和传输了,浪费了资源。

配置 Eager Loading 本身就很复杂。开发者需要理解 lazyjoinedloadselectinloadsubquery 等多种加载策略的区别,知道什么时候用哪一种,以及它们各自会有什么副作用。这种配置错误很容易导致性能问题或意外的数据加载行为。而且,这种"一刀切"的配置方式意味着所有 API 都使用相同的加载策略,无法针对特定场景进行优化。

模式三:手动组装数据

@router.get("/teams/{team_id}", response_model=TeamDetail)
async def get_team(team_id: int, session: AsyncSession = Depends(get_session)):
    # 1. 批量获取所有需要的数据
    team = await session.get(Team, team_id)

    sprints_result = await session.execute(
        select(Sprint).where(Sprint.team_id == team_id)
    )
    sprint_ids = [s.id for s in sprints_result.scalars().all()]

    stories_result = await session.execute(
        select(Story).where(Story.sprint_id.in_(sprint_ids))
    )
    story_ids = [s.id for s in stories_result.scalars().all()]

    tasks_result = await session.execute(
        select(Task).where(Story.id.in_(story_ids))
    )
    tasks = tasks_result.scalars().all()

    owner_ids = list(set(t.owner_id for t in tasks))
    owners_result = await session.execute(
        select(User).where(User.id.in_(owner_ids))
    )
    owners = {u.id: u for u in owners_result.scalars().all()}

    # 2. 手动组装数据结构
    sprint_dict = {s.id: s for s in sprints_result.scalars().all()}
    story_dict = {s.id: s for s in stories_result.scalars().all()}

    for story in story_dict.values():
        story.tasks = [t for t in tasks if t.story_id == story.id]
        for task in story.tasks:
            task.owner = owners.get(task.owner_id)

    for sprint in sprint_dict.values():
        sprint.stories = [s for s in story_dict.values() if s.sprint_id == sprint.id]

    team.sprints = list(sprint_dict.values())

    return team

为了获得最优的性能和精确的数据控制,有经验的开发者会选择手动组装数据。这种方式完全掌控查询逻辑,可以精确控制每个查询的 SQL 语句,避免不必要的数据库访问。通过批量查询和智能的数据组装,可以获得最佳的性能,而且没有冗余数据。

但这种方式的代价是代码变得非常冗长。如上面的例子所示,为了获取一个团队的完整信息,我们需要编写多个查询,手动构建数据字典,然后通过嵌套循环组装数据。代码的长度和复杂度都大幅增加,而真正表达业务逻辑的代码反而被淹没在数据组装的细节中。

更容易出错也是个大问题。手动组装数据涉及到大量的索引操作和循环嵌套,很容易出现索引错误、空指针引用等 bug 。而且这些错误往往只有在运行时、特定数据条件下才会暴露,难以在开发阶段发现。

维护成本更是高昂。当业务规则发生变化时(比如需要添加一个新的关联关系),开发者需要在所有相关的 API 中修改数据组装逻辑。如果遗漏了某个地方,就会导致数据不一致。而且,相似的数据组装逻辑会在多个 API 中重复出现,违反了 DRY 原则。

最根本的问题是,这种代码已经变成了纯粹的数据搬运工,看不出任何业务意图。代码中充满了字典操作、循环嵌套、索引查找,而这些都是技术细节,与业务需求毫无关系。新加入的团队成员很难从这些代码中理解业务逻辑,业务知识的传递变得异常困难。

模式四:使用 GraphQL

type Query {
    team(id: ID!): Team
}

type Team {
    id: ID!
    name: String!
    sprints: [Sprint!]!
}

type Sprint {
    id: ID!
    name: String!
    stories: [Story!]!
}

type Story {
    id: ID!
    name: String!
    tasks: [Task!]!
}

type Task {
    id: ID!
    name: String!
    owner: User!
}

GraphQL 确实是一个很有吸引力的方案。前端可以按需获取数据,需要什么字段就查什么字段,不会有过度获取的问题。它提供了类型安全的查询接口,而且通过 DataLoader 可以自动解决 N+1 查询问题。这些特性让 GraphQL 在前端开发中广受欢迎。

但 GraphQL 的学习曲线非常陡峭。开发者需要学习全新的查询语言、Schema 定义、Resolver 编写、DataLoader 配置等一堆概念,这与 REST API 的直观性形成了鲜明对比。更麻烦的是,GraphQL 的过度灵活性给后端带来了巨大的挑战。前端可以构造任意复杂的查询,有些查询甚至可能是开发者没有想到过的,这导致后端很难进行针对性的优化。当一个查询嵌套了 10 层,返回了数百万条数据时,数据库和服务器都会面临巨大的压力。

调试 GraphQL API 也比调试 REST API 复杂得多。当一个 GraphQL 查询出错时,错误信息往往很难定位到具体的问题源头。而且 GraphQL 需要额外的服务器和工具链支持,无法直接利用现有的 FastAPI 生态系统。比如 FastAPI 的依赖注入、中间件、自动文档生成等特性,在 GraphQL 中都无法直接使用。

还有一个更深层次的问题是 ERD 和用例的界限模糊。GraphQL 的 Schema 同时扮演了实体模型和查询接口两个角色。当我们设计一个 GraphQL Schema 时,很难确定应该按照实体来组织(一个 Type 对应一个数据库表),还是按照用例来组织(不同的业务场景需要不同的字段)。这导致最佳实践不清晰,不同的项目、不同的开发者可能有完全不同的组织方式。

而且随着业务增长,所有的用例都会堆砌在同一个 Schema 中,导致 Schema 膨胀,难以维护。权限控制也变得异常复杂。不同的 API 端点可能有不同的权限要求,但它们可能都查询同一个实体(比如 User ),在 GraphQL 中很难针对不同的查询场景应用不同的权限规则。

1.2 问题根源分析

上面我们探讨的所有模式,虽然表面上的问题各不相同,但它们的核心困境其实是一致的。

问题 1:业务模型与数据模型混淆

# SQLAlchemy ORM 同时扮演两个角色:
# 1. 数据模型(如何存储)
# 2. 业务模型(业务概念)

class Team(Base):
    __tablename__ = 'teams'

    id = Column(Integer, primary_key=True)
    name = Column(String)

    # 这是数据库的外键关系,还是业务关系?
    sprints = relationship("Sprint", back_populates="team")

在传统的 ORM 开发中,业务模型和数据模型是混在一起的。看看这个例子,Team 类既表达了业务概念(团队是什么),又承载了数据模型的细节(如何在数据库中存储)。当我们在 sprints 字段上定义 relationship 时,这到底是在描述一个业务关系(团队有多个 Sprint ),还是在声明一个数据库外键约束?这种模糊性会导致很多问题。

数据库的设计约束会直接影响我们的业务建模。比如,如果数据库中的 teams 表没有直接到 users 的外键,而是通过中间表 team_members 关联,那么在 ORM 中我们也必须通过这个中间表来定义关系。这意味着业务模型被迫适应数据库的实现细节,而不是反过来。

更严重的是,这种方式无法表达跨库、跨服务的业务关系。现代系统中,数据可能分布在不同的数据库中,甚至存储在外部服务里。比如用户的基本信息在 PostgreSQL ,而用户的偏好设置在 MongoDB ,用户的实时状态在 Redis 中。ORM 的 relationship 无法跨越这些边界,业务模型因此被限制在了单一数据库的范围内。

问题 2:依赖方向错误

传统架构的依赖方向:
┌─────────────┐
│   API Layer │  ← 依赖于
└──────┬──────┘
       │
       ↓
┌─────────────┐
│ ORM Models  │  ← 依赖于
└──────┬──────┘
       │
       ↓
┌─────────────┐
│  Database   │
└─────────────┘

问题:业务规则依赖于数据库实现!

这违反了 Clean Architecture 的依赖规则。正确的依赖关系应该是:业务规则最稳定,不依赖任何外层;数据库是实现细节,应该依赖业务规则;当数据库变化时,业务规则不应该受影响。但传统架构的依赖方向恰恰相反,业务规则被数据库的实现细节所绑架。

问题 3:缺少业务关系的显式声明

# 传统方式:业务关系隐藏在查询中
async def get_team_tasks(team_id: int):
    # "团队的任务"这个业务概念隐藏在 SQL WHERE 中
    result = await session.execute(
        select(Task)
        .join(Sprint, Sprint.id == Task.sprint_id)
        .where(Sprint.team_id == team_id)
    )
    return result.scalars().all()

业务关系没有被显式声明出来,这是个很隐蔽但危害很大的问题。看看这个例子,"团队的任务"是一个清晰的业务概念,但这个概念被隐藏在 SQL 的 JOIN 和 WHERE 子句中。新加入团队的成员需要阅读大量代码才能理解系统中有哪些业务关系,这些关系是如何定义的。更糟糕的是,没有自动化的方式来检查业务关系的一致性。当需求变化需要修改某个关系时,开发者很难找到所有相关的代码,很容易遗漏某个地方,导致业务逻辑的不一致。

问题 4:中间表的技术暴露

在 SQLAlchemy ORM 中,多对多关系需要显式定义中间表,这导致技术细节泄漏到业务层。

# SQLAlchemy ORM:必须定义中间表
class Team(Base):
    __tablename__ = 'teams'
    id = Column(Integer, primary_key=True)
    name = Column(String)

    # ORM relationship 需要指定中间表
    members = relationship("User",
                          secondary="team_members",  # 必须指定中间表
                          back_populates="teams")

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String)

    teams = relationship("Team",
                        secondary="team_members",  # 必须指定中间表
                        back_populates="members")

# 中间表(技术实现细节)
class TeamMember(Base):
    __tablename__ = 'team_members'
    team_id = Column(Integer, ForeignKey('teams.id'), primary_key=True)
    user_id = Column(Integer, ForeignKey('users.id'), primary_key=True)
    role = Column(String)  # 可能还有额外字段

# 查询时需要关心中间表的存在
@router.get("/teams/{team_id}")
async def get_team_members(team_id: int, session: AsyncSession):
    # 必须通过中间表查询
    result = await session.execute(
        select(User)
        .join(TeamMember, TeamMember.user_id == User.id)  # 中间表暴露
        .where(TeamMember.team_id == team_id)
    )
    return result.scalars().all()

这个问题的根源在于,ORM 的多对多关系需要显式定义中间表,这导致技术细节直接泄漏到业务层代码中。业务代码必须知道 team_members 中间表的存在,查询时也需要显式地 join 这个中间表。这增加了代码复杂度,更重要的是,业务逻辑被数据库的实现细节所绑架。

更深层的问题是业务语义变得模糊。TeamMember 到底是一个有意义的业务概念,还是纯粹的技术实现?如果中间表还有额外的字段(比如 role 表示用户在团队中的角色,joined_at 表示加入时间),这些字段应该被建模为独立的实体吗?不同的开发者可能给出不同的答案,缺乏统一的指导原则。

数据组装也因此变得复杂。查询"团队的所有成员"需要 join 中间表,查询"用户所属的团队"也需要 join 中间表。所有涉及多对多关系的查询都变得冗长和难以理解。当业务规则要求"获取用户在所有团队中的角色"时,情况就更加复杂了。这些技术细节让业务逻辑的实现变得异常沉重。

对比:Pydantic-Resolve ERD 的方式

# ERD:业务概念清晰,无需关心中间表
class TeamEntity(BaseModel, BaseEntity):
    """团队实体 - 业务概念"""
    __relationships__ = [
        # 直接表达"团队有多个成员"的业务关系
        Relationship(
            field='id',
            target_kls=list[UserEntity],
            loader=team_to_users_loader  # loader 内部处理中间表
        ),
    ]
    id: int
    name: str

class UserEntity(BaseModel, BaseEntity):
    """用户实体 - 业务概念"""
    __relationships__ = [
        # 直接表达"用户属于多个团队"的业务关系
        Relationship(
            field='id',
            target_kls=list[TeamEntity],
            loader=user_to_teams_loader
        ),
    ]
    id: int
    name: str

# Loader 实现细节:中间表只在这里出现
async def team_to_users_loader(team_ids: list[int]):
    """加载团队成员 - 内部处理中间表"""
    async with get_session() as session:
        # 只有这里需要知道中间表的存在
        result = await session.execute(
            select(User)
            .join(TeamMember, TeamMember.user_id == User.id)
            .where(TeamMember.team_id.in_(team_ids))
        )
        users = result.scalars().all()

        # 构建映射
        users_by_team = {}
        for user in users:
            for tm in user.team_memberships:
                if tm.team_id not in users_by_team:
                    users_by_team[tm.team_id] = []
                users_by_team[tm.team_id].append(user)

        return [users_by_team.get(tid, []) for tid in team_ids]

关键差异

维度 SQLAlchemy ORM Pydantic-Resolve ERD
中间表位置 暴露在业务层 隐藏在 loader 实现中
业务语义 技术关系 (secondary) 业务关系 (团队包含成员)
查询代码 需要 join 中间表 loader.load(team_id)
代码位置 分散在多处 集中在 loader
测试 依赖数据库表结构 可 mock loader

架构优势

传统方式:
Team → TeamMember (中间表) → User
业务层需要知道中间表的存在

Pydantic-Resolve 方式:
Team → User (业务关系)
中间表是数据层的实现细节,业务层不关心

这意味着:

  1. 业务模型纯净:Team 和 User 的关系直接表达业务语义

  2. 技术细节封装:中间表的存在被封装在 loader 中

  3. 灵活的存储策略


    • 数据库可以用中间表实现
    • 也可以用 JSON 字段存储
    • 甚至可以是外部服务(如 LDAP )
    • 业务层代码无需修改
  4. 易于理解:新人看到 ERD 就能理解业务关系,不需要先学习数据库设计


2. Clean Architecture 思想

2.1 核心原则

Clean Architecture 由 Robert C. Martin (Uncle Bob) 提出,核心思想是:

"Software architecture is the art of drawing lines that I call boundaries."
软件架构的艺术在于画界线。

原则 1:依赖规则

外层依赖内层,内层不依赖外层。

                ↓ 依赖方向
    ┌─────────────────────┐
    │   Frameworks &      │  外层
    │   Drivers           │  (实现细节)
    ├─────────────────────┤
    │   Interface         │
    │   Adapters          │
    ├─────────────────────┤
    │   Use Cases         │
    │   (Application)     │
    ├─────────────────────┤
    │   Entities          │  内层
    │   (Business Rules)  │  (核心)
    └─────────────────────┘

遵循依赖规则有几个关键点需要注意。首先,内层不知道外层的存在,这意味着核心业务逻辑不依赖于任何框架、数据库或 UI 的细节。其次,内层不包含外层的信息,比如业务规则不应该知道数据是用 PostgreSQL 还是 MongoDB 存储的。最后,外层的实现可以随时替换而不影响内层,这意味着我们可以从 SQLAlchemy 切换到 MongoDB ,或者从 FastAPI 切换到 Django ,而业务逻辑代码无需修改。

原则 2:业务规则独立

# ❌ 错误:业务规则依赖数据库
class Task:
    def calculate_priority(self, session):
        # 业务逻辑被数据库实现细节污染
        if self.assignee_id in session.query(TeamMember).filter_by(role='lead'):
            return 'high'

# ✅ 正确:业务规则独立
class Task:
    def calculate_priority(self, assignee_roles):
        # 业务逻辑只依赖业务概念
        if 'lead' in assignee_roles:
            return 'high'

原则 3:跨边界的数据传递

# 内层定义数据结构
class TaskEntity(BaseModel):
    id: int
    name: str
    assignee_id: int

# 外层负责转换
def task_entity_to_orm(entity: TaskEntity) -> Task:
    return Task(
        id=entity.id,
        name=entity.name,
        assignee_id=entity.assignee_id
    )

2.2 依赖规则

在 Web 开发中,依赖规则可以这样理解:

┌────────────────────────────────────────────────────┐
│         Presentation Layer (外层)                   │
│  - FastAPI Routes                                   │
│  - Request/Response Models                          │
│  - 依赖: Application Layer                          │
└────────────────────────────────────────────────────┘
                    ↓
┌────────────────────────────────────────────────────┐
│      Application Layer (Use Cases)                 │
│  - 业务用例(获取用户、创建订单)                    │
│  - 依赖: Domain Layer                               │
└────────────────────────────────────────────────────┘
                    ↓
┌────────────────────────────────────────────────────┐
│           Domain Layer (内层)                      │
│  - Entities (业务实体)                              │
│  - Business Rules (业务规则)                        │
│  - Value Objects (值对象)                           │
│  - 不依赖任何外层                                    │
└────────────────────────────────────────────────────┘
                    ↓
┌────────────────────────────────────────────────────┐
│    Infrastructure Layer (最外层)                   │
│  - Database (SQLAlchemy)                           │
│  - External Services                               │
│  - File System                                     │
└────────────────────────────────────────────────────┘

关键洞察

  • Entities 不应该知道 SQLAlchemy 的存在
  • Business Rules 不应该知道数据库表结构
  • Use Cases 不应该知道 HTTP 协议的细节

2.3 在 Web 开发中的应用

传统架构的问题

# 传统方式:所有层次耦合

# Domain Layer (应该独立,但实际上依赖了 ORM)
class User(Base):  # ← SQLAlchemy Base
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)

# Application Layer (应该只依赖 Domain ,但直接使用了 ORM)
async def create_user(data: dict, session: AsyncSession):
    user = User(**data)  # ← 直接使用 ORM Model
    session.add(user)
    await session.commit()

# Presentation Layer
@router.post("/users")
async def api_create_user(data: dict, session=Depends(get_session)):
    return await create_user(data, session)  # ← 暴露了数据库细节

这段代码暴露了传统架构的核心问题。SQLAlchemy 虽然建立了对象关系映射( ORM ),让数据库表可以通过 Python 对象来操作,但这种映射关系过于紧密。ORM Model 既承担了数据持久化的职责,又要表达业务概念,导致对象无法自由地代表业务模型。业务实体被数据库的实现细节所绑架,每个字段、每个关系都必须与数据库表结构一一对应,完全失去了作为独立业务概念存在的自由。

更深层次的问题包括:

  1. Domain Layer 被 SQLAlchemy 绑定:业务实体继承了 SQLAlchemy 的 Base ,无法独立于数据库存在
  2. 业务逻辑无法脱离数据库测试:编写单元测试时必须启动完整的数据库环境,大大降低了测试效率
  3. 切换数据库需要修改所有层:当从 PostgreSQL 迁移到 MongoDB 时,所有使用 ORM Model 的代码都需要重写


。。。

这是一款通过 Vibe Coding 完成的项目。想法来源于在做毕设的时候迭代我自己项目里的提示词,然后辗转 Gemini 和 GPT 复制修改,感觉非常的不方便,所以冒出了这个项目开发的想法。
论坛里也看到有佬友实现 / 开源了,但是似乎后续也没有跟进,目前我这个项目应该至少会在近半年以内跟进更新一些需要的功能。以及现在 V1.0 也是还有一些功能没实现,近段时间会先慢慢补全。
项目链接~
具体的操作流程:
config/providers.example.yaml 内配置好 API 和 URL(目前只支持 OPENAI 格式,也是有待继续开发)之后删除.example 即可使用
设置栏里可以自行配置增加模型,也可以修改提示词
后端使用 FastAPI,端口默认 8000,配备了启动脚本,会默认清理一些端口,如果有需要避免的问题可以自行修改。
欢迎大家提 issue 和 pr~ 有什么问题也欢迎在评论区指出~
以下是效果展示:


📌 转载信息
转载时间:
2026/1/11 19:28:02

Meeting Mind 是一个智能会议记录与分析系统,结合了实时语音识别(ASR)、说话人分离(Diarization)和本地大语言模型(LLM)智能分析功能。它能够实时转录会议内容,区分不同的发言者,并利用本地部署的 LLM 自动生成会议标题、总结、关键要点和行动项,确保数据隐私和安全

核心功能

功能描述
实时语音转写基于 FunASR (SenseVoiceSmall) 模型,提供高精度的实时语音转文字功能
本地 LLM 智能分析集成 Transformers/vLLM,支持 Qwen2.5 等本地大模型,自动生成会议总结、要点和行动项
说话人分离自动识别并区分会议中的不同发言人(基于 CAM++ 说话人识别模型)
异步重新转写支持对历史会议录音进行后台重新转写,优化转录质量
涉密模式切换支持本地处理(涉密)与云端高精度转写(非涉密)两种模式
移动端适配响应式设计,支持手机和平板访问
深色 / 浅色模式提供舒适的 UI 体验,支持一键切换主题

技术栈

后端

  • 框架:Python 3.10+, FastAPI
  • ASR:FunASR (SenseVoiceSmall, FSMN-VAD, CAM++)
  • LLM:Transformers /vLLM (GPU 加速), Qwen2.5-1.5B-Instruct
  • 依赖管理:uv

前端

  • 框架:React 18, Vite, React Router
  • 通信:WebSocket (实时语音流)

📌 转载信息
原作者:
mini_h
转载时间:
2026/1/1 15:52:52

Meeting Mind 是一个智能会议记录与分析系统,结合了实时语音识别(ASR)、说话人分离(Diarization)和本地大语言模型(LLM)智能分析功能。它能够实时转录会议内容,区分不同的发言者,并利用本地部署的 LLM 自动生成会议标题、总结、关键要点和行动项,确保数据隐私和安全

核心功能

功能描述
实时语音转写基于 FunASR (SenseVoiceSmall) 模型,提供高精度的实时语音转文字功能
本地 LLM 智能分析集成 Transformers/vLLM,支持 Qwen2.5 等本地大模型,自动生成会议总结、要点和行动项
说话人分离自动识别并区分会议中的不同发言人(基于 CAM++ 说话人识别模型)
异步重新转写支持对历史会议录音进行后台重新转写,优化转录质量
涉密模式切换支持本地处理(涉密)与云端高精度转写(非涉密)两种模式
移动端适配响应式设计,支持手机和平板访问
深色/浅色模式提供舒适的 UI 体验,支持一键切换主题

技术栈

后端

  • 框架:Python 3.10+, FastAPI
  • ASR:FunASR (SenseVoiceSmall, FSMN-VAD, CAM++)
  • LLM:Transformers / vLLM (GPU 加速), Qwen2.5-1.5B-Instruct
  • 依赖管理:uv

前端

  • 框架:React 18, Vite, React Router
  • 通信:WebSocket (实时语音流)

📌 转载信息
原作者: mini_h
转载时间: 2025/12/31 21:52:40

基于 FastAPI + Gradio + Qwen3-VL 的多平台视频解析、下载与 AI 内容提取系统。 支持抖音、哔哩哔哩、小红书、快手、好看视频等平台的无水印视频解析与下载,提供在线播放功能。集成 Qwen3-VL 视觉语言模型,可智能提取视频内容并生成文字描述。项目开源地址:GitHub - wwwzhouhui/video-parser: 基于 FastAPI + Gradio + Qwen3-VL 的多平台视频解析、下载与 AI 内容提取系统。 支持抖音、哔哩哔哩、小红书、快手、好看视频等平台的无水印视频解析与下载,提供在线播放功能。集成 Qwen3-VL 视觉语言模型,可智能提取视频内容并生成文字描述。


📌 转载信息
转载时间:
2025/12/27 21:40:26