当前位置: 首页 > news >正文

给AI助手加个收件箱:FastAPI与SQLAlchemy异步任务处理实践

实际业务里的 AI 助手,很少只是“用户发一句话、模型回一句话”这么简单。批量生成文案、后台审核、多系统提交任务、失败重试,任何一个环节出现,同步调用大模型接口的设计就会变得很难维护。一个可行思路,是给 AI 助手增加一个自己的收件箱(inbox):所有任务先投递到一张持久化消息表,后台消费者逐个处理并回写结果,请求方通过接口查询状态或接收回调。这种模式把“请求-立即响应”改成“投递-处理-回执”,能明显减少超时、任务丢失和人工介入困难的问题。

下文会从零搭一个最小可运行的服务:FastAPI 提供提交和查询接口,SQLAlchemy 存消息,后台 worker 消费 inbox,AI 调用层用 Mock 客户端代替。读完就能理解 inbox 的数据结构、状态机、并发领取和重试机制,也可以直接把这套模型搬进真实项目。

1. 先理解为什么 AI 助手需要一个收件箱

1.1 同步调用 AI 接口的真实痛点

很多初版 AI 功能都是同步接口:用户请求进来,服务端直接调用模型 API,等模型返回后把结果交还给前端。这种写法在演示时很快,一旦进入业务场景,问题会逐步暴露。

大模型响应时间不稳定。一次生成可能耗时几秒,也可能几十秒甚至更长。同步接口挂在网关后面时,调用方通常会设置超时,超过十几秒就断开。服务端还在继续处理,但调用方已经等不到结果,任务状态不可见,用户只能看到“请求失败”。

同步模式还无法承载批量任务。运营要一次生成 200 条商品文案,如果每条都同步等结果,用户必须一直停留在页面上。中间任何一条网络抖动,整个流程都要重新设计。而多系统同时接入时,服务端也缺少排队机制,流量一上来,模型 API 的限流、配额、并发控制都会变成新的瓶颈。

这些问题的根源,是把“用户任务”和“HTTP 请求”绑在了一起。HTTP 请求的生命周期很短,任务的真实处理却可能很长,需要有一个地方把任务存下来,让处理过程独立于请求过程。

1.2 inbox 的本质:投递、处理、回执

收件箱(inbox)在这个架构里不是邮箱界面,而是一张持久化消息表。调用方不再要求 AI 助手立刻回答,而是先把一条消息投递到 inbox,后台 worker 从 inbox 里取消息、调用模型、写回结果。调用方通过任务 ID 轮询状态,或者等服务端回调通知。

可以这样理解:

  • 投递:调用方提交一条任务,服务端写入 inbox,状态为待处理。
  • 处理:worker 领取消息,调用 AI 模型,更新处理状态和结果。
  • 回执:调用方查询消息状态,拿到最终结果或失败原因。

这个模型把三个环节解耦。投递接口只负责快速落库,不等待模型返回;worker 根据自己的消费能力处理任务;调用方只需要记录一个 message_id,后续查询即可。

1.3 适用场景与不适用场景

适用场景包括批量生成、后台审核、定时任务、重试敏感任务,以及多个业务系统同时接入同一个 AI 能力的场景。

典型的例子:内容平台用 AI 生成素材,每条素材需要先经过人工审核再发布;审批系统调用 AI 生成摘要,但生成失败后要自动重试;运营平台每天定时生成数据报告,凌晨提交,早上查看结果。

不适合的场景是实时对话。用户正在聊天时,不可能等任务进入队列再回轮询。在线对话仍然需要 WebSocket 或流式输出,inbox 更适合作为对话背后的任务管理底座,而不是替代对话通道。

2. 搭建环境与最小项目骨架

2.1 技术选型与依赖安装

为了把注意力放在 inbox 机制本身,演示项目采用尽量少的依赖:

  • Python 3.10 或更高版本。
  • FastAPI 提供 HTTP 接口。
  • Uvicorn 作为本地服务进程。
  • SQLAlchemy 2.x 负责 ORM 和建表。
  • SQLite 做本地存储,零配置文件。

依赖文件requirements.txt可以写成:

fastapi uvicorn[standard] sqlalchemy pydantic

安装时使用虚拟环境,避免污染系统 Python:

python -m venv .venv source .venv/bin/activate pip install -r requirements.txt

