QMT - Qwen-MT CLI
命令行翻译工具,默认使用百炼上的通用大模型 qwen3.8-max,同时支持通义多模态翻译 (AnyTrans) 与 DashScope Qwen-MT 系列。通用大模型没有服务端翻译协议,术语干预、翻译记忆、领域提示改由提示词在客户端重建,详见 通用大模型后端;要走 AnyTrans 的服务端术语干预与 BatchTranslate 摊销,显式加 -m anytrans。提供单句翻译、交互式 REPL、CSV/Excel 批量翻译、半价批量推理,以及术语表、翻译记忆、领域提示、向量语义匹配等专业翻译辅助功能。
安装
需要 Python >= 3.10。
pip install qwen-mt-cli
或从源码安装:
git clone https://github.com/leoleils/qwen-mt-cli.git
cd qwen-mt-cli
pip install -e .
安装后即可使用 qmt 命令。
配置
DashScope(默认模型所需 / 向量语义匹配 / qwen-mt-*)
设置 DashScope API Key(申请地址):
export DASHSCOPE_API_KEY="sk-xxxxxxxx"
默认的
qwen3.8-max、其余通用大模型后端(-m qwen3.8-flash、-m llm:<名>,含--batch-api)、qwen-mt-*模型,以及向量语义匹配,走的都是同一个百炼账号与同一把 Key。只有-m anytrans不用它。
端点默认打北京百炼。 要换区域(新加坡 / 美国 / 香港)、换业务空间专属的 MaaS 网关、或换任意 OpenAI 兼容的第三方网关,用端点档案:它把 base URL 与该区域的 API Key 绑成一对存起来,一条命令切换。QMT_BASE_URL 仍然可用,是 CI 与临时覆盖的逃生口。
AnyTrans(-m anytrans 时需要)
设置阿里云 AccessKey 和业务空间 ID:
export ALIBABA_CLOUD_ACCESS_KEY_ID="your-ak-id"
export ALIBABA_CLOUD_ACCESS_KEY_SECRET="your-ak-secret"
export ANYTRANS_WORKSPACE_ID="llm-xxxxxxxx"
或通过 qmt config set 持久化保存:
qmt config set --access-key-id <AK> --access-key-secret <SK> --workspace-id <WID>
项目默认值(语向 / 场景 / 术语检索模式)
四个键可持久化到 .qmt/config,省去每条命令重复指定:
qmt config set --source-lang Chinese --target-lang Korean --scene mt-turbo --rag-mode cloud
qmt config show # 逐项标注每个值的来源 (CLI / env / 项目 / 全局 / 默认)
qmt config clear --rag
优先级:命令行 > 环境变量(QMT_SOURCE_LANG / QMT_TARGET_LANG / QMT_SCENE / QMT_RAG_MODE)> 项目 .qmt/config > 全局 ~/.qmt/config,逐键覆盖。显式 -t English 永远赢过持久化的 target_lang。
副作用:这四个键对所有后端生效,不只 AnyTrans。持久化
target_lang = Korean之后,qmt -m qwen-mt-plus "hello"也会翻成韩语。
rag_mode 的完整说明见 术语 RAG 模式。
端点档案(多区域切换)
百炼的 API Key 是分区域的:一把 Key 只在签发它的那个区域生效,配上别的区域的 base URL 直接 401(官方说明)。所以端点档案把 base_url 与该端点的 api_key 绑成一对存取,切换时一起换——只换 URL 不换 Key 正是这个 401 的成因,而它的报错完全看不出因果。
# 建档案:--base-url 收完整 URL,也收下面四个预置区域名
qmt endpoint add sg --base-url singapore --api-key sk-xxxxxxxx
qmt endpoint add gw --base-url https://my-gateway.example.com/v1 --api-key sk-yyyyyyyy
qmt endpoint use sg # 设为生效(写进 .qmt/config,之后每条命令都用它)
qmt endpoint list # 档案 / 层级 / 生效项 / 本次运行实际会用的 URL 与 Key 来源
qmt endpoint remove sg
qmt --endpoint sg "你好" -t English # 只覆盖这一次运行,不改配置
--global 在 add / use / remove 上都可用,写到 ~/.qmt/config。档案名限 1-32 个 A-Za-z0-9_- 字符(不能带点,点是配置分层的分隔符),URL 必须以 http:// / https:// 开头。预置区域名只在 --base-url 处展开,--endpoint 只认已存档案名或完整 URL。
| 预置名 | base URL |
|---|---|
beijing(默认) |
https://dashscope.aliyuncs.com/compatible-mode/v1 |
singapore |
https://dashscope-intl.aliyuncs.com/compatible-mode/v1 |
us |
https://dashscope-us.aliyuncs.com/compatible-mode/v1 |
hongkong |
https://cn-hongkong.dashscope.aliyuncs.com/compatible-mode/v1 |
业务空间专属的 MaaS 端点(形如 https://ws-xxxx.<region>.maas.aliyuncs.com/compatible-mode/v1)没有预置——它需要控制台才知道的 WorkspaceId,照上面的 gw 例子原样填即可。
跟随档案的流量:通用大模型后端(含 --batch-api 的文件上传、提交、轮询、下载)、qwen-mt-*、以及本地 RAG 的 qwen3.7-text-embedding-flash。-m anytrans 不跟随:它用账号级 AK/SK 签名,endpoint/region 钉在 cn-beijing。
优先级:--endpoint > QMT_BASE_URL > 项目 .qmt/config 的生效档案 > 全局 ~/.qmt/config 的生效档案 > 默认(北京)。
两条与上面项目默认值不同的分层规则,都是为了防止半覆盖拼出一个必然 401 的组合:
- 档案整体覆盖,不逐键合并。 项目级的同名档案整条遮蔽全局级的。逐键合并会把项目的 URL 拼到全局的 Key 上,而这个错配发生在配置文件内部,没有任何一层能看见它。
QMT_BASE_URL整体胜出,连带丢弃档案的 Key。 它是 CI 与临时覆盖的逃生口,行为与引入档案之前一致;但 shell 里 export 的北京 URL 配上档案里存的新加坡 Key 同样必然 401,所以这里打一条醒目警告并点名被覆盖的档案,Key 回落到--api-key/DASHSCOPE_API_KEY/ 配置里的api_key。刻意不提供QMT_ENDPOINT(档案名)环境变量:它带不了 Key,等于把刚消掉的半覆盖请回来。
档案没存 Key 时走上面那条回落链并提示一次——档案可以只存 URL。
翻译缓存不按端点隔离。 同一个模型换个区域还是同一个模型,cache.db 共用,跨区域重跑照样命中。
向量索引会记下建库时的端点。 换区域后本地检索仍然可用(索引本身不依赖端点),qmt vectordb status 会指出不一致并建议 rebuild,翻译时只警告一次、不失败。嵌入模型变更则相反,是硬失败。注意 qwen3.7-text-embedding-flash 是分区域上架的,新加坡端点没有这个模型——切到那边嵌入调用会直接 404,需要先把模型换回该区域可用的。
401 / InvalidApiKey 的报错会多一行诊断,点名当前端点、档案名与 Key 的来源。qmt config show 的 API Endpoint 段是同一份信息的常驻版本,换端点后先跑它确认。REPL 里用 /endpoint,见交互式模式。
快速开始
# 翻译文本(默认 qwen3.8-max,通用大模型后端)
qmt "你好世界" -t English
# 切换到 AnyTrans(服务端术语干预、专家角色、BatchTranslate 摊销)
qmt "你好世界" -t English -m anytrans
# 指定专家角色(仅 -m anytrans 生效,不带 -m anytrans 会被静默忽略)
qmt "你好世界" -t English -m anytrans --agent game
# 切换到 DashScope 翻译模型
qmt "Hello World" -t Chinese -m qwen-mt-plus
# 换一个通用大模型
qmt "Hello World" -t Chinese -m qwen3.8-max
# 百炼上的任意模型
qmt "Hello World" -t Chinese -m llm:qwen-plus
# 翻译文件
qmt -f article.txt -t English
# 管道输入
echo "你好" | qmt -t English
# 流式输出(默认模型即支持;qwen-mt-flash/lite 与所有通用大模型也支持)
qmt "你好世界" -t English --stream
# 交互式模式
qmt -i -t English
# 列出所有模型及其能力矩阵
qmt models
可用模型
qmt models 打印的就是这张表,且直接读注册表,不会与代码漂移——以它为准。
| 模型 | 后端 | 特点 | 流式 | 批量推理 | 语言数 |
|---|---|---|---|---|---|
anytrans |
AnyTrans | 通义多模态翻译,服务端术语干预/专家角色/BatchTranslate | - | - | 92 |
qwen-mt-plus |
DashScope 翻译 | 最高翻译质量,适用于专业领域 | - | - | 92 |
qwen-mt-flash |
DashScope 翻译 | 通用首选,效果/速度/成本平衡 | 支持 | - | 92 |
qwen-mt-lite |
DashScope 翻译 | 最快响应速度,适用于实时场景 | 支持 | - | 31 |
qwen3.8-max |
通用大模型 | 通义千问 3.8 旗舰 (默认) | 支持 | 支持 | - |
qwen3.7-plus |
通用大模型 | 通义千问 3.7 Plus,质量/成本平衡 | 支持 | 支持 | - |
qwen3.8-flash |
通用大模型 | 通义千问 3.8 Flash,响应最快 | 支持 | - | - |
deepseek-v4-pro-0813 |
通用大模型 | DeepSeek V4 Pro (第三方,百炼托管) | 支持 | - | - |
kimi-k3 |
通用大模型 | Kimi K3 (第三方,百炼托管) | 支持 | - | - |
通用大模型的语言覆盖由模型本身决定,不是翻译协议规定的清单,所以不列数字。
百炼上的其他模型用 -m llm:<模型名>(如 -m llm:qwen-plus)。注册表里没有的名字,能力位按最保守方向推断:假定支持流式(判错最坏是端点返一个明确错误),假定支持批量推理(判错最坏是任务在 validating 阶段被拒,此时不计费)。
批量推理 为 - 的模型带 --batch-api 时会提示一次并回退到实时并发路径——不是报错,因为百炼本来就会在 validating 阶段拒绝它们;但回退后按实时全价计费,这一点会明确说出口。
llm: 只接受模型名,不接受保留名:-m llm:anytrans 与 -m llm:qwen-mt-plus 会在发请求前被拒绝,因为这些模型的翻译能力来自服务端私有协议,提示词替代不了。
选型实测
2026-09,中→韩,1000 行 MMO 修仙文案,1135 条术语,同一份领域提示词,四个全新无缓存目录。裁判 qwen3.7-plus——不是四个选手中的任何一个,否则自我偏好会污染结果;抽 200 行,每行跑两次不同的标签洗牌(四份译文随机分到甲乙丙丁)消除位置偏见。
| 模型 | 术语 | 语义 | 语域 | 格式 | 综合 | 平均名次 |
|---|---|---|---|---|---|---|
qwen3.8-max |
4.88 | 4.84 | 4.72 | 4.98 | 4.85 | 1.90 |
llm:deepseek-v4.1-flash |
4.76 | 4.72 | 4.61 | 4.89 | 4.75 | 2.22 |
qwen3.8-flash |
4.77 | 4.79 | 4.12 | 4.96 | 4.66 | 2.48 |
qwen-mt-plus |
4.62 | 4.75 | 2.99 | 4.91 | 4.32 | 3.40 |
两两胜出:max 胜 mt 85.9%、胜 flash 66.3%、胜 deepseek 58.0%;deepseek 胜 flash 58.0%。
语域是唯一拉开差距的维度——但这只是中→韩的结论。 mt 2.99 对 max 4.72,其余三维四个模型全在 0.3 分以内。历次对比(max vs flash、max vs deepseek)都是同一个结论。纯规则指标独立印证了这件事:领域提示词要求 UI 文案简洁无敬语,而 mt 有 62.5% 的行用了 합니다 体(max 11.4%、deepseek 14.1%、flash 38.0%),长度比中位数 2.17 对其余的 1.50–1.67——mt 把 UI 短语写成了完整敬语句。
别把这条搬去别的语向。 同一个 mt、同一份领域提示词,换到中→日就基本合规了,于是语域不再是区分维度,max 改成四个维度全部第一地赢——见日语复核。「选后端先看敬语率」是中→韩这一轮的经验,不是通用启发式:敬语率衡量的是模型对你这个语向的语域要求的服从度,换语向必须重测。
术语的字面命中率不能单独拿来排名。 mt 走服务端受约束解码,字面命中却是四个里最低的:
| 模型 | 字面命中 | 裁判术语分 | 残留中文 |
|---|---|---|---|
qwen3.8-max |
95.3% | 4.88 | 0 |
qwen3.8-flash |
94.4% | 4.77 | 0 |
llm:deepseek-v4.1-flash |
91.5% | 4.76 | 2 行 |
qwen-mt-plus |
91.4% | 4.62 | 3 行 |
字面命中算的是"译词是否作为子串出现",它把两类完全不同的东西混在一起。mt 漏的 33 个术语里 14 个 max 也漏——那是四个模型共同的坑,开启→오픈、装备→장비、高级→고급 这类词性与词边界误判(装备 作动词该译 장착,最高级 里含 高级)。mt 独有的 44 次里,23 次是同一个条目 已达上限→상한 달성:mt 译成完整句 상한에 도달했습니다,max 译成名词短语 상한 달성,两个语义都对,但只有后者字面命中,而且后者才符合领域提示词的"UI 简洁"。真错也有——该槽位未激活 被 mt 译成"未被解除的槽位"。mt 那 3 行残留中文是把汉字原样留在了韩文里(귀蟾왕、도화槽)。
批量吞吐不是模型速度。 单条延迟 mt 反而最快:
| 模型 | 单条延迟 | 批量有效 | 倍率 |
|---|---|---|---|
qwen-mt-plus |
1.50s | 2.57s/行 | 0.6× |
qwen3.8-max |
1.88s | 0.26s/行 | 7.2× |
qwen3.8-flash |
2.03s | 0.50s/行 | 4.1× |
llm:deepseek-v4.1-flash |
9.29s | 1.11s/行 | 8.4× |
四个都是并发 5、批 10。其余三个拿到了 4–8 倍并行加速,mt 只有 0.6 倍——比串行还慢。本轮日志里没有可见的限流记录,所以不能坐实原因;机制上的嫌疑是全局退避:一个车道吃到 429 会暂停所有车道。结论是别按 行/分 给模型排速度,那个数字混进了退避等待。deepseek 的 9.29s 是模型自身的(思考型,还出现过一次 28.69s),不适合实时场景。
llm:deepseek-v4.1-flash 必须配 --no-thinking,否则 1.9% 的行会直接失败——见「思考模式」。
日语复核
2026-09,中→日,500 行,7265 条术语,同一份领域提示词(多了一节段位名冲突说明),四个全新无缓存目录。裁判、抽样、标签洗牌口径与中→韩那轮一致。
两轮不能直接比数字:语料零重合,术语表 7265 条对 1135 条,行数 500 对 1000。能跨轮比的只有结论的结构。
| 模型 | 术语 | 语义 | 语域 | 格式 | 综合 | 平均名次 |
|---|---|---|---|---|---|---|
qwen3.8-max |
4.89 | 4.82 | 4.81 | 4.89 | 4.85 | 2.19 |
llm:deepseek-v4.1-flash |
4.81 | 4.79 | 4.72 | 4.80 | 4.78 | 2.40 |
qwen3.8-flash |
4.83 | 4.66 | 4.81 | 4.73 | 4.76 | 2.42 |
qwen-mt-plus |
4.64 | 4.64 | 4.29 | 4.74 | 4.58 | 3.00 |
两两胜出:max 胜 mt 69.6%、胜 flash 56.0%、胜 deepseek 55.8%;flash 对 deepseek 50.8%,是平局。耗时 mt 31m22s、flash 6m26s、deepseek 4m07s、max 2m04s——大模型反而最快这个反直觉结果与中→韩一致,同一个警告适用。
max 仍然第一,但赢法完全不同。 中→韩是一维拉爆(语域差 1.73,其余三维 ≤0.3),中→日是四个维度全部第一的广谱领先,语域只差 0.52。
mt 的敬语率在日语里是假信号。 它 56 行用了 です・ます体(11.2%),按原文长度拆开后是:411 行短 UI 文案里 2 行(0.5%),89 行长句里 54 行(60.7%)。领域提示词只约束「UI 与系统提示文案」,长句用敬体在日语里是对的。统计敬语率必须按文案类型分层,否则会把合规的模型判成最差。同一件事的另一面:flash 在中→韩的短板(语域 4.12,합니다 体 38%)在日语里不存在(UI 敬语 0/411),于是它和 deepseek 打平了。
mt 在日语输在结构性的地方,改提示词治不了。 段位名 秩序白银V 该译 秩序シルバーV(前缀条目 + 金属条目 + 罗马数字),max/flash/deepseek 15/15 全对,mt 只有 7/15。不是没收到提示词——domains 字段确实带上了整份领域提示词(840 字符,没触 3000 上限)——而是结构化 terminologies 走服务端受限解码,硬约束压过自由文本指令。另有 15 行把 ASCII V 改写成罗马数字码位 Ⅴ,21 行把中文人名转写成片假名(浦立群 → プーリー群),而仙侠题材的日文惯例是直接用汉字。
日语特有的保真缺陷是简体字形漏转,不是残留中文。 日语目标语本身就写汉字,中→韩那轮的「残留中文」指标在这里失效(carries_source_script 刻意排除日语是同一个理由);「译文与原文逐字相同」也不能当未翻译——500 行里有 37 行四个模型全部原样输出,全是 夏侯惇、九天玄女、敖丙 这类专名,原样是正确行为。真正能规则化检查的是字形:乐山白 该写 楽山白、洪泰华 该写 洪泰華。flash 一家 10 行,其余三家各 1 行;裁判给 flash 的语义分 4.66 是三个通用模型里最低的,与此吻合。
字面命中率这轮会惩罚遵守提示词的行为。 术语表里 黄金→金 与 荣耀黄金→ゴールド 并存,max 正确遵守「长条目优先」写出 栄光ゴールドV,反被记一次未命中。
AnyTrans 专家角色
使用 --agent 指定翻译专家(仅 -m anytrans 生效):
game medical legal tech finance news academy novel ebook music design web3 github twitter reddit ao3 chess ecom vocab wordalign summarize simplify mixlang dongbei bai2wen wen2bai sublearn
AnyTrans 模型场景
使用 --scene 选择模型档位(默认 mt-turbo):
mt-plus— 专业版,更高质量mt-turbo— 轻量版,更快响应
通用大模型后端
AnyTrans 与 Qwen-MT 的翻译能力来自服务端私有协议:Qwen-MT 读 translation_options,AnyTrans 读 ext.terminologies / ext.examples / ext.domain_hint。百炼上的通用大模型不认识这些字段,传了会被忽略。
这个后端把同样的三种干预在客户端重建为提示词,其中术语一分为二——一个源词有多个已批准译词时,它需要的是与术语表其余部分相反的指令:
| 干预 | 翻译特化模型 | 通用大模型 |
|---|---|---|
| 术语 | terms / ext.terminologies,服务端受约束解码级硬干预 |
system prompt 的 ## Glossary 段,要求逐字采用 |
| 一词多译的术语 | 从硬约束通道整条撤出,折进 domains / ext.domain_hint 的自由文本 |
## Terms with context-dependent translations 段,让模型按上下文从候选里选一个 |
| 翻译记忆 | tm_list / ext.examples,few-shot 参考 |
system prompt 的 ## Approved reference translations 段,并明确"这是风格示例、不是待译文本" |
| 领域提示 | domains / ext.domain_hint |
system prompt 一行,用户文本逐字插入(中文写的领域提示不会被再翻译一道) |
| 专家角色 | agent / ext.agent |
映射成角色短语替换首句的领域定语 |
上层完全不变:单句、REPL、CSV/Excel 批量、向量语义匹配、--learn 回写、缓存、断点恢复都照常工作。
骨架英文、用户内容原样。 角色定义与输出纪律用英文写(指令遵从更稳),domain / 术语 / 记忆里你的文本一律逐字插入。
上下文裁剪。 先按 4000 字符预算裁(术语优先吃预算、记忆填剩余,与其他后端同一策略),再叠一道条数闸门:术语 ≤30 条、一词多译的冲突术语 ≤8 条、记忆 ≤6 条。40 条示例在 AnyTrans 那边是按请求发一次被整批摊薄,在这里是每行重发——既烧 token,也让模型更可能去续写示例而不是守规则,所以闸门比字符预算收得更紧。冲突术语的 8 条是独立闸门,不继承术语的 30:一行冲突最多渲染 k 个候选 × 120 字符,30 行就是一堵歧义墙,而这一节最不能成为的恰恰是歧义墙。
输出清洗只剥不可能是合法译文的东西。 外层代码围栏、包裹整句的引号、开头的 Translation: 一类前缀标签会被剥掉;但源文自己以引号开头时引号保留,正文中间的围栏和 Translation: 不动,尾部的 \n\n注: 一律不动——游戏文本里这些都可能是真实台词,剥掉就是数据损坏,还会被 --learn 写进 memory.csv 变成永久污染。流式输出走同样的清洗。可疑但没被修改的内容用 -v 提示,交给你判断。
思考模式
qwen3.8 / 3.7 系列默认开启思考且思考 token 计费。翻译用不上思维链,所以注册表标记为"默认开"的模型会被主动关掉(发 enable_thinking=false),不关就是白烧钱 + 变慢。
实测代价(真实调用,total_tokens)。同一行 CSV、12 条术语 + domain 注入:
| 模型 | 不发 flag | 关掉后 | 其中思考 token |
|---|---|---|---|
qwen3.8-flash |
1019 | 531 | 443 |
deepseek-v4-pro-0813 |
1024 | 526 | 418 |
kimi-k3 |
1255 | 558 | 623 |
约 2 倍。文本越短比例越夸张——裸发一句 10 字中文时是 115→40、166→21、350→41,思考占比随源文长度摊薄。第三方模型(DeepSeek / Kimi)也认这个参数,说明它是百炼网关层的开关而非模型自带。
qmt "你好" -t English -m qwen3.8-max # 自动关闭思考
qmt "你好" -t English -m qwen3.8-max --thinking # 强制开启
qmt "你好" -t English -m qwen3.8-max --thinking --thinking-budget 1024
注册表里没有的模型(含 -m llm:<自由名>)不发这个参数:给不认识它的模型发 enable_thinking 会直接 400,而判错的代价(多花思考 token)远小于命令失败。要强制就用 --thinking / --no-thinking,报 400 就说明该模型不认。
判错的代价不止是多花钱。思考预算和答案共用 max_tokens,所以一个没被注册表标出来的思考模型可能把额度全花在思考上、答案被截断,qmt 直接报"输出被 max_tokens 截断"而不是交回半句话。实测 -m llm:deepseek-v4.1-flash 跑 1000 行有 19 行(1.9%)这样失败,加 --no-thinking 后 19 行全部正常、零吐回。用 llm: 接第三方思考模型时先拿几十行试跑,见到这个报错就加 --no-thinking。
思考内容永远不进输出——只读 content,不读 reasoning_content。批量推理的结果里根本没有 reasoning_content,所以 --batch-api 配 --thinking 只会烧钱、看不到思考过程。
已知限制
1. 术语干预靠提示词,不靠受约束解码。 translation_options.terms(qwen-mt)与 ext.terminologies(AnyTrans)是受约束解码级别的硬干预,服务端保证逐字采用;通用大模型后端只是把术语表渲染进 system prompt,那是"请求"而不是约束。glossary 非空且选了通用大模型时会向 stderr 提示一次(管道输出保持干净)。
机制上的差别是真的,但它在 1000 行 / 1135 条术语的量级上没有转化成可测的遵从度优势。四方实测里走硬约束的 qwen-mt-plus 字面命中 91.4%,是四个里最低的;走提示词的 qwen3.8-max 是 95.3%,裁判的术语分两者也只差 0.26(4.62 对 4.88)。详见「选型实测」,包括为什么字面命中率本身不能单独当结论——它把词性误判、词边界误判和语域差异混在一个数字里。
早期只测过 12 条武侠术语表(五个预设全部 11/11 命中),那个量级看不出衰减,别拿它外推。
想量化自己的术语表上的差距,跑这两条对比:
qmt "而这套生物传感器运用了石墨烯" -t English -m qwen-mt-plus
qmt "而这套生物传感器运用了石墨烯" -t English -m qwen3.8-max
2. 占位符可能损坏。 通用模型会把 {0} 规整成 { 0 }、把 %s 前后的词一起译掉。system prompt 有保护规则,但只能缓解不能保证。游戏 CSV 里这是致命的,跑完请抽查带占位符的行。
同一类形态也出现在分隔符上。实测 21 行带 | 的文案,| 的个数一次都没丢,但其中 2 行被加了空格:格挡|15% → Block | 15%、神兵暴击|983.46万 → Divine Weapon Crit | 9.8346M(同批韩文 10 行全干净)。客户端若按 | 切分又不 trim,字段就会带前后空格。输出清洗刻意不自动删这些空格:分隔符两侧的空格在某些文案里是合法的,按猜测改译文比留着它更危险。代价是没有任何自动检测——-v 的可疑提示只认尾部的解释块,认不出分隔符空格,所以这一类只能靠人工抽查。
3. token 成本与 AnyTrans 是两套算法。
- 实时逐行:AnyTrans 的 BatchTranslate 把 ext 按请求发一次、被整批 N 行摊薄(约 8 倍降幅,见 批量为什么省钱);通用大模型后端每行重发完整 system prompt。10 条术语 + 6 条记忆 + 规则骨架 ≈ 800–1500 input token/行,1 万行 CSV 就是额外 800 万–1500 万 input token。建议从
-C 3起步。 --batch-api:单价是实时的 50%,但每行仍带完整 system prompt,且批量推理不支持上下文缓存。所以"1 万行 × 1200 token × 半价"仍可能贵于 AnyTrans BatchTranslate 摊薄后的全价。
粗略盈亏平衡(用下面那张表的数字):设通用大模型单行 system prompt ≈ 1200 token、源文本 ≈ 30 token,则 --batch-api 是 1230 token/行 × 半价,折合 615 token/行的全价开销;AnyTrans --batch-size 10 摊薄后是 130 token/行全价。差约 4.7 倍。
在术语/记忆较重的场景下,AnyTrans 批量仍然明显更便宜;通用大模型后端的价值在于模型选择面,不在于成本。要压这 615,只能减少注入的术语与记忆条数。
4. 改提示词骨架后请 qmt cache clear。 缓存键不含提示词版本号,旧译文会继续命中。
5. 关掉思考可能让模型把原文原样吐回。 实测 qwen3.8-flash 在收到 enable_thinking=false 时会把短中文串一字不改地交回来(20/20 确定性复现,不关思考时 0/20)——它把"不要思考"理解成了"不要处理"。两道防线:
- 译文仍带源文字、且本次确实是 qmt 关掉了思考时,自动开着思考重试一次,第二次还不行就交回第一次的结果(不报错,因为那可能只是这一行的问题)。
- 缓存拒绝落盘空译文和与原文相同的译文。少了这一道,一次模型抽风会被冻结成永久命中:之后每次都报缓存命中并交回没翻译的原文。
已知缺口:重试的触发条件是"qmt 自己关过思考"。用 -m llm:<自由名> 接的未注册模型什么都不发(见「思考模式」),它要是自己吐回原文,重试不会触发,那一行会进输出文件——缓存仍然拒绝落盘,所以不会污染后续运行。四方实测里 deepseek 有 1 行是这样。
目标语本身就写汉字时(中→日、auto)不做这个检测,否则满是汉字的正确日文译文会被判成未翻译。误报的代价是多调一次,漏报的代价是一行没翻译还被缓存冻住,所以这个检测刻意只覆盖"CJK 源 → 非 CJK 目标"。
批量翻译
支持 CSV (.csv/.tsv) 和 Excel (.xlsx/.xls) 文件的批量翻译。翻译文件第一列内容,结果追加为新列。
# CSV 批量翻译(默认 5 路并发)
qmt -B input.csv -t English
# Excel 批量翻译(所有 sheet)
qmt -B input.xlsx -t Korean
# 指定并发数(1-20)
qmt -B input.csv -t English -C 10
# 指定输出文件
qmt -B input.csv -O output.csv -t English
# 首行也翻译(无表头模式)
qmt -B input.csv -t English --no-header
# 断点恢复(中断后从上次位置继续)
qmt -B input.csv -t English --resume
# 调整每批携带条数(默认 10,范围 1-50,仅 anytrans)
qmt -B input.csv -t English --batch-size 20
# 退化回逐条翻译(逃生口)
qmt -B input.csv -t English --batch-size 1
默认行为:
- 批量请求:仅
-m anytrans走 BatchTranslate,相邻行合并成一个请求(--batch-size控制条数)。默认的qwen3.8-max与其他后端逐行请求,--batch-size对它们无效,显式传了会 warn 一次 - 并发翻译:默认 5 路并发调用翻译 API,可通过
-C调整(1-20)。-C始终是并发请求数:走 BatchTranslate 时实际在飞行文本数 ≈C × batch-size,逐行后端就等于C - 自适应限流:遇到 API 429 自动降低并发度,恢复后自动回升
- 翻译缓存:相同文本+参数命中缓存时直接返回,不重复调用 API(缓存存储在
.qmt/cache.db) - 分批写入:翻译完一批立即落盘(默认约 100 行一次),崩溃时未落盘的行由
--resume重译 - 首行视为表头,不翻译(
--no-header可改变此行为) - 输出文件自动命名为
原文件名_translated.ext - Excel 文件翻译所有工作表
- 触发 API 限流时自动指数退避重试(2s -> 4s -> 8s -> 16s -> 32s,最多 5 次)
- 支持
--resume断点恢复,中断后可继续翻译
批量为什么省钱(token 摊销)
AnyTrans 按 token 计费,而术语与翻译记忆(ext)是按请求发送的。逐条翻译时每行都重发一遍同样的上下文;批量后这份上下文被整批 N 行摊薄。
以典型游戏文本为例(ext ≈ 2000 字符 ≈ 1000 token,单行 ≈ 30 token):
| 模式 | 输入 token / 行 |
|---|---|
逐条 --batch-size 1 |
1000 + 30 = 1030 |
批量 --batch-size 10 |
1000/10 + 30 = 130 |
约 8 倍降幅。摊销随条数单调增强,但官方未公布单请求条数上限,且一条坏数据的爆炸半径随批增大,所以默认保守取 10。跑通后可用 --batch-size 20~30 继续压低——单批源文本总字符另有 20000 的自限保护。实际开销在翻译摘要的 Token 消耗 行输出。
批内上下文共享
同一批所有行共用一份术语与翻译记忆:按批做一次块级语义匹配(批内各行向量取最大相关分聚合、取 Top-N、按库顺序输出),而不是逐行各配一份。
- 收益:索引同步从每行一次降到每批一次(25K 术语 × 1000 行的量级下差异显著);块内重复短串("确定"/"取消"/"返回")从各自 miss 变成同一缓存 key、第二次起直接命中
- 代价:单行术语召回精度下降。这是刻意取舍——相邻行通常来自同一屏、同一语境,用词高度重叠
- 需要逐行精度时用
--batch-size 1
滚动翻译记忆仍跨批、跨并发 lane 共享(一批完成后整批写入,下一批取最近 5 条),但不参与缓存 key,否则命中率会随运行顺序漂移。
云端模式下「付一次、处处用」的性质保持不变:整个文件的行在 warm 阶段一次性打包成 TermQuery 单元(每单元 ≤800 字符且 ≤10 行,总数超过 200 次调用则直接降级),之后每个批次的召回都是纯字典查表、零网络 I/O。所以批循环里不可能因为检索而触发限流。
--agent 与批量翻译
--agent(27 个专家角色)只有 TextTranslate 支持,BatchTranslate 的 ext 没有该字段。两者共用时批量被整轮禁用,降级为逐条 TextTranslate 并提示一次——宁可慢,也不静默丢弃你显式指定的专家角色。
要批量加速就去掉 --agent;要专家角色就接受逐条速度。
批量推理(--batch-api,半价)
这与上面的 BatchTranslate 是两个东西,只是中文都叫"批量"。 BatchTranslate 是一次请求带 N 行、秒级返回、只有 AnyTrans 有;批量推理是把整个文件拼成 JSONL 上传,任务在服务端排队,等待以小时计,单价是实时的 50%。仅通用大模型后端可用。
qmt -B input.csv -t English -m qwen3.8-max --batch-api -v
约束(都是平台限制,不是可调项):
- 仅 CSV/TSV。 Excel 会直接报错而不是静默回退——你点名要半价,悄悄按全价跑实时路径就是撒谎。请先导出为 CSV。
- 仅通用大模型后端。
-m anytrans/-m qwen-mt-*带--batch-api会报错:这些模型的批量能力来自服务端私有协议,批量推理无法表达。 - 注册表标
批量推理 = ✗的模型会提示一次并回退到实时并发路径,按全价计费。 - 支持面按端点变,注册表预测不了。 实测某个国际业务空间端点上,五个预设全部被拒(
not supported by the Batch API),连百炼文档明确列过的qwen3.5-flash也拒,而qwen-plus/qwen-max正常完成。注册表那一位是照文档查证的白名单,所以刻意不据此改成 ✗——判错的代价不对称:多声明一次只换来一个几十秒内failed、不计费、且报错里写明了出路的任务;少声明一次则是在最多 5 万行上静默付全价。遇到这个报错就照它说的做:去掉--batch-api走实时全价,或换成该端点接受批量的模型。 - 单个输入文件 ≤ 50000 条请求且 ≤ 500MB;超出自动拆成多个任务。
- 一个文件只能是同一个模型 + 同一种思考模式。
completion_window固定 24h,但实际等待可能更久;超时就变expired。- 结果文件 30 天后删除。
- 批量推理不支持上下文缓存,所以每行仍带完整 system prompt。成本对比见 已知限制 第 3 条。
命中缓存的行不会进 JSONL——半价的请求也是请求。
中断与重连。 提交成功后 batch_id 立刻写入 .qmt/batch_jobs.json,然后才开始轮询。所以 Ctrl-C 之后重跑同一条命令会继续等待已提交的任务,不会重新提交:
检测到未完成的批量任务 batch_xxx,继续等待结果 (未重新提交)
匹配条件是 (输入文件, 输出文件, 模型, 思考模式, 待译行号, 端点) 全部一致;改了输入或切了端点档案就不重连、提交新任务。端点必须参与匹配:batch_id 是该账号该区域的资源,北京提交的任务在新加坡端点上重连,换来的是数小时等待之后一个莫名的 404。想放弃一个在跑的任务:删掉 .qmt/batch_jobs.json(任务本身仍会在服务端跑完并计费,只是本地不再等它)。
部分失败是一等输出。 completed 的任务仍可能带一个 error 文件——那些行写 [ERROR: ...],其余行照常,退出码非零。failed / expired / cancelled 的任务若已有输出文件,已完成的行会先写盘再报错,因为那些行你已经付过钱了。
批量翻译故障排查
| 现象 | 原因与处理 |
|---|---|
某行输出 [ERROR: ...] |
批量失败后已自动回退逐条 TextTranslate 再试一次,仍失败才标记。--resume 只重译这些行 |
| 速度没变快、仍逐条 | -m 不是 anytrans;或带了 --agent;或 --batch-size 1 |
| 首批之后全部降级逐条 | 业务空间未开通 BatchTranslate(403/404)。运行级降级已避免每批白付一次失败请求,需去控制台确认权限 |
| 频繁 429 | -C 是并发请求数,批量不改变它,但单请求更重。先降 --batch-size,再降 -C |
| 在飞行行数被自动压低 | C × batch-size 超过 100 行软上限时会收窄 lane 数并提示一次 |
| 想量化摊销收益 | 同一份文件分别用 --batch-size 1 与默认值跑,对比 Token 消耗 |
--batch-api 报 Excel 不支持 |
刻意不静默回退。导出为 CSV/TSV 再跑 |
--batch-api 报模型不支持 |
-m 是 anytrans / qwen-mt-*。批量推理只服务通用大模型后端。这是提交前的本地拦截 |
--batch-api 任务提交成功却 failed,报 not supported by the Batch API |
端点不收这个模型,是服务端拒绝,本地白名单预测不了。报错里已写明出路:去掉 --batch-api 走实时全价,或换 -m llm:<该端点支持批量的模型>。不计费。任务文件保留并标记 terminal,重跑会提交新任务而不是重新挂上去 |
--batch-api 提示"已回退到实时并发翻译" |
该模型不在百炼批量支持列表(qmt models 的批量推理列为 ✗)。回退后是全价,不是半价 |
--batch-api 一直显示"排队中" |
正常。轮询退避 10s→60s,进度条读服务端的 request_counts,提交后长时间 0% 是预期行为 |
--batch-api 重跑却又提交了新任务 |
pending_row_indices 变了(输入文件被改过)、端点变了(切了档案),或 .qmt/batch_jobs.json 被删了 |
任务终止于 expired |
超过 completion_window。已完成部分若已产出会写盘,job 条目保留在 .qmt/batch_jobs.json 供排查 |
进度观察与 Hook 唤醒
批量翻译跑起来之后,终端里那条进度条只有盯着它的人能看见。如果命令是后台跑的、或由外部 Agent 调起的,进度就是个黑盒。两件事解决它:运行状态落盘成 JSON 供随时查询,以及终态时敲一下你指定的命令把 Agent 唤醒。
查进度:qmt status
qmt status # 人类可读面板
qmt status --json # 机器可读快照(走 stdout,可直接管道进 jq)
qmt status --all # 列出近期运行
qmt status --run <run_id> # 查指定那次运行
qmt status --wait # 阻塞到最近一次运行结束再输出
qmt status --wait --timeout 600 # 最多等 600s
--wait 是给收不到 Hook 的 Agent 的兜底:后台跑批之后直接 qmt status --wait --json,一次调用拿到结果,不用自己写轮询循环。
状态文件落在 .qmt/runs/<run_id>.json,同时维护一份 .qmt/runs/latest.json 指向最近一次运行。写入节流到约每 0.5s 一次,阶段切换与终态立即落盘。
快照字段:
| 字段 | 含义 |
|---|---|
status |
running / completed / failed / interrupted |
phase |
reading / embedding / translating / writing / collecting / queued |
mode |
realtime / batch_api |
total / completed |
可翻译行数 / 已推进行数 |
succeeded / failed / skipped |
成功 / 失败 / resume 已跳过 |
rows_per_minute / eta_seconds |
速率与预计剩余;运行头 2s 内为 null(样本太短,算了会误导) |
sheet |
Excel 当前在翻的工作表 |
error |
失败原因 |
run_id / pid / updated_at_epoch |
运行标识与存活信号 |
phase 不是装饰:25k 行文件的向量预热能静默几十秒,没有它你只会看到进度条卡在 0% 而无从判断是在预热还是挂了。--batch-api 排队同理,会显示 queued。
两个容易踩的坑:
completed可能领先succeeded + failed。completed跟的是进度条(缓存命中、resume 跳过、空行、失败行都算推进),而succeeded/failed只在每个 chunk 边界对账。判断"跑完了没"要看status,不要看这两个数相加。stale。进程被 SIGKILL 时来不及写终态,qmt status会把它标成stale(updated_at超过 30s 没动)。--wait遇到这种状态以退出码 3 结束,不会无限挂住。无运行记录时退出码是 1。
Hook:终态唤醒
批量翻译进入终态时执行一条你指定的 shell 命令。Agent 因此不必轮询守候——配一次,跑完自动被叫醒。
三层配置,优先级依次升高:
# 1. 配置文件(持久,项目级覆盖全局)
qmt config set --hook 'echo "$QMT_EVENT" >> /tmp/qmt-done.log'
qmt config show # 看当前生效值与来源层
qmt config clear --hook
# 2. 环境变量(临时)
export QMT_HOOK='curl -X POST https://example.com/wake'
# 3. CLI 参数(单次运行覆盖)
qmt -B input.csv -t Korean --hook './notify.sh'
三个终态事件共用同一条命令,靠 $QMT_EVENT 区分:batch_complete / batch_fail / batch_interrupted。要按事件分流就在命令里判:
qmt config set --hook '[ "$QMT_EVENT" = batch_complete ] || exit 0; ./review.sh'
载荷有两份,命令按需取用:
- stdin:完整 JSON,
{"event": "...", "state": {...全部快照字段...}} - 环境变量:关键值摊平成
QMT_*,让一行 shell 就能用、不必依赖 jq
| 环境变量 | 内容 |
|---|---|
QMT_EVENT |
事件名 |
QMT_STATUS |
completed / failed / interrupted |
QMT_TOTAL / QMT_COMPLETED / QMT_SUCCEEDED / QMT_FAILED / QMT_SKIPPED |
计数 |
QMT_INPUT / QMT_OUTPUT |
输入与输出文件绝对路径 |
QMT_STATE_FILE |
状态文件路径(想读全量细节就 cat 它) |
QMT_ELAPSED_SECONDS / QMT_RUN_ID / QMT_MODEL / QMT_TARGET_LANG |
耗时与标识 |
QMT_ERROR |
失败原因;成功时为空串(不是字面量 None) |
唤醒配方,按你的 Agent 运行时挑一条:
# 写哨兵文件,Agent 侧轮询这个文件即可
qmt config set --hook 'echo "$QMT_STATUS $QMT_OUTPUT" > /tmp/qmt-wake'
# 打 webhook
qmt config set --hook 'curl -sf -X POST -H "Content-Type: application/json" -d @- https://example.com/hook'
# 桌面通知(macOS)
qmt config set --hook 'osascript -e "display notification \"$QMT_SUCCEEDED/$QMT_TOTAL 完成\" with title \"qmt 翻译\""'
几条约定:
- 顺序是先落终态状态、再触发 Hook。Hook 的第一个动作通常就是读状态文件,那时它已经是终态了,不会读到
running。 - Hook 失败不影响翻译。非零退出、超时(30s)、命令不存在都只打一条警告,翻译结果照常落盘。
- 失败与中断也会触发。这正是最该叫醒 Agent 的时刻——以前这两种情况一路抛到顶层,没有任何出口。
- Hook 命令只能来自 CLI 参数 / 环境变量 / 配置文件这三个你亲手填的来源,绝不从翻译内容或输入文件派生。
术语管理
固定术语确保特定词汇翻译一致。保存在项目目录 .qmt/terms.csv,无表头,每行 源词,译词[,备注]。第三列可选,两列老行和三列新行可以共存于同一个文件,升级不需要迁移。
# 添加术语
qmt terms add "React" "React"
qmt terms add "组件" "コンポーネント"
# 同一个源词有多个已批准译词时,用 -n 写清各自的使用语境(见「一词多译」)
qmt terms add "丹药" "단약" -n "物品栏用词"
qmt terms add "丹药" "영약" -n "修炼语境"
# 查看术语列表
qmt terms list
# 删除术语
qmt terms remove "React"
# 清空所有术语
qmt terms clear
# 从 CSV/TSV 文件批量导入
qmt terms import terms.csv
# 翻译时使用内联术语(不保存)
qmt "React组件开发" -t Japanese --terms "React:React,组件:コンポーネント"
# 对账上传到 AnyTrans 云端干预词库(详见「术语 RAG 模式」)
qmt terms push --dry-run
qmt terms push
qmt terms pull
terms import不同步向量索引:批量导入的术语在qmt vectordb rebuild之前对本地语义匹配不可见。云端模式无此问题,push后即刻可召回。
一词多译
同一个源词允许有多个已批准译词。这时该源词整条撤出硬约束通道:AnyTrans 的 terminologies 与 Qwen-MT 的 terms 都只有 src/tgt 两个字段,两条 src 相同、tgt 互斥的条目是受约束解码级的矛盾指令,模型必须违反其中一条。撤出后所有候选译词连同备注折进自由文本(AnyTrans 的 domain_hint、Qwen-MT 的 domains、通用大模型提示词里独立的一节),由模型按当前上下文选一个。
备注就是给模型的选择依据,也只在这种情况下生效。单译词术语写备注不会有任何效果——硬约束通道没有备注字段,把它渲染出来等于将一个确定的术语降级成软提示。qmt terms list 用 ! 标出会走软裁决的源词。
两条边界:
- 备注是本地独有的。
TermEdit只有 src/tgt,terms push推不上去;terms pull --export写的是服务端快照,也只有两列。 - 内联
--terms "源:译"表达不了备注。
缓存:改了冲突源词的备注会让相关行的键移动一次(定向 miss,重跑即可),其余备注编辑都不动键——细则见「缓存」。
复合词与子词译法不一致
一词多译是同一个源词有多个译词。还有另一类歧义,源词互不相同,所以 split_conflicts 检测不到,任何代码路径都拦不住——只能写进领域提示词。
典型形态是复合条目的译词只覆盖了原文的一部分,而它内部的子词另有自己的条目:
秩序白银 → シルバー 秩序 → 秩序
荣耀黄金 → ゴールド 荣耀 → 栄光
永恒钻石 → ダイヤ 永恒 → 永遠
段位名实际是「前缀 + 金属 + 罗马数字」三段,秩序白银 → シルバー 给的只是金属段。模型看到长条目字面命中就照抄,译出 シルバーV——它没有违规,它在严格执行术语表。
处理办法是在项目级 .qmt/domain.md 里把这类条目点名说清楚:列出冲突的条目、说明长条目只覆盖哪一段、给出拼装后的正确译法。抽象规则(「术语要完整译出」)不管用,必须落到具体条目。
实测(qwen3.8-max,中→日,15 行段位名):领域提示词点名说明后 4 次独立运行全部 15/15;对照(同一术语表、无这一节)平均 7.5/15 且波动很大(6–10)。500 行全量上召回后遵守率 91.0% → 93.7%,召回率不变(领域提示词不参与检索)。顺带把罗马数字被转成全角(V → Ⅴ)的 8 行压到 1 行。
翻译记忆
提供参考翻译对,帮助模型保持翻译风格一致。保存在 .qmt/memory.csv。
# 添加翻译记忆
qmt memory add "你好世界" "Hello World"
# 查看记忆列表
qmt memory list
# 删除记忆
qmt memory remove "你好世界"
# 清空所有记忆
qmt memory clear
# 从文件批量导入
qmt memory import memory.csv
记忆对所有后端生效,注入方式不同:默认的通用大模型后端把它渲染进 system prompt(见 通用大模型后端),AnyTrans 映射为 ext.examples few-shot 示例,qwen-mt-* 映射为 tm_list。云端 RAG 模式下 AnyTrans 改为把记忆折叠进 ext.domain_hint(见下节)。
本地模式下术语与记忆共享 4000 字符的请求预算,超出时按「术语优先」顺序截断——术语是硬约束必须保留,记忆只是软参考。云端模式的记忆走 domain_hint(另有预算),不再与术语争抢这 4000 字符。
术语 RAG 模式
术语与翻译记忆的召回方式有两种模式,由 --rag 选择:
| 模式 | 术语来源 | 记忆来源 | 需要 DashScope Key |
|---|---|---|---|
local |
本地 terms.csv + zvec HNSW 向量索引 |
本地 memory.csv + 向量索引 |
是(embedding) |
cloud |
AnyTrans 服务端干预词库(TermQuery 召回) | 折叠进 ext.domain_hint,词法匹配 |
否(仅指召回;模型本身仍可能要,只有 -m anytrans 不要) |
默认跟随后端:-m anytrans 走 cloud,其余后端(含默认的 qwen3.8-max)走 local。--rag、QMT_RAG_MODE、qmt config set --rag-mode 可强制任一种。
解析出的模式是
cloud不等于真的走云端——还有一道「必须先 push」的闸门,见下。
--rag cloud -m qwen-mt-plus 是合法组合——术语查 AnyTrans 词库、翻译走 DashScope。但记忆的 domain_hint 折叠只发生在 AnyTrans 的线上序列化层,所以这种组合下记忆仍按 DashScope 的方式注入。
云端模式的前提:必须先 push
.qmt/ 在 .gitignore 里,记录 termId 的边车 .qmt/terms.remote.json 不随仓库走。因此有一道硬闸门:解析出的云端作用域在边车里没有条目时,直接留在 local,不发任何探测请求。
实际含义:只有跑过 qmt terms push 的那台机器才真正进入云端模式。同事克隆仓库后行为与升级前完全一致,不会每次翻译白花一个 TermQuery 往返加一条警告。接手一个已有云端词库的项目,先 qmt terms pull 把 termId 拉回本地。
作用域由四个字段界定,四者共同决定查的是哪个词库:workspace_id / source_lang / target_lang / scene。改动其中任一个都会指向另一个(可能是空的)词库。
上手
# 1. AnyTrans 凭证与业务空间(已有则跳过)
qmt config set --access-key-id <AK> --access-key-secret <SK> --workspace-id <WID>
# 2. 固定语向与场景。auto 不是语言代码,不能作为词库的键
qmt config set --rag-mode cloud --source-lang Chinese --target-lang Korean --scene mt-turbo
# 3. 预览差异,再对账上传
qmt terms push --dry-run
qmt terms push
# 4. 验证
qmt config show # 看「RAG / 术语检索」段是否显示「云端可用」
qmt "攻击力提升" -v # -v 会打印「术语检索: 云端词库 (scene=mt-turbo)」
-v 只确认走了云端通路,不列出召回到的具体术语。要看服务端到底持有什么,用 qmt terms pull --export 导出词库对照。
术语对账(push / pull)
terms.csv 是真源,云端是派生镜像。push 拿本地 CSV 与边车对账,产出 ADD / MODIFY / DELETE 三类变更,分批(默认 100 条一次 TermEdit)上传,每批确认后立即落盘 termId——Ctrl-C 不会丢掉已确认的部分。
qmt terms push --dry-run # 只打印增/改/删计数,不发任何请求
qmt terms push -v # 逐条列出变更的术语
qmt terms push --prune # 同时删除云端有、本地 CSV 没有的术语
qmt terms push --chunk-size 50 # 调整单次 TermEdit 携带条数
qmt terms pull # 拉云端状态到 .qmt/terms.remote.json
qmt terms pull --export # 另外导出云端词库为 terms.cloud.csv
三条安全约束:
pull从不写terms.csv。默认只更新边车,--export也只生成新文件,绝不覆盖真源。- 删除必须显式
--prune。清空本地 CSV 后直接push产生零条删除,防止顺手清空云端词库。 - 本地同一源词有多个译词时
push折叠为末行胜出,warn 一次并列出这些源词——云端词库一个源词只允许一个译词。只有 push 折叠:翻译时所有候选仍一起交给模型裁决,见「一词多译」。
源语言为 auto(默认值)时 push/pull 硬失败并点名修复命令:往 auto 这个键上写词库是不可恢复的垃圾数据。翻译时遇到 auto 则只是降级为 local——写坏数据不可逆,召回次优可逆。
云端模式的翻译记忆
AnyTrans 没有翻译记忆接口,所以云端模式把筛选出的记忆对折叠进 ext.domain_hint(引导语用英文,记忆对本身保持原语言),这是两个端点唯一共享的自由文本字段。一词多译的冲突块也用这个字段,但两个模式都用;记忆折叠仍然只在云端发生,本地模式有原生的 ext.examples。
云端模式没有 embedding 可用,记忆筛选是零 API 调用的词法匹配:子串包含(记忆源文出现在行文本里)+ CJK 二元组 Jaccard 重叠。确定性、可缓存,同一输入永远得到同一选择。
预算:冲突块和记忆块各有 1200 字符的独立上限,冲突块先扣费;整个 domain_hint 上限 3000 字符。触顶时先砍折叠块,绝不砍你的 domain.md 正文——正文每条请求都一样长,砍了会表现成「提示词随机失效」。一块放不下也不会把另一块带走。
降级链
云端模式任何一环失败都降级为本地向量检索,各只提示一次,绝不中断翻译:
| 触发条件 | 结果 |
|---|---|
| AnyTrans 凭证缺失 | 降级 local,提示一次 |
作用域不完整(缺 workspace_id / source_lang / target_lang) |
降级 local |
| 边车无此作用域条目 | 静默留在 local(不发请求,避免新克隆机器每条命令都警告) |
| TermQuery 调用数超过 200 上限 | 降级 local,不静默发数百个付费调用 |
| TermQuery 出错 / 超时 / 部分失败 | 丢弃整轮召回并降级 local |
| 云端词库空但本地 CSV 非空 | 降级 local(镜像是空的,不是没有术语) |
批量运行中降级是永久的:词库不可达、为空、或太大无法查询,在整个运行期间都不会变好,按块重试只是每次都白付一个往返去重新得到同一个答案。交互式模式下每轮重建,一次网络抖动不会拖垮整个会话。
排查入口:config show
qmt config show 的 RAG / 术语检索 段是唯一的诊断入口——降级本身是静默的(除了那一行提示),这里才能看出为什么:
RAG / 术语检索:
rag_mode = cloud
生效模式 = cloud
source_lang = Chinese 来源: project
target_lang = Korean 来源: project
scene = mt-turbo 来源: default
workspace_id = llm-xxxxxxxx
状态: 云端可用,边车已记录 25452 条 termId
作用域不完整时会点名具体卡在哪个字段并给出确切的修复命令:
状态: 云端 scope 不完整,翻译时将降级为本地向量检索
缺 source_lang: -s/--source 指定源语言,或 qmt config set --source-lang Chinese
("auto" 不是语言代码,不能作为词库的键)
缺 workspace_id: --workspace-id 指定业务空间,或 qmt config set --workspace-id <ID>
缓存
rag_mode 参与翻译缓存键。服务端可能自行施加已注册的干预术语,所以同文本、同术语、同记忆的 cloud 请求与 local 请求是不同的 API 载荷,不能共用缓存条目。该字段仅在取值 cloud 时拼入键,因此升级不会让已有的 cache.db 失效。
术语哈希同样只把真的会上线的东西算进去:备注仅在它所属的源词确实有多个已批准译词时才拼入键。于是无备注的 terms.csv 与升级前逐字节同哈希;把备注写在单译词术语上也不动键——那种备注根本不渲染,载荷逐字节相同;而改了冲突源词的备注会移动一次键,让「冲突已存在 → 补备注 → 重跑批量」这个主用法不会命中旧缓存、备注永远不被行使。
qmt cache stats # 条目数 / 上限 / 存储路径
qmt cache clear # 清空(会先确认)
缓存键覆盖文本、语向、模型、领域、术语与记忆的内容哈希、rag_mode——但不含提示词骨架的版本。所以改了通用大模型后端的提示词(或升级到改了提示词的版本)之后,旧译文仍会命中,需要 qmt cache clear。
端点也不在键里,这是刻意的:同一个模型换个区域还是同一个模型,跨区域重跑命中缓存是省钱,不是错配。别来"修"它。
向量语义匹配
本节描述的机制属于 local 模式。云端模式的术语召回走 AnyTrans TermQuery、记忆召回走词法匹配,全程不调用 DashScope embedding。但本地索引仍然是云端降级时的兜底目标,所以
--rag local覆盖和降级链都还需要它。
当术语或翻译记忆条目较多时(默认超过 20 条),QMT 自动启用向量语义匹配,从大量条目中智能筛选与当前翻译内容最相关的 Top-K 条(默认 10 条)传给模型,而不是全量传入。
底层使用 zvec 向量数据库(HNSW 索引 + 余弦距离)存储嵌入向量,通过 DashScope qwen3.7-text-embedding-flash 模型生成向量嵌入进行语义检索。向量索引以二进制格式存储在 .qmt/terms.zvec/ 和 .qmt/memory.zvec/,首次翻译时自动构建,后续增量同步。
# 默认行为:术语/记忆超过 20 条时自动启用
qmt "进入副本后需要先组队" -t Korean -v
# 控制返回条数(默认 10)
qmt "进入副本" -t Korean --top-k 5
# 调整启用阈值(默认 20,设高则不启用)
qmt "进入副本" -t Korean --threshold 50
# 批量翻译时自动预嵌入所有源文本,逐行筛选最相关术语/记忆
qmt -B input.xlsx -t Korean -v
--learn 自动学习
翻译结果可自动写回翻译记忆库,并即时同步向量索引:
# 单条翻译 + 自动学习
qmt "锻造装备需要金币" -t Korean --learn
# 批量翻译 + 批量学习(所有成功翻译自动写入记忆)
qmt -B input.csv -t Korean --learn
# 交互模式中切换学习
qmt -i -t Korean
# 输入 /learn 开启自动学习
向量索引管理
当术语库条目较多时(如 25000+),首次翻译需要嵌入所有术语会非常耗时。推荐提前构建向量索引:
# 查看当前索引状态
qmt vectordb status
# 从 CSV 重建全部向量索引(术语 + 翻译记忆)
qmt vectordb rebuild
# 清除所有向量索引(不影响 CSV 数据)
qmt vectordb clear
rebuild 会调用 DashScope embedding API 逐批嵌入所有术语/记忆,构建完成后后续翻译即可使用向量语义匹配。如果未预先构建索引且术语超过 500 条,翻译时会自动降级为截取前 N 条。
status 首行会报当前 RAG 模式。云端模式下 rebuild / clear / query 仍然可用——--rag local 是合法的临时覆盖,它需要一个能用的索引,所以这里只报告模式、不禁用命令。
每个索引还会记下落盘时用的端点,与当前生效端点不一致时 status 多打一行:
端点: https://dashscope.aliyuncs.com/compatible-mode/v1 (当前: https://dashscope-intl.aliyuncs.com/compatible-mode/v1) ⚠ 不一致,建议 rebuild
这是警告,不是失败。 索引本身不依赖端点,照样能查,真实风险只是命中率悄悄不对——为这个把翻译卡住不划算。翻译时同样只警告一次。但 qwen3.7-text-embedding-flash 是分区域上架的,新加坡端点没有它,切过去嵌入调用会直接 404,那是 API 层的失败、不走这条警告。对比之下嵌入模型变更是硬失败(报"嵌入模型已变更,请先运行: qmt vectordb rebuild"),因为不同模型的向量彼此无意义。manifest 早于端点档案功能时没有这一项,视为"历史/未知",不告警。
领域提示
领域提示告诉模型翻译的专业领域和风格要求,支持两级存储:
- 项目级 — 保存在当前目录
.qmt/domain.md,优先级最高 - 全局级 — 保存在
~/.qmt/domain.md,作为默认值
# 设置项目级领域
qmt domain set "technology"
# 设置全局级领域
qmt domain set "medical translation, formal tone" --global
# 查看当前配置
qmt domain show
# 清除项目级领域
qmt domain clear
# 清除全局级领域
qmt domain clear --global
优先级:命令行 -d 参数 > 项目级 > 全局级。
翻译时自动加载已保存的领域提示,无需每次手动指定:
# 先设置领域
qmt domain set "game localization, martial arts world view"
# 后续翻译自动生效
qmt "仙缘副本" -t Korean
qmt -B game_strings.xlsx -t Korean
分工。 qmt 内置的 system prompt 是语言无关的,只放通用纪律:术语逐字采用、记忆作参考、只输出译文。任何跟具体语言或具体项目绑定的规则都写在 domain.md 里——敬语体、数字与标点习惯、占位符处理方式,以及术语表条目互相矛盾时怎么裁决(见「复合词与子词译法不一致」)。用户文本逐字插入,不会被再翻译一道。
交互式模式
qmt -i -t English
交互模式下可用命令:
| 命令 | 说明 |
|---|---|
/help |
显示帮助 |
/target <lang> |
切换目标语言 |
/source <lang> |
切换源语言 |
/model <name> |
切换模型,并重建客户端 (anytrans/plus/flash/lite/qwen3.8-max/llm:<名>) |
/endpoint <name> |
切换端点档案,base URL 与该区域的 API Key 一起换;无参数列出当前端点与已存档案 |
/stream |
切换流式输出 |
/domain <text> |
设置领域提示 |
/domain save [global] |
保存当前领域到项目/全局 |
/domain clear |
清除当前领域 |
/agent <name> |
AnyTrans 专家角色 (game/medical/tech/...,clear 清除) |
/scene <name> |
AnyTrans 模型场景 (mt-plus/mt-turbo) |
/rag <mode> |
术语检索模式 (local/cloud),切换后立即重建检索上下文 |
/topk <n> |
设置语义匹配 Top-K |
/learn |
切换翻译记忆自动学习 |
/info |
显示当前设置 |
exit / quit |
退出 |
/target /source /scene /model /rag /topk /endpoint 都会改变术语与记忆的召回条件,因此每条命令执行后立即重建检索上下文——会话余下部分不会继续查询过期的语向或作用域。云端召回出错时同样按轮重建,一次网络抖动不会让整个会话都退化成无术语翻译。
/model 还会重建翻译客户端并关闭旧的。它此前只换掉模型名变量,请求仍旧走原来那个 client,新名字被静默忽略;三个后端之后这不只是"用错模型",而是"用错凭证体系"(DashScope API Key vs AnyTrans AK/SK)。所以 /model anytrans 与 /model qwen3.8-max 之间的往返切换是真的换了后端。
/endpoint 换的不只是 URL,Key 跟着一起换——这是端点档案存在的理由。会话内的翻译客户端、本地 RAG 的 embedding 调用、以及 --learn 的记忆回写全部改用新区域的 Key;只搬 URL 会得到一个用 A 区 Key 打 B 区 URL 的会话,而它的表现是几轮之后一个看不出因果的 401。切换失败(档案名不存在、Key 短了)时打一行原因并留在原端点,会话不受影响。/info 里能看到当前端点与掩码后的 Key。-m anytrans 下切换照样有效(本地 RAG 的 embedding 确实跟随),但 AnyTrans 自身仍钉在 cn-beijing。
完整参数一览
qmt [TEXT] [OPTIONS]
参数:
TEXT 待翻译文本
选项:
-t, --target TEXT 目标语言 (默认 Chinese)
-s, --source TEXT 源语言 (默认 auto)
-m, --model TEXT 模型: anytrans / qwen-mt-plus|flash|lite / qwen3.8-max
/ qwen3.7-plus / qwen3.8-flash / deepseek-v4-pro-0813
/ kimi-k3 / llm:<任意百炼模型名>
(默认 qwen3.8-max;完整能力表见 qmt models)
-f, --file PATH 翻译文件内容
-d, --domain TEXT 领域提示
-i, --interactive 交互式模式
--stream 流式输出 (qwen-mt-flash/lite 与通用大模型支持)
--terms TEXT 内联术语 (源:译,源:译)
--terms-file PATH 术语文件 (CSV/TSV)
--memory-file PATH 记忆文件 (CSV/TSV)
-B, --batch PATH 批量翻译文件 (CSV/Excel)
-O, --output PATH 批量翻译输出文件
-C, --concurrency INT 批量翻译并发请求数 (默认 5, 范围 1-20)
--batch-size INT 每批携带文本条数 (默认 10, 范围 1-50, 仅 anytrans)
--batch-api 百炼批量推理: 半价, 但等待以小时计 (仅通用大模型 + CSV/TSV)
--hook TEXT 批量终态时执行的 shell 命令 (唤醒外部 Agent,见「进度观察与 Hook 唤醒」)
--no-header CSV/Excel 无表头模式
--resume 断点恢复
--top-k INT 语义匹配返回条数 (默认 10)
--threshold INT 语义匹配启用阈值 (默认 20)
--learn 翻译结果自动写入翻译记忆
--agent TEXT AnyTrans 专家角色 (仅 -m anytrans 生效),与 -B 共用时禁用批量
--scene TEXT AnyTrans 模型场景 (mt-plus/mt-turbo,仅 -m anytrans 生效)
--thinking / --no-thinking
思考模式 (仅通用大模型)。不传时按注册表决定
--thinking-budget INT 思考 token 预算 (1-32768, 仅在思考开启时发送)
--rag [local|cloud] 术语检索模式 (默认 -m anytrans 走 cloud、其余后端走 local)
--workspace-id TEXT AnyTrans 业务空间 ID
--api-key TEXT DashScope API Key
--endpoint TEXT 本次请求的端点: 已存档案名 (qmt endpoint list) 或完整 URL。
档案自带该区域的 Key,URL 与 Key 一起切;优先级高于
QMT_BASE_URL 与持久化的生效档案。-m anytrans 不受影响
-v, --verbose 显示详细信息
--no-store 不加载本地术语/记忆库
-h, --help 显示帮助
-V, --version 显示版本
项目结构
src/qmt/
cli.py # CLI 入口与命令定义
model_registry.py # 模型名解析与后端路由 (llm: 前缀在此剥掉)
backends.py # 按后端构建客户端的工厂 + 各后端的 warn-once 提示
anytrans_client.py # AnyTrans 翻译客户端 (ROA 签名,推荐)
client.py # Qwen-MT API 客户端 (同步,OpenAI 兼容模式)
llm_prompt.py # 通用大模型提示词适配层 (纯函数,零 I/O)
llm_client.py # 通用大模型客户端 (同步,OpenAI SDK)
async_client.py # 异步客户端 (批量翻译用,含通用大模型后端)
batch.py # CSV/Excel 批量翻译 (并发 + 自适应限流)
batch_inference.py # 百炼批量推理 (--batch-api,上传 JSONL + 轮询)
run_config.py # RunConfig: 一次运行解析好的配置,在 CLI 边界构建一次后下发
endpoint.py # 端点档案: base_url 与该区域 API Key 的解析/校验/发布 (含模块级 holder)
cache.py # 翻译结果缓存 (SQLite)
interactive.py # 交互式 REPL
matcher.py # 向量语义匹配编排层
rag.py # RAG 作用域与 ContextProvider 抽象 (local/cloud 模式选择)
cloud_terms.py # AnyTrans 云端干预词库客户端 (TermQuery / TermEdit)
term_sync.py # terms.csv 与云端词库的对账 (push/pull,无 SDK 依赖)
embedding.py # DashScope qwen3.7-text-embedding-flash 嵌入
vectorstore.py # 向量索引存储 (zvec HNSW)
store.py # 本地持久化存储 (术语/记忆/领域/凭证,带文件锁)
filelock.py # 跨进程文件锁 (fcntl)
models.py # 数据模型 + 一词多译的冲突裁决 (拆分/渲染/切片)
parsers.py # 输入解析
formatters.py # 终端输出格式化
constants.py # 常量定义
exceptions.py # 自定义异常
notify.py # 邮件通知
runstate.py # 批量运行状态落盘 (.qmt/runs/,供 qmt status 读取)
hooks.py # 终态 Hook: 执行用户配置的 shell 命令唤醒外部 Agent
开发
# 安装开发依赖
pip install -e ".[dev]"
# 代码检查(门禁;规则集在 pyproject.toml 里钉死)
ruff check src/qmt/ tests/
# 运行测试
pytest
不要对整棵树跑 ruff format:这棵树从来没被 format 过,一次会重写 53 个文件里的 36 个,真实改动就埋进无关 diff 里了。只手工格式化自己碰过的行,向周围风格看齐。
License
Apache-2.0
Metadata
Release files for qwen-mt-cli 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| qwen_mt_cli-1.0.0.tar.gz | 395.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| qwen_mt_cli-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 611.2 kB
Release files / qwen_mt_cli-1.0.0.tar.gz
| Download URL | qwen_mt_cli-1.0.0.tar.gz |
|---|---|
| Size | 395.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3dc2b1d29d7306cf0a2c8fcdd9c7e4acfacd30b6d3491c6f766d94fb9de6cdfa
|
|
BLAKE2b-256 checksum How to use checksums |
4fef82e6fa5feb89107af1d4822d99a26831bb22dc61c27d97d00996766dc889
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.3
|
Release files / qwen_mt_cli-1.0.0-py3-none-any.whl
| Download URL | qwen_mt_cli-1.0.0-py3-none-any.whl |
|---|---|
| Size | 215.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1d847d391336f11b2fbc4e751ac6a8d7746a442cb1d765214184c69f1cc367d6
|
|
BLAKE2b-256 checksum How to use checksums |
a98eb34e543d76f6eef054934b1ec67864595db1643a3acfff0d9f3ef410cb3a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.3
|