大模型应用开发极简入门

第1章 初识 GPT-4

LLM 概述

探索语言模型和 NLP 的基础

GPT-4 和 ChatGPT 基于一种特定的神经网络架构 Transformer;

Transformer 通过关注句子和段落的不同部分,学习到句子中各个单词的模式和关系,从而能够理解上下文;因此,Transformer 很擅长语言翻译、问题回答和文本生成类的任务;

最早的语言模型是 n-gram,它的原理很简单,就是统计各个单词之间按顺序出现的概率。当给出 A 单词时,它根据概率预测下一个单词 B;

为了提高 n-gram 的性能,之后引入了循环神经网络(RNN,recurrent neural networ)和长短期记忆(LSTM,long short term memory)的方法,以便模型能够更好的理解上下文;

理解 Transformer

在处理短文本时,RNN 效果还行,但如果是长文本,上下文很大,超过了 RNN 的处理能力,容易出现上下文遗忘的问题;

Transformer 通过使用注意力机制,解决了 RNN 面临的问题;所谓的注意力机制,即模型不再将所有单词视为同等重要,而是只关注其中少数几个关键词,这样既降低了数据处理的负担,又提高了准确性;

注意力机制有两个模块,分别为:

  • 交叉注意力:输入与输出的相关性匹配;(关注重点)
  • 自注意力:区分输入的不同部分之间的相对重要性;(分清主次)

自注意力机制给句子中的不同单词分配不同的权重后,有助于抓出核心单词,构建新的抽象概念;

解密 GPT 模型的标记化和预测步骤

文本补全:接受一段输入(提示词),生成一段输出;

Transformer 架构使得模型能够识别关键信息和上下文;它在生成预测时,也用到了迭代机制,即先预测下一个最有可能出现的词,之后基于包含预测词的新句子,再次预测下一个可能出现的词。不断迭代重复这个过程,直到生成整个完整的句子;

GPT 模型简史

早期的 NLP 模型是监督式的,用于分类预测,需要使用标注数据进行训练,但标注的成本很高,难以获得大规模的训练数据;GPT 则是无监督式的,突破了这个限制,模型参数的数量也因此急剧膨胀;

基于预测的工作模式,并不能满足大部分使用场景,因此 GPT3 引入了微调和反馈,即使用提示词让模型根据反馈再次学习,从而得出更准确的结果;

所谓的微调和反馈,即人工编写一个示例打个样,然后做为训练数据,让模型学习;

微调的三个步骤:

  • 制作示例,训练模型
  • 基于相同问题,设置不同的随机数(temperature),以便模型输出多个不同的结果,然后给结果打分;
  • 基于打分结果,设置奖励机制,再次训练模型;

案例

  • Be My Ayes:作为盲人的虚拟助手,帮助他们获得视觉信息;
  • 摩根士丹利:从公司报告库中,快速检索文本,以获得答案;
  • 可汗学院:生成虚拟的教学助手,回答学生的提问,并给予鼓励;
  • 多邻国:通过角色扮演,让学生获得沉浸式的学习氛围
  • Yabble:分析消费者的数据,提供见解,辅助决策;
  • Waymark:快速生成视频脚本,提高视频制作人员的效率;
  • Inworld AI:生成有趣、有独特性格特点的游戏角色,提高游戏开发效率;

AI 幻觉

模型很适合用来处理创意型的任务,因为这些任务并没有标准答案。但目前还不太适合用来生成严肃问题的答案,例如医学诊断等;因为这些任务的准确性要求很高,而且 AI 的本质上是预测,因此有时候它提供的答案并不正确,却对错误答案信誓旦旦,有可能会误导提问者;

使用插件和微调优化 GPT 模型

使用插件能够让 GPT 能够更好的处理特定任务;

第2章 深入了解 API

基本概念

针对不同的使用场景,OpenAI 提供了不同的模型,几种场景包括:

  • 聊天
  • 助手
  • 补全
  • 文本转语音

OpenAI 的模型

  • InstructGPT
  • ChatGPT
  • GPT-4