如果使用较旧版本的环境,需要确认 SQLAlchemy 2.x 和 Pydantic v2 能正常工作。实际项目落地前,也建议先锁定依赖版本。

2.2 项目目录结构

演示项目不拆太深,直接放在同一个目录:

inbox-ai-assistant/ ├── main.py ├── worker.py ├── db.py ├── requirements.txt └── README.md
  • db.py:数据库连接、ORM 模型、Session 工厂。
  • worker.py:后台消费者,负责领取消息和处理 AI 任务。
  • main.py:FastAPI 应用,提供提交、查询、列表接口,并启动后台线程。

这种目录适合学习。真正进入项目时,建议把模型、仓储、worker、API 拆分到独立模块,再加入测试目录。

2.3 学习环境与生产环境的差异

维度演示项目生产化建议
存储SQLite 单文件PostgreSQL,支持行锁和 SKIP LOCKED
消费者单线程 while 循环多进程 worker 或独立队列服务
配置写在代码里环境变量或配置中心
日志logging 输出到终端结构化日志,集中采集
AI 调用Mock 客户端真实模型 API,配置超时和限流
部署uvicorn 单机启动容器化,分离 API 进程和 worker 进程

演示代码的 worker 在 FastAPI 进程里以线程方式启动,最简单。真实项目应把 worker 作为独立进程部署,否则 API 进程重启时在途任务会中断。

3. 设计 inbox 数据模型和状态机

3.1 消息表字段设计

inbox 表的核心价值是保存一条任务的完整生命周期。字段设计不能只存“要做什么”,还要保存当前状态、处理次数、重试时间、最终结果和错误信息。

db.py中的 ORM 模型如下:

# db.py import json from datetime import datetime, timezone from sqlalchemy import ( create_engine, Column, Integer, String, Text, DateTime ) from sqlalchemy.orm import declarative_base, sessionmaker engine = create_engine( "sqlite:///inbox.db", connect_args={"check_same_thread": False} ) Base = declarative_base() SessionLocal = sessionmaker(bind=engine, expire_on_commit=False) class InboxMessage(Base): __tablename__ = "inbox_messages" id = Column(Integer, primary_key=True, autoincrement=True) idempotency_key = Column(String(64), nullable=False, unique=True, index=True) status = Column(String(20), nullable=False, default="PENDING", index=True) payload = Column(Text, nullable=False) result = Column(Text, nullable=True) error = Column(Text, nullable=True) priority = Column(Integer, nullable=False, default=5) attempts = Column(Integer, nullable=False, default=0) max_attempts = Column(Integer, nullable=False, default=3) available_at = Column( DateTime, nullable=False, default=lambda: datetime.now(timezone.utc) ) created_at = Column( DateTime, nullable=False, default=lambda: datetime.now(timezone.utc) ) updated_at = Column( DateTime, nullable=False, default=lambda: datetime.now(timezone.utc) ) def to_dict(self): return { "id": self.id, "idempotency_key": self.idempotency_key, "status": self.status, "payload": json.loads(self.payload) if self.payload else None, "result": json.loads(self.result) if self.result else None, "error": self.error, "priority": self.priority, "attempts": self.attempts, "max_attempts": self.max_attempts, "available_at": self.available_at.isoformat() if self.available_at else None, "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None, }

idempotency_key是幂等键,用于防止同一业务任务被重复提交。status是状态机当前节点。payload保存要交给 AI 模型执行的请求内容,这里按 JSON 字符串存储。attemptsmax_attempts控制重试次数。available_at表示这条消息最早能被处理的时间,既能支持延迟任务,也能实现失败退避。

3.2 状态机与流转规则

inbox 消息不是简单的队列存储,它有明确状态流转。演示项目使用五个状态:

状态含义
PENDING已投递,等待 worker 领取
PROCESSING已被 worker 领取,正在处理
SUCCEEDED处理成功,结果已写回
FAILED多次失败后放弃
NEEDS_REVIEW业务规则要求人工审核

流转规则如下:

  1. 投递时写入 PENDING。
  2. worker 原子领取后变为 PROCESSING。
  3. 模型调用成功,变为 SUCCEEDED。
  4. 模型调用失败,若还有重试次数,回到 PENDING,并更新 available_at;若已达最大次数,变为 FAILED。
  5. 业务规则要求人工审核,变为 NEEDS_REVIEW。

