开发一个模型插件#
本教程将引导您开发一个新的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模型可以接受选项。对于大型语言模型,这些选项可以是temperature或top_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.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上。这些Gist可以通过GitHub账户免费获得,并且可以是公开的或私有的。Gist可以包含多个文件但不支持目录结构——这没关系,因为我们的插件只有两个文件,pyproject.toml 和 llm_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