把”怎么用大模型”沉淀成可复用的资产,才是这类平台真正的门槛。平台早期只是把 prompt 写死在代码里,市场部想复用一段客服话术,得找研发要文件;产品同学改个活动名,要通知所有写了相关 prompt 的人改一遍。这种状态撑到几十个业务方接入后彻底失控,我才开始设计场景(Scene)、提示词(Prompt)、变量(Variable)、版本(Version)四层管理模型。这套结构让平台从”调模型的工具”变成了”存经验的知识库”,下面把设计思路、数据模型和落地坑都讲一遍。
一、为什么要做场景/提示词管理
直接写 prompt 调 API 是 1.0 形态,规模上来后问题会集中爆发。我在设计前专门做了个摸底,把业务方的反馈汇总成四类典型痛点:
- 同一个 prompt 在代码里改来改去,谁也说不清哪个版本是对的;
- 业务团队想”复用市场部的客服话术”,找不到沉淀在哪;
- prompt 里的产品名/活动名要换,发 100 个文件改到崩溃;
- A/B 测试不同 prompt 效果,没有版本化就回不去。
这些痛点的共同根源是”提示词散落在各处、没有身份”。把它们抽象成”场景 + 提示词 + 变量”三层管理之后,问题就变成了增删改查加版本控制。场景是业务入口(比如”代码审查””客服话术”),提示词挂在场景下、有多版本,变量是模板里的占位符、运行时才填值。三层各管一段,改动的影响范围就清晰了。
二、三层数据模型
三层模型在数据库里的组织方式是这样的:一个场景下面挂多条提示词记录,每条提示词带一个自增版本号,变量定义存在提示词的 variables JSON 字段里:
场景 (Scene) ← 一个业务场景,如"代码审查"
└─ 提示词 (Prompt) ← 多版本,如 v1/v2/v3
└─ 变量 (Variable) ← 模板中的占位符
场景表负责”这个场景属于谁、是否公开”,提示词表负责”内容 + 版本 + 当前是否生效”,历史表负责留痕。设计时我特意把”当前激活版本”这个字段放在提示词表而不是场景表,因为一个场景同时只有一个 active 版本,但查询”哪个是激活的”要高频发生,放一张表能少一次 join:
CREATE TABLE scene (
id BIGINT PRIMARY KEY,
code VARCHAR(64) UNIQUE, -- scene_code
name VARCHAR(128) NOT NULL, -- 中文名
description TEXT,
category VARCHAR(64), -- 分类
is_public TINYINT DEFAULT 0, -- 公共/私有
user_id BIGINT, -- 创建者
created_at DATETIME
);
CREATE TABLE prompt (
id BIGINT PRIMARY KEY,
scene_id BIGINT NOT NULL,
version INT NOT NULL, -- 自增版本
name VARCHAR(128),
system_tpl TEXT, -- 系统提示词
user_tpl TEXT, -- 用户提示词模板
variables JSON, -- 变量定义 JSON
model_code VARCHAR(64), -- 推荐模型
parameters JSON, -- temperature/max_tokens
is_active TINYINT DEFAULT 0, -- 当前生效版本
created_at DATETIME,
UNIQUE KEY uk_scene_version (scene_id, version)
);
CREATE TABLE prompt_history (
id BIGINT PRIMARY KEY,
prompt_id BIGINT NOT NULL,
version INT NOT NULL,
snapshot JSON, -- 完整快照
changed_by BIGINT,
change_note VARCHAR(255),
created_at DATETIME
);
这套模型跑通后,有个直观的效果:任何一个 prompt 当前是什么、历史上改过什么、谁改的、为什么改,全都能查到。运维同学再也不用”打开 10 个文件对比 diff”来确认线上跑的是哪版。
三、提示词模板与变量
模板层要做的事是”把占位符替换成真实值”。平台采用 Mustache/Handlebars 风格,变量用双花括号 {{var_name}} 包裹。一个”内容评估”模板长这样:
你是一名{{role}}。
任务:评估以下{{content_type}}。
---
{{user_input}}
---
变量定义用 JSON 存 MySQL 或 JSONB 存 PG,每个变量声明类型、是否必填、可选值和默认值。这样前端能根据变量定义动态渲染表单,而不是让用户手填一坨模板:
[
{"name": "role", "type": "string", "required": true,
"default": "资深技术评审"},
{"name": "content_type", "type": "string", "required": true,
"enum": ["代码", "方案", "需求文档"]},
{"name": "user_input", "type": "string", "required": true}
]
调用时把变量值和模板一起喂给渲染引擎,Java 端一行代码就能完成绑定:
String userPrompt = templateEngine.render(prompt.getUserTpl(), Map.of(
"role", "算法工程师",
"content_type", "代码",
"user_input", codeSubmission
));
这套”模板 + 变量”设计解决了换名字要改 100 个文件的痛点:产品名、活动名都抽成变量,改一处定义,所有引用它的场景自动生效。我对比过直接用字符串拼接的方式,变量一多代码全是拼接逻辑,可读性差还容易漏拼,模板引擎显然是更省心的选择。
四、版本管理策略
提示词最忌讳覆盖写。用户改一个词,如果直接更新原记录,之前跑出来的结果就永远对不上了。平台的策略是”每次修改生成新版本,旧版本只读保留”。新建版本接口的核心逻辑是:读旧版,版本号加一,存成新记录,默认不激活,同时给旧版写一条历史快照:
@PostMapping("/prompt/{id}/newVersion")
public BaseResponse<Long> newVersion(@PathVariable long id,
@RequestBody PromptDTO dto) {
Prompt old = promptService.getById(id);
int nextVer = promptService.maxVersion(old.getSceneId()) + 1;
Prompt newOne = new Prompt();
newOne.setSceneId(old.getSceneId());
newOne.setVersion(nextVer);
newOne.setUserTpl(dto.getUserTpl());
// ... 其它字段
newOne.setIsActive(0); // 新版默认不激活
promptService.save(newOne);
// 写历史快照
historyService.snapshot(old);
return ResultUtils.success(newOne.getId());
}
激活新版本用单独接口,确保”生效”这个动作可控、可审计:
@PostMapping("/prompt/{id}/activate")
public BaseResponse<Void> activate(@PathVariable long id) {
Prompt p = promptService.getById(id);
// 同 scene 其它版本置为 inactive
promptService.deactivateOthers(p.getSceneId());
p.setIsActive(1);
promptService.update(p);
return ResultUtils.success();
}
新版默认不激活这个设计是关键:想先跑个 A/B 对比,直接拿新版本去评测,验证效果后再激活,全程不碰线上正在用的版本。激活接口做了二次确认,防止误操作把线上流量瞬间切到未验证的版本。
五、场景的导入/导出/共享
平台还做了场景市场(marketplace),让沉淀的知识能跨团队流转。这个功能看起来只是”导出再导入”,实际上涉及数据完整性和所有权问题,实现时每个操作都有讲究:
| 操作 | 实现 |
|---|---|
| 导出 | 把 scene + 所有 active prompt 打包成 JSON zip |
| 导入 | 解析 zip,code 重名时提示”覆盖/跳过/重命名” |
| 共享 | 设 is_public=1 后其它用户可见(只读),可”克隆到我的场景” |
| 复制 | 跨用户复制为私有,version 从 1 重新计 |
导出打包这里我踩过一个坑:只导出 active 版本的话,别人拿到场景就丢了历史迭代记录。后来改成”导出全部版本 + 标记哪个是 active”,克隆过去的用户既能看到现状也能追溯历史。共享的只读权限也很重要,否则别人改坏公共场景会影响所有引用它的业务方。
六、与评测流程的衔接
场景和提示词管理不是孤岛,最终要能一键发起评测,否则资产存得再漂亮也只是静态库存。平台的做法是让激活版本的提示词能直接转成 Side-by-Side 评测会话:
"代码审查"场景
└─ v3 提示词 (active)
└─ 点击「跑这个」
→ 选 3 个模型
→ 批量调 LLM
→ 转成 SxS session
promptService 提供 toEvalSession(modelCodes) 方法,把激活版 prompt + 用户输入的变量值打包成 SxS session 请求,零代码完成”看效果”。这个衔接让”调 prompt → 看效果 → 再调”形成闭环,用户不用在两个系统里手动搬数据。实现时注意要把变量值一起传进会话,否则评测时模板渲染缺参数,直接跑出一堆空字段。
七、踩过的坑
这套资产库上线后踩过的坑,集中在变量校验、模板安全和版本切换三个方向,逐个列出来:
- 变量缺失未报错:用户传
{role, content_type}漏user_input,直接渲染出空字符串,调 LLM 给一堆废话。要在渲染前校验required字段。 - 模板注入风险:用户输入里含
{{xxx}}字面量,被 Mustache 二次渲染执行。要么用不冲突的占位符(如[[var]]),要么用escapeHtml处理。 - 版本切换影响线上:激活 v3 后所有用这个场景的 SxS 立刻切到 v3,可能引起评分漂移。激活操作要二次确认+灰度。
- JSON 字段查询慢:早期用 MySQL JSON 字段做”按变量名查询”,性能差。改为把”变量定义”做反范式表
prompt_variable单独存。 - 删除场景有外键:提示词、评测历史都引用 scene_id,硬删会爆。改为软删
is_deleted=1,后台定期归档。
模板注入这个坑是我在一次安全自查时发现的:用户在变量里填了 {{system}} 这样的字面量,渲染时被 Mustache 当成占位符二次替换,会执行出意外内容。改成 [[var]] 占位符后彻底绕开。变量缺失报错那条则是线上真出过的事故,评测结果全是空输出,排查半天发现是模板渲染少了变量,之后所有渲染前强制走 JSON Schema 校验。
八、最佳实践清单
跑通一年多后,把经验收敛成五条最佳实践,新业务方接入时直接照做:
- 提示词版本不可变:只能新增不能修改,每次改动写一行新版本。
- 激活版本不重复:同一场景只能有一个 active 提示词。
- 变量必填校验:渲染前用 JSON Schema 校验,必填缺失返回 400。
- 变更留痕:每次改动写一行 prompt_history,记录变更人、变更说明。
- 场景可复用:跨场景可”引用”提示词(不复制),适合基础角色定义。
最后一条”场景可复用”是后期才加的能力:不同场景都要用”资深技术评审”这个基础角色,如果每个场景复制一份,改一处要同步十处。改成引用关系后,基础角色定义只维护一份,所有引用场景即时生效。到这里,从场景建模到提示词版本、再到市场流通的知识资产库设计就完整了。
常见问题(FAQ)
Q1:为什么不直接用文件/YAML 存提示词?
规模小可以,团队 5 人内 YAML 够用。规模上来后查、改、版本、权限都难做,结构化存储是必然。
Q2:变量校验用 JSON Schema 吗?
简单场景用 Map 校验即可,复杂字段(如 enum、range)才用 JSON Schema。平台支持两种混合模式。
Q3:场景市场会被刷垃圾内容吗?
公共场景走审核流,发布前管理员抽样评估质量。is_public=1 后不可回退到私有,避免共享后突然隐藏。