这个状态机让系统在任意时刻都能回答一个问题:“这条消息现在在哪个环节”。

3.3 字段默认值与参数策略

priority默认 5,范围 1 到 10。数字越小优先级越高。worker 领取消息时,先按 priority 升序,再按 created_at 升序。这样能保证高优任务先处理。

max_attempts默认 3。重试太多会放大模型 API 的异常压力,太少又不够处理偶发抖动。本地演示可以设 3,生产环境建议结合模型超时时间和业务容忍度调整。

available_at用 UTC 时间。SQLite 对 DateTime 的支持并不强,实际生产切换 PostgreSQL 后,时间行为会更可靠。务必统一使用 UTC,避免服务器时区偏移导致消息延迟处理或提前处理。

4. 实现消息提交、领取和处理闭环

4.1 AI 客户端抽象与 Mock 实现

AI 调用是 inbox 消费链路上最容易失败的一环。为了不依赖真实模型 API,先定义一个接口,再写一个 Mock 实现,用来演示成功、失败、人工审核三种分支。

# worker.py import json import logging import time from datetime import datetime, timedelta, timezone from sqlalchemy import update from db import InboxMessage, SessionLocal logger = logging.getLogger("inbox-worker") class LLMClient: def generate(self, instruction: str) -> str: raise NotImplementedError class NeedsReview(Exception): pass class MockLLMClient(LLMClient): def generate(self, instruction: str) -> str: if "触发失败" in instruction: raise RuntimeError("mock model rejected this instruction") return f"mocked result for: {instruction}"

Mock 中的“触发失败”是人为指定的关键字,用于后续验证重试机制。真实项目中,这个接口里放的是对大模型 API 的调用,并需要配置超时时间。

4.2 Worker 消费循环:领取、成功、失败

worker 是 inbox 的核心。它不断扫描 PENDING 消息,将消息状态改为 PROCESSING,然后调用 AI 客户端。

并发场景下最关键的问题是“领取”必须原子。如果先查询 PENDING,再修改状态,两个 worker 可能同时读到同一条消息。正确做法是:先按条件选中候选消息,再用条件更新把 PENDING 改成 PROCESSING,并判断更新影响行数。只有更新成功的那一方才拥有这条消息。

核心处理逻辑如下:

# worker.py def process_one(session): now = datetime.now(timezone.utc) candidate = ( session.query(InboxMessage) .filter( InboxMessage.status == "PENDING", InboxMessage.available_at <= now, ) .order_by(InboxMessage.priority.asc(), InboxMessage.created_at.asc()) .first() ) if candidate is None: return False claimed = session.execute( update(InboxMessage) .where( InboxMessage.id == candidate.id, InboxMessage.status == "PENDING", ) .values(status="PROCESSING", attempts=InboxMessage.attempts + 1) ) session.commit() if claimed.rowcount == 0: return False session.refresh(candidate) payload = json.loads(candidate.payload) instruction = payload.get("instruction", "") try: result = MockLLMClient().generate(instruction) candidate.status = "SUCCEEDED" candidate.result = json.dumps(result, ensure_ascii=False) candidate.error = None except NeedsReview as exc: candidate.status = "NEEDS_REVIEW" candidate.error = str(exc) except Exception as exc: if candidate.attempts >= candidate.max_attempts: candidate.status = "FAILED" else: candidate.status = "PENDING" backoff_seconds = 2 ** candidate.attempts candidate.available_at = datetime.now(timezone.utc) + timedelta( seconds=backoff_seconds ) candidate.error = str(exc) session.commit() return True def run_worker(poll_interval=2.0): logger.info("inbox worker started") while True: session = SessionLocal() try: handled = process_one(session) except Exception: logger.exception("unexpected worker error") time.sleep(5) handled = True finally: session.close() if not handled: time.sleep(poll_interval)

attempts + 1在领取阶段完成,而不是在处理完成后,这样能避免进程崩溃后重试次数不准确。失败分支使用指数退避,第一次失败等 2 秒,第二次失败等 4 秒。available_at决定了消息何时重新变回可领取状态。

需要注意,这里的update是条件更新。SQLite 不支持面向并发的FOR UPDATE SKIP LOCKED,所以用rowcount判断是否抢到。生产环境切换到 PostgreSQL 后,可以直接使用行锁。

