开发一个模型插件#

本教程将引导您开发一个新的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']}

执行马尔可夫链#

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

有些单词可能没有来自我们训练句子的后续单词。对于我们的实现,我们将退回到从我们的集合中随机选择一个单词。

我们将使用Python生成器来实现这一点,使用yield关键字生成每个标记:

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}

现在当你运行日志命令时,你也会看到:

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: 一个浮点数,表示输出令牌之间的延迟

delay 令牌将让我们模拟一个流式语言模型,其中令牌需要时间生成,并在准备就绪时通过 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个词的完成,每个词之间有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

在这种情况下,它仍然会总共延迟2秒,同时收集令牌,然后一次性输出它们。

那个--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"
  }
]

分发你的插件#

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

你可以创建一个可下载的轮子或.zip.tar.gz文件,或者通过GitHub Gists或仓库分享插件。

你也可以将你的插件发布到PyPI,即Python包索引。

轮子和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上。这些Gist可以通过GitHub账户免费获得,并且可以是公开的或私有的。Gist可以包含多个文件但不支持目录结构——这没关系,因为我们的插件只有两个文件,pyproject.tomlllm_markov.py

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

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

你可以通过右键点击“下载ZIP”按钮并选择“复制链接”来将一个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 Package Index (PyPI) 是 Python 包的官方存储库。您可以将您的插件上传到 PyPI 并为其保留一个名称 - 一旦完成,任何人都可以使用 llm install 安装您的插件。

按照这些说明将包发布到PyPI。简而言之:

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

你需要在PyPI上拥有一个账户,然后你可以输入你的用户名和密码 - 或者在PyPI设置中创建一个令牌,并使用__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作为依赖项,确保如果有人尝试在没有它的情况下安装你的插件包时,它将被安装。

它添加了一些有用页面的链接(如果这些链接对你的项目没有用,你可以删除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