跳转到主要内容
把 prompt 当代码对待:改一句就是一次发布。vibestrap 用两张表存 prompt —— prompt (slug + 最新指针)和 prompt_version(不可变历史)。新版本靠 INSERT 一行发布,绝 不去 UPDATE 已有行。回滚 = 把老版本的文本作为新版本再发布一次。可审计、无 race、 无聊 —— 这就是好特性。

前置条件

  • 数据库已经迁移(pnpm db:push),prompt / prompt_version 表存在。
  • 已登录的 admin 用户 —— 只有 adminActionClient 应该有发布权限。
  • 看一遍 src/db/ai.schema.ts 了解列级真相。

两张表

// src/db/ai.schema.ts
export const prompt = pgTable('prompt', {
  id: text('id').primaryKey(),               // nanoid 前缀:'pmt_...'
  slug: text('slug').notNull(),              // 唯一 —— 对外名字
  name: text('name').notNull(),
  description: text('description').notNull().default(''),
  latestVersionId: text('latest_version_id'),// 指向当前发布的版本
  // ...时间戳、createdBy
});

export const promptVersion = pgTable('prompt_version', {
  id: text('id').primaryKey(),
  promptId: text('prompt_id').notNull().references(() => prompt.id),
  version: integer('version').notNull(),     // 1, 2, 3,按 prompt 单调
  template: text('template').notNull(),      // 内容,含 {{variable}} 占位符
  variables: jsonb('variables').$type<Record<string, { description?: string; required?: boolean }>>(),
  note: text('note').notNull().default(''),
  // ...createdAt, createdBy
});
(promptId, version) 上的唯一索引保证版本序列不乱。latestVersionId 是反规范化的 指针,方便一查到位。

步骤:注册一个新 prompt

三条写操作放进一个事务,避免落到中间状态。
import { db } from '@/db';
import { prompt, promptVersion } from '@/db/schema';
import { newPrefixedId } from '@/lib/id';

await db.transaction(async (tx) => {
  const promptId = newPrefixedId('pmt');
  const versionId = newPrefixedId('pv');

  await tx.insert(prompt).values({
    id: promptId,
    slug: 'summarize-article',
    name: '总结一篇文章',
  });

  await tx.insert(promptVersion).values({
    id: versionId,
    promptId,
    version: 1,
    template: '用 3 条要点总结以下文章:\n\n{{article}}',
    variables: { article: { description: '原文', required: true } },
  });

  await tx.update(prompt).set({ latestVersionId: versionId }).where(eq(prompt.id, promptId));
});
发布 v2 同理:插入一行 version: 2,更新 latestVersionId。老版本通过 (promptId, version) 唯一索引依然可查。

客户端渲染

usePrompt(slug) 拉最新版本,并暴露 render(vars)
const { prompt, render } = usePrompt('summarize-article');
const finalText = prompt ? render({ article: input }) : null;
// finalText 传给 useGeneration → 服务端 → AI provider
替换是正则的({{name}} → 值)。缺失变量会变成空串 —— 客户端不强制 required,要在 服务端发起调用前自己校验。

服务端渲染

别 import hook,直接查:
import { db } from '@/db';
import { prompt, promptVersion } from '@/db/schema';
import { eq } from 'drizzle-orm';

const row = await db
  .select({
    template: promptVersion.template,
    promptId: prompt.id,
    promptVersionId: promptVersion.id,
  })
  .from(prompt)
  .innerJoin(promptVersion, eq(promptVersion.id, prompt.latestVersionId))
  .where(eq(prompt.slug, 'summarize-article'))
  .then((r) => r[0]);

const text = row.template.replace(/{{\s*(\w+)\s*}}/g, (_, k) => values[k] ?? '');
chat({ ... }, { promptId, promptVersionId, userId }),这样 ai_call 行就能记到 是哪一版 prompt 触发的。

回滚

没有 UPDATE。回滚 = 「把旧文本作为新版本发布」:
  1. 拉出你想恢复那一版 promptVersion
  2. 插入新行 version: max+1,模板照抄,note 写 'rollback to v3'
  3. 更新 latestVersionId 到这条新行。
历史是线性的,审计链完整,正在路上的请求也不会出意外。

验证生效

  1. 按上面步骤插入一个 prompt。
  2. psqlSELECT slug, latest_version_id FROM prompt WHERE slug = 'summarize-article';latest_version_id 应该有值。
  3. 在客户端组件里调 usePrompt('summarize-article').render({ article: 'hello' }),应该拿到替换后的 字符串。
  4. { promptId, promptVersionId } 参数发一次 AI 调用,查 ai_callprompt_idprompt_version_id 列应该填上了。

常见坑

  • insert 后忘了更新 latestVersionId 版本创建出来了,但 usePrompt 返回 null —— 指针没动。永远在同一个事务里更新。
  • 去 UPDATE 已有的 prompt_version 行。 别这样。唯一索引拦不住你,但这么做会 悄无声息地改写历史,让 ai_call.promptVersionId 的引用变成谎言。请插新版本。
  • 服务端漏校 required 变量。 render 会乐呵呵替成空串。调 chat 之前先 Object.entries(variables).filter(([_, s]) => s.required) 校一遍。
  • slug 撞名。 prompt.slug 有唯一索引 —— insert 直接抛错。换成 summarize-article-v2 这种命名空间式 slug,别复用旧的。
  • prompt 来自磁盘文件。 如果你的模板是构建时读的 .txt,数据库根本不知道。请 只保留一个真相源。

官方文档