全栈开发知识库

01 · 环境搭建: uv 与项目骨架

一句话总结

这一章从一个空文件夹开始, 装好 Python 和包管理器 uv, 建出项目的目录骨架, 然后写下第一个真正有用的文件 —— 配置模块。做完之后项目还什么功能都没有, 但你会有一个能跑起来、能打印出自己配置的 Python 项目, 以及一份不会把 API Key 泄漏到 GitHub 上的 .gitignore。

本章你能学到什么

  • 为什么 2026 年做 Python 项目应该用 uv 而不是 pip + venv
  • 虚拟环境到底是什么, 以及"不用虚拟环境会怎样"
  • 依赖版本为什么必须钉死, 不钉死会在什么时候咬你
  • 配置为什么不能写在代码里, 以及一个只有 70 行的配置模块怎么支撑整个项目
  • .env 和 .gitignore: 唯一一件"做错了会上新闻"的事

一、装 Python 和 uv

1.1 先看看你有没有 Python

打开终端 (Mac 上是"终端"或 iTerm, Windows 上用 PowerShell), 输入:

python3 --version

如果显示 Python 3.11.x 或更高, 可以跳到 1.3。显示 3.10 或更低, 或者 command not found, 继续往下。

为什么要 3.11 以上: 这个项目用了两个新语法 —— X | None 这种类型写法 (3.10+) 和 Enum 的一些行为 (3.11 更稳)。用 3.9 跑会直接报语法错误。

1.2 什么是 uv, 为什么用它

是什么。 uv 是一个用 Rust 写的 Python 包管理器和项目管理器, 一个命令行工具同时干四件事: 装 Python 本身、建虚拟环境、装依赖包、跑脚本。

为什么不用传统的 pip。 传统流程是这样的:

python3 -m venv .venv          # 建虚拟环境
source .venv/bin/activate      # 激活
pip install fastapi            # 装包
pip freeze > requirements.txt  # 记录版本

四条命令, 三个坑: 忘记 activate 就装到全局去了; pip freeze 会把间接依赖也记进去, 文件变成 80 行没人看得懂; 装 torch 这种大包要等好几分钟。

uv 的对应流程:

uv init --python 3.12    # 建项目, 顺便装好对应版本的 Python
uv add fastapi           # 装包, 自动建虚拟环境、自动写进 pyproject.toml、自动生成 lock 文件
uv run python xxx.py     # 跑脚本, 不需要 activate

装依赖比 pip 快一个数量级, 因为它用了全局缓存和硬链接 —— 同一个包在十个项目里只在磁盘上存一份。

1.3 装 uv

Mac 和 Linux:

curl -LsSf https://astral.sh/uv/install.sh | sh

Windows (PowerShell):

powershell -c "irm https://astral.sh/uv/install.ps1 | iex"

装完重开一个终端 (让 PATH 生效), 然后:

uv --version

✅ Checkpoint 1: 看到版本号。这是本文档验证时的真实输出:

uv 0.11.32 (3010295ae 2026-07-23 aarch64-apple-darwin)

你的版本号会不一样, 只要能打印出来就行。aarch64-apple-darwin 表示 Apple 芯片的 Mac, Intel Mac 是 x86_64-apple-darwin, Linux 是 x86_64-unknown-linux-gnu —— 这个后缀不影响任何后续步骤。


二、虚拟环境: 为什么非要有这东西

是什么。 虚拟环境就是项目自己的一个私有 Python。它是一个目录 (通常叫 .venv), 里面有一份 Python 解释器的链接和这个项目自己的所有第三方包。

不用会怎样。 假设你有两个项目, A 项目用 pydantic 1.x, B 项目用 pydantic 2.x。两者的 API 完全不兼容。如果都装到全局, 装 B 的时候会把 A 的 pydantic 升级掉, 然后 A 就跑不了了。这不是假设 —— 这是每个 Python 开发者都遇到过的事, 而且报错通常长得完全不像"版本冲突"。

怎么用。 uv 会自动管理它, 你几乎不用手动碰。但要知道它在哪:

  • .venv/bin/python (Mac/Linux) 或 .venv\Scripts\python.exe (Windows) 就是这个项目的 Python
  • 用 uv run xxx 时, uv 自动用的就是这个 Python
  • .venv 目录绝对不要提交到 git, 它有几百兆而且换台机器就用不了