4.3 FastAPI 接口提交和查询

API 层只做两件事:投递消息、查询消息。提交接口不应该现场调用模型,而是写入 inbox 后立即返回 message_id。

# main.py import json import uuid from datetime import datetime, timedelta, timezone from threading import Thread from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field from db import Base, InboxMessage, SessionLocal, engine from worker import run_worker Base.metadata.create_all(engine) class SubmitRequest(BaseModel): instruction: str = Field(..., min_length=1, examples=["给新人写一份入职指引"]) priority: int = Field(5, ge=1, le=10) max_attempts: int = Field(3, ge=1, le=5) idempotency_key: str | None = None available_after_seconds: int = Field(0, ge=0, le=86400) requires_review: bool = False @asynccontextmanager async def lifespan(app): worker_thread = Thread(target=run_worker, daemon=True) worker_thread.start() yield app = FastAPI(title="AI Assistant Inbox Demo", lifespan=lifespan) @app.post("/api/inbox/messages", status_code=201) def submit_message(req: SubmitRequest): key = req.idempotency_key or str(uuid.uuid4()) session = SessionLocal() try: existing = ( session.query(InboxMessage) .filter_by(idempotency_key=key) .first() ) if existing: return existing.to_dict() payload = json.dumps( { "instruction": req.instruction, "requires_review": req.requires_review, }, ensure_ascii=False, ) now = datetime.now(timezone.utc) message = InboxMessage( idempotency_key=key, status="PENDING", payload=payload, priority=req.priority, max_attempts=req.max_attempts, available_at=now + timedelta(seconds=req.available_after_seconds), created_at=now, updated_at=now, ) session.add(message) session.commit() session.refresh(message) return message.to_dict() finally: session.close() @app.get("/api/inbox/messages") def list_messages(status: str | None = None): session = SessionLocal() try: query = session.query(InboxMessage) if status: query = query.filter_by(status=status) messages = ( query.order_by(InboxMessage.created_at.desc()) .limit(50) .all() ) return [message.to_dict() for message in messages] finally: session.close() @app.get("/api/inbox/messages/{message_id}") def get_message(message_id: int): session = SessionLocal() try: message = session.get(InboxMessage, message_id) if not message: raise HTTPException(status_code=404, detail="message not found") return message.to_dict() finally: session.close()

idempotency_key为空时自动生成 UUID,这只能保证接口调用本身不重复。如果上游业务已经存在业务单号,应该把业务单号传进来作为幂等键,否则一个业务事件会被判定为多个任务。

5. 运行服务并验证完整流程

5.1 安装依赖并启动

在项目目录执行:

pip install -r requirements.txt uvicorn main:app --reload

启动时,FastAPI 的 lifespan 会创建后台线程,worker 开始轮询 inbox。

注意:演示代码里 worker 是独立线程,真实项目应该用独立进程运行,否则 API 进程重启时在途任务会中断。

5.2 提交第一条消息

提交一条普通任务:

curl -X POST http://127.0.0.1:8000/api/inbox/messages \ -H "Content-Type: application/json" \ -d '{ "instruction": "帮我写一封请假邮件", "idempotency_key": "demo-001" }'

接口会返回:

{ "id": 1, "idempotency_key": "demo-001", "status": "PENDING", "payload": { "instruction": "帮我写一封请假邮件", "requires_review": false }, "attempts": 0, "max_attempts": 3 }

几秒后查询状态:

curl http://127.0.0.1:8000/api/inbox/messages/1

此时status应该变为SUCCEEDED,并且result中包含 mock 返回结果。

5.3 模拟失败重试和人工审核状态

提交一条包含“触发失败”的任务:

curl -X POST http://127.0.0.1:8000/api/inbox/messages \ -H "Content-Type: application/json" \ -d '{ "instruction": "触发失败", "idempotency_key": "demo-fail" }'

查看状态时,它可能先回到PENDINGattempts会递增,随后再次进入PROCESSING。达到max_attempts后,最终变成FAILED

要验证人工审核分支,提交requires_review: true的任务:

curl -X POST http://127.0.0.1:8000/api/inbox/messages \ -H "Content-Type: application/json" \ -d '{ "instruction": "删除一批用户数据", "requires_review": true }'

