☰
DSH-Sharp:用C#/.NET构建的AI提示词回归测试框架
2026/10/26 4:25:52 网站建设 项目流程

做 AI 应用开发大半年之后,我大部分时间都花在了一件事上:改提示词。每次改完都觉得效果更好了,结果回归测试一跑,之前能通过的对话场景翻车了。DeepSeek-Harness-Sharp(简称 DSH-Sharp)就是为这个痛点做的——一套用 C#/.NET 写的提示词测试框架,把模型调用、用例组织、结果断言和回归报告串成一条流水线。它的目标不是替代你写测试用例,而是把"评估模型输出质量"这件事变成可以重复执行的工程流程。如果你正在写对接 DeepSeek 的对话机器人、RAG 管道、或者任何基于模型的业务逻辑,这篇文章值得读完。

这套工具的核心思路,实际上借鉴了传统软件测试里很成熟的一个概念:Test Harness(测试夹具/测试台)。在嵌入式开发和硬件测试里,Harness 指的是把被测设备接上电源、传感器、信号发生器的那套物理连接和驱动逻辑。到了 AI 应用这边,模型就相当于"被测设备",而 DSH-Sharp 就是那套把它接进测试流程的装置——定义输入提示词、记录模型输出、比较预期行为、输出评估结果。下面我从设计思路到实操细节,完整拆解一遍。

1. DSH-Sharp 是什么:一个给 AI 应用开发者的测试工作台

1.1 从"改一次提示词,整个人都不好了"说起

如果你只是本地跑着玩,模型输出好不好,肉眼看一眼就够了。可一旦进了正经项目,情况完全不一样:业务方要看效果对比,测试同学要能重复执行,你自己也需要知道"这次改动到底优化了什么、破坏了什么"。传统单元测试有 JUnit、XUnit 那套成熟的断言和报告体系,但那是给确定性的代码逻辑用的。模型的输出是概率性的——同样输入,温度一浮动,输出就不一样;场景一换,之前调好的格式又乱了。

DSH-Sharp 做的事情,就是把"提示词->模型输出->结果评估"这个链路固化成可重复的测试用例。它不关心你的提示词写得好不好,它只做一件事:把提示词和配套的评估规则保存下来,每次运行都重新调一遍模型,然后告诉你这次哪些用例过了、哪些挂了、挂的点和上次有什么不一样。

这样带来的直接好处是:提示词再也不是散落在聊天窗口和代码里的"玄学",而是一份版本可控、可回归、可协作的测试资产。我在实际项目里是把提示词模板和测试用例一起提交进代码仓库的,每次改动都必须保证全部用例通过才能合入。效果非常明显——项目后期几乎没再出现过"之前能行的场景突然不行了"这种问题。

1.2 DSH-Sharp 在技术栈中的位置

先说清楚,DSH-Sharp 不是模型调用库。它不会帮你拼 HTTP 请求,也不会替你处理流式输出的解析,那部分是 DeepSeek 官方 SDK 或者你自研的 Client 层该干的活。DSH-Sharp 的关注点更往上一层:测试编排。

一个典型的使用方式是这样:

  • 你已经在项目里封装了DeepSeekClient,负责鉴权、请求、解析响应。
  • DSH-Sharp 负责的是:加载测试用例定义(包含提示词、参数、期望行为)、调用你的 Client 执行对话、把返回结果交给评估器(Evaluator)打分、收集全部结果并输出报告。

为了不绑定具体实现,DSH-Sharp 里定义了一套接口约定。你只需要把自定义的模型调用逻辑塞进一个实现类里,剩下的用例调度、超时控制、结果汇总都是框架帮你处理。这一点和平常写 XUnit 测试很像——你只关心测试本身,运行器、报告器都是现成的。

如果你问我它和 XUnit、NUnit 是什么关系:可以理解为 DSH-Sharp 去掉了"单元测试"的针对性,不改普通测试框架也能跑,但如果搭配使用,效果更好。我的实操经验是,把 DSH-Sharp 的用例通过一个适配层包装成 NUnit 的[Test]方法,这样整个团队的既有测试基建不用动,还能一起进 CI。

1.3 适合谁用、解决什么问题