相关知识: uv run 和 activate 的区别。 老教程会让你先 source .venv/bin/activate 再跑命令, 这会修改当前终端的 PATH。uv run 不修改任何东西, 只是用正确的 Python 跑一次命令 —— 更干净, 也不会出现"我明明激活了怎么还是用错了 Python"。本文档全程用 uv run。


三、建项目骨架

3.1 初始化

mkdir android_interviewer && cd android_interviewer
uv init --python 3.12 --no-readme

uv init 会生成 pyproject.toml、.python-version 和一个示例 main.py (可以删掉)。

3.2 一次性建好所有目录

mkdir -p app/core app/rag app/agent \
         corpus/lifecycle corpus/kotlin corpus/jetpack corpus/memory corpus/service \
         eval finetune scripts tests static docker data

Python 要求包目录里有 __init__.py 才认它是包:

touch app/__init__.py app/core/__init__.py app/rag/__init__.py app/agent/__init__.py eval/__init__.py

为什么 eval/ 也要 __init__.py: 因为我们要用 python -m eval.eval_retrieval 这种方式跑评测脚本。-m 要求它是一个包。少了这个文件会报 No module named eval.eval_retrieval, 而报错信息完全不提"你缺一个 __init__.py"。

为什么 corpus/ 和 tests/ 不要 __init__.py: corpus/ 里全是 Markdown 不是代码; tests/ 用 pytest 跑, pytest 有自己的发现机制, 加了反而可能和 conftest.py 的路径处理打架。

3.3 目录职责一览

android_interviewer/
├── app/          所有生产代码
│   ├── core/     基础设施: 配置、LLM 客户端、数据库、鉴权
│   ├── rag/      检索: 加载、切块、索引、检索
│   └── agent/    面试逻辑: 数据契约、出题、判分、状态机、报告
├── corpus/       语料 (Markdown), 按知识点分子目录
├── eval/         评测集与评测脚本
├── finetune/     微调数据导出
├── scripts/      一次性脚本 (冒烟测试、造测试数据)
├── tests/        自动化测试
├── static/       前端 (就一个 index.html)
├── docker/       部署相关
└── data/         运行产物 (向量库、SQLite), 不进 git

分层的一条硬规则: core/ 和 rag/ 不许 import agent/, agent/ 不许 import api.py。 依赖方向永远是从外往里。第 7 章写完的状态机, 第 8 章接 Web 时一行都不用改, 靠的就是这条规则。


四、装依赖, 并把版本钉死

4.1 装

uv add "fastapi==0.141.1" "uvicorn[standard]==0.52.3" "pydantic==2.13.4" \
       "python-dotenv==1.2.3" "pyyaml==6.0.3" "openai==3.1.0" \
       "chromadb==1.4.1" "sentence-transformers==5.1.2" \
       "rank-bm25==0.2.2" "numpy==2.5.2" "httpx==0.28.1"

uv add --dev "pytest==9.1.1" "pytest-cov==7.1.0" "ruff==0.16.3"

这一步会下载 torch (sentence-transformers 的依赖), 有 200 多兆, 慢是正常的。

4.2 为什么用 == 钉死版本

是什么。 fastapi==0.141.1 表示只装这一个版本。对比 fastapi>=0.115 表示"0.115 以上都行"。

为什么钉死。 因为"能跑"是有时效的。真实发生过的事:

  • chromadb 在小版本升级里改过 get_or_create_collection 的参数
  • pydantic 从 1.x 到 2.x 几乎重写了整套 API, 而且 2.x 内部也调整过 validator 的行为

如果你写 >=, 那么: 今天构建的 Docker 镜像能跑, 三个月后同一份代码重新构建就可能起不来 —— 因为依赖悄悄升级了。这种问题最难查, 因为你的 git 记录里一行改动都没有。

代价。 需要定期手动升级 (uv add "fastapi==新版本"), 并且升级时要跑一遍测试。这个代价是值得的: 你控制升级的时机, 而不是让升级在某个周五晚上突然发生。

4.3 lock 文件必须提交

uv add 会生成 uv.lock, 里面记录了所有依赖以及依赖的依赖的精确版本和哈希。

pyproject.toml 里写的是"我要什么", uv.lock 记录的是"实际装了什么"。前者是意图, 后者是事实。两个都必须提交 git。不提交 lock 文件的话, 你钉死了 fastapi==0.141.1, 但 fastapi 依赖的 starlette 版本还是浮动的。

4.4 配置 ruff

