开发模型插件#

本教程将引导您开发一个新的 LLM 插件,以添加对新的大型语言模型的支持。

我们将开发一个实现简单马尔可夫链的插件,用于基于输入字符串生成单词。马尔可夫链在技术上并非大型语言模型,但它们提供了一个有用的练习,用于演示如何通过插件扩展 LLM 工具。

插件的初始结构#

首先创建一个以您的插件命名的目录 - 它应该被称为类似于llm-markov的名称。

mkdir llm-markov
cd llm-markov

在该目录中创建一个名为llm_markov.py的文件,包含以下内容

import llm

@llm.hookimpl
def register_models(register):
    register(Markov())

class Markov(llm.Model):
    model_id = "markov"

    def execute(self, prompt, stream, response, conversation):
        return ["hello world"]

这里的def register_models()函数由插件系统调用(感谢@hookimpl装饰器)。它使用传递给它的register()函数来注册新模型的一个实例。

这里的Markov类实现了模型。它设置了一个model_id - 一个可以传递给llm -m的标识符,用于识别要执行的模型。

执行模型的逻辑位于execute()方法中。我们将在后续步骤中扩展此方法以执行更有用的操作。

接下来,创建一个pyproject.toml文件。这是必需的,用于告诉 LLM 如何加载您的插件

[project]
name = "llm-markov"
version = "0.1"

[project.entry-points.llm]
markov = "llm_markov"

这是最简单的配置。它定义了插件名称,并为llm提供了一个入口点,告诉它如何加载插件。

如果您熟悉 Python 虚拟环境,现在可以为您的项目创建一个,激活它,并在下一步之前运行pip install llm

如果您不熟悉虚拟环境,请不用担心:您可以在没有它们的情况下开发插件。您需要使用 Homebrew 或pipx或其他安装选项之一安装 LLM。

安装您的插件进行试用#

在创建了一个包含pyproject.toml文件和llm_markov.py文件的目录后,您可以通过在您的llm-markov目录中运行此命令将您的插件安装到 LLM 中

llm install -e .

-e代表“可编辑” - 这意味着您将能够对llm_markov.py文件进行进一步更改,这些更改将无需重新安装插件即可生效。

.表示当前目录。您也可以通过传递插件目录的路径来安装可编辑插件,如下所示

llm install -e path/to/llm-markov

要确认您的插件已正确安装,请运行此命令

llm plugins

输出应该如下所示

[
  {
    "name": "llm-markov",
    "hooks": [
      "register_models"
    ],
    "version": "0.1"
  },
  {
    "name": "llm.default_plugins.openai_models",
    "hooks": [
      "register_commands",
      "register_models"
    ]
  }
]

此命令列出 LLM 包含的默认插件以及已安装的新插件。

现在,让我们通过运行一个提示来尝试该插件

llm -m markov "the cat sat on the mat"

输出为

hello world

接下来,我们将使其执行并返回马尔可夫链的结果。

构建马尔可夫链#

马尔可夫链可以被认为是生成语言模型最简单的例子。它们的工作原理是构建一个索引,记录出现在其他词语之后的词语。

对于短语“the cat sat on the mat”,该索引如下所示

{
  "the": ["cat", "mat"],
  "cat": ["sat"],
  "sat": ["on"],
  "on": ["the"]
}

这是一个 Python 函数,用于从文本输入构建该数据结构

def build_markov_table(text):
    words = text.split()
    transitions = {}
    # Loop through all but the last word
    for i in range(len(words) - 1):
        word = words[i]
        next_word = words[i + 1]
        transitions.setdefault(word, []).append(next_word)
    return transitions

我们可以通过将其粘贴到交互式 Python 解释器中并运行此代码来试用它

>>> transitions = build_markov_table("the cat sat on the mat")
>>> transitions
{'the': ['cat', 'mat'], 'cat': ['sat'], 'sat': ['on'], 'on': ['the']}

执行马尔可夫链#