使用 GPT 模型

预设提示词的使用示例,每个示例提供了一些特定的自定义参数;

OpenAI 既可以使用 GUI 来调用,也可以通过 API 来调用;

1
2
3
4
5
6
7
8
9
10
11
import openai
openai.api_key = os.getenv("OPENAI_API_KEY")
response = openai.Completion.create(
model="text-davinci-003",
prompt="As Descartes said, I think therefore",
temperature=0.7,
max_tokens=3,
top_p=1,
frequency_penalty=0,
presence_penalty=0,
)

OpenAI Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import openai
#对GPT-3.5 Turbo来说,端点是ChatCompletion
openai.ChatCompletion.create(
#对GPT-3.5 Turbo来说,模型是gpt-3.5-turbo
model="gpt-3.5-turbo",
#消息列表形式的对话
messages=[
{"role": "system", "content": "You are a helpful teacher."},
{
"role": "user",
"content": "Are there other measures than time \
complexity for an algorithm?",
},
{
"role": "assistant",
"content": "Yes, there are other measures besides time \
complexity for an algorithm, such as space complexity.",
},
{"role": "user", "content": "What is it?"},
],
)

每次调用 API 时,都需要附带整个对话记录,因为模型并不记忆上下文,所以需要在请求中携带上下文;

从文本补全到函数

以下示例通过文字描述想要查询的目标,由 GPT 转成 SQL 语句,然后调用预设好的函数,执行该 SQL 语句;得到数据库的查询结果后,再发给 GPT,转换成普遍人能够读懂的结果(用文字来描述查询结果);

1
2
3
4
5
6
7
# 示例函数(有点像 JS 里面的回调函数)
def find_product(sql_query):
results = [
{"name": "pen", "color": "blue", "price": 1.99},
{"name": "pen", "color": "red", "price": 1.78},
]
return results
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 函数定义
functions = [
{
"name": "find_product",
"description": "Get a list of products from a sql query",
"parameters": {
"type": "object",
"properties": {
"sql_query": {
"type": "string",
"description": "A SQL query",
}
},
"required": ["sql_query"],
},
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#示例问题
user_question = "I need the top 2 products where the price is less
than 2.00"
messages = [{"role": "user", "content": user_question}]
#使用函数定义调用ChatCompletion端点
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613", messages=messages,
functions=functions
)
response_message = response["choices"][0]["message"]
messages.append(response_message)

'''
"function_call": {
"name": "find_product",
"arguments": '{\n "sql_query": "SELECT * FROM products \
WHERE price < 2.00 ORDER BY price ASC LIMIT 2"\n}',
}
'''
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#调用函数
function_args = json.loads(
response_message["function_call"]["arguments"]
)
products = find_product(function_args.get("sql_query"))
#将函数的响应附加到消息中
messages.append(
{
"role": "function",
"name": function_name,
"content": json.dumps(products),
}
)
#将函数的响应格式化为自然语言
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo-0613",
messages=messages,
)

基于聊天模型的输入,必须使用对话格式;而补全模型的输入则可以是单独的提示词;

其他 API 功能

嵌入

将不是数值类型的元素,转换成数值向量;嵌入有个突出的特点,即它将数据转换成数值格式后,这些数值之间仍然保持语义相似性;即含义相近的词,在数值空间的距离也更近;

嵌入在 LLM 中用得非常广泛,一般使用 512 维来表示,这使得模型能够存储复杂的关系;

内容审核模型

为了确保用户输入的提示词,符合法律规定,OpenAI 还部署了一个内容审核模型,以确保用户的输入合法;

Whipser 和 DALL-E

Whipser 用于语音识别、翻译等场景;

DALL-E 则能够根据自然语言的描述,生成图片;

第3章 构建应用程序

应用程序开发概述

从环境变量读取密钥

软件架构设计原则

OpenAPI 的接口很可能会更新,因此有必要在前端与 OpenAI 之间增加一层抽象,以便将前端和 OpenAI 之间进行解耦

