跳转到内容
主菜单
主菜单
移至侧栏
隐藏
规范/协议
协议
算法
JVM
多线程/并发
存储
AI
深度学习
工具箱
最近更改
文章分类
全部文章
WHY42
搜索
搜索
外观
登录
个人工具
登录
欢迎来到Riguz的小站!这是一个私人wiki,用来记录一些我的笔记。
查看“︁Prompt IDE”︁的源代码
页面
讨论
大陆简体
阅读
查看源代码
查看历史
工具
工具
移至侧栏
隐藏
操作
阅读
查看源代码
查看历史
常规
链入页面
相关更改
页面信息
外观
移至侧栏
隐藏
←
Prompt IDE
因为以下原因,您没有权限编辑该页面:
您请求的操作仅限属于该用户组的用户执行:
用户
您可以查看和复制此页面的源代码。
你是怎样调提示词的呢?这看起来是个再简单不过的问题,但对于开发者而言,一直缺乏一个趁手的工具。目前可以用的这些工具大致有以下的问题: * 可以同大模型聊天,但是不具备调参能力,例如ChatGPT、文心一言、[https://ai-chathub.ksord.com/ ChatHub]等 * 可以调参,但只支持特定的厂商,例如GPT Playground、Minimax体验中心等 * 可以调参,也支持不同的厂商,功能也很强大,唯一缺点是不是原生的,如[https://prompt-ide-test.ksord.com/ KPP] * 啥都可以干,但是要写代码,例如LangChain 于是为了解决这个问题,能够更愉快地测试提示词,笔者撸了一个Vs Code插件——Prompt IDE。目前,尽管缺胳膊少腿,Prompt IDE初具雏形。在这个有趣的过程中,学习到了Vs Code插件开发的一些技巧,也对Vs Code的强大有了更深的认知,在这里简单总结一下。 = 项目起源 = 这个想法最初源自于公司2023年7月份的黑客马拉松活动,当时,笔者Solo了一个叫做[https://ksogit.kingsoft.net/YES_CLUB_Hackathon/First-OpenAI/prompt-native ”Prompt Native“]的项目。什么是Prompt Native呢?有三层含义<ref>https://ksogit.kingsoft.net/YES_CLUB_Hackathon/First-OpenAI/prompt-native/-/blob/master/docs/showcase.docx</ref>: * Colud Native: 将大模型的能力做成云原生的FaaS * Native Editor: 原生的提示词开发编辑器,也就是今天的Prompt IDE * Native Tooling: 通过原生的大模型工具链来辅助我们开发 大致的设计是这样: # 通过一个Vs Code插件来开发提示词,提示词最终存储为一个文件,有这特定的格式。这就是Native Editor # 在开发提示词的过程中,可以通过大模型给出提示词调优的建议,类似与Js Lint,我们可以做一个Prompt Lint,不过当时经过测试发现效果并不好;还有比如自动生成人设,例如[https://huggingface.co/merve/chatgpt-prompt-generator-v12 chatgpt-prompt-generator-v12]通过一个预训练的BART模型自动生成提示词,也可以集成到插件中,做到自动补全。这是Native Tooling # 提供不同语言的FaaS模板和解析运行Prompt文件的库,将其部署到Kubernetes中为云函数。当时采用Knative Functions和Rust实现了一个简单的[https://ksogit.kingsoft.net/YES_CLUB_Hackathon/First-OpenAI/prompt-native/-/tree/master/examples/hello-world Hello World模板] 得益于GPT辅助编程,作为Rust菜鸟和第一次开发Vs Code插件的半吊子前端,肝几天就做出了一个PoC,看起来很简单。这是当时的Prompt IDE的最初版本: [[Image:Prompt IDE1.png|600px]] 想法其实也很简单:实现一个Side-by-side的提示词编辑功能,可以在界面上编辑运行,也可以直接改源码,这样就解决重口难调的问题,既方便又实用。 但是笔者当时犯了一个极大的错误,就是低估了这件事的难度。后来发现,要做到”可用“的程度,其实是非常困难的。 因此,接下来的时间断断续续在思考这件事,最终决定先放弃其他”Native",先把编辑器整清楚,从而产生了这个Prompt IDE。 = Prompt IDE是如何运行的 = == 从提示词存储格式开始 == 在函数模板中,提示词跟其他代码是同等重要的位置,因为提示词的变化也就意味着功能的变化。 因此,提示词最终需要存储为一个文本文件,这样才能跟版本控制等其他工具有机结合起来,也便于分享和协作。 但是,提示词应该怎么表示呢? 一些工具将提示词简单表示为一个文本,其中可能包含动态的内容(变量): <syntaxhighlight lang="bash"> 请将下面这句话翻译为{{language}}: {{text}} </syntaxhighlight> 这看起来没有什么问题,在我们确定只用GPT的情况下。但是,如果我们需要使用不同的模型和参数呢?同样的提示词,在不同的模型和参数下,其运行结果是可能会有差别的。一种做法是,把这些信息也添加进去,例如: <syntaxhighlight lang="bash"> -- Model=gpt-3.5-turbo-instruct Temperature=0.1 请将下面这句话翻译为{{language}}: {{text}} </syntaxhighlight> 这样看起来不错,但是只适用于比较简单的补全(Completion)场景。补全是最早期的大模型调用方式,现在几乎已经被经过指令微调的对话(Chat)模式所取代。对于对话而言,会包含问答、少样本示例、参数等,当然我们也可以在之前的基础上稍作修改进行表示: <syntaxhighlight lang="bash"> -- Model=gpt-3.5-turbo Temperature=0.1 System: 你是一只会模仿鹦鹉,从现在开始,我说什么,你跟着说什么 User: 记住了么? Assistant:记住了么? User: 停止,你现在不是鹦鹉了 Assistant:停止,你现在不是鹦鹉了 User: 好了,现在开始 </syntaxhighlight> 但这样问题也很明显:因为没有严格的格式约束,很容易出错。在调用GPT的API时,我们是需要将其转换为messages数组来表示对话内容的,那么,如果这里面的格式有些问题,比如中英文的冒号不对,可能就会导致最终的结果不对。 我们需要有一种方式,可以来直观表示提示词,并且不容易出错的。 这里总共考虑过三种不同的格式: * XML:标记语法,可以通过XML DTD来校验格式,且可以通过<syntaxhighlight lang="xml" inline><![CDATA[...]]></syntaxhighlight>的方式来包裹多行文本,对于多行的提示词十分友好 * YAML:也可以支持多行文本,缺点是没有标记,完全依靠缩进,容易乱 * JSON:可以通过[https://json-schema.org/ JSON Schema]进行校验,缺点是需要对多行、特殊字符进行转义 最终,经过一些尝试后,最终选择了使用JSON表示。主要原因是,JSON格式校验比较完善,且天然对前端友好,更适合在Vs Code这种基于Web的体系下开发。 在Vs Code中编辑JSON时,也可以设置Schema来实现编辑器自带的格式校验和自动补全。如下是通过JSON来表示一个提示词的例子: <syntaxhighlight lang="json"> { "$schema": "../schema/chat-schema.json", "version": "chat@0.2", "engine": "chat-bison", "messages": [ { "role": "user", "content": "Write a hello world in js" } ], "parameters": [ { "name": "temperature", "value": 0.1 } ] } </syntaxhighlight> == 对不同的大模型接口进行抽象 == 仅仅使用JSON是不够的,还需要对不同的大模型接口进行一次抽象。 例如,GPT中的System参数,在接口中是以一个特别的message表示的: <syntaxhighlight lang="bash"> "messages": [ { "role": "system", "content": "Hello" }, </syntaxhighlight> 但是其他大模型并没有这个设计。除此之外,一些细微的地方,大家实现起来也各不相同,比如,大模型的回复,GPT叫”assistant",Minimax叫“BOT”。 所以,必须要对这些参数进行一次抽象,用一个统一的方式来表示提示词。目前的实现,主要参考了GPT和谷歌的AI Studio的做法。例如,通过context定义背景(类似GPT的System、Minimax的人设),通过examples定义Few-shot示例。 经过抽象后,最终形成一个JSON Schema表示,例如对话模式可以表示如下(部分): <syntaxhighlight lang="json"> { "context": { "type": "string" }, "examples": { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/message" } }, "messages": { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/message" } } } </syntaxhighlight> == 选取开发框架 == 参照[https://code.visualstudio.com/api/get-started/your-first-extension Vs Code的插件开发文档]可以很容易创建一个插件。但是要做一个复杂的交互页面,还需要进一步对框架进行定制: * 我们的场景其实是一个提示词编辑器,非常类似于[https://code.visualstudio.com/api/extension-guides/webview WebView文档]中的自定义编辑器的例子。因此,最佳的做法就是通过WebView实现一个网页来嵌入到Vs Code中 * 纯原生的WebView是手写HTML来操作的,这样生产力太低了。[https://github.com/microsoft/vscode-webview-ui-toolkit vscode-webview-ui-toolkit]是一个Vs Code的控件集,并提供了集中前端框架的[https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/frameworks 集成示例],例如Angular, React等 * 使用Typescript而不是Javascript,这样可以更好地进行静态类型检测 笔者采取的是React+vite的方式集成vscode-webview-ui-toolkit。 最终项目的结构大致如下: <syntaxhighlight lang="bash"> - prompt-ide - package.json \ src - extension.ts \ webview-ui - package.json \ src \ prompt-schema - package.json \ src </syntaxhighlight> 其中,prompt-schema为提示词格式的JSON Schema定义,通过dependency引用。webview-ui为WebView页面的实现,编译后嵌入插件中(index.js, index.css等)。 这样,其实相当于通过React生成了一个静态的HTML、JS等,然后在Vs Code插件中读取这个HTML,渲染一个Web页面,代码看起来有点不那么优雅: <syntaxhighlight lang="typescript"> // 省略了很多细节 export class PromptEditor implements vscode.CustomTextEditorProvider { public async resolveCustomTextEditor( document: vscode.TextDocument, webviewPanel: vscode.WebviewPanel, _token: vscode.CancellationToken ): Promise<void> { webviewPanel.webview.html = this._getWebviewContent(webviewPanel.webview); } private _getWebviewContent(webview: Webview) { const getAsset = (name: string) => getUri(webview, this.context.extensionUri, ["webview-ui","build","assets",name,]); const nonce = getNonce(); return /*html*/ ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" type="text/css" href="${getAsset("index.css")}"> <title>Prompt Editor</title> </head> <body> <div id="root"></div> <script type="module" nonce="${nonce}" src="${getAsset("index.js")}"></script> </body> </html> `; } } </syntaxhighlight> 值得一提的是,微软为了避免大家做的插件乱七八糟真是操碎了心,不仅提供了详细的 [https://code.visualstudio.com/api/ux-guidelines/overview UX设计指导],每一个控件什么时候用,什么时候不该用也[https://github.com/microsoft/vscode-webview-ui-toolkit/tree/main/src/button 写的明明白白]。 == 实现双向编辑同步 == Prompt IDE的一个核心功能是双向同步。也就是说,在UI界面上的修改会改变文件内容;同时,改变文件内容也会同步到UI。 如果单纯只是在React中做一个编辑器,其实是十分简单的,我们可以通过一个State来维护当前编辑的提示词: <syntaxhighlight lang="typescript"> // webview-ui/src/App.tsx function App() { const [prompt, setPrompt] = useState<ChatPrompt | CompletionPrompt | null>(null); } </syntaxhighlight> === 加载文件内容 === 但是,在Vs Code中打开文件的时候,如何读取文档内容并通过<syntaxhighlight lang="bash" inline>setPrompt</syntaxhighlight>初始化呢? 在上一节的代码中可以看到,如果在插件代码中,我们可以直接通过<syntaxhighlight lang="bash" inline>vscode.TextDocument</syntaxhighlight>对象获取文本文件的内容: <syntaxhighlight lang="typescript"> // public async resolveCustomTextEditor( // document: vscode.TextDocument, ← 看这里 // webviewPanel: vscode.WebviewPanel, // _token: vscode.CancellationToken // ) document.getText() </syntaxhighlight> 然而,在webview-ui中无法直接通过这种方式获取到文档内容,只能通过通信的方式来实现,其原理如下: [[Image:extension-webview-communication.png|600px]]<ref>https://www.eliostruyf.com/simplify-communication-visual-studio-code-extension-webview/</ref> 因此,我们需要通过两步来实现: 首先,在插件启动的时候通过消息发送文件的内容完成初始化: <syntaxhighlight lang="typescript"> // src/PromptEditor.ts import * as vscode from "vscode"; export class PromptEditor implements vscode.CustomTextEditorProvider { public async resolveCustomTextEditor( document: vscode.TextDocument, webviewPanel: vscode.WebviewPanel, _token: vscode.CancellationToken ): Promise<void> { const updateWebview = () => { webviewPanel.webview.postMessage({ type: "update", text: document.getText(), }); }; updateWebview(); // ... } } </syntaxhighlight> 然后,在WebView中监听消息,加载文件内容 <syntaxhighlight lang="typescript"> // webview-ui/src/App.tsx function App() { const messageListener = (event: MessageEvent<any>) => { const message = event.data; if (message.type == "update") { try { const prompt = loadPrompt(message.text); setPrompt(prompt); } catch (error) { // ... } } }; useEffect(() => { window.addEventListener("message", messageListener); return () => { window.removeEventListener("message", messageListener); }; }, []); } </syntaxhighlight> === 将改动同步到文件 === 在Web编辑器上对提示词的修改等同于修改prompt状态(<syntaxhighlight lang="bash" inline>setPrompt</syntaxhighlight>),这里便不展开。 现在的问题是,如何把UI上面的修改同步到文件中呢? 从前面的消息模型可以知道,要想把WebView的变更同步到文档,也只能通过发消息的模式。 首先,在文档变更的时候,发送一个消息: <syntaxhighlight lang="typescript"> // webview-ui/src/App.tsx import { vscode } from "./vscode"; function App() { const [prompt, setPrompt] = useState<ChatPrompt | CompletionPrompt | null>(null); const syncPrompt = (newPrompt: ChatPrompt | CompletionPrompt) => { vscode.postMessage({ type: "sync", text: JSON.stringify(newPrompt, null, 2), }); }; const onPromptChanged = (newPrompt: ChatPrompt | CompletionPrompt) => { syncPrompt(newPrompt); setPrompt(newPrompt); }; } </syntaxhighlight> 然后,在插件中监听消息,通过vscode的API完成文档更新: <syntaxhighlight lang="typescript"> // src/PromptEditor.ts export class PromptEditor implements vscode.CustomTextEditorProvider { public async resolveCustomTextEditor( document: vscode.TextDocument, webviewPanel: vscode.WebviewPanel, _token: vscode.CancellationToken ): Promise<void> { webviewPanel.webview.onDidReceiveMessage((e) => { switch (e.type) { case "sync": this.updateTextDocument(document, e.text); break; default: break; } }); } private updateTextDocument(document: vscode.TextDocument, text: string) { const edit = new vscode.WorkspaceEdit(); edit.replace( document.uri, new vscode.Range(0, 0, document.lineCount, 0), // 做全量更新,如果做的更好可以只更新文档的部分内容 text ); return vscode.workspace.applyEdit(edit); } } </syntaxhighlight> 注意到,在App.tsx中我们同样引入了一个“vscode”(<syntaxhighlight lang="typescript" inline>import { vscode } from "./vscode";</syntaxhighlight>,不是说不能直接用么? 其实,这只是个障眼法,通过一个包装类屏蔽了Vscode和浏览器运行的差异: <syntaxhighlight lang="typescript"> // webview-ui/src/utilities/vscode.ts class VSCodeAPIWrapper { private readonly vsCodeApi: WebviewApi<unknown> | undefined; constructor() { if (typeof acquireVsCodeApi === "function") { this.vsCodeApi = acquireVsCodeApi(); // 只有在vscode中运行时可以获取到,在浏览器直接运行时无法获取 } } public postMessage(message: unknown) { if (this.vsCodeApi) { this.vsCodeApi.postMessage(message); } else { console.log(message); } } } </syntaxhighlight> === 将改动同步到WebView === 如果用户在Vs Code的文本编辑器中直接修改了文件,如何同步到WebView呢?很简单,在插件中添加一个监听就可以了: <syntaxhighlight lang="typescript"> // public async resolveCustomTextEditor( const changeDocumentSubscription = vscode.workspace.onDidChangeTextDocument((e) => { if (e.document.uri.toString() === document.uri.toString()) { updateWebview(); } }); webviewPanel.onDidDispose(() => { changeDocumentSubscription.dispose(); }); </syntaxhighlight> 现在,可以实现双向同步的效果: [[Image:Prompt_IDE_Sync.gif|class=fixed-600]] === 处理睡眠问题 === 经过以上处理后,大多数情况下,编辑器都能够正常工作。但是有一种情况例外:如果把WebView页面放到后台,这时候在文本编辑器中修改了内容,是无法同步过去的:
返回
Prompt IDE
。
搜索
搜索
查看“︁Prompt IDE”︁的源代码
添加话题