☰
自然语言转 Shell 命令:用 Python 实现一个 AI 命令行助手
2026/9/25 2:19:00 网站建设 项目流程

Watn 这种工具,解决的是一个特别具体的痛点:你已经知道想做什么,但记不住那一条命令的完整写法。给 shell 输入一句自然语言,比如“找出当前目录下最大的三个文件”,它返回一条候选命令,你确认之后再执行。这就是 Watn 的核心形态——type a question in your shell, get a command back。

这类工具出现的前提,是大模型接口已经可以稳定地把自然语言翻译成结构化指令。但真正决定工具能不能在日常工作中用起来的,不是模型多聪明,而是外围工程细节:提示词怎么组织、输出怎么解析、命令怎么确认、报错怎么处理。这些细节才是本文要展开的内容。

文章以 Watn 为引子,先拆解自然语言转命令工具的工作链路,再给出一版适合学习的 Python 简化实现。读者需要有基本的 Python 和 shell 使用经验,并且有一个可用的 OpenAI 兼容 API 接口。学完之后,你可以照着这个思路做出自己的版本,也可以把它接入现有工作流。

1. Watn 的工作链路:自然语言如何变成一条可执行命令

1.1 先给这类工具一个清晰定位

Watn 的定位不是替代 shell,也不是替代人类的命令知识,而是解决“长尾命令”的记忆问题。

日常高频命令,比如cd、ls、grep、git status,没有人会去问模型。真正需要帮助的是那些低频但关键时刻必须准确的操作:查看某个端口被哪个进程占用、批量重命名文件、找出最近一周修改过的文件、查磁盘空间分布。这些命令语法不复杂,问题在于记不住参数,或者记混了 Linux 和 macOS 的差异。

把自然语言转成命令,本质上是“翻译任务”。模型负责翻译,人负责验收。这也是 Watn 与“自动执行一切”的智能体类工具最大的区别:它默认把执行权留在用户手里。

1.2 一次完整转换要经过七个环节

一次完整的“问题到命令”过程,可以拆成七个环节:

  1. 捕获输入:用户在 shell 里输入一句自然语言问题。
  2. 组装提示词:把问题、当前操作系统、当前 shell 类型、输出约束一起发给模型。
  3. 调用接口:请求大模型,让模型生成候选命令。
  4. 解析输出:从模型返回的文本里提取出真正的命令,去掉解释文字和 Markdown 代码块。
  5. 展示命令:把候选命令显示给用户。
  6. 用户确认:用户检查命令内容,决定是否执行。
  7. 执行或跳过:确认后交给当前 shell 执行,否则直接跳过。

前两步决定生成质量,第四步决定能不能直接使用,第五六步决定安全性。很多类似工具只实现了前三步,把第四步到第六步做成“直接执行”,这在实际使用中风险很大。

1.3 为什么确认环节不能省

模型生成的命令,可能有三种错误:

  • 语法对,但语义错。比如把-type f写成-type d,结果从“找文件”变成“找目录”。
  • 命令对,但系统不对。比如在 Linux 上生成了 macOS 的lsof写法,或者反过来。
  • 占位符没有替换。命令里的文件名、路径、IP 还是<>方括号内容,直接执行必然失败。

这些问题都不能靠模型自身解决,只有人能在执行前发现。所以 Watn 类工具必须坚持“先展示、再确认、后执行”。哪怕用户每次都直接按回车确认,这个环节的存在本身也在提醒用户:命令是候选结果,不是最终结论。

2. 动手前先定接口:环境变量、模型参数和安全边界

2.1 运行环境与依赖清单

为了不引入过多依赖,简化实现使用 Python 标准库中的urllib调用 HTTP 接口,不安装requests和任何 SDK。这样在任何一台有 Python 的机器上都能直接跑。