ruff 是代码检查工具 (linter), 一秒钟扫完整个项目, 能找出未使用的 import、可能的 bug、不一致的代码风格。把它的规则写进 pyproject.toml:

[tool.ruff]
line-length = 110
target-version = "py312"

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM"]
ignore = [
    "E501",    # 行长交给 formatter, lint 不重复报
    "B008",    # FastAPI 的 Depends()/Header() 就是写在默认参数里的, 这是框架规定的写法
    "UP042",   # str+Enum 而不是 StrEnum: Pydantic 序列化行为更明确, 且兼容 3.10
]

[tool.ruff.lint.per-file-ignores]
# eval/ 和 finetune/ 的脚本要先 sys.path.insert 再 import 本项目模块, 顺序不能调
"eval/*.py" = ["E402"]
"finetune/*.py" = ["E402"]
"scripts/*.py" = ["E402"]
"tests/*.py" = ["E402"]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-q"

六组规则的含义: E 代码风格、F 逻辑错误 (未定义变量、未使用 import)、I import 排序、UP 用新语法替代过时写法、B 常见 bug 模式、SIM 可以简化的写法。

关于 ignore 的重要原则: 每一条 ignore 后面必须写理由。没有理由的 ignore 半年后没人知道能不能删, 于是永远留着, 于是 lint 配置越来越松, 最后形同虚设。上面三条都能说清为什么。

为什么现在就配 lint: 项目只有一个空 app/ 目录时配置 lint 成本最低。等到 3000 行代码时再开 select = [...], 你会一次性收到 200 个错误然后放弃。


五、写第一个文件: 配置模块

5.1 为什么配置不能写在代码里

假设你把模型名字写在 grader.py 里:

resp = client.chat.completions.create(model="deepseek-chat", ...)

三个问题会依次出现:

  1. 想换模型试试效果, 得改代码 —— 而改代码就要提交、要 review、要重新部署
  2. 本地想用便宜模型、线上用贵模型, 做不到 (同一份代码不能有两种行为)
  3. API Key 只能写在代码里, 一提交就泄漏

所以配置必须来自环境变量。 代码只读环境变量, 不关心它是从哪儿来的 —— 本地从 .env 文件来, 线上从容器的环境变量来, CI 里从 workflow 的 env 来。

5.2 app/core/config.py

"""配置: 只从环境变量读, 一个 Settings 实例走全程。"""
from __future__ import annotations

import os
from dataclasses import dataclass, field

try:
    from dotenv import load_dotenv

    load_dotenv()
except ImportError:
    pass


def _int(key: str, default: int) -> int:
    try:
        return int(os.getenv(key, str(default)))
    except ValueError:
        return default


def _float(key: str, default: float) -> float:
    try:
        return float(os.getenv(key, str(default)))
    except ValueError:
        return default

三个细节值得停一下:

  • load_dotenv() 包在 try 里: 生产容器里不装 python-dotenv 也能跑 (环境变量由容器注入)。少了这个 try, 缺一个只在开发时用的包会让整个服务起不来。
  • _int / _float 吞掉 ValueError: 环境变量都是字符串。有人在 .env 里写 TOP_K=五, 我们希望回退到默认值 5 继续跑, 而不是让服务因为一个配置错误彻底起不来。
  • from __future__ import annotations: 让类型注解变成"字符串", 这样 X | None 这种写法在旧版本 Python 上也不报错, 而且能引用还没定义的类。这个项目每个文件都有这一行。

接着是配置本体:

