24.11-Prompt模板市场

要点

  • Prompt 模板是可复用的提示词,支持变量替换
  • 模板市场分为:系统预置、租户私有、公开分享
  • 模板支持版本管理,可以回滚到历史版本
  • 模板可以关联到工作空间,控制访问范围

内容

1. Prompt 模板概念

Prompt 模板解决的问题:

  • 复用性:常用的提示词不需要每次重新编写
  • 标准化:团队使用统一的 Prompt 格式
  • 变量化:支持动态插入上下文信息
  • 版本管理:记录修改历史,可以回滚

模板类型:

类型可见性用途
系统预置所有租户官方提供的通用模板
租户私有租户内租户自定义的业务模板
公开分享所有租户用户分享的优质模板

2. 数据模型

-- Prompt 模板表
CREATE TABLE prompt_templates (
  id TEXT PRIMARY KEY,
  tenant_id TEXT,                       -- NULL 表示系统预置
  workspace_id TEXT,
  name TEXT NOT NULL,
  description TEXT,
  category TEXT,                        -- writing/coding/analysis/chat
  content TEXT NOT NULL,                -- 模板内容
  variables TEXT,                       -- JSON: 变量定义
  model_preferences TEXT,               -- JSON: 推荐模型配置
  is_public INTEGER DEFAULT 0,
  is_system INTEGER DEFAULT 0,
  usage_count INTEGER DEFAULT 0,
  version INTEGER DEFAULT 1,
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL,
  FOREIGN KEY (tenant_id) REFERENCES tenants(id),
  FOREIGN KEY (workspace_id) REFERENCES workspaces(id)
)
 
-- 模板历史版本表
CREATE TABLE prompt_template_versions (
  id TEXT PRIMARY KEY,
  template_id TEXT NOT NULL,
  version INTEGER NOT NULL,
  content TEXT NOT NULL,
  variables TEXT,
  created_by TEXT NOT NULL,
  created_at INTEGER NOT NULL,
  FOREIGN KEY (template_id) REFERENCES prompt_templates(id),
  UNIQUE(template_id, version)
)
 
-- 模板收藏表
CREATE TABLE prompt_template_favorites (
  id TEXT PRIMARY KEY,
  user_id TEXT NOT NULL,
  template_id TEXT NOT NULL,
  created_at INTEGER NOT NULL,
  FOREIGN KEY (template_id) REFERENCES prompt_templates(id),
  UNIQUE(user_id, template_id)
)

TypeScript 类型:

// src/types/prompt.ts
export interface PromptTemplate {
  id: string
  tenantId?: string
  workspaceId?: string
  name: string
  description?: string
  category?: string
  content: string
  variables: PromptVariable[]
  modelPreferences?: {
    model?: string
    temperature?: number
    maxTokens?: number
  }
  isPublic: boolean
  isSystem: boolean
  usageCount: number
  version: number
  createdAt: number
  updatedAt: number
}
 
export interface PromptVariable {
  name: string
  description?: string
  type: 'string' | 'number' | 'boolean' | 'list'
  required: boolean
  defaultValue?: any
  options?: string[]    // 当 type 为 list 时
}
 
export interface PromptTemplateVersion {
  id: string
  templateId: string
  version: number
  content: string
  variables: PromptVariable[]
  createdBy: string
  createdAt: number
}

3. 模板管理

3.1 创建模板

// src/routes/prompts.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { generateId } from '../lib/utils'
 
const app = new Hono()
 
const VariableSchema = z.object({
  name: z.string(),
  description: z.string().optional(),
  type: z.enum(['string', 'number', 'boolean', 'list']),
  required: z.boolean().optional(),
  defaultValue: z.any().optional(),
  options: z.array(z.string()).optional(),
})
 
