大模型 API 按时间段自动切换(零 token 定时方案)
需求:DeepSeek 有空闲时段半价,GLM 有白天的活动价——想让 AI 助手"晚上自动用 DeepSeek、白天自动用 GLM",且切换过程不消耗任何模型调用。本文记录一套脚本 + 定时任务的零成本方案,含完整的模拟测试方法。
一、方案总览
两个供应商(智谱 glm-5.3-flash 和 deepseek deepseek-v4-flash)的密钥都在配置里。方案 = 1 个零 token 切换脚本 + 2 个 no_agent 定时任务:
定时任务用 no_agent 模式(纯脚本执行,不启动 LLM,零成本),切换脚本本身只是改配置文件。
二、脚本的关键实现细节
1. 直接编辑 YAML,不要用 yaml 库重写
用 PyYAML dump() 重写整个配置文件会丢注释、丢格式、甚至改变键顺序。正确做法是按行定位顶层 model: 块:
lines = text.splitlines(keepends=True)
# 找到行首 "model:" 的行,块终点 = 下一个行首非缩进非注释键
for j in range(start + 1, len(lines)):
stripped = lines[j].strip()
if stripped and not stripped.startswith("#") and not lines[j][:1].isspace():
end = j
break
new_text = "".join(lines[:start]) + new_block + "\n" + "".join(lines[end:])
不要用正则 (?ms)^model:\n(?:[ \t]+.*\n)+? ——DOTALL/贪婪模式下 .* 会吞掉整个文件,实测不可靠。
2. 密钥不硬编码
模型名、base_url、api_key 全部从配置的 providers 段动态读取,脚本里不含任何密钥,泄漏面为零。
3. 安全措施三件套
# 改前备份(只备份一次,首次的原始配置永远保留)
if not BACKUP.exists():
shutil.copy2(CONFIG, BACKUP)
# 写后校验
try:
yaml.safe_load(new_text)
except yaml.YAMLError:
print("校验失败,放弃写入") # 绝不写入坏配置
return 1
# 幂等:已是目标模型则静默跳过(cron 重复触发无害)
4. 日期门控
脚本内置生效日判断(如 START_DATE = "2026-09-10"),生效日前运行直接空转退出——提前部署,到点自动生效。
5. 切换即生效
每次模型请求都会重新读配置,不需要重启 gateway。注意:切的是全局默认模型,若某个定时任务单独指定了 model/provider 则以任务自己的为准。
三、定时任务(no_agent,零成本)
任务A「模型切换-夜间DeepSeek」:0 19 * * * script=model_time_switch.py no_agent=true
任务B「模型切换-日间GLM」: 0 8 * * * script=model_time_switch.py no_agent=true
日志写入专用文件,每次切换留痕可审计。
四、不碰真实配置的模拟测试法
上线前必须实测("先验证再给结论")。技巧:把模块的 CONFIG/LOG 指向临时目录 + 注入假时间:
import importlib.util, shutil, yaml, datetime as real_dt
spec = importlib.util.spec_from_file_location("mts", "model_time_switch.py")
mts = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mts)
tmp = Path("/tmp/mts_test"); tmp.mkdir()
mts.CONFIG = tmp/"config.yaml"; mts.BACKUP = tmp/"config.yaml.bak"; mts.LOG = tmp/"switch.log"
shutil.copy("/real/path/config.yaml", mts.CONFIG) # 真实配置拷贝到临时区
class FakeDateTime(real_dt.datetime):
now_inst = None
@classmethod
def now(cls, tz=None): return cls.now_inst
mts.datetime = FakeDateTime # 时间注入
def run(h, label):
FakeDateTime.now_inst = FakeDateTime(2026, 9, 10, h, 0)
mts.main()
c = yaml.safe_load(mts.CONFIG.read_text())["model"]
print(f"{label}: {c['provider']}/{c['default']}")
实测通过的用例:
最后一条很重要:把临时配置全文与原配置比对(model 块除外),确认切换脚本对文件其余部分零损伤。
五、踩坑记录
GLM-5.3 系列的 reasoning_effort 只接受 low/high/max(强制思考模型,medium 等值直接 400 code 1210)。多模型混用时在配置里用 per-model override:
reasoning_overrides: {glm-5.3-flash: high},其他模型不受影响。不要用
hermes config set写嵌套 key——会把 YAML 结构写坏,嵌套配置直接编辑文件。正则改 YAML 不可靠:贪婪匹配吞全文,按行解析是唯一稳妥路线。
定时任务用 no_agent 模式跑纯脚本,模型调用次数为 0,切换本身不产生任何 API 成本。
切换粒度提醒:按时段切价只在"任务对模型价格不敏感/可预测用量"时有意义;如果任务有单独指定的模型,时段切换对它无效,需要在任务层面单独规划。