Prompt IDE:修订间差异

来自WHY42
第209行: 第209行:
在上一节的代码中可以看到,如果在插件中,我们可以直接通过<syntaxhighlight lang="bash" inline>vscode.TextDocument</syntaxhighlight>对象获取文本文件的内容:
在上一节的代码中可以看到,如果在插件中,我们可以直接通过<syntaxhighlight lang="bash" inline>vscode.TextDocument</syntaxhighlight>对象获取文本文件的内容:


<syntaxhighlight lang="bash">
<syntaxhighlight lang="typescript">
// public async resolveCustomTextEditor(
// public async resolveCustomTextEditor(
//    document: vscode.TextDocument,    ← 看这里
//    document: vscode.TextDocument,    ← 看这里

2024年1月16日 (二) 08:32的版本

你是怎样调提示词的呢?这看起来是个再简单不过的问题,但对于开发者而言,一直缺乏一个趁手的工具。目前可以用的这些工具大致有以下的问题:

  • 可以同大模型聊天,但是不具备调参能力,例如ChatGPT、文心一言、ChatHub
  • 可以调参,但只支持特定的厂商,例如GPT Playground、Minimax体验中心等
  • 可以调参,也支持不同的厂商,功能也很强大,唯一缺点是不是原生的,如KPP
  • 啥都可以干,但是要写代码,例如LangChain

于是为了解决这个问题,能够更愉快地测试提示词,笔者撸了一个Vs Code插件——Prompt IDE。目前,尽管缺胳膊少腿,Prompt IDE初具雏形。在这个有趣的过程中,学习到了Vs Code插件开发的一些技巧,也对Vs Code的强大有了更深的认知,在这里简单总结一下。

项目起源

这个想法最初源自于公司2023年7月份的黑客马拉松活动,当时,笔者Solo了一个叫做”Prompt Native“的项目。什么是Prompt Native呢?有三层含义[1]:

  • Colud Native: 将大模型的能力做成云原生的FaaS
  • Native Editor: 原生的提示词开发编辑器,也就是今天的Prompt IDE
  • Native Tooling: 通过原生的大模型工具链来辅助我们开发

大致的设计是这样:

  1. 通过一个Vs Code插件来开发提示词,提示词最终存储为一个文件,有这特定的格式。这就是Native Editor
  2. 在开发提示词的过程中,可以通过大模型给出提示词调优的建议,类似与Js Lint,我们可以做一个Prompt Lint,不过当时经过测试发现效果并不好;还有比如自动生成人设,例如chatgpt-prompt-generator-v12通过一个预训练的BART模型自动生成提示词,也可以集成到插件中,做到自动补全。这是Native Tooling
  3. 提供不同语言的FaaS模板和解析运行Prompt文件的库,将其部署到Kubernetes中为云函数。当时采用Knative Functions和Rust实现了一个简单的Hello World模板

得益于GPT辅助编程,作为Rust菜鸟和第一次开发Vs Code插件的半吊子前端,肝几天就做出了一个PoC,看起来很简单。这是当时的Prompt IDE的最初版本:

想法其实也很简单:实现一个Side-by-side的提示词编辑功能,可以在界面上编辑运行,也可以直接改源码,这样就解决重口难调的问题,既方便又实用。

但是笔者当时犯了一个极大的错误,就是低估了这件事的难度。后来发现,要做到”可用“的程度,其实是非常困难的。

因此,接下来的时间断断续续在思考这件事,最终决定先放弃其他”Native",先把编辑器整清楚,从而产生了这个Prompt IDE。

Prompt IDE的开发过程

从提示词存储格式开始

在函数模板中,提示词跟其他代码是同等重要的位置,因为提示词的变化也就意味着功能的变化。 因此,提示词最终需要存储为一个文本文件,这样才能跟版本控制等其他工具有机结合起来,也便于分享和协作。 但是,提示词应该怎么表示呢?

一些工具将提示词简单表示为一个文本,其中可能包含动态的内容(变量):

请将下面这句话翻译为{{language}}:

{{text}}

这看起来没有什么问题,在我们确定只用GPT的情况下。但是,如果我们需要使用不同的模型和参数呢?同样的提示词,在不同的模型和参数下,其运行结果是可能会有差别的。一种做法是,把这些信息也添加进去,例如:

-- Model=gpt-3.5-turbo-instruct Temperature=0.1

请将下面这句话翻译为{{language}}:

{{text}}

这样看起来不错,但是只适用于比较简单的补全(Completion)场景。补全是最早期的大模型调用方式,现在几乎已经被经过指令微调的对话(Chat)模式所取代。对于对话而言,会包含问答、少样本示例、参数等,当然我们也可以在之前的基础上稍作修改进行表示:

-- Model=gpt-3.5-turbo Temperature=0.1

System: 你是一只会模仿鹦鹉,从现在开始,我说什么,你跟着说什么
User: 记住了么?
Assistant:记住了么?
User: 停止,你现在不是鹦鹉了
Assistant:停止,你现在不是鹦鹉了
User: 好了,现在开始

但这样问题也很明显:因为没有严格的格式约束,很容易出错。在调用GPT的API时,我们是需要将其转换为messages数组来表示对话内容的,那么,如果这里面的格式有些问题,比如中英文的冒号不对,可能就会导致最终的结果不对。

我们需要有一种方式,可以来直观表示提示词,并且不容易出错的。

这里总共考虑过三种不同的格式:

  • XML:标记语法,可以通过XML DTD来校验格式,且可以通过<![CDATA[...]]>的方式来包裹多行文本,对于多行的提示词十分友好
  • YAML:也可以支持多行文本,缺点是没有标记,完全依靠缩进,容易乱
  • JSON:可以通过JSON Schema进行校验,缺点是需要对多行、特殊字符进行转义

最终,经过一些尝试后,最终选择了使用JSON表示。主要原因是,JSON格式校验比较完善,且天然对前端友好,更适合在Vs Code这种基于Web的体系下开发。 在Vs Code中编辑JSON时,也可以设置Schema来实现编辑器自带的格式校验和自动补全。如下是通过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
        }
    ]
}

对不同的大模型接口进行抽象

仅仅使用JSON是不够的,还需要对不同的大模型接口进行一次抽象。 例如,GPT中的System参数,在接口中是以一个特别的message表示的:

"messages": [
    {
      "role": "system",
      "content": "Hello"
    },

但是其他大模型并没有这个设计。除此之外,一些细微的地方,大家实现起来也各不相同,比如,大模型的回复,GPT叫”assistant",Minimax叫“BOT”。 所以,必须要对这些参数进行一次抽象,用一个统一的方式来表示提示词。目前的实现,主要参考了GPT和谷歌的AI Studio的做法。例如,通过context定义背景(类似GPT的System、Minimax的人设),通过examples定义Few-shot示例。 经过抽象后,最终形成一个JSON Schema表示,例如对话模式可以表示如下(部分):

{
  "context": {
    "type": "string"
  },
  "examples": {
    "type": "array",
    "minItems": 1,
    "items": {
      "$ref": "#/definitions/message"
    }
  },
  "messages": {
    "type": "array",
    "minItems": 1,
    "items": {
      "$ref": "#/definitions/message"
    }
  }
}

选取开发框架

参照Vs Code的插件开发文档可以很容易创建一个插件。但是要做一个复杂的交互页面,还需要进一步对框架进行定制:

  • 我们的场景其实是一个提示词编辑器,非常类似于WebView文档中的自定义编辑器的例子。因此,最佳的做法就是通过WebView实现一个网页来嵌入到Vs Code中
  • 纯原生的WebView是手写HTML来操作的,这样生产力太低了。vscode-webview-ui-toolkit是一个Vs Code的控件集,并提供了集中前端框架的集成示例,例如Angular, React等
  • 使用Typescript而不是Javascript,这样可以更好地进行静态类型检测

笔者采取的是React+vite的方式集成vscode-webview-ui-toolkit。 最终项目的结构大致如下:

- prompt-ide
  - package.json
  \ src
    - extension.ts
  \ webview-ui 
    - package.json
    \ src
  \ prompt-schema
    - package.json
    \ src

其中,prompt-schema为提示词格式的JSON Schema定义,通过dependency引用。webview-ui为WebView页面的实现,编译后嵌入插件中(index.js, index.css等)。 这样,其实相当于通过React生成了一个静态的HTML、JS等,然后在Vs Code插件中读取这个HTML,渲染一个Web页面,代码看起来有点不那么优雅:

// 省略了很多细节
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>
    `;
  }
}

值得一提的是,微软为了避免大家做的插件乱七八糟真是操碎了心,不仅提供了详细的 UX设计指导,每一个控件什么时候用,什么时候不该用也写的明明白白

实现文本编辑器和自定义编辑器的双向同步

Prompt IDE的一个核心功能是双向同步。也就是说,在UI界面上的修改会改变文件内容;同时,改变文件内容也会同步到UI。 如果单纯只是在React中做一个编辑器,其实是十分简单的,难点就在于同vscode进行交互。 在上一节的代码中可以看到,如果在插件中,我们可以直接通过vscode.TextDocument对象获取文本文件的内容:

// public async resolveCustomTextEditor(
//     document: vscode.TextDocument,    ← 看这里
//     webviewPanel: vscode.WebviewPanel,
//     _token: vscode.CancellationToken
// )
document.getText()