**Guidance是一种高效的编程范式,用于引导语言模型。**通过Guidance,您可以控制输出的结构,并为您的用例获得高质量的输出——*同时与传统的提示或微调相比,降低延迟和成本。*它允许用户约束生成(例如使用正则表达式和上下文无关文法),以及无缝地交织控制(条件语句、循环、工具使用)和生成。
安装
Guidance可通过PyPI获得,要使用特定模型,请参阅加载模型。
pip install guidance
功能特性
编写纯Python代码,附加语言模型功能。
from guidance import models, gen
# 加载模型(可以是Transformers、LlamaCpp、VertexAI、OpenAI等)
llama2 = models.LlamaCpp(path)
# 向模型追加文本或生成内容
llama2 + f'你想要笑话还是诗歌?' + gen(stop='。')
使用选择(即选项集)、正则表达式和上下文无关文法以及预构建组件(如子字符串、json)来约束生成。
from guidance import select
# 在两个选项之间进行简单选择
llama2 + f'你想要笑话还是诗歌?' + select(['笑话', '诗歌'])
轻松调用和部署工具,自动交织控制和生成。
轻松使用工具,模型在调用工具时停止生成,调用工具,然后恢复生成。例如,这里是一个简单版本的计算器,通过四个独立的"工具"实现:
@guidance
def add(lm, input1, input2):
lm += f' = {int(input1) + int(input2)}'
return lm
@guidance
def subtract(lm, input1, input2):
lm += f' = {int(input1) - int(input2)}'
return lm
@guidance
def multiply(lm, input1, input2):
lm += f' = {float(input1) * float(input2)}'
return lm
@guidance
def divide(lm, input1, input2):
lm += f' = {float(input1) / float(input2)}'
return lm
现在我们使用这些工具作为选项调用gen
。注意生成是如何自动停止和重新启动的:
lm = llama2 + '''\
1 + 1 = add(1, 1) = 2
2 - 3 = subtract(2, 3) = -1
'''
lm + gen(max_tokens=15, tools=[add, subtract, multiply, divide])
获得高兼容性——在多个后端执行单个Guidance程序
适用于Transformers、llama.cpp、AzureAI、VertexAI、OpenAI等。用户可以编写一个guidance程序并在多个后端执行。(注意,最强大的控制功能需要端点集成,目前在Transformers和llama.cpp上效果最佳)。
gpt = models.OpenAI("gpt-3.5-turbo")
with user():
lm = gpt + "法国的首都是什么?"
with assistant():
lm += gen("capital")
with user():
lm += "关于它的一个简短而令人惊讶的事实是什么?"
with assistant():
lm += gen("fact")
通过有状态控制 + 生成函数获得速度提升——无需中间解析器。
与链式调用相比,Guidance程序相当于单次LLM调用。更重要的是,所有非生成文本的追加都会被批处理,因此当您有固定结构时,Guidance程序比让LLM生成中间文本更快。
令牌修复
用户处理文本(或字节)而不是令牌,因此不必担心令牌边界问题,如"提示以空白结尾"。
使用f-strings的丰富模板。
llama2 + f'''\
你想要笑话还是诗歌?{select(['笑话', '诗歌'])}。
好的,这是一句话:"{gen(stop='"')}"
'''
抽象聊天接口,为任何聊天模型使用正确的特殊令牌。
# 将我们的选择捕获在名为'answer'下
lm = llama2 + f"你想要笑话还是诗歌?{select(['笑话', '诗歌'], name='answer')}。\n"
# 根据模型之前的选择做出选择
if lm["answer"] == "笑话":
lm += f"这是一个关于猫的一句话笑话:" + gen('output', stop='\n')
else:
lm += f"这是一首关于狗的一句话诗:" + gen('output', stop='\n')
易于编写的可重用组件。
import guidance
@guidance
def one_line_thing(lm, thing, topic):
lm += f'这是一个关于{topic}的一句话{thing}:' + gen(stop='\n')
return lm # 返回我们更新后的模型
# 选择笑话或诗歌
lm = llama2 + f"你想要笑话还是诗歌?{select(['笑话', '诗歌'], name='thing')}。\n"
# 调用我们的guidance函数
lm += one_line_thing(lm['thing'], '猫')
预构建组件库
常见语法元素开箱即用,下面是substring
的示例,其他(如json
)请查看文档。
from guidance import substring
# 定义一组可能的陈述
text = 'guidance很棒。guidance非常好。guidance是自切片面包以来最好的东西。'
# 强制模型做出精确引用
llama2 + f'这是关于guidance库的一个真实陈述:"{substring(text)}"'
流式支持,也集成了Jupyter笔记本。
lm = llama2 + '这是一首关于猫和狗的可爱五行诗:\n'
for i in range(5):
lm += f"第 {i+1} 行:" + gen(temperature=0.8, suffix="\n")
对于不支持guidance的丰富IPython/Jupyter/HTML可视化的环境(如控制台应用程序),可以通过在任何guidance.models
对象的构造函数中设置echo=False
来禁止所有可视化和控制台输出:
llama2 = models.LlamaCpp(path, echo=False)
多模态支持。
from guidance import image
gemini = models.VertexAI("gemini-pro-vision")
with user():
lm = gemini + "这是什么的图片?" + image("longs_peak.jpg")
with assistant():
lm += gen("回答")
示例笔记本
我们正在更新我们的示例笔记本。以下是已更新的内容:
更多内容即将推出
基本生成
lm
对象是不可变的,所以你通过创建新的副本来改变它。默认情况下,当你向lm
追加内容时,它会创建一个副本,例如:
from guidance import models, gen, select
llama2 = models.LlamaCpp(model)
# llama2没有被修改,`lm`是`llama2`的副本,其状态中追加了'This is a prompt'
lm = llama2 + 'This is a prompt'
你可以向模型对象追加_生成_调用,例如
lm = llama2 + 'This is a prompt' + gen(max_tokens=10)
你也可以在生成调用之间穿插纯文本或控制流:
# 注意我们如何设置停止标记
lm = llama2 + '我喜欢和我的' + gen(stop=' ') + '玩' + gen(stop=['\n', '。', '!'])
约束生成
选择(基础)
select
将生成限制在一组选项中:
lm = llama2 + '我喜欢的颜色是' + select(['红色', '蓝色', '绿色'])
正则表达式
gen
有可选参数regex
和stop_regex
,它们允许通过正则表达式控制生成(和停止)。
用正则表达式约束生成
无约束:
lm = llama2 + '问题:小明有十个球。他给了他弟弟三个。\n'
lm += '小明还剩多少个球?\n'
lm += '答案:' + gen(stop='\n')
用正则表达式约束:
lm = llama2 + '问题:小明有十个球。他给了他弟弟三个。\n'
lm += '小明还剩多少个球?\n'
lm += '答案:' + gen(regex='\d+')
使用正则表达式作为停止标准
无约束:
lm = llama2 + '19, 18,' + gen(max_tokens=50)
使用传统停止文本,当模型生成数字7时停止:
lm = llama2 + '19, 18,' + gen(max_tokens=50, stop='7')
当模型生成不被其他数字包围的字符7
时停止:
lm = llama2 + '19, 18,' + gen(max_tokens=50, stop_regex='[^\d]7[^\d]')
上下文无关文法
我们提供了多种操作符,使定义CFG变得容易,这些CFG可以用来约束生成。
例如,我们可以使用select
操作符(它接受CFG作为选项)、zero_or_more
和one_or_more
来定义数学表达式的语法:
import guidance
from guidance import one_or_more, select, zero_or_more
# stateless=True表示此函数不依赖于LLM生成
@guidance(stateless=True)
def number(lm):
n = one_or_more(select(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']))
# 允许负数或正数
return lm + select(['-' + n, n])
@guidance(stateless=True)
def operator(lm):
return lm + select(['+' , '*', '**', '/', '-'])
@guidance(stateless=True)
def expression(lm):
# 可以是
# 1. 一个数字(终结符)
# 2. 两个表达式加上一个运算符和可选的空格
# 3. 一个带括号的表达式
return lm + select([
number(),
expression() + zero_or_more(' ') + operator() + zero_or_more(' ') + expression(),
'(' + expression() + ')'
])
@guidance(stateless=True)
装饰器使得函数(如expression
)作为无状态语法存在,直到我们调用lm + expression()
或lm += expression()
时才会"执行"。例如,这里是一个_无约束_生成的例子:
# 无约束
lm = llama2 + '问题:小明有一百零六个球。然后他丢失了三十六个。\n'
lm += '等效的算术表达式:' + gen(stop='\n') + '\n'
注意模型写出了正确的等式但(错误地)解答了它。如果我们想约束模型只写出有效表达式(而不尝试解答),我们可以直接将我们的语法附加到它上面:
grammar = expression()
lm = llama2 + '问题:小明有一百零六个球。然后他丢失了三十六个。\n'
lm += '等效的算术表达式:' + grammar + '\n'
语法很容易组合。例如,假设我们想要一个语法,生成一个数学表达式或一个表达式后跟一个解答再跟另一个表达式。创建这个语法很简单:
from guidance import regex
grammar = select([expression(), expression() + regex(' = \d+; ') + expression()])
我们可以根据它生成:
llama2 + '这是一个二加二的数学表达式:' + grammar
llama2 + '2 + 2 = 4; 3+3\n' + grammar
即使你不喜欢以递归语法的方式思考,这种形式也可以轻松地限制生成。例如,假设我们有以下的一次性提示:
@guidance(stateless=True)
def ner_instruction(lm, input):
lm += f'''\
请用PER、ORG、LOC或无标记来标记输入中的每个单词
---
输入: John worked at Apple.
输出:
John: PER
worked:
at:
Apple: ORG
.:
---
输入: {input}
输出:
'''
return lm
input = 'Julia never went to Morocco in her life!!'
llama2 + ner_instruction(input) + gen(stop='---')
注意模型没有正确拼写"Morocco"这个词。有时模型也可能产生不存在的标记。我们可以通过添加更多的少样本例子来改进,但我们也可以限制生成过程来得到我们想要的确切格式:
import re
@guidance(stateless=True)
def constrained_ner(lm, input):
# 分词
words = [x for x in re.split('([^a-zA-Z0-9])', input) if x and not re.match('\s', x)]
ret = ''
for x in words:
ret += x + ': ' + select(['PER', 'ORG', 'LOC', '']) + '\n'
return lm + ret
llama2 + ner_instruction(input) + constrained_ner(input)
虽然constrained_ner(input)
是一个限制模型生成的语法,但它感觉就像你在用+=
和selects
编写普通的命令式Python代码。
捕获生成内容
通过使用capture
函数,无状态函数生成的字符串可以被保存到lm
对象中。capture
接受两个参数:无状态函数和用于存储捕获变量的名称。
from guidance import capture, one_or_more
ans = lm + "要闭合开放的括号序列[[ 对应的闭合括号是 " + capture(one_or_more("]"), "brackets")
ans["brackets"]
有状态控制 + 生成
不可变对象中的状态
每当你执行lm + grammar
、lm + gen
或lm + select
等操作时,你都会返回一个带有额外状态的新lm对象。例如:
lm = llama2 + '这是一个提示' + gen(name='test', max_tokens=10)
lm += select(['这个', '那个'], name='test2')
lm['test'], lm['test2']
有状态的{guidance}
函数
guidance装饰器默认为@guidance(stateless=False)
,这意味着带有此装饰器的函数依赖于lm状态来执行(可以是先前的状态或函数内生成的状态)。例如:
@guidance(stateless=False)
def test(lm):
lm += '我应该说"Scott"吗?\n' + select(['是', '否'], name='answer') + '\n'
if lm['answer'] == '是':
lm += 'Scott'
else:
lm += '不是Scott'
return lm
llama2 + test()
示例:ReAct
有状态控制的一大优势是你不需要编写任何中间解析器,即使后续操作依赖于模型生成的内容,添加后续"提示"也很容易。 例如,假设我们要实现这里中ReAct提示的第一个例子,并且假设有效的行为只有"搜索"或"完成"。我们可以这样写:
@guidance
def react_prompt_example(lm, question, max_rounds=10):
lm += f'问题: {question}\n'
i = 1
while True:
lm += f'思考 {i}: ' + gen(suffix='\n')
lm += f'行动 {i}: ' + select(['搜索', '完成'], name='act')
lm += '[' + gen(name='arg', suffix=']') + '\n'
if lm['act'] == '完成' or i == max_rounds:
break
else:
lm += f'观察 {i}: ' + search(lm['arg']) + '\n'
i += 1
return lm
注意我们不需要为行动和参数编写解析器并希望模型生成有效内容:我们强制执行它。还要注意,只有当模型选择以"完成"行动时(或者当我们达到最大轮数时),循环才会停止。
示例:更改聊天会话的中间步骤
我们还可以隐藏或更改模型生成的部分内容。例如,下面我们让一个聊天模型(注意我们使用特殊的role
块)命名一些专家来回答问题,但如果提到了"Ferriss",我们总是从列表中删除他:
from guidance import user, system, assistant
lm = llama2
query = '我如何提高生产力?'
with system():
lm += '你是一个有帮助且简洁的助手。'
with user():
lm += f'我想要回答以下问题:\n{query}\n'
lm += '列出3位能很好回答这个问题的世界级专家(过去或现在)。'
with assistant():
temp_lm = lm
for i in range(1, 4):
# 这个正则表达式只允许看起来像名字的字符串(每个单词都大写)
# list_append将结果添加到列表中
temp_lm += f'{i}. ' + gen(regex='([A-Z][a-z]*\s*)+', suffix='\n',
name='experts', list_append=True)
experts = [x for x in temp_lm['experts'] if 'Ferriss' not in x]
# 注意,即使模型在上面生成了'Ferriss',
# 它也不会被添加到`lm`中,只会添加到`temp_lm`中
lm += '、'.join(experts)
with user():
lm += '请像这些专家合作撰写匿名回答一样回答这个问题。'
with assistant():
lm += gen(max_tokens=100)
控制和生成的自动交错:工具使用
工具使用是有状态控制的常见情况。为了使其易于实现,gen
调用将tools
作为可选参数,其中每个工具由以下两部分定义:(1)触发其调用并捕获参数(如果有)的语法,以及(2)实际的工具调用。然后,随着生成的展开,每当模型生成的内容与工具调用的语法匹配时,它会(1)停止生成,(2)调用工具(工具可以向LM会话添加任何内容),然后(3)继续生成。
例如,这里我们如何利用上面的expression
语法来实现一个计算器工具:
from guidance import capture, Tool
@guidance(stateless=True)
def calculator_call(lm):
# capture只是"命名"表达式,以保存在LM状态中
return lm + '计算器(' + capture(expression(), 'tool_args') + ')'
@guidance
def calculator(lm):
expression = lm['tool_args']
# 出于安全考虑,你通常不想直接运行eval
# 这里我们保证只有数学表达式
lm += f' = {eval(expression)}'
return lm
calculator_tool = Tool(calculator_call(), calculator)
lm = llama2 + '这里有五个表达式:\n计算器(3 *3) = 33\n计算器(2 + 1 * 3) = 5\n'
lm += gen(max_tokens=30, tools=[calculator_tool], stop='\n\n')
Gsm8k示例
注意计算器在生成过程中是无缝调用的。这里是一个更现实的例子,展示模型解决gsm8k问题:
@guidance
def math_with_calc(lm, question):
# 两个示例
lm += '''\
问题:John开始有2个球。然后他将球的数量增加到原来的5倍。之后他失去了一半。然后他给了他弟弟3个。他还剩下多少个?
推理过程:
1. 他将球的数量增加到5倍。所以他有calculator(2 * 5) = 10个球。
2. 他失去了一半。所以他有calculator(10 / 2) = 5个球。
3. 他给了他弟弟3个。所以他有calculator(5 - 3) = 2个球。
答案:2
问题:Jill每天得到7美元的零用钱。她每天用1美元买公交车票,然后把一半的钱捐出去。她每天还剩多少钱?
推理过程:
1. 她每天得到7美元。
2. 她花1美元买公交车票。所以她有calculator(7 - 1) = 6美元。
3. 她捐出一半。所以剩下calculator(6 / 2) = 3美元。
答案:3
'''
lm += f'问题:{question}\n'
lm += '推理过程:\n' + gen(max_tokens=200, tools=[calculator_tool], stop='答案')
# 只允许数字或逗号
lm += '答案:' + gen(regex='[-\d,]+')
return lm
question = '''Janet的鸭子每天下16个蛋。她每天早上吃三个当早餐,每天用四个给朋友烤松饼。她每天在农贸市场以每个新鲜鸭蛋2美元的价格出售剩下的蛋。她每天在农贸市场能赚多少美元?'''
llama2 + math_with_calc(question)
@guidance函数的自动调用语法
你也可以用任何带有@guidance装饰器的函数初始化一个Tool
,默认的调用语法就像Python调用一样。这里有一个在同一个gen
调用中使用多个这样的工具的例子:
@guidance
def say_scott(lm, n):
lm += '\n'
for _ in range(int(n)):
lm += 'Scott\n'
return lm
@guidance
def say_marco(lm, n):
lm += '\n'
for _ in range(int(n)):
lm += 'marco\n'
return lm
tools = [Tool(callable=say_scott), Tool(callable=say_marco)]
llama2 + '''\
我将多次调用say_scott和say_marco:
say_scott(1)
Scott
''' + gen(max_tokens=20, tools=tools)
文本,而非词元
大多数语言模型使用的标准贪婪分词方法会引入各种微妙而强大的偏见,这可能会对你的提示产生各种意想不到的后果。 例如,看下面这个给gpt-2(标准贪婪分词)的提示:
hf_gen(prompt, max_tokens=10)
from transformers import pipeline
pipe = pipeline("text-generation", model="gpt2")
def hf_gen(prompt, max_tokens=100):
return pipe(prompt, do_sample=False, max_length=max_tokens, return_full_text=False)[0]['generated_text']
prompt = 'http:'
hf_gen(prompt, max_tokens=10)
注意LLM生成的输出并没有用明显的下一个字符(两个正斜杠)来完成URL。相反,它在中间加了一个空格,创建了一个无效的URL字符串。为什么?因为字符串://
是一个独立的词元,所以一旦模型看到单独的冒号,它就假设下一个字符不可能是//
;否则,分词器就不会使用:
,而是会使用://
。这就是为什么有警告说不要在提示词的末尾加空格,但问题远比这更普遍:任何可能跨越多个词元的边界都会造成问题,例如,注意部分词如何导致错误的补全:
prompt = 'John is a'
hf_gen(prompt, max_tokens=5)
prompt = 'John is a fo'
hf_gen(prompt, max_tokens=5)
虽然对普通提示来说已经够problematic了,但对于我们在这个readme中写的那种提示,这些问题将是灾难性的,因为在这种提示中,提示和生成是多次交错进行的(因此有多个出问题的机会)。这就是为什么{guidance}
实现了词元修复,这个功能可以自动处理提示边界,让用户只需考虑文本而不是词元。例如:
from guidance import models
gpt = models.Transformers('gpt2')
prompt = 'http:'
gpt + prompt + gen(max_tokens=10)
prompt = 'John is a fo'
gpt + prompt + gen(max_tokens=2)
快速
集成的有状态控制更快
我们在与transformers
和llamacpp
的集成中完全控制解码循环,允许我们添加控制和额外提示而不需要任何额外成本。
相反,如果我们调用服务器,我们需要付出额外的成本来进行额外的请求,如果服务器有缓存,这可能还好,但如果服务器没有细粒度的缓存,这很快就变得不切实际。例如,再次注意上面gsm8k示例和计算器的输出:
每次我们调用calculator
,我们都必须停止生成,将结果附加到提示中,然后恢复生成。为了避免第一次调用后速度变慢,服务器需要保持KV缓存到'3 for breakfast. So she has calculator(16 - 3)',然后从那一点开始继续生成。即使有缓存的服务器通常也没有办法保证在每次停止和开始时都保留状态,因此用户在每次中断时都会付出显著的开销。将所有内容都视为新提示的正常方法会导致每次调用calculator
时都会大大减慢速度。
Guidance加速
除了上述好处外,由于我们可以批处理执行展开时用户添加的任何额外文本(而不是生成它),{guidance}
调用通常比传统方式运行等效提示更快。看下面的例子,我们使用llama.cpp执行的GGUF压缩llama2
7B生成一个json:
@guidance
def character_maker(lm, id, description, valid_weapons):
lm += f"""\
以下是RPG游戏中的角色简介,采用JSON格式。
```json
{{
"id": "{id}",
"description": "{description}",
"name": "{gen('name', stop='"')}",
"age": {gen('age', regex='[0-9]+', stop=',')},
"armor": "{select(options=['皮甲', '锁子甲', '板甲'], name='armor')}",
"weapon": "{select(options=valid_weapons, name='weapon')}",
"class": "{gen('class', stop='"')}",
"mantra": "{gen('mantra', stop='"')}",
"strength": {gen('strength', regex='[0-9]+', stop=',')},
"items": ["{gen('item', list_append=True, stop='"')}", "{gen('item', list_append=True, stop='"')}", "{gen('item', list_append=True, stop='"')}"]
}}```"""
return lm
a = time.time()
lm = llama2 + character_maker(1, '一个敏捷的战士', ['斧头', '剑', '弓'])
time.time() - a
所有非绿色部分实际上并非由模型生成,因此被批处理(速度更快)。这个提示在A100 GPU上大约需要1.2秒。现在,如果我们让模型生成所有内容(如下面大致等效的提示所示),大约需要2.6秒(不仅速度更慢,我们对生成的控制也更少)。
@guidance
def character_maker2(lm, id, description):
lm += f"""\
以下是RPG游戏中JSON格式的角色简介。包含'id'、'description'、'name'、'age'、'armor'、'weapon'、'class'、'mantra'、'strength'和'items(仅3件物品的名称)'字段
请将description设置为'{description}'
```json""" + gen(stop='```')
return lm
a = time.time()
lm = llama2 + character_maker2(1, '一个敏捷的战士')
time.time() - a
加载模型
llama.cpp
安装Python绑定:
CMAKE_ARGS="-DGGML_CUDA=on" pip install llama-cpp-python
加载模型:
from guidance import models
lm = models.LlamaCpp(path_to_model, n_gpu_layers=-1)
Transformers
安装transformers:
from guidance import models
lm = models.Transformers(model_name_or_path)
Vertex AI
没有明确guidance集成的远程端点以"乐观"方式运行。这意味着所有可强制的文本都作为提示(或聊天上下文)提供给模型,然后模型以流式模式运行,没有硬性约束(因为远程API不支持)。如果模型违反约束,则停止模型流,并可选择在该点重试。这意味着所有API支持的控制按预期工作,而API不支持的更复杂控制/解析在模型与程序保持一致时也能工作。
palm2 = models.VertexAI("text-bison@001")
with instruction():
lm = palm2 + "请说一个关于西雅图的有趣事实?"
lm + gen("fact", max_tokens=100)
OpenAI
OpenAI端点没有直接支持guidance语法,但通过乐观运行,我们仍然可以以匹配模型类型的方式控制它们:
传统补全模型:
curie = models.OpenAI("text-curie-001")
curie + "最小的猫是" + gen(stop="。")
指令微调模型:
gpt_instruct = models.OpenAI("gpt-3.5-turbo-instruct")
with instruction():
lm = gpt_instruct + "最小的猫是哪些?"
lm += gen(stop="。")
聊天模型:
gpt = models.OpenAI("gpt-3.5-turbo")
with system():
lm = gpt + "你是一位猫咪专家。"
with user():
lm += "最小的猫是哪些?"
with assistant():
lm += gen("answer", stop="。")