1. [Prompt 资产化管理的必要性](#1-prompt-资产化管理的必要性)
  2. [Prompt 管理服务的职责与架构](#2-prompt-管理服务的职责与架构)
  3. [模板数据模型与存储设计](#3-模板数据模型与存储设计)
  4. [变量渲染引擎实现](#4-变量渲染引擎实现)
  5. [版本管理与灰度发布](#5-版本管理与灰度发布)
  6. [Prompt AB 测试框架](#6-prompt-ab-测试框架)
  7. [缓存策略与性能优化](#7-缓存策略与性能优化)
  8. [Prompt 质量评估与回归测试](#8-prompt-质量评估与回归测试)
  9. [管理后台与可视化编辑](#9-管理后台与可视化编辑)
  10. [本章小结与最佳实践清单](#10-本章小结与最佳实践清单)

1.1 从硬编码到资产化的演进

在大模型应用的早期阶段,Prompt 通常以字符串常量的形式硬编码在业务代码里。这种方式在原型阶段没问题,但进入生产后会暴露一系列问题。第一,Prompt 的调整需要改代码、走发布流程,周期长、风险高。算法工程师想测试一个新的 Prompt 变体,要等开发排期、改代码、测试、上线,可能一周就过去了。第二,同一个 Prompt 散落在多个代码文件里,版本不一致,维护困难。第三,无法做 AB 测试——想对比两个 Prompt 的效果,得在代码里写 if-else 分支,侵入性强。

Prompt 资产化管理的核心思路是:把 Prompt 当作独立的"软资产"来管理,像管理配置一样管理 Prompt,像管理代码一样管理 Prompt 的版本。Prompt 管理服务就是这个思路的落地——它提供 Prompt 模板的集中存储、版本管理、变量渲染、灰度发布、AB 测试能力,让 Prompt 的迭代脱离代码发布周期。

1.2 资产化管理带来的收益

把 Prompt 独立成服务后,收益体现在四个方面。

**迭代效率**:算法工程师通过管理后台修改 Prompt 模板,保存后即时生效(或灰度生效),不需要改代码、不需要重启服务。一次 Prompt 优化的周期从"一周"缩短到"一分钟"。

**版本可追溯**:每次修改都有版本记录,可以随时回滚到任意历史版本。线上 Prompt 出问题时,一键回滚到上一个稳定版本,比代码回滚快得多。

**效果可度量**:AB 测试框架让不同 Prompt 变体的效果可以量化对比。不再依赖"感觉哪个好",而是用数据说话。

**复用与标准化**:不同业务线可以共享通用的 Prompt 模板(如通用的 RAG 系统提示词),避免重复造轮子。模板的标准化也有助于保持输出风格的一致性。

2.1 职责边界

Prompt 管理服务的职责用一句话概括:**存储 Prompt 模板,按需渲染并返回**。它不负责调用大模型(那是推理服务的事),不负责组装完整的消息列表(那是编排服务的事),不负责会话管理。编排服务向 Prompt 服务请求"渲染好的系统提示词",拿到后自行组装消息列表再调用推理服务。

2.2 核心功能模块

Prompt 管理服务包含以下核心功能模块:

  • **模板存储**:持久化 Prompt 模板,支持分类、标签、搜索。
  • **变量渲染**:把模板中的变量占位符替换为实际值。
  • **版本管理**:每次修改生成新版本,支持回滚和差异对比。
  • **灰度发布**:按比例或按条件灰度新版本。
  • **AB 测试**:同时运行多个变体,收集效果数据。
  • **缓存**:高频访问的模板缓存到内存,减少数据库查询。
  • **管理后台**:可视化编辑模板,无需改代码。

2.3 服务架构

服务采用标准的分层架构:Controller 层提供 REST API,Service 层实现业务逻辑,Repository 层访问 MySQL。缓存层用 Caffeine 做本地缓存、Redis 做分布式缓存。配置(灰度规则、AB 测试配置)存在 Nacos,支持热更新。

┌──────────────────────────────────────────┐
│  管理后台 (可视化编辑)                      │
├──────────────────────────────────────────┤
│  Controller: 模板CRUD / 渲染API / 版本管理  │
├──────────────────────────────────────────┤
│  Service: 渲染引擎 / 灰度路由 / AB测试       │
├──────────────────────────────────────────┤
│  Cache: Caffeine(本地) → Redis(分布式)     │
├──────────────────────────────────────────┤
│  Repository: MySQL (模板表 / 版本表)        │
└──────────────────────────────────────────┘

3.1 模板表设计

Prompt 模板的核心字段包括:模板 ID、名称、分类、模板内容、变量定义、当前版本号、状态。下面是建表语句。

CREATE TABLE prompt_template (
    id              BIGINT PRIMARY KEY AUTO_INCREMENT,
    template_id     VARCHAR(64) NOT NULL COMMENT '业务模板标识,如 rag-chat-v2',
    name            VARCHAR(128) NOT NULL COMMENT '模板名称',
    category        VARCHAR(64) DEFAULT NULL COMMENT '分类',
    content         TEXT NOT NULL COMMENT '模板内容,含变量占位符',
    variables_json  TEXT DEFAULT NULL COMMENT '变量定义JSON',
    current_version INT NOT NULL DEFAULT 1 COMMENT '当前生效版本号',
    status          VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE/DISABLED',
    description     VARCHAR(512) DEFAULT NULL,
    created_at      DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at      DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_template_id (template_id),
    KEY idx_category (category),
    KEY idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Prompt模板表';

3.2 版本表设计

每次修改模板内容都生成一条版本记录,支持回滚和差异对比。

CREATE TABLE prompt_version (
    id              BIGINT PRIMARY KEY AUTO_INCREMENT,
    template_id     VARCHAR(64) NOT NULL,
    version         INT NOT NULL,
    content         TEXT NOT NULL COMMENT '该版本的模板内容',
    variables_json  TEXT DEFAULT NULL,
    change_log      VARCHAR(512) DEFAULT NULL COMMENT '变更说明',
    created_by      VARCHAR(64) NOT NULL COMMENT '修改人',
    created_at      DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_template_version (template_id, version),
    KEY idx_template_id (template_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Prompt版本表';

3.3 AB 测试配置表

CREATE TABLE prompt_ab_test (
    id              BIGINT PRIMARY KEY AUTO_INCREMENT,
    test_name       VARCHAR(128) NOT NULL,
    template_id     VARCHAR(64) NOT NULL,
    status          VARCHAR(16) NOT NULL DEFAULT 'RUNNING' COMMENT 'RUNNING/STOPPED',
    variants_json   TEXT NOT NULL COMMENT '变体配置JSON',
    traffic_rules   TEXT DEFAULT NULL COMMENT '流量分配规则',
    start_at        DATETIME NOT NULL,
    end_at          DATETIME DEFAULT NULL,
    created_at      DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    KEY idx_template_status (template_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Prompt AB测试表';

3.4 实体类映射

@Data
@Entity
@Table(name = "prompt_template")
public class PromptTemplate {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "template_id", nullable = false, unique = true)
    private String templateId;
    @Column(nullable = false)
    private String name;
    private String category;
    @Column(nullable = false, columnDefinition = "TEXT")
    private String content;
    @Column(name = "variables_json", columnDefinition = "TEXT")
    private String variablesJson;
    @Column(name = "current_version")
    private Integer currentVersion;
    @Column(nullable = false)
    private String status;
    private String description;
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
    public List<VariableDef> parseVariables() {
        if (variablesJson == null || variablesJson.isEmpty()) {
            return Collections.emptyList();
        }
        return JsonUtils.parseList(variablesJson, VariableDef.class);
    }
}
@Data
@AllArgsConstructor
public class VariableDef {
    private String name;          // 变量名
    private String type;          // string/number/list
    private boolean required;     // 是否必填
    private String defaultValue;  // 默认值
    private String description;   // 描述
}

4.1 模板语法选择

Prompt 模板需要支持变量占位符。选择模板引擎时,要考虑安全性和灵活性。不能用 OGNL 或 SpEL 这类可执行任意代码的引擎(有注入风险),应该用纯文本替换的模板引擎。推荐用 Mustache 语法(`{{variable}}`),它简单、安全、无副作用。

4.2 渲染引擎实现

下面是渲染引擎的核心实现。它支持 Mustache 变量替换、默认值填充、条件块、循环块。

@Component
public class PromptRenderEngine {
    private static final Pattern VAR_PATTERN = Pattern.compile("\\{\\{(\\w+)\\}\\}");
    private static final Pattern CONDITIONAL_PATTERN = Pattern.compile(
        "\\{\\{#(\\w+)\\}\\}(.*?)\\{\\{/\\1\\}\\}", Pattern.DOTALL);
    /**
     * 渲染模板
     * @param template 模板内容
     * @param variables 变量值
     * @return 渲染后的Prompt
     */
    public String render(String template, Map<String, Object> variables) {
        if (template == null || template.isEmpty()) {
            return "";
        }
        if (variables == null) {
            variables = Collections.emptyMap();
        }
        String result = template;
        // 1. 处理条件块 {{#var}}content{{/var}}
        result = processConditionals(result, variables);
        // 2. 处理变量替换 {{var}}
        result = processVariables(result, variables);
        return result;
    }
    private String processConditionals(String template, Map<String, Object> variables) {
        Matcher matcher = CONDITIONAL_PATTERN.matcher(template);
        StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            String varName = matcher.group(1);
            String blockContent = matcher.group(2);
            Object value = variables.get(varName);
            // 变量为真(非null、非空、非false)时保留块内容
            if (isTruthy(value)) {
                matcher.appendReplacement(sb, Matcher.quoteReplacement(blockContent));
            } else {
                matcher.appendReplacement(sb, "");
            }
        }
        matcher.appendTail(sb);
        return sb.toString();
    }
    private String processVariables(String template, Map<String, Object> variables) {
        Matcher matcher = VAR_PATTERN.matcher(template);
        StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            String varName = matcher.group(1);
            Object value = variables.get(varName);
            String replacement = value != null ? value.toString() : "";
            matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
        }
        matcher.appendTail(sb);
        return sb.toString();
    }
    private boolean isTruthy(Object value) {
        if (value == null) return false;
        if (value instanceof Boolean) return (Boolean) value;
        if (value instanceof String) return !((String) value).isEmpty();
        if (value instanceof Collection) return !((Collection<?>) value).isEmpty();
        return true;
    }
}

4.3 渲染 API

对外提供渲染 API,编排服务调用它获取渲染后的 Prompt。

@RestController
@RequestMapping("/api/prompt")
public class PromptController {
    @Autowired
    private PromptService promptService;
    @PostMapping("/render")
    public ApiResult<RenderResult> render(@RequestBody @Valid RenderRequest request) {
        RenderResult result = promptService.render(
            request.getTemplateId(),
            request.getVariables()
        );
        return ApiResult.success(result);
    }
    @PostMapping("/render/batch")
    public ApiResult<List<RenderResult>> renderBatch(
            @RequestBody @Valid BatchRenderRequest request) {
        List<RenderResult> results = request.getTemplates().stream()
            .map(t -> promptService.render(t.getTemplateId(), t.getVariables()))
            .collect(Collectors.toList());
        return ApiResult.success(results);
    }
}
@Data
public class RenderRequest {
    @NotBlank
    private String templateId;
    private Map<String, Object> variables;
}
@Data
@Builder
public class RenderResult {
    private String renderedPrompt;
    private int tokenEstimate;
    private String version;
    private String variantId;   // AB测试变体ID(如有)
}

4.4 模板示例

一个 RAG 场景的系统提示词模板如下:

你是一个专业的知识助手。请根据以下参考信息回答用户问题。
{{#hasContext}}
参考信息:
{{context}}
{{/hasContext}}
要求:
1. 回答必须基于参考信息,不要编造
2. 如果参考信息不足以回答,请说明
3. 回答简洁明了,使用中文
当前对话历史中涉及的实体:{{entities}}

当 `hasContext` 为 true 且 `context` 有值时,参考信息段被保留;当没有检索到知识时,`hasContext` 为 false,参考信息段被移除。这种条件块设计让一个模板适配多种场景,比写多个模板更易维护。

5.1 版本生成策略

每次修改模板内容,都生成一个新版本。版本号是递增整数。当前生效版本由 `current_version` 字段指向。修改流程是:编辑新内容 -> 保存为新版本 -> 灰度验证 -> 设为当前版本。

@Service
public class PromptVersionService {
    @Autowired
    private PromptVersionRepository versionRepo;
    @Autowired
    private PromptTemplateRepository templateRepo;
    @Transactional
    public int createVersion(String templateId, String content,
                             String variablesJson, String changeLog,
                             String operator) {
        // 获取当前最大版本号
        int nextVersion = versionRepo.findMaxVersion(templateId) + 1;
        PromptVersion version = new PromptVersion();
        version.setTemplateId(templateId);
        version.setVersion(nextVersion);
        version.setContent(content);
        version.setVariablesJson(variablesJson);
        version.setChangeLog(changeLog);
        version.setCreatedBy(operator);
        versionRepo.save(version);
        return nextVersion;
    }
    @Transactional
    public void publish(String templateId, int version) {
        // 将指定版本设为当前生效版本
        PromptTemplate template = templateRepo.findByTemplateId(templateId)
            .orElseThrow(() -> new PromptNotFoundException(templateId));
        // 验证版本存在
        versionRepo.findByTemplateIdAndVersion(templateId, version)
            .orElseThrow(() -> new VersionNotFoundException(templateId, version));
        template.setCurrentVersion(version);
        templateRepo.save(template);
        // 清除缓存
        cacheManager.evict(templateId);
    }
    @Transactional
    public void rollback(String templateId, int targetVersion) {
        log.info("回滚模板 {} 到版本 {}", templateId, targetVersion);
        publish(templateId, targetVersion);
    }
    public String diff(String templateId, int v1, int v2) {
        String c1 = versionRepo.findByTemplateIdAndVersion(templateId, v1)
            .map(PromptVersion::getContent).orElse("");
        String c2 = versionRepo.findByTemplateIdAndVersion(templateId, v2)
            .map(PromptVersion::getContent).orElse("");
        return DiffUtils.diff(c1, c2);
    }
}

5.2 灰度发布

新版本上线前,可以先灰度一小部分流量验证效果。灰度策略有两种:按比例灰度(10% 流量用新版本)和按条件灰度(特定用户用新版本)。灰度配置存在 Nacos,支持热更新。

@Component
public class GrayscaleRouter {
    @Autowired
    private NacosConfigManager configManager;
    private volatile Map<String, GrayscaleRule> rules = new HashMap<>();
    @NacosConfigListener(dataId = "prompt-grayscale-rules.json")
    public void onRulesUpdate(String config) {
        rules = JsonUtils.parseMap(config, GrayscaleRule.class);
        log.info("灰度规则已更新, {}条规则", rules.size());
    }
    public int resolveVersion(String templateId, String userId) {
        int currentVersion = templateRepo.findCurrentVersion(templateId);
        GrayscaleRule rule = rules.get(templateId);
        if (rule == null || !rule.isEnabled()) {
            return currentVersion;
        }
        // 按比例灰度
        if (rule.getStrategy() == Strategy.PERCENTAGE) {
            int hash = Math.abs(userId.hashCode()) % 100;
            if (hash < rule.getPercentage()) {
                log.debug("模板{}用户{}命中灰度版本{}", templateId, userId, rule.getNewVersion());
                return rule.getNewVersion();
            }
        }
        // 按条件灰度
        if (rule.getStrategy() == Strategy.CONDITION) {
            if (rule.getConditions().stream().allMatch(c -> c.matches(userId))) {
                return rule.getNewVersion();
            }
        }
        return currentVersion;
    }
}

6.1 AB 测试的设计

Prompt 的 AB 测试和传统 Web 页面的 AB 测试不同。Web 页面看的是点击率和转化率,Prompt 看的是回答质量——准确性、相关性、流畅性。这些指标很难自动计算,通常需要人工标注或用 LLM 做自动评估。

AB 测试框架的设计包括三部分:流量分配(把请求分到不同变体)、效果收集(记录每个请求用了哪个变体、效果如何)、效果分析(统计各变体的效果指标)。

6.2 流量分配

@Service
public class AbTestService {
    @Autowired
    private AbTestRepository abTestRepo;
    public AbVariant assignVariant(String templateId, String userId) {
        List<PromptAbTest> activeTests = abTestRepo
            .findByTemplateIdAndStatus(templateId, "RUNNING");
        if (activeTests.isEmpty()) {
            return null;  // 没有进行中的AB测试
        }
        PromptAbTest test = activeTests.get(0);
        List<AbVariant> variants = JsonUtils.parseList(
            test.getVariantsJson(), AbVariant.class);
        // 一致性哈希:同一用户始终分到同一变体
        int hash = Math.abs((templateId + userId).hashCode());
        int bucket = hash % 100;
        int cumulative = 0;
        for (AbVariant v : variants) {
            cumulative += v.getTrafficPercent();
            if (bucket < cumulative) {
                return v;
            }
        }
        return variants.get(variants.size() - 1);
    }
}
@Data
public class AbVariant {
    private String variantId;       // 变体ID
    private int version;            // 对应的模板版本
    private int trafficPercent;     // 流量占比(0-100)
    private String description;     // 变体描述
}

6.3 效果收集

效果数据通过异步事件收集,不阻塞主流程。编排服务在推理完成后,发送一个效果事件到消息队列,AB 测试分析服务消费并统计。

@Component
public class AbTestEventCollector {
    @Autowired
    private RocketMQTemplate mqTemplate;
    @Async
    public void recordOutcome(String templateId, String userId,
                              AbVariant variant, String question,
                              String answer, QualityScore score) {
        AbTestOutcome outcome = AbTestOutcome.builder()
            .templateId(templateId)
            .userId(userId)
            .variantId(variant != null ? variant.getVariantId() : "default")
            .version(variant != null ? variant.getVersion() : 0)
            .question(question)
            .answer(answer)
            .score(score)
            .timestamp(LocalDateTime.now())
            .build();
        mqTemplate.convertAndSend("prompt-ab-outcome", outcome);
    }
}

6.4 自动评估

用 LLM 做自动评估是当前的主流方案。让一个强模型(如 GPT-4o)扮演"评委",对候选回答打分。虽然不如人工准确,但可以快速筛选。

@Service
public class LlmEvaluator {
    @Autowired
    private InferenceClient inferenceClient;
    public QualityScore evaluate(String question, String answer, String reference) {
        String evalPrompt = String.format("""
            请评估以下回答的质量,从1-5分打分。
            问题:%s
            回答:%s
            参考答案:%s
            评估维度:准确性、相关性、完整性、流畅性
            请返回JSON格式:{"accuracy": 4, "relevance": 5, "completeness": 3, "fluency": 4}
            """, question, answer, reference);
        InferenceResponse resp = inferenceClient.chat(InferenceRequest.builder()
            .model("gpt-4o")
            .messages(List.of(Message.of("user", evalPrompt)))
            .temperature(0.0)
            .build());
        return JsonUtils.parse(resp.getContent(), QualityScore.class);
    }
}

7.1 多级缓存

Prompt 模板是"读多写少"的典型场景,非常适合缓存。采用 Caffeine 本地缓存 + Redis 分布式缓存的两级缓存策略。本地缓存命中率高(绝大多数请求走本地)、延迟低(亚毫秒级);Redis 作为本地缓存失效后的兜底,保证多实例间的数据一致性。

@Configuration
public class PromptCacheConfig {
    @Bean
    public Cache<String, PromptTemplate> localPromptCache() {
        return Caffeine.newBuilder()
            .maximumSize(1000)               // 最多缓存1000个模板
            .expireAfterWrite(Duration.ofMinutes(5))  // 5分钟过期
            .recordStats()                    // 记录统计信息
            .build();
    }
}
@Service
public class PromptCacheManager {
    @Autowired
    private Cache<String, PromptTemplate> localCache;
    @Autowired
    private RedisTemplate<String, String> redis;
    @Autowired
    private PromptTemplateRepository repo;
    @Autowired
    private MeterRegistry meterRegistry;
    public PromptTemplate getTemplate(String templateId) {
        // 1. 查本地缓存
        PromptTemplate cached = localCache.getIfPresent(templateId);
        if (cached != null) {
            meterRegistry.counter("prompt.cache", "level", "local").increment();
            return cached;
        }
        // 2. 查Redis
        String redisKey = "prompt:template:" + templateId;
        String json = redis.opsForValue().get(redisKey);
        if (json != null) {
            meterRegistry.counter("prompt.cache", "level", "redis").increment();
            PromptTemplate template = JsonUtils.parse(json, PromptTemplate.class);
            localCache.put(templateId, template);
            return template;
        }
        // 3. 查数据库
        meterRegistry.counter("prompt.cache", "level", "db").increment();
        PromptTemplate template = repo.findByTemplateId(templateId)
            .orElseThrow(() -> new PromptNotFoundException(templateId));
        // 回填缓存
        localCache.put(templateId, template);
        redis.opsForValue().set(redisKey, JsonUtils.toJson(template),
            Duration.ofMinutes(10));
        return template;
    }
    public void evict(String templateId) {
        localCache.invalidate(templateId);
        redis.delete("prompt:template:" + templateId);
        log.info("缓存已清除: {}", templateId);
    }
}

7.2 渲染结果缓存

对于变量组合固定的场景,渲染结果本身也可以缓存。key 是 templateId + variables 的哈希值。这种缓存的命中率取决于变量组合的基数——如果每个用户的变量都不同,命中率低;如果是通用系统提示词(变量少),命中率高。

7.3 批量渲染优化

编排服务一次对话可能需要渲染多个模板(系统提示词、few-shot 示例、知识摘要),逐个调用会有多次网络开销。提供批量渲染接口,一次请求渲染多个模板,减少网络往返。

8.1 回归测试的必要性

Prompt 修改后,最怕的是"修好了 A 场景、弄坏了 B 场景"。回归测试的核心是维护一个测试用例集,每次 Prompt 变更后自动跑一遍,确保已有场景不受影响。

8.2 测试用例集

测试用例集包括:输入变量、期望输出特征、评估标准。评估标准可以是关键词匹配(回答中必须包含/不包含某些词)、长度范围、LLM 评分阈值。

@Data
public class PromptTestCase {
    private String caseId;
    private String templateId;
    private Map<String, Object> inputVariables;
    private Expectation expectation;
}
@Data
public class Expectation {
    private List<String> mustContain;      // 回答必须包含的关键词
    private List<String> mustNotContain;   // 回答不能包含的关键词
    private int minLength;                 // 最小长度
    private int maxLength;                 // 最大长度
    private double minScore;               // LLM评分最低分
}
@Service
public class PromptRegressionTest {
    @Autowired
    private PromptRenderEngine renderEngine;
    @Autowired
    private InferenceClient inferenceClient;
    @Autowired
    private LlmEvaluator evaluator;
    @Autowired
    private PromptCacheManager cacheManager;
    public RegressionReport run(String templateId, int version) {
        PromptTemplate template = cacheManager.getTemplate(templateId);
        // 加载指定版本的模板内容
        String content = loadVersionContent(templateId, version);
        List<PromptTestCase> cases = loadTestCases(templateId);
        RegressionReport report = new RegressionReport();
        for (PromptTestCase tc : cases) {
            String prompt = renderEngine.render(content, tc.getInputVariables());
            InferenceResponse resp = inferenceClient.chat(buildRequest(prompt));
            boolean passed = checkExpectation(resp.getContent(), tc.getExpectation());
            report.addResult(tc.getCaseId(), passed, resp.getContent());
        }
        return report;
    }
    private boolean checkExpectation(String answer, Expectation exp) {
        if (exp.getMustContain() != null) {
            for (String kw : exp.getMustContain()) {
                if (!answer.contains(kw)) return false;
            }
        }
        if (exp.getMustNotContain() != null) {
            for (String kw : exp.getMustNotContain()) {
                if (answer.contains(kw)) return false;
            }
        }
        if (exp.getMinLength() > 0 && answer.length() < exp.getMinLength()) {
            return false;
        }
        if (exp.getMaxLength() > 0 && answer.length() > exp.getMaxLength()) {
            return false;
        }
        return true;
    }
}

8.3 CI 集成

回归测试集成到 CI 流水线中:每次 Prompt 模板变更提交后,自动触发回归测试,测试不通过则阻止发布。

9.1 管理后台功能

管理后台是 Prompt 服务不可或缺的组成部分,它让非开发人员(算法工程师、产品经理)也能管理 Prompt。核心功能包括:模板列表与搜索、可视化编辑器、版本历史与差异对比、灰度发布控制台、AB 测试配置与报表、回归测试触发与结果查看。

9.2 可视化编辑器

编辑器需要支持变量高亮、语法提示、实时预览。下面是编辑器后端提供的预览 API。

@RestController
@RequestMapping("/admin/prompt")
public class PromptAdminController {
    @Autowired
    private PromptRenderEngine renderEngine;
    @Autowired
    private PromptVersionService versionService;
    @Autowired
    private TokenEstimator tokenEstimator;
    @PostMapping("/preview")
    public ApiResult<PreviewResult> preview(@RequestBody PreviewRequest req) {
        String rendered = renderEngine.render(req.getContent(), req.getVariables());
        int tokenEst = tokenEstimator.estimate(rendered);
        return ApiResult.success(PreviewResult.builder()
            .renderedPrompt(rendered)
            .tokenEstimate(tokenEst)
            .build());
    }
    @PostMapping("/save")
    public ApiResult<Integer> save(@RequestBody @Valid SaveRequest req) {
        int version = versionService.createVersion(
            req.getTemplateId(), req.getContent(),
            req.getVariablesJson(), req.getChangeLog(),
            req.getOperator());
        return ApiResult.success(version);
    }
    @PostMapping("/publish")
    public ApiResult<Void> publish(@RequestBody PublishRequest req) {
        versionService.publish(req.getTemplateId(), req.getVersion());
        return ApiResult.success(null);
    }
    @GetMapping("/versions")
    public ApiResult<List<VersionVo>> versions(@RequestParam String templateId) {
        return ApiResult.success(versionService.listVersions(templateId));
    }
    @GetMapping("/diff")
    public ApiResult<String> diff(@RequestParam String templateId,
                                   @RequestParam int v1,
                                   @RequestParam int v2) {
        return ApiResult.success(versionService.diff(templateId, v1, v2));
    }
}

9.3 权限控制

管理后台需要权限控制:算法工程师可以编辑和发布,产品经理只能查看和预览,运维可以管理灰度规则。基于 RBAC 模型实现即可。

10.1 核心要点回顾

Prompt 管理服务的核心价值是把 Prompt 从代码中解耦,实现资产化管理。通过模板存储、变量渲染引擎、版本管理、灰度发布、AB 测试、回归测试,Prompt 的迭代周期从"一周"缩短到"一分钟",且每次变更可追溯、可回滚、可度量。多级缓存保证了渲染性能,管理后台让非开发人员也能参与 Prompt 优化。

10.2 最佳实践清单

  1. **模板语法选 Mustache**:安全无副作用,不要用可执行代码的模板引擎。
  2. **条件块设计**:用 `{{#var}}...{{/var}}` 让一个模板适配多场景,减少模板数量。
  3. **版本必留痕**:每次修改生成版本,变更说明必填,方便追溯。
  4. **灰度先于发布**:新版本先灰度 10% 流量验证,再全量发布。
  5. **一致性哈希分桶**:AB 测试用一致性哈希,同一用户始终分到同一变体。
  6. **多级缓存**:Caffeine 本地 + Redis 分布式,读多写少场景命中率可达 99%。
  7. **缓存主动清除**:发布/回滚时主动清缓存,不要等自然过期。
  8. **回归测试集**:维护测试用例集,每次变更自动跑,防止"修好A弄坏B"。
  9. **LLM 自动评估**:用强模型做自动评估,快速筛选,人工抽检校准。
  10. **批量渲染**:多模板场景用批量接口,减少网络往返。
  11. **token 估算前置**:渲染后估算 token 数,超限提前告警。
  12. **管理后台必备**:让算法工程师自助管理 Prompt,不依赖开发排期。

10.3 后续预告

下一篇也是最后一篇,将聚焦 Embedding 向量服务的独立部署与接口设计,包括多模型切换、批量化、向量入库全流程。

> 本文是"Java 程序员第 44 阶段"系列的第 04 篇,聚焦 Prompt 管理服务的设计与实现。建议结合前三篇阅读。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