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 一次完整转换要经过七个环节一次完整的“问题到命令”过程可以拆成七个环节捕获输入用户在 shell 里输入一句自然语言问题。组装提示词把问题、当前操作系统、当前 shell 类型、输出约束一起发给模型。调用接口请求大模型让模型生成候选命令。解析输出从模型返回的文本里提取出真正的命令去掉解释文字和 Markdown 代码块。展示命令把候选命令显示给用户。用户确认用户检查命令内容决定是否执行。执行或跳过确认后交给当前 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和shellTrue执行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_KEYWATN_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, encodingutf-8) as f: cfg json.load(f) except (json.JSONDecodeError, OSError) as exc: print(f[watn] 读取配置文件失败: {exc}, filesys.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 f5. 当前操作系统: {system_info}\n f6. 当前 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, datajson.dumps(payload).encode(utf-8), headers{ Content-Type: application/json, Authorization: fBearer {api_key}, }, methodPOST, ) with urllib.request.urlopen(req, timeouttimeout) 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。第二temperature0表示确定性的贪心生成命令场景下这是正确选择。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 [ rrm\s-rf, rmkfs, rdd\sif, rcurl\b[^|]*\|\s*(ba)?sh, rwget\b[^|]*\|\s*(ba)?sh, r\s*/dev/sd, rchmod\s-R\s777, 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, shellTrue, executableshell_name) return proc.returncode这里需要注意subprocess.run的executable参数。它会指定由哪个 shell 来解释这条命令避免系统默认 shell 和用户日常使用的 shell 不一致。生产环境中executable必须传绝对路径不能传bash这种短名字。最后是main函数把所有环节串起来def main(): parser argparse.ArgumentParser(descriptionwatn: 自然语言转 shell 命令) parser.add_argument(question, help自然语言问题) parser.add_argument(--dry-run, actionstore_true, help只生成并展示命令不执行) parser.add_argument(--shell, help指定目标 shell例如 /bin/zsh) parser.add_argument(--json, actionstore_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。, filesys.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}, filesys.stderr) sys.exit(1) except urllib.error.URLError as exc: print(f[watn] 网络错误: {exc.reason}, filesys.stderr) sys.exit(1) except Exception as exc: print(f[watn] 调用失败: {exc}, filesys.stderr) sys.exit(1) if args.json: print(raw) return 0 cmd extract_command(raw) if not cmd: print([watn] 未能从模型输出中提取命令请检查提示词或换一个问法。, filesys.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 Keyexport WATN_API_KEYsk-你的密钥推荐第一次调用使用--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 给新手的练习路径如果想把这类工具吃透建议按这个顺序练习先手工构造提示词在 API 调试工具里测试不同模型对“只输出命令”这个约束的遵循程度。再实现最小命令行工具先只做--dry-run不做执行功能。确认生成稳定后再加入确认执行环节并补上危险命令提示。最后才考虑配置文件、多模型切换、历史记录这些外围能力。不要把第一步和第四步颠倒。很多人一上来就写全套工具结果模型输出解析不稳确认和执行的边界又模糊最后变成“能跑但没人敢用”的半成品。Watn 这类工具最核心的技术判断只有一个模型负责生成人负责决策。只要这个边界清晰它就是一个高效率的辅助工具边界一旦模糊它就是一把随时可能误伤自己的利器。