Files
vrsub/tests/test_llm_default_model.py
T
cat-shark 1007612734 feat: 翻译节点默认模型切换为 Qwen3.5-35B-A3B 并按节点分离兜底
评测结论(同片 2 小时日语 ASR,8 个模型全量对比):
Qwen/Qwen3.5-35B-A3B 与旧默认 Qwen3.6-35B-A3B 质量持平,
速度 0.232 s/行(全评测最快,旧模型档位)。

改动:
- nodes/llm.py 兜底默认值改为 Qwen/Qwen3.5-35B-A3B;
- nodes/subtitle_correction.py **有意保留** Qwen/Qwen3.6-35B-A3B:
  同一误听泛化场景各跑 4 次,旧模型 4/4 正确推断性器官语义,
  新模型 0/4(输出"阴道/曼果/曼戈"字面直译)——该节点不能跟随全局默认;
- tests/test_llm_default_model.py 锁定该分叉不被"顺手统一":
  翻译兜底必须与 .env 一致,纠错兜底必须保留旧模型,
  三处都必须读 LLM_MODEL 环境变量(保留单点覆盖能力)。

模型仍是工作流 DAG 的数据(params.model),切换不需改代码。
2026-09-13 10:15:31 +08:00

156 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""LLM 默认模型解析契约测试(切换默认模型只改数据/环境,不改代码)。
背景
----
2026-09 评测结论(data/experiments/translate_models/REPORT.md)决定把翻译/过滤/
纠错三类节点的**默认模型**从 `Qwen/Qwen3.6-35B-A3B` 换成 `Qwen/Qwen3.5-35B-A3B`
(质量持平、速度 0.232 s/行,是基线档最快)。
仓库约定"切换模型不改代码":三个节点(llm-translate / llm-filter /
subtitle-correction)的模型解析顺序都是
`params["model"]` → 环境变量 `LLM_MODEL` → 兜底默认值。
因此换默认模型 = 改 `.env` 的 `LLM_MODEL`(+ 文档),代码只在兜底默认值上同步。
本测试锁定两件事,防止"改了 .env 但节点仍走旧模型"或"params 覆盖失效"
1. 环境变量 `LLM_MODEL` 能真正决定请求体里的 model(在 HTTP 边界观测真实请求);
2. `params["model"]` 优先级高于环境变量(工作流参数覆盖仍有效)。
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from nodes import llm
from wov_sdk.models import InvokeRequest
def _capture_model(monkeypatch, run_id: str = "r") -> list[dict]:
"""在 HTTP 边界记录真实请求体,并返回正常结构响应(模型名取请求体的)。"""
sent: list[dict] = []
class Response:
"""最小可用的 OpenAI 兼容响应;内容由请求体推导,便于断言对齐。"""
def __init__(self, body: dict):
items = json.loads(body["messages"][1]["content"])
self._payload = json.dumps(
{
"choices": [
{
"message": {
"content": json.dumps(
[{"id": i["id"], "text": "译文"} for i in items],
ensure_ascii=False,
)
}
}
],
"usage": {"total_tokens": 1},
}
).encode()
def __enter__(self):
return self
def __exit__(self, *args):
return False
def read(self):
return self._payload
def open_request(request, **kwargs):
body = json.loads(request.data)
sent.append(body)
return Response(body)
monkeypatch.setattr("urllib.request.urlopen", open_request)
return sent
def test_env_model_决定请求体里的模型(monkeypatch, tmp_path: Path) -> None:
"""环境变量 LLM_MODEL 生效:旧模型默认值不会被写死进请求。
这是"改 .env 就换默认模型"的硬契约——若节点把默认模型写死在代码里,
本用例会因请求体仍是旧模型而失败。
"""
monkeypatch.setenv("LLM_MODEL", "新默认/模型")
source = tmp_path / "input.srt"
source.write_text("1\n00:00:01,000 --> 00:00:02,000\nこんにちは\n", encoding="utf-8")
sent = _capture_model(monkeypatch)
response = llm.invoke(
InvokeRequest(
run_id="r", node_instance_id="",
inputs={"srt_uri": str(source)}, output_dir=str(tmp_path / "out"),
)
)
assert response.status == "completed", response.error
assert sent[0]["model"] == "新默认/模型"
def test_params_model_优先于环境变量(monkeypatch, tmp_path: Path) -> None:
"""工作流 DAG 的 params.model 覆盖环境变量(切换模型是数据,不是环境依赖)。"""
monkeypatch.setenv("LLM_MODEL", "环境/模型")
source = tmp_path / "input.srt"
source.write_text("1\n00:00:01,000 --> 00:00:02,000\nこんにちは\n", encoding="utf-8")
sent = _capture_model(monkeypatch)
response = llm.invoke(
InvokeRequest(
run_id="r", node_instance_id="",
inputs={"srt_uri": str(source)}, params={"model": "工作流/模型"},
output_dir=str(tmp_path / "out"),
)
)
assert response.status == "completed", response.error
assert sent[0]["model"] == "工作流/模型"
def test_兜底默认模型按节点职责分离() -> None:
"""翻译节点跟随 .env 的 LLM_MODEL;纠错节点保留旧默认(实测更优)。
2026-09 实测(同一误听泛化场景,各跑 4 次):
- `Qwen/Qwen3.6-35B-A3B`4/4 正确推断(“小穴”);
- `Qwen/Qwen3.5-35B-A3B`0/4(输出“阴道/曼果/曼戈”,字面直译)。
而翻译节点上新模型与旧模型逐条一致(1160 vs 1160,时间戳完全对齐)。
因此**不能三处同源**:翻译跟全局(.env),纠错独立保留旧模型;
过滤节点默认不调 LLM(use_llm=0),其兜底仅作启用时的默认值。
本用例锁定“分叉是有意为之”:翻译兜底必须与 .env 一致;纠错兜底必须
保留旧模型;三处都必须读 LLM_MODEL 环境变量(保留单点覆盖能力)。
"""
import inspect
from nodes import llm_filter, subtitle_correction
env_model = None
env_path = Path(__file__).resolve().parent.parent / ".env"
if env_path.is_file():
for line in env_path.read_text(encoding="utf-8").splitlines():
if line.strip().startswith("LLM_MODEL=") and not line.strip().startswith("#"):
env_model = line.split("=", 1)[1].strip()
# 模型解析所在函数:翻译在 translate_lines、过滤在 _judge_category、
# 纠错在 correct_entry。
sources = {
"llm-translate": inspect.getsource(llm.translate_lines),
"llm-filter": inspect.getsource(llm_filter._judge_category),
"subtitle-correction": inspect.getsource(subtitle_correction.correct_entry),
}
fallbacks = {}
for name, src in sources.items():
marker = 'os.getenv("LLM_MODEL", "'
assert marker in src, f"{name} 未按约定读取 LLM_MODEL 环境变量"
fallbacks[name] = src.split(marker, 1)[1].split('"', 1)[0]
# 翻译节点跟随全局配置。
if env_model:
assert fallbacks["llm-translate"] == env_model, (
f"翻译兜底 {fallbacks['llm-translate']} 与 .env 的 LLM_MODEL={env_model} 不一致"
)
assert fallbacks["llm-filter"] == env_model, "过滤节点兜底应与全局保持一致"
# 纠错节点**有意**保留旧模型(实测新模型 0/4 vs 旧 4/4)。
assert fallbacks["subtitle-correction"] == "Qwen/Qwen3.6-35B-A3B", (
"纠错节点应保留旧默认(新模型在该节点泛化实测 0/4,明显劣化)"
)