app.post('/', zValidator('json', z.object({
  name: z.string().min(1).max(100),
  description: z.string().optional(),
  category: z.string().optional(),
  content: z.string(),
  variables: z.array(VariableSchema).optional(),
  modelPreferences: z.object({
    model: z.string().optional(),
    temperature: z.number().optional(),
    maxTokens: z.number().optional(),
  }).optional(),
  workspaceId: z.string().optional(),
  isPublic: z.boolean().optional(),
})), async (c) => {
  const tenantId = c.get('tenantId')
  const userId = c.get('userId')
  const data = c.req.valid('json')
 
  const id = generateId()
  const now = Date.now()
 
  await c.env.DB.prepare(`
    INSERT INTO prompt_templates (id, tenant_id, workspace_id, name, description, category, content, variables, model_preferences, is_public, is_system, version, created_at, updated_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 1, ?, ?)
  `).bind(
    id,
    tenantId,
    data.workspaceId || null,
    data.name,
    data.description || null,
    data.category || null,
    data.content,
    JSON.stringify(data.variables || []),
    data.modelPreferences ? JSON.stringify(data.modelPreferences) : null,
    data.isPublic ? 1 : 0,
    now,
    now
  ).run()
 
  // 创建初始版本
  await c.env.DB.prepare(`
    INSERT INTO prompt_template_versions (id, template_id, version, content, variables, created_by, created_at)
    VALUES (?, ?, 1, ?, ?, ?, ?)
  `).bind(
    generateId(),
    id,
    data.content,
    JSON.stringify(data.variables || []),
    userId,
    now
  ).run()
 
  return c.json({ id, name: data.name, version: 1 }, 201)
})

3.2 获取模板列表

app.get('/', async (c) => {
  const tenantId = c.get('tenantId')
  const workspaceId = c.req.query('workspaceId')
  const category = c.req.query('category')
  const search = c.req.query('search')
 
  let query = `
    SELECT * FROM prompt_templates
    WHERE (tenant_id = ? OR is_public = 1 OR is_system = 1)
  `
  const params: any[] = [tenantId]
 
  if (workspaceId) {
    query += ` AND (workspace_id = ? OR workspace_id IS NULL)`
    params.push(workspaceId)
  }
 
  if (category) {
    query += ` AND category = ?`
    params.push(category)
  }
 
  if (search) {
    query += ` AND (name LIKE ? OR description LIKE ?)`
    params.push(`%${search}%`, `%${search}%`)
  }
 
  query += ` ORDER BY is_system DESC, usage_count DESC, created_at DESC`
 
  const templates = await c.env.DB.prepare(query).bind(...params).all()
 
  return c.json({
    templates: templates.results.map(t => ({
      id: t.id,
      name: t.name,
      description: t.description,
      category: t.category,
      variables: JSON.parse(t.variables || '[]'),
      isPublic: t.is_public === 1,
      isSystem: t.is_system === 1,
      usageCount: t.usage_count,
      version: t.version,
    })),
  })
})

3.3 更新模板

