最近在给 DeepSeek Harness 做可视化工作台改造前后花了一周左右把核心流程跑通代码已经整理好开源。这篇文章把整个改造过程做一个完整复盘为什么要做可视化、Harness 测试框架到底卡在哪里、数据怎么设计、界面怎么组织、代码怎么落地以及实际使用中会遇到哪些坑。如果你正在做 LLM 评测平台、提示词回归测试、模型迭代对比这类工作这篇文章应该可以直接复用一部分思路和代码。1. 为什么 Harness 需要可视化工作台很多团队在接入 DeepSeek 这类大模型的时候早期测试都是这样进行的写一个 Python 脚本拉起模型手动输入几个问题看一眼回答觉得“还不错”然后就准备上线。这种验证方式在 Demo 阶段没问题但一旦进入真实业务迭代问题就开始密集出现第一测试结果无法沉淀。上一轮测试跑出来的回答没有结构化记录两周后想对比某个提示词改动前后的效果差异发现当时的输出已经找不到了。第二指标口径不统一。有人看“回答长度”有人看“是否包含关键词”有人纯凭感觉打分。同一个用例在不同人口中结论完全不一样。第三回归测试没有自动化。改了一个系统提示词不知道哪些历史用例被影响只能重新手工测一轮效率极低。第四协作成本高。算法工程师、后端开发、业务方需要看同一个测试结果但结果分散在每个人的终端里根本没有一个可视化的聚合视图。Harness 这个词在大模型评测领域其实由来已久。它本质上是把“测试用例 模型调用 结果评估 报告输出”串成一条流水线的测试工具链。问题在于很多 Harness 类工具停留在脚本和命令行层面测试跑完了结果散落在 JSON、日志和终端输出里非技术角色根本没法用。可视化工作台要解决的不是“让界面好看”而是把 Harness 变成一个可沉淀、可对比、可评审、可追溯的评测平台。这是这次改造的核心目标。2. DeepSeek Harness 的核心机制与改造切入点从工程实践角度看一个可用的 DeepSeek Harness 需要具备以下能力2.1 用例管理测试用例不能再是零散的 prompt。它应该有结构化的字段比如用例名称任务类型问答、摘要、代码生成、分类等输入内容用户 prompt系统提示词 / 上下文期望结果可选用于自动评估标签用于筛选和分组创建人和创建时间没有结构化用例后续的一切都无从谈起。2.2 模型调用层Harness 需要封装对 DeepSeek API 的调用。注意这里不应该把模型调用逻辑散落在业务代码里而是要做一个统一的执行器负责构造请求参数模型名称、temperature、max_tokens调用 DeepSeek API处理超时、重试、限流记录请求耗时、token 消耗2.3 评估与指标调用完模型之后需要把结果和期望值做对比生成指标。一般会包含是否成功执行响应耗时token 消耗输出长度自动评估结果长度比对、关键词匹配、语义相似度等人工评估结果打标、备注2.4 报告聚合测试跑完后需要把一批用例的结果聚合成可读的报告包括标签维度的通过率、平均耗时、失败列表对比等。改造时最核心的一点是把测试执行的产物变成结构化数据而不是打印日志。可视化只是最后一公里前面所有环节都要为数据服务。3. 可视化工作台改造目标与架构设计这次改造的设计目标没有追求大而全而是聚焦在四个能力上3.1 核心功能目标功能模块解决什么问题核心交互用例管理测试用例结构化沉淀支持新建、编辑、批量导入用例测试执行一键跑通一批用例选择用例集、选择模型参数、发起测试结果可视化测试结果不用盯终端列表 详情 图表展示对比报告多次测试可横向比较选择一个时间段的执行记录生成对比视图3.2 技术选型考虑到团队里 Python 比较多而且 DeepSeek 官方 SDK 是 Python 优先所以整个方案基于 Python 技术栈后端 APIFastAPI数据存储SQLite本地单机或 PostgreSQL团队共享前端Vue 3 Vite ECharts测试执行Python DeepSeek SDK 封装任务调度使用简单的后台任务机制也可以用 Celery 替换这里做一个重要提醒团队规模小、用例量初期不超过几千条时SQLite 完全够用不要一上来就上重型中间件。可视化工作台的复杂度主要在前端交互和后端数据组织数据库本身不是瓶颈。3.3 整体架构浏览器Vue 3 工作台 | | HTTP / JSON v FastAPI 服务 |--- 用例管理 API |--- 执行记录 API |--- 报告聚合 API |--- 测试执行调度 | | SQL v SQLite / PostgreSQL ^ | 执行器 --- 调用 --- DeepSeek API前端负责交互和展示后端负责数据、执行和算力调度两者通过 RESTful API 通信。4. 环境准备与项目结构在动手写代码之前先把环境准备好。4.1 运行环境Python 3.10Node.js 18DeepSeek API Keypip / npm具体版本建议以实际环境为准不要死盯某个版本号重点是依赖之间的兼容性。Python 环境建议使用 venv 或 conda 隔离python -m venv .venv source .venv/bin/activate pip install fastapi uvicorn openai pandasDeepSeek API 兼容 OpenAI 接口协议所以可以直接用openaiSDK只需要修改base_url和api_key即可这一点后面会详细说。4.2 后端项目结构一个清晰的项目结构能让改造少走很多弯路。这里给出一个经过整理的目录结构deepseek-harness-workbench/ ├── backend/ │ ├── app/ │ │ ├── main.py # FastAPI 入口 │ │ ├── database.py # 数据库连接 │ │ ├── models.py # ORM 模型定义 │ │ ├── schemas.py # Pydantic 数据模型 │ │ ├── routers/ │ │ │ ├── cases.py # 用例管理接口 │ │ │ ├── runs.py # 测试执行接口 │ │ │ └── reports.py # 报告对比接口 │ │ └── executor/ │ │ ├── client.py # DeepSeek 客户端封装 │ │ ├── runner.py # 执行调度 │ │ └── evaluator.py # 指标评估 │ ├── requirements.txt │ └── data/ # SQLite 文件目录 ├── frontend/ │ ├── src/ │ │ ├── api/ │ │ ├── views/ │ │ ├── components/ │ │ └── App.vue │ └── package.json └── README.md把后端拆成routers和executor两个大块是为了让API 层和执行逻辑层解耦。实际改造中你会发现如果不分离跑测试的耗时任务会直接把 API 请求阻塞住后续想加异步也没法加。5. 数据库设计与核心模型可视化工作台的质量一半取决于数据模型设计。这里给出一个比较简洁的三表设计。5.1 用例表test_cases测试用例是所有流程的源头必须具备稳定 ID 和标签体系CREATE TABLE test_cases ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, task_type TEXT NOT NULL DEFAULT chat, system_prompt TEXT, user_prompt TEXT NOT NULL, expected_output TEXT, tags TEXT DEFAULT [], created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP );设计说明system_prompt和user_prompt分开存储因为它们的评估逻辑完全不同。tags存 JSON 数组字符串便于前端做筛选和聚合统计。expected_output是可选字段有值时可以做自动比对没有值时交给人工评估。5.2 执行计划表test_runs一次测试执行对应一批用例、一组模型参数CREATE TABLE test_runs ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, model_name TEXT NOT NULL, temperature REAL DEFAULT 0.7, max_tokens INTEGER DEFAULT 2048, status TEXT DEFAULT running, total_cases INTEGER DEFAULT 0, passed_cases INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );5.3 执行结果表run_results这是最重要的表记录了每个用例的单次执行结果CREATE TABLE run_results ( id INTEGER PRIMARY KEY AUTOINCREMENT, run_id INTEGER NOT NULL, case_id INTEGER NOT NULL, response_content TEXT, latency_ms INTEGER, token_usage INTEGER, evaluation_score REAL, evaluation_detail TEXT, status TEXT DEFAULT success, error_message TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );核心设计判断run_results 不与 test_cases 直接覆盖更新而是每次执行都产生新记录。这样做的目的是保留完整的历史执行快照支持后续的对比分析。虽然会占用更多存储但换来的是可追溯性和对比能力这笔账非常划算。6. 核心代码实现6.1 DeepSeek 客户端封装DeepSeek API 兼容 OpenAI 协议这是很大的一个便利。封装客户端时只需要定义好 base_url# 文件路径backend/app/executor/client.py import os import time from openai import OpenAI class DeepSeekClient: def __init__(self, api_key: str | None None): # 如果没有传 api_key则从环境变量读取 self.api_key api_key or os.getenv(DEEPSEEK_API_KEY) if not self.api_key: raise ValueError(请设置 DEEPSEEK_API_KEY) self.client OpenAI( api_keyself.api_key, base_urlos.getenv(DEEPSEEK_BASE_URL, https://api.deepseek.com) ) def chat_completion( self, system_prompt: str, user_prompt: str, model_name: str deepseek-chat, temperature: float 0.7, max_tokens: int 2048 ) - dict: messages [] if system_prompt: messages.append({role: system, content: system_prompt}) messages.append({role: user, content: user_prompt}) start_time time.time() try: response self.client.chat.completions.create( modelmodel_name, messagesmessages, temperaturetemperature, max_tokensmax_tokens ) latency_ms int((time.time() - start_time) * 1000) content response.choices[0].message.content usage response.usage return { content: content, latency_ms: latency_ms, token_usage: usage.total_tokens if usage else 0 } except Exception as e: latency_ms int((time.time() - start_time) * 1000) return { content: None, latency_ms: latency_ms, token_usage: 0, error: str(e) }这个封装的作用非常直接调用方不需要关心 API 细节只需要出入问题拿到标准化的返回结构。注意这段代码做了两层防护第一API Key 必须存在否则提前报错第二模型调用失败时不会让整个测试崩溃而是把这个错误写进结果里方便后续排查。6.2 测试执行器执行器的职责是加载用例 - 逐个调用模型 - 评估结果 - 保存记录。# 文件路径backend/app/executor/runner.py import json from app.models import TestCase, TestRun, RunResult from app.executor.client import DeepSeekClient from app.executor.evaluator import evaluate_result class TestRunner: def __init__(self, db_session): self.db db_session self.client DeepSeekClient() def run_case(self, case: TestCase, model_name: str, temperature: float, max_tokens: int) - dict: result self.client.chat_completion( system_promptcase.system_prompt or , user_promptcase.user_prompt, model_namemodel_name, temperaturetemperature, max_tokensmax_tokens ) # 评估没有 error 才评估否则直接记失败 if result.get(content) is not None: evaluation evaluate_result( expectedcase.expected_output, actualresult[content] ) else: evaluation { score: 0.0, detail: {error: result.get(error, unknown error)} } return { case_id: case.id, response_content: result.get(content), latency_ms: result.get(latency_ms, 0), token_usage: result.get(token_usage, 0), evaluation_score: evaluation.get(score, 0.0), evaluation_detail: json.dumps(evaluation.get(detail, {}), ensure_asciiFalse), status: success if result.get(content) is not None else error, error_message: result.get(error) if result.get(content) is None else None } def run_batch(self, run: TestRun, case_ids: list[int]): cases self.db.query(TestCase).filter(TestCase.id.in_(case_ids)).all() run.total_cases len(cases) self.db.commit() for case in cases: result_data self.run_case( casecase, model_namerun.model_name, temperaturerun.temperature, max_tokensrun.max_tokens ) db_result RunResult(run_idrun.id, **result_data) self.db.add(db_result) if result_data[status] success: run.passed_cases 1 self.db.commit() run.status completed self.db.commit()执行器是 Harness 的核心纽带。它把“用例 - API 调用 - 评估 - 入库”连成了一条完整的链路。设计上的一个关键点执行过程会一条条地更新数据库而不是全部跑完再批量写。这样做可以让你看到执行进度而且一条用例失败不会影响其他用例。代价是数据库写入会频繁一些但对这个量级来说完全没问题。6.3 前端可视化页面前端最核心的页面是“测试结果仪表盘”这里用 ECharts 做一个直观的展示。为保持示例简单这里用原生 HTML Vue 3 的 CDN 方式展示核心思路实际项目中可以通过 Vite Vue SFC 组织。!-- 文件路径frontend/src/views/ReportView.vue核心模板片段 -- template div classreport-container div classsummary-cards div classcard h4总用例数/h4 p{{ summary.total }}/p /div div classcard h4通过数/h4 p classtext-success{{ summary.passed }}/p /div div classcard h4平均耗时/h4 p{{ summary.avgLatency }}ms/p /div div classcard h4通过率/h4 p{{ summary.passRate }}%/p /div /div div classchart-section h4耗时分布/h4 div reflatencyChart classchart/div /div div classresult-table h4详细结果/h4 el-table :dataresults el-table-column propcase_name label用例名称 width180/el-table-column el-table-column propstatus label状态 width100/el-table-column el-table-column proplatency_ms label耗时(ms) width120/el-table-column el-table-column propevaluation_score label评分 width100/el-table-column el-table-column propresponse_content label模型回答 show-overflow-tooltip/el-table-column /el-table /div /div /template实际项目中前端可以通过fetch请求后端 API 获取数据然后交给 ECharts 渲染。这里把重点放在页面的模块划分上后端接口会在第七节给出。6.4 FastAPI 接口层最后是把上面的逻辑全部用 FastAPI 串起来。# 文件路径backend/app/main.py from fastapi import FastAPI, Depends from fastapi.middleware.cors import CORSMiddleware from sqlalchemy.orm import Session from app.database import SessionLocal, engine from app import models, schemas from app.executor.runner import TestRunner app FastAPI(titleDeepSeek Harness Workbench) # 开发环境允许跨域生产环境需要按域名收紧 app.add_middleware( CORSMiddleware, allow_origins[http://localhost:5173, http://127.0.0.1:5173], allow_credentialsTrue, allow_methods[*], allow_headers[*], ) # 数据库表创建生产环境推荐用 Alembic 做迁移 models.Base.metadata.create_all(bindengine) def get_db(): db SessionLocal() try: yield db finally: db.close() app.post(/api/cases) def create_case(case: schemas.TestCaseCreate, db: Session Depends(get_db)): db_case models.TestCase(**case.dict()) db.add(db_case) db.commit() db.refresh(db_case) return db_case app.get(/api/cases) def list_cases(db: Session Depends(get_db)): return db.query(models.TestCase).all() app.post(/api/runs) def create_run(run: schemas.TestRunCreate, db: Session Depends(get_db)): db_run models.TestRun(**run.dict()) db.add(db_run) db.commit() db.refresh(db_run) return db_run app.get(/api/runs/{run_id}/results) def get_results(run_id: int, db: Session Depends(get_db)): return db.query(models.RunResult).filter_by(run_idrun_id).all() app.get(/api/reports/summary) def get_summary(run_id: int, db: Session Depends(get_db)): results db.query(models.RunResult).filter_by(run_idrun_id).all() total len(results) passed sum(1 for r in results if r.status success) avg_latency sum(r.latency_ms for r in results) / total if total else 0 return { total: total, passed: passed, pass_rate: round(passed / total * 100, 2) if total else 0, avg_latency: round(avg_latency, 2) }这段接口层解决了前端最核心的数据需求用例管理、发起测试、拉取结果、聚合报告。注意 CORS 配置开发环境下前端跑在 Vite 的 5173 端口后端跑到 8000 端口跨域是必须要处理的。生产环境建议通过 Nginx 反向代理统一端口可以省略 CORS 配置。7. 运行与验证7.1 启动后端cd backend export DEEPSEEK_API_KEYyour_api_key_here uvicorn app.main:app --reload --port 8000看到类似下面的输出说明后端启动成功INFO: Uvicorn running on http://127.0.0.1:8000 INFO: Application startup complete.7.2 启动前端cd frontend npm install npm run dev打开浏览器访问http://localhost:5173能看到工作台界面。7.3 验证测试流程第一步创建一个测试用例curl -X POST http://127.0.0.1:8000/api/cases \ -H Content-Type: application/json \ -d { name: 基础知识问答, task_type: chat, system_prompt: 你是一个严谨的技术助手。, user_prompt: 解释一下什么是回调函数, tags: [\基础\,\编程\] }第二步发起一次测试执行curl -X POST http://127.0.0.1:8000/api/runs \ -H Content-Type: application/json \ -d { name: 初版提示词回归测试, model_name: deepseek-chat, temperature: 0.3, max_tokens: 1024 }第三步查看执行结果curl http://127.0.0.1:8000/api/runs/1/results如果返回的结果列表里包含response_content和evaluation_score字段说明整条链路已经跑通了。7.4 判断成功的关键标志用例创建成功数据库test_cases表中能看到数据。执行成功run_results表有新增记录且status字段为success。报告聚合成功调用 summary 接口能返回total、passed、pass_rate等统计值。如果失败第一步先看后端控制台日志然后检查 API Key 是否配置正确最后用 curl 直接调用 DeepSeek API 验证连通性。8. 常见问题与排查思路改造过程中最容易踩的坑基本都集中在下面几个环节问题现象可能原因排查方式解决方案请求 DeepSeek API 超时网络不稳定或单次请求耗时过长查看执行器日志确认 latency增加超时时间与重试机制控制 max_tokens 的规模返回内容为空API 返回异常或 prompt 触发了拦截用 curl 直接调 DeepSeek API 验证检查 prompt 内容和返回状态判断是否是输入问题前端图表不展示后端跨域未配置或数据结构不匹配打开浏览器控制台查看请求响应检查 CORS 配置并使用浏览器 Network 面板对比数据结构用例批量执行时一直转圈后台任务没有异步化查看接口响应时间把批量执行改成后台异步任务返回任务 ID 供轮询多轮测试结果无法对比结果表没有保留历史快照检查 run_results 是否覆盖旧数据确认每次执行都在 run_results 中新增记录而不是更新更新数据库锁死SQLite 并发写入冲突查看错误日志中的 database is locked团队规模大时换 PostgreSQL或用 WAL 模式这里特别强调一点SQLite 在并发写入场景下确实会有锁问题。如果你的团队有多个人同时在跑测试建议尽早切换到 PostgreSQL。切换成本很低只要把数据库连接串换掉ORM 模型不用改动。9. 最佳实践与工程建议9.1 数据先行先设计好结果结构再考虑 UI这个观点是这次改造最重要的经验。先想清楚一条测试结果要包含哪些字段一次执行的批次怎么标识多次执行之间怎么关联这些问题没想清楚就开始画前端界面后面一定会返工。可视化工作台看着是前端工程实际上核心是数据组织问题。9.2 评估指标不要一刀切不同任务类型的评估维度完全不同知识问答类参考答案关键词覆盖度、语义相似度、事实一致性。代码生成类能否通过静态编译、是否包含指定函数、格式是否规范。摘要生成类关键信息保留率、冗余度、字数范围。可以在test_cases表中增加task_type字段评估器根据类型选择不同的策略。初期不用做完美先做关键词覆盖 长度比对后续引入语义相似度。9.3 预留人工评审通道自动评估永远不能完全替代人工评审。建议在run_results表中预留human_score和human_comment字段让团队成员可以对关键用例进行人工标注。这个功能在模型迭代评审会上极其有用。9.4 测试用例要版本化提示词和用例本身也会持续变化所以建议在用例表中增加版本号字段或者为用例变更建立审计日志。你看到的很多“模型效果变差但找不到原因”的问题实际上是因为提示词在某个时间点被悄悄改了但没有人记录。9.5 安全边界API Key 永远不要写进前端也不要提交到 Git 仓库统一放在后端环境变量或密钥管理服务中。生产部署时通过 Nginx 限制访问 IP 或增加登录认证。不要在后端日志中打印完整 prompt 和模型输出尤其是业务敏感数据脱敏后再记录。9.6 回归测试要定时跑可视化工作台不只是“测一次看个结果”它的真正价值是持续回归。团队可以在每次提示词变更、模型版本切换时跑一遍固定的核心用例集然后把结果对比图发给业务方评审整个过程耗时不到十分钟。10. 总结与下一步方向这次改造的核心结论其实可以压缩成一句话DeepSeek Harness 可视化工作台的价值不在于画了几个网页而是把大模型测试从临时脚本提升到了可沉淀、可对比、可追踪的工程化流程。整个改造过程的技术难点集中在三个地方测试用例和结果的结构化设计这决定了后续所有分析能力的天花板。DeepSeek API 调用层的稳定封装这决定了测试流程的可靠性。报告聚合与对比能力这决定了业务方能不能真正用起来。下一步如果你想继续完善这个工作台以下几个方向优先级比较高支持批量导入导出测试用例JSON / CSV。增加多轮对话测试能力而不只是单轮 prompt。接入向量化语义评估计算答案与期望结果的余弦相似度。增加定时回归任务每天自动跑核心用例集并推送报告。支持多个模型参数方案对比比如同一批用例分别跑 temperature0.2 和 0.7 的效果差异。代码已经整理好放到开源仓库建议直接 clone 下来跑一遍流程再结合自己的业务场景改。实际改造中你会比看文章更深刻地理解一个道理真正让 Harness 可用的不是界面而是数据和流程的规范化。