这条消息会进入NEEDS_REVIEW。真实项目中,需要有运营后台列出这些消息,由人工确认后要么继续执行,要么取消。

5.4 从日志和数据库判断消息状态

运行日志会输出 worker 启动和异常信息。需要确认时,可以先用 API 列表接口查看当前消息:

curl "http://127.0.0.1:8000/api/inbox/messages?status=PENDING" curl "http://127.0.0.1:8000/api/inbox/messages?status=FAILED"

也可以直接打开 SQLite 数据库检查:

sqlite3 inbox.db select id, status, attempts, available_at, error from inbox_messages;

观察attemptsavailable_at,能判断是否发生了失败重试,以及下一次重试是否已经到达。

6. 常见问题排查:卡处理、重复消费、超时

6.1 消息一直 PENDING 却不被消费

现象是消息已经提交成功,状态一直是 PENDING,日志里也没有异常。

优先检查四点:

  1. worker 是否启动。日志中是否出现inbox worker started
  2. available_at是否在当前时间之后。刚提交且请求里带了available_after_seconds时,消息会延迟处理,属于正常现象。
  3. 查询条件是否匹配。status是否为 PENDING,prioritycreated_at排序是否正确。
  4. session 是否频繁打开和关闭,导致事务没有提交。

演示环境里最常见的原因是把available_at设置到了未来,或者 worker 线程没有启动。

6.2 消息在并发下被处理超过一次

处理逻辑中先查候选,再改状态,如果没有条件更新,两个 worker 会同时领取同一条消息。演示代码使用了:

update(InboxMessage) .where( InboxMessage.id == candidate.id, InboxMessage.status == "PENDING", ) .values(status="PROCESSING")

通过rowcount == 0判断是否抢到,避免重复消费。

预防措施还包括:消息的idempotency_key加唯一约束;AI 调用如果有可能重复执行,应尽量设计成幂等操作,或者在结果写回时做唯一性控制。

6.3 失败后没有进入重试或人工审核

可能原因有三个:

  • attempts已经达到max_attempts,消息进入 FAILED。
  • 异常处理时把status设置成了 PENDING,但忘记更新available_at,导致消息立刻被再次领取,看起来像一直在处理。
  • 业务上需要人工审核,但 worker 中没有捕获对应异常。

检查error字段和attempts字段,能区分到底走了失败分支还是审核分支。生产系统通常还会加一个死信队列或 FAILED 列表,定期人工处理。

6.4 AI 接口超时导致 inbox 积压

大模型接口响应慢时,worker 会被卡在同步调用上。如果同时只有一个 worker,其他 PENDING 消息全部排队。

处理方式有两个方向。一是给模型调用设置超时时间,例如使用httpx.Timeout;二是增加 worker 数量,但要注意数据库行锁和 API 并发限制。

问题现象常见原因检查方式处理建议
消息一直 PENDINGworker 未启动或已退出看日志是否有 worker started启动 worker,检查异常处理
消息一直 PENDINGavailable_at 在未来查询 available_at 字段等待延迟任务,或修正提交参数
消息被重复处理领取逻辑不是条件更新并发提交同一条消息观察使用条件更新和唯一约束
失败后不重试attempts 达到上限查询 attempts 和 error调整 max_attempts 或进入人工审核
积压严重AI 调用超时查看 PROCESSING 数量设置超时,增加 worker 并发
时间处理异常时区不统一比较 DB 时间与当前 UTC 时间统一使用 UTC 时间

7. 生产化建议和复习清单

7.1 从演示代码到生产系统的差异

演示代码已经把 inbox 的核心链路跑通,但距离生产还有明显距离。存储层面,SQLite 适合单机演示,遇到两个 worker 同时领取消息时仍能靠条件更新保证正确性,但写入并发能力有限。生产环境应切换到 PostgreSQL,使用SELECT ... FOR UPDATE SKIP LOCKED领取消息,能显著降低锁竞争。

消费进程层面,生产环境不推荐用 FastAPI 进程内的线程。更好的方式是单独部署 worker 应用,通过容器或进程管理器拉起多个副本,每个副本独立消费。这样 worker 升级、重启、扩容都不会影响 API 服务。

日志和监控也必须在生产化时补齐。inbox 机制最大的优点是状态可追溯,不能浪费这一点。至少要监控:

  • PENDING 消息积压数量。
  • FAILED 消息数量和失败原因。
  • 平均处理时延。
  • PROCESSING 状态长时间不变化的超时消息。