app.put('/:id', zValidator('json', z.object({
  name: z.string().optional(),
  description: z.string().optional(),
  category: z.string().optional(),
  content: z.string().optional(),
  variables: z.array(VariableSchema).optional(),
  modelPreferences: z.object({
    model: z.string().optional(),
    temperature: z.number().optional(),
    maxTokens: z.number().optional(),
  }).optional(),
  isPublic: z.boolean().optional(),
})), async (c) => {
  const templateId = c.req.param('id')
  const tenantId = c.get('tenantId')
  const userId = c.get('userId')
  const updates = c.req.valid('json')
 
  // 验证权限
  const template = await c.env.DB.prepare(`
    SELECT * FROM prompt_templates WHERE id = ? AND tenant_id = ?
  `).bind(templateId, tenantId).first()
 
  if (!template) {
    return c.json({ error: '模板不存在' }, 404)
  }
 
  if (template.is_system === 1) {
    return c.json({ error: '系统模板不能修改' }, 403)
  }
 
  const now = Date.now()
  const newVersion = template.version + 1
 
  // 更新模板
  const setClauses = ['version = ?', 'updated_at = ?']
  const values: any[] = [newVersion, now]
 
  if (updates.name) {
    setClauses.push('name = ?')
    values.push(updates.name)
  }
  if (updates.description !== undefined) {
    setClauses.push('description = ?')
    values.push(updates.description)
  }
  if (updates.category !== undefined) {
    setClauses.push('category = ?')
    values.push(updates.category)
  }
  if (updates.content) {
    setClauses.push('content = ?')
    values.push(updates.content)
  }
  if (updates.variables) {
    setClauses.push('variables = ?')
    values.push(JSON.stringify(updates.variables))
  }
  if (updates.modelPreferences) {
    setClauses.push('model_preferences = ?')
    values.push(JSON.stringify(updates.modelPreferences))
  }
  if (updates.isPublic !== undefined) {
    setClauses.push('is_public = ?')
    values.push(updates.isPublic ? 1 : 0)
  }
 
  values.push(templateId)
 
  await c.env.DB.prepare(`
    UPDATE prompt_templates SET ${setClauses.join(', ')} WHERE id = ?
  `).bind(...values).run()
 
  // 创建新版本
  if (updates.content || updates.variables) {
    await c.env.DB.prepare(`
      INSERT INTO prompt_template_versions (id, template_id, version, content, variables, created_by, created_at)
      VALUES (?, ?, ?, ?, ?, ?, ?)
    `).bind(
      generateId(),
      templateId,
      newVersion,
      updates.content || template.content,
      JSON.stringify(updates.variables || JSON.parse(template.variables || '[]')),
      userId,
      now
    ).run()
  }
 
  return c.json({ success: true, version: newVersion })
})

4. 模板使用

4.1 渲染模板

// src/lib/prompt-renderer.ts
import { PromptTemplate, PromptVariable } from '../types/prompt'
 
export function renderPromptTemplate(
  template: PromptTemplate,
  variables: Record<string, any>
): string {
  let content = template.content
 
  // 验证必需变量
  for (const v of template.variables) {
    if (v.required && !(v.name in variables)) {
      throw new Error(`Missing required variable: ${v.name}`)
    }
  }
 
  // 替换变量
  for (const v of template.variables) {
    const value = variables[v.name] ?? v.defaultValue ?? ''
    const placeholder = new RegExp(`\\{\\{${v.name}\\}\\}`, 'g')
    content = content.replace(placeholder, String(value))
  }
 
  return content
}
 
// 使用示例
const template: PromptTemplate = {
  id: '1',
  name: '翻译助手',
  content: '请将以下{{sourceLanguage}}文本翻译成{{targetLanguage}}:\n\n{{text}}',
  variables: [
    { name: 'sourceLanguage', type: 'string', required: true },
    { name: 'targetLanguage', type: 'list', required: true, options: ['中文', '英文', '日文'] },
    { name: 'text', type: 'string', required: true },
  ],
}
 
const rendered = renderPromptTemplate(template, {
  sourceLanguage: '英文',
  targetLanguage: '中文',
  text: 'Hello, world!',
})
// => 请将以下英文文本翻译成中文:\n\nHello, world!

4.2 使用模板进行 AI 调用

// src/routes/chat.ts(补充)
app.post('/chat/with-template', zValidator('json', z.object({
  templateId: z.string(),
  variables: z.record(z.any()),
})), async (c) => {
  const tenantId = c.get('tenantId')
  const userId = c.get('userId')
  const { templateId, variables } = c.req.valid('json')
 
  // 获取模板
  const template = await c.env.DB.prepare(`
    SELECT * FROM prompt_templates
    WHERE id = ? AND (tenant_id = ? OR is_public = 1 OR is_system = 1)
  `).bind(templateId, tenantId).first()
 
  if (!template) {
    return c.json({ error: '模板不存在' }, 404)
  }
 
  // 渲染模板
  const content = renderPromptTemplate(
    {
      ...template,
      variables: JSON.parse(template.variables || '[]'),
      isPublic: template.is_public === 1,
      isSystem: template.is_system === 1,
    },
    variables
  )
 
  // 获取模型配置
  const modelPreferences = template.model_preferences
    ? JSON.parse(template.model_preferences)
    : {}
 
  // 调用 LLM
  const modelRouter = new ModelRouter(c.env)
  const { model, config } = await modelRouter.selectModel({
    tenantId,
    requestedModel: modelPreferences.model,
  })
 
  const finalConfig = {
    ...config,
    temperature: modelPreferences.temperature ?? config.temperature,
    maxTokens: modelPreferences.maxTokens ?? config.maxTokens,
  }
 
  const response = await callLLM(
    [{ role: 'user', content }],
    model,
    finalConfig,
    c.env
  )
 
  // 更新使用次数
  await c.env.DB.prepare(`
    UPDATE prompt_templates SET usage_count = usage_count + 1 WHERE id = ?
  `).bind(templateId).run()
 
  return c.json({
    message: response.content,
    usage: response.usage,
    template: {
      id: template.id,
      name: template.name,
    },
  })
})

