Python / AI · 工程与数据输入 · LESSON 10

CLI 参数与配置入口

将 npm scripts 迁移到 argparse,让命令行工具具备帮助信息和可测试入口。

14 分钟cli · argparse · configuration

学习目标:把 CLI 当成稳定的自动化接口

本节结束时,你能把 JavaScript/TypeScript 中的 process.argv、环境变量和 npm script 迁移到 Python argparse,并解释参数解析、配置合并、退出码和业务执行各自负责什么。你还会写出可由 pytest 直接调用的 main(argv=None),让数据准备、评估或推理命令既适合人手运行,也适合 CI、容器和调度系统运行。

CLI 不是“把几行脚本包进一个文件名”。一旦它被数据管线调用,参数名、默认值、帮助文本、标准输出、标准错误和退出码就成为公共 API。本节的所有例子都把配置收束成一个对象,再把这个对象交给纯业务函数;这样运行结果更容易重放,也更容易判断是输入问题还是处理问题。

从 JS/TS 迁移的心智模型:argv 只是配置的一种来源

JavaScript/TypeScript 里常见的写法是直接读 process.argv[2],再用 process.env.LIMIT 补默认值。它能快速启动,但缺少参数说明、类型错误和稳定的失败边界。Python 的 ArgumentParser 会根据声明生成 --help,把字符串转换成目标类型,并在解析失败时写入 stderr、返回退出码 2;它不会替你判断数值是否有业务意义,也不会自动合并配置文件。

TRANSLATION LENS 同一个意图,两种工程表达 窄屏可左右滑动查看完整代码
JS / TS
const input = process.argv[2] ?? "events.jsonl";
const limit = Number(process.env.LIMIT ?? 100);
await run({ input, limit });
Python
parser = argparse.ArgumentParser(description="prepare AI events")
parser.add_argument("input", nargs="?", default="events.jsonl")
parser.add_argument("--limit", type=int, default=100)
args = parser.parse_args(argv)
run(Config(input=args.input, limit=args.limit))

1. argparse 的解析行为:字符串先进入边界

命令行本质上从 shell 传来字符串列表。位置参数描述必需的输入,选项参数用 --name 表达可选配置,type=int 只做转换而不做范围校验,choices 可以限制离散值,action="store_true" 可以表示开关。parse_args() 默认读取全局 sys.argv[1:],而 parse_args(argv) 使用调用者提供的列表;后一种形式是可测试入口的基础。

import argparse
from dataclasses import dataclass

@dataclass(frozen=True)
class Config:
    input_path: str
    output_path: str
    limit: int
    label: str | None

def parse_args(argv: list[str] | None = None) -> Config:
    parser = argparse.ArgumentParser(description="prepare labeled AI events")
    parser.add_argument("input_path")
    parser.add_argument("--output", dest="output_path", default="prepared.jsonl")
    parser.add_argument("--limit", type=int, default=100)
    parser.add_argument("--label")
    args = parser.parse_args(argv)
    if args.limit < 1:
        parser.error("--limit must be positive")
    return Config(args.input_path, args.output_path, args.limit, args.label)

注意 parser.error() 的行为:它打印一行以 usage: 开头的诊断到 stderr,然后抛出 SystemExit(2)。这与“数据文件读不到”不同,后者是业务执行阶段的失败,通常由 main 返回 1。把范围检查写在解析边界,可以让错误尽早出现;把文件读取放在 run,则可以单独测试解析而不需要磁盘 fixture。

2. 配置优先级:默认值、文件、环境变量、CLI

一个可解释的约定是:内置默认值 < 配置文件 < 环境变量 < CLI 显式值。关键不在于一定选哪套顺序,而在于每个来源是否可见、是否能区分“用户没有提供”和“用户明确提供了默认值”。如果 argparse 直接把默认值填成 100,后续就不知道 --limit 是否由用户指定;可以先用 default=None 解析,再单独合并配置。

import os
from pathlib import Path

def merge_config(args, file_config: dict[str, object]) -> Config:
    limit = args.limit
    if limit is None:
        limit = os.getenv("EVENT_LIMIT", file_config.get("limit", 100))
    limit = int(limit)
    if limit < 1:
        raise ValueError("limit must be positive")
    output = args.output_path or str(file_config.get("output", "prepared.jsonl"))
    return Config(Path(args.input_path).as_posix(), output, limit, args.label)

这个例子中,环境变量仍然是字符串,所以合并阶段必须再次转换和校验;配置文件的值也不能盲信。生产命令启动时可以记录最终的非敏感配置、输入文件哈希、代码版本和 schema 版本,但绝不能把 API token 放在帮助文本或日志里。配置优先级如果不写在文档和测试里,CI 与本地运行很快会产生不同结果。

3. 可测试的 main 与退出码

main(argv=None) -> int 只做三件事:解析并校验配置、调用 run(config)、把可预期的业务异常翻译成 stderr 和非零返回值。不要在深层函数里直接调用 sys.exit,因为测试一旦调用它就会被进程级异常打断。模块末尾才使用 raise SystemExit(main()),把整数交给 shell。

import sys

def run(config: Config) -> dict[str, int]:
    if config.input_path == config.output_path:
        raise ValueError("input and output must be different")
    return {"read": 2, "written": min(config.limit, 2)}