7.2 inbox 机制上线前检查清单

下面这份清单可以直接用于真实项目上线前自检:

  • 每条消息是否都有稳定的idempotency_key,并由业务侧传入,而不是每次生成 UUID。
  • 消息领取是否使用原子操作,确保并发安全。
  • 失败重试是否配置了指数退避和available_at
  • 是否有 FROM PROCESSING 到 PENDING 的超时恢复任务。
  • 是否记录attemptserrorresultupdated_at
  • payloadresult是否统一使用 JSON 字符串存储,并限制大小。
  • 是否配置了模型调用的超时时间。
  • 是否对高危操作设置人工审核状态。
  • 是否存在 worker 重启后消息丢失或重复执行的场景。
  • 是否有 FAILED 或 NEEDS_REVIEW 消息的人工处理入口。
  • 是否监控了 inbox 积压和失败率。

7.3 优先扩展的方向

如果要在真实项目里继续完善,可以在三个方向扩展:

第一,引入真正的队列或消息中间件。inbox 表本身是业务任务存储,中间件负责高吞吐投递。两者不冲突,inbox 可以保存最终状态,中间件负责消费者分发。

第二,增加人工审批工作流。NEEDS_REVIEW消息可以由运营后台复核,通过后进入执行队列,拒绝后进入 CANCELED 状态。这能覆盖很多 AI 自动化的合规场景。

第三,把 AI 调用层替换成可插拔实现。目前 MockLLMClient 只是为了演示,生产环境可以按模型厂商、模型版本、超时策略分别实现同一个接口,再配合限流和重试,形成稳定的模型网关。

对新手而言,最大的价值不是背下这套代码,而是真正理解“投递、处理、回执”的思路。以后遇到任何耗时长、需要重试、需要人工介入的 AI 任务,都可以先用 inbox 模型落地,再逐步演进成分布式任务系统。

http://www.cnnetsun.cn/news/4288133.html

相关文章:

  • oMLX 模型自动发现全解:一个服务器同时加载 LLM、VLM、Embedding 与 Reranker
  • Bun 私有包管理上手:一份 bunfig.toml 配好私有源,安装不再 401
  • MinerU 文档解析故障排查手册:12 个高频常见问题一次讲清
  • Android Studio项目源码zip解压、Gradle导入与EOCD修复实战指南
  • 研发工程师校招笔试全解析:从网易真题看算法与基础考察
  • turbovec 原理篇(三):Lloyd-Max 量化器如何逼近香农失真-率极限
  • lazygit 快速上手指南:8 个 Git 高频操作如何在一块终端屏里完成
  • 登录日志与管理员审计日志存储决策
  • 保姆级 | Linux 系统命令(tar解压和压缩)
  • Vue3进度条(Progress)
  • 2026年AI论文平台推荐:9款高效AI工具一站式清单
  • DSP算法FPGA实现:从理论到硬件的完整工程链路与实践指南
  • mermaid流程图
  • 千问本地部署实战:从Ollama到Spring AI的完整接入指南
  • 前端工程协作的构建与发布
  • IBASE MI1001 Mini-ITX工业主板:边缘计算与工控替换的可靠之选
  • REDRIVER2:PS1经典游戏《Driver 2》的现代C++重实现与逆向工程解析
  • C++ I/O流与模板编程:从基础原理到实战应用
  • AI+3D人体解剖可视化工具的技术实现与搭建指南
  • 创业产品如何识别价值主张和替代方案
  • 转向系统编程的选型方法
  • ai文章怎么去掉ai痕迹?朱雀AIGC检测后怎样降AI率又保留品牌事实
  • 从自我造题到自我迭代:35B模型如何用数据飞轮逼近万亿参数
  • 皮尔逊相关系数:从原理到实战,避开数据分析中的常见陷阱
  • 第二代Open Virtual Platforms API:从组件化建模到多核虚拟平台实战
  • 【算法】数字滤波
  • 基于TensorFlow与CNN的猫狗识别实战:从环境搭建到模型部署
  • 跨端界面选型看真实页面
  • WinForm应用实战开发指南 - 应用程序如何实现手写签名?
  • 效率工具评审从用户任务出发