@dataclass
class Settings:
    # LLM
    llm_api_key: str = field(default_factory=lambda: os.getenv("LLM_API_KEY", ""))
    llm_base_url: str = field(
        default_factory=lambda: os.getenv("LLM_BASE_URL", "https://api.deepseek.com/v1")
    )
    llm_model: str = field(default_factory=lambda: os.getenv("LLM_MODEL", "deepseek-chat"))
    temperature: float = field(default_factory=lambda: _float("LLM_TEMPERATURE", 0.3))

    # RAG
    corpus_dir: str = field(default_factory=lambda: os.getenv("CORPUS_DIR", "corpus"))
    chroma_dir: str = field(default_factory=lambda: os.getenv("CHROMA_DIR", "data/chroma"))
    embed_model: str = field(
        default_factory=lambda: os.getenv("EMBED_MODEL", "BAAI/bge-small-zh-v1.5")
    )
    rerank_model: str = field(
        default_factory=lambda: os.getenv("RERANK_MODEL", "BAAI/bge-reranker-base")
    )
    use_rerank: bool = field(
        default_factory=lambda: os.getenv("USE_RERANK", "false").lower() == "true"
    )
    top_k: int = field(default_factory=lambda: _int("TOP_K", 5))
    fetch_k: int = field(default_factory=lambda: _int("FETCH_K", 30))

    # 面试
    db_path: str = field(default_factory=lambda: os.getenv("DB_PATH", "data/app.db"))
    max_questions: int = field(default_factory=lambda: _int("MAX_QUESTIONS", 6))
    max_followup_depth: int = field(default_factory=lambda: _int("MAX_FOLLOWUP_DEPTH", 2))

    # 鉴权
    api_keys: str = field(default_factory=lambda: os.getenv("API_KEYS", ""))
    daily_limit: int = field(default_factory=lambda: _int("DAILY_LIMIT", 100))

    # 离线模式: 规则判分 + Hash 向量, 不调任何外部服务。CI 和无网演示用。
    offline: bool = field(
        default_factory=lambda: os.getenv("OFFLINE", "false").lower() == "true"
    )

    @property
    def allowed_keys(self) -> set[str]:
        return {k.strip() for k in self.api_keys.split(",") if k.strip()}


settings = Settings()

为什么用 field(default_factory=lambda: ...) 而不是直接 = os.getenv(...): dataclass 的默认值在类定义时就求值了。直接写 = os.getenv("LLM_MODEL", "...") 的话, 这个值在 import 这个模块的瞬间被固定, 后面测试里改环境变量就没用了。用 default_factory 推迟到实例化时才读。

use_rerank 为什么要 .lower() == "true": 环境变量 USE_RERANK=false 是字符串 "false", 而 Python 里 bool("false") 是 True —— 非空字符串都是真。直接 bool(os.getenv(...)) 会让"关掉"变成"打开", 这是环境变量最经典的坑。

5.3 每个文件都自带一个自测

在文件末尾加上:

if __name__ == "__main__":
    s = settings
    print(f"llm_model     = {s.llm_model}")
    print(f"llm_base_url  = {s.llm_base_url}")
    print(f"llm_api_key   = {'已配置(' + str(len(s.llm_api_key)) + '字符)' if s.llm_api_key else '未配置'}")
    print(f"embed_model   = {s.embed_model}")
    print(f"use_rerank    = {s.use_rerank}")
    print(f"top_k/fetch_k = {s.top_k}/{s.fetch_k}")
    print(f"api_keys      = {len(s.allowed_keys)} 个")

这个模式贯穿整个项目: 每个模块末尾都有一个 if __name__ == "__main__":, 单独跑它就能验证这个模块是活的。好处是出问题时能一层层往下查, 而不是只知道"整个服务起不来"。

注意 API Key 只打印长度不打印内容。日志会被收集、被截图、被贴到群里 —— 打印 Key 明文是"迟早出事"的那类习惯。

✅ Checkpoint 2: 跑一下配置模块。

uv run python -m app.core.config

真实输出 (还没建 .env, 所以 Key 未配置):

llm_model     = deepseek-chat
llm_base_url  = https://api.deepseek.com/v1
llm_api_key   = 未配置
embed_model   = BAAI/bge-small-zh-v1.5
use_rerank    = False
top_k/fetch_k = 5/30
api_keys      = 0 个

如果报 No module named app.core.config, 检查两件事: 你在项目根目录 (ls 能看到 pyproject.toml), 以及 app/__init__.py 和 app/core/__init__.py 都存在。


六、.env 与 .gitignore

6.1 .env.example: 给人看的模板

.env 里有真 Key, 不能提交。但新同学 clone 下来得知道有哪些配置项 —— 所以提交一份不含真值的模板:

# 复制成 .env 再改: cp .env.example .env
# .env 已经在 .gitignore 里, 永远不要把真 Key 提交上去。

# ---------- LLM (判分和追问用) ----------
LLM_API_KEY=
LLM_BASE_URL=https://api.deepseek.com/v1
LLM_MODEL=deepseek-chat
# 判分要稳定复现, 温度必须低。不要用默认的 1.0 ——
# 同一份答案两次给出的分数差 3 分, 你就没法做任何对照实验了。
LLM_TEMPERATURE=0.3

# ---------- 离线模式 ----------
# true = 规则判分 + HashEmbedder, 不联网不花钱。CI 和无网演示用。
OFFLINE=false

