byzerllm 大模型编程快速指南
本文示例 Notebook
安装
pip install -U byzerllm
ray start --head
启动一个模型代理
byzerllm 支持私有化模型或者SaaS模型的部署。
这里以 deepseek 官方API 为例:
easy-byzerllm deploy deepseek-chat --token xxxxx --alias deepseek_chat
或者跬基流动API:
easy-byzerllm deploy alibaba/Qwen1.5-110B-Chat --token xxxxx --alias qwen110b_chat
将上面的 API KEY 替换成你们自己的。
如果你想部署私有化模型或者对接 Ollama 等更多需求,参考 002_使用byzerllm进行模型部署.md 或者 README.md。
之后,你就可以在代码里使用 deepseek_chat 或者 qwen110b_chat 访问模型了。
hello world
来和我们的大模型打个招呼:
import byzerllm
llm = byzerllm.ByzerLLM.from_default_model(model="deepseek_chat")
@byzerllm.prompt(llm=llm)
def hello(q:str) ->str:
'''
你好, {{ q }}
'''
s = hello("你是谁")
print(s)
## 输出:
## '你好!我是一个人工智能助手,专门设计来回答问题、提供信息和帮助解决问题。如果你有任何疑问或需要帮助,请随时告诉我。'
恭喜,你和大模型成功打了招呼!
可以看到,我们通过 @byzerllm.prompt
装饰器,将一个方法转换成了一个大模型的调用,然后这个方法的主题是一段文本,文本中
使用了 jinja2 模板语法,来获得方法的参数。当正常调用该方法时,实际上就发起了和大模型的交互,并且返回了大模型的结果。
在 byzerllm 中,我们把这种方法称之为 prompt 函数。
查看发送给大模型的prompt
很多情况你可能需要调试,查看自己的 prompt 渲染后到底是什么样子的,这个时候你可以通过如下方式 获取渲染后的prompt:
hello.prompt("你是谁")
## '你好, 你是谁'
动态换一个模型
前面的 hello 方法在初始化的时候,我们使用了默认的模型 deepseek_chat,如果我们想换一个模型,可以这样做:
hello.with_llm(llm).run("你是谁")
## '你好!我是一个人工智能助手,专门设计来回答问题、提供信息和帮助解决问题。如果你有任何疑问或需要帮助,请随时告诉我。'
通过 with_llm 你可以设置一个新的 llm 对象,然后调用 run 方法,就可以使用新的模型了。
超长文本生成
我们知道,大模型一次生成的长度其实是有限的,如果你想生成超长文本,你可能需手动的不断获得 生成结果,然后把他转化为输入,然后再次生成,这样的方式是比较麻烦的。
byzerllm 提供了更加易用的 API :
import byzerllm
from byzerllm import ByzerLLM
llm = ByzerLLM.from_default_model("deepseek_chat")
@byzerllm.prompt()
def tell_story() -> str:
"""
讲一个100字的故事。
"""
s = (
tell_story.with_llm(llm)
.with_response_markers()
.options({"llm_config": {"max_length": 10}})
.run()
)
print(s)
## 从前,森林里住着一只聪明的小狐狸。一天,它发现了一块闪闪发光的宝石。小狐狸决定用这块宝石帮助森林里的动物们。它用宝石的光芒指引迷路的小鹿找到了回家的路,用宝石的温暖治愈了受伤的小鸟。从此,小狐狸成了森林里的英雄,动物们都感激它的善良和智慧。
实际核心部分就是这一行:
tell_story.with_llm(llm)
.with_response_markers()
.run()
我们只需要调用 with_response_markers
方法,系统就会自动的帮我们生成超长文本。
在上面的案例中,我们通过
.options({"llm_config": {"max_length": 10}})
认为的限制大模型一次交互最多只能输出10个字符,但是系统依然自动完成了远超过10个字符的文本生成。
对象输出
前面我们的例子都是返回字符串,但是我们也可以返回对象,这样我们就可以更加灵活的处理返回结果。
import pydantic
class Story(pydantic.BaseModel):
'''
故事
'''
title: str = pydantic.Field(description="故事的标题")
body: str = pydantic.Field(description="故事主体")
@byzerllm.prompt()
def tell_story()->Story:
'''
讲一个100字的故事。
'''
s = tell_story.with_llm(llm).run()
print(isinstance(s, Story))
print(s.title)
## True
## 勇敢的小鸟
可以看到,我们很轻松的将输出转化为格式化输出。
自定义字段抽取
前面的结构化输出,其实会消耗更多token,还有一种更加精准的结构化输出方式。 比如让大模型生成一个正则表达式,但实际上大模型很难准确只输出一个正则表达式,这个时候我们可以通过自定义抽取函数来获取我们想要的结果。
from loguru import logger
import re
@byzerllm.prompt()
def generate_regex_pattern(desc: str) -> str:
"""
根据下面的描述生成一个正则表达式,要符合python re.compile 库的要求。
{{ desc }}
最后生成的正则表达式要在<REGEX></REGEX>标签对里。
"""
def extract_regex_pattern(regex_block: str) -> str:
pattern = re.search(r"<REGEX>(.*)</REGEX>", regex_block, re.DOTALL)
if pattern is None:
logger.warning("No regex pattern found in the generated block:\n {regex_block}")
raise None
return pattern.group(1)
pattern = "匹配一个邮箱地址"
v = generate_regex_pattern.with_llm(llm).with_extractor(extract_regex_pattern).run(desc=pattern)
print(v)
## ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
在上面的例子里,我们根据一句话生成一个正则表达式。我们通过 with_extractor
方法,传入了一个自定义的抽取函数,这个函数会在大模型生成结果后,对结果进行处理,然后返回我们想要的结果。
我们在 prompt 明确说了,生成的结果要放到 <REGEX></REGEX>
标签对里,然后我们通过 extract_regex_pattern 函数,从结果中提取出了我们想要的正则表达式。
在实例方法中使用大模型
import byzerllm
data = {
'name': 'Jane Doe',
'task_count': 3,
'tasks': [
{'name': 'Submit report', 'due_date': '2024-03-10'},
{'name': 'Finish project', 'due_date': '2024-03-15'},
{'name': 'Reply to emails', 'due_date': '2024-03-08'}
]
}
class RAG():
def __init__(self):
self.llm = byzerllm.ByzerLLM()
self.llm.setup_template(model="deepseek_chat",template="auto")
self.llm.setup_default_model_name("deepseek_chat")
@byzerllm.prompt(lambda self:self.llm)
def generate_answer(self,name,task_count,tasks)->str:
'''
Hello {{ name }},
This is a reminder that you have {{ task_count }} pending tasks:
{% for task in tasks %}
- Task: {{ task.name }} | Due: {{ task.due_date }}
{% endfor %}
Best regards,
Your Reminder System
'''
t = RAG()
response = t.generate_answer(**data)
print(response)
## 输出:
## Hello Jane Doe,
##I hope this message finds you well. I wanted to remind you of your 3 pending tasks to ensure you stay on track:
## 1. **Submit report** - This task is due on **2024-03-10**. Please ensure that you allocat
这里我们给了个比较复杂的例子,但我们可以看到,给一个实例prompt方法和普通prompt 方法差异不大。 唯一的区别是如果你希望在定义的时候就指定大模型,使用一个lambda函数返回实例的 llm 对象即可。
@byzerllm.prompt(lambda self:self.llm)
你也可以不返回,在调用的时候通过 with_llm
方法指定 llm 对象。
此外,这个例子也展示了如何通过jinja2模板语法,来处理复杂的结构化数据。
通过 Python 代码处理复杂入参
上面的一个例子中,我们通过 jinja2 模板语法,来处理复杂的结构化数据,但是有时候我们可能需要更加复杂的处理,这个时候我们可以通过 Python 代码来处理。
import byzerllm
data = {
'name': 'Jane Doe',
'task_count': 3,
'tasks': [
{'name': 'Submit report', 'due_date': '2024-03-10'},
{'name': 'Finish project', 'due_date': '2024-03-15'},
{'name': 'Reply to emails', 'due_date': '2024-03-08'}
]
}
class RAG():
def __init__(self):
self.llm = byzerllm.ByzerLLM.from_default_model(model="deepseek_chat")
@byzerllm.prompt()
def generate_answer(self,name,task_count,tasks)->str:
'''
Hello {{ name }},
This is a reminder that you have {{ task_count }} pending tasks:
{{ tasks }}
Best regards,
Your Reminder System
'''
tasks_str = "\n".join([f"- Task: {task['name']} | Due: { task['due_date'] }" for task in tasks])
return {"tasks": tasks_str}
t = RAG()
response = t.generate_answer.with_llm(t.llm).run(**data)
print(response)
## Just a gentle nudge to keep you on track with your pending tasks. Here's a quick recap:....
在这个例子里,我们直接把 tasks 在方法体里进行处理,然后作为一个字符串返回,最够构建一个字典,字典的key为 tasks,然后
你就可以在 docstring 里使用 {{ tasks }}
来引用这个字符串。
这样对于很复杂的入参,就不用谢繁琐的 jinja2 模板语法了。
如何自动实现一个方法
比如我定义一个签名,但是我不想自己实现里面的逻辑,让大模型来实现。这个在 byzerllm 中叫 function impl。我们来看看怎么 实现:
import pydantic
class Time(pydantic.BaseModel):
time: str = pydantic.Field(...,description="时间,时间格式为 yyyy-MM-dd")
@llm.impl()
def calculate_current_time()->Time:
'''
计算当前时间
'''
pass
calculate_current_time()
#output: Time(time='2024-06-14')
在这个例子里,我们定义了一个 calculate_current_time 方法,但是我们没有实现里面的逻辑,我们通过 @llm.impl()
装饰器,让大模型来实现这个方法。
为了避免每次都要“生成”这个方法,导致无法适用,我们提供了缓存,用户可以按如下方式打印速度:
start = time.monotonic()
calculate_current_time()
print(f"first time cost: {time.monotonic()-start}")
start = time.monotonic()
calculate_current_time()
print(f"second time cost: {time.monotonic()-start}")
# output:
# first time cost: 6.067266260739416
# second time cost: 4.347506910562515e-05
可以看到,第一次执行花费了6s,第二次几乎是瞬间完成的,这是因为第一次执行的时候,我们实际上是在生成这个方法,第二次执行的时候,我们是执行已经生成好的代码,所以速度会非常快。你可以显示的调用 llm.clear_impl_cache()
清理掉函数缓存。
Stream 模式
前面的例子都是一次性生成结果,但是有时候我们可能需要一个流式的输出,这个时候我们可能需要用底层一点的API来完成了:
import byzerllm
llm = byzerllm.ByzerLLM.from_default_model(model="deepseek_chat")
v = llm.stream_chat_oai(model="deepseek_chat",conversations=[{
"role":"user",
"content":"你好,你是谁",
}],delta_mode=True)
for t in v:
print(t,flush=True)
# 你好
# !
# 我
# 是一个
# 人工智能
# 助手
# ,
# 旨在
# 提供
# 信息
# 、
# 解答
# 问题....
如果你不想要流式输出,但是想用底层一点的API,你可以使用 llm.chat_oai
方法:
import byzerllm
llm = byzerllm.ByzerLLM.from_default_model(model="deepseek_chat")
v = llm.chat_oai(model="deepseek_chat",conversations=[{
"role":"user",
"content":"你好,你是谁",
}])
print(v[0].output)
## 你好!我是一个人工智能助手,旨在提供信息、解答问题和帮助用户解决问题。如果你有任何问题或需要帮助,请随时告诉我。
Function Calling
byzerllm 可以不依赖模型自身就能提供 function calling 支持,我们来看个例子:
from typing import List,Dict,Any,Annotated
import pydantic
import datetime
from dateutil.relativedelta import relativedelta
def compute_date_range(count:Annotated[int,"时间跨度,数值类型"],
unit:Annotated[str,"时间单位,字符串类型",{"enum":["day","week","month","year"]}])->List[str]:
'''
计算日期范围
Args:
count: 时间跨度,数值类型
unit: 时间单位,字符串类型,可选值为 day,week,month,year
'''
now = datetime.datetime.now()
now_str = now.strftime("%Y-%m-%d %H:%M:%S")
if unit == "day":
return [(now - relativedelta(days=count)).strftime("%Y-%m-%d %H:%M:%S"),now_str]
elif unit == "week":
return [(now - relativedelta(weeks=count)).strftime("%Y-%m-%d %H:%M:%S"),now_str]
elif unit == "month":
return [(now - relativedelta(months=count)).strftime("%Y-%m-%d %H:%M:%S"),now_str]
elif unit == "year":
return [(now - relativedelta(years=count)).strftime("%Y-%m-%d %H:%M:%S"),now_str]
return ["",""]
def compute_now()->str:
'''
计算当前时间
'''
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
我们定义了两个方法,一个是计算日期范围,一个是计算当前时间。
现在我么可以来测试下,系统如何根据自然语言决定调用哪个方法:
t = llm.chat_oai([{
"content":'''计算当前时间''',
"role":"user"
}],tools=[compute_date_range,compute_now],execute_tool=True)
t[0].values
## output: ['2024-06-14 15:18:02']
我们可以看到,他正确的选择了 compute_now 方法。
接着我们再试一个:
t = llm.chat_oai([{
"content":'''最近三个月趋势''',
"role":"user"
}],tools=[compute_date_range,compute_now],execute_tool=True)
t[0].values
## output: [['2024-03-14 15:19:13', '2024-06-14 15:19:13']]
模型正确的选择了 compute_date_range 方法。
注意事项
- prompt函数方法体返回只能是dict,实际的返回类型和方法签名可以不一样,但是方法体返回只能是dict。
- 大部分情况prompt函数体为空,如果一定要有方法体,可以返回一个空字典。
- 调用prompt方法的时候,如果在@byzerllm.prompt()里没有指定llm对象,那么需要在调用的时候通过with_llm方法指定llm对象。