5. 模板版本管理

// 获取模板历史版本
app.get('/:id/versions', async (c) => {
  const templateId = c.req.param('id')
  const tenantId = c.get('tenantId')
 
  // 验证权限
  const template = await c.env.DB.prepare(`
    SELECT * FROM prompt_templates
    WHERE id = ? AND (tenant_id = ? OR is_public = 1 OR is_system = 1)
  `).bind(templateId, tenantId).first()
 
  if (!template) {
    return c.json({ error: '模板不存在' }, 404)
  }
 
  const versions = await c.env.DB.prepare(`
    SELECT v.*, u.email as createdByEmail, u.name as createdByName
    FROM prompt_template_versions v
    JOIN users u ON v.created_by = u.id
    WHERE v.template_id = ?
    ORDER BY v.version DESC
  `).bind(templateId).all()
 
  return c.json({
    versions: versions.results.map(v => ({
      id: v.id,
      version: v.version,
      content: v.content,
      variables: JSON.parse(v.variables || '[]'),
      createdBy: {
        email: v.createdByEmail,
        name: v.createdByName,
      },
      createdAt: v.created_at,
    })),
  })
})
 
// 回滚到指定版本
app.post('/:id/rollback', zValidator('json', z.object({
  version: z.number(),
})), async (c) => {
  const templateId = c.req.param('id')
  const tenantId = c.get('tenantId')
  const { version } = c.req.valid('json')
 
  // 验证权限
  const template = await c.env.DB.prepare(`
    SELECT * FROM prompt_templates WHERE id = ? AND tenant_id = ?
  `).bind(templateId, tenantId).first()
 
  if (!template) {
    return c.json({ error: '模板不存在' }, 404)
  }
 
  // 获取目标版本
  const targetVersion = await c.env.DB.prepare(`
    SELECT * FROM prompt_template_versions
    WHERE template_id = ? AND version = ?
  `).bind(templateId, version).first()
 
  if (!targetVersion) {
    return c.json({ error: '版本不存在' }, 404)
  }
 
  // 创建新版本(内容是目标版本的)
  const newVersion = template.version + 1
  const now = Date.now()
 
  await c.env.DB.batch([
    c.env.DB.prepare(`
      UPDATE prompt_templates
      SET content = ?, variables = ?, version = ?, updated_at = ?
      WHERE id = ?
    `).bind(targetVersion.content, targetVersion.variables, newVersion, now, templateId),
    c.env.DB.prepare(`
      INSERT INTO prompt_template_versions (id, template_id, version, content, variables, created_by, created_at)
      VALUES (?, ?, ?, ?, ?, ?, ?)
    `).bind(
      generateId(),
      templateId,
      newVersion,
      targetVersion.content,
      targetVersion.variables,
      c.get('userId'),
      now
    ),
  ])
 
  return c.json({ success: true, version: newVersion })
})

6. 收藏模板

app.post('/:id/favorite', async (c) => {
  const templateId = c.req.param('id')
  const userId = c.get('userId')
 
  const existing = await c.env.DB.prepare(`
    SELECT id FROM prompt_template_favorites WHERE user_id = ? AND template_id = ?
  `).bind(userId, templateId).first()
 
  if (existing) {
    return c.json({ error: '已收藏' }, 409)
  }
 
  await c.env.DB.prepare(`
    INSERT INTO prompt_template_favorites (id, user_id, template_id, created_at)
    VALUES (?, ?, ?, ?)
  `).bind(generateId(), userId, templateId, Date.now()).run()
 
  return c.json({ success: true })
})
 
