Side-by-Side 与 Prompt Lab 后端实现区别(AI 大模型评测平台的双场景架构)

Side-by-Side 是横向比模型、Prompt Lab 是纵向调 prompt,二者的技术实现完全是两套。当时前端设计稿出来,两个页面长得都像”左边输入、右边对比”,产品同学一度以为后端复用一套逻辑就行。结果等我真正动手做技术方案时才发现,这两个场景对实时性、数据存储、调用方式的要求完全相反,硬塞进一套通用实现,两头都会别扭。后来我按各自的业务特性拆成两套独立后端,把踩过的坑也沉淀了下来。下面把两者的本质差异、实现细节和取舍理由完整讲清楚。

一、先说本质差异

要理解两套后端为什么长不一样,先抓住一个根本区别:Side-by-Side 回答的是”哪个模型或哪份答案更好”,Prompt Lab 回答的是”同一个模型的这个 prompt 怎么改更好”。前者一次要调多个模型横向对比,后者是同模型反复试同一个 prompt 的纵向调优。这个区别直接决定了调用次数、数据模型和交互方式。

下面从核心问题、自变量、因变量等五个维度做对比:

维度 Side-by-Side Prompt Lab
核心问题 “哪个模型/答案更好” “这个 prompt 怎么改更好”
自变量 模型、参考答案 提示词、参数
因变量 人类/AI 评分 答案质量指标
调用次数 通常 2-N 个模型各 1 次 同模型多次(不同 prompt)
数据存储 一组答案 + 评分 多版本 prompt + 各次输出

看完表格就明白:Side-by-Side 是一次性把多个模型拉来”同题竞技”,数据落库后还要评分;Prompt Lab 是单模型的反复试错,数据量小但迭代频率高。这两者的本质差异,决定了后面所有技术选型。

二、Side-by-Side 后端实现

数据模型

Side-by-Side 的核心是”一次评测会话包含多个模型输出”。所以数据模型拆成两张表:eval_session 存会话主体,eval_session_item 存每个模型的单条输出和评分。我当时设计时特意把评分字段放在 item 上而不是 session 上,因为不同模型答案质量独立,后续换评分员重评也只需要改 item:

CREATE TABLE eval_session (
  id           BIGINT PRIMARY KEY,
  user_id      BIGINT NOT NULL,
  question     TEXT NOT NULL,
  reference    TEXT,
  status       VARCHAR(16) NOT NULL,    -- pending/running/done/error
  created_at   DATETIME
);

CREATE TABLE eval_session_item (
  id           BIGINT PRIMARY KEY,
  session_id   BIGINT NOT NULL,
  model_code   VARCHAR(64) NOT NULL,
  prompt       TEXT,
  output       LONGTEXT,
  score        DECIMAL(4,2),
  duration_ms  INT,
  INDEX idx_session (session_id)
);

核心流程

会话的提交链路是异步的。用户提交 question 和 N 个模型后,后端先建会话,再把每个模型拆成一个任务丢进消息队列,Worker 并发去调各家模型接口,每完成一个更新一次进度。这个流程拆成六个步骤:

  1. 用户提交 question + 选定 N 个模型;
  2. 后端创建 eval_session,给每个模型创建一行 eval_session_item;
  3. 通过 RabbitMQ 异步并行调用各模型;
  4. 每完成一个 item 更新进度,session status 转 done;
  5. 调用 AI 评分 prompt 打分(可选多评委交叉);
  6. 前端 WebSocket 推送实时进度。

入口 Controller 只负责建会话和投递任务,真正的调用逻辑在 MQ 消费端。这里我踩过一个坑:最初把”调用模型”直接写在 Controller 里,一次请求要等全部模型返回,5 个模型串行跑要一两分钟,前端一直转圈。改成异步后才把耗时从”所有模型之和”降成”最长那个模型”:

@PostMapping("/session/submit")
public BaseResponse<Long> submit(@RequestBody EvalSubmitRequest req) {
    long sid = evalService.createSession(req);
    for (String model : req.getModelCodes()) {
        mqSender.sendEvalTask(new EvalTask(sid, model, req.getQuestion()));
    }
    return ResultUtils.success(sid);
}

关键点

异步化之后还有三个细节不能省,否则线上会出各种幺蛾子。

  • 并行调用而非串行:用 Flux.merge 或 CompletableFuture,多模型同时请求,5 个模型 20 秒完成(串行要 100 秒)。
  • WebSocket 实时进度:用 STOMP 或原生 WebSocket,每个 item 完成就 push 一次,前端不轮询。
  • 存储原始输出:所有模型输出完整保存,支持”事后人工复核”和”换评分员重评”。

存储原始输出这个点,是我做”评分漂移复查”时意识到的。AI 评分结果偶尔会波动,没有原始输出做底账,根本没法复盘到底哪个模型答了什么。现在每次评分变更都能追溯到当时的模型输出,运营同学处理申诉也快得多。

三、Prompt Lab 后端实现

数据模型

Prompt Lab 的核心是”版本”和”运行记录”。prompt_lab 存当前 prompt 配置,prompt_lab_run 存每一次运行的输入输出和费用。为什么每次运行都要单独存一行?因为用户调 prompt 靠的就是对比”上一版这么写、这一版那么写,输出差在哪”,没有运行快照,这个对比就无从谈起:

CREATE TABLE prompt_lab (
  id           BIGINT PRIMARY KEY,
  user_id      BIGINT NOT NULL,
  name         VARCHAR(128) NOT NULL,
  model_code   VARCHAR(64) NOT NULL,
  system_prompt TEXT,
  user_prompt  TEXT,
  temperature  DECIMAL(3,2),
  max_tokens   INT,
  created_at   DATETIME
);

