第 5 章 · 02 命令解析与任务历史 本节摘要:本节讲 的两个支撑机制——命令解析与任务历史。命令解析用标准库 ,处理启动时的 / / 等参数;进入 REPL 后,命令路由则是一套简单的字符串比对( 、 )。任务历史存在 ,用纯文本逐行追加,保留最近 5 条,欢迎屏会展示其中最近 3 条。本节逐行拆 、 入口、以及 / 两个历史读写函数,讲清这套「极简但不简陋」的持久化设计。读完本节,你理解 AutoHedge 如何用几十行标准库做出可用的命令路由与跨会话记忆。 内容来源:原项目源码 (argparse 段第 189-214 行、main 第 217-226 行、历史读写第 47-72 行),精读并套用体系化模板。 学习目标 阅读完本节,你应当能够: 逐行读懂 与 的入口逻辑。
本节摘要:本节讲
cli.py的两个支撑机制——命令解析与任务历史。命令解析用标准库argparse,处理启动时的--version/--help/help等参数;进入 REPL 后,命令路由则是一套简单的字符串比对(quit/exit/q、help/?/h)。任务历史存在~/.autohedge/recent_tasks.txt,用纯文本逐行追加,保留最近 5 条,欢迎屏会展示其中最近 3 条。本节逐行拆_build_parser()、main()入口、以及_get_recent_tasks()/_append_recent()两个历史读写函数,讲清这套「极简但不简陋」的持久化设计。读完本节,你理解 AutoHedge 如何用几十行标准库做出可用的命令路由与跨会话记忆。
内容来源:原项目源码
autohedge/cli.py(argparse 段第 189-214 行、main 第 217-226 行、历史读写第 47-72 行),精读并套用体系化模板。
阅读完本节,你应当能够:
_build_parser() 与 main() 的入口逻辑。argparse 的 --version/--help 如何自动处理。autohedge help 要特判而 --help 不用。_get_recent_tasks() 与 _append_recent()。AutoHedge 的「命令解析」其实分两层,容易混淆,先理清:
| 时机 | 机制 | 处理对象 | 代码 |
|---|---|---|---|
启动期(终端敲 autohedge ...) |
argparse |
--version、--help、bare help |
_build_parser() + main() |
| REPL 期(进入循环后的输入) | 字符串比对 | quit/exit/q、help/?/h、任务 |
run_repl()(上一节讲过) |
本节聚焦启动期(argparse)。REPL 期的路由在上一节 run_repl 已讲,核心就是 if lower in ("quit", ...) 这套简单比对——没用 argparse,因为 REPL 内的命令太简单,不值得背一个解析器。
cli.py 第 189-214 行:
def _build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="autohedge", description="AutoHedge — interactive REPL for running research and hedging tasks.", epilog=""" Commands (when running the REPL): <task> Run a task (e.g. 'Analyze NVDA for 50k allocation') help, ?, h Show in-REPL tips quit, exit, q Exit the REPL Examples: autohedge Start the interactive REPL autohedge help Show this help autohedge --help Show this help autohedge --version Show version """, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "--version", "-v", action="version", version=f"%(prog)s {VERSION}", help="Show program version and exit.", ) return parser
逐段解读:
prog="autohedge":命令名(帮助文本里显示用)。description:一句话描述(出现在 --help 顶部)。epilog:长文本,出现在 --help 底部——列了 REPL 内命令、示例。注意它同时写了 autohedge help 与 autohedge --help,这是因为作者特判了 bare help(见下文)。formatter_class=argparse.RawDescriptionHelpFormatter:保留 epilog 的原始换行(不被 argparse 自动重排),让示例的缩进对齐不被破坏。parser.add_argument("--version", "-v", action="version", version=f"%(prog)s {VERSION}", ...)
"--version" 与 "-v" 是长短两种写法。action="version":argparse 的内置动作——遇到 --version/-v,自动打印 version 字符串后直接 sys.exit()。%(prog)s 会被替换成 prog 即 autohedge,所以输出形如 autohedge 0.1.5。add_argument("task", ...)——也就是说 argparse 不接受位置参数任务。任务只能进 REPL 后输入。💡 核心心法:argparse 的内置
action="version"与--help(默认就有)是「自退出」动作——它们被触发后 argparse 自己调sys.exit(),不会返回到你的代码。所以调用方不需要处理这两种情况,只管parser.parse_args()后继续走主逻辑即可。
cli.py 第 217-226 行:
def main() -> None: """Entry point for the AutoHedge CLI.""" parser = _build_parser() # Treat bare "help" as --help (e.g. "autohedge help") if len(sys.argv) == 2 and sys.argv[1].lower() == "help": parser.print_help() sys.exit(0) parser.parse_args() # exits on --help / --version run_repl() sys.exit(0)
逐行解读:
if len(sys.argv) == 2 and sys.argv[1].lower() == "help": parser.print_help() sys.exit(0)
sys.argv 是命令行参数列表(argv[0] 是程序名)。这句判断「如果用户敲的是 autohedge help(恰好一个参数,且是 help)」,就打印帮助并退出。
为什么要特判? 因为 argparse 默认只认 --help(双横线),不认 bare help。但用户的直觉是 autohedge help(像 git 那样),不是 autohedge --help。作者用一行特判满足直觉。注意它不区分大小写(.lower()),所以 autohedge HELP 也行。
parser.parse_args() # exits on --help / --version run_repl() sys.exit(0)
parser.parse_args():解析参数。如果用户敲了 --help/--version,argparse 内部会 sys.exit(),不会执行到这里(注释明说)。所以这行的隐含语义是:「能走到这里,说明没要 help/version,直接进 REPL」。run_repl():进 REPL 循环(上一节讲)。sys.exit(0):REPL 退出后,以 0(成功)退出进程。实际上 run_repl 本身不退出进程,这行保证进程干净收尾。最后看入口注册(第 229-230 行):
if __name__ == "__main__": main()
直接 python -m autohedge.cli 或 python cli.py 时触发。但正常用户不会这么敲——他们敲 autohedge,由 pyproject.toml 的 [tool.poetry.scripts] 注册(autohedge = "autohedge.cli:main")直接调 main()。
cli.py 第 47-48 行:
RECENT_FILE = Path.home() / ".autohedge" / "recent_tasks.txt" MAX_RECENT = 5
Path.home():跨平台的用户主目录(Windows 是 C:\Users\用户名,macOS/Linux 是 /home/用户名 或 /Users/...)。~/.autohedge/recent_tasks.txt:任务历史文件。注意是点开头目录(.autohedge)——类 Unix 风格的「隐藏」配置目录(虽然 Windows 不靠点隐藏)。MAX_RECENT = 5:最多保留 5 条。欢迎屏只展示其中最近 3 条(上一节 _welcome 里 recent[-3:])。这个位置选择有讲究:放在用户主目录而非项目目录,意味着历史是跨项目/跨会话的——你在 A 项目跑的任务,B 项目也能看到。这是「用户级」记忆,与项目代码解耦。
cli.py 第 51-60 行:
def _get_recent_tasks() -> list[str]: if not RECENT_FILE.exists(): return [] try: lines = RECENT_FILE.read_text().strip().splitlines() return [ ln.strip() for ln in lines[-MAX_RECENT:] if ln.strip() ] except Exception: return []
逐行解读:
if not RECENT_FILE.exists(): return []:文件不存在(首次运行)直接返回空列表,不报错。read_text().strip().splitlines():整个文件读成字符串,去首尾空白,按行切分成列表。lines[-MAX_RECENT:]:取最后 5 行(最近 5 条)。ln.strip() for ln in ... if ln.strip():每行去空白,跳过空行(防脏数据)。except Exception: return []:任何 IO 异常(权限、编码等)都吞掉,返回空列表——绝不让历史读取搞崩欢迎屏。这是一个防御性极强的函数:文件不存在、读失败、内容损坏,都优雅降级为「无历史」。门面层的健壮性体现。
⚠️ 注意
list[str]类型注解:它用 Python 3.10+ 的内置泛型语法(list[str]而非List[str])。这就是为什么pyproject.toml要求python = "^3.10"——3.9 不支持这种写法。
cli.py 第 63-72 行:
def _append_recent(task: str) -> None: try: RECENT_FILE.parent.mkdir(parents=True, exist_ok=True) recent = _get_recent_tasks() if task in recent: recent.remove(task) recent.append(task) RECENT_FILE.write_text("\n".join(recent[-MAX_RECENT:])) except Exception: pass
逐行解读:
RECENT_FILE.parent.mkdir(parents=True, exist_ok=True):创建 ~/.autohedge/ 目录(含父目录,已存在不报错)。recent = _get_recent_tasks():先读出已有历史。if task in recent: recent.remove(task):去重——如果这个任务已在历史里,先删掉旧的。recent.append(task):把新任务追加到末尾(末尾 = 最新)。"\n".join(recent[-MAX_RECENT:]):取最后 5 条,用换行符拼成字符串,整体覆盖写入(write_text 是覆盖,不是追加)。except Exception: pass:任何异常静默吞掉——历史写失败不影响任务执行。注意一个反直觉点:函数名叫 _append_recent(追加),但实际是「读 → 去重 → 改列表 → 覆盖写」。这种「读改写」模式保证了去重与截断,代价是不是真正的 append(每次都重写整个文件)。对 5 条小文本,这点开销可忽略。
💡 核心心法:这种「文本逐行 + 覆盖写 + 截尾 N 条」是极简持久化的经典套路——不用数据库、不用 JSON,一个 txt 文件搞定。适合数据量小、结构简单的场景(如最近任务、最近打开文件)。代价是没有索引、没有并发保护,数据量大或要并发写就得换 SQLite。
把本节的两块与上一节串起来,完整流程:
启动 autohedge └─ main() → parser.parse_args() → run_repl() └─ _welcome() └─ _get_recent_tasks() ← 读历史,展示最近 3 条 └─ while True: 接收输入 └─ 用户输入任务 └─ _append_recent(task) ← 写历史(去重 + 截尾 5) └─ AutoHedge().run(task) ← 执行(下一节)
历史是跨会话的:你今天跑的任务,明天再开 autohedge,欢迎屏「Recent activity」里还能看到。这是 AutoHedge 唯一的「记忆」——Agent 本身没有跨会话记忆,只有这个用户级的任务列表。
--version/--help/bare help),REPL 期用字符串比对(quit/help/任务);REPL 命令太简单没用 argparse。_build_parser:ArgumentParser(prog, description, epilog, RawDescriptionHelpFormatter);--version/-v 用 action="version" 自退出;不接受位置参数任务。help 特判:sys.argv[1].lower()=="help" 时 print_help 后退出——满足用户 autohedge help(像 git)的直觉;--help 由 argparse 自动处理。main() 三步:parse_args()(自退出 help/version)→ run_repl() → sys.exit(0);由 pyproject.scripts 注册为 autohedge 命令。~/.autohedge/recent_tasks.txt,MAX_RECENT=5,欢迎屏展示最近 3;放用户主目录=跨项目跨会话。_get_recent_tasks:防御性极强——文件不存在/读失败/内容损坏都优雅降级为空列表,不崩欢迎屏。_append_recent:「读 → 去重 → 改列表 → 覆盖写」(名 append 实 rewrite);异常静默吞掉不影响任务;极简持久化适合小数据。下一节,我们看 REPL 背后的核心——
autohedge/main.py的 AutoHedge 主类,以及它支持的三种输出格式(list/dict/str)。