LLM 驱动型应用程序的漏洞

由于用户的输入存在不可控的因素,因此有必要采取措施,对输入进行检查,否则有可能会发生注入攻击;

示例项目

新闻稿生成器

根据某个事实发生的事件,生成新闻稿;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import openai
def ask_chatgpt(messages):
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo", messages=messages
)
return response["choices"][0]["message"]["content"]


prompt_role = "You are an assistant for journalists. \
Your task is to write articles, based on the FACTS that are given to you. \
You should respect the instructions: the TONE, the LENGTH, \
and the STYLE"


from typing import Listdef assist_journalist(
facts: List[str], tone: str, length_words: int, style: str
):
facts = ", ".join(facts)
prompt = f"{prompt_role} \
FACTS: {facts} \
TONE: {tone} \
LENGTH: {length_words} words \
STYLE: {style}"
return ask_chatgpt([{"role": "user", "content": prompt}])
1
2
3
4
5
6
7
# 测试
print(
assist_journalist(
["The sky is blue", "The grass is green"], "informal", \
100, "blogpost"
)
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 生成结果
"Hey, everyone! Did you know that the sky is blue and the grass is green?
I mean, it's something we see every day and probably take for granted,
but it's still pretty amazing if you think about it! The sky appears
blue to us because of something called Rayleigh scattering – basically,
the molecules in the Earth's atmosphere scatter sunlight in all different
directions. Blue light has a shorter wavelength, so it gets scattered
more than the other colors in the spectrum. That's why the sky looks
blue most of the time! As for the grass being green... that's due to
chlorophyll, the pigment that helps plants capture sunlight to make
their food. Chlorophyll absorbs red and blue light, but reflects
green light, which is why we see plants as green.
It's pretty cool how science explains these things we take for granted,
don't you think? Next time you're outside, take a moment to appreciate
the color palette around you!"

Youtube 视频摘要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import openai

#从文件中读取文字记录with open("transcript.txt", "r") as f:
transcript = f.read()

#调用ChatCompletion端点,并使用gpt-3.5-turbo模型
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Summarize the following text"},
{"role": "assistant", "content": "Yes."},
{"role": "user", "content": transcript},
],
)

print(response["choices"][0]["message"]["content"])

如果视频脚本很长,超过了 API 字数上限的话,那么可以考虑将脚本分成多个段落,每个段落生成一份小摘要,最后再汇总小摘要,生成一份大摘要;

打造游戏专家

目标:构建一个 AI 助手,它能够根据任天堂的操作指南,回答用户关于《塞尔达传说》的相关问题;

有两种常见的微调模型的方法

  • 基于特定的数据集,对模型进行二次训练;
  • 小样本学习;

还有第三种方法:

即将整个过程拆分三个步骤:

  • 先让 GPT 解析问题的意图(将用户的问题作为提示关键词,让转成嵌入);
  • 基于解析后的意图,从数据库中搜索匹配的数据(数据需要提前转成嵌入);
  • 将数据提交给 GPT,让其整理后返回最终答案;

以上方法规避了每次将整本指南发送给 GPT 的问题。虽然效果不一定很完美,但大部分情况都OK;

语音控制

控制流程示例如下:

可使用语音转文本工具,例如 Whisper,先将语音转成文本;之后是一个状态机,让 GPT 判断用户的意图,然后根据不同的意图,调用不同的接口,来实现意图;当然,有可能用户的意图不明确,此时可让用户提供更多的信息;有点像是一个状态机;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 定义discussion函数,它让系统能够在各个状态之间切换:
def discussion(messages, last_step):
#调用OpenAI API以获取下一个状态
answer = generate_answer(messages)
if answer in prompts.keys():
#找到一个新状态,将其添加到消息列表中
messages.append({"role": "assistant", "content": answer})
messages.append({"role": "user", "content": prompts[answer]})
#递归式遍历状态机return discussion(messages, answer)
elif answer in actions.keys():
#新状态是一个动作
do_action(answer)
else:
#我们处于END状态
#如果上一步是MORE,那么保留消息的历史记录
#否则重新开始if last_step != 'MORE':
messages=[]
last_step = 'END'
return answer