CREATE TABLE prompt_lab_run (
  id           BIGINT PRIMARY KEY,
  lab_id       BIGINT NOT NULL,
  version      INT NOT NULL,            -- 第 N 次运行
  output       LONGTEXT,
  input_tokens INT,
  output_tokens INT,
  fee          DECIMAL(10,6),
  created_at   DATETIME,
  INDEX idx_lab (lab_id, version)
);

核心流程

Prompt Lab 的交互链路和 Side-by-Side 完全相反,它是同步的、立即返回的:

  1. 用户编辑 prompt,点击”运行” → 调用 LLM,返回结果展示;
  2. 每次运行生成一行 prompt_lab_run,记录完整输入输出;
  3. 用户对比多次运行结果(左侧历史版本,右侧最新),调 prompt;
  4. 支持”对比运行”:固定 system prompt,user prompt 写两个版本同次跑。

关键点

Prompt Lab 有三个技术选型,都是围绕”用户要快速试错”这个核心诉求展开的。

  • 响应要快:用户调 prompt 是高频操作,单次调用必须 < 3s,所以走同步 API 不进 MQ。
  • Token 计数是核心 UI:每次运行都展示本次 token、累计费用,用户据此决定是否继续调。
  • 版本快照:每次 prompt 内容 + 参数完整快照到 prompt_lab_run,避免”覆盖写”导致历史回不去。

Token 计数这条在实现时很折腾:不同模型返回的 token 字段名不一样,有的叫 usage.prompt_tokens,有的藏在别处,我封装了一个统一的解析层把各家 usage 归一化,前端才拿到一致的展示格式。

四、后端架构差异

两个场景的数据流走的是两条完全不同的路径。Side-by-Side 是”提交 → 异步 → 推送”,Prompt Lab 是”提交 → 同步 → 返回”,用图表示最直观:

Side-by-Side 路径:
Client → Controller → MQ (异步) → Worker 调用 LLM → DB 写 result → WebSocket 推

Prompt Lab 路径:
Client → Controller → 同步调用 LLM → DB 写 run → 立即返回

两条路径在工程上的差异,用一张表能说清楚:

路径 Side-by-Side Prompt Lab
入口 Controller (REST) Controller (REST)
异步队列 RabbitMQ 无
WebSocket 是(实时进度) 否
评分服务 LLM-as-Judge 不评分
限流策略 5 次/分钟 30 次/分钟
单次耗时 10-60s 1-3s
状态机 pending/running/done 无状态

限流策略的差异值得多说一句:Side-by-Side 一次调用多个模型、成本高,限得严;Prompt Lab 是高频试错,限得太严用户没法干活,就放宽到 30 次/分钟。这个数字是压测加观察用户行为后调出来的。

五、为什么不能合并

既然两个功能都涉及”调大模型 + 存输出”,早期我确实动过合并的心思,想用一个通用 eval_task 表兼容两个场景。结果真做起来,两边都难用:

  • SxS 需要”多模型并行 + 实时进度”,强行复用单条 task 的同步返回会让前端等 30s;
  • Lab 需要”快速试错 + 版本回滚”,合并后表字段 30 个,90% 字段在某一场景下是冗余的;
  • 评分流程只在 SxS 有,Lab 用不到;
  • 限流档位不同,合一个接口粒度不好做。

合并方案最致命的问题是”状态机”没法统一。SxS 有 pending/running/done 三态,还要支撑进度推送;Lab 根本不需要状态,每次运行都是独立快照。为了兼容而引入的状态字段,在两个场景里互相添乱。最终拆成 eval_session 与 prompt_lab 两套表加两套服务,互不污染,各自演进也快。这个教训我记了很久:相似的需求表面,后端设计别被”看起来像”带偏。

六、踩过的坑

两套系统上线前后踩过的坑,都列在这里,每个都对应一次线上事故或返工。

  • SxS 进度推送丢消息:WebSocket 断线后用户重连,需要补发”已完成的 item”,实现”快照比对+增量推送”双通道。
  • Lab 高频调用被风控:用户连续点 50 次运行,平台限流 30 次/分钟。给前端加按钮 loading + 倒计时引导。
  • SxS 评分员和选手撞了:评分员要选”非选手”模型,否则自家模型评自家答案全 9 分。
  • Lab 历史版本泄漏:早期覆盖写 user_prompt,用户改回旧版找不到之前那次的输出。改成 immutable 追加 + latest_version 标记。

WebSocket 丢消息这个坑影响最大:用户提交评测后页面进度卡在 2/5,刷新也回不去。后来定位是断线重连没有补发机制。改成重连时先下发会话快照、再推送增量后,进度就再没丢过。Lab 覆盖写那个坑则属于设计缺陷,不是 bug,immutable 追加之后,用户每次运行都有独立版本,彻底告别”找不到上次输出”的投诉。到这一步,双场景的后端架构就稳定运行了,后续再扩展新评测玩法,直接照这两套模板加即可。

常见问题(FAQ)

Q1:SxS 和 Lab 数据能互相转换吗?

能。SxS 的每个 item 可以”提取 prompt 存到 Lab”,Lab 的 prompt 也可以”跑多模型转成 SxS session”。平台提供转换按钮。

Q2:为什么 SxS 走 MQ 异步而 Lab 不走?

SxS 一次提交多个模型任务、要实时进度,异步队列天然适合;Lab 用户期待”立等可取”,异步反而让交互变卡。

Q3:两者能否共享模型价目/token 计数?

能。这部分下沉到 usage_record 公共表,任意调用都写一行,平台统一查询、统计、限价。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
其AI的头像其AI普通用户

相关推荐

返回顶部