项目要求说明
操作系统Linux / macOS / WSL需要支持subprocess和shell=True执行
Python3.9 及以上使用urllib.request、json、argparse标准库
模型接口OpenAI 兼容的/chat/completions接口可以是官方接口,也可以是兼容网关或本地服务
API Key一个可用密钥通过环境变量传入,不写进代码
网络能访问接口地址如果使用本地模型,则不需要外部网络

这里的关键是“OpenAI 兼容接口”这个约束。只要你使用的模型服务支持POST /chat/completions这个路由,代码就只需要改base_url,不用改逻辑。

2.2 用环境变量和配置文件管理参数

工具的参数不能写死在代码里。最直接的方案是环境变量优先、配置文件其次、代码里的默认值兜底。

支持的配置项如下:

环境变量配置文件字段默认值含义
WATN_API_KEYapi_key无接口密钥,也兼容OPENAI_API_KEY
WATN_MODELmodelgpt-4o-mini使用的模型名称
WATN_BASE_URLbase_urlhttps://api.openai.com/v1接口地址前缀
WATN_TIMEOUTtimeout30请求超时秒数
WATN_SHELLshell当前$SHELL执行命令时使用的 shell 路径

配置文件放在~/.config/watn/config.json,内容示例:

{ "api_key": "sk-xxxx", "model": "gpt-4o-mini", "base_url": "https://api.openai.com/v1", "timeout": 30, "shell": "/bin/bash" }

配置优先级要明确:环境变量大于配置文件,配置文件大于默认值。这样可以做到“不同项目临时换 key,不修改任何文件”。

2.3 生成参数里的三个关键决定

调用模型时,有几个参数直接影响输出结果,要单独说明。

第一个是temperature。命令生成需要确定性,所以固定为0。如果调高,模型会生成更“有创意”的写法,但也会带来无意义的变量名、多余管道和语法差异。命令生成不是写作任务,不需要随机性。

第二个是max_tokens。一条 shell 命令通常很短,512足够。如果模型的上下文窗口支持,这个值也可以放大,但没必要。限制输出长度也能避免模型生成长篇解释,虽然不能完全阻止。

第三个是提示词里的“系统信息注入”。在请求模型之前,把当前操作系统类型和 shell 名称直接拼接进提示词,而不是让模型猜测。这是因为“查看端口占用”这条指令,在 Linux 上可能是ss -ltnp,在 macOS 上更可能是lsof -i。模型不知道你的系统,就只能瞎猜。

3. 用 Python 实现一个最小可用的 watn 命令

3.1 项目结构与入口

简化实现只需要一个 Python 文件和一个示例配置。目录结构如下:

~/tools/watn/ ├── watn.py ├── config.example.json └── README.md

入口文件使用argparse解析参数。命令格式设计为:

python3 watn.py "自然语言问题" python3 watn.py --dry-run "自然语言问题" python3 watn.py --json "自然语言问题"

--dry-run只生成并展示命令,不执行。--json输出模型的原始响应,用于调试解析逻辑。

3.2 提示词组装和模型调用

先定义配置读取和环境变量解析逻辑:

#!/usr/bin/env python3 """watn: 在 shell 里输入自然语言问题,得到一条可执行的 shell 命令。 简化示例,用于演示思路。实际使用前请根据自己的 API 地址、模型和系统环境调整。 """ import argparse import json import os import platform import re import subprocess import sys import urllib.error import urllib.request CONFIG_PATH = os.path.expanduser("~/.config/watn/config.json") DEFAULT_TIMEOUT = 30 def load_config(): cfg = {} if os.path.exists(CONFIG_PATH): try: with open(CONFIG_PATH, "r", encoding="utf-8") as f: cfg = json.load(f) except (json.JSONDecodeError, OSError) as exc: print(f"[watn] 读取配置文件失败: {exc}", file=sys.stderr) return cfg def resolve_values(cfg): api_key = ( os.environ.get("WATN_API_KEY") or os.environ.get("OPENAI_API_KEY") or cfg.get("api_key", "") ) model = os.environ.get("WATN_MODEL") or cfg.get("model", "gpt-4o-mini") base_url = os.environ.get("WATN_BASE_URL") or cfg.get( "base_url", "https://api.openai.com/v1" ) timeout = int(os.environ.get("WATN_TIMEOUT") or cfg.get("timeout", DEFAULT_TIMEOUT)) shell_name = ( os.environ.get("WATN_SHELL") or cfg.get("shell") or os.path.basename(os.environ.get("SHELL", "/bin/bash")) ) return api_key, model, base_url, timeout, shell_name