第4章 高级技巧

提示工程

提示词的数量会影响 OpenAI 的使用成本,稳妥起见,最好设置 max_tokens 参数,以便意义发送大量数据,导致费用大幅超支;

设计有效的提示词

提示词的三个要素:

  • 角色
  • 上下文
  • 任务

如何描述上下文,以便 GPT 更好的理解我们的意图,有时候并不容易,因为我们本身对问题也不是非常了解,所以无法提供更具体的信息。遇到这种情况,我们可以询问 GPT 它想知道哪些上下文,让它提问题,我们来回答。

任务:用来告诉 GPT 需要做什么,因此应该尽量描述得具体和明确一些;

角色:用来告诉 GPT 要以什么样的身份给出回答。相同的问题,由不同的角色回答,其答案会有所不同;角色并不是必须的,但使用角色可以让结果更加符合我们的预期;

prompt 提示词的作用有点像是函数参数,用来调整 GPT 内部的概率分布,引导模型生成我们想要的结果;上下文+角色+任务只是一种提示词的常用结构,但并不是唯一的结构,完成可以根据任务需要,设计其他更合理的结构;

逐步思考

有时候有些问题可能比较复杂,难以在下一句对话中得出结果。此时可以考虑在提示词中添加 ”Let’s think step by step“(让我们逐步思考),来引导 GPT 模拟中间推理,将一个大问题,拆分成多个小问题进行处理;

以下是模型的输出:

实现少样本学习

少样本学习:在提示词中,给 GPT 提供示例,让其模仿;

以下示例的目标,是让 GPT 将部分文本转成表情;

对于简一些的任务,甚至可以使用单样本,这样有助于减少费用;

提示词工程并没有标准答案,它更像是医学,在反复试错中,找到更好的方案和技巧;

改善提示效果

一些提示词的技巧:

指示模型提出更多的问题

在提示词的结尾,询问模型是否理解问题,并指示模型提出更多的问题,示例如下:

你清楚地理解我的请求了吗?如果没有,请问我关于上下文的问题。这样一来,当我回答时,你就能够更高效地执行我所请求的任务

格式化输出

明确指示结果应该被某种格式化工具所接受

重复指示

在提示词中多次添加相同意图的指令,但指令的表达方式有所不同

使用负面提示

在提示词中举反例,告诉模型自己不想要什么样的结果。这些负面提示在本质上有点像是结果筛选器;

添加长度限制

在提示词明确提出答案应该满足的长度要求,示例如下:

如果你能回答,说 YES;如果你需要更多信息,说 MORE;如果你不能回答,说 NO;回答的长度只限一个单词

微调模型

想要获得预期结果,除了使用提示词技巧外,还有一种策略是微调模型,以便提高模型处理特定任务时的表现;

开始微调

目标:实现一个电子邮件自动回复,邮件内容保持跟之前相同的风格,即使用行业专用语汇

微调的本质是修改模型中原有参数的权重,得到一个新的模型,以更好的适应特定任务;

注:OpenAI 上面有多个基础模型,参数数量不同;其中能够进行微调的模型数量是有限的;

问:什么是 InstructGPT 系列?

答:对原始模型(GPT-3)的输出,使用人工反馈进行强化学习(RLHF),微调后的新模型;

微调步骤:

  • 选择一个大小合适的基础模型;
  • 准备好足够的数据集,重新训练模型;

对于简单的任务,通常使用少样本学习就够用了。对于特定领域的复杂任务,则需要使用微调模型的方法。但使用这个方法有一个局限性,则受限于可使用的训练数据数量。当任务较为简单,可能几百个训练数据就够了。但如果任务特别复杂,那可能将涉及以万或者百万为单位的数据集。数据集来源不足可能会是一个问题,另外训练成本也将急剧上升;

使用 OpenAI API 进行模型微调

准备数据