要执行模型,我们从一个词开始。我们查看可能出现在其后的词语选项,并随机选择其中一个。然后重复此过程,直到生成所需数量的输出词语。

有些词语在我们的训练句子中可能没有任何后续词语。对于我们的实现,我们将退而选择从我们的词语集合中随机挑选一个词语。

我们将使用 yield 关键字来实现一个Python 生成器,以生成每个 token

def generate(transitions, length, start_word=None):
    all_words = list(transitions.keys())
    next_word = start_word or random.choice(all_words)
    for i in range(length):
        yield next_word
        options = transitions.get(next_word) or all_words
        next_word = random.choice(options)

如果您不熟悉生成器,上面的代码也可以这样实现 - 创建一个 Python 列表并在函数末尾返回它

def generate_list(transitions, length, start_word=None):
    all_words = list(transitions.keys())
    next_word = start_word or random.choice(all_words)
    output = []
    for i in range(length):
        output.append(next_word)
        options = transitions.get(next_word) or all_words
        next_word = random.choice(options)
    return output

您可以像这样试用generate()函数

lookup = build_markov_table("the cat sat on the mat")
for word in generate(transitions, 20):
    print(word)

或者您可以使用它生成一个完整的字符串句子,如下所示

sentence = " ".join(generate(transitions, 20))

将其添加到插件中#

我们之前的execute()方法目前返回列表["hello world"]

更新它以使用我们的新马尔可夫链生成器。以下是新的llm_markov.py文件的完整文本

import llm
import random

@llm.hookimpl
def register_models(register):
    register(Markov())

def build_markov_table(text):
    words = text.split()
    transitions = {}
    # Loop through all but the last word
    for i in range(len(words) - 1):
        word = words[i]
        next_word = words[i + 1]
        transitions.setdefault(word, []).append(next_word)
    return transitions

def generate(transitions, length, start_word=None):
    all_words = list(transitions.keys())
    next_word = start_word or random.choice(all_words)
    for i in range(length):
        yield next_word
        options = transitions.get(next_word) or all_words
        next_word = random.choice(options)

class Markov(llm.Model):
    model_id = "markov"

    def execute(self, prompt, stream, response, conversation):
        text = prompt.prompt
        transitions = build_markov_table(text)
        for word in generate(transitions, 20):
            yield word + ' '

execute()方法可以使用 prompt.prompt访问用户提供的文本提示 - prompt是一个Prompt对象,其中可能还包含其他更高级的输入详细信息。

现在运行此命令,您应该会看到马尔可夫链的输出!

llm -m markov "the cat sat on the mat"
the mat the cat sat on the cat sat on the mat cat sat on the mat cat sat on

理解 execute()#

execute()方法的完整签名是

def execute(self, prompt, stream, response, conversation):

prompt参数是一个Prompt对象,包含用户提供的文本、系统提示和提供的选项。

stream是一个布尔值,表示模型是否正在以流模式运行。

response是模型正在创建的Response对象。提供此对象以便您可以将附加信息写入response.response_json,该信息可能会被记录到数据库中。

conversation是提示所属的Conversation - 如果未提供对话,则为None。某些模型可能会使用conversation.responses访问对话中的先前提示和响应,并使用它们来构建包含先前上下文的 LLM 调用。

提示和响应会被记录到数据库中#

提示和响应将由 LLM 自动记录到 SQLite 数据库中。您可以使用以下命令查看日志中最近添加的单条记录

llm logs -n 1

输出应该类似于此

[
  {
    "id": "01h52s4yez2bd1qk2deq49wk8h",
    "model": "markov",
    "prompt": "the cat sat on the mat",
    "system": null,
    "prompt_json": null,
    "options_json": {},
    "response": "on the cat sat on the cat sat on the mat cat sat on the cat sat on the cat ",
    "response_json": null,
    "conversation_id": "01h52s4yey7zc5rjmczy3ft75g",
    "duration_ms": 0,
    "datetime_utc": "2023-07-11T15:29:34.685868",
    "conversation_name": "the cat sat on the mat",
    "conversation_model": "markov"
  }
]

