基于Vue 3的AI系统提示词工程化工具Lyra-设计与实现
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 refPromptProject[]([]); const activeProjectId refstring(); // 计算属性 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 classeditor-container div refeditorRef classeditor/div div classeditor-status-bar span字符数: {{ charCount }}/span span | 估算Tokens: {{ estimatedTokens }}/span el-button sizesmall clickhandleSaveVersion保存为新版本/el-button /div /div /template script setup langts 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 refHTMLElement(); 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 classab-test-panel div classconfig-section el-input v-modeltestUserPrompt placeholder输入用于测试的标准用户问题... typetextarea rows3/ el-button clickrunTest :disabled!selectedVersions[0] || !selectedVersions[1]执行对比测试/el-button /div div classcomparison-section !-- 版本A -- div classversion-panel h4版本 A: {{ selectedVersions[0]?.name || 未选择 }}/h4 el-select v-modelselectedVersions[0] placeholder选择版本A el-option v-forv in availableVersions :keyv.id :labelv.name :valuev/ /el-select div classsystem-prompt-preview{{ selectedVersions[0]?.content }}/div h5模型输出结果/h5 el-input v-modelresults[selectedVersions[0]?.id] typetextarea :rows8 placeholder将AI对上述问题的回答粘贴到这里.../ /div !-- 版本B -- div classversion-panel h4版本 B: {{ selectedVersions[1]?.name || 未选择 }}/h4 el-select v-modelselectedVersions[1] placeholder选择版本B el-option v-forv in availableVersions :keyv.id :labelv.name :valuev/ /el-select div classsystem-prompt-preview{{ selectedVersions[1]?.content }}/div h5模型输出结果/h5 el-input v-modelresults[selectedVersions[1]?.id] typetextarea :rows8 placeholder将AI对上述问题的回答粘贴到这里.../ /div /div div classaction-section el-button clicksaveTestCase保存此测试案例/el-button /div /div /template script setup langts 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 refRecordstring, 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。这样未保存的修改只存在于当前编辑会话中切换版本会给出“是否保存”的提示或自动放弃草稿逻辑更清晰。问题2LocalStorage存储空间有限提示词项目多了怎么办现象随着创建的提示词项目和版本越来越多可能会碰到浏览器本地存储的空间限制通常是5MB左右。解决思路定期清理在应用内增加“项目归档”或“删除旧版本”的功能。对于已确定的最终版本可以只保留该版本删除中间的迭代历史。导出备份提供完整的项目导出功能JSON格式让用户可以手动将不常用的项目备份到本地文件然后从应用中删除。升级存储方案这是未来的演进方向。可以考虑集成IndexedDB它拥有更大的存储空间。或者为应用增加后端支持将数据同步到云端。问题3A/B测试手动粘贴结果太麻烦能自动化吗这是最常被问到的功能需求。自动化调用API确实能极大提升效率但也会带来复杂性API密钥管理需要让用户安全地输入和存储他们的OpenAI、Claude等平台的API密钥。多模型支持不同模型的API接口、参数、费用都不一样。网络与错误处理需要处理网络超时、API限流、扣费失败等问题。我的建议是分步走V1.0坚持手动粘贴因为它零成本、无风险、适用于所有模型包括网页版。在后续版本中可以作为一个“高级功能”或“插件”来提供。提供一个配置面板让用户自行填写API端点、密钥和模型参数。务必做好风险提示明确告知用户自动化调用会产生费用并且他们的密钥需要自行保管。问题4如何组织越来越复杂的提示词现象一个强大的系统提示词可能包含角色设定、任务描述、思考链指令、输出格式规范、禁忌列表等多个模块全部写在一起显得冗长混乱。进阶功能设想可以引入“模块化”或“片段”管理。在编辑器中不是纯文本而是可以插入/拖拽预定义的“角色卡”、“格式模板”、“规则列表”等片段。后台依然存储为完整文本但前端编辑体验更结构化。这可以作为Lyra- 未来一个重要的差异化功能。Lyra- 这个项目从一个小小的Reddit帖子灵感开始最终落地成一个切实可用的工具。它印证了一个简单的道理最好的工具往往诞生于解决自身痛点的过程。它可能没有酷炫的AI功能但它精准地解决了一个高频、刚需且被忽视的痛点——如何科学地管理和优化你与AI对话中最关键的那段“开场白”。如果你也厌倦了在无数个聊天窗口和记事本之间切换不妨试试这个思路或者基于这个开源项目打造属于你自己的提示词工作台。