适合的人群我总结为三类:

  • 正在用 DeepSeek 做业务应用的后端开发,尤其是 C# 技术栈的团队。市面上这类工具大多以 Python 为主,C# 生态里能找到的轻量框架非常少。
  • 需要给 AI 功能建立回归测试体系的测试工程师。DSH-Sharp 的用例组织方式和传统测试接近,上手成本低。
  • 提示词工程师 / AI 应用架构师,把提示词当作代码来管理,需要可量化的效果基准。

不适合的人群也很明确:如果你只是用模型做一次性实验,不需要用专门的框架去管理。另外,希望框架帮你"自动优化提示词"的也不用指望,它只告诉你效果好坏,优化还是要靠人。

2. 架构设计与技术选型:为什么用 C#,为什么叫 Harness

2.1 "Harness"在测试行业里的原意

先解释一下命名。"Harness"这个词在软件测试里对应的是"测试台"或"测试夹具",它指的不是某几个测试用例本身,而是承载这些用例并驱动被测对象运行的那套基础设施。在嵌入式领域,Harness 通常是一块定制电路板,把待测芯片的引脚引出到标准接口上,让测试仪器可以稳定地触碰每一个信号。

这个类比放到 AI 测试上非常贴切。模型服务就是那颗"芯片",提示词是输入信号,输出是回传信号,而 DSH-Sharp 就是把这一切接到你测试体系里的那根"电路板"。它固定住了测试环境的不稳定因素:网络超时怎么处理、并发请求怎么排队、输出格式怎么归一化、评分规则怎么统一。没有这层固定,你测得越频繁,得到的噪声越多,结果越不可信。

所以这个工具取名为 Harness,表达的是一种工程姿态:把模型当作一个可以被反复测量的"被测系统",而不是一个只能靠感觉调试的黑盒。这也是我写这个框架时始终坚持的理念——AI 应用的质量保障,首先要把模型放上测试台。

2.2 技术选型的实际考量

我选 C#/.NET 作为实现语言,有几个很实际的考量,不完全是个人偏好。

第一,.NET 的异步生态非常成熟。模型调用天然是 IO 密集型操作,一次对话可能动辄几秒到几十秒。DSH-Sharp 里大量用async/await处理并发调用,Task.WhenAll并发跑几十个用例时,资源占用非常可控。如果让我用 Java 写,也能写,但协程和内存占用管理上没这么顺手。

第二,强类型在"测试用例定义"这种场景下特别有价值。提示词模板、模型参数、评估规则都可以用record类型定义得一清二楚,配合编译期检查,很多配置错误在启动前就暴露了。这在 Python 动态语言里做不到。

第三,企业环境的兼容性。大量传统企业现有的后端基础设施是 Windows + SQL Server + .NET 这套组合,让团队引入一个 Python 测试框架,成本不低。DSH-Sharp 可以直接嵌进现有的测试工程,NuGet 包一发,依赖冲突几乎为零。

实现上的一个关键设计是"评估器"抽象。模型输出是自然语言,你不能像断言整数那样做相等比较,所以评估器必须可插拔。框架内置了几种常见的评估策略——关键词匹配、JSON Schema 校验、自定义规则、以及"让另一个模型打分",你可以在用例级别指定用哪种策略组合。这个设计是整个框架的灵魂,后面实操部分我会详细展开。

2.3 源码项目结构速览

如果你拿到源码,会发现项目结构非常直白:

DSH.Sharp/ ├── DSH.Sharp.Core/ # 核心抽象:评测接口、用例模型、结果模型 ├── DSH.Sharp.Engine/ # 测试引擎:用例加载、调度执行、超时控制 ├── DSH.Sharp.Evaluators/ # 内置评估器:关键词、JSON 校验、模型评分 ├── DSH.Sharp.Reporting/ # 结果汇总与报告输出(Markdown / JSON) ├── DSH.Sharp.Console/ # 命令行入口,方便本地跑和 CI 集成 └── DSH.Sharp.Examples/ # 完整示例项目,覆盖常见的三种用法