插件可以通过在execute()方法执行期间将字典赋值给response.response_json属性来向数据库记录附加信息。

以下是如何在日志的response_json中包含完整的transitions

    def execute(self, prompt, stream, response, conversation):
        text = self.prompt.prompt
        transitions = build_markov_table(text)
        for word in generate(transitions, 20):
            yield word + ' '
        response.response_json = {"transitions": transitions}

现在运行 logs 命令时,您也会看到该信息

llm logs -n 1
[
  {
    "id": 623,
    "model": "markov",
    "prompt": "the cat sat on the mat",
    "system": null,
    "prompt_json": null,
    "options_json": {},
    "response": "on the mat the cat sat on the cat sat on the mat sat on the cat sat on the ",
    "response_json": {
      "transitions": {
        "the": [
          "cat",
          "mat"
        ],
        "cat": [
          "sat"
        ],
        "sat": [
          "on"
        ],
        "on": [
          "the"
        ]
      }
    },
    "reply_to_id": null,
    "chat_id": null,
    "duration_ms": 0,
    "datetime_utc": "2023-07-06T01:34:45.376637"
  }
]

然而,在这种特定情况下,这不是一个好主意:transitions表是重复信息,因为它可以通过输入数据重新生成 - 而且对于较长的提示,它会变得非常大。

添加选项#

LLM 模型可以接受选项。对于大型语言模型,这些选项可以是诸如temperaturetop_k之类的值。

选项使用-o/--option命令行参数传递,例如

llm -m gpt4 "ten pet pelican names" -o temperature 1.5

我们将为我们的马尔可夫链模型添加两个选项

  • length:要生成的单词数量

  • delay:输出 token 之间的延迟(浮点数)

delay token 将允许我们模拟流式语言模型,其中 token 生成需要时间,并在准备好后由execute()函数返回。

选项是通过模型上的一个内部类定义的,名为Options。它应该继承llm.Options类。

首先,将此导入添加到您的llm_markov.py文件顶部

from typing import Optional

然后将此Options类添加到您的模型中

class Markov(Model):
    model_id = "markov"

    class Options(llm.Options):
        length: Optional[int] = None
        delay: Optional[float] = None

让我们为选项添加额外的验证规则。长度必须至少为 2。延迟必须在 0 到 10 之间。

Options类使用了Pydantic 2,它支持各种高级验证规则。

我们还可以添加内联文档,然后可以通过llm models --options命令显示。

将这些导入添加到llm_markov.py顶部

from pydantic import field_validator, Field

现在我们可以为我们的两条新规则添加 Pydantic 字段验证器,以及内联文档

    class Options(llm.Options):
        length: Optional[int] = Field(
            description="Number of words to generate",
            default=None
        )
        delay: Optional[float] = Field(
            description="Seconds to delay between each token",
            default=None
        )

        @field_validator("length")
        def validate_length(cls, length):
            if length is None:
                return None
            if length < 2:
                raise ValueError("length must be >= 2")
            return length

        @field_validator("delay")
        def validate_delay(cls, delay):
            if delay is None:
                return None
            if not 0 <= delay <= 10:
                raise ValueError("delay must be between 0 and 10")
            return delay

让我们测试一下选项验证

llm -m markov "the cat sat on the mat" -o length -1
Error: length
  Value error, length must be >= 2

接下来,我们将修改execute()方法来处理这些选项。将此代码添加到llm_markov.py的开头

import time

然后用此代码替换execute()方法

    def execute(self, prompt, stream, response, conversation):
        text = prompt.prompt
        transitions = build_markov_table(text)
        length = prompt.options.length or 20
        for word in generate(transitions, length):
            yield word + ' '
            if prompt.options.delay:
                time.sleep(prompt.options.delay)

Markov模型类的顶部,紧随`model_id = “markov”`的那一行,添加can_stream = True。这告诉 LLM 该模型能够将内容流式传输到控制台。

