之前做 AI 应用联调时,最头疼的不是模型效果不理想,而是缺少一套系统化的测试评估工具。临时脚本写了一个又一个,用例管理靠文件夹堆,结果对比靠肉眼盯,跑完一轮连“哪个版本更稳”都回答不出来。后来我把这套流程整理成了 Harness 方式——一个轻量、可扩展、围绕 DeepSeek 模型 API 的自动化测试与评估框架。本文把这套完整方案拆解成可落地的教程,包含安装配置、核心模块设计、完整代码和排错清单,无论你是在做应用开发、Prompt 调优,还是模型选型对比,都可以直接用。
在正式进入代码之前,先说明一点:本文所讲的 Harness 不是某个特定的官方软件包名称,而是一类“大模型测试评估工程化框架”的统称。下面我将带你从头搭建一套属于自己的 DeepSeek Harness。
1. 什么是 DeepSeek Harness
1.1 Harness 在 LLM 测试中的含义
Harness 这个词在软件工程里一般理解为“测试脚手架”或“测试夹具”。在传统后端开发中,它指的是驱动被测代码运行、收集运行结果、判定测试是否通过的一套基础设施。
到了大模型时代,这套概念被自然地迁移到了 LLM 评估场景中。你的被测对象不再是一个函数、一个接口,而是一个通过 API 暴露出来的大语言模型。此时 Harness 需要做几件事:
- 管理测试用例,包括输入、期望输出、判定规则。
- 调用模型接口,将用例中的输入发送给模型。
- 捕获模型输出,记录耗时、Token 消耗等运行指标。
- 执行断言评估,判断模型输出是否符合预期。
- 生成测试报告,以结构化方式展示测试结果。
所以,DeepSeek Harness 本质上就是“面向 DeepSeek 模型 API 的自动化测试与评估框架”。它解决的核心痛点是:当模型版本升级、Prompt 调整、参数变化时,如何能够快速、可重复、低主观性地判断“变化是好是坏”。
1.2 为什么需要专门一套 Harness
用临时脚本当然也能调 API、打日志。但一旦用例数量超过几十条,或者测试对象不只一个 Prompt 时,临时脚本就会暴露出几个明显问题:
- 用例没有统一格式,散落在代码里难以维护。
- 断言逻辑和业务代码混在一起,改一个标准就要改多处。
- 没有累计对比,上一轮和这一轮的结果差异无法量化。
- 没有输出结构化报告,回归测试形同虚设。
Harness 的核心价值就是把“测什么、怎么测、怎么判、怎么报”四件事解耦,让测试用例可以单独维护,评估逻辑可以复用,报告可以自动化产出。
1.3 本文方案的技术边界
需要说明的是,目前公开渠道中并不存在一个名为“Deepseek Harness”的官方统一框架,不同团队会根据自身需求实现不同的评估工具。为了不误导读者,本文采用纯 Python 实现,基于 DeepSeek 官方提供的 OpenAI 兼容接口来构建一套最小可用的 Harness 框架。
这种实现方式的优势在于:
- 不依赖特定封闭框架,代码逻辑完全可控。
- 只要替换 Base URL 和模型名称,就能兼容其他 OpenAI 兼容模型服务。
- 结构清晰,方便你按项目需要扩展新的评估维度。
2. 环境准备与版本说明
2.1 运行环境
本文示例以常见的 Python 3.10 环境为例,操作系统不限,Windows / macOS / Linux 都可以运行。你需要先确认本机已经安装 Python 3.10 或以上版本:
python --version如果提示找不到 python,可能是 Python 未安装或未加入环境变量,需要先自行安装。
2.2 安装依赖
本文的 Harness 核心依赖有两个:
- requests:用于直接调用 HTTP API,避免引入过重的 SDK。
- openai:DeepSeek 官方文档推荐使用 OpenAI SDK 兼容方式接入,用它更省事。
安装命令如下:
pip install requests openai如果你的网络环境使用国内镜像源,可以加上镜像参数加快下载:
pip install requests openai -i https://pypi.tuna.tsinghua.edu.cn/simple2.3 API Key 与访问配置
使用 DeepSeek API 需要一个有效的 API Key。你需要到 DeepSeek 开放平台注册账号,然后在控制台创建 API Key。
创建完成后,建议把 Key 配置为环境变量,不要直接写在代码里。在 Linux / macOS 下:
export DEEPSEEK_API_KEY="sk-xxxx"在 Windows PowerShell 下:
$env:DEEPSEEK_API_KEY="sk-xxxx"在 Python 代码中通过 os.environ 读取:
import os api_key = os.environ.get("DEEPSEEK_API_KEY") if not api_key: raise RuntimeError("请先设置 DEEPSEEK_API_KEY 环境变量")2.4 模型版本选择
DeepSeek 官方提供的模型名称会随着版本迭代变化,常见的有 deepseek-chat 和 deepseek-reasoner 两类。其中 deepseek-chat 适合普通对话和大多数文本生成任务,deepseek-reasoner 适合复杂推理场景。
本文示例默认使用 deepseek-chat。具体可用的模型名以你在 DeepSeek 开放平台控制台看到的实际列表为准,不必死记硬背。
3. 核心原理拆解:一个 Harness 由哪几层组成
在照抄代码之前,值得先花几分钟理解 Harness 的分层设计。这是整套框架能保持灵活的关键。
3.1 模型调用层
模型调用层负责与 DeepSeek API 通信,将测试输入转换成模型请求,并返回模型输出。
需要关注三个点:
- 请求地址:DeepSeek 兼容 OpenAI 接口,Base URL 为 https://api.deepseek.com。
- 请求参数:主要是 model、messages、temperature、max_tokens 等。
- 响应解析:从响应结构中提取 content 字段、usage 字段和耗时信息。
这一层应该保持纯粹,只做通信和解析,不参与业务判断。这样当模型服务从 DeepSeek 切换到其他兼容服务时,只需要修改这一层。
3.2 测试用例层
测试用例层负责定义“测什么”。
一条典型的测试用例至少应该包含:
- 用例 ID:唯一标识。
- 输入内容:发送给模型的 prompt 或用户消息。
- 期望结果(可选):用于比对或参考。
- 判定规则:指定使用哪种评估方式。
- 元信息:所属分类、创建时间等,便于统计。
用例推荐的存储格式为 JSON 文件,而不是硬编码在 Python 脚本里。原因很简单:JSON 可以被非开发人员理解和维护,也便于做版本管理。
3.3 评估与断言层
评估与断言层是 Harness 的核心,决定模型输出“好不好”或者“对不对”。
根据任务类型,可以选择不同的评估策略:
- 规则匹配:适合答案格式固定、关键词明确的场景,比如分类结果、关键词抽取。
- 数值比较:适合数学计算、数值抽取场景。
- 参考答案比对:适合有标准答案的问答。
- 模型打分:适合开放式生成任务,由另一个模型(或同一个模型)对输出打分。
- 人工评估:适合主观性强的任务,Harness 输出待评估列表,人工在报告上标记。
本文示例以规则判定和参考答案比对为主,模型打分作为扩展思路在最佳实践部分讨论。
3.4 报告输出层
报告输出层把测试结果汇总成结构化数据。
最简单的方式是输出 Markdown 表格,方便直接在 CSDN 或 GitHub 上阅读。进阶方式包括输出 JSON 报告,方便程序继续处理;或者输出 HTML 报告,方便共享给团队成员。
报告至少应包含:
- 总体通过率。
- 每条用例的通过状态。
- 模型输出的摘要或完整内容。
- 消耗的 Token 数、耗时。
- 失败用例的失败原因。
4. 完整实战:搭建一个可运行的 DeepSeek Harness
下面进入正题,从零开始搭建一套可运行的 Harness。
4.1 创建项目结构
建议按下面结构创建目录:
deepseek-harness/ ├── requirements.txt ├── config.py ├── model_client.py ├── evaluate.py ├── run_harness.py └── testcases/ ├── example_tasks.json └── history/其中:
- requirements.txt 声明 Python 依赖。
- config.py 存放公共配置。
- model_client.py 封装模型调用。
- evaluate.py 实现评估逻辑。
- run_harness.py 主入口。
- testcases 目录存放 JSON 测试用例文件。
先创建目录:
mkdir deepseek-harness && cd deepseek-harness mkdir testcases testcases/history4.2 添加依赖文件
新建 requirements.txt:
openai>=1.0.0 requests>=2.28.0安装依赖:
pip install -r requirements.txt4.3 编写配置文件 config.py
config.py 负责统一管理模型名称、API 地址、网络超时等参数。
# 文件路径:deepseek-harness/config.py import os # DeepSeek 兼容 OpenAI 接口 BASE_URL = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com") API_KEY = os.environ.get("DEEPSEEK_API_KEY", "") # 默认使用的模型,按平台实际可用模型调整 DEFAULT_MODEL = os.environ.get("DEEPSEEK_MODEL", "deepseek-chat") # 请求参数 TEMPERATURE = float(os.environ.get("HARNESS_TEMPERATURE", "0.3")) MAX_TOKENS = int(os.environ.get("HARNESS_MAX_TOKENS", "1024")) TIMEOUT = int(os.environ.get("HARNESS_TIMEOUT", "60")) # 报告输出路径 REPORT_PATH = os.environ.get("HARNESS_REPORT_PATH", "report.md")这里把所有可变参数都提升到环境变量,是为了后续接入 CI 时不用改代码,只改环境变量即可。
4.4 编写模型调用模块 model_client.py
model_client.py 使用 openai 库封装 DeepSeek 调用。它的职责只有一个:输入一段用户消息,输出模型返回的文本。
# 文件路径:deepseek-harness/model_client.py from openai import OpenAI from config import BASE_URL, API_KEY, DEFAULT_MODEL, TEMPERATURE, MAX_TOKENS, TIMEOUT class DeepSeekClient: """封装 DeepSeek 模型调用的客户端""" def __init__(self): if not API_KEY: raise RuntimeError("请先设置 DEEPSEEK_API_KEY 环境变量") self.client = OpenAI(api_key=API_KEY, base_url=BASE_URL, timeout=TIMEOUT) self.model = DEFAULT_MODEL def chat(self, user_content: str, system_content: str = None) -> dict: messages = [] if system_content: messages.append({"role": "system", "content": system_content}) messages.append({"role": "user", "content": user_content}) response = self.client.chat.completions.create( model=self.model, messages=messages, temperature=TEMPERATURE, max_tokens=MAX_TOKENS, ) # 提取结果文本 content = response.choices[0].message.content.strip() # 提取 token 用量 usage = response.usage return { "content": content, "prompt_tokens": usage.prompt_tokens, "completion_tokens": usage.completion_tokens, "total_tokens": usage.total_tokens, }这里把返回结果设计成字典,是因为评估层需要同时用到文本结果和 Token 消耗数据。如果只返回字符串,后面统计成本时还需要重新解析,比较麻烦。
4.5 编写测试用例加载模块
测试用例文件 testcases/example_tasks.json 内容如下:
{ "tasks": [ { "id": "case-001", "category": "cloze", "prompt": "请补全下面的句子,只输出答案:秋天的校园,___。", "expected": [], "keywords": ["落叶", "桂花"], "rule": "keyword_match" }, { "id": "case-002", "category": "extract", "prompt": "从下面这段话中提取城市名称,只输出城市名,多个城市用逗号分隔。文本:小明从北京出发,先飞到上海,然后坐高铁去了杭州。", "expected": ["北京", "上海", "杭州"], "keywords": [], "rule": "exact_match" }, { "id": "case-003", "category": "cloze", "prompt": "请补全下面的句子,只输出答案:冬天到了,___。", "expected": [], "keywords": ["雪花", "寒风"], "rule": "keyword_match" } ] }写一个 load_cases.py 用于加载用例:
# 文件路径:deepseek-harness/load_cases.py import json from typing import List, Dict def load_test_cases(path: str) -> List[Dict]: """从 JSON 文件加载测试用例""" with open(path, "r", encoding="utf-8") as f: data = json.load(f) return data["tasks"] if __name__ == "__main__": cases = load_test_cases("testcases/example_tasks.json") print(f"共加载 {len(cases)} 条测试用例")4.6 编写评估与断言模块 evaluate.py
evaluate.py 实现判定规则。这里实现两种简单规则:
- keyword_match:检查模型输出是否包含至少一个关键词。
- exact_match:将模型输出与期望列表进行比对。
# 文件路径:deepseek-harness/evaluate.py from typing import Dict, List def normalize_text(text: str) -> str: """去空格、去标点,用于宽松比对""" import re text = text.strip().lower() text = re.sub(r"[\s,。!?、,.!?;;::]+", "", text) return text def evaluate_case(case: Dict, model_output: str) -> Dict: """ 对单条测试用例进行判定。 返回包含 pass、reason、details 的字典。 """ rule = case.get("rule", "keyword_match") keywords = case.get("keywords", []) expected = case.get("expected", []) if rule == "keyword_match": if not keywords: return {"pass": False, "reason": "未配置关键词,无法判定"} hit_keywords = [kw for kw in keywords if kw in model_output] passed = len(hit_keywords) > 0 return { "pass": passed, "reason": "命中关键词: " + ("、".join(hit_keywords) if hit_keywords else "无"), "details": {"hit_keywords": hit_keywords} } if rule == "exact_match": # 期望列表转小写比对 expected_set = {normalize_text(e) for e in expected} output_items = model_output.replace(",", ",").split(",") output_set = {normalize_text(o) for o in output_items} missed = expected_set - output_set passed = len(missed) == 0 return { "pass": passed, "reason": "缺失项: " + ("、".join(missed) if missed else "无"), "details": {"expected": list(expected_set), "output": list(output_set)} } return {"pass": False, "reason": f"未知评估规则: {rule}"}normalize_text 的作用是消除中英文标点和空格带来的干扰。比如模型输出“北京,上海, 杭州”和期望列表可以正常比对,不会因为格式差异误判失败。
4.7 编写主流程入口 run_harness.py
主流程负责:
- 加载所有测试用例。
- 调用模型获取输出。
- 执行评估。
- 汇总结果并输出 Markdown 报告。
# 文件路径:deepseek-harness/run_harness.py import time import json import datetime from model_client import DeepSeekClient from load_cases import load_test_cases from evaluate import evaluate_case from config import REPORT_PATH SYSTEM_PROMPT = "你是一个严谨的测试助手,请严格按照用户要求输出结果,不要有多余解释。" def run_single_case(client, case_info: dict, index: int, total: int) -> dict: print(f"[{index}/{total}] 执行用例 {case_info['id']} ...") start_time = time.time() try: result = client.chat(case_info["prompt"], SYSTEM_PROMPT) elapsed = round(time.time() - start_time, 2) model_output = result["content"] evaluation = evaluate_case(case_info, model_output) return { "id": case_info["id"], "category": case_info.get("category", ""), "prompt": case_info["prompt"], "model_output": model_output, "pass": evaluation["pass"], "reason": evaluation["reason"], "elapsed": elapsed, "prompt_tokens": result["prompt_tokens"], "completion_tokens": result["completion_tokens"], "total_tokens": result["total_tokens"], } except Exception as e: elapsed = round(time.time() - start_time, 2) return { "id": case_info["id"], "category": case_info.get("category", ""), "prompt": case_info["prompt"], "model_output": f"ERROR: {str(e)}", "pass": False, "reason": f"调用失败: {str(e)}", "elapsed": elapsed, "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0, } def generate_markdown_report(results: list) -> str: passed = sum(1 for r in results if r["pass"]) total = len(results) pass_rate = passed / total * 100 if total else 0 lines = [ "# DeepSeek Harness 测试报告", "", f"- 生成时间:{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", f"- 总用例数:{total}", f"- 通过数:{passed}", f"- 失败数:{total - passed}", f"- 通过率:{pass_rate:.1f}%", "", "## 用例明细", "", "| 用例ID | 分类 | 是否通过 | 耗时(s) | 判定原因 |", "| --- | --- | --- | --- | --- |", ] for r in results: lines.append( f"| {r['id']} | {r['category']} | {'✅' if r['pass'] else '❌'} | {r['elapsed']} | {r['reason']} |" ) lines.append("") lines.append("## 详细输出") for r in results: lines.append("") lines.append(f"### {r['id']}") lines.append("") lines.append(f"**问题:** {r['prompt']}") lines.append("") lines.append(f"**模型输出:**") lines.append("") lines.append(f"```text") lines.append(r["model_output"]) lines.append(f"```") lines.append("") lines.append(f"**判定结果:** {r['reason']}") return "\n".join(lines) def main(): client = DeepSeekClient() cases = load_test_cases("testcases/example_tasks.json") total = len(cases) if total == 0: print("没有加载到测试用例") return print(f"共加载 {total} 条测试用例,开始执行...") results = [] for index, case in enumerate(cases, start=1): result = run_single_case(client, case, index, total) results.append(result) # 打印简要结果 for r in results: status = "PASS" if r["pass"] else "FAIL" print(f" [{status}] {r['id']}: {r['reason']}") # 生成报告 report = generate_markdown_report(results) with open(REPORT_PATH, "w", encoding="utf-8") as f: f.write(report) # 同时保存 JSON 格式结果,方便后续处理 json_path = REPORT_PATH.replace(".md", ".json") with open(json_path, "w", encoding="utf-8") as f: json.dump(results, f, ensure_ascii=False, indent=2) print(f"\n测试完成,报告已保存:{REPORT_PATH}") print(f"JSON 结果已保存:{json_path}") if __name__ == "__main__": main()4.8 运行与验证
确认环境变量已设置后,执行:
python run_harness.py预期输出类似:
共加载 3 条测试用例,开始执行... [1/3] 执行用例 case-001 ... [2/3] 执行用例 case-002 ... [3/3] 执行用例 case-003 ... [PASS] case-001: 命中关键词: 落叶 [FAIL] case-002: 缺失项: 杭州 [PASS] case-003: 命中关键词: 雪花 测试完成,报告已保存:report.md JSON 结果已保存:report.json注意:由于大模型输出的不确定性,case-002 可能因为模型输出格式略有不同而判定失败,这正是 Harness 需要存在的原因——它帮你在第一时间发现问题,而不是上线后才发现。
5. 常见问题与排查思路
在搭建和使用 Harness 过程中,以下几类问题最常遇到。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 运行时报“请先设置 DEEPSEEK_API_KEY” | 环境变量未设置,或设置后未重启终端 | 重新 export 环境变量,并在脚本中打印 os.environ.get 验证 |
| 报错 timeout 或连接超时 | 网络环境无法访问 DeepSeek API,或超时时间设置过短 | 检查网络连通性,调大 HARNESS_TIMEOUT 环境变量 |
| 返回内容包含多余解释 | 没有设置 system prompt 或 system prompt 不够严格 | 在 SYSTEM_PROMPT 中明确“只输出答案,不要解释” |
| 同一条用例反复失败,但人工看输出没问题 | 断言规则过于严格,如 exact_match 对格式要求过高 | 改用 keyword_match,或增强 normalize_text 兼容更多符号 |
| 报告乱码 | 控制台编码问题 | 在 Python 脚本入口设置 PYTHONIOENCODING=utf-8,或输出到文件后查看 |
| Token 消耗过大,测试成本偏高 | 用例过多,或 max_tokens 设置偏大 | 分批执行,压缩 prompt,按需求调小 HARNESS_MAX_TOKENS |
5.1 环境变量不生效
这是一个非常常见的坑。如果你在终端里执行了 export,但 Python 读到的还是空字符串,先确认是不是设置命令之后新开了终端窗口。更稳妥的做法是在运行命令前先打印验证:
echo $DEEPSEEK_API_KEY如果为空,说明环境变量没有在当前 shell 中生效。
另一种更推荐的方式是使用 .env 文件配合 python-dotenv 管理密钥,避免每次手动 export。
5.2 模型输出格式不稳定
大模型很难保证每次都输出完全相同的格式。用 exact_match 做严格比对时,经常因为一个逗号全角半角差异导致误判。
建议先跑一轮测试,把模型实际输出打印出来,观察输出格式的规律,再决定断言规则。不要一开始就使用非常严格的匹配规则。
5.3 连续调用 API 触发限流
如果你一次性执行几百条用例,可能触发 API 速率限制。此时 Harness 会自动记录“调用失败”,表现为大量 FAIL。
解决方案是在主循环中加入一个简单的延迟,比如在 run_single_case 调用之间加上 time.sleep(0.5)。也可以在 config.py 中增加一个 REQUEST_INTERVAL 参数,由环境变量控制,这样测试节奏不需要改代码。
5.4 失败用例重跑
并不是所有失败都代表模型效果不好。可能是临时网络抖动导致单次调用失败。
建议增加一个 retry 机制:当一次调用抛异常时,重试 1 到 2 次,仍然失败才标记为 FAIL。重试次数同样建议通过环境变量配置,而不是硬编码。
6. 最佳实践与工程建议
6.1 测试用例与代码分离
前面已经提到,测试用例应该使用 JSON 文件单独维护。这一点值得反复强调。
当用例数量增长到成百上千条时,如果你还在 Python 代码里写“测试数据”,维护成本会急剧上升。分离之后,你可以用 Git 对用例做版本管理,每次 Prompt 或模型调整,都清楚知道改动了哪些用例。
6.2 把 Harness 接入定时任务或 CI
当 Harness 能稳定输出 Markdown 报告后,下一步就是让它自动跑。
最简单的用法是配合 cron 定时执行:
# 每天早上 9 点跑一次测试 0 9 * * * cd /path/to/deepseek-harness && python run_harness.py >> runner.log 2>&1在开发阶段,也可以在 Git 的 pre-commit 或 CI 中配置:当 Prompt 仓库有变更时,自动运行一份小规模用例集,快速判断改动是否有明显退化。
6.3 断言规则要分层设计
不要只依赖一种判定方式。建议按任务类型分层:
- 格式校验:先判断输出是否包含必要结构。
- 关键词/字符校验:再判断核心内容是否出现。
- 语义相似度:用于开放式生成。
- 模型打分:用 LLM 做裁判,适合没有标准答案的任务。
- 人工抽检:机器判定通过后,人工再审查一部分结果。
每一层解决不同的问题,组合使用才能覆盖足够多的失败场景。
6.4 记录每次运行的原始输出
测试报告之外,建议把每次运行的完整输出保存为带时间戳的文件,而不是覆盖旧的 report.md。比如:
reports/ ├── 20250510_093000_report.md ├── 20250511_093000_report.md ├── 20250512_093000_report.md有了历史数据,才能回答“上周的模型效果是不是比这周好”这类问题。
6.5 控制成本与 Token 消耗
每次测试都会真实消耗 Token,所以在用例设计上要节制。
在实际项目中,建议把测试集拆成两层:
- 快速回归集:50 条以内,高频执行。
- 完整评估集:数百条以上,低频执行(如每周一次)。
这样既能及时发现问题,又不会让成本失控。Token 消耗数据已经保存在 JSON 结果中,可以定期累计分析。
6.6 安全与合规提醒
使用 DeepSeek API 时,需要注意以下安全边界:
- API Key 不要提交到公开代码仓库,建议加入 .gitignore。
- 测试用例中不要包含真实用户隐私信息。
- 对模型的输出不要盲目采信,特别是涉及事实性、时效性内容的场景。
- 生产环境接入时,应遵循合法合规的数据使用规范,先评估数据脱敏和合规要求。
7. 总结与后续学习建议
至此,你已经掌握了一套面向 DeepSeek 模型 API 的 Harness 搭建方法,包括环境配置、模型调用封装、测试用例管理、评估断言和报告输出。这套框架虽然是针对 DeepSeek 实现,但底层逻辑适用于任何 OpenAI 兼容的模型服务。
如果你希望继续深入,可以按以下路线扩展:
- 增加模型打分评估:用一个强大的模型对另一个模型的输出打分,适用于没有标准答案的开放任务。
- 接入向量检索评估:对“知识库问答”类应用,需要同时评估检索结果和生成结果,这里需要引入检索评估指标。
- 做 Prompt 版本对比:同一组用例跑两个 Prompt,对比通过率和输出质量,用数据驱动 Prompt 迭代。
- 增加多模型对比:把同一个用例集同时跑 DeepSeek 和其他模型,输出横向对比报告。
本质上,Harness 的价值不在于代码本身,而在于它替你建立了“持续观察模型行为”的基础设施。建议你从今天的三五条用例开始,先跑通流程,再逐步扩充。当某天你发现一个 Prompt 改动让通过率从 90% 掉到 60% 时,你会感谢当初留下这套框架的决定。