前端转 AI 第 20 天CLI 打包与发布 - 让助手变成真正的命令行工具昨日回顾Day 18-19 把测试写完了核心模块覆盖 90%。但有个尴尬的问题——我的助手现在还得这样跑cd~/day19/ai-assistant-v6 python-msrc.ai_assistant.main这根本不像一个工具。真正的命令行工具应该是ai-assistant在任何目录下都能敲。今天把它打包成真正的 CLI 工具还能发布到 PyPI 让别人pip install。Day 20 目标把项目打包成可安装的 CLI 工具理解 Python 打包生态。一、为什么前端对打包很亲切前端其实天天在打包npm run build把源码打成dist/package.json的bin字段声明 CLInpm publish发布到 npm registrynpx直接运行已发布的工具Python 的打包逻辑几乎一模一样前端Pythonpackage.jsonpyproject.tomlpackage.json的bin[project.scripts]npm install -gpip installnpm linkpip install -e .npm publishtwine upload/flit publishnpm registryPyPInpx xxxpipx run xxxnode_modules/site-packages/dist/dist/*.whl理解了 npmPython 的打包就是换个名字。二、Python 打包的几种方式Python 的打包生态比 npm 乱多了——有好几套工具。工具现状推荐度setuptools老牌、万能、啰嗦⭐⭐⭐ 兼容性好poetry一体化、依赖 打包⭐⭐⭐⭐ 生态好flit极简、只做打包⭐⭐⭐ 简单项目hatch现代、官方推荐⭐⭐⭐⭐ 新项目推荐pdm现代、PEP 582⭐⭐⭐uv极快Rust、全能⭐⭐⭐⭐⭐2024 后新宠这么多工具怎么选好消息不管用哪个工具pyproject.toml是共同标准。写一份配置所有工具都能读。我的建议学习阶段用setuptoolsPython 自带最稳新项目用hatch或uv团队协作用poetry依赖锁定好今天用setuptools因为它最简单、最不容易出坑学会了其他工具 10 分钟能上手。三、理解pyproject.toml先看完整结构我们从 Day 6 起就在用[build-system] requires [setuptools68, wheel] build-backend setuptools.build_meta [project] name ai-assistant version 0.1.0 description 命令行 AI 助手 readme README.md requires-python 3.11 authors [ { name 你的名字, email youexample.com } ] license { text MIT } keywords [ai, agent, cli] dependencies [ pydantic2.0.0, python-dotenv1.0.0, rich13.0.0, ] [project.optional-dependencies] dev [pytest8.0.0, pytest-cov5.0.0, pytest-asyncio0.24.0] [project.scripts] ai-assistant ai_assistant.main:main [project.urls] Homepage https://github.com/yourname/ai-assistant Repository https://github.com/yourname/ai-assistant三块核心配置1.[build-system]用哪个打包工具[build-system] requires [setuptools68, wheel] build-backend setuptools.build_meta这段不用改模板化。意思是用 setuptools 打包需要 68 版本以上。2.[project]项目元信息字段必填说明name✅包名PyPI 上唯一version✅版本号description一句话描述readmeREADME 文件requires-pythonPython 版本要求dependencies依赖列表等于requirements.txtauthors作者license开源协议关键点dependencies写在这里等于requirements.txt。装包时自动装依赖。3.[project.scripts]CLI 入口最重要[project.scripts] ai-assistant ai_assistant.main:main这行的含义左边ai-assistant命令行敲的命令右边ai_assistant.main:main调用ai_assistant包的main.py里的main()函数等价于前端// package.json{bin:{ai-assistant:./bin/cli.js}}一模一样的心智。四、改造项目结构1. 复制 Day 19 的项目cp-rday19/ai-assistant-v6 day20/ai-assistant-v7cdday20/ai-assistant-v72. 检查目录结构标准 Python 包结构长这样ai-assistant-v7/ ├── pyproject.toml ├── README.md ├── .gitignore ├── .env.example ├── src/ │ └── ai_assistant/ │ ├── __init__.py │ ├── __main__.py # ← 新增 │ ├── main.py │ ├── config.py │ ├── models.py │ └── ... └── tests/ ├── conftest.py ├── test_storage.py └── test_memory.py关键点src/布局。为什么用src/布局避免意外 import如果包直接在根目录测试时可能 import 到源码而不是安装的版本强制显式安装必须pip install -e .才能 import更接近用户实际使用3. 创建__main__.py__main__.py让包可以这样跑python-mai_assistant内容支持 python -m ai_assistantfrom.mainimportmainif__name____main__:main()同时[project.scripts]里的main()是给ai-assistant命令用的。两者都可以。4. 修改main.py确保有一个可被调用的main()函数程序入口importargparseimportasyncioimportsysfrom.configimportconfigfrom.loggerimportsetup_logging,get_loggerfrom.sessionimportAIAssistantdefparse_args():parserargparse.ArgumentParser(progai-assistant,description命令行 AI 助手本地,formatter_classargparse.ArgumentDefaultsHelpFormatter,)parser.add_argument(--version,actionversion,version%(prog)s 0.1.0,)parser.add_argument(--no-stream,actionstore_true,help关闭流式输出,)parser.add_argument(--log-level,defaultconfig.LOG_LEVEL,choices[DEBUG,INFO,WARNING,ERROR],help日志级别,)returnparser.parse_args()asyncdefasync_main(args):appAIAssistant(use_streamnotargs.no_stream)try:awaitapp.run()finally:app.close()defmain():CLI 入口函数argsparse_args()config.ensure_dirs()setup_logging(args.log_level)loggerget_logger(__name__)try:asyncio.run(async_main(args))exceptKeyboardInterrupt:print(\n已退出)sys.exit(0)exceptExceptionase:logger.exception(应用崩溃)print(f应用崩溃:{e},filesys.stderr)sys.exit(1)if__name____main__:main()这里加了--version参数——CLI 工具的标配。5. 补充pyproject.toml[build-system] requires [setuptools68, wheel] build-backend setuptools.build_meta [project] name ai-assistant-cli-demo # PyPI 上必须唯一加个后缀 version 0.1.0 description 命令行 AI 助手 - 前端转 AI 学习项目 readme README.md requires-python 3.11 authors [ { name 前端转 AI } ] license { text MIT } keywords [ai, agent, cli, llm] dependencies [ pydantic2.0.0, python-dotenv1.0.0, rich13.0.0, ] [project.optional-dependencies] dev [ pytest8.0.0, pytest-cov5.0.0, pytest-asyncio0.24.0, build1.0.0, ] [project.scripts] ai-assistant ai_assistant.main:main [project.urls] Homepage https://github.com/yourname/ai-assistant Repository https://github.com/yourname/ai-assistant # setuptools 需要的告诉它去哪找包 [tool.setuptools.packages.find] where [src] # pytest 配置 [tool.pytest.ini_options] testpaths [tests] asyncio_mode auto addopts -v --tbshort注意最后两段——这是 setuptools 的专属配置。[tool.setuptools.packages.find]告诉 setuptools 去src/下找包。不写的话setuptools 会在根目录找找不到ai_assistant。6. 加一个 READMEREADME.md# ai-assistant 命令行 AI 助手学习项目。 ## 安装 bash pip install -e .使用# 直接运行ai-assistant# 或者python-mai_assistant开发pipinstall-e.[dev]pytest许可MIT--- ## 五、本地安装可编辑模式 ### 1. pip install -e . bash cd day20/ai-assistant-v7 pip install -e .-e editable 开发模式。它做了什么不会复制文件而是在site-packages里放一个指向当前目录的软链接改代码立刻生效不用重新安装同时注册ai-assistant命令输出Obtaining file:///Users/you/day20/ai-assistant-v7 Installing build dependencies ... done Checking if build backend supports build_editable ... done ... Successfully built ai-assistant-cli-demo Successfully installed ai-assistant-cli-demo-0.1.02. 测试新命令在任何目录下cd~ ai-assistant应该会启动助手╭───────────────────────────╮ │ │ │ AI 助手 v2带记忆 │ │ │ ╰───────────────────────────╯ ℹ 会话 ID: a3f9b2e1 你 ›同时也能ai-assistant--version# ai-assistant 0.1.0ai-assistant--help# usage: ai-assistant [-h] [--version] [--no-stream] ...python-mai_assistant--version# ai-assistant 0.1.0是不是超有成就感你的项目从一个文件夹变成了真正的工具。3. 卸载pip uninstall ai-assistant-cli-demo-e安装的也能卸载干净。六、构建分发包安装到本地和发布给别人是两回事。1. 安装buildpipinstallbuildbuild是 Python 官方的构建工具比老式的python setup.py sdist简单得多。2. 构建python-mbuild输出* Creating venv isolated environment... * Installing packages in isolated environment... (setuptools68, wheel) * Getting build dependencies for sdist... * Building sdist... * Building wheel from sdist * Creating venv isolated environment... * Installing packages in isolated environment... (setuptools68, wheel) * Getting build dependencies for wheel... * Building wheel... Successfully built ai_assistant_cli_demo-0.1.0.tar.gz and ai_assistant_cli_demo-0.1.0-py3-none-any.whldist/目录下多出两个文件dist/ ├── ai_assistant_cli_demo-0.1.0.tar.gz # 源码包 (sdist) └── ai_assistant_cli_demo-0.1.0-py3-none-any.whl # 二进制包 (wheel)两种包的区别类型后缀内容用途sdist.tar.gz源码需要编译的场景wheel.whl预构建优先用这个装得快前端类比sdist≈ 源码包含package.json装的时候要 buildwheel≈ 打包好的dist/直接就能用3. 从 wheel 安装pipinstalldist/ai_assistant_cli_demo-0.1.0-py3-none-any.whl和从 PyPI 装是一样的效果。可以把这个.whl发给同事同事pip install xxx.whl就能用。七、发布到 PyPI了解发布 上传到 PyPIPython 官方的包仓库相当于 npm registry。1. 注册账号测试环境https://test.pypi.org/生产环境https://pypi.org/先在测试环境玩别一上来就往生产发。2. 生成 API Token登录 → Account settings → API tokens → Add API token得到一个pypi-xxx...开头的 token。3. 用twine上传pipinstalltwine# 上传到测试环境twine upload--repositorytestpypi dist/*# 上传到生产twine upload dist/*会提示输入用户名和密码用户名__token__密码粘上pypi-xxx...4. 验证# 从测试环境安装pipinstall--index-url https://test.pypi.org/simple/ ai-assistant-cli-demo或者同事直接pipinstallai-assistant-cli-demo你就发布了一个 Python 包⚠️PyPI 的包名是全局唯一的且不能删除只能 yank。测试时一定加后缀比如xxx-demo/xxx-test。5. 自动发布GitHub Actions手动上传太麻烦。生产项目一般配 CI# .github/workflows/publish.ymlname:Publish to PyPIon:release:types:[published]jobs:publish:runs-on:ubuntu-latestpermissions:id-token:write# 用 Trusted Publisher不用 tokensteps:-uses:actions/checkoutv4-uses:actions/setup-pythonv5with:python-version:3.11-run:pip install build-run:python-m build-uses:pypa/gh-action-pypi-publishrelease/v1流程在 GitHub 上创建 releaseActions 自动构建自动上传 PyPI这叫 Trusted PublisherOIDC 认证——不用手动存 token更安全。这部分今天只要知道有这回事用的时候再查。八、版本管理版本号不是随便起的Python 遵循语义化版本SemVer主版本.次版本.修订号 ↑ ↑ ↑ Major Minor Patch变化示例含义Patch0.1.0 → 0.1.1bug 修复Minor0.1.0 → 0.2.0加功能向后兼容Major0.1.0 → 1.0.0破坏性变更版本号写在pyproject.toml里[project] version 0.1.0但是——手动改太容易忘。更好的做法1. 用__version__变量src/ai_assistant/__init__.py__version__0.1.0pyproject.toml里引用[project] dynamic [version] [tool.setuptools.dynamic] version { attr ai_assistant.__version__ }这样版本号只在源码里改一次打包时自动读取。2. 用setuptools_scm从 git tag 自动生成最强方案——版本号从 git tag 来[build-system] requires [setuptools68, setuptools_scm8] build-backend setuptools.build_meta [project] dynamic [version] [tool.setuptools_scm]然后打 tag 就是发版本gittag v0.1.1gitpush--tags打包时版本号自动是0.1.1。前端类比类似npm version patch但更自动化。建议学习项目手动写在__init__.py正式项目用setuptools_scm git tag九、用pipx隔离安装 CLI 工具问题pip install ai-assistant会装到当前环境如果你在项目 A 的 venv 里就污染了。解决pipx——专门装 CLI 工具的工具。# 安装 pipxbrewinstallpipx# Mac# 或pipinstall--userpipx# 用 pipx 装工具pipxinstallai-assistant-cli-demo# 现在 anywhere 都能用ai-assistantpipx会为每个工具创建独立虚拟环境互不干扰。前端类比pipx≈npx全局安装的工具但更隔离。推荐所有 CLI 工具都用pipx装不用pip install。十、实战把 Day 19 项目完整打包1. 完整项目结构ai-assistant-v7/ ├── pyproject.toml ✅ ├── README.md ✅ ├── .gitignore ├── .env.example ├── LICENSE ← 新增 ├── src/ │ └── ai_assistant/ │ ├── __init__.py 加 __version__ │ ├── __main__.py ✅ 新增 │ ├── main.py 加 --version │ ├── config.py │ ├── models.py │ ├── orm_models.py │ ├── db_orm.py │ ├── storage_orm.py │ ├── llm.py │ ├── memory.py │ ├── commands.py │ ├── ui.py │ ├── logger.py │ └── schema.sql ├── tests/ │ ├── conftest.py │ ├── test_storage.py │ └── test_memory.py ├── scripts/ │ └── migrate_json_to_db.py └── dist/ 构建产物不提交2.__init__.pyAI 助手 - 命令行工具__version__0.1.03.LICENSEMIT 示例MIT License Copyright (c) 2026 前端转 AI Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Software), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.4..gitignore加构建产物# 已有 __pycache__/ *.py[cod] .venv/ venv/ .env data/ logs/ .vscode/ .DS_Store # 新增 build/ dist/ *.egg-info/ .pytest_cache/ .coverage htmlcov/5. 完整构建流程# 1. 清理旧的rm-rfbuild dist *.egg-info# 2. 跑测试pytest# 3. 构建python-mbuild# 4. 检查产物lsdist/# ai_assistant_cli_demo-0.1.0-py3-none-any.whl# ai_assistant_cli_demo-0.1.0.tar.gz# 5. 在一个干净的环境里测试安装python-mvenv /tmp/test-envsource/tmp/test-env/bin/activate pipinstalldist/ai_assistant_cli_demo-0.1.0-py3-none-any.whl ai-assistant--versiondeactivaterm-rf/tmp/test-env第 5 步是关键不要在自己开发环境测要在干净环境里测——能发现忘了声明某个依赖的问题。6. 加一个 Makefile可选但推荐Makefile.PHONY: install dev test cov build clean publish install: pip install -e . dev: pip install -e .[dev] test: pytest cov: pytest --covsrc/ai_assistant --cov-reporthtml build: clean python -m build clean: rm -rf build dist *.egg-info .pytest_cache .coverage htmlcov find . -type d -name __pycache__ -exec rm -rf {} publish: build twine upload dist/*用法makedev# 装开发依赖maketest# 跑测试makebuild# 打包makepublish# 发布前端类比相当于package.json的scripts但 Python 用 Makefile 更普遍。十一、前端视角Python 打包 vs npm用一张表彻底对照环节npmPython (setuptools)项目配置package.jsonpyproject.toml声明 CLIbin: {...}[project.scripts]入口文件#!/usr/bin/env nodedef main(): ...本地链接npm linkpip install -e .依赖安装npm installpip install -e .依赖锁package-lock.jsonpoetry.lock/uv.lock构建npm run buildpython -m build构建产物dist/(JS 文件)dist/*.whl*.tar.gz发布npm publishtwine upload/ CI仓库npm registryPyPI全局安装npm i -gpipx install免安装运行npxpipx run/uvx版本来源package.jsonpyproject.toml/ git tag最爽的一点Python 有-e模式改代码立刻生效。npm 的link也能做到但没这么顺滑。最烦的一点Python 的构建工具太多。npm 就一套Python 有一堆。选一套用别纠结。十二、今日踩坑忘了[tool.setuptools.packages.find]→ setuptools 找不到src/下的包打出来的 wheel 是空的[project.scripts]路径写错→ 比如写成src.ai_assistant.main:main会报ModuleNotFoundErrorPyPI 包名冲突→ 想叫ai-assistant结果被占了加前缀或后缀忘了把dist//build/加到.gitignore→ 提交一堆构建产物pip install -e .后忘了pip uninstall→ 环境里一堆开发包冲突README.md里的相对链接在 PyPI 上失效→ PyPI 渲染的是打包时那一刻的内容别放动态链接license { text MIT }写成license MIT→ 新版 PEP 639 支持简写但旧版会报错兼容起见用老写法没在干净环境测→ 自己机器上装了各种库跑得好好的用户装完直接ModuleNotFoundErrorpython -m build报错 “No module named ‘build’”→ 忘了pip install buildpip install .装了旧版本→ pip 有缓存加--no-cache-dir或先pip uninstall十三、阶段小结Day 16-20 你有了什么从数据到测试到打包四天打完了一套工程化地基Day内容产出16SQLiteDatabase封装类17SQLAlchemy ORMSessionStorageORM18pytest 基础tests/目录 19 个测试19pytest 进阶mock / 集成 / 性能测试20打包一个能pip install的 CLI 工具你的项目现在有的能力✅ 数据持久化SQLite ORM✅ 测试保障90% 覆盖率✅ 命令行工具ai-assistant命令✅ 分发包.whl能给同事✅ 自动发布GitHub Actions 可配这已经是一个能拿出来说的项目了。简历上可以写命令行 AI 助手基于 Python 3.11集成 SQLAlchemy Pydantic pytest支持会话持久化、记忆管理、CLI 打包发布测试覆盖率 90%。但它还差一块——它现在只能本地跑没有 API。十四、今日总结今天核心收获三件事[project.scripts]是 Python 的 CLI 入口等价于 npm 的binpip install -e .是开发模式改代码立刻生效本地开发必备python -m build是标准构建工具产出 wheel sdist上传 PyPI 就能发布一个感受前端的打包发布是显式的——你有dist/有npm publish每次改动都清楚这一步是在打包。Python 的打包发布是隐式的——你的项目一直就是一个包pyproject.toml定义了一切pip install -e .只是让它知道自己是个包。从今天起你写的 Python 项目不再是脚本集合而是可安装、可分发、可发布的软件。这是从写代码到做产品的关键一步。Day 20 完成度✅明日预告Day 21进入 FastAPI正式进入后端世界。FastAPI 是什么为什么比 Flask 更适合 AI 应用写第一个接口GET /和POST /chatPydantic 模型自动校验Day 11 的成果自动生成 API 文档Swagger UI把ai-assistant从 CLI 变成 HTTP 服务准备好从命令行到服务端的跨越。最后如果这篇对你有用点个赞让我知道有人在看。评论区互动你之前打包发布过 npm 包吗感觉 Python 的流程和前端比哪个更舒服有问题评论区见我们第 21 天继续。100 天第 21 天见。附录系列目录已完成 ✅阶段天数内容基础01-06Python 语法、数据结构、函数、OOP、文件、依赖管理网络07-08HTTP 请求、异步 asyncio工程09-12正则、装饰器、Pydantic、日志调试项目13-14命令行 AI 助手 v1 / v2复盘15阶段总结数据16-17SQLite 数据库、SQLAlchemy ORM测试18-19pytest 入门 进阶工具20CLI 打包与发布 ✅进行中 阶段天数内容后端21-30FastAPI 完整服务端大模型31-45LLM API Prompt 工程RAG46-60向量数据库 知识库Agent61-80LangChain / 工具调用实战81-100完整 Agent 项目上线100 天后见一起从切图仔变成 Agent 开发者。