👤 关于作者
JavaAgent架构师 — 十年Java分布式架构老兵,专注AI Agent企业级落地。
主导过数字员工、SOP智能引擎等项目,开发过RPC框架、消息中间件、ORM框架。
正在输出:
专栏:
专栏一:《前端AI工程化》10期适合前端 SSE/流式渲染/.../企业级AI架构/AI前端面试深度解析
专栏二:《Java体系也能玩转AI》24期加急中,适合java深度玩家
专栏三:《从0构建Agent系统》 15期加急中,适合所以玩家
让Java开发者不转Python也能构建企业级AI应用。
-----------------------------------------------------------------------------------------------
点赞+关注+评论 走一波。
-------------------------淘汰自己不是别人,是自己!!!------------------------------------
核心定位:前端工程师在Prompt Engineering中的独特价值与实操
关键产出:Prompt模板管理系统
Prompt Engineering:不只是后端的事
开篇:被忽视的前端阵地
大多数开发者听到"Prompt Engineering",第一反应是:这是后端/算法同学的事,前端只管展示结果。
这个认知在2024年之前大体成立。但在AI应用深度渗透的今天,前端正在成为Prompt工程链路中不可替代的一环——因为前端是最靠近用户意图的地方。
用户点击了什么按钮、输入了什么文字、选择了什么风格、上传了什么参考图——这些信息只有前端能完整捕获并结构化为Prompt。后端拿到的,只是前端"翻译"后的产物。
一、Prompt Engineering核心概念速览
1.1 基础结构
一个完整的LLM请求通常由三部分组成:
interface ChatMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
// System Prompt:定义AI的角色和行为边界
const systemPrompt: ChatMessage = {
role: 'system',
content: '你是一个专业的前端技术顾问,只回答前端相关的问题。回答要简洁、专业、附带代码示例。',
};
// User Prompt:用户的具体问题
const userPrompt: ChatMessage = {
role: 'user',
content: '如何在React中实现虚拟滚动?',
};
// Assistant Prompt(历史回复):提供上下文
const assistantPrompt: ChatMessage = {
role: 'assistant',
content: '虚拟滚动的核心思路是...',
};
1.2 高级技巧
// Few-shot:通过示例引导输出格式
const fewShotPrompt = `
请按以下格式提取信息:
输入:张三,1990年出生,住在北京
输出:{"name": "张三", "birthYear": 1990, "city": "北京"}
输入:李四,1985年出生,住在上海
输出:{"name": "李四", "birthYear": 1985, "city": "上海"}
输入:${userInput}
输出:`;
// CoT(Chain of Thought):引导模型逐步推理
const cotPrompt = `
请一步一步思考:
1. 首先,分析问题的核心需求
2. 然后,列出可能的解决方案
3. 接着,评估每个方案的优缺点
4. 最后,给出推荐方案
用户问题:${question}
`;
// 格式约束:强制结构化输出
const formatConstraint = `
请以JSON格式返回结果,严格遵循以下Schema:
{
"title": "string",
"summary": "string (不超过100字)",
"tags": ["string"],
"confidence": "number (0-1)"
}
不要输出任何JSON之外的内容。
`;
二、前端在Prompt Engineering中的四个独特角色
角色1:用户意图→结构化Prompt的转换层
用户说:“帮我写个爬虫”——这是自然语言,直接丢给LLM效果不稳定。
前端要做的是:将模糊意图转换为结构化Prompt。
// 用户在前端界面的操作
interface UserAction {
taskType: 'write_code' | 'debug' | 'optimize' | 'review';
language?: string;
framework?: string;
inputCode?: string;
requirements?: string;
}
// 前端将用户操作转换为结构化Prompt
function buildPromptFromAction(action: UserAction): ChatMessage[] {
const systemPrompts: Record<string, string> = {
write_code: `你是一个${action.language ?? ''}编程专家。请根据需求编写代码,要求:
1. 代码完整可运行
2. 包含必要的注释
3. 遵循${action.framework ?? ''}最佳实践`,
debug: `你是一个代码调试专家。请分析以下代码,找出bug并修复。
输出格式:
- Bug位置:...
- Bug原因:...
- 修复方案:...
- 修复后代码:...`,
optimize: `你是一个代码优化专家。请优化以下代码,重点关注:
1. 性能优化
2. 可读性提升
3. 最佳实践对齐
输出优化前后的对比。`,
review: `你是一个代码审查专家。请对以下代码进行Review,按以下维度评分(1-5):
1. 代码质量
2. 性能
3. 安全性
4. 可维护性
每个维度给出具体改进建议。`,
};
return [
{ role: 'system', content: systemPrompts[action.taskType] },
{ role: 'user', content: action.inputCode ?? action.requirements ?? '' },
];
}
核心价值:前端把"用户做了什么"翻译成"LLM需要什么",这个翻译质量直接决定AI输出质量。
角色2:上下文窗口的Token预算管理
LLM有上下文窗口限制(GPT-4 Turbo是128K tokens)。在长对话场景中,前端需要管理Token预算:
class TokenBudgetManager {
private totalBudget: number;
private reservedForSystem: number;
private reservedForOutput: number;
constructor(options: {
totalBudget: number; // 总token预算
systemReserve: number; // 系统Prompt预留
outputReserve: number; // 输出预留
}) {
this.totalBudget = options.totalBudget;
this.reservedForSystem = options.systemReserve;
this.reservedForOutput = options.outputReserve;
}
/** 获取可用于对话历史的token预算 */
getHistoryBudget(): number {
return this.totalBudget - this.reservedForSystem - this.reservedForOutput;
}
/** 对话历史超出预算时的裁剪策略 */
trimHistory(
messages: ChatMessage[],
maxSize: number
): ChatMessage[] {
const budget = this.getHistoryBudget();
// 保留System Prompt
const systemMessages = messages.filter(m => m.role === 'system');
// 对话历史(user + assistant交替)
const historyMessages = messages.filter(m => m.role !== 'system');
if (this.estimateTokens(historyMessages) <= budget) {
return messages; // 未超预算,无需裁剪
}
// 策略1:滑动窗口——保留最近的N轮对话
let trimmed = this.slidingWindowTrim(historyMessages, budget);
// 策略2:如果裁剪后仍然超预算,对较早的对话做摘要压缩
if (this.estimateTokens(trimmed) > budget) {
trimmed = this.summaryCompressTrim(trimmed, budget);
}
return [...systemMessages, ...trimmed];
}
/** Token估算(简化版:中文1字≈1.5token,英文1词≈1token) */
private estimateTokens(messages: ChatMessage[]): number {
return messages.reduce((total, msg) => {
const chineseChars = (msg.content.match(/[\u4e00-\u9fff]/g) ?? []).length;
const otherChars = msg.content.length - chineseChars;
return total + chineseChars * 1.5 + otherChars * 0.25;
}, 0);
}
private slidingWindowTrim(messages: ChatMessage[], budget: number): ChatMessage[] {
// 从最新的消息开始保留,直到接近预算
const result: ChatMessage[] = [];
let tokenCount = 0;
for (let i = messages.length - 1; i >= 0; i--) {
const msgTokens = this.estimateTokens([messages[i]]);
if (tokenCount + msgTokens > budget) break;
tokenCount += msgTokens;
result.unshift(messages[i]);
}
return result;
}
private summaryCompressTrim(messages: ChatMessage[], budget: number): ChatMessage[] {
// 将较早的对话压缩为摘要
const recentCount = Math.floor(messages.length * 0.3); // 保留最近30%
const recentMessages = messages.slice(-recentCount);
const oldMessages = messages.slice(0, -recentCount);
// 生成摘要(实际项目中会调用LLM生成摘要)
const summary: ChatMessage = {
role: 'user',
content: `[以下是之前对话的摘要:${oldMessages.map(m => `${m.role}: ${m.content.slice(0, 50)}...`).join(' | ')}]`,
};
return [summary, ...recentMessages];
}
}
角色3:输出格式约束与解析保障
LLM的输出是不确定的——你要求JSON,它可能返回"json\n{...}\n"。前端需要做防御性解析:
class LLMOutputParser {
/** 安全解析LLM返回的JSON */
parseJSON<T>(raw: string, schema: z.ZodSchema<T>): T {
// 第一步:提取JSON部分
let jsonStr = raw;
// 处理Markdown代码块包裹
const codeBlockMatch = raw.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
if (codeBlockMatch) {
jsonStr = codeBlockMatch[1];
}
// 处理前后多余文本
const firstBrace = jsonStr.indexOf('{');
const lastBrace = jsonStr.lastIndexOf('}');
if (firstBrace !== -1 && lastBrace !== -1) {
jsonStr = jsonStr.slice(firstBrace, lastBrace + 1);
}
// 解析JSON
try {
const parsed = JSON.parse(jsonStr);
return schema.parse(parsed); // Zod校验
} catch (error) {
throw new LLMOutputParseError(raw, error);
}
}
}
class LLMOutputParseError extends Error {
constructor(public rawOutput: string, public cause: unknown) {
super(`LLM输出解析失败: ${cause}`);
}
}
角色4:Prompt模板的版本化与A/B测试
interface PromptTemplate {
id: string;
name: string;
version: number;
content: string;
variables: string[]; // 模板变量列表
metadata: {
createdAt: Date;
updatedAt: Date;
author: string;
abTestGroup?: 'A' | 'B';
performanceScore?: number; // 基于输出质量的评分
};
}
// 模板变量插值
function renderTemplate(
template: PromptTemplate,
variables: Record<string, string>
): string {
let content = template.content;
for (const [key, value] of Object.entries(variables)) {
const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
content = content.replace(regex, value);
}
// 检查是否有未替换的变量
const unresolvedVars = content.match(/\{\{(\w+)\}\}/g);
if (unresolvedVars) {
console.warn(`[PromptTemplate] 未替换的变量: ${unresolvedVars.join(', ')}`);
}
return content;
}
三、前端驱动的Prompt优化闭环
用户操作 → 前端捕获意图 → 结构化Prompt → 发送到LLM → 接收输出
↑ ↓
← ← ← 用户反馈(点赞/点踩/重新生成)← ← ← 输出质量评估 ←
前端是唯一能同时触达"用户输入"和"用户反馈"的层,这使得前端成为Prompt优化闭环的关键节点:
class PromptFeedbackLoop {
private feedbackStore: FeedbackStore;
/** 记录用户对AI输出的反馈 */
recordFeedback(params: {
promptId: string;
templateVersion: number;
outputQuality: 'good' | 'bad' | 'neutral';
userAction: 'accept' | 'regenerate' | 'edit' | 'copy';
latencyMs: number;
}): void {
this.feedbackStore.save(params);
}
/** 获取模板的性能数据,用于A/B测试决策 */
getTemplatePerformance(templateId: string): {
goodRate: number;
regenerateRate: number;
avgLatency: number;
} {
const feedbacks = this.feedbackStore.getByTemplate(templateId);
// ... 计算性能指标
}
}
实践任务
任务:设计一个Prompt模板DSL,支持变量插值、条件分支、片段组合,并实现对应的编译器。
DSL规范要求:
{{variable}} → 变量插值
{{#if condition}}...{{/if}} → 条件分支
{{> snippet_name}} → 片段引用
{{#each items}}...{{/each}} → 循环
验收标准:
- 变量插值正常工作
- 条件分支根据运行时值正确渲染
- 片段可复用、可嵌套
- 循环渲染列表数据
- 未定义变量不报错,渲染为空字符串
面试题解析
Q:谈谈你对Prompt Engineering的理解,前端在其中扮演什么角色?
答题要点:
- Prompt Engineering定义:通过设计输入文本来引导LLM产生期望输出的技术,包括System Prompt设计、Few-shot示例、CoT推理、格式约束等
- 前端四个独特角色:
- 意图转换层:把用户操作翻译成结构化Prompt
- Token预算管理者:上下文窗口的裁剪与分配
- 输出保障层:防御性解析LLM的不确定输出
- 优化闭环节点:唯一同时触达输入和反馈的层
- 延伸:前端的Prompt能力不仅是"用",更是"管"——模板化、版本化、A/B测试、效果追踪
4.2 Prompt模板管理系统设计
开篇:从"硬编码"到"工程化"
在项目初期,Prompt散落在代码各处:
// ❌ Prompt硬编码在各组件中
const chatPrompt = `你是一个助手,请回答以下问题:${userInput}`;
const codePrompt = `请写一段${language}代码:${requirement}`;
const summaryPrompt = `请总结以下内容,不超过${maxWords}字:\n${content}`;
问题:
- 修改Prompt需要改代码、重新部署
- 无法A/B测试不同Prompt的效果
- 无法追踪Prompt版本与输出质量的关联
- 多人协作时Prompt变更无审计
Prompt模板管理系统的目标:把Prompt当作产品资产来管理,而不是代码中的字符串常量。
一、系统架构
┌─────────────────────────────────────────────────┐
│ PromptTemplateManager │
├─────────────────────────────────────────────────┤
│ 模板引擎 │
│ ├─ DSL解析器 (变量/条件/循环/片段) │
│ ├─ 模板编译器 (DSL → 渲染函数) │
│ └─ 片段仓库 (可复用的Prompt片段) │
├─────────────────────────────────────────────────┤
│ 版本管理 │
│ ├─ 模板版本链 (v1 → v2 → v3) │
│ ├─ 灰度发布 (按比例分配新旧版本) │
│ └─ 回滚机制 │
├─────────────────────────────────────────────────┤
│ 效果追踪 │
│ ├─ 输出质量评分 (自动+手动) │
│ ├─ A/B测试看板 │
│ └─ 效果趋势图 │
├─────────────────────────────────────────────────┤
│ 协作机制 │
│ ├─ 前后端共享模板仓库 │
│ ├─ 变更审计日志 │
│ └─ 模板评审流程 │
└─────────────────────────────────────────────────┘
二、模板引擎实现
2.1 DSL解析器
// DSL语法定义
// {{variable}} → 变量
// {{#if condition}} → 条件开始
// {{else}} → 条件分支
// {{/if}} → 条件结束
// {{#each array}} → 循环开始
// {{/each}} → 循环结束
// {{> snippetName}} → 片段引用
type ASTNode =
| { type: 'text'; value: string }
| { type: 'variable'; name: string }
| { type: 'if'; condition: string; body: ASTNode[]; elseBody?: ASTNode[] }
| { type: 'each'; array: string; body: ASTNode[] }
| { type: 'snippet'; name: string };
class PromptDSLParser {
parse(template: string): ASTNode[] {
const nodes: ASTNode[] = [];
let remaining = template;
while (remaining.length > 0) {
// 匹配各种语法结构
const varMatch = remaining.match(/^\{\{(\w+)\}\}/);
const ifMatch = remaining.match(/^\{\{#if\s+(\w+)\}\}/);
const eachMatch = remaining.match(/^\{\{#each\s+(\w+)\}\}/);
const snippetMatch = remaining.match(/^\{\{>\s*(\w+)\}\}/);
if (ifMatch) {
const { node, consumed } = this.parseIfBlock(remaining);
nodes.push(node);
remaining = remaining.slice(consumed);
} else if (eachMatch) {
const { node, consumed } = this.parseEachBlock(remaining);
nodes.push(node);
remaining = remaining.slice(consumed);
} else if (snippetMatch) {
nodes.push({ type: 'snippet', name: snippetMatch[1] });
remaining = remaining.slice(snippetMatch[0].length);
} else if (varMatch) {
nodes.push({ type: 'variable', name: varMatch[1] });
remaining = remaining.slice(varMatch[0].length);
} else {
// 纯文本,推进到下一个模板标记
const nextTag = remaining.indexOf('{{');
if (nextTag === -1) {
nodes.push({ type: 'text', value: remaining });
remaining = '';
} else {
nodes.push({ type: 'text', value: remaining.slice(0, nextTag) });
remaining = remaining.slice(nextTag);
}
}
}
return nodes;
}
private parseIfBlock(template: string): { node: ASTNode; consumed: number } {
const ifMatch = template.match(/^\{\{#if\s+(\w+)\}\}/)!;
const condition = ifMatch[1];
let pos = ifMatch[0].length;
// 解析if body
const body = this.parseUntilTag(template.slice(pos), ['else', '/if']);
pos += body.consumed;
// 检查是否有else分支
let elseBody: ASTNode[] | undefined;
const elseMatch = template.slice(pos).match(/^\{\{else\}\}/);
if (elseMatch) {
pos += elseMatch[0].length;
const elseResult = this.parseUntilTag(template.slice(pos), ['/if']);
elseBody = elseResult.nodes;
pos += elseResult.consumed;
}
// 消耗 {{/if}}
const endMatch = template.slice(pos).match(/^\{\{\/if\}\}/);
if (endMatch) pos += endMatch[0].length;
return {
node: { type: 'if', condition, body: body.nodes, elseBody },
consumed: pos,
};
}
private parseEachBlock(template: string): { node: ASTNode; consumed: number } {
const eachMatch = template.match(/^\{\{#each\s+(\w+)\}\}/)!;
const array = eachMatch[1];
let pos = eachMatch[0].length;
const result = this.parseUntilTag(template.slice(pos), ['/each']);
pos += result.consumed;
const endMatch = template.slice(pos).match(/^\{\{\/each\}\}/);
if (endMatch) pos += endMatch[0].length;
return {
node: { type: 'each', array, body: result.nodes },
consumed: pos,
};
}
private parseUntilTag(
template: string,
endTags: string[]
): { nodes: ASTNode[]; consumed: number } {
// 简化实现:递归解析直到遇到指定的结束标签
// 实际实现需要处理嵌套结构
const nodes: ASTNode[] = [];
let remaining = template;
let consumed = 0;
while (remaining.length > 0) {
// 检查是否遇到了结束标签
for (const tag of endTags) {
const regex = new RegExp(`^\\{\\{${tag.replace('/', '\\/')}\\}\\}`);
if (regex.test(remaining)) {
return { nodes, consumed };
}
}
// 递归解析内容(简化:一次推进一个字符或一个模板标记)
const nextTag = remaining.indexOf('{{');
if (nextTag === -1) {
nodes.push({ type: 'text', value: remaining });
consumed += remaining.length;
remaining = '';
} else if (nextTag > 0) {
nodes.push({ type: 'text', value: remaining.slice(0, nextTag) });
consumed += nextTag;
remaining = remaining.slice(nextTag);
} else {
// 在标签处,尝试解析子结构
const varMatch = remaining.match(/^\{\{(\w+)\}\}/);
if (varMatch && !varMatch[1].startsWith('#') && !varMatch[1].startsWith('/')) {
nodes.push({ type: 'variable', name: varMatch[1] });
consumed += varMatch[0].length;
remaining = remaining.slice(varMatch[0].length);
} else {
// 遇到无法解析的标签,推进一个字符避免死循环
nodes.push({ type: 'text', value: remaining[0] });
consumed += 1;
remaining = remaining.slice(1);
}
}
}
return { nodes, consumed };
}
}
2.2 模板渲染器
class PromptTemplateRenderer {
private snippets = new Map<string, string>();
/** 注册可复用片段 */
registerSnippet(name: string, content: string): void {
this.snippets.set(name, content);
}
/** 渲染模板 */
render(ast: ASTNode[], context: Record<string, any>): string {
return ast.map(node => this.renderNode(node, context)).join('');
}
private renderNode(node: ASTNode, context: Record<string, any>): string {
switch (node.type) {
case 'text':
return node.value;
case 'variable':
return String(context[node.name] ?? '');
case 'if':
if (context[node.condition]) {
return this.render(node.body, context);
} else if (node.elseBody) {
return this.render(node.elseBody, context);
}
return '';
case 'each': {
const array = context[node.array];
if (!Array.isArray(array)) return '';
return array
.map((item, index) => this.render(node.body, { ...context, item, index }))
.join('');
}
case 'snippet': {
const snippetContent = this.snippets.get(node.name);
if (!snippetContent) {
console.warn(`[PromptTemplate] 片段 "${node.name}" 未注册`);
return '';
}
const parser = new PromptDSLParser();
const snippetAst = parser.parse(snippetContent);
return this.render(snippetAst, context);
}
}
}
}
三、版本管理与灰度发布
interface TemplateVersion {
version: number;
content: string;
createdAt: Date;
author: string;
changelog: string;
rolloutPercentage: number; // 灰度比例 0-100
isActive: boolean;
}
class PromptVersionManager {
private versions = new Map<string, TemplateVersion[]>();
/** 创建新版本 */
createVersion(
templateId: string,
content: string,
author: string,
changelog: string,
): TemplateVersion {
const existing = this.versions.get(templateId) ?? [];
const newVersion: TemplateVersion = {
version: existing.length + 1,
content,
createdAt: new Date(),
author,
changelog,
rolloutPercentage: 0,
isActive: false,
};
existing.push(newVersion);
this.versions.set(templateId, existing);
return newVersion;
}
/** 灰度发布:按比例分配新旧版本 */
resolveVersion(templateId: string, userId: string): TemplateVersion | null {
const versions = this.versions.get(templateId);
if (!versions || versions.length === 0) return null;
// 按灰度比例分配
const activeVersions = versions.filter(v => v.isActive && v.rolloutPercentage > 0);
if (activeVersions.length === 0) {
// 没有灰度版本,返回最新版本
return versions[versions.length - 1];
}
// 基于userId的哈希决定使用哪个版本
const hash = this.simpleHash(userId);
const bucket = hash % 100;
let cumulative = 0;
for (const version of activeVersions) {
cumulative += version.rolloutPercentage;
if (bucket < cumulative) {
return version;
}
}
// 默认返回最新版本
return versions[versions.length - 1];
}
private simpleHash(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // 转为32位整数
}
return Math.abs(hash);
}
}
四、效果追踪
interface PromptEffectRecord {
templateId: string;
version: number;
userId: string;
timestamp: Date;
// 输入
variables: Record<string, any>;
// 输出质量评估
outputLength: number;
userRating?: 1 | 2 | 3 | 4 | 5; // 用户评分
userAction: 'accept' | 'regenerate' | 'edit' | 'copy' | 'abandon';
responseTimeMs: number;
// 自动评估
jsonParseSuccess?: boolean; // 是否成功解析为JSON
formatCompliance?: boolean; // 是否符合格式要求
}
class PromptEffectTracker {
private records: PromptEffectRecord[] = [];
/** 记录一次Prompt使用的效果 */
record(record: PromptEffectRecord): void {
this.records.push(record);
}
/** 生成A/B测试报告 */
generateABReport(
templateId: string,
versionA: number,
versionB: number
): ABTestReport {
const recordsA = this.records.filter(
r => r.templateId === templateId && r.version === versionA
);
const recordsB = this.records.filter(
r => r.templateId === templateId && r.version === versionB
);
return {
versionA: {
version: versionA,
sampleSize: recordsA.length,
avgRating: this.avg(recordsA.map(r => r.userRating).filter(Boolean)),
acceptRate: recordsA.filter(r => r.userAction === 'accept').length / recordsA.length,
regenerateRate: recordsA.filter(r => r.userAction === 'regenerate').length / recordsA.length,
avgResponseTime: this.avg(recordsA.map(r => r.responseTimeMs)),
},
versionB: {
version: versionB,
sampleSize: recordsB.length,
avgRating: this.avg(recordsB.map(r => r.userRating).filter(Boolean)),
acceptRate: recordsB.filter(r => r.userAction === 'accept').length / recordsB.length,
regenerateRate: recordsB.filter(r => r.userAction === 'regenerate').length / recordsB.length,
avgResponseTime: this.avg(recordsB.map(r => r.responseTimeMs)),
},
recommendation: '...', // 基于数据给出版本推荐
};
}
private avg(nums: number[]): number {
return nums.length > 0 ? nums.reduce((a, b) => a + b, 0) / nums.length : 0;
}
}
五、完整PromptTemplateManager
class PromptTemplateManager {
private parser = new PromptDSLParser();
private renderer = new PromptTemplateRenderer();
private versionManager = new PromptVersionManager();
private effectTracker = new PromptEffectTracker();
/** 注册片段 */
registerSnippet(name: string, content: string): void {
this.renderer.registerSnippet(name, content);
}
/** 创建模板新版本 */
createTemplate(
templateId: string,
content: string,
author: string,
changelog: string,
): void {
// 验证DSL语法
try {
this.parser.parse(content);
} catch (error) {
throw new Error(`模板DSL语法错误: ${error}`);
}
this.versionManager.createVersion(templateId, content, author, changelog);
}
/** 渲染模板(自动选择版本) */
render(
templateId: string,
variables: Record<string, any>,
userId: string,
): string {
const version = this.versionManager.resolveVersion(templateId, userId);
if (!version) {
throw new Error(`模板 "${templateId}" 不存在`);
}
const ast = this.parser.parse(version.content);
return this.renderer.render(ast, variables);
}
/** 记录效果 */
recordEffect(record: PromptEffectRecord): void {
this.effectTracker.record(record);
}
/** 获取A/B测试报告 */
getABReport(templateId: string, versionA: number, versionB: number): ABTestReport {
return this.effectTracker.generateABReport(templateId, versionA, versionB);
}
}
六、前后端共享模板仓库的接口设计
// 前端通过API获取模板(而不是硬编码)
// GET /api/prompt-templates/:id?userId=xxx
interface PromptTemplateAPIResponse {
templateId: string;
version: number;
content: string; // 已选择灰度版本的模板内容
variables: string[]; // 模板需要的变量列表
}
// 前端提交效果反馈
// POST /api/prompt-templates/:id/feedback
interface PromptFeedbackAPIRequest {
version: number;
userId: string;
userAction: 'accept' | 'regenerate' | 'edit' | 'copy' | 'abandon';
userRating?: 1 | 2 | 3 | 4 | 5;
responseTimeMs: number;
}
关键设计:模板内容由后端存储和分发,前端只负责渲染和反馈。这样:
- 运营人员可以在后台直接修改Prompt,不需要代码部署
- 灰度发布由后端控制,前端无感知
- 效果数据统一存储,方便分析
实践任务
任务:实现PromptTemplateManager,支持模板定义、编译、版本管理、效果统计。
验收标准:
- DSL模板可正确渲染变量、条件、循环、片段
- 模板版本链可追溯
- 灰度发布基于userId哈希稳定分配
- 效果追踪可生成A/B测试报告
- 片段可跨模板复用
面试题解析
Q:前端如何管理Prompt模板?如何进行A/B测试?
答题要点:
- 模板工程化:Prompt不应该硬编码,应通过模板DSL+变量插值管理
- 版本化:每次修改创建新版本,支持回滚
- 灰度发布:基于userId哈希稳定分配版本,逐步放量
- 效果追踪:记录用户行为(接受/重新生成/编辑)作为Prompt质量的代理指标
- 前后端协作:模板存储在后端,前端渲染+反馈,形成优化闭环
下期预告:前端AI工程化(五):AI对话状态管理,我们将进入React/Vue的实战领域,拆解长对话场景下的状态管理架构。