# ---------- RAG ----------
CORPUS_DIR=corpus
CHROMA_DIR=data/chroma
EMBED_MODEL=BAAI/bge-small-zh-v1.5
RERANK_MODEL=BAAI/bge-reranker-base
# 重排默认关。开之前先跑 eval/eval_retrieval.py --rerank 看看是不是真的变好了。
USE_RERANK=false
TOP_K=5
# 粗召数量。必须显著大于 TOP_K, 否则精排没有挑选空间。
FETCH_K=30
# 向量召回的相似度阈值。低于这个分就当没检索到 —— 宁可交白卷也不要拿无关资料去编。
RETRIEVE_THRESHOLD=0.35

# ---------- 面试 ----------
DB_PATH=data/app.db
MAX_QUESTIONS=6
# 一道题最多追问几层。不封顶的话模型会一直往下钻, 面试变成拷问。
MAX_FOLLOWUP_DEPTH=2

# ---------- 鉴权与限流 ----------
# 逗号分隔的多个 Key。留空 = 接口对所有人开放, 只有本地开发才这样。
# 生成一个随机 Key: python -c "import secrets; print(secrets.token_urlsafe(32))"
API_KEYS=
DAILY_LIMIT=100

# ---------- HuggingFace ----------
# 国内直连 huggingface.co 基本下不动, 走镜像站。
HF_ENDPOINT=https://hf-mirror.com

然后建自己的:

cp .env.example .env

第 7 章之前 .env 里什么都不用填 —— 前面几章全程离线。

6.2 .gitignore: 唯一一件"做错了会上新闻"的事

# ---------- 绝对不能进仓库的 ----------
# 真实 API Key。这一行是这个文件里最重要的一行。
.env
.env.*
!.env.example

# ---------- 数据与模型 ----------
data/
runs/
*.db
*.sqlite3

finetune/data/
finetune/output/
*.gguf
*.safetensors

.cache/
hf_cache/

# ---------- Python ----------
__pycache__/
*.py[cod]
*.egg-info/
build/
dist/
.venv/
venv/

# ---------- 工具缓存 ----------
.pytest_cache/
.ruff_cache/
.mypy_cache/
.coverage
htmlcov/
coverage.xml

# ---------- 编辑器与系统 ----------
.idea/
.vscode/
*.swp
.DS_Store

三个细节:

  • .env.* 后面跟 !.env.example: 前一行忽略所有 .env. 开头的文件, 后一行用 ! 把模板重新放回来。顺序不能反。
  • data/ 必须忽略: 向量库几十兆, 而且它是运行产物 —— 从语料重新生成即可。提交它会让每次 git pull 都下载几十兆二进制。
  • uv.lock 不在忽略列表里, 这是故意的。它必须提交, 理由见 4.3。

6.3 如果已经把 Key 提交了怎么办

先撤销 Key, 再清历史。 顺序很重要 —— 很多人先花两小时用 git filter-repo 清历史, 期间 Key 还是有效的, 而爬 GitHub 新提交的机器人是分钟级的。

正确做法: 立刻去 API 提供商后台把这个 Key 删掉并生成新的 (30 秒), 然后再考虑历史。因为 Key 一旦推到 GitHub 就必须当作已泄漏, 清历史只是清理, 不是补救。


七、最后确认一遍

uv run ruff check .

✅ Checkpoint 3: 应该看到:

All checks passed!

现在的项目结构:

ls -a

应该有: .env、.env.example、.gitignore、.python-version、.venv、app/、corpus/、data/、docker/、eval/、finetune/、pyproject.toml、scripts/、static/、tests/、uv.lock。


踩坑与专家提示

  1. 不要用 sudo pip install。 它会把包装到系统 Python 里, 可能破坏操作系统自带的工具 (macOS 和 Ubuntu 都用 Python 写系统脚本)。所有包都应该装在虚拟环境里。
  2. uv run 而不是 python。 直接敲 python -m app.core.config 用的是系统 Python, 它没有你装的包。报 ModuleNotFoundError: No module named 'pydantic' 十次里九次是这个原因。
  3. __init__.py 忘了加, 报错完全不提它。 报的是 No module named app.core, 让人以为是路径问题。建项目时一次性 touch 齐, 别边写边加。
  4. 环境变量里的布尔值。 os.getenv("USE_RERANK") 拿到的是字符串, bool("false") == True。必须显式 .lower() == "true"。
  5. .env 不要用引号。 写 LLM_MODEL="deepseek-chat" 有些解析器会把引号也当成值的一部分, 于是你的模型名变成 "deepseek-chat" (带引号), 请求直接 400。
  6. 配置项加了就要在 .env.example 里补一行。 否则半年后你自己都不知道有哪些开关。这个文件是配置项的唯一文档。
  7. 不要在 config.py 里做任何验证之外的事。 见过有人在配置模块里初始化数据库连接, 结果 import 配置的每一个文件都被迫连库, 单元测试跑不起来。配置模块只做一件事: 读环境变量。

