目录[-]
一、背景
2025年底,公司计划接入AI大模型做翻译。问题来了:怎么测?
传统接口测试的断言是“相等”,但AI翻译的结果不是唯一的。比如 Hello world 可以翻译成“你好世界”“你好,世界”“世界你好”——语义都对,但无法用 True/False 判断。
传统测试断言失效,是因为我们还在用“相等”的尺子量化“语义”。如果硬套传统断言,这道题就是0分,但是它真的是0分么?
测试组的困境:
-
没有量化标准:凭感觉判断好坏,不同人标准不一
-
无法回归:模型升级后,不知道效果是变好了还是变差了
-
人力成本高:190条用例纯手动测,一个人一天都跑不完
我的目标(建立一套AI翻译测试体系,核心三个层次):
- 可量化:把“翻译得好不好”从主观判决变成客观分数。用5个维度(关键词、BLEU、长度、重复、相似度)加权打分,60分及格、90分优秀——谁来判断都是一个标准。
- 自动化:从用例读取、调用模型、评分到报告生成,全自动执行。190条用例,手动要测半天,自动化跑完只需几分钟。
- 可回归:每次模型调整,一键重跑,对比前后得分和错误场景,判断变好了还是变差了,用数据比较更加可靠。
二、整体架构
测试用例集(Excel) → 翻译器(Ollama/公司API) → 多维评估器 → 综合评分 → HTML报告
| 模块 | 技术栈 | 职责 |
|---|---|---|
| 测试用例管理 | Excel + pandas | 管理原文、参考答案、关键词、领域分类 |
| 翻译器 | Ollama / requests | 调用本地模型或公司API获取翻译结果 |
| 评估器 | Python + 正则 | 5维度打分:关键词、相似度、BLEU、长度比率、重复惩罚 |
| 报告生成 | HTML + CSS | 生成可视化测试报告 |
| 执行入口 | run.py | 串联全流程,支持模型切换 |
关键词(keyword):业务核心术语是否命中(如:发票、附件、机器学习、文献)。翻译可以换说法,但核心术语不能丢。“机器学习”翻成“人工智能”——意思接近,但术语不精准,扣分。
BLEU:语义贴近度,行业标准指标。衡量翻译结果和参考答案在“连续片段”上的匹配程度。“Hello world”翻成“世界你好”——语义对,但语序不对,BLEU分会降低。
长度(length):防止漏译或过度翻译。原文50字,翻译20字→漏译,扣分;翻译150字→过度发挥,扣分。
重复(repeated):检测“车轱辘话”。翻译结果中出现大量重复词或重复短语(如“好的好的好的”),扣分。
相似度(similary):辅助参考,基于共有词比例。当关键词和BLEU都失效时,相似度能提供一个参考值。权重低,不直接决定分数。
三、核心设计
1. 翻译器抽象(支持无缝切换)
class BaseTranslator(ABC):
@abstractmethod
def translate(self, text: str) -> str:
pass
class OllamaClient(BaseTranslator):
def translate(self, text: str) -> str:
prompt = f"英译中,简单回答:{text}"
return self.generate(prompt)
class CompanyTranslator(BaseTranslator):
def translate(self, text: str) -> str:
# 调用公司内部API
response = requests.get(self.base_url, params={...})
return self._parse_response(response)
2. 五维评估指标
| 指标 | 权重 | 作用 |
|---|---|---|
| 关键词命中 | 0.3 | 确保核心业务词汇正确(如“发票”“附件”) |
| BLEU语义 | 0.4 | 行业标准,衡量语义贴近度 |
| 相似度 | 0.05 | 基于共有词的比例,辅助判断 |
| 长度比率 | 0.12 | 防止漏译或过度发挥 |
| 重复惩罚 | 0.08 | 检测“车轱辘话” |
| LLM评价 | 待定 | 调用第三方LLM工具,对原文和翻译后的内容进行比较打分 |
第一部分:关键词命中,只关注核心术语,不处理同义词、语序,语义语序由BLEU和相似度兜底。如果一个模型,连最简单的关键词命中都无法保证,那就有很大缺陷。
@staticmethod
def keyword_score(text, keywords):
"""关键词命中率
比如关键词: ["请", "查收", "附件"]
翻译: "请查收附件" → 3个关键词全命中 → 得分 1.0
翻译: "请查看附件"→ "查收"没命中 → 得分 0.67
"""
if not text or not keywords:
return 0
matched = sum(1 for k in keywords if k in text) # 生成器表达式
return round(matched / len(keywords), 2)
第二部分:BLEU语义
- 分词:list(translation.lower()) 按字符切分,中英文都能处理
- 匹配:set(trans_chars) & set(ref_chars) 集合交,它其实就是将“翻译结果”和“预期结果”做一次比对,找出共同词有多少。
- 长度惩罚:如果翻译结果长度 小于 参考答案长度,比如翻译结果是2个字,参考答案是5个字,评分等于 2/5=0.4,再乘以precision 得到结果,也就是说,翻译越多,分数越低。
@staticmethod
def simple_bleu(translation, reference):
"""完全不用NLTK的简易BLEU
BLEU 是机器翻译行业标准,它的核心思想是:看翻译结果和参考答案有多少"连续片段"是一样的
1-gram(单个字):参考答案: 知、情、同、意、书;;;翻译结果: 知、情、同、意、表
共同字: 知、情、同、意 (4个)————》1-gram得分 = 4/5 = 0.8,另外长度过短也会扣分
2-gram(连续2个字):参考答案:知情、情同、同意、意书;;;翻译结果:知情、情同、同意、意表。
共同字:3个=0。75分,也就是说,n越大,校验的标准越严格。
translation:翻译结果;reference:参考答案
"""
if not translation or not reference:
return 0
# 简单的字符级分词
trans_chars = list(translation.lower())
ref_chars = list(reference.lower())
if not trans_chars or not ref_chars:
return 0
# 计算1-gram匹配率
common = set(trans_chars) & set(ref_chars)
precision = len(common) / len(set(trans_chars)) if trans_chars else 0
# 长度惩罚
if len(trans_chars) < len(ref_chars):
brevity_penalty = len(trans_chars) / len(ref_chars)
precision *= brevity_penalty
return round(precision, 2)
第三部分:相似度,比如现在的“知情同意书”和“知情同意表”,判断翻译后内容和参考答案的相似度(后续优化)
- word1:{"知情","同意","书"}
- word2:{"知情","同意","表"}
- 交集:{"知情","同意"} → 2个
- 并集:{"知情","同意","书","表"} → 4个
- 相似度:2/4 = 0.5 分 (后续修改路线:下载引入公共向量调整分数 SentenceTransformer )
@staticmethod
def similarity_score(text1, text2):
"""
简易文本相似度(基于共有词比例),不懂语意,权重低
参考答案: "知情同意书"
翻译结果: "知情同意表"
→ 共同词: 知情、同意 → 相似度 0.5
"""
words1 = set(re.findall(r'[\w\u4e00-\u9fff]+', text1))
words2 = set(re.findall(r'[\w\u4e00-\u9fff]+', text2))
if not words1 or not words2: return 0
common = words1 & words2
return round(len(common) / len(words1 | words2), 2)
第四部分:长度比率,如果长度和原文长短一致,得1分,否则分数降低,最低0.5分。
@staticmethod
def length_ratio(translation, reference):
"""翻译长度是否合理(防止漏译或过度发挥)
translation:翻译结果;reference:参考答案
"""
if not translation or not reference:
return 0
ratio = len(translation) / len(reference)
# 理想区间0.7-1.5,越接近1分越高,最低分数0.5
return round(1 - min(abs(ratio - 1), 0.5), 2)
第五部分:重复度,对比翻译前和翻译后的词语长度比较。
@staticmethod
def repetition_penalty(translation):
"""检查是否有词语重复(车轱辘话)"""
# 1、如果单词总数少于4个,直接返回1.0分(不惩罚)。因为太短的句子无法判断是否重复,比如‘金融XX,XX金融XX’虽然短,但不一定是车轱辘话
# 待优化:对中文识别不好,中文词没有空格限制,依赖空格分词
words = translation.split()
if len(words) < 4:
return 1.0
# 2、set()方法指保留唯一词,比如:【"好的","好的","好的"】,set后变成一个【"好的"】,相除,参考:1/3=0.33分
unique_ratio = len(set(words)) / len(words)
return round(unique_ratio, 2)
第六部分:LLM评分,调用第三方(可内置)模型工具进行评分。
# 在 SimpleEvaluator 类里新增这个方法
def llm_evaluate(self, translation, source, reference):
"""用大模型评估翻译质量,返回分数和说明"""
if not translation or not source:
return {"score": 0, "explanation": "输入为空"}
# 修改后的 prompt - 要求同时返回分数和说明
prompt = f"""你是一个专业的翻译评估专家。请评估以下英译中翻译的质量。
原文:{source}
翻译结果:{translation}
参考答案:{reference}
请从准确性、流畅性、完整性三个方面综合打分,并给出简短评估说明。
请按以下格式返回:
分数:[0-1之间的小数,如0.85]
说明:[你的评估说明,20字左右]
"""
try:
response = requests.post(
"http://localhost:11434/api/generate",
json={
"model": "qwen2.5:0.5b",
"prompt": prompt,
"stream": False,
"temperature": 0
},
timeout=30
)
if response.status_code == 200:
result = response.json().get("response", "").strip()
# print(f"🔍 LLM返回原始结果: {result}") # 调试用
# 解析分数和说明
score = 0
explanation = "解析失败"
def evaluate_case(self, translation, reference, keywords):
scores = {
"keyword": self.keyword_score(translation, keywords),
"bleu": self.simple_bleu(translation, reference),
"similarity": self.similarity_score(translation, reference),
"length_ratio": self.length_ratio(translation, reference),
"repetition": self.repetition_penalty(translation),
}
final_score = sum(scores[k] * weights[k] for k in scores)
return {"scores": scores, "final_score": final_score, "passed": final_score >= 0.5}
3. 测试用例集设计
Excel管理,按领域分类,便于分析模型短板:
| ID范围 | 领域 | 用例数 |
|---|---|---|
| 101-120 | 商业与贸易 | 20 |
| 201-220 | 科技与教育 | 20 |
| 301-320 | 医疗与法律 | 20 |
| 401-420 | 旅游与生活 | 20 |
| 501-520 | 媒体与娱乐 | 20 |
| 601-620 | 政府与公共事务 | 20 |
| 701-720 | 其他 | 20 |
| 801-830 | 异常/边界 | 30 |
| 831-881 | 长文本 | 30+ |
共计 190+用例,覆盖日常、专业术语、长文本、异常输入等。
商业与贸易:
Shipping address confirmation → 收货地址确认 → 【"收货"、"地址"、"确认"】
Non-disclosure agreement → 保密协议 → 【"保密"、"协议"】
Please find attached the pro forma invoice for your reference. The total amount due is USD 5,000, payable within 60 days from the invoice date. Payment shall be made via wire transfer to the bank account designated below. → 附件为形式发票,供您参考。应付总额为5000美元,请于发票日期起60日内支付。付款请通过电汇方式汇至以下指定银行账户。→ ["形式发票","应付总额","电汇","60日"]
科技与教育:
Machine learning → 机器学习 → 【"机器"、"学习"】
4. 报告生成
自动生成HTML报告,包含:
-
总用例数、通过数、通过率、综合得分
-
每条用例的原文、翻译结果、各项得分
-
失败用例高亮显示
四、踩坑记录
坑1:Ollama服务启动慢
os.system("start ollama serve") 启动后立即发送请求会失败。
解决:添加健康检查,轮询直到服务就绪。
def check_ollama_ready():
for i in range(10):
try:
requests.get("http://localhost:11434/api/tags", timeout=5)
return True
except:
time.sleep(2)
return False
坑2:BLEU需要NLTK
本想用标准BLEU,但NLTK依赖太重,且内网无法下载语料库。
解决:自己实现简化版BLEU(基于字符级n-gram匹配)。
坑3:公司翻译API返回SSE格式
API返回的是text/event-stream格式,需要解析workflow_finished事件。
解决:用正则匹配最后一个workflow_finished事件,提取outputs.answer字段。
坑4:关键词匹配太“死”
“请提供发票”匹配了“请”“提供”“发票”得满分,但“发票提供请”也是满分——语序错了。
解决:引入BLEU指标,降低关键词权重,让语义和字面共同作用。
五、发现的典型缺陷
通过这套框架,发现了模型的多个问题:
| 问题类型 | 示例 | 表现 |
|---|---|---|
| 过度翻译 | Best regards |
翻译成30多个字,而非“此致敬礼” |
| 数字转换错误 | 6210 000 |
译成“六二一零零零”,而非保留数字格式 |
| 专业术语错误 | Machine learning |
翻译结果不是“机器学习” |
| 俚语直译 | give me a hand |
“给我一只手”,而非“帮我一下” |
六、优化方向
-
权重动态化:专业文档提高关键词权重,文学翻译提高BLEU权重
-
引入同义词:关键词匹配从“字面匹配”升级为“语义匹配”
-
硬件升级:7b模型推理慢(30秒+),换14b/32b+GPU可大幅提升
-
更多领域:补充法律合同、医疗报告等专业领域用例
七、总结
这套框架的核心价值不在于“写代码”,而在于:
-
建立了量化标准:AI测试不再是“凭感觉”
-
可回归可演进:模型升级后,跑一次报告就知道效果变化
-
方法论可复用:这套框架后续被复用到音生文、图生文、RAG测试中
框架不是一次性脚本,而是后续所有AI测试项目的基石。