CLI( Command Line Interface)最近特别火,我还特意写了一篇文章分析了一下:AI 时代,为什么说万物皆可 CLI?。在码农眼中,命令行工具在日常开发中出现的频率很高,不管是数据处理脚本、自动化任务,还是代码辅助工具,背后基本都有 CLI 的影子。因此,本篇内容就来详细地讲一讲CLI是怎么写的,以及它的价值。
一、sys.argv
最简单的 CLI,本质上就是“读取命令行参数”。
import sys
print(sys.argv)
运行:
python script.py hello world
输出:
['.\script.py', 'hello', 'world']
可以看到:
sys.argv[0]是脚本名- 后面的元素是输入参数
举一个简单例子:实现一个加法工具
import sys
a = int(sys.argv[1])
b = int(sys.argv[2])
print(a + b)
运行:python add.py 3 5,会输出8。
这种方式适合快速验证,但存在明显问题:
- 参数位置必须严格对应
- 不支持参数说明
- 扩展性较差
二、argparse
argparse 是 Python 标准库 中专门用于构建 命令行工具 的模块,可以让 CLI 更清晰、更易用。
-
基本用法
import argparse
# description 参数用于设置帮助信息描述,当用户运行 -h 或 --help 时会显示
parser = argparse.ArgumentParser(description="简单加法工具")
# type=int 表示该参数会被自动转换为整数类型,help 是该参数的帮助说明,会在帮助信息中显示
parser.add_argument("a", type=int, help="第一个数字")
parser.add_argument("b", type=int, help="第二个数字")
# 调用 parse_args() 方法解析命令行传入的参数
# 返回一个命名空间对象(Namespace),可以通过属性访问各个参数值
args = parser.parse_args()
print(args.a + args.b)
运行:python add.py 3 5,输出:8。若运行:python add.py -h,输出:
usage: script.py [-h] a b
简单加法工具
positional arguments:
a 第一个数字
b 第二个数字
options:
-h, --help show this help message and exit
-
支持可选参数
parser.add_argument(
"--verbose", # 参数名,以 -- 开头表示是可选参数
action="store_true", # 当用户提供了 --verbose,将args.verbose设置为true
help="输出详细信息" # 帮助文档中显示的说明文字
)
if args.verbose:
print(f"{args.a} + {args.b} = {args.a + args.b}")
else:
print(args.a + args.b)
运行:python add.py 3 5 --verbose,则输出为:3 + 5 = 8
三、一个简单日志统计 CLI
假设有一个日志文件 log.txt:
INFO: start process
ERROR: failed to load
INFO: retry
ERROR: timeout
我们希望通过 CLI 统计某种日志级别的数量:
import argparse
def count_logs(file_path, level):
count = 0
with open(file_path, "r") as f:
for line in f:
if line.startswith(level):
count += 1
return count
parser = argparse.ArgumentParser(description="日志统计工具")
parser.add_argument("file", help="日志文件路径")
parser.add_argument("--level", default="ERROR", help="日志级别")
args = parser.parse_args()
result = count_logs(args.file, args.level)
print(f"{args.level} count: {result}")
运行:python log_cli.py log.txt --level ERROR,输出:ERROR count: 2
四、CLI 的真正价值
换个角度想,命令行接口的本质是把一段逻辑暴露成可调用的接口,这个接口不只是给人用的。
-
自动化入口
CLI 工具天然适合接入 cron 定时任务或 CI/CD 流水线。上面这个日志工具,加一行 cron 就变成了每天自动跑的监控任务:
# 每天早上 8 点检查昨天的错误日志,结果写入文件
0 8 * * * python log_filter.py /var/log/app.log --level ERROR >> /tmp/daily_report.txt
脚本不需要改动,只是换了一种触发方式。
-
工具接口
当系统里有多个 CLI 工具时,它们可以通过管道或脚本串联,形成处理流水线:
bash
# 提取错误日志 → 发送到另一个分析脚本
python log_filter.py app.log --level ERROR | python analyze.py --format json
这是 Unix 哲学的体现:每个工具只做一件事,组合起来完成复杂任务。设计 CLI 工具时,养成输出结构化内容(纯文本、JSON)的习惯,工具之间的协作会顺滑很多。
-
AI Agent 的执行外壳
大模型本身不能直接操作文件、调用服务、执行计算,它需要借助外部工具。CLI 工具天然适合充当这个角色。
一个 CLI 工具改造成 Agent 工具的成本很低,核心逻辑不变,只需要加一层包装:
# 把核心逻辑提取成纯函数
def filter_logs(file: str, level: str = "ERROR", tail: int = 0) -> str:
path = Path(file)
if not path.exists():
return f"文件不存在:{file}"
lines = path.read_text(encoding="utf-8").splitlines()
matched = [l for l in lines if level.upper() in l]
if tail:
matched = matched[-tail:]
return "\n".join(matched) if matched else f"未找到 {level} 级别日志"
# 包装成 LangChain Tool(示意)
from langchain.tools import StructuredTool
log_tool = StructuredTool.from_function(
func=filter_logs,
name="log_filter",
description="从日志文件中提取指定级别的日志记录,支持按条数截取"
)
Agent 拿到这个工具后,面对"帮我看看今天的日志有没有报错"这类问题,会自动决定调用它、传入正确的参数、把结果整合进回答里。
关键在于:把核心逻辑写成函数, CLI 是一种调用方式,Agent 是另一种调用方式,二者共用同一套实现。一开始就把逻辑和接口分开写,后面扩展的代价几乎为零。
五、AI 时代的 CLI 设计
知道 CLI 可以对接 Agent 还不够,真正落地时会遇到一个问题:给人用的 CLI 和给 Agent 用的 CLI,设计思路其实不一样。
人读报错信息,理解语境,可以判断下一步怎么做。Agent 读输出,需要的是可解析的结构、明确的状态码、以及没有歧义的返回格式。写给 Agent 调用的工具,有几个地方值得专门设计。
-
输出要机器可读
人眼友好的输出往往对机器不友好。加一个 --json 开关,让工具在需要时输出结构化内容:
import json
def main():
parser = argparse.ArgumentParser()
parser.add_argument("file")
parser.add_argument("--level", default="ERROR")
parser.add_argument("--tail", type=int, default=0)
parser.add_argument("--json", action="store_true", dest="json_output",
help="以 JSON 格式输出结果")
args = parser.parse_args()
path = Path(args.file)
lines = path.read_text(encoding="utf-8").splitlines()
matched = [l for l in lines if args.level.upper() in l]
if args.tail:
matched = matched[-args.tail:]
if args.json_output:
print(json.dumps({
"file": args.file,
"level": args.level,
"count": len(matched),
"records": matched
}, ensure_ascii=False))
else:
for line in matched:
print(line)
Agent 调用时加上 --json,拿到的是可以直接解析的结构,不需要从自然语言里提取信息,出错的概率低很多。
-
用退出码传递执行状态
命令行有一个常被忽略的机制:退出码(exit code) 。sys.exit(0) 表示成功,非 0 表示失败,具体数值可以自定义含义。
Agent 或自动化脚本通过退出码判断工具是否执行成功,决定要不要重试或走降级逻辑:
import sys
def main():
...
path = Path(args.file)
if not path.exists():
# 输出给人看的信息走 stderr,不污染 stdout 的结构化输出
print(f"文件不存在:{args.file}", file=sys.stderr)
sys.exit(1) # 非 0 表示失败
matched = [...]
if not matched:
sys.exit(2) # 2 表示"执行成功但没有结果",有别于报错
# 正常输出
...
sys.exit(0)
在 shell 脚本或 Agent 的工具调用层,可以用 $? 或返回码分支处理不同情况。$?:Linux/macOS 里上一条命令的退出码,-eq 2:等于 2。
# 运行日志过滤脚本 → 检查有没有 ERROR
# 没找到 ERROR → 脚本退出码 = 2 → 显示“日志干净”
# 找到 ERROR → 脚本退出码 ≠ 2 → 不显示任何内容
python log_filter.py app.log --level ERROR
if [ $? -eq 2 ]; then
echo "日志干净,无错误"
fi
-
把描述写清楚,方便模型理解
Agent 决定要不要调用某个工具,依据是工具的描述。描述写得含糊,模型就容易用错或不用。一份好的工具描述应该包含三个要素:做什么、接收什么、返回什么。
log_tool = StructuredTool.from_function(
func=filter_logs,
name="log_filter",
description=(
"从本地日志文件中按级别过滤日志记录。"
"接收文件路径、日志级别(DEBUG/INFO/WARNING/ERROR/CRITICAL)和可选的条数限制。"
"返回匹配的日志行列表,无结果时返回空列表。"
"适用于排查错误、统计异常频次等场景。"
)
)
这三点加在一起,工具对 Agent 来说就是一个职责清晰、行为可预期的模块,而不是一个黑盒。
-
保持幂等性
Agent 执行任务时可能因为网络波动、超时等原因重试同一个工具调用。如果工具有副作用(写文件、发请求、修改数据),重复执行可能造成问题。
设计工具时尽量保证相同输入多次执行结果一致,有副作用的操作加上检查:
def write_report(output_path: str, content: str, overwrite: bool = False) -> str:
path = Path(output_path)
if path.exists() and not overwrite:
return f"文件已存在,跳过写入:{output_path}(传入 overwrite=True 强制覆盖)"
path.write_text(content, encoding="utf-8")
return f"写入成功:{output_path}"
对于读操作,天然幂等,不需要额外处理。对于写操作,加一个 --overwrite 开关比静默覆盖要好,Agent 和人都能感知到这个行为。
六、总结
sys.argv 够用,但只适合参数极少的临时脚本。只要工具需要给别人用,或者参数超过两个,argparse 就该上了。更重要的是对 CLI 工具定位的理解:写好了可以接入定时任务、串联成流水线、也可以直接成为 AI Agent 的能力模块。给 Agent 用的工具,在输出格式、退出码、描述和幂等性上多花一点心思,后续接入时会省很多调试时间。
这条从脚本到 Agent 工具的路径,技术门槛并不高,差别主要在设计意识上——一开始就把逻辑和接口分开,后面想怎么接都方便。