def main(argv: list[str] | None = None) -> int:
    try:
        config = parse_args(argv)
        stats = run(config)
    except (OSError, ValueError) as exc:
        print(f"error: {exc}", file=sys.stderr)
        return 1
    print(f"written={stats['written']}")
    return 0

if __name__ == "__main__":
    raise SystemExit(main())

这里要区分三种结果:合法参数且业务成功,输出统计并返回 0;参数语法错误,由 argparse 返回 2;参数合法但文件或业务规则失败,返回 1。稳定的退出码让 CI 能自动阻断坏数据,也让上层调度器区分“重跑无效”与“暂时故障”。输出统计应是可解析的短文本或 JSON,不要混入完整样本内容。

运行验证:从 help 到可观察输出

先运行 python -m package.prepare --help,确认帮助中出现 input、output、limit 和 label;再运行 python -m package.prepare events.jsonl --limit 2,预期输出类似 written=2、进程退出码为 0。随后运行 --limit 0,预期 stderr 出现 --limit must be positive、退出码为 2;让输入路径不存在,预期业务错误退出码为 1。

pytest 不需要修改全局 sys.argv:直接调用 parse_args(["events.jsonl", "--limit", "2"]),断言 Config.limit 等于 2;调用 main(["events.jsonl", "--limit", "0"]) 时捕获 argparse 的 SystemExit 并断言 code 为 2;给 run 注入 fake 数据读取器,验证统计和输出。这样每个运行结果都对应一条明确的输入路径。

常见错误、排错与调试路径

直接访问 sys.argv[1] 会在缺参时产生 IndexError;把 --limit 只声明成 type=int 会让 0 和负数通过;在 main 中使用 print 打印调试对象会污染机器可读输出;把相对路径当成固定路径则会在 CI 工作目录变化时失败。还有一个隐蔽问题是用环境变量覆盖 CLI 显式值,导致用户传入的参数看似没有生效。

排错按四步进行:第一运行 --help 看声明是否真的加载;第二打印或断言归一化后的 Config,而不是原始 argv;第三用最小 JSONL fixture 重现业务错误;第四检查 stderr、stdout 和退出码是否符合约定。若 AI 数据准备任务结果数量不对,再对照最终配置、输入哈希、limit 和过滤统计,不要先怀疑模型。凭据问题只记录变量名和是否存在,不记录变量值。

练习:为数据准备工具设计入口

任务是增加 input--output--limit--label--config 参数,采用“默认值 < 配置文件 < 环境变量 < CLI 显式值”的优先级。parse_args(argv) 必须可被 pytest 直接调用,main 在业务异常时返回 1,成功时返回 0;请验收帮助文本、缺少 input、limit 为零、路径冲突和合法配置五种结果。

01
TRY IT YOURSELF

CLI 参数与配置入口练习

为数据准备工具增加 input、output、limit 和 label 参数,让 parse_args(argv) 可被 pytest 直接调用,并让 main 在业务异常时返回 1。

给我一点提示

argparse 只负责解析;让 main 只负责组装 Config、调用 run 和翻译 OSError/ValueError。

查看参考答案
def main(argv=None):
  try:
      config = parse_args(argv)
      stats = run(config)
      print(stats)
      return 0
  except (OSError, ValueError) as exc:
      print(f"error: {exc}", file=sys.stderr)
      return 1

if __name__ == "__main__":
  raise SystemExit(main())
本节结论

完整答案还应包括测试:合法参数返回 Config,limit 为零在解析阶段得到退出码 2,run 抛出 OSError 时 main 返回 1,合法 run 输出统计并返回 0。再添加一个配置优先级测试,确保 CLI 显式值不会被环境变量悄悄覆盖。

与 AI 数据、模型和服务的连接

AI 任务往往不是由人手敲命令完成:数据抽取、JSONL 清洗、embedding 批处理、模型评估和推理服务健康检查都会由 CLI 启动。把最终配置、输入哈希、模型名称、模型版本、token 预算和输出路径写进运行记录,才能在指标变化时复盘“究竟跑了什么”。将凭据从环境变量或密钥管理服务读取,并让 CLI 只报告是否配置,避免 secrets 进入 shell history 和日志。

如果 CLI 要调用远程模型服务,它还应给每批任务返回成功、跳过、失败数量,并把可重试错误与坏样本分开。退出码可以让调度器决定是否重试,输出的 JSON 统计可以让下游监控样本量和标签分布。也就是说,命令行入口既是开发者体验,也是 AI 数据管线的控制面。

小结

可靠的 Python CLI = argparse 的显式声明 + 可解释的配置优先级 + 薄的 main(argv) + 分层退出码 + 可重放的运行记录。迁移自 JS/TS 时,不要只把 process.argv 换成 sys.argv;要把解析、校验、业务执行和服务调用的责任分开,才能得到可测试、可调度的 AI 工具。

FURTHER READING

延伸阅读

先完成本节练习,再用这些资料查阅完整 API 和真实项目组织方式。

当前学习阶段工程与数据输入
0/8

阶段共 8 节课,按顺序完成更容易建立完整的迁移模型。