数据集是一个 JSONL 文件,相当于多行的 JSON 组成的文件,示例如下:

1
2
3
{"prompt": "<prompt text>", "completion": "<completion text>"}
{"prompt": "<prompt text>", "completion": "<completion text>"}
{"prompt": "<prompt text>", "completion": "<completion text>"}

OpenAI 有自带一个格式转换的命令行工具,可用来协助生成数据集,

1
openai tools fine_tunes.prepare_data -f <LOCAL_FILE>

上传数据

1
2
3
4
openai.File.create(
file=open("out_openai_completion_prepared.jsonl", "rb"),
purpose='fine-tune' # 此参数表示要微调模型
)

调用 OpenAI 内置函数上传数据,该函数会返回一个对象;对象中有文件 ID,该 ID 可作为创建模型的参数,以便创建模型时,可引用该份已上传的数据;

创建模型

上传完训练数据后,OpenAI 服务端会开启一个训练作业。可调用函数查询该作业的排队和训练的进度状态;

微调的应用场景

  • 法律文本分析:因为法律文件一般包含很多法律术语,所以微调模型可以显著提高性能;
  • 自动代码审查:例如 Github 的 Copilot;
  • 财务报表摘要
  • 技术文档翻译:同样是包含诸多专业术语;

根据以上的应用场景可以发现,微调模型特别适用于在专业领域中生成专业的内容,例如某个特定领域的客服聊天机器人;

案例演示:电子邮件营销

目的:使用电子邮件,向目标用户(线下商店)推广在线支付服务(例如微信、支付宝等);

步骤如下:

创建合成的数据集

理论上最理想的办法是使用人工来创建训练数据,但如果需要的数据量很大,会导致成本很高,并不划算。因此我们也可以考虑让 GPT 生成一些数据;