app.delete('/:id/favorite', async (c) => {
  const templateId = c.req.param('id')
  const userId = c.get('userId')
 
  await c.env.DB.prepare(`
    DELETE FROM prompt_template_favorites WHERE user_id = ? AND template_id = ?
  `).bind(userId, templateId).run()
 
  return c.json({ success: true })
})
 
app.get('/favorites/list', async (c) => {
  const userId = c.get('userId')
 
  const templates = await c.env.DB.prepare(`
    SELECT t.* FROM prompt_templates t
    JOIN prompt_template_favorites f ON t.id = f.template_id
    WHERE f.user_id = ?
    ORDER BY f.created_at DESC
  `).bind(userId).all()
 
  return c.json({ templates: templates.results })
})

7. 系统预置模板

// 初始化系统模板
export async function initializeSystemTemplates(env: Env) {
  const systemTemplates = [
    {
      name: '通用助手',
      description: '通用的 AI 助手模板',
      category: 'chat',
      content: '你是一个友好的 AI 助手。请根据用户的问题提供帮助。',
      variables: [],
    },
    {
      name: '文本翻译',
      description: '将文本翻译成指定语言',
      category: 'writing',
      content: '请将以下{{sourceLanguage}}文本翻译成{{targetLanguage}}:\n\n{{text}}',
      variables: [
        { name: 'sourceLanguage', type: 'string', required: true },
        { name: 'targetLanguage', type: 'list', required: true, options: ['中文', '英文', '日文', '韩文'] },
        { name: 'text', type: 'string', required: true },
      ],
    },
    {
      name: '代码审查',
      description: '审查代码并提供改进建议',
      category: 'coding',
      content: '请审查以下{{language}}代码,指出潜在的问题并提供改进建议:\n\n```&#123;&#123;language&#125;&#125;\n&#123;&#123;code&#125;&#125;\n```',
      variables: [
        { name: 'language', type: 'list', required: true, options: ['JavaScript', 'Python', 'TypeScript', 'Go'] },
        { name: 'code', type: 'string', required: true },
      ],
    },
    {
      name: '文章摘要',
      description: '为长文章生成简洁摘要',
      category: 'writing',
      content: '请为以下文章生成一个{{length}}字的摘要:\n\n{{article}}',
      variables: [
        { name: 'length', type: 'number', required: true, defaultValue: 200 },
        { name: 'article', type: 'string', required: true },
      ],
    },
  ]
 
  const now = Date.now()
 
  for (const template of systemTemplates) {
    const existing = await env.DB.prepare(`
      SELECT id FROM prompt_templates WHERE name = ? AND is_system = 1
    `).bind(template.name).first()
 
    if (!existing) {
      await env.DB.prepare(`
        INSERT INTO prompt_templates (id, name, description, category, content, variables, is_system, version, created_at, updated_at)
        VALUES (?, ?, ?, ?, ?, ?, 1, 1, ?, ?)
      `).bind(
        generateId(),
        template.name,
        template.description,
        template.category,
        template.content,
        JSON.stringify(template.variables),
        now,
        now
      ).run()
    }
  }
}

8. 小结

Prompt 模板市场的关键点:

  1. 模板类型:系统预置、租户私有、公开分享
  2. 变量系统:支持字符串、数字、布尔、列表类型
  3. 版本管理:每次修改创建新版本,支持回滚
  4. 模板渲染:将变量替换到模板中生成最终 Prompt
  5. 收藏功能:用户可以收藏常用模板
  6. 使用统计:记录模板使用次数

一句话带走:

Prompt 模板市场是「模板 + 变量 + 版本」的三层架构。模板定义格式,变量实现动态,版本保证安全。系统预置兜底,租户自定义扩展,公开分享促进协作。