把 2,082 个测试守着的 Python 语音智能体搬进 C#,怎么证明「搬对了」 - 张善友

把 2,082 个测试守着的 Python 语音智能体搬进 C#,怎么证明「搬对了」 - 张善友

💡 原文中文,约3700字,阅读约需9分钟。
📝

内容提要

文章介绍 speech-to-speech-dotnet 如何以“行为等价重写”方式将 Python 语音项目移植到 C#。核心不在四万行代码,而在四本账:锁定上游版本与文件指纹;将上游测试取舍为可执行基线;用探针对拍生成差分语料,发现单测遗漏的真实缺陷;用差异账本记录已解决与刻意接受的差异。该方法强调证据链、诚实标注未覆盖风险,适用于任何新旧系统替换。

🔎

延伸解读

“行为等价”的验收标准如何落地

文章强调,移植的验收标准是“可观察行为与上游一致,而不是源码相似”。为此,项目先锁定上游 commit 和文件指纹,再通过探针对拍生成差分语料,逐字节比对两个实现的输出。这提示读者:判断重写是否成功,不能只看代码结构或单元测试通过率,而要有可复现的参照物和自动化比对证据。

单元测试全绿不等于行为正确

文中提到,差分语料抓出了七个真实缺陷,共同点是“单元测试全绿,对拍全红”。原因是单测用例由人编写,可能带入对行为的错误理解。项目因此规定语料只能由参照实现生成,不能手写。这对任何重写项目都有借鉴意义:人工设计的测试覆盖有限,需要引入独立于实现者的验证手段。

差异账本:区分“已解决”与“刻意接受”

differences.jsonl 记录了 75 条差异,其中已解决 58 条、已接受 17 条、缺口 0。文章特别说明,“已接受”是已知且刻意接受的差异,不是尚未完成。每条差异都包含是什么、为什么、怎么决定、拿什么测试锁、状态五个要素。这种记账方式让读者能清楚看到哪些行为差异是主动决策,哪些是遗留风险。

对“锁不住的东西”保持诚实

项目对无法精确比对的部分同样做了说明:音频层不做样本级比对,只锁采样率、声道、位深、段数、时长误差带;真人麦克风听感、Linux/macOS 真机运行等未覆盖项被单列成表,并写明为何进不了 CI、风险多大。这种“未覆盖不等于跳过”的态度,比只报告通过率更能帮助读者判断项目的真实成熟度。

❓

Q&A

speech-to-speech-dotnet 项目最核心的价值是什么?

最核心的价值不是四万行 C# 代码,而是一套回答「两个不同语言的程序,凭什么说它们行为一样」的完整方法,即通过四本账(参照锁定、基线测试、差分语料、差异账本)构建证据链,证明行为等价。

如何确保移植时参照的上游版本是固定的?

通过 porting/00-reference.md 将上游锁定在特定 commit(0835002),并用脚本对上游整棵树计算指纹,385 个文件的哈希存入 reference-lock.json,使「上游行为」成为可检查的文件。

为什么不能直接照抄上游的测试用例?

因为单元测试用例是人写的,会带上作者对行为的理解,如果理解本身有误,测试全绿也没用。因此项目对 2,082 个测试函数逐类取舍,只保留跨语言边界上可复现的语义(41 个文件、518 个方法、713 例),并注明不搬的原因。

差分语料是如何生成的?它发现了什么单测遗漏的问题?

差分语料通过探针对拍生成:Python 探针调上游代码,C# 探针调本实现,对同一批输入输出同形 JSON 并逐字节比对。六对探针共 115 条语料,抓出了七个单元测试全绿但真实存在的缺陷,例如 JavaScriptEncoder 转义、UnsafeRelaxedJsonEscaping 使用等。

differences.jsonl 账本中「已接受」的差异是什么意思?

「已接受」指已知且刻意接受的差异,不是尚未完成。每条差异记录五个要素:是什么、为什么、怎么决定、拿什么测试锁、状态。账本共 75 条差异,已解决 58、已接受 17、缺口 0。

对于无法精确锁定的部分(如音频样本),项目是如何处理的?

音频层不做样本级比对,因为模型推理受浮点累加顺序影响,两次运行结果不同。只锁定采样率、声道、位深、段数、时长误差带这五样,不锁样本值。同时单列「未覆盖不等于跳过」表,诚实标注未系统测试的风险(如真人听感、Linux/macOS 真机未实跑)。

🏷️

标签

➡️

继续阅读