1
2
3
4
5
6
# 区别不同类型、不同城市、不同规模的商店,以便生成有针对性的内容
l_sector = ['Grocery Stores', 'Restaurants', 'Fast Food Restaurants',
'Pharmacies', 'Service Stations (Fuel)', 'Electronics
Stores']
l_city = ['Brussels', 'Paris', 'Berlin']
l_size = ['small', 'medium', 'large']
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 创建提示词,将类型、城市、规模设置为参数
f_prompt = """
Role: You are an expert content writer with extensive direct marketing
experience. You have strong writing skills, creativity, adaptability to
different tones and styles, and a deep understanding of audience needs
and preferences for effective direct campaigns.
Context: You have to write a short message in no more than 2 sentences
for a direct marketing campaign to sell a new e-commerce payment
service to stores.
The target stores have the following three characteristics:
- The sector of activity: {sector}
- The city where the stores are located: {city}
- The size of the stores: {size}
Task: Write a short message for the direct marketing campaign. Use the
skills defined in your role to write this message! It is important that
the message you create takes into account the product you are selling
and the characteristics of the store you are writing to.
"""
1
2
# sub_prompt 将作为键,合成的数据作为值,组成训练数据集
f_sub_prompt = "{sector}, {city}, {size}"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 合成数据
df = pd.DataFrame()
for sector in l_sector:
for city in l_city:
for size in l_size:
for i in range(3): # 每个重复3次
prompt = f_prompt.format(sector=sector, city=city, size=size)
sub_prompt = f_sub_prompt.format(sector=sector, city=city, size=size)
response_txt = chat_completion(
prompt, model="gpt-3.5-turbo",
temperature=1
)
new_row = {"prompt": sub_prompt, "completion": response_txt}
new_row = pd.DataFrame([new_row])
df = pd.concat([df, new_row], axis=0, ignore_index=True)
df.to_csv("out_openai_completion.csv", index=False)
1
2
# 使用命令行格式化训练数据,该工具会提示我们如何处理数据,可根据其提示进行操作,例如补全换行符
openai tools fine_tunes.prepare_data -f out_openai_completion.csv

使用合成的数据训练模型

1
2
3
4
5
6
7
8
9
10
# 上传数据
ft_file = openai.File.create(
file=open("out_openai_completion_prepared.jsonl", "rb"),
purpose="fine-tune"
)

# 训练模型
openai.FineTune.create(
training_file=ft_file["id"], model="davinci", suffix="direct_marketing"
)

调用函数创建训练模型的作业后,会得到一个 fine_tune_id,该 id 可用来查询进度,也可用于取消或者删除作业;

使用微调后的模型进行文本补全

由于我们之前使用商店关键字 + 广告组成的训练集进行训练,因此,对于微调后的模型,我们现在可以直接使用关键字来获取想要的结果了,例如只需输入提示词:Hotel、New York、Small,模型便会自动生成相应的广告邮件内容了;

1
2
3
4
5
6
7
openai.Completion.create(
model="davinci:ft-book:direct-marketing-2023-05-01-15-20-35", # 调用微调后的模型
prompt="Hotel, New York, small ->", # 使用特定格式的提示词,与训练数据中的格式一致
max_tokens=100,
temperature=0,
stop="\n"
)

微调的成本

使用微调模型调用 API 单价会更高一些;以下价格仅供参考,因为 OpenAI 会不断调整价格;

小结

第5章 使用 LangChain 构架和插件增加 LLM 功能

LangChain 框架

LangChain 是一个让开发基于大模型的应用程序变得更加方便的开发框架;它有以下几个核心模块:

  • Models:提供通用的接口,让调用多个不同厂商的大模型变得更方便;
  • Prompts:创建和管理提示词
  • Indexes:对接本地数据,目前已改名为 Retrieval
  • Chain:用于创建调用序列,以方便组合多个模型或提示词;
  • Agents:用于处理用户的输入,做出判断,并调用合适的工具完成任务;
  • Memory:用于存储中间状态,保持上下文;

动态提示词

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 创建一个可动态提问的程序
from langchain.chat_models import ChatOpenAI
from langchain import PromptTemplate, LLMChain

template = """Question: {question}
Let's think step by step.
Answer: """
prompt = PromptTemplate(
template=template,
input_variables=["question"]
)
llm = ChatOpenAI(model_name="gpt-4")
llm_chain = LLMChain(prompt=prompt, llm=llm) # LLMChain 组合了模型和提示词
question = """ What is the population of the capital of the country
where the Olympic Games were held in 2016? """
llm_chain.run(question)
1
2
3
4
5
6
7
8
9
10
# 得到的输出如下
Step 1: Identify the country where the Olympic Games were held in 2016.
Answer: The 2016 Olympic Games were held in Brazil.
Step 2: Identify the capital of Brazil.
Answer: The capital of Brazil is Brasília.
Step 3: Find the population of Brasília.
Answer: As of 2021, the estimated population of Brasília is around
3.1 million. So, the population of the capital of the country where
the Olympic Games were held in 2016 is around 3.1 million. Note that
this is an estimate and may vary slightly.'

智能体及工具

智能体及工具是 LangChain 框架的核心功能,它将一些常用的功能,抽象并集成成更容易调用的方式;

逐步思考是一个很强大的范式,它能够利用模型来解决一些复杂的问题;原理也不复杂,相当于将一个复杂的问题,拆解成多个简单的小问题;智能体的一个功能就是内隐了这种范式,用户没有感知;

步骤如下:

  1. 智能体收到用户的输入;
  2. 智能体分析输入,决定要使用的工具,以及要输入给工具的文本;
  3. 智能体调用工具,接收工具输出的文本;
  4. 智能体将收到的文本输入,作为输入,输入到智能体的上下文中
  5. 重复步骤 2 到步骤 4,直到智能体不再需要调用工具后,将结果输出给用户;

1
2
3
4
5
6
7
8
9
10
11
12
from langchain.chat_models import ChatOpenAI
from langchain.agents import load_tools, initialize_agent, AgentType

llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)
tools = load_tools(["wikipedia", "llm-math"], llm=llm)
agent = initialize_agent(
tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True
)
question = """What is the square root of the population of the capital
of the Country where the Olympic Games were held in 2016?"""
agent.run(question)
1
2
3
4
5
6
7
8
9
10
# 计算过程如下