接下来是提示词组装。提示词是这套工具的核心,它决定了模型会不会乖乖只输出命令:

def build_prompt(question, shell_name, system_info): return ( "你是一个 shell 命令转换助手。用户会输入一个自然语言问题。\n" "你的任务:只输出一条可以直接粘贴到 shell 里执行的命令。\n" "要求:\n" "1. 不要输出任何解释、说明、Markdown 代码块或多余符号。\n" "2. 只输出一条命令,使用单行格式。\n" "3. 如果问题包含多个步骤,优先用 && 或 ; 连接成一条命令。\n" "4. 命令中涉及文件名、路径、IP 等不确定信息时,使用 <文件名> 这样的占位符,不要编造具体值。\n" f"5. 当前操作系统: {system_info}\n" f"6. 当前 shell: {shell_name}\n" f"用户问题: {question}" )

调用接口时使用urllib.request,把 payload 构造成标准 chat 结构:

def call_llm(base_url, api_key, model, prompt, timeout): url = base_url.rstrip("/") + "/chat/completions" payload = { "model": model, "messages": [ {"role": "system", "content": "你是 shell 命令生成助手,只输出命令本身。"}, {"role": "user", "content": prompt}, ], "temperature": 0, "max_tokens": 512, } req = urllib.request.Request( url, data=json.dumps(payload).encode("utf-8"), headers={ "Content-Type": "application/json", "Authorization": f"Bearer {api_key}", }, method="POST", ) with urllib.request.urlopen(req, timeout=timeout) as resp: data = json.loads(resp.read().decode("utf-8")) content = data["choices"][0]["message"]["content"].strip() return content

这段代码有两个关键点。第一,base_url是接口前缀,真正请求的路径是base_url + "/chat/completions"。第二,temperature=0表示确定性的贪心生成,命令场景下这是正确选择。

3.3 命令提取、危险提示与确认执行

模型输出不可完全信任。即使提示词要求“只输出命令”,实际返回也可能带 Markdown 代码块、前导说明、或者把命令拆成多行。所以解析函数要做防御式处理:

def extract_command(text): text = re.sub(r"^```[a-zA-Z]*\s*", "", text.strip()) text = text.replace("```", "") lines = [line.strip() for line in text.splitlines() if line.strip()] return lines[0] if lines else ""

这里先把开头的```bash或```sh去掉,再把剩余的三反引号删除,最后取第一行非空内容。这样即使模型不守规矩,也能尽最大可能捞回命令。

执行前需要展示命令,并对敏感操作给出警告。危险命令不能完全阻止,但至少要提醒:

DANGER_PATTERNS = [ r"rm\s+-rf", r"mkfs", r"dd\s+if=", r"curl\b[^|]*\|\s*(ba)?sh", r"wget\b[^|]*\|\s*(ba)?sh", r">\s*/dev/sd", r"chmod\s+-R\s+777", r":\(\)\s*\{", ] def has_danger(cmd): return any(re.search(pattern, cmd) for pattern in DANGER_PATTERNS) def print_command(cmd, shell_name): print(f"\n[watn] 候选命令({shell_name}):") print(f" {cmd}") if has_danger(cmd): print(" [警告] 该命令包含删除、格式化、下载并执行等敏感操作,请反复确认。") def confirm_and_run(cmd, shell_name, dry_run): print_command(cmd, shell_name) if dry_run: print("[watn] 当前是 dry-run 模式,不会执行命令。") return 0 answer = input("确认执行?输入 y 执行,其他任意键跳过:").strip().lower() if answer != "y": print("[watn] 已跳过,未执行。") return 0 proc = subprocess.run(cmd, shell=True, executable=shell_name) return proc.returncode

