code/06-production/cost_latency.py

83 行 · 3.8 KB
"""几种省钱、提速的办法,各做一个小实验,量一量效果。

在 AI-Course/code/06-production 目录下运行:python cost_latency.py
一共约 40 次调用,按 2026 年 9 月的价格花费约 0.05 美元。
"""
import hashlib
import os
import time
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

from openai import OpenAI

client = OpenAI(api_key=os.environ["LLM_API_KEY"], base_url=os.environ.get("LLM_BASE_URL", "https://api.deepseek.com"))
MODEL = "deepseek-flash"
PRICES = (0.006, 0.30, 1.20)  # 缓存命中输入、未命中输入、输出,美元 / 百万词元,高峰价,截至 2026 年 9 月
NO_THINKING = {"thinking": {"type": "disabled"}}
DOCS = "\n\n".join(p.read_text() for p in sorted(Path(__file__, "../../../data/httpx-docs").resolve().glob("*.md")))


def cost(u):
    hit = getattr(u, "prompt_cache_hit_tokens", 0) or 0
    return (hit * PRICES[0] + (u.prompt_tokens - hit) * PRICES[1] + u.completion_tokens * PRICES[2]) / 1e6


def ask(messages, **kw):
    start = time.time()
    r = client.chat.completions.create(model=MODEL, messages=messages, extra_body=kw.pop("extra_body", NO_THINKING), **kw)
    return r.choices[0].message.content, r.usage, time.time() - start


print("== 1. 缓存:固定的资料放在前面,还是问题放在前面")
questions = ["httpx 怎么设置超时?", "httpx 怎么开启 HTTP/2?", "httpx 怎么上传文件?"]
for label, build in [
    ("问题在前", lambda q: [{"role": "user", "content": f"问题:{q}\n\n参考文档:\n{DOCS}\n\n一句话回答。"}]),
    ("文档在前", lambda q: [{"role": "system", "content": f"参考文档:\n{DOCS}"}, {"role": "user", "content": q + "一句话回答。"}]),
]:
    total, hits = 0.0, []
    for q in questions:
        _, u, _ = ask(build(q))
        total += cost(u)
        hits.append(f"{u.prompt_cache_hit_tokens}/{u.prompt_tokens}")
    print(f"  {label}:三次的缓存命中 {hits},共 {total:.5f} 美元")

print("\n== 2. 输出长度:不做要求 vs 要求简短")
q = "httpx 和 requests 有什么区别?"
for label, prompt in [("不做要求", q), ("要求简短", q + "用三句话以内回答。")]:
    text, u, sec = ask([{"role": "user", "content": prompt}])
    print(f"  {label}:输出 {u.completion_tokens} 词元,{sec:.1f} 秒,{cost(u):.5f} 美元")

print("\n== 3. 简单问题开不开思考")
q = [{"role": "user", "content": "httpx 的 get 方法怎么传查询参数?一句话回答。"}]
for label, extra in [("不思考", NO_THINKING), ("思考", {"thinking": {"type": "enabled"}})]:
    text, u, sec = ask(q, extra_body=extra)
    print(f"  {label}:输出 {u.completion_tokens} 词元,{sec:.1f} 秒,{cost(u):.5f} 美元 | {text[:40]!r}")

print("\n== 4. 自己做结果缓存:同样的问题直接返回上次的答案")
cache = {}


def cached_ask(question):
    key = hashlib.sha256(" ".join(question.lower().split()).encode()).hexdigest()  # 忽略大小写和多余空格
    if key in cache:
        return cache[key], 0.0
    text, u, _ = ask([{"role": "user", "content": question}])
    cache[key] = text
    return text, cost(u)


spent = sum(cached_ask(q)[1] for q in ["httpx 怎么设置代理?", "httpx 怎么设置代理?", "HTTPX  怎么设置代理?", "httpx 怎么设置代理"])
print(f"  问了 4 次(写法略有不同),实际调用 {len(cache)} 次模型,共 {spent:.5f} 美元")

print("\n== 5. 串行 vs 并发")
batch = [f"用一句话解释 HTTP 状态码 {c}。" for c in [200, 301, 302, 400, 401, 403, 404, 429, 500, 503]]
start = time.time()
for b in batch:
    ask([{"role": "user", "content": b}])
serial = time.time() - start
start = time.time()
with ThreadPoolExecutor(5) as pool:
    list(pool.map(lambda b: ask([{"role": "user", "content": b}]), batch))
print(f"  10 个请求:一个一个来 {serial:.1f} 秒,5 个并发 {time.time() - start:.1f} 秒")