返回博客

成本优化指南

用 LLM API 将自然语言转换为 shell 命令:JSON 与成本

一个 Python 脚本,向廉价模型请求一条 shell 命令作为 JSON,拒绝或标记不确定的请求,从不运行任何东西,并计算 1,000 次请求的价格。

作者:CheapTokens Team发布于 5 分钟阅读

一个用简单英语表达的请求被转换为一个 JSON 对象中的一条 shell 命令,类型为 command、refuse 和 unsure
本页内容
  1. 一个范围很窄的任务:为什么回答很短
  2. 回复格式:command、refuse 或 unsure
  3. 脚本
  4. 1,000 次请求的成本
  5. 在你自己的请求上检查

让模型返回一个包含三个字段(type、command、explanation)的 JSON 对象,将回答限制在几百个 token 以内,并打印命令而不是执行它。按 token 计算,这样的请求主要是提示词加上简短、可预测的回答:在下面的假设下,1,000 次这样的请求在 DeepSeek-V4-Flash 上花费 $0.012。

假设你要给一个内部 CLI 加一个“描述你想要什么”的输入框。有人输入 find files bigger than 100 MB in my home folder,你希望返回一行他们能阅读、检查并粘贴的内容。随之而来有两个问题:如何防止模型猜测或做出危险操作,以及一百个人全天使用时的成本是多少?

一个范围很窄的任务:为什么回答很短#

2026 年 11 月 9 日,一个 Telegram 频道转述了 dale-0.8b 的模型卡,这是一个专门做这一件事的开源模型。根据 Hugging Face 上的模型卡(2026 年 11 月 10 日查看),dale-0.8b 是在 Apache-2.0 许可下对 Qwen3.5-0.8B 的完整微调,可将西班牙语或英语请求转换为一条 Linux bash 命令。作者 Manuel García 报告称,在他自己的测试集上达到 79.4%(471 个案例中的 374 个),而 Qwen3.5-9B 基础版和未经微调的 Gemma 4 E4B-it 为 62.8%。这些是自报数据,来自一个人的测试集,因此应将其视为一个数据点,而非排名。

对我们有用的是任务的形式,而不是模型本身。模型卡描述了一个固定的系统提示词、一行上下文(操作系统、发行版、包管理器、工具)加上请求、关闭思考,以及将回复限制在 200 个 token。这个上限是成本的两个杠杆之一:当回答是一个简短的 JSON 对象时,输出侧简短且可预测,因为上限限制了它。另一个杠杆是提示词。两侧各占账单的多少份额,下表显示;它因模型而异。

你不需要一个 0.8B 的模型来做这件事。同样的格式适用于任何 API 模型,本文其余部分就是构建这个。

一次请求的路径:提示词输入,JSON 输出,由人决定
脚本中的请求流程:在你亲自执行之前,什么都不会运行。

回复格式:command、refuse 或 unsure#

诀窍在于模型始终有一种合法的方式说“不”。回复是一个对象:

json
{"type": "command | refuse | unsure", "command": "...", "explanation": "..."}
  • command:一条 bash 命令,加上一句说明它做什么的解释。
  • refuse:请求具有破坏性或与 shell 无关。没有命令。
  • unsure:请求太模糊(“清理我的磁盘”)。解释中包含一个澄清问题。

没有 unsure,模糊的请求会迫使模型猜测,而猜出来的 rm 正是你会记住的那个。模型卡的格式有同样的三种类型;我们复用这个想法,而不是模型。

配对

将每种回复类型与脚本应执行的操作匹配

  • command显示命令和解释;由人自己复制并运行
  • refuse打印解释,不提供任何可运行的内容
  • unsure打印问题,让用户重新表述

脚本#

一个文件,openai 包,密钥放在环境变量中。基础 URL 和完整模型 id 来自 CheapTokens 文档;不接受短 id,所以使用 deepseek-ai/DeepSeek-V4-Flash-0731。

python
import json
import os
import sys

from openai import OpenAI

client = OpenAI(
    base_url='https://api.cheaptokens.dev/v1',
    api_key=os.environ['CHEAPTOKENS_API_KEY'],
)
MODEL = 'deepseek-ai/DeepSeek-V4-Flash-0731'

SYSTEM = '''You turn a request into ONE Linux bash command.
Reply with a single JSON object and nothing else:
{"type": "command" | "refuse" | "unsure", "command": "<one command, or empty>", "explanation": "<one short sentence>"}
Use "refuse" when the request is destructive or not about the shell.
Use "unsure" when it is too vague, and put a clarifying question in "explanation".'''

CONTEXT = 'os: Ubuntu 24.04; package manager: apt; tools: git, curl, jq'


def parse(text):
    start = text.find('{')
    if start == -1:
        return None
    try:
        obj, _ = json.JSONDecoder().raw_decode(text[start:])
    except json.JSONDecodeError:
        return None
    if not isinstance(obj, dict) or obj.get('type') not in ('command', 'refuse', 'unsure'):
        return None
    return obj