完整的llm_markov.py文件现在应该如下所示

import llm
import random
import time
from typing import Optional
from pydantic import field_validator, Field


@llm.hookimpl
def register_models(register):
    register(Markov())


def build_markov_table(text):
    words = text.split()
    transitions = {}
    # Loop through all but the last word
    for i in range(len(words) - 1):
        word = words[i]
        next_word = words[i + 1]
        transitions.setdefault(word, []).append(next_word)
    return transitions


def generate(transitions, length, start_word=None):
    all_words = list(transitions.keys())
    next_word = start_word or random.choice(all_words)
    for i in range(length):
        yield next_word
        options = transitions.get(next_word) or all_words
        next_word = random.choice(options)


class Markov(llm.Model):
    model_id = "markov"
    can_stream = True

    class Options(llm.Options):
        length: Optional[int] = Field(
            description="Number of words to generate", default=None
        )
        delay: Optional[float] = Field(
            description="Seconds to delay between each token", default=None
        )

        @field_validator("length")
        def validate_length(cls, length):
            if length is None:
                return None
            if length < 2:
                raise ValueError("length must be >= 2")
            return length

        @field_validator("delay")
        def validate_delay(cls, delay):
            if delay is None:
                return None
            if not 0 <= delay <= 10:
                raise ValueError("delay must be between 0 and 10")
            return delay

    def execute(self, prompt, stream, response, conversation):
        text = prompt.prompt
        transitions = build_markov_table(text)
        length = prompt.options.length or 20
        for word in generate(transitions, length):
            yield word + " "
            if prompt.options.delay:
                time.sleep(prompt.options.delay)

现在我们可以请求一个 20 个单词的补全,token 之间延迟 0.1 秒,如下所示

llm -m markov "the cat sat on the mat" \
  -o length 20 -o delay 0.1

LLM 提供一个--no-stream选项,用户可以使用它来关闭流式传输。使用此选项会使 LLM 从流中收集响应,然后一次性将其返回到控制台。您可以这样尝试

llm -m markov "the cat sat on the mat" \
  -o length 20 -o delay 0.1 --no-stream

在这种情况下,它在收集 token 时仍会总共延迟 2 秒,然后一次性输出所有 token。

--no-stream选项会导致传递给execute()stream参数为 false。您的execute()方法可以根据是否进行流式传输表现出不同的行为。

选项也会被记录到数据库中。您可以在此处查看它们

llm logs -n 1
[
  {
    "id": 636,
    "model": "markov",
    "prompt": "the cat sat on the mat",
    "system": null,
    "prompt_json": null,
    "options_json": {
      "length": 20,
      "delay": 0.1
    },
    "response": "the mat on the mat on the cat sat on the mat sat on the mat cat sat on the ",
    "response_json": null,
    "reply_to_id": null,
    "chat_id": null,
    "duration_ms": 2063,
    "datetime_utc": "2023-07-07T03:02:28.232970"
  }
]

分发您的插件#

有许多不同的选项可以分发您的新插件,以便其他人可以试用。

您可以创建可下载的 wheel 或.zip.tar.gz文件,或通过 GitHub Gists 或仓库共享插件。

您还可以将您的插件发布到 PyPI(Python 包索引)。

Wheels 和 sdist 包#

生成可分发包的最简单选项是使用build命令。首先,通过运行此命令安装build

python -m pip install build

然后在您的插件目录中运行build以创建包

python -m build

这将创建两个文件:dist/llm-markov-0.1.tar.gzdist/llm-markov-0.1-py3-none-any.whl

这两个文件中的任何一个都可以用于安装插件

llm install dist/llm_markov-0.1-py3-none-any.whl

如果您将此文件托管在某个在线位置,其他人就可以使用pip install针对您的包的 URL 进行安装

llm install 'https://.../llm_markov-0.1-py3-none-any.whl'

