很多做过 AI Agent 开发的人应该都有同感:让智能体去“看”一个网页,成本高得让人心疼。随便打开一个常见的技术博客,页面源码动辄几十 KB,如果把内联脚本、样式、评论区和各种统计代码都算上,上到一两百 KB 也不奇怪。而主流大模型是按 token 计费的,粗略估算,1 个 token 大约对应 3 到 4 个英文字符,一个完整的网页转成 token,往往就是几千甚至上万。更麻烦的是,agent 每次要获取最新内容都得重新抓一遍,几轮对话下来,单单“读网页”这一个动作,就会消耗掉大量上下文。
所以当 Hacker News 上出现“Show HN: Turn any website into a CLI for AI agents”这个项目时,它能引起关注一点都不意外。项目标题里最有冲击力的信息是:这套方案声称比直接读 HTML 少用 142 倍的 token。先不管这个数字在多大范围内成立,至少它指出了一个非常本质的问题——HTML 是给人看的,不是给模型读的。与其让 agent 去忍受一堆视觉和交互相关的噪音,不如先把网页转成紧凑、结构化、命令式的接口,让 agent 像调用命令行一样去获取信息。
这篇文章不打算只复述这个标题,而是想把这个思路彻底拆开:它到底解决了什么问题、背后的架构是什么、和现在大热的 MCP / Codex CLI / Claude Code CLI 有什么关系、你自己怎么用几十行代码做一个最小版本,以及这套方案在真实工程里的边界在哪里。如果你正在做 AI 编程工具、Agent 工作流,或者只是心疼自己账单里的 token 消耗,这篇文章应该能给你一些可落地的参考。
1. 为什么 AI Agent 读网页时会“吃”掉大量 Token
很多人第一次调通 AI Agent 时都会有一个困惑:明明模型能力很强,为什么让它从网页里提取几条信息,效果却这么差、花费却这么高?问题往往不在模型,而在输入格式。HTML 是一种为浏览器渲染设计的文档格式,它包含大量与语义无关的噪音。一个典型网页里,真正承载信息的正文可能只占 10% 到 20%,剩下的全是导航菜单、侧边栏、推荐位、社交分享按钮、内联 CSS、JavaScript 变量定义、埋点脚本、JSON-LD 结构化数据和层层嵌套的 DIV 容器。
这些噪音对 token 消耗的影响是叠加的。首先是体积问题。一个看起来不算复杂的商业页面,浏览器保存下来的完整 HTML 经常超过 100 KB,这在 token 层面就是两三万 token。如果是英文内容,一个 token 大约对应 3 到 4 个字符,100 KB 的 HTML 大约在 2 万 token 以上;如果是中文为主的混合页面,token 数量还要再乘上一个系数。其次是重复问题。Agent 做信息提取时往往要多次访问同一个站点的不同页面,或者在同一页面反复请求最新内容,每一次都会重新计算整页 HTML 的 token。
另一个容易被忽视的因素是上下文长度。上下文窗口不是无限大的,即使模型支持百万元素的窗口,窗口里塞满了 HTML 标签和脚本,真正留给推理和工具调用的空间就被压缩了。Agent 在回答一个问题时,需要同时容纳浏览器返回的原始页面、当前对话历史、系统提示词、中间推理结果。页面的冗余部分挤占了大量上下文空间,导致 agent 能记住的有效信息更少,更容易在长任务中出现遗漏或前后不一致。
更进一步看,动态渲染的页面还会加剧成本。很多现代网站使用前端框架,真正的内容由 JavaScript 在浏览器端渲染,直接在 HTML 里拿不到。为了让 agent 读到这类页面,开发者往往要引入无头浏览器,一个完整的 Chromium 实例在内存和 CPU 上的开销姑且不提,它抓回来的 HTML 同样带着大量运行时产生的节点和样式标记,token 消耗只会更高。
所以这里的核心结论是:让 agent 直接读 HTML,本质上是在给模型布置一道“从工程文件里找用户可见内容”的逆向题目。模型确实能做,但效率极低、成本极高、稳定性还差。与其提高模型的解析能力,不如换一个思路,在输入端就把网页转换成 agent 真正需要的那种结构化信息。这也是“把网站变成 CLI”这个方案最值得讨论的地方。
2. 把网站变成 CLI:核心思路与整体架构
“把网站变成 CLI”这句话看起来很直接,但它并不只是“写一个爬虫脚本再套上命令行参数”那么简单。它的核心增量在于改变了数据消费方。传统爬虫抓下来的数据要展示给人看,开发者会设计排版、分页、筛选逻辑;而这里的 CLI 是给 AI Agent 调用的,输出必须足够紧凑、足够稳定、足够容易被解析。
可以做个类比。一个普通网站的访问方式,就像走进一家餐厅,你要看菜单、等服务员、理解桌号和特色推荐,整个过程充满视觉和人机交互设计;而面向 agent 的 CLI 方案,就像把这家餐厅的“点菜协议”直接变成一组标准命令,比如 list 看菜品、detail 看做法、order 下单。虽然最终吃到的东西一样,但 agent 不需要理解环境,只需要执行命令、读取返回值。
从架构上看,这个方案的链路并不复杂,核心步骤可以概括为:抓取、解析、暴露、调用。抓取是获得目标 URL 的原始 HTML,这里可以直接用 HTTP 请求,也可以用无头浏览器处理动态页面;解析是把 HTML 中真正有价值的信息抽取出来,通常依赖 BeautifulSoup、XPath 或 Readability 这类工具;暴露是指把解析结果封装成一组有固定语义的命令,比如 list、detail、search;调用则是让 agent 通过 subprocess 或函数调用去执行这些命令,拿到标准的文本或 JSON 输出。
相比把整页 HTML 塞给模型,CLI 方案最直接的收益是 token 数量大幅下降。原因很简单:CLI 输出只包含目标数据字段,不包含任何装饰性标记。同样是看一篇文章列表,HTML 需要把整个 DOM 树带着样式和脚本提供给模型,而 CLI 的 list 命令可能只返回“标题、链接、发布时间、摘要”四个字段。指令本身也很短,agent 不需要理解页面结构,它只需要知道这个 CLI 暴露了哪些命令,然后按命令去取数。
这里有一个容易混淆的地方,就是 CLI 和 MCP 的关系。很多读者看到“让 agent 调用工具”会想到 MCP,两者其实在不同的抽象层。MCP 是模型上下文协议,它定义的是“大模型如何发现并调用外部工具”的协议规范,而 CLI 是一个程序暴露给操作系统的命令入口。网站转 CLI 方案里可以用 MCP 做工具发现,也可以让 CLI 命令被 MCP 服务包装一层;反过来,MCP 服务内部也可能只是执行了一个命令行程序。所以它们不是替代关系,而是可以组合的关系。理解这个区别,对后续搭建 agent 应用非常有帮助。
关于标题里的 142 倍,我需要多说一句。这个数字是项目作者在特定网站上测试得到的结果,可以用来建立直觉,但不应该被当作普适保证。从工程上说,节省 10 倍、50 倍还是 100 倍,取决于两个因素:原始页面的 HTML 冗余度,以及 CLI 输出的抽象程度。如果一个网站页面极其复杂、导航和脚本占了 95%,而 CLI 只暴露一个“新闻标题列表”,那节省 142 倍完全有可能。如果一个页面本身已经接近纯文本,CLI 又要求返回完整正文,那节省空间就有限。理解了这一点,你在评估方案时就不会被单个数字误导。
3. 当前技术背景:CLI 正在成为 AI 编程的新入口
把网站变成 CLI 这个想法,放在两年前可能只是一个普通的效率工具,但放在现在的技术节点上,它踩中了一个更大的趋势:CLI 正在从“开发者手里的高级工具”变成“AI Agent 的标准交互界面”。如果近期关注过 AI 编程工具,应该能注意到 Codex CLI、Claude Code CLI 这类终端型产品的热度。它们和传统 IDE 插件最大的不同是,把整个编程助手做成了可以直接在终端里运行、可以被脚本驱动、输出天然结构化的程序。
这个变化的意义在于,AI Agent 不只是需要理解自然语言,还需要一个能稳定调用外部能力的“操作层”。终端 CLI 恰好具备几个非常适合 agent 调用的特性:它输入简洁,通常一个命令加几个参数就能表达完整意图;它输出可控,标准输出和错误输出分开;它天然可组合,可以在 Shell 脚本里串起来形成更复杂的流程。当 agent 要通过代码和系统交互时,CLI 往往是成本最低、兼容性最好的方式。
网站转 CLI 的方案,正好是这一趋势在信息获取侧的复刻。AI 编程工具已经把“怎么写代码”的入口变成了 CLI,那么“怎么获取网页信息”的入口也有机会变成 CLI。一个 agent 如果既能在终端里操作代码,又能通过一组命令读取外部网站的数据,它就能完成更多端到端的任务,而不是每走一步都要把搜索结果、网页原文一股脑塞进上下文。
对普通开发者来说,这个趋势带来的启示是:如果你正在设计一个面向 AI 的应用,除了 REST API,不妨把 CLI 也当作一等公民来考虑。API 适合服务与服务之间调用,格式严谨但要定义完整的请求和响应约束;CLI 更适合 agent 在终端环境里快速试探和使用。给产品加一个薄薄的 CLI 层,成本不高,却能显著降低 agent 接入的门槛。尤其在内部工具、个人自动化脚本、DevOps 运维这类场景里,CLI 的灵活性和可观测性往往比一个完整的 Web 服务更好。
当然,CLI 也不是万能的。它不适合需要图形操作界面、需要实时视觉反馈、或者参数极其复杂的场景。但用来做“信息查询型”的网页封装,它几乎是最优解。这也是为什么这个 HN 项目能在众多 Agent 基础设施里脱颖而出,它没有发明新协议,也没有引入重型框架,只是把一个一直存在的问题换了一个方式解决,而且解决得足够简洁。
4. 环境准备与前置条件
为了让你真正理解这个方案,我们用一个最小示例跑通“网页转 CLI”的完整流程。示例采用 Python 实现,因为 Python 在 HTML 解析和命令行工具开发两个方向都有非常成熟的生态,也适合快速验证想法。如果你用的是其他语言,思路完全一致,不需要照搬代码。
运行环境方面,建议使用 Python 3.9 或更高版本,操作系统不限,Windows、macOS、Linux 都可以。示例依赖三个库:requests 用来发送 HTTP 请求、beautifulsoup4 用来解析 HTML、click 用来构建命令行接口。这些库都很常见,安装命令如下:
pip install requests beautifulsoup4 click为了不引入外部网站的不确定性,也为了让你能立刻跑通,我不会直接用某个线上页面做演示,而是在本地准备一个模拟的 HTML 文件,然后在同一个项目里编写解析器和 CLI。这样整个实验不依赖网络环境,也不会涉及抓取授权问题,你可以先把流程跑通,再替换成自己真实需要的数据源。
项目目录结构建议如下:
website-cli-demo/ ├── pages/ │ └── tech_news.html ├── site_cli.py └── agent_call_demo.pypages 目录存放要解析的 HTML 文件,site_cli.py 是转出来的 CLI 主入口,agent_call_demo.py 用来演示一个 AI Agent 如何调用这个 CLI 并拿到结构化结果。下面我们会依次创建这些文件。如果你暂时不想用 click 这类第三方库,也可以用 Python 标准库里的 argparse 替代,但对多命令工具来说 click 的可读性好很多,所以我这里选择 click。
5. 完整示例:把一个网页改造成 Agent 可调用的 CLI
5.1 准备示例 HTML 文件
先创建一个模拟的“技术早报”页面,内容刻意保持简单,方便观察解析逻辑。文件路径为 pages/tech_news.html,内容如下:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>技术早报</title> <style>body { font-family: sans-serif; }</style> </head> <body> <article> <h2><a href="/posts/1">AI Agent 入门指南</a></h2> <p class="meta">发布时间:2025-01-10</p> <p class="summary">本文讲解 Agent 的基本概念、工具调用与工作流设计。</p> </article> <article> <h2><a href="/posts/2">CLI 工具设计最佳实践</a></h2> <p class="meta">发布时间:2025-01-11</p> <p class="summary">稳定输出、错误处理和可组合性是 CLI 工具设计的核心。</p> </article> <article> <h2><a href="/posts/3">大模型 Token 计费指南</a></h2> <p class="meta">发布时间:2025-01-12</p> <p class="summary">了解 tokenizer 的工作方式,才能有效控制 AI 应用成本。</p> </article> </body> </html>这个文件里既有标题、链接、正文摘要,也包含样式和少量嵌套结构,足够用来演示 HTML 噪音如何被剥离。实际开发中,你需要把这里的选择器替换成目标网站的真实节点。
5.2 编写 CLI 主程序
接下来创建 site_cli.py,这是整个示例的核心。它读取 HTML 文件,用 BeautifulSoup 提取文章信息,然后通过 click 暴露三个命令:list 列出文章列表、detail 查看单篇文章、后续可以继续扩展 search 搜索。输出支持 text 和 json 两种格式,json 格式是为了让 agent 更容易解析。
# 文件路径:site_cli.py import json from pathlib import Path import click from bs4 import BeautifulSoup PAGE_PATH = Path(__file__).parent / "pages" / "tech_news.html" def load_articles(): if not PAGE_PATH.exists(): raise FileNotFoundError(f"找不到页面文件:{PAGE_PATH}") soup = BeautifulSoup(PAGE_PATH.read_text(encoding="utf-8"), "html.parser") articles = [] for idx, item in enumerate(soup.select("article"), start=1): link = item.select_one("h2 a") meta = item.select_one(".meta") summary = item.select_one(".summary") articles.append({ "id": idx, "title": link.get_text(strip=True) if link else "", "url": link.get("href", "") if link else "", "date": meta.get_text(strip=True).replace("发布时间:", "") if meta else "", "summary": summary.get_text(strip=True) if summary else "", }) return articles def render(articles, fmt): if fmt == "json": return json.dumps(articles, ensure_ascii=False, indent=2) lines = [] for a in articles: lines.append(f"[{a['id']}] {a['title']} | {a['date']}") lines.append(f" {a['summary']}") return "\n".join(lines) @click.group() def cli(): """将网页转换为 AI Agent 可调用的 CLI 接口。""" pass @cli.command("list") @click.option("--limit", default=10, show_default=True, help="最多返回多少条") @click.option("--fmt", type=click.Choice(["text", "json"]), default="text") def list_articles(limit, fmt): """列出文章列表。""" articles = load_articles()[:limit] click.echo(render(articles, fmt)) @cli.command("detail") @click.argument("article_id", type=int) @click.option("--fmt", type=click.Choice(["text", "json"]), default="text") def detail_article(article_id, fmt): """查看单篇文章摘要。""" articles = load_articles() target = next((a for a in articles if a["id"] == article_id), None) if target is None: raise click.ClickException(f"不存在 id={article_id} 的文章") click.echo(render([target], fmt)) if __name__ == "__main__": cli()这份代码有几点值得解释。load_articles 是整个转换流程的“解析层”,它从 HTML 文件中读取原始页面,然后用 soup.select 定位文章节点,再用 get_text 和 get 提取文本与链接。format 函数把文章对象渲染成不同格式,text 适合人看,json 适合程序解析。list 和 detail 是两个命令,参数通过 click 的 option 和 argument 定义,这样 agent 调用时语义非常清楚。
5.3 让 Agent 调用这个 CLI
CLI 本身写好后,Agent 可以用多种方式调用它。最直接的方式就是通过 subprocess 执行命令,然后解析标准输出。下面这个 agent_call_demo.py 演示了如何调用 site_cli.py,并把 JSON 输出解析成 Python 对象:
# 文件路径:agent_call_demo.py import json import subprocess def call_site_cli(*args): result = subprocess.run( ["python", "site_cli.py", *args], capture_output=True, text=True, encoding="utf-8", check=False, ) if result.returncode != 0: raise RuntimeError(f"CLI 调用失败:{result.stderr}") return result.stdout if __name__ == "__main__": output = call_site_cli("list", "--limit", "2", "--fmt", "json") articles = json.loads(output) for item in articles: print(f"标题:{item['title']},链接:{item['url']},摘要:{item['summary']}")在实际的 Agent 应用中,这段 subprocess 调用逻辑通常会被封装成一个 function calling 工具或 MCP 工具,工具的 description 可以写成“调用站点 CLI 获取技术文章信息”,然后让模型在需要时调用。这里的关键是:CLI 已经成为 agent 和网站之间的稳定接口,模型不需要看 HTML 源码,只需要知道命令有哪些、参数是什么、返回值结构是什么。
6. 运行结果与 Token 消耗对比
示例写完以后,可以实际运行验证。先看文本输出:
cd website-cli-demo python site_cli.py list --limit 2预期输出如下:
[1] AI Agent 入门指南 | 2025-01-10 本文讲解 Agent 的基本概念、工具调用与工作流设计。 [2] CLI 工具设计最佳实践 | 2025-01-11 稳定输出、错误处理和可组合性是 CLI 工具设计的核心。再验证 JSON 输出,它更适合 agent 解析:
python site_cli.py detail 3 --fmt json预期输出如下:
[ { "id": 3, "title": "大模型 Token 计费指南", "url": "/posts/3", "date": "2025-01-12", "summary": "了解 tokenizer 的工作方式,才能有效控制 AI 应用成本。" } ]到这里,整个“网页转 CLI”的最小链路已经跑通。现在我们来做一个直观的 token 消耗对比。pages/tech_news.html 文件大小大约 3.6 KB,而 list 命令输出两条记录时,文本模式只有 180 字节左右,JSON 模式也只有大约 300 字节。不同模型 tokenizer 不一样,但按英文语料约 4 字符/token 的粗略标准看,HTML 原文大约对应 900 token,CLI 输出大约对应 50 到 80 token,节省了一个数量级以上。
这个演示页面本身并不复杂,所以还没有达到 142 倍那样的量级。如果你把方案套到一个真实的大型商业网站上,页面 HTML 可能超过 100 KB,而 CLI 只返回标题和摘要,缩小幅度会非常可观。节省 token 的同时还有一个容易被忽略的收益:因为输入变小了,agent 的上下文窗口被释放出来,它可以一次查看更多候选内容,或者在同一轮对话中调用更多工具,整体任务的成功率往往会提高。
要判断这个方案是否值得你用,我建议不要只看别人的测试数字。你可以在自己的目标页面上做一次对比实验:把原始 HTML 保存下来,再把 CLI 输出保存下来,分别统计字符数,再换算成 token 数,看差距是多少。这个差距就是你的真实收益。页面动态渲染程度越高、导航脚本越多、CLI 抽象越精简,收益越大。
7. 常见问题与排查思路
在实际运用“网站转 CLI”的方案时,你会碰到不少具体问题。我把最常见的几类整理成表格,方便遇到问题时快速定位。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 抓取不到正文内容 | 页面由 JavaScript 动态渲染 | 用 curl 查看原始 HTML,对比浏览器渲染结果 | 改用无头浏览器或等待渲染完成后再抓取 |
| 中文内容乱码 | 页面编码识别错误 | 查看响应头中的 charset | 在解析前根据 charset 显式指定编码,示例中使用 encoding="utf-8" |
| 解析不到目标节点 | 页面结构改版或选择器写错 | 在浏览器开发者工具中重新检查 DOM 节点 | 更新 BeautifulSoup 的 select 路径,做好选择器配置化 |
| agent 调用的结果不稳定 | CLI 输出格式不固定 | 对比多次输出,检查是否夹杂日志或错误信息 | 强制输出 JSON,错误信息写入 stderr,不要污染标准输出 |
| 请求频繁被限流 | 短时间内请求过多 | 检查网站返回状态码和响应头 | 增加缓存、设置超时和重试,必要时降低请求频率 |
| 需要登录才能查看内容 | 目标页面在鉴权之后 | 确认是否有公开访问权限 | 为 CLI 增加 Cookie 或 Token 注入能力,但必须先确认授权 |
这里我想展开讲两个最常见的问题。第一个是动态渲染。很多现代网站虽然 URL 看起来是普通路径,但真正内容是通过 JavaScript 异步请求拿到的,直接请求 URL 拿到的 HTML 只是空壳。遇到这种情况,不要急着上无头浏览器,先观察页面是否包含 JSON 数据接口,很多框架会把初始数据放在 script 标签里的NEXT_DATA或 window.INITIAL_STATE中,解析这些字段通常比重启一个 Chromium 实例便宜得多。只有数据缺口无法用静态请求补齐时,才考虑无头浏览器方案。
第二个问题是输出格式污染。很多命令行程序会把调试日志、警告信息混在标准输出里,这对人来说不碍事,但对 agent 来说会直接破坏 JSON 解析。一个可靠的 CLI 工具应该做到:正常数据只输出到 stdout,日志和错误只输出到 stderr;如果命令执行失败,返回非零退出码。agent 调用端也应该养成检查 returncode 的习惯,而不是无脑解析 stdout。
8. 最佳实践与工程建议
把网页转成 CLI 在技术上并不复杂,但要想在真实项目里稳定运行,还要考虑很多工程细节。第一个要强调的就是合规与授权。抓取外部网站前,先确认网站的 robots.txt 是否允许访问目标路径,是否要求附加条款;涉及登录态、个人数据、付费内容的页面,没有明确授权就不要处理,更不要做成公开服务。这个方案真正适合的场景是:你自己有权限的内容、公司内部系统、公开授权接口的包装、或者通过正规 API 获取数据后的再加工。
CLI 命令设计方面,建议遵循“动词 + 对象 + 参数”的命名习惯,list、detail、search、export 这类动词容易让 agent 从 description 里理解用途。每个命令的输出字段要保持稳定,不能今天返回 title,明天改成 heading。为了向前兼容,新增字段比改名更安全。输出格式上,建议同时支持 text 和 json,但让 agent 默认使用 json,因为结构化的键值对能减少模型在文本里搜索信息的开销。
缓存在这个方案里非常重要。网页内容和新闻标题可能有分钟级延迟,但 10 秒内的重复请求完全可以用内存缓存挡住。设计缓存时要给每个页面设置合理的 TTL,并在 CLI 里增加一个 --no-cache 参数用于强制刷新。这样既降低目标网站的请求压力,也降低 token 消耗。可以说,缓存是“少买 token”之外最容易获得收益的优化手段。
如果你打算把多个网站转成同一套 CLI 提供给 agent,我建议把这些 CLI 脚本统一包装成工具描述,暴露为 MCP 工具或 function calling 工具。工具的描述文本要写清楚“这个工具返回什么数据、适合什么场景、可能有什么限制”,比如“返回最近 5 条文章标题和链接,适合用于生成技术早报,不支持搜索历史数据”。描述越清楚,模型在复杂任务里就越不会误调用。这里需要注意的是,CLI 和 MCP 不要重复造轮子:CLI 负责执行和输出,MCP 负责工具发现和约束,两者职责分离。
最后说一下什么时候不适合这个方案。如果你要处理的是需要视觉理解的任务,比如“看页面顶部广告位是哪个客户的”、需要拖动登录滑块、需要实时双向交互的图表页面,CLI 封装就不是最合适的方案。CLI 适合的是信息查询型、结构化程度较高、人类通过点击几下就能获得数据的场景。页面越接近“数据展示页面”,CLI 方案越有价值;页面越接近“交互应用”,越不适合用这种方式强行抽象。
9. 总结与下一步
这篇文章从“AI Agent 读网页太费 token”这个实际问题出发,说了三层意思。第一层,直接给模型读 HTML 是一种高成本、低稳定的方案,问题不在模型而在输入格式;第二层,把网站封装成 CLI,能够让 agent 用极小的 token 代价获取结构化信息,而且这个思路和 Codex CLI、Claude Code CLI 推动的“CLI 作为 AI 原生交互入口”是同一个方向;第三层,这个方案落地成本很低,几十行代码就能做出来,但真正用好它,还需要在授权、缓存、输出稳定性和工具封装上下工夫。
如果你看完想动手实践,建议的下一步是:找一个你自己经常访问、信息结构清晰、允许抓取的网页,用上面示例里的思路写一个最小 CLI,先跑通 list 和 detail 两个命令,再在 Agent 工作流里通过 subprocess 或 MCP 调用它。跑通之后,你有两个可以继续深入的方向:一个是把解析层做扎实,支持更多页面类型和节点配置;另一个是把 CLI 服务化,部署成一个供团队多个 agent 共享的工具接口。
最后再提醒一次,项目标题里的 142 倍 token 节省是作者在特定页面上的测试结果,你可以把它当作一个值得验证的假设,但不要直接拿它去写汇报材料。真正可靠的做法,是在你的目标页面上自己做一次字符量级对比,用数据判断这个方案值不值得投入。如果你也在做 Agent 相关的工作,不妨现在就试试,把一个你经常手工复制给模型的网页,转成一个 50 行的 CLI。