首页
随机
最近更改
特殊页面
社群首页
参数设置
关于WHY42
免责声明
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
。