尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于ProseMirror构建AI智能编辑器:从文档模型到交互协议全流程实战

基于ProseMirror构建AI智能编辑器:从文档模型到交互协议全流程实战 1. 从“输入框”到“智能工作台”的认知跃迁我们每天都在和各种输入框打交道从最简单的单行文本框到功能稍强的富文本编辑器。但你是否想过一个输入框的潜力远不止于此它完全可以成为一个集成了AI能力的“超级入口”——一个能理解你意图、辅助你创作、甚至帮你完成复杂任务的智能工作台。这听起来像是未来但通过ProseMirror这个强大的编辑器框架我们今天就能亲手把它搭建出来。ProseMirror是什么简单说它是一个用于在Web上构建富文本编辑器的工具包。但它的强大之处在于它不仅仅是一个“所见即所得”的编辑器更是一个文档模型和状态管理的框架。这意味着我们可以精确地控制文档的每一个节点、每一次变更并在此基础上无缝地注入AI能力。比如你输入“/summarize”编辑器能理解这是一个指令并调用AI接口自动总结上一段内容或者你选中一段代码编辑器能调用AI进行解释或重构。这篇文章我将带你从零开始完成一个集成AI能力的ProseMirror编辑器的全流程实战。这不是一个简单的“调用API”的教程而是深入到如何设计编辑器与AI的交互协议、如何管理异步的AI状态、如何优雅地处理AI生成的内容插入等核心问题。无论你是前端开发者想为产品增加智能特性还是对下一代人机交互感兴趣这篇内容都将提供一套可落地的完整方案。2. 为什么是ProseMirror核心优势与项目选型在开始敲代码之前我们必须先回答一个问题市面上编辑器框架那么多为什么偏偏选择ProseMirror直接使用现成的富文本编辑器如TinyMCE、Quill然后外挂一个AI按钮不行吗答案是可以但体验会非常割裂且能力受限。ProseMirror的选择是基于以下几个关键考量2.1 基于Schema的强类型文档模型这是ProseMirror的基石。在ProseMirror中文档不是一个充满HTML标签的字符串而是一个由严格定义的“节点”和“标记”组成的树状结构。我们可以定义一个paragraph节点、一个code_block节点甚至自定义一个ai_command节点。这种强类型模型带来了两个决定性优势 第一数据纯净且结构化。AI模型处理结构化数据远比处理杂乱的HTML要高效和准确。我们可以轻松地将文档的特定部分如一个代码块序列化成JSON送给AI也能精准地将AI返回的结构化结果如一个列表反序列化成编辑器节点。 第二行为可预测且可扩展。每个节点类型都可以定义自己的“视图”和“行为”。这意味着我们可以为ai_command节点设计一个特殊的渲染方式比如显示一个旋转的加载图标并定义当用户点击它时触发什么操作。2.2 一流的变更追踪与协同编辑支持ProseMirror内置了Transaction事务系统每一次对文档的修改输入、删除、格式化都是一个可追溯、可撤销、可重放的事务。这对于AI集成至关重要。当AI异步返回内容时我们需要确保在AI“思考”的这段时间里用户的其他编辑操作不会导致状态错乱。ProseMirror的事务系统让我们能安全地处理这种并发编辑场景。此外其底层设计天然支持OT操作转换为未来实现多人协同的AI辅助编辑打下了基础。2.3 插件化架构与低层级API控制ProseMirror本身是一个“内核”所有功能如菜单、快捷键、输入规则都以插件形式存在。这种架构让我们可以以最小的侵入性集成AI能力。我们可以编写一个aiAssistantPlugin在其中集中管理所有AI相关的状态、事件和UI而不会污染核心的编辑逻辑。同时ProseMirror提供了低层级的API允许我们直接操作选区、调度自定义事务这为实现复杂的AI交互如在特定位置插入一个交互式AI控件提供了可能。相比之下传统富文本编辑器更像是黑盒其内部文档模型通常是扁平的HTML难以进行精细化的控制和扩展。外挂AI按钮只能实现“选中文本 - 点击按钮 - 替换文本”这种粗粒度的交互无法实现行内AI建议、连续对话、上下文感知等高级功能。因此选择ProseMirror就是选择了一条既能实现深度集成又能保证架构优雅和未来可扩展性的技术路径。3. 搭建基础编辑器环境与核心插件配置理论说得再多不如动手搭建。我们首先创建一个最基础的ProseMirror编辑器然后逐步加入我们需要的核心插件。3.1 初始化项目与安装依赖假设我们使用Vite TypeScript来构建项目这能为我们提供良好的开发体验和类型安全。# 创建项目 npm create vitelatest prose-mirror-ai-starter -- --template vanilla-ts cd prose-mirror-ai-starter # 安装ProseMirror核心包 npm install prosemirror-state prosemirror-view prosemirror-model prosemirror-schema-basic prosemirror-schema-list prosemirror-keymap prosemirror-commands prosemirror-history prosemirror-inputrules prosemirror-dropcursor prosemirror-gapcursor3.2 构建基础Schema与编辑器状态在src/editor.ts中我们开始构建编辑器的核心。首先定义我们的文档Schema。我们将基于基础Schema添加一些对AI协作友好的节点。import { Schema } from prosemirror-model; import { schema as baseSchema } from prosemirror-schema-basic; import { orderedList, bulletList, listItem } from prosemirror-schema-list; // 扩展基础Schema创建我们自己的Schema export const mySchema new Schema({ nodes: { doc: baseSchema.spec.nodes.get(doc), paragraph: baseSchema.spec.nodes.get(paragraph), blockquote: baseSchema.spec.nodes.get(blockquote), horizontal_rule: baseSchema.spec.nodes.get(horizontal_rule), heading: baseSchema.spec.nodes.get(heading), code_block: { // 重点为代码块定义更详细的属性便于AI识别语言 ...baseSchema.spec.nodes.get(code_block), attrs: { language: { default: plaintext } }, }, text: baseSchema.spec.nodes.get(text), // 列表节点 ordered_list: orderedList, bullet_list: bulletList, list_item: listItem, // 自定义AI命令占位符节点 ai_command: { inline: true, group: inline, draggable: false, attrs: { type: { default: }, state: { default: pending } }, // state: pending, thinking, done, error parseDOM: [{ tag: span.ai-command }], toDOM(node) { return [span, { class: ai-command ai-command-${node.attrs.state}, data-type: node.attrs.type }, ]; }, }, }, marks: baseSchema.spec.marks, });这里的关键是自定义的ai_command节点。它是一个行内节点用来在文档中可视化地表示一个AI指令如正在思考、执行完成。我们通过attrs来存储指令类型和状态并通过CSS类来控制其外观。接下来创建编辑器状态和视图import { EditorState } from prosemirror-state; import { EditorView } from prosemirror-view; import { keymap } from prosemirror-keymap; import { baseKeymap } from prosemirror-commands; import { history, redo, undo } from prosemirror-history; import { dropCursor } from prosemirror-dropcursor; import { gapCursor } from prosemirror-gapcursor; // 创建插件集合 function setupPlugins() { return [ history(), // 历史记录支持撤销/重做 keymap(baseKeymap), // 基础快捷键如CtrlZ, CtrlB keymap({ Mod-z: undo, Mod-y: redo, Mod-Shift-z: redo }), // 明确的撤销/重做映射 dropCursor(), // 拖拽时显示光标 gapCursor(), // 允许在不可编辑位置插入光标 // 后续我们的AI插件将在这里加入 ]; } // 初始化编辑器状态 const initialState EditorState.create({ doc: mySchema.topNodeType.create(null, mySchema.text(在这里开始输入试试输入“/”触发AI命令...)), plugins: setupPlugins(), }); // 创建编辑器视图并挂载到DOM const editorView new EditorView(document.querySelector(#editor), { state: initialState, });现在一个具备基本编辑能力的ProseMirror编辑器已经运行起来了。你可以输入文字使用**加粗**等Markdown快捷方式需要额外配置inputRules以及撤销重做。4. 设计AI与编辑器的交互协议从指令识别到状态管理这是整个项目的核心设计环节。AI能力如何被触发编辑器如何与异步的AI服务通信如何管理AI任务的生命周期我们需要设计一套清晰的交互协议。4.1 指令识别与解析Slash Command模式我们采用类似Notion AI或Cursor的“/”命令模式。当用户输入“/”时我们弹出一个命令面板。这需要通过输入规则插件来实现。import { InputRule } from prosemirror-inputrules; // 定义AI命令类型 type AICommandType summarize | translate | explain_code | continue_writing | fix_grammar; // 创建Slash命令输入规则 function createSlashCommandRule() { return new InputRule(/^\/(\w)?$/, (state, match, start, end) { // match[1] 是“/”后面输入的内容 const commandText match[1]?.toLowerCase() || ; const tr state.tr; // 删除输入的“/xxx” tr.delete(start, end); // 在当前位置插入一个ai_command节点 const aiCommandNode mySchema.nodes.ai_command.create({ type: commandText, state: pending }); tr.insert(start, aiCommandNode); // 同时我们可以在这里触发一个事件通知UI层显示命令面板 // 为了解耦我们可以使用Plugin的state来管理 tr.setMeta(showAIPalette, { query: commandText, pos: start }); return tr; }); }这个规则监听以“/”开头的输入。当用户输入“/sum”时它会删除“/sum”并在该位置插入一个状态为pending的ai_command节点同时通过事务的元数据setMeta来触发UI更新。4.2 构建AI状态管理插件我们需要一个专门的插件来集中管理所有AI相关的状态当前活动的AI任务、命令面板的显示、AI模型的选择等。这是避免状态分散和逻辑混乱的关键。import { Plugin, PluginKey } from prosemirror-state; interface AIAssistantState { // 当前显示的命令面板信息 palette: { query: string; pos: number; visible: boolean } | null; // 正在进行的AI任务队列 activeTasks: Array{ taskId: string; pos: number; command: AICommandType }; // 选中的AI模型配置 selectedModel: string; } export const aiAssistantPluginKey new PluginKeyAIAssistantState(aiAssistant); export const aiAssistantPlugin new PluginAIAssistantState({ key: aiAssistantPluginKey, state: { init() { return { palette: null, activeTasks: [], selectedModel: gpt-4 }; }, apply(tr, prevState) { // 通过事务元数据更新状态 const paletteMeta tr.getMeta(showAIPalette); const taskMeta tr.getMeta(updateAITask); let newState { ...prevState }; if (paletteMeta) { newState.palette { ...paletteMeta, visible: true }; } // 如果事务是普通编辑且命令面板已打开则关闭它除非是在面板内输入 else if (!tr.selectionSet prevState.palette?.visible) { newState.palette { ...prevState.palette, visible: false }; } if (taskMeta) { // 处理任务的开始、更新、结束 if (taskMeta.action start) { newState.activeTasks [...prevState.activeTasks, taskMeta.task]; } else if (taskMeta.action end) { newState.activeTasks prevState.activeTasks.filter(t t.taskId ! taskMeta.taskId); } } return newState; }, }, // 插件可以持有视图用于执行副作用如发起网络请求 view(editorView) { return { update(view, prevState) { const state aiAssistantPluginKey.getState(view.state); const prev aiAssistantPluginKey.getState(prevState); // 监听状态变化执行相应操作 // 例如当palette.visible从false变为true时渲染命令面板DOM if (state.palette?.visible !prev?.palette?.visible) { renderAIPalette(view, state.palette); } // 当有新的activeTask加入时发起AI请求 if (state.activeTasks.length prev?.activeTasks.length) { const newTask state.activeTasks[state.activeTasks.length - 1]; executeAITask(view, newTask); } }, }; }, });这个插件是AI功能的中枢神经系统。它将UI状态命令面板和任务状态异步AI调用统一管理并通过ProseMirror的插件视图生命周期来协调副作用。4.3 AI任务执行与内容插入策略当用户从命令面板选择了一个具体命令如“总结上一段”我们需要执行AI任务。这个过程必须是异步且非阻塞的并且要处理好可能发生的编辑冲突。async function executeAITask(editorView: EditorView, task: { taskId: string; pos: number; command: AICommandType }) { const { state, dispatch } editorView; // 1. 更新ai_command节点状态为“thinking” let tr state.tr; const nodePos tr.doc.resolve(task.pos); const aiNode nodePos.parent.childAfter(nodePos.parentOffset); if (aiNode.node?.type.name ai_command) { tr.setNodeMarkup(task.pos, undefined, { ...aiNode.node.attrs, state: thinking }); dispatch(tr); } // 2. 根据命令类型准备上下文和提示词 const context extractContextForAI(state, task.pos, task.command); const prompt buildPrompt(task.command, context); try { // 3. 调用AI服务这里用fetch模拟实际可能是WebSocket或Server-Sent Events const response await callAIService(prompt, task.command); // 4. AI返回后生成ProseMirror可插入的节点片段 const contentFragment convertAIResponseToFragment(state.schema, response); // 5. 在一个新的事务中替换ai_command节点为实际内容 // 使用dispatch确保在正确的视图更新周期内 editorView.dispatch( state.tr .replaceWith(task.pos, task.pos aiNode.node.nodeSize, contentFragment) .setMeta(updateAITask, { action: end, taskId: task.taskId }) ); } catch (error) { // 6. 错误处理将节点状态更新为error并可能显示错误信息 editorView.dispatch( state.tr .setNodeMarkup(task.pos, undefined, { ...aiNode.node.attrs, state: error }) .setMeta(updateAITask, { action: end, taskId: task.taskId }) ); console.error(AI task failed:, error); } } // 辅助函数提取AI任务所需的上下文 function extractContextForAI(state: EditorState, pos: number, command: AICommandType): string { const doc state.doc; const resolvedPos doc.resolve(pos); switch (command) { case summarize: // 获取光标所在段落的前一个段落 const prevParagraphPos findPrevNodeOfType(pos, state.schema.nodes.paragraph); if (prevParagraphPos -1) { return doc.textBetween(prevParagraphPos, prevParagraphPos doc.resolve(prevParagraphPos).parent.nodeSize); } return ; case explain_code: // 获取光标所在的代码块 const codeBlockNode resolvedPos.parent; if (codeBlockNode.type.name code_block) { return codeBlockNode.textContent; } return ; // ... 其他命令的上下文提取逻辑 default: return doc.textBetween(Math.max(0, pos - 500), pos); // 默认取光标前500字符 } }这个执行流程体现了几个关键设计状态可视化通过更新ai_command节点的状态属性用户能立即看到“思考中”的反馈。上下文感知不同的AI命令提取不同的上下文使AI的辅助更精准。事务安全所有对文档的修改都通过dispatch一个新的事务来完成这能完美融入ProseMirror的撤销/重做栈并且能避免在异步回调中直接操作DOM或编辑器状态可能引发的竞态条件。错误恢复任务失败时有明确的降级处理显示错误状态不会让编辑器卡死。5. 实现核心AI功能代码解释、内容续写与智能格式化有了稳固的交互协议和状态管理我们就可以实现具体的AI功能了。这里以三个典型场景为例展示如何将AI能力深度融入编辑流程。5.1 代码块解释与注释生成这个功能的目标是用户选中或光标位于一个代码块内输入/explainAI能生成该代码的解释或内联注释。首先我们需要增强命令识别使其能感知上下文。修改之前的createSlashCommandRule使其能根据光标位置推荐不同的命令。// 在输入规则触发后决定插入什么类型的ai_command function getRelevantCommandsAtPos(state: EditorState, pos: number): AICommandType[] { const node state.doc.resolve(pos).parent; const selected state.selection; if (node.type.name code_block) { return [explain_code, add_comments, optimize_code]; } if (!selected.empty) { return [summarize, translate, rephrase]; } return [continue_writing, expand_idea, fix_grammar]; }然后在executeAITask函数中为explain_code命令实现具体的逻辑async function executeExplainCodeTask(editorView: EditorView, taskPos: number, codeContent: string) { const prompt 你是一个资深的编程助手。请解释以下代码 \\\ ${codeContent} \\\ 请用中文以清晰易懂的方式解释这段代码的功能、关键步骤和可能的用途。如果代码有潜在问题或可以改进的地方也请指出。 ; const explanation await callAIService(prompt, explain_code); // 将解释内容作为一个新的“引用块”插入到代码块下方 const schema editorView.state.schema; const explanationBlock schema.nodes.blockquote.create( null, schema.nodes.paragraph.create(null, schema.text(explanation)) ); // 找到代码块的结束位置在其后插入 const codeBlockEnd taskPos editorView.state.doc.resolve(taskPos).parent.nodeSize; editorView.dispatch(editorView.state.tr.insert(codeBlockEnd, explanationBlock)); }5.2 智能内容续写与扩写这是提高写作效率的利器。当用户写下一个开头或一个要点时AI能帮助完成段落。关键在于设计一个能保持上下文连贯性和用户写作风格的提示词。我们不能简单地把光标前的所有文本都扔给AI那样可能包含无关信息。async function executeContinueWritingTask(editorView: EditorView, taskPos: number, context: string) { // 提取更智能的上下文例如当前段落的前两句或者上一段的最后一句。 const writingContext extractWritingContext(editorView.state, taskPos); const prompt 请根据以下上下文以自然流畅的风格续写内容。保持原有的语气和写作风格。 上下文“${writingContext}” 续写 ; const continuation await callAIService(prompt, continue_writing); // 将续写内容直接插入到光标位置 const schema editorView.state.schema; const newNodes []; // AI返回的可能是多段文本我们需要按段落拆分并创建节点 continuation.split(\n).forEach(para { if (para.trim()) { newNodes.push(schema.nodes.paragraph.create(null, schema.text(para))); } }); editorView.dispatch(editorView.state.tr.insert(taskPos, newNodes)); }5.3 基于AI的智能格式化与结构优化AI不仅可以生成内容还可以优化现有内容的结构。例如将一段杂乱无章的笔记整理成清晰的要点列表。async function executeOrganizeToListTask(editorView: EditorView, selectedText: string, selectionFrom: number, selectionTo: number) { const prompt 将以下文本内容整理成清晰、有条理的要点列表Markdown格式。去除冗余信息合并相似点使逻辑更清晰。 文本“${selectedText}” ; const organizedList await callAIService(prompt, organize); // 解析AI返回的Markdown列表并转换为ProseMirror的list节点 const listFragment parseMarkdownListToFragment(organizedList, editorView.state.schema); // 替换选中的文本为整理后的列表 editorView.dispatch(editorView.state.tr.replaceWith(selectionFrom, selectionTo, listFragment)); }这里的一个技术难点是parseMarkdownListToFragment函数它需要将AI返回的Markdown字符串如- 要点一\n- 要点二解析成ProseMirror的bullet_list和list_item节点树。这通常需要一个简单的Markdown解析器或者利用ProseMirror已有的Markdown转换工具链。6. 前端工程化性能、用户体验与错误处理将AI集成到编辑器对前端工程化提出了更高要求。我们需要考虑网络请求管理、加载状态、错误处理以及性能优化。6.1 请求防抖、节流与取消用户可能快速触发多个AI命令。我们需要防止重复请求并允许取消进行中的任务。class AIServiceManager { private pendingRequests: Mapstring, AbortController new Map(); async callAIService(prompt: string, command: AICommandType, signal?: AbortSignal): Promisestring { const requestId ${command}_${Date.now()}; const controller new AbortController(); const abortSignal signal || controller.signal; this.pendingRequests.set(requestId, controller); try { const response await fetch(/api/ai/completion, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ prompt, command }), signal: abortSignal, }); if (!response.ok) throw new Error(HTTP ${response.status}); const data await response.json(); return data.content; } finally { this.pendingRequests.delete(requestId); } } cancelRequest(requestId: string) { const controller this.pendingRequests.get(requestId); if (controller) { controller.abort(); this.pendingRequests.delete(requestId); } } cancelAllRequests() { this.pendingRequests.forEach(controller controller.abort()); this.pendingRequests.clear(); } }在插件视图的update函数中我们可以监听编辑器状态变化。如果用户快速删除了ai_command节点或移动了光标应该取消对应的AI请求。6.2 加载状态与用户反馈除了改变ai_command节点的图标我们还需要在UI层面提供更丰富的反馈。可以在命令面板或编辑器角落添加一个全局的轻量级状态指示器。function renderGlobalAIStatus(editorView: EditorView, aiPluginState: AIAssistantState) { const container document.getElementById(ai-status-bar); if (!container) return; if (aiPluginState.activeTasks.length 0) { container.textContent AI正在处理 ${aiPluginState.activeTasks.length} 个任务...; container.style.display block; } else { container.style.display none; } }6.3 错误处理与降级策略网络可能不稳定AI服务可能超时或返回错误。我们需要有完善的错误处理。网络错误重试逻辑最多2次并提示用户检查网络。服务端错误显示友好的错误信息如“AI服务暂时不可用请稍后再试”。内容安全过滤对AI返回的内容进行基本的敏感词或格式检查防止注入恶意脚本或不合规内容。超时控制为每个AI请求设置超时如30秒超时后自动取消并提示用户“请求超时可能是内容过长请简化后重试”。// 在executeAITask的catch块中 catch (error) { let errorMessage AI处理失败; if (error.name AbortError) { errorMessage 请求已取消; } else if (error.name TimeoutError) { errorMessage 请求超时请稍后重试; } else if (error.message.includes(500)) { errorMessage AI服务内部错误; } // 更新ai_command节点为错误状态并可以添加一个tooltip显示具体错误 editorView.dispatch( state.tr.setNodeMarkup(task.pos, undefined, { ...aiNode.node.attrs, state: error, data-error: errorMessage, // 将错误信息存储在属性中 }) ); // ... 同时更新全局状态移除任务 }6.4 性能优化虚拟化与差分更新如果AI生成了非常长的内容如一篇完整的文章一次性插入大量节点可能会导致编辑器卡顿。ProseMirror本身处理大文档性能很好但DOM操作可能成为瓶颈。我们可以采用“分块流式插入”的策略。如果AI服务支持流式响应如Server-Sent Events我们可以在收到一部分内容时就将其插入文档而不是等全部完成。// 模拟流式插入 async function executeAITaskWithStream(editorView: EditorView, taskPos: number) { // 初始插入一个空的文本节点作为占位符 const schema editorView.state.schema; let streamPos taskPos; const placeholderText schema.text(); editorView.dispatch(editorView.state.tr.insert(streamPos, placeholderText)); // 建立流式连接 const eventSource new EventSource(/api/ai/stream?taskId${taskId}); let accumulatedContent ; eventSource.onmessage (event) { const data JSON.parse(event.data); if (data.content) { accumulatedContent data.content; // 增量更新替换从streamPos开始的内容 const tr editorView.state.tr; tr.replaceWith(streamPos, streamPos placeholderText.nodeSize, schema.text(accumulatedContent)); editorView.dispatch(tr); } if (data.done) { eventSource.close(); // 最终可能将纯文本转换为更丰富的节点如段落 finalizeContent(editorView, streamPos, accumulatedContent); } }; }这种方式能让用户几乎实时地看到AI生成的内容体验更佳。7. 安全、隐私与模型选择的后端考量虽然本文聚焦前端实现但一个完整的AI编辑器系统离不开安全可靠的后端。这里简要提几个关键点供全栈开发者参考。7.1 API密钥管理与代理绝对不应该在前端代码中硬编码AI服务如OpenAI、Claude的API密钥。所有AI请求都应通过你自己的后端服务器进行代理。后端角色接收前端请求附加合法的API密钥转发给AI服务并将结果返回前端。好处隐藏密钥保护你的付费API密钥不被泄露。请求审计与限流可以记录谁在什么时间使用了什么功能防止滥用。统一错误处理与重试在后端实现更复杂的错误处理逻辑。内容过滤与审核在返回给前端前对AI生成的内容进行安全审核。7.2 提示词工程与系统角色设定后端的另一个核心职责是构建安全、有效的提示词。前端发送的可能是简短的指令和上下文后端需要将其组合成符合AI模型要求的、包含系统指令的完整提示词。// 后端示例 (Node.js) async function handleAIRequest(req, res) { const { prompt, command, context } req.body; const systemPrompt 你是一个集成在文本编辑器中的智能助手。请严格遵守以下规则 1. 只回答与文本编辑、写作、代码相关的问题。 2. 生成的内容必须积极、健康符合法律法规。 3. 如果用户请求涉及敏感或非法内容礼貌拒绝并说明原因。 4. 对于代码解释确保准确无误。 5. 使用简洁明了的中文。; const fullPrompt ${systemPrompt}\n\n用户指令${command}\n上下文${context}\n用户输入${prompt}; const aiResponse await openai.chat.completions.create({ model: gpt-4, messages: [{ role: system, content: systemPrompt }, { role: user, content: fullPrompt }], temperature: 0.7, max_tokens: 1000, }); res.json({ content: aiResponse.choices[0].message.content }); }通过系统提示词可以极大地约束AI的行为使其更符合产品定位和安全要求。7.3 模型选择与成本控制不同的AI任务可以使用不同的模型以平衡效果和成本。复杂的创作、推理任务如文章续写、代码生成使用能力强的模型如GPT-4、Claude 3。简单的格式化、修正任务如语法检查、简繁转换使用更轻量、更便宜的模型如GPT-3.5-Turbo、本地模型。实时性要求高的任务如输入补全考虑使用响应速度更快的模型或专用API。后端可以根据command字段路由到不同的模型服务并实施用量统计和成本监控。8. 从Demo到产品可维护性、测试与扩展最后我们来谈谈如何将这个原型打磨成一个可维护、可测试、可扩展的产品级功能。8.1 插件化与配置化将AI功能彻底插件化。创建一个独立的NPM包prosemirror-ai-assistant包含核心的aiAssistantPlugin。一系列预定义的AICommand如SummarizeCommand,ExplainCodeCommand。可配置的命令面板UI组件。与不同AI后端适配的AIServiceAdapter接口。这样其他项目可以通过简单的配置引入import { AIAssistantPlugin, createDefaultCommands, OpenAIAdapter } from prosemirror-ai-assistant; const aiPlugin AIAssistantPlugin.configure({ commands: createDefaultCommands(), serviceAdapter: new OpenAIAdapter({ endpoint: /api/openai-proxy }), palettePosition: bottom, // 命令面板位置 });8.2 单元测试与集成测试测试是保证复杂编辑器功能稳定的关键。单元测试Jest/Vitest测试纯函数如extractContextForAI、buildPrompt、convertAIResponseToFragment。集成测试Playwright/Cypress模拟用户完整操作流程。测试“输入/sum- 选择命令 - 显示加载状态 - 成功插入内容”。测试“在AI思考时进行编辑不会导致状态错乱”。测试“网络错误时UI有正确的错误反馈”。8.3 扩展性设计自定义AI命令与工作流设计一个开放的插件系统允许开发者或用户自定义AI命令。// 自定义命令示例翻译成粤语 const translateToCantoneseCommand: AICommand { name: translate_to_cantonese, description: 将选中文本翻译成粤语, icon: , // 匹配规则可以是Slash Command也可以是快捷键 matcher: (state) state.selection.empty ? null : [translate_to_cantonese], // 执行器 execute: async (view, context) { const prompt 将以下文本翻译成粤语口语${context.selectedText}; const result await callAIService(prompt, translate); return { action: replaceSelection, content: result }; }, }; // 注册到插件 const aiPlugin AIAssistantPlugin.configure({ commands: [...createDefaultCommands(), translateToCantoneseCommand], // ... });更进一步可以设计“AI工作流”将多个AI命令串联起来。例如一个“博客草稿润色”工作流可以依次执行“检查语法”、“优化措辞”、“添加标题建议”。8.4 性能监控与用户体验度量上线后需要收集数据来持续优化。性能指标每个AI命令的平均响应时间、成功率、错误类型分布。用户体验指标最常用的AI命令、用户取消请求的频率、在命令面板中的停留时间。业务指标AI功能是否提高了用户的编辑效率如平均会话时长、内容产出量。这些数据可以帮助你决定优化哪个模型的性能或者优先开发哪个新功能。走到这一步你的输入框已经彻底蜕变为一个强大的“AI超级入口”。它不再是 passively 接收字符的工具而是一个能主动理解、辅助和增强你创作过程的智能伙伴。从精准的指令识别到流畅的异步状态管理再到可扩展的架构设计每一步都考验着我们对编辑器原理和交互设计的理解。希望这篇全流程实战能为你打开一扇门让你在构建下一代智能应用时有更清晰的蓝图和更趁手的工具。
返回列表