这里需要注意subprocess.run的executable参数。它会指定由哪个 shell 来解释这条命令,避免系统默认 shell 和用户日常使用的 shell 不一致。生产环境中,executable必须传绝对路径,不能传bash这种短名字。

最后是main函数,把所有环节串起来:

def main(): parser = argparse.ArgumentParser(description="watn: 自然语言转 shell 命令") parser.add_argument("question", help="自然语言问题") parser.add_argument("--dry-run", action="store_true", help="只生成并展示命令,不执行") parser.add_argument("--shell", help="指定目标 shell,例如 /bin/zsh") parser.add_argument("--json", action="store_true", help="输出模型原始响应(调试用)") args = parser.parse_args() cfg = load_config() api_key, model, base_url, timeout, shell_name = resolve_values(cfg) if args.shell: shell_name = args.shell if not api_key: print("[watn] 缺少 API Key。请设置环境变量 WATN_API_KEY 或 OPENAI_API_KEY。", file=sys.stderr) sys.exit(2) system_info = f"{platform.system()} {platform.release()}" prompt = build_prompt(args.question, shell_name, system_info) try: raw = call_llm(base_url, api_key, model, prompt, timeout) except urllib.error.HTTPError as exc: body = exc.read().decode("utf-8", "ignore") print(f"[watn] 接口返回 HTTP {exc.code}: {body}", file=sys.stderr) sys.exit(1) except urllib.error.URLError as exc: print(f"[watn] 网络错误: {exc.reason}", file=sys.stderr) sys.exit(1) except Exception as exc: print(f"[watn] 调用失败: {exc}", file=sys.stderr) sys.exit(1) if args.json: print(raw) return 0 cmd = extract_command(raw) if not cmd: print("[watn] 未能从模型输出中提取命令,请检查提示词或换一个问法。", file=sys.stderr) sys.exit(1) return confirm_and_run(cmd, shell_name, args.dry_run) if __name__ == "__main__": sys.exit(main())

3.4 接入 bash 和 zsh

直接运行python3 ~/tools/watn/watn.py "问题"太长,应该在 shell 里加一个函数:

# 添加到 ~/.bashrc 或 ~/.zshrc watn() { python3 "$HOME/tools/watn/watn.py" "$@" }

加入后重载配置:

source ~/.bashrc

之后就可以用watn "问题"的方式调用。注意函数名和 Python 脚本名相同,所以函数内部必须写python3加绝对路径,否则会形成死循环。

4. 从问题到命令的完整验证

4.1 准备 API Key 并做一次 dry-run

先给脚本加执行权限,并确认 Python 可以正常运行:

chmod +x ~/tools/watn/watn.py python3 --version

设置 API Key:

export WATN_API_KEY="sk-你的密钥"

推荐第一次调用使用--dry-run,只观察生成结果,不执行任何东西:

python3 ~/tools/watn/watn.py --dry-run "列出当前目录下大于 100M 的文件"

正常输出类似:

[watn] 候选命令(bash): find . -type f -size +100M [watn] 当前是 dry-run 模式,不会执行命令。

如果这一步失败,问题大概率集中在 API Key、网络、base_url地址这三处,先不要往下走。

4.2 三个典型问题验证生成结果

用三个覆盖不同场景的问题,验证工具是否真的可用。

场景一:查找占用端口号的进程。

python3 ~/tools/watn/watn.py --dry-run "查看 8080 端口被哪个进程占用"

在 Linux 上预期的合理输出是ss -ltnp | grep 8080或lsof -i :8080。在 macOS 上更可能是lsof -i :8080。这取决于提示词里注入的系统信息是否准确。

场景二:批量重命名文件。

python3 ~/tools/watn/watn.py --dry-run "把当前目录下所有 jpg 文件改名为 png 后缀"