def ask(request):
    resp = client.chat.completions.create(
        model=MODEL,
        max_tokens=200,
        temperature=0,
        messages=[
            {'role': 'system', 'content': SYSTEM},
            {'role': 'user', 'content': f'Context: {CONTEXT}\nRequest: {request}'},
        ],
    )
    return parse(resp.choices[0].message.content), resp.usage


if __name__ == '__main__':
    result, usage = ask(' '.join(sys.argv[1:]))
    if result is None:
        print('Could not read the reply as JSON. Try again.')
    elif result['type'] == 'command':
        print(result['explanation'])
        print('\n    ' + result['command'] + '\n')
        print('Read it, then copy it yourself. This script never runs it.')
    else:
        print(f"[{result['type']}] {result['explanation']}")
    print(f'tokens: {usage.prompt_tokens} in, {usage.completion_tokens} out')

以 python shellhelp.py find files bigger than 100 MB in my home folder 运行它。这里有三个选择很重要:

  • 任何地方都没有 subprocess。 命令只是屏幕上的文本。运行它是你的决定,在你阅读之后。
  • max_tokens=200,与模型卡的上限相同。 一个 JSON 对象很容易容纳;冗长的回答会在 200 个 token 处被截断,而不是以你的费用继续下去。上限适用于整个回复,所以如果模型写出它的推理过程,那会消耗限额。如果回复以 finish_reason 等于 length 结束,稍微提高上限。更多内容见用 max_tokens 削减输出 token。
  • 格式在提示词中请求,然后防御性地解析。 raw_decode 会跳过多余的代码围栏或 { 之前的一句话。如果想更进一步,见从 OpenAI 兼容 API 获得有效 JSON 以及回复上的 json.JSONDecodeError 的修复方法。

在运行任何东西之前,检查步骤顺序是否清晰:

排序

将脚本的步骤按正确顺序排列

  1. 构建提示词:固定的系统文本、一行上下文、请求
  2. 用较小的 max_tokens 调用 API
  3. 解析 JSON 并检查 type 是三种之一
  4. 显示命令并等待人运行它

1,000 次请求的成本#

脚本的最后一行打印 usage。那是真实的数字,你应该读取你自己的。在得到它之前,先做一个假设:提示词大约 400 个输入 token(脚本中的裸提示词粗略计数约为 150 个 token;其余是你可能添加的三到四个示例请求-回复对的空间),JSON 回复约为 60 个输出 token。

对于 1,000 次请求,即 400 × 1,000 = 400,000 个输入 token 和 60 × 1,000 = 60,000 个输出 token。下表对目录中的每个模型精确计算了这些,旁边是 OpenRouter 列;数据来自实时价格,因此保持最新。

模型 每次请求 1,000 次请求 OpenRouter,1,000 次请求
GLM-5.3-Flash $0.00001 $0.0104 $0.0184
DeepSeek-V4-Flash $0.000012 $0.012 $0.0364
MiniMax-M2.7 $0.000029 $0.029 $0.348

每次请求 400 个输入 token 和 60 个输出 token。每 100 万 token 的价格为当前价格,会随我们的资费一同变动;OpenRouter 列显示相同工作负载的参考价格。

总的来说,将输入和输出加在一起,对于这个工作负载,DeepSeek-V4-Flash 比 OpenRouter 更便宜。按 token 计算,请求几乎全是输入,回复很短,但两侧都计费,所以提示词和回答都要精简。每个模型的价格在定价页面上,模型本身在 DeepSeek 页面上描述:380K 上下文,最多 32,768 个输出 token,任何时段一个价格。

快速感受比例的方法:

快速检查

max_tokens=200 限制请求的哪一部分?

显示答案

答案: 回复

max_tokens 只限制回复;提示词按原样计费,所以单独精简它。

想要每月 100,000 次请求?乘以 100:40,000,000 个输入和 6,000,000 个输出 token。以同样的方式将这两个数字放入 ::cost 并读取该行。

在你自己的请求上检查#

模型卡的 79.4% 并不能说明你的提示词、你的操作系统或用户的措辞。写 20 个真实请求以及你会接受的命令并进行比较,完全如在小评估集上测试提示词中所述。包括一些脚本应拒绝或标记为不确定的请求,例如“删除所有内容”或“让我的电脑更快”。

当出现问题时,症状通常指向修复方法:

  • Could not read the reply as JSON:打印一次原始文本。像“Sure!”这样的前缀会被处理;被上限截断的回复则不会,所以检查 finish_reason。
  • 401 或 404:密钥、基础 URL 或模型 id 错误。列表在 API 401 或 404:密钥、基础 URL、模型 id 中。
  • 命令适用于不同的系统:上下文行是原因。保持准确(操作系统、包管理器、工具);它花费几十个 token,并避免在 Fedora 机器上出现错误的 apt。

同样的任务,不同的模型?将 MODEL 替换为文档中的 GLM 或 MiniMax id,保留提示词,并将脚本打印的 token 计数与上表进行比较。