开发模型插件#
本教程将引导您开发一个新的 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 模型可以接受选项。对于大型语言模型,这些选项可以是诸如temperature
或top_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.gz
和dist/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.toml
和llm_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