1. 项目缘起与核心思路拆解
最近在折腾AI提示词工程的时候,我偶然在Reddit上看到一个帖子,标题是“在147次失败的ChatGPT提示词尝试后,我有了一个想法”。这个帖子本身内容不多,但标题里蕴含的那种挫败感和随之而来的顿悟,瞬间就击中了我。我相信每一个深度使用过大型语言模型(LLM)的人,无论是ChatGPT、Claude还是国内的各类大模型,都经历过这种时刻:你心里有一个非常明确的目标,但无论你怎么调整你的问题描述、怎么变换句式、怎么添加“请一步步思考”或者“请扮演专家”这样的指令,模型给出的答案总是差那么点意思,要么过于笼统,要么完全跑偏,要么就是缺少你真正想要的那个“灵魂”。
这个名为“Lyra-”的项目,其灵感内核就来源于此。它不是一个功能复杂的软件,也不是一个庞大的系统,而是一个极其聚焦的工具:一个专门用于生成、管理和优化“系统提示词”(System Prompt)的Web界面。所谓“系统提示词”,就是你在与ChatGPT等模型对话时,在正式对话开始前,你给模型设定的那个“角色”或“背景设定”。比如,“你是一位资深的全栈软件工程师,精通Python和JavaScript,擅长用通俗易懂的语言解释复杂概念”,这就是一个系统提示词。它的好坏,直接决定了后续整个对话的基调和质量。
传统的做法是,我们把这些长长的、精心设计的提示词保存在记事本、备忘录或者某个聊天窗口里,每次使用的时候复制粘贴。但问题在于:
- 难以复用和对比:当你针对同一个任务(比如“代码审查”)设计了A、B、C三个不同侧重点的系统提示词时,来回切换、粘贴、测试非常麻烦。
- 缺乏结构化管理:提示词可能包含角色、任务、输出格式、禁忌等多个部分,混在一段文本里,修改其中一点就可能破坏整体结构。
- 优化过程不直观:哪次修改让结果变好了?哪次又变差了?缺乏一个直观的记录和对比环境。
Lyra- 就是为了解决这些痛点而生的。它的核心思路是:将系统提示词的创作和管理,变成一个可视化、可迭代、可A/B测试的轻量级工程化过程。你可以把它想象成一个专为提示词设计的“IDE”(集成开发环境)的雏形,或者一个功能强大的“提示词草稿本”。它不直接调用任何AI API,它的任务就是帮你把“喂养”给AI的“第一口饭”做得更香、更精准。
1.1 为什么是“系统提示词”?
这里需要深入解释一下。在与LLM交互时,提示主要分为两类:系统提示(System Prompt)和用户提示(User Prompt)。系统提示在对话开始时一次性注入,用于设定模型的全局行为、身份和知识边界;而用户提示则是我们每次对话时输入的具体问题。大量的实践表明,一个优秀的系统提示,往往能起到“四两拨千斤”的效果。它能在后台持续地、微妙地引导模型的思考方向,远比在每一个用户提示里重复强调规则要有效得多。
例如,如果你想让模型帮你进行严肃的学术分析,一个强有力的系统提示可能是:“你是一位严谨的哲学教授,你的分析必须基于文本证据,避免主观臆断,在给出任何结论前必须列出正反两方面的论点。” 这个设定会渗透到后续每一个回答中。Lyra- 聚焦于此,是因为这里是提示词工程中杠杆效应最高、也最容易被忽视的环节。
1.2 从灵感到原型:工具选型的考量
原帖只是一个想法,而我要把它实现出来。作为一个全栈开发者,我的选型基于以下几个原则:
- 极简与快速:想法需要快速验证,工具链必须轻量、高效,能让我专注于核心功能。
- 现代与通用:技术栈应具备良好的开发体验和广泛的适用性,方便其他开发者理解和复现。
- 前端为重:这是一个以交互和管理为核心的Web工具,前端体验至关重要。
基于此,我选择了以下技术栈:
- 前端框架:Vue 3 + Composition API。Vue 3的响应式系统非常适合构建这种状态驱动、交互复杂的单页面应用。Composition API相比Options API,在逻辑组织和复用上更灵活,尤其是在管理多个提示词版本和对比状态时。
- UI框架:Element Plus。它基于Vue 3,组件丰富且设计成熟,能极大加速开发进程,让我不用在基础组件(如按钮、输入框、布局、标签页)上花费过多时间。
- 构建工具:Vite。毋庸置疑的快。它的热更新速度对于需要频繁调整UI和逻辑的前期开发阶段来说,是巨大的生产力提升。
- 状态管理:Pinia。Vue官方的下一代状态管理库,比Vuex更简洁,TypeScript支持更好,非常适合管理提示词列表、当前编辑项、对比状态等全局数据。
- 本地存储:浏览器LocalStorage。作为一个初始版本,数据完全保存在本地是最简单、最直接的方式。它避免了服务器和数据库的依赖,让应用可以完全离线运行,也降低了分享和部署的复杂度。当然,这只是V1.0的选择,未来可以考虑加入云同步。
这个选型组合,让我能在几个小时内就搭起一个可运行的原型,并迅速迭代核心交互。
2. 核心功能解析与界面设计
Lyra- 的核心功能围绕“创建-编辑-测试-对比”这个循环展开。界面设计上,我遵循了“功能聚合,减少跳转”的原则,主要分为三个核心区域。
2.1 提示词编辑器:你的主工作台
这是应用的核心区域。我设计了一个全功能的文本编辑器,但不仅仅是简单的<textarea>。
- 语法高亮:虽然系统提示词本质是文本,但通过简单的规则(如识别
{括号}、[关键词]或## 标题),可以为其添加柔和的语法高亮,提升可读性。我使用了@codemirror编辑器内核,它轻量且高度可定制。 - 实时字数统计与Token估算:在编辑器底部,实时显示当前提示词的字符数和估算的Token数(基于类似GPT-3的Tokenizer进行粗略估算)。这一点非常重要,因为大多数模型对系统提示都有Token长度限制(如4096 tokens)。你必须时刻清楚自己是否在安全范围内。
- 常用片段插入:在编辑器侧边或顶部,我设置了一个“常用片段”抽屉。里面预置或由用户自定义一些高频使用的语句块,例如“请一步步思考”、“你的输出格式必须是JSON”、“严禁虚构信息”等。点击即可插入到光标位置,避免重复输入。
注意:Token估算只是一个近似值,不同模型的分词方式不同。这个功能的主要目的是提供一个相对参考,防止提示词无节制地膨胀。在关键使用场景前,最好还是到目标模型的官方工具中进行精确校验。
2.2 版本管理与历史快照
这是Lyra- 区别于记事本的核心功能之一。每次你点击“保存”,系统并不会覆盖之前的提示词,而是创建一个新的“版本快照”。
- 自动版本号:每次保存生成一个带时间戳的版本(如
v1.2 - 2023-10-27 14:30)。版本号可以手动修改以增加可读性(如v1.0-基础角色,v2.0-增加输出格式约束)。 - 版本树视图:在侧边栏,所有版本以时间线或树状图的形式列出。你可以清晰地看到提示词的演变历程。
- 一键回滚与对比:点击任意历史版本,编辑器内容会立即切换为该版本的内容。更重要的是,你可以选择任意两个版本,进入“对比模式”。差异会以行内高亮的形式显示出来(新增、删除、修改),让你一眼就看出每次迭代具体改了哪里。
这个功能的价值在于,它把提示词优化这个“黑盒”过程变得可追溯、可分析。你不再需要靠记忆去回想“我上次到底改了什么导致效果变差了?”,直接对比版本差异即可。
2.3 A/B测试面板:效果对比的利器
这是将想法落地的关键一步。设计出多个版本的提示词后,如何知道哪个更好?Lyra- 内置了一个简单的A/B测试环境。
- 选择测试用例:你首先需要定义一个或几个“标准问题”(User Prompts)。比如,对于“代码审查助手”这个角色,你的测试用例可以是:“请审查以下Python函数:
def calculate_average(nums): return sum(nums) / len(nums)”。 - 选择待测提示词:从你的版本列表中,选择两个(或更多)你想要对比的系统提示词版本,比如
版本A:强调安全性和版本B:强调性能。 - 并行执行与结果展示:界面会并排显示两个“对话窗口”。每个窗口都模拟了一个独立的对话,应用了对应的系统提示词,并自动发送你定义的标准问题。然后,你需要手动将两个模型的输出结果分别粘贴到对应的结果框中。是的,V1.0版本不直接调用API,而是采用“手动粘贴”的方式。这听起来有点原始,但有其深意:
- 成本与速度:直接调用API意味着需要处理密钥、网络延迟、计费等问题。手动粘贴虽然多了一步操作,但零成本、即时可用,且不受任何API限制或故障影响。
- 深度参与:迫使你仔细阅读和评估每一个输出结果,而不是匆匆一瞥。这个过程本身就是一个深度思考的过程。
- 结果保存:对比结果(包括系统提示词、用户问题、模型输出)可以作为一个“测试案例”保存下来,附在提示词项目里,形成宝贵的经验资产。
并排对比的视觉冲击力是非常强的。哪个回答更严谨?哪个更富有创意?哪个更严格遵守了格式要求?一目了然。这比在脑海里模糊地比较要有效得多。
3. 实操构建过程与核心代码实现
下面,我将拆解Lyra- 几个关键功能的实现思路和部分核心代码。假设你已经有一个使用Vite创建的Vue 3 + TypeScript项目,并安装了Element Plus和Pinia。
3.1 数据模型与状态管理设计
首先,我们需要定义核心的数据结构。在stores/promptStore.ts中,我们用Pinia来管理状态。
// stores/promptStore.ts import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; export interface PromptVersion { id: string; // 唯一ID,可以用Date.now().toString() name: string; // 版本名称,如 "v1.0" content: string; // 提示词内容 createdAt: number; // 创建时间戳 description?: string; // 可选描述,记录修改原因 } export interface PromptProject { id: string; title: string; // 项目/角色名称,如 "Python代码审查专家" currentVersionId: string; // 当前选中版本的ID versions: PromptVersion[]; // 所有版本历史 testCases: TestCase[]; // 关联的测试用例 } export interface TestCase { id: string; userPrompt: string; // 用于测试的用户问题 results: { [versionId: string]: string }; // 记录不同版本提示词下的输出结果 } export const usePromptStore = defineStore('prompt', () => { // 状态 const projects = ref<PromptProject[]>([]); const activeProjectId = ref<string>(''); // 计算属性 const activeProject = computed(() => projects.value.find(p => p.id === activeProjectId.value) ); const activeVersion = computed(() => { const proj = activeProject.value; if (!proj) return null; return proj.versions.find(v => v.id === proj.currentVersionId); }); // 动作 function createProject(title: string) { const newProject: PromptProject = { id: generateId(), title, currentVersionId: '', versions: [], testCases: [] }; projects.value.push(newProject); activeProjectId.value = newProject.id; // 同时创建第一个版本 createVersion('v1.0', '初始版本'); return newProject; } function createVersion(name: string, description?: string) { const proj = activeProject.value; if (!proj) return; // 基于当前版本内容创建新版本,如果无当前版本则用空字符串 const baseContent = activeVersion.value?.content || ''; const newVersion: PromptVersion = { id: generateId(), name, content: baseContent, createdAt: Date.now(), description }; proj.versions.push(newVersion); proj.currentVersionId = newVersion.id; // 保存到本地存储 saveToLocalStorage(); } function updateCurrentVersionContent(content: string) { const version = activeVersion.value; if (version) { version.content = content; saveToLocalStorage(); } } // 辅助函数:生成ID和本地存储 function generateId() { return Date.now().toString(36) + Math.random().toString(36).substr(2); } function saveToLocalStorage() { localStorage.setItem('lyra-projects', JSON.stringify(projects.value)); } function loadFromLocalStorage() { /* ... 从localStorage加载数据 ... */ } return { projects, activeProjectId, activeProject, activeVersion, createProject, createVersion, updateCurrentVersionContent, loadFromLocalStorage }; });这个Store管理了所有的提示词项目、版本和历史。createVersion函数是关键,它总是在当前内容基础上创建新版本,实现了“永不覆盖”的版本管理逻辑。
3.2 编辑器与实时Token估算集成
在编辑器组件components/PromptEditor.vue中,我们集成CodeMirror并实现Token估算。
<template> <div class="editor-container"> <div ref="editorRef" class="editor"></div> <div class="editor-status-bar"> <span>字符数: {{ charCount }}</span> <span> | 估算Tokens: {{ estimatedTokens }}</span> <el-button size="small" @click="handleSaveVersion">保存为新版本</el-button> </div> </div> </template> <script setup lang="ts"> import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'; import { EditorState } from '@codemirror/state'; import { EditorView, basicSetup } from 'codemirror'; import { usePromptStore } from '@/stores/promptStore'; const promptStore = usePromptStore(); const editorRef = ref<HTMLElement>(); let editorView: EditorView | null = null; // 初始化编辑器 onMounted(() => { if (!editorRef.value) return; const startState = EditorState.create({ doc: promptStore.activeVersion?.content || '', extensions: [ basicSetup, EditorState.readOnly.of(false), // 可以添加语法高亮、主题等扩展 EditorView.updateListener.of(update => { if (update.docChanged) { const content = update.state.doc.toString(); // 实时更新Store中的内容(防抖处理可以在实际应用中添加) promptStore.updateCurrentVersionContent(content); } }) ] }); editorView = new EditorView({ state: startState, parent: editorRef.value }); }); // 当切换活动版本时,更新编辑器内容 watch(() => promptStore.activeVersion, (newVersion) => { if (editorView && newVersion) { const currentContent = editorView.state.doc.toString(); if (currentContent !== newVersion.content) { editorView.dispatch({ changes: { from: 0, to: currentContent.length, insert: newVersion.content } }); } } }, { deep: true }); // 计算属性 const charCount = computed(() => promptStore.activeVersion?.content.length || 0); const estimatedTokens = computed(() => { // 一个非常粗略的估算:英文大致1 token ~ 4字符,中文1 token ~ 2字符。 // 这里采用一个简单的混合估算。更准确的估算需要集成类似`gpt-3-encoder`的库。 const text = promptStore.activeVersion?.content || ''; let tokens = 0; for (const char of text) { if (/[\u4e00-\u9fa5]/.test(char)) { tokens += 2; // 中文估2 tokens } else { tokens += 0.25; // 非中文估0.25 tokens (即4字符1token) } } return Math.ceil(tokens); }); // 保存版本 const handleSaveVersion = () => { const versionName = `v${(promptStore.activeProject?.versions.length || 0) + 1}.0`; const desc = window.prompt('为新版本添加描述(可选):', `基于${promptStore.activeVersion?.name}的修改`); promptStore.createVersion(versionName, desc || undefined); }; onBeforeUnmount(() => { editorView?.destroy(); }); </script>这个组件负责核心的编辑功能。通过EditorView.updateListener,我们将编辑器的每一次变化都同步到Pinia Store中,实现了自动保存草稿的效果。Token估算虽然粗糙,但足以起到警示作用。
3.3 实现A/B测试对比面板
A/B测试面板components/AbTestPanel.vue是交互最复杂的部分。它的核心逻辑是管理两组独立的状态。
<template> <div class="ab-test-panel"> <div class="config-section"> <el-input v-model="testUserPrompt" placeholder="输入用于测试的标准用户问题..." type="textarea" rows="3"/> <el-button @click="runTest" :disabled="!selectedVersions[0] || !selectedVersions[1]">执行对比测试</el-button> </div> <div class="comparison-section"> <!-- 版本A --> <div class="version-panel"> <h4>版本 A: {{ selectedVersions[0]?.name || '未选择' }}</h4> <el-select v-model="selectedVersions[0]" placeholder="选择版本A"> <el-option v-for="v in availableVersions" :key="v.id" :label="v.name" :value="v"/> </el-select> <div class="system-prompt-preview">{{ selectedVersions[0]?.content }}</div> <h5>模型输出结果:</h5> <el-input v-model="results[selectedVersions[0]?.id]" type="textarea" :rows="8" placeholder="将AI对上述问题的回答粘贴到这里..."/> </div> <!-- 版本B --> <div class="version-panel"> <h4>版本 B: {{ selectedVersions[1]?.name || '未选择' }}</h4> <el-select v-model="selectedVersions[1]" placeholder="选择版本B"> <el-option v-for="v in availableVersions" :key="v.id" :label="v.name" :value="v"/> </el-select> <div class="system-prompt-preview">{{ selectedVersions[1]?.content }}</div> <h5>模型输出结果:</h5> <el-input v-model="results[selectedVersions[1]?.id]" type="textarea" :rows="8" placeholder="将AI对上述问题的回答粘贴到这里..."/> </div> </div> <div class="action-section"> <el-button @click="saveTestCase">保存此测试案例</el-button> </div> </div> </template> <script setup lang="ts"> import { ref, computed } from 'vue'; import { usePromptStore } from '@/stores/promptStore'; import type { PromptVersion } from '@/stores/promptStore'; const promptStore = usePromptStore(); const testUserPrompt = ref(''); const selectedVersions = ref<[PromptVersion | null, PromptVersion | null]>([null, null]); const results = ref<Record<string, string>>({}); // key: versionId, value: output const availableVersions = computed(() => promptStore.activeProject?.versions || []); const runTest = () => { // 这里不实际调用API,只是清空旧结果,提示用户去外部测试 if (selectedVersions.value[0]) results.value[selectedVersions.value[0].id] = ''; if (selectedVersions.value[1]) results.value[selectedVersions.value[1].id] = ''; alert(`请分别打开两个AI对话窗口,应用对应的系统提示词,并向它们提问:“${testUserPrompt.value}”,然后将回答粘贴到上方对应的结果框中。`); }; const saveTestCase = () => { if (!promptStore.activeProject || !testUserPrompt.value) return; // 将当前测试用例和结果保存到项目的testCases中 const newTestCase = { id: Date.now().toString(), userPrompt: testUserPrompt.value, results: { ...results.value } }; promptStore.activeProject.testCases.push(newTestCase); promptStore.saveToLocalStorage(); // 假设Store中有此方法 alert('测试案例已保存!'); }; </script> <style scoped> .comparison-section { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px; } .system-prompt-preview { background: #f5f7fa; padding: 10px; border-radius: 4px; margin: 10px 0; font-size: 0.9em; max-height: 150px; overflow-y: auto; white-space: pre-wrap; } </style>这个面板的逻辑很清晰:选择两个版本,定义一个测试问题,然后用户手动去外部获取结果并粘贴回来进行对比。system-prompt-preview区域用于快速预览选中的系统提示词,确保你没有选错版本。保存测试案例的功能,使得每一次有价值的对比都能被记录下来,成为优化过程中的重要参考。
4. 部署、使用心得与避坑指南
开发完成后,我使用Vite的构建命令npm run build生成了静态文件,并部署到了GitHub Pages。整个过程非常简单,因为项目纯前端,没有任何服务器依赖。
4.1 实际使用流程与心得
在实际使用Lyra- 优化我的“技术写作助手”提示词时,我总结了一个高效的流程:
- 创建项目:新建一个名为“技术博客起草助手”的项目。
- 撰写初稿(v1.0):在编辑器中写下第一个版本的系统提示词,例如:“你是一位经验丰富的技术博主,擅长将复杂概念用通俗易懂、生动有趣的语言解释清楚...”。
- 保存版本:点击保存,命名为“v1.0-基础设定”。
- 迭代优化:根据第一次测试输出的结果,我发现文章风格有点过于随意。于是,我直接在编辑器中修改,增加了“文章结构需清晰,需包含引言、主体、总结三部分”的约束,然后点击“保存为新版本”,命名为“v2.0-增加结构要求”。
- A/B测试:在A/B测试面板,我选择v1.0和v2.0,输入同一个测试问题:“请用500字左右,向编程新手解释什么是RESTful API”。然后,我分别打开两个ChatGPT窗口,应用对应的系统提示词,提问,并将回答粘贴回Lyra-。
- 对比决策:并排对比发现,v2.0版本的文章确实结构更清晰,但v1.0的语言更活泼有趣。于是,我创建v3.0,尝试融合两者优点:“...语言风格需在保持专业准确的同时,适当活泼...文章结构需清晰...”。
- 固化与复用:经过几轮测试,我确定了v3.5为最终版本。我可以将这个提示词项目导出为JSON文件,或者直接复制内容,用于任何支持系统提示词的AI平台。
使用心得:
- 小步快跑,频繁保存:不要在一个版本里做大量修改。每有一个明确的优化想法(比如“加强语气”、“增加示例要求”、“限定输出长度”),就保存一个新版本。这能让版本历史更清晰。
- 测试用例要具体、有代表性:你的测试用户问题(User Prompt)应该能精准地触发你关心的点。比如测试“代码审查助手”,就用一个包含典型安全漏洞(如SQL注入)的代码片段作为测试用例。
- 对比时关注“差异点”:对比A/B结果时,不要泛泛地看“哪个更好”,而是带着问题看:“在‘指出潜在问题’这一点上,A和B谁更细致?”、“在‘给出修改建议’的可行性上,谁更强?”。这样优化方向才明确。
- 善用“描述”字段:每次保存版本时,花几秒钟写下修改描述,比如“增加了禁止生成代码注释的规则”。一周后回头看,这些描述能帮你快速理解当时的思路。
4.2 常见问题与排查技巧
在开发和使用的过程中,我也踩过一些坑,这里分享给大家:
问题1:编辑器内容在切换版本后,修改未保存却丢失了?
- 现象:我正在编辑v2.0,没有点保存,然后去侧边栏点击了v1.0查看,再点回v2.0,发现刚才的编辑不见了。
- 原因与解决:这是最初版本的一个Bug。因为编辑器内容直接绑定到
activeVersion.content,切换版本时,编辑器内容被新版本的内容覆盖,未保存的修改就丢了。解决方案是引入一个“编辑缓冲区”的概念。在组件内部维护一个draftContent的ref,它实时同步编辑器的内容。只有当用户主动点击“保存”时,才将draftContent提交到Store中更新当前版本。切换版本时,用新版本的内容重置draftContent。这样,未保存的修改只存在于当前编辑会话中,切换版本会给出“是否保存”的提示或自动放弃草稿,逻辑更清晰。
问题2:LocalStorage存储空间有限,提示词项目多了怎么办?
- 现象:随着创建的提示词项目和版本越来越多,可能会碰到浏览器本地存储的空间限制(通常是5MB左右)。
- 解决思路:
- 定期清理:在应用内增加“项目归档”或“删除旧版本”的功能。对于已确定的最终版本,可以只保留该版本,删除中间的迭代历史。
- 导出备份:提供完整的项目导出功能(JSON格式),让用户可以手动将不常用的项目备份到本地文件,然后从应用中删除。
- 升级存储方案:这是未来的演进方向。可以考虑集成IndexedDB,它拥有更大的存储空间。或者,为应用增加后端支持,将数据同步到云端。
问题3:A/B测试手动粘贴结果太麻烦,能自动化吗?
- 这是最常被问到的功能需求。自动化调用API确实能极大提升效率,但也会带来复杂性:
- API密钥管理:需要让用户安全地输入和存储他们的OpenAI、Claude等平台的API密钥。
- 多模型支持:不同模型的API接口、参数、费用都不一样。
- 网络与错误处理:需要处理网络超时、API限流、扣费失败等问题。
- 我的建议是分步走:V1.0坚持手动粘贴,因为它零成本、无风险、适用于所有模型(包括网页版)。在后续版本中,可以作为一个“高级功能”或“插件”来提供。提供一个配置面板,让用户自行填写API端点、密钥和模型参数。务必做好风险提示,明确告知用户自动化调用会产生费用,并且他们的密钥需要自行保管。
问题4:如何组织越来越复杂的提示词?
- 现象:一个强大的系统提示词可能包含角色设定、任务描述、思考链指令、输出格式规范、禁忌列表等多个模块,全部写在一起显得冗长混乱。
- 进阶功能设想:可以引入“模块化”或“片段”管理。在编辑器中,不是纯文本,而是可以插入/拖拽预定义的“角色卡”、“格式模板”、“规则列表”等片段。后台依然存储为完整文本,但前端编辑体验更结构化。这可以作为Lyra- 未来一个重要的差异化功能。
Lyra- 这个项目从一个小小的Reddit帖子灵感开始,最终落地成一个切实可用的工具。它印证了一个简单的道理:最好的工具往往诞生于解决自身痛点的过程。它可能没有酷炫的AI功能,但它精准地解决了一个高频、刚需且被忽视的痛点——如何科学地管理和优化你与AI对话中最关键的那段“开场白”。如果你也厌倦了在无数个聊天窗口和记事本之间切换,不妨试试这个思路,或者基于这个开源项目打造属于你自己的提示词工作台。