面试视角: 本章高频五问

Q1: uv 和 pip / poetry / conda 的区别?

pip 只管装包, 虚拟环境要自己建。poetry 管项目和依赖但不管 Python 版本本身, 而且解析依赖慢。conda 能管非 Python 依赖 (CUDA、编译器), 适合科学计算, 但环境很重。uv 用 Rust 写, 同时管 Python 版本、虚拟环境、依赖和运行, 速度比 pip 快一个数量级, 是目前新项目的默认选择。选型的实际考虑: 如果项目依赖 CUDA 生态, conda 仍有优势; 其他情况 uv。

Q2: 为什么要钉死依赖版本? 不怕安全补丁装不上吗?

钉死版本换来的是"可复现构建" —— 同一个 commit 在任何时间任何机器上装出完全一样的依赖。安全补丁的问题用另一套机制解决: 定期 (比如每月) 主动升级并跑测试, 或者用 Dependabot 自动提 PR。关键区别是升级发生在你选的时间, 而不是某次构建时突然发生。

Q3: .env 泄漏了怎么办?

先撤销 Key 再清历史, 不能反。Key 推到 GitHub 后必须当作已泄漏 —— 有机器人分钟级扫描新提交。更好的做法是从一开始就不让它有机会泄漏: .gitignore 里第一行就写 .env, 并且 CI 里加一条扫描 (比如 gitleaks) 拦住带密钥的提交。生产环境则完全不用 .env, 用云厂商的密钥管理服务注入。

Q4: 配置为什么要收在一个 Settings 类里, 到处 os.getenv 不行吗?

三个原因。一是可发现性: 想知道这个项目有哪些配置项, 看一个类就行, 不用全局搜 os.getenv。二是类型转换只写一次: TOP_K 是 int、USE_RERANK 是 bool, 转换逻辑集中在一处, 不会有的地方忘记转。三是可测试: 测试里 monkeypatch.setattr(settings, "daily_limit", 2) 一行就能改, 而散落的 os.getenv 要一个个 patch。

Q5: from __future__ import annotations 是干什么的?

让类型注解延迟求值 (存成字符串, 不在定义时执行)。三个好处: X | None 这种语法在 3.9 上也能写; 可以引用后面才定义的类而不用加引号; 减少一点 import 时的开销。Python 3.14 之后这会成为默认行为, 但现在还得手写。这个项目每个文件第一行都是它。


划重点

  1. uv 一个工具搞定 Python 版本、虚拟环境、依赖、运行。用 uv run 不用 activate。
  2. 虚拟环境的意义是隔离: 两个项目要不同版本的同一个包时不至于互相破坏。
  3. 依赖版本用 == 钉死, uv.lock 必须提交。"能跑"是有时效的, 钉死是为了让升级发生在你选的时间。
  4. 配置只从环境变量读, 收在一个 Settings 类里。用 default_factory 推迟求值。
  5. 环境变量的布尔值必须 .lower() == "true", 因为 bool("false") 是 True。
  6. .gitignore 第一行是 .env。Key 泄漏了先撤销再清历史。
  7. 每个模块末尾写一个 if __name__ == "__main__": 自测。项目大了以后这是最省时间的习惯。
  8. 项目还是空的时候就配好 ruff。3000 行以后再配你会直接放弃。

下一章预告

有了骨架, 下一章开始填内容 —— 但填的不是代码, 是语料。我们会先定一份 Android 知识点大纲, 然后设计一套带元数据的 Markdown 规范 (知识点、难度、标签), 手写 7 篇语料, 最后写一个 60 行的加载器把它们读进来。这一章决定了整个系统的知识天花板: 语料里没有的东西, 后面再强的检索和模型都变不出来。

本页由 Markdown 自动渲染 · 共 628 篇笔记