其中Core里的三个核心接口你需要重点关注,因为扩展点都在这:

  • IModelConnector:模型调用抽象。实现它来告知框架"如何发起一次对话并拿到文本输出"。
  • ITestEvaluator:评估抽象。输入是模型原始输出和期望规则,输出是EvaluationVerdict(通过/失败/待人工确认)。
  • IReportSink:报告输出抽象。把结果写到控制台、文件、还是你团队内部的测试管理平台,由你决定。

框架的调度逻辑是:Engine读取一组用例定义,逐个调用IModelConnector得到模型输出,然后把输出连同用例上的评估规则交给ITestEvaluator,最后把所有 verdict 汇总给IReportSink。整个流程是串行还是并行,由Engine的配置决定。

3. 核心功能实操:从零搭一套提示词回归测试

3.1 配置模型客户端:对接 API 的关键参数

首先你要在测试项目里引用DSH.Sharp.Core,并且把你已经封装好的 DeepSeek 调用逻辑,接进框架。以最常见的 OpenAISDK 兼容模式为例,我习惯用一个简单的实现类:

public class DeepSeekConnector : IModelConnector { private readonly HttpClient _httpClient; private readonly string _apiKey; private readonly string _modelName; public DeepSeekConnector(string apiKey, string modelName) { _apiKey = apiKey; _modelName = modelName; _httpClient = new HttpClient { BaseAddress = new Uri("https://api.deepseek.com/v1"), Timeout = TimeSpan.FromSeconds(60) // 重要:下面会细说 }; } public async Task<ModelResponse> CompleteAsync( ModelRequest request, CancellationToken ct = default) { // 这是最简实现:把 DSH.Sharp 的消息格式翻译为 SDK 的消息格式 var messages = request.Messages .Select(m => new ChatMessage(m.Role, m.Content)) .ToList(); var chatClient = new ChatClient(_modelName, _apiKey, _httpClient); var response = await chatClient.CompleteAsync( new ChatCompletionOptions() { Messages = messages, Temperature = request.Temperature, MaxOutputTokens = request.MaxTokens }, ct); return new ModelResponse(response.Text); } }

有三个参数我每次都要提醒团队注意。第一个是Timeout。模型接口的响应时间方差极大,高峰期一个复杂推理请求可能十几秒才返回,60 秒起步,90 秒不嫌多。如果沿用默认的 100 秒反而最安全。第二个是温度参数。测试用例里Temperature建议固定为较低值(0.1 到 0.3),提高可重复性。第三个是MaxOutputTokens,一定要给上限,不然模型无限输出会直接拖爆测试时长和 token 成本。

注意:ApiKey千万别硬编码在代码里走版本库。我用环境变量DEEPSEEK_API_KEY,在 CI 里用 Secret 注入,本地开发用.env文件配合dotnet user-secrets。泄露一次 API Key 的教训,够你受的。

3.2 用属性标记和组织测试用例:像写普通单元测试一样写 AI 用例

DSH-Sharp 提供了一套声明式属性,让用例组织非常接近 XUnit 风格。每个用例包含三个部分:提示词内容、模型参数、评估规则。看一个完整示例:

public class CustomerSupportTests { private static IModelConnector CreateConnector() => new DeepSeekConnector( Environment.GetEnvironmentVariable("DEEPSEEK_API_KEY"), modelName: "deepseek-chat"); [DshTest( Id = "CS-001", Description = "用户询问退款政策时,客服助手必须提到 30 天期限")] [Prompt("你是某电商平台的客服助手。请用两句话以内,回答用户问题:我买了东西想退款,可以吗?")] [Temperature(0.2)] [MaxTokens(200)] [Evaluator(typeof(ContainsTextEvaluator), "30 天")] public async Task RefundPolicy_ShouldMentionDeadline() { var connector = CreateConnector(); var engine = new DshTestRunner(connector); var result = await engine.RunSingleAsync( testCase: TestCaseDescriptor.FromCurrentMethod()); Assert.True(result.Verdict == EvaluationVerdict.Passed, result.Summary); } }

这里有个扩展点我很喜欢:测试用例的方法名本身不需要包含提示词内容,提示词通过[Prompt]特性挂到方法上。这样测试报告里可以直接显示"正在执行的提示词是什么",而不是只看一个方法名。

[Evaluator]特性接收两个参数:评估器的类型和构造参数。框架会在运行时反射创建实例,所以扩展自定义评估器很简单——只需要实现ITestEvaluator接口,然后把类型名填进特性里即可。

如果你要组织的用例非常多,比如一个客服系统有 200 个多轮对话场景,建议按业务模块拆文件,一个文件内用[DshTestCategory("退款流程")]这类 Category 特性分组。运行的时候可以按 Category 过滤,只跑和当前改动的模块相关的用例。这个过滤机制在 CI 流水线里极其有用。

3.3 断言与评分:怎么判断一句模型输出"对不对"

这是整个框架里最难也最重要的部分。传统断言的Assert.Equal(3, actual)在这里完全失效。模型输出的正确性判断,我实践下来至少有四种策略,DSH-Sharp 把它们都做成了可组合的评估器。

第一种是关键词/正则匹配。比如客服场景里要求"必须出现'30 天'"。这种评估简单粗暴,适合"必须提到核心信息"的场景。缺点是容易误判:模型可能说"31 天",也可能说"三十天",这种同义表达就匹配不上。所以我又写了一个RegexEvaluator,把"30 天|三十天|一个月内"这组正则塞进去,效果立刻好了不少。

第二种是 JSON Schema 规则校验。如果你的提示词要求模型输出结构化 JSON(比如抽取实体),那么评估模型输出是否正确的最佳方式,是直接校验 JSON 结构。内置的JsonSchemaEvaluator会把模型输出先做规范化(去掉 Markdown 代码块标记),再按 Schema 校验。这个方式确定性最强,我强烈建议所有结构化输出场景都用。

第三种是"用模型评模型"。当输出是开放性内容时,关键词和 Schema 都无能为力。比如"给用户写一段安抚情绪的话,语气要温暖自然"。这时候我让一个评分模型按 1-5 分打分。具体做法是:拿一个预设的评分提示词模板,把模型原始输出嵌入进去,让评分模型输出一个包含分数和理由的 JSON,DSH-Sharp 解析后对比阈值判断通过与否。成本高,但应对开放性问题几乎是唯一可选的路。

第四种是语义相似度。用 Embedding 把模型输出和"标准答案"的表意向量做余弦相似度,设定一个阈值。这个方案对"意思对了但表述完全不同"的场景特别有效。

你在实践中不要只依赖一种评估器。我的习惯是:核心要求用 JSON Schema 或者关键词做硬校验,开放要求用模型评分或相似度做软校验,软校验结果不进 Pass/Fail 的硬判定,而是作为"警告"标记。这样既有硬性红线,又不会因为语气问题误杀整个用例。

3.4 跑起来:命令行运行、报告输出与 CI 集成

本地运行最简单的方式是直接用 CLI 工具:

dotnet dsh run --project ./tests/Demo.Tests.csproj \ --category "退款流程" \ --report-format markdown \ --output ./artifacts/report.md

--category参数按分类过滤用例;--report-format支持markdown和json两种。我日常调试用 markdown,CI 里用 json 方便后续脚本解析统计结果。

一份 markdown 报告大概长这样:

# DSH-Sharp 测试报告 运行时间: 2025-06-15 14:32:08 模型: deepseek-chat 用例总数: 18 | 通过: 15 | 失败: 2 | 警告: 1 | 跳过: 0 耗时: 3分24秒 ## 失败用例 | ID | 名称 | 评估器 | 摘要 | |----|------|--------|------| | CS-003 | 退货流程引导 | JsonSchemaEvaluator | 输出缺少 "return_id" 字段 | | CS-012 | 安抚语气检查 | ModelScoringEvaluator | 情感温度得分 2.1,低于阈值 3.0 |

在 CI 里集成时,我直接让构建服务器执行上面的 CLI 命令,然后让任务在存在失败用例时以非零退出码结束,这样 Jenkins 或 GitHub Actions 就能自动标记构建失败:

dotnet dsh run --project ./tests/Demo.Tests.csproj \ --fail-on-error \ --report-format json \ --output ./artifacts/report.json

加不加--fail-on-error区别在于:我平时开发阶段不加,只输出警告;CI 合入主干时加,让任何用例失败都阻断合入。

4. 实战中反复踩过的坑与排查记录

4.1 超时与重试:大模型接口不能按普通 HTTP 的思维处理

我一开始给HttpClient设了 30 秒超时,结果 CI 里三天两头飘红。排查后发现,模型在高峰期的首字延迟(TTFT)可能就要 20 多秒,加上生成长文本的时间,整个请求超过 60 秒很正常。后来我把超时调到 90 秒,并且加了一级重试机制:对 429(限流)和 5xx(服务端过载)做指数退避重试,最多重试 3 次。这里有段核心配置我直接贴出来:

public async Task<ModelResponse> CompleteWithRetryAsync( ModelRequest request, CancellationToken ct) { var delay = TimeSpan.FromSeconds(2); for (int attempt = 1; attempt <= 3; attempt++) { try { return await _connector.CompleteAsync(request, ct); } catch (HttpRequestException ex) when ( ex.StatusCode == HttpStatusCode.TooManyRequests || ex.StatusCode == HttpStatusCode.InternalServerError) { if (attempt == 3) throw; await Task.Delay(delay, ct); delay *= 2; // 2s -> 4s -> 8s } } throw new InvalidOperationException("无法完成请求"); }

重试逻辑这里有两个细节容易踩坑。第一个是重试必须配指数退避,不要固定间隔重试。理由是:429 通常是因为触发了服务端的并发限制,你固定间隔重试会让服务端雪崩。第二个是重试一定要带上CancellationToken,否则测试被 Ctrl+C 中断时,后台线程还在偷偷跑请求,CI 里的构建任务就一直"挂起"到超时。

另一个经验是,如果你跑 100 个用例并发,建议把并发度限制在 5 左右。模型 API 的并发限制通常比较严格,我记得 API 文档明确限制了每分钟请求数(RPM),SemaphoreSlim(5)就能稳定绕开限流。

4.2 温度参数对断言稳定性的影响

这是我踩得最深的一个坑。有一次我把一批用例的Temperature设成了 0.7,结果同一个用例连续跑三次,两次过、一次挂。排查下来发现,失败的场景是模型输出里的解释顺序变了,导致关键词评估器的正则刚好匹配不上。

从此我定了一条规矩:回归测试的用例,温度一律固定到 0.1~0.3 之间。需要高创造力的场景单独建"探索性测试",不放进回归集。探索性测试的目标是让模型发散,温度给到 0.8 甚至 1.0,但它的结果不参与 CI 判定,只作为人工参考。

温度对模型评分评估器的影响更大。用一个模型去评另一个模型的输出,评分模型的温度如果太高,会给同样的文本打出 4 分又打出 3 分。评分类评估器我直接固定温度 0,确保评分行为尽量稳定。

4.3 模型输出格式不稳定的处理:强制 JSON 也算招

模型在输出 JSON 时,经常会在前后加上 ```json 代码块标记,或者加一句"好的,以下是提取结果"之类的废话。我第一次写 JSON Schema 评估器时,直接拿原始文本JsonDocument.Parse,报错报得我怀疑人生。

解决这个问题我用了两招。第一招,DeepSeek 官方支持response_format参数,你把ResponseFormat设为"json_object"模型就会尽量只输出纯 JSON。DSH-Sharp 的ModelRequest里已经预留了这个字段,直接传即可。第二招,无论如何都要做一次输出清理:去掉三引号代码块、去掉前导的"好的/以下是"这类废话、定位到第一个{和最后一个}做截取。

我把这段清理逻辑写进了内置工具类OutputNormalizer,它的核心就几行:

public static string ExtractJson(string raw) { var trimmed = raw.Trim(); // 去掉 ```json ... ``` 包裹 if (trimmed.StartsWith("```")) { trimmed = trimmed.Substring(trimmed.IndexOf('\n') + 1); trimmed = trimmed.Substring(0, trimmed.LastIndexOf("```")); } // 截取第一对花括号 var start = trimmed.IndexOf('{'); var end = trimmed.LastIndexOf('}'); return start >= 0 && end > start ? trimmed.Substring(start, end - start + 1) : trimmed; }

实现简单,但效果立竿见影。格式化稳定性问题解决之后,至少 80% 的"模型输出格式不对"类问题都不会再出现。剩下的 20% 属于模型真的没按指令输出 JSON,那就应该从提示词本身去补强,而不是在测试框架里兜底。

4.4 成本控制:怎么避免测试跑一夜烧掉大量 token

模型测试是有真金白银成本的。我见过有人把 200 个用例全量并发跑,一次 CI 跑掉几百万 token,账单出来吓一跳。这里给出我控制成本的几条土办法。

第一,精确控制MaxTokens。客服场景的简答用例,MaxTokens给 200 就够,不要留太多余量。输出 token 上限越高,模型越容易发散,浪费越大。

第二,按 Category 分片运行。改动提示词模板时,只跑和该模板相关的用例;全量回归只在主干合并前跑一次。我通常会这么安排流水线阶段:PR 阶段跑"影响模块"用例,主干阶段跑全量。

第三,对评估器做"短路"逻辑。比如,先用关键词评估器做快速失败,如果模型输出里连核心关键词都没有,直接判失败,不继续调用评分模型。这样能省掉大量高成本的模型评分调用:

public async Task<EvaluationVerdict> EvaluateAsync( ModelResponse output, EvaluationRule rule) { var quickCheck = new ContainsTextEvaluator(rule.RequiredKeywords); var quickVerdict = await quickCheck.EvaluateAsync(output, rule); if (quickVerdict == EvaluationVerdict.Failed) return quickVerdict; // 短路,不花冤枉钱调评分模型 var deepCheck = new ModelScoringEvaluator(rule.ScoreThreshold); return await deepCheck.EvaluateAsync(output, rule); }

第四,每次跑完批量用例,把PromptTokens和CompletionTokens汇总写进报告。DSH-Sharp 的ModelResponse里保留了 token 用量元数据,报告器会按用例维度统计,再按运行批次汇总。我把"每批次测试的 token 成本估算"直接加到报告的末尾,让团队对测试的代价有感知。没过多久,大家写测试用例时就自觉从简了。

4.5 常见问题速查表

整理了一份我在不同团队里被问过最多的问题,按症状、原因、解法列在下面:

症状常见原因解决思路
用例一直超时模型响应慢,HttpClient 超时设置过短超时调到 90 秒以上,增加指数退避重试
同样的用例结果不稳定Temperature 设置过高回归用例固定 0.1~0.3,评分模型设 0
JSON 解析经常失败模型输出带代码块或废话用response_format=json_object,再用OutputNormalizer清理
大量 429 限流并发度设置过高SemaphoreSlim限制并发到 5,配合退避重试
token 成本过高全量用例频繁执行、MaxTokens 过大按 Category 分片、控制 MaxTokens、评估器短路
报告显示通过但实际效果差只用关键词评估器,语义没校验叠加 Schema 校验或模型评分做软校验

写到这里,再分享两条我个人的习惯,算是给大家的额外建议。

第一,把提示词模板也纳入版本管理。DSH-Sharp 的用例定义如果能把提示词模板以.dsh-template文件的形式放在测试工程的Templates/目录下,那么提示词的每次修改都会暴露在 Code Review 里。这个习惯让我避免过很多次"提示词被同事顺手改了一个字,效果翻天覆地"的诡异问题。

第二,用"输出快照"做差异对比。我扩展了一个SnapshotReporter,跑完用例后把模型输出存一份快照文件,下次运行做 line-by-line diff。如果某个用例没有改动,但输出和上次差异巨大,通常说明模型端行为变了,值得警惕。这个"无变化输入下的输出稳定性"指标,其实比单个用例的 Pass/Fail 更能反映线上真实体验的稳定性。

DSH-Sharp 目前在我这边已经跑了几个月的回归,最大的感受是:AI 应用的测试终于不再是"靠感觉",而是一个可以每天重复、能够发现问题、也能源源不断沉淀资产的事情。工具本身不复杂,复杂的是坚持把"提示词作为代码"这个理念落到工程流程里。框架只是把这条路铺平了,剩下的路还是要你自己走。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询