> Entering new chain...
I need to find the country where the Olympic Games were held in 2016
and then find the population of its capital city. Then I can take the
square root of that population.
Action: Wikipedia
Action Input: "2016 Summer Olympics"
Observation: Page: 2016 Summer Olympics
[...]
1
2
3
4
5
6
7
8
9
10
11
12
Thought:I need to search for the capital city of Brazil.
Action: Wikipedia
Action Input: "Capital of Brazil"
Observation: Page: Capitals of Brazil
Summary: The current capital of Brazil, since its construction in
1960, is Brasilia. [...]
Thought: I have found the capital city of Brazil, which is Brasilia.
Now I need to find the population of Brasilia.
Action: Wikipedia
Action Input: "Population of Brasilia"
Observation: Page: Brasilia
[...]
1
2
3
4
5
6
Thought: I have found the population of Brasilia, but I need to
calculate the square root of that population.
Action: Calculator # 此处调用了计算器
Action Input: Square root of the population of Brasilia (population:
found in previous observation)
Observation: Answer: 1587.051038876822
1
2
3
4
5
# 得出最终的答案
Thought: I now know the final answer
Final Answer: The square root of the population of the capital of the
country where the Olympic Games were held in 2016 is approximately 1587.
> Finished chain.

记忆

记忆之前的状态,相当于记住上下文,这在某些场景中是必须和有用的,例如构建聊天机器人

1
2
3
4
5
6
# 有专门的 ConversationChain
from langchain import OpenAI, ConversationChain

chatbot_llm = OpenAI(model_name='text-ada-001')
chatbot = ConversationChain(llm=chatbot_llm , verbose=True)
chatbot.predict(input='Hello')
1
2
3
4
5
6
7
8
9
10
11
12
# 输出结果如下
> Entering new ConversationChain chain...
Prompt after formatting: # 虽然用户只输入了 Hello,但实际上 LangChain 提交给模型的提示词要多很多
The following is a friendly conversation between a human and an
AI. The AI is talkative and provides lots of specific details from
its context. If the AI does not know the answer to a question, it
truthfully says it does not know.
Current conversation:
Human: Hello
AI:
> Finished chain.
' Hello! How can I help you?'
1
2
3
4
5
6
7
8
9
10
11
# 在后续的对话中,LangChain 会将聊天记录提交给模型
> Entering new ConversationChain chain...
Prompt after formatting:
The following [...] does not know.
Current conversation:
Human: Hello
AI: Hello! How can I help you?
Human: Can I ask you a question? Are you an AI?
AI:
> Finished chain.
'\n\nYes, I am an AI.'

有些模型如 GPT-4 有专门针对聊天场景的微调模型,有时候使用这些微调模型效果更好,不一定非要用 LangChain 的接口;

嵌入

嵌入用于将模型与本地的数据相结合,这样模型的回答将更有针对性,例如之前讨论过的游戏说明书的案例;

1
2
3
from langchain.document_loaders import PyPDFLoader
loader = PyPDFLoader("ExplorersGuide.pdf")
pages = loader.load_and_split() # 加载 PDF 文件,并将其分页
1
2
from langchain.embeddings import OpenAIEmbeddings
embeddings = OpenAIEmbeddings() # 用于转分页的 PDF 内容转换成向量,并保存到数据库中

1
2
3
# 之后当用户输入问题时,就到数据库中查询相近的嵌入
q = "What is Link's traditional outfit color?"
db.similarity_search(q)[0]
1
2
3
4
5
# 查询结果如下,从数据库中查询到与问题最相关的页面
Document(page_content='''While Link’s traditional green tunic is certainly an iconic look,
his wardrobe has expanded [...] Dress for Success''',
metadata={'source': 'ExplorersGuide.pdf', 'page': 35}
)

1
2
3
4
5
6
7
8
9
10
# 将嵌入整合到聊天机器人中,让聊天机器人可以回答用户的提问