这个命令有风险,正确做法是模型输出占位符或用rename命令,而不是直接写死文件名。如果模型返回了带真实文件名的命令,要警惕这是编造的。

场景三:Git 操作。

python3 ~/tools/watn/watn.py --dry-run "撤销最近一次提交但保留改动"

合理输出是git reset --soft HEAD~1。如果模型给出git reset --hard HEAD~1,那是完全不同的语义,执行前必须注意到。

4.3 验证清单:除了“能跑”,还要看这几点

工具能启动只是第一步。建议按下面的清单逐项验证:

  • 命令内容是否和问题语义一致,尤其是-type、-rf、--hard这类关键参数。
  • 命令是否匹配当前系统,而不是另一个操作系统。
  • 文件名、路径、IP 是否使用了占位符,有没有编造具体值。
  • 确认环节是否生效,输入非 y 字符会不会真的跳过。
  • --json是否能输出原始模型响应,便于后续调试。
  • 危险命令是否触发了警告提示。

5. 常见问题排查:从现象定位到原因

5.1 模型返回解释文字,不是命令

现象:终端输出的不是一条命令,而是一段带 Markdown 代码块的解释文字,或者多行说明。

原因:模型没有遵守“只输出命令”的约束;也可能是提示词没有强调单行输出。

检查方式:加--json查看原始响应。如果响应里包含 ``` 包裹的代码块,说明问题出在解析前;如果响应本身就是解释文字,说明问题出在提示词。

解决:先让extract_command做防御式解析,把反引号和代码块语言标记剥离。同时增强提示词,增加一条 one-shot 示例,比如“用户问题:查看磁盘空间;输出:df -h”。防御式解析是最后一道兜底,提示词才是根本解法。

5.2 命令在自己的系统上执行报错

现象:命令生成成功,确认后执行,但 shell 报command not found或参数错误。

原因:模型生成的命令基于通用模板,没有考虑当前系统的 shell 和已安装工具。比如系统里没有lsof,模型却生成了lsof -i。

检查方式:单独运行这条候选命令,看报错信息;用which lsof确认工具是否存在;查看提示词里注入的system_info是否正确。

解决:在提示词里注入更精确的信息,例如操作系统发行版、包管理器类型、是否已有lsof。也可以通过--shell参数显式指定目标 shell。

5.3 接口超时、401 和网络错误

现象:调用接口时报HTTP 401、URLError或timeout。

原因:API Key 错误或过期、网络不通、base_url拼错、超时时间设置过短。

检查方式:先用curl单独测试接口连通性,不要直接在 Python 里排查。

curl -s -o /dev/null -w "%{http_code}" -X POST \ -H "Authorization: Bearer $WATN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}' \ https://api.openai.com/v1/chat/completions

返回200说明接口和 Key 都没问题,问题在 Python 侧。返回401说明 Key 有问题。如果curl一直卡住,检查WATN_TIMEOUT和网络代理。

5.4 中文引号、编码和占位符问题

现象:命令里出现中文全角引号,或者<>占位符没有被替换,shell 执行后报错。

原因:模型有时会把说明性文字里的中文符号带进命令;占位符是设计行为,但用户没意识到需要替换。

检查方式:用--json查看原始响应,确认符号是否来自模型。

解决:提示词里明确要求“使用英文单双引号”。占位符问题上,要么在提示词里要求模型对不确定信息使用占位符,要么在确认环节打印提示,让用户先替换占位符再执行。

5.5 API Key 泄露风险

现象:代码仓库的 Git 历史里出现了sk-开头的密钥。

原因:API Key 写死在代码或配置文件中,并提交到了仓库。

检查方式:在仓库里搜索sk-前缀;查看.gitignore是否包含.env和配置文件。

解决:立即轮换 Key,然后从仓库删除敏感文件。推荐做法是只使用环境变量,配置文件里不存放真实 Key,只放非敏感的模型和地址信息。如果确实需要本地配置文件,必须加入.gitignore。

5.6 问题速查表

问题现象常见原因检查方式处理建议
输出是解释文字提示词约束不足加--json看原始响应增强提示词,加 one-shot 示例
命令执行报 command not found系统信息不准确which检查工具是否存在注入精确系统信息和包管理器
HTTP 401API Key 错误curl单独测接口轮换 Key,检查环境变量
请求超时网络慢或超时太短调大WATN_TIMEOUT测试检查网络代理,调整超时
中文全角引号模型生成内容混入--json查看原始响应提示词要求英文符号
占位符未替换设计如此检查命令中<>内容确认前手动替换占位符
危险命令未提示匹配规则不完整检查DANGER_PATTERNS按需补充敏感模式

6. 最佳实践与扩展方向

6.1 安全执行清单

命令生成工具是“方便”和“危险”并存的产品形态,上线前至少过一遍下面的清单:

  • 默认使用--dry-run,执行必须显式确认。
  • 不使用 root 用户运行 watn。如果命令需要 sudo,应该由用户在执行时自行输入,而不是把sudo提前编进命令。
  • 危险命令模式要单独匹配并提示。注意不能只匹配字符串,rm -rf和rm -rf /的语义完全不同。
  • API Key 只从环境变量读取,代码和配置文件里不出现真实密钥。
  • 调用接口必须设置超时,避免网络问题导致终端长时间卡住。
  • 模型返回的命令只能作为候选,最终语义判断必须由人完成。

6.2 提升生成质量的几条经验

第一,few-shot 示例比单纯强调“只输出命令”更有效。在提示词里放两个输入输出对,模型会更稳定地遵循格式。

第二,系统信息越具体越好。platform.system()只能区分到 Linux、Darwin、Windows。实际项目中可以读取/etc/os-release,把发行版、包管理器也注入进去。这样“安装 nginx”这类问题,模型才能生成apt install nginx而不是yum install nginx。

第三,对模型输出做“命令候选而不是直接执行”的姿态。工具可以内置黑名单模式,但对匹配到的危险命令不阻止,只警告。完全阻止会让用户绕过工具自己执行,反而更危险。

第四,超时重试要有上限。一次调用失败后重试一次可以,但不要无限重试,否则接口持续异常时工具会一直挂起。

6.3 从单条命令到自动化工作流

当前实现只回答“一条命令怎么做”。再往前一步,可以做三个扩展。

扩展一:多轮上下文。用户可以接着追问“上一条命令会把文件覆盖吗”,工具把上一轮的命令作为上下文传给模型,而不是每次从零开始。

扩展二:命令保存。把确认执行的命令追加到本地历史文件,带问题和时间戳。这相当于你自己的“命令记忆库”,下次同样的问题可以直接取历史,不调用模型。

扩展三:集成本地模型。base_url已经支持替换,只要本地模型服务暴露 OpenAI 兼容接口,就可以把WATN_BASE_URL指向http://localhost:11434/v1这类地址。这样命令生成不依赖外部网络,隐私性更好,但命令质量取决于本地模型的指令遵循能力。

6.4 给新手的练习路径

如果想把这类工具吃透,建议按这个顺序练习:

  1. 先手工构造提示词,在 API 调试工具里测试不同模型对“只输出命令”这个约束的遵循程度。
  2. 再实现最小命令行工具,先只做--dry-run,不做执行功能。
  3. 确认生成稳定后,再加入确认执行环节,并补上危险命令提示。
  4. 最后才考虑配置文件、多模型切换、历史记录这些外围能力。

不要把第一步和第四步颠倒。很多人一上来就写全套工具,结果模型输出解析不稳,确认和执行的边界又模糊,最后变成“能跑但没人敢用”的半成品。

Watn 这类工具最核心的技术判断只有一个:模型负责生成,人负责决策。只要这个边界清晰,它就是一个高效率的辅助工具;边界一旦模糊,它就是一把随时可能误伤自己的利器。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询