您可以随时运行以下命令来卸载您的插件,这对于测试不同的安装方法非常有用

llm uninstall llm-markov -y

GitHub Gists#

分发简单插件的一个巧妙快捷的方法是将其托管在 GitHub Gist 中。GitHub 账户免费提供 Gists,可以是公开或私有的。Gists 可以包含多个文件,但不支持目录结构 - 这没问题,因为我们的插件只有两个文件,pyproject.tomlllm_markov.py

这是我为本教程创建的一个示例 Gist

https://gist.github.com/simonw/6e56d48dc2599bffba963cef0db27b6d

您可以通过右键点击“Download ZIP”按钮并选择“Copy Link”将 Gist 变成可安装的.zip URL。这是我的示例 Gist 的链接

https://gist.github.com/simonw/6e56d48dc2599bffba963cef0db27b6d/archive/cc50c854414cb4deab3e3ab17e7e1e07d45cba0c.zip

插件可以使用llm install命令安装,如下所示

llm install 'https://gist.github.com/simonw/6e56d48dc2599bffba963cef0db27b6d/archive/cc50c854414cb4deab3e3ab17e7e1e07d45cba0c.zip'

GitHub 仓库#

同样的方法也适用于常规的 GitHub 仓库:可以通过点击仓库顶部的绿色“Code”按钮找到“Download ZIP”按钮。由此提供的 URL 可用于安装位于该仓库中的插件。

发布插件到 PyPI#

Python 包索引 (PyPI)是 Python 包的官方仓库。您可以将您的插件上传到 PyPI 并为其保留一个名称 - 一旦完成,任何人都可以使用llm install <name>安装您的插件。

按照这些说明将包发布到 PyPI。简要版本如下

python -m pip install twine
python -m twine upload dist/*

您需要一个 PyPI 账户,然后可以输入您的用户名和密码 - 或者在 PyPI 设置中创建一个 token,并使用__token__作为用户名,token 作为密码。

添加元数据#

在将包上传到 PyPI 之前,最好添加文档并使用附加元数据扩展pyproject.toml

在您的插件目录根目录中创建一个README.md文件,其中包含有关如何安装、配置和使用您的插件的说明。

然后您可以将pyproject.toml替换为类似如下内容

[project]
name = "llm-markov"
version = "0.1"
description = "Plugin for LLM adding a Markov chain generating model"
readme = "README.md"
authors = [{name = "Simon Willison"}]
license = {text = "Apache-2.0"}
classifiers = [
    "License :: OSI Approved :: Apache Software License"
]
dependencies = [
    "llm"
]
requires-python = ">3.7"

[project.urls]
Homepage = "https://github.com/simonw/llm-markov"
Changelog = "https://github.com/simonw/llm-markov/releases"
Issues = "https://github.com/simonw/llm-markov/issues"

[project.entry-points.llm]
markov = "llm_markov"

这将拉取您的 README 文件,作为您的项目在 PyPI 上的列表页面的一部分显示。

它将llm添加为依赖项,确保如果有人尝试安装您的插件包时未安装 LLM,则会安装它。

它添加了一些有用页面的链接(如果这些链接对您的项目没有用,您可以删除project.urls部分)。

您还应该在您的包的 GitHub 仓库中放入一个LICENSE文件。我喜欢使用 Apache 2 许可证,就像这样

如果出现问题怎么办#

有时,您可能会对插件进行更改,导致其损坏,阻止llm启动。例如,您可能会看到类似以下的错误

$ llm 'hi'
Traceback (most recent call last):
  ...
  File llm-markov/llm_markov.py", line 10
    register(Markov()):
                      ^
SyntaxError: invalid syntax

您可能会发现无法使用llm uninstall llm-markov卸载插件,因为命令本身会因同样的错误而失败。

如果发生这种情况,您可以先使用LLM_LOAD_PLUGINS环境变量禁用插件,然后再卸载它,如下所示

LLM_LOAD_PLUGINS='' llm uninstall llm-markov