from langchain.chains import RetrievalQA
from langchain import OpenAI

llm = OpenAI()
# 注意此处的 retriever 用到了向量数据库,它将提取最相近的嵌入,然后做为上下文和问题一起发给模型
chain = RetrievalQA.from_llm(llm=llm, retriever=db.as_retriever())
q = "What is Link's traditional outfit color?"
chain(q, return_only_outputs=True)

GPT-4 插件

插件主要用于实时获取一些必要数据,它的功能不需要模型实现,而是由开发者实现好的,供模型进行调用。调用这些插件后,它会返回必要的数据,做为上下文参数发给模型,这样模型便能够处理更加复杂的任务,例如查询实时的比赛分数、股票价格、机票价格、预定机票等;

插件 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 以下代码定义了一个 Todo 待办事项程序的 web 接口

import json
import quart # quart 是一个 web 框架,类似 flask 那种类型
import quart_cors
from quart import request

app = quart_cors.cors(
quart.Quart(__name__),
allow_origin="https://chat.openai.com"
)

_TODOS = {} # 跟踪待办事项,如果Python会话重新启动,则数据会丢失

@app.post("/todos/<string:username>")
async def add_todo(username):
request = await quart.request.get_json(force=True)
if username not in _TODOS:
_TODOS[username] = []
_TODOS[username].append(request["todo"])
return quart.Response(response="OK", status=200)

@app.get("/todos/<string:username>")
async def get_todos(username):
return quart.Response(
response=json.dumps(_TODOS.get(username, [])), status=200
)

@app.get("/.well-known/ai-plugin.json") # 该接口用于提供插件信息,以便模型安装该插件
async def plugin_manifest():
host = request.headers["Host"]
with open("./.well-known/ai-plugin.json") as f:
text = f.read()
return quart.Response(text, mimetype="text/json")

@app.get("/openapi.yaml") # 该接口用于提供 OpenAPI 的调用规范,方便模型了解如何使用插件
async def openapi_spec():
host = request.headers["Host"]
with open("openapi.yaml") as f:
text = f.read()
return quart.Response(text, mimetype="text/yaml")

def main():
app.run(debug=True, host="0.0.0.0", port=5003)

if __name__ == "__main__":
main()

插件信息

以下是 ai-plugin.json 文件示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"schema_version": "v1",
"name_for_human": "TODO Plugin",
"name_for_model": "todo",
"description_for_human": "Plugin for managing a TODO list. \
You can add, remove and view your TODOs.",
"description_for_model": "Plugin for managing a TODO list. \
You can add, remove and view your TODOs.",
"auth": {
"type": "none"
},
"api": {
"type": "openapi",
"url": "http://localhost:3333/openapi.yaml",
"is_user_authenticated": false
},
"logo_url": "http://localhost:3333/logo.png",
"contact_email": "support@thecompany.com",
"legal_info_url": "http://thecompany-url/legal"
}

OpenAPI 规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 以下是 openapi.yaml 示例,详细描述接口的调用方法
openapi: 3.0.1
info:
title: TODO Plugin
description: A plugin that allows the user to create and manage a
TODO list using ChatGPT. If you do not know the user's username,
ask them first before making queries to the plugin. Otherwise,
use the username "global".
version: 'v1'
servers:
- url: http://localhost:5003
paths:
/todos/{username}:
get:
operationId: getTodos
summary: Get the list of todos
parameters:
- in: path
name: username
schema:
type: string
required: true
description: The name of the user.
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/getTodosResponse'

插件描述

openapi.yaml 中应该尽量描述接口的用途,这样有助于模型判断何时可调用接口获得数据,以及如何调用接口;这其中最重要的字段便是 description_for_model,这个字段是对于插件的功能描述;应该尽量写得清晰易懂,一目了然;

功能描述应该是声明式的,而不是命令式的;


大模型应用开发极简入门
https://ccw1078.github.io/2024/08/22/大模型应用开发极简入门/
作者
ccw
发布于
2024年8月22日
许可协议