1. 大模型与 Langchain
很多人可能没有机会训练、甚至微调大模型,但对大模型的使用却是未来趋势。那么,我们应该如何拥抱这一变化呢?答案就是 Langchain。
大模型提供的是一种泛而通用的基础能力,目前,我看到的有两种主要落地方式:
目前,在我们的工作场景中,大模型常常还不足以大量直接替代人的工作。原因有两个:
大模型的落地是必然,现在大模型是我们的 Copilot,以后我们可能就是大模型的 Copilot。结合大模型、贴合业务场景,开发出一些 Copilot 工具、提升效率,是我最近在思考的问题。
开发应用少不了各种框架。Langchain 的定位就是提供开发大模型应用的框架,以解决大模型落地过程中的一些通用问题。比如,对接多种大模型,Prompt 管理,上下文,外部文档加载,向量库对接,Chains 任务链等。
Langchain 已经由之前的个人项目转为商业公司运作,2023 年还进行了多轮融资。从中可以看到,创投行业对基于大模型的应用开发是非常看好的。既然投资人已经帮我们做出了判断,我们只需要多学习和使用 Langchain 即可。
2. 对话直接调用函数
在 2023 年 6 月份,OpenAI 和 Langchain 相继发布了版本,支持直接调用函数。这意味着,大模型不仅仅可以用来聊天,还可以用来触发一些业务逻辑。
2.1 先看看效果
1
2
3
4
| manual_input:获取 default 这个命名空间的全部 pod
function_bot: ["pod1", "pod2", "pod3"]
manual_input:获取 c1 这个集群的全部节点
function_bot: ["node1", "node2", "node3"]
|
输入自然语言,自动执行函数,并返回结果。这里举了两个例子,一个是获取 default 命名空间下全部 Pod,一个是获取 c1 集群下全部节点。
2.2 代码实现
1
2
| export OPENAI_API_BASE="https://api.openai.com/v1"
export OPENAI_API_KEY="xxx"
|
在使用 OpenAI API 时,会自动读取环境变量中设置的 API KEY。
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
| # -*- coding: utf-8 -*-
import json
from typing import Type
from pydantic import BaseModel, Field, create_model
from typing import Optional
from langchain.tools import BaseTool
from langchain.callbacks.manager import (
AsyncCallbackManagerForToolRun,
CallbackManagerForToolRun,
)
from langchain.tools import format_tool_to_openai_function
import openai
class GetClusterNodes(BaseTool):
name: str = "get_cluster_nodes"
description: str = "get all nodes in kubernetes cluster"
args_schema: Type[BaseModel] = create_model(
"GetClusterNodesArgs",
cluster=(str, Field(
description="the cluster of you want to query", type="string")),
)
def _run(
self, query: str,
run_manager: Optional[CallbackManagerForToolRun] = None
) -> str:
return json.dumps(["node1", "node2", "node3"])
async def _arun(
self, query: str,
run_manager: Optional[AsyncCallbackManagerForToolRun] = None
) -> str:
return json.dumps(["node1", "node2", "node3"])
class GetClusterPodsByNamespaces(BaseTool):
name: str = "get_cluster_pods_by_namespace"
description: str = "get special pods in kubernetes special namespace"
args_schema: Type[BaseModel] = create_model(
"GetClusterPodsByNamespacesArgs",
namespace=(str, Field(
description="the namespace of you want to query", type="string")),
)
def _run(
self, query: str,
run_manager: Optional[CallbackManagerForToolRun] = None
) -> str:
return json.dumps(["pod1", "pod2", "pod3"])
async def _arun(
self, query: str,
run_manager: Optional[AsyncCallbackManagerForToolRun] = None
) -> str:
return json.dumps(["pod1", "pod2", "pod3"])
functions_list: list = [GetClusterNodes, GetClusterPodsByNamespaces]
functions_map: dict = {fun().name: fun for fun in functions_list}
def run(msg: str):
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": msg}],
functions=[
format_tool_to_openai_function(t()) for t in functions_list],
function_call="auto",
)
message = response["choices"][0]["message"]
if message.get("function_call"):
function_name = message["function_call"]["name"]
function_response = functions_map[function_name]().run(
message["function_call"]["arguments"])
return function_response
if __name__ == "__main__":
while True:
user_input = input("manual_input:")
if user_input == "exit":
break
print("function_bot:", run(user_input))
|
这里为了简化实现,_run
都直接进行了返回,没有实际调用函数。在实际生产中,我们需要去根据输入的 query,调用函数,返回结果。
2.3 逐步解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| def run(msg: str):
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": msg}],
functions=[
format_tool_to_openai_function(t()) for t in functions_list],
function_call="auto",
)
message = response["choices"][0]["message"]
if message.get("function_call"):
function_name = message["function_call"]["name"]
function_response = functions_map[function_name]().run(
message["function_call"]["arguments"])
return function_response
|
在 openai.ChatCompletion.create
设置两个参数:
function_call
设置为 auto,默认即 auto,由模型自己决定是否调用函数。这里并不是真的调用,而是返回一些函数元信息。
functions
是一个列表对象,OpenAI 根据传入的函数描述加用户输入的内容 msg 做出判断,返回函数的名字、获取到的参数。
有两种写法的定义: 一种是直接使用列表对象拼接,一种是继承 BaseTool 实现。
下面是列表对象拼接:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| [
{
"name": "get_cluster_nodes",
"description": "get all nodes in kubernetes cluster",
},
{
"name": "get_cluster_pods_by_namespace",
"description": "get special pods in kubernetes special namespace",
"parameters": {
"type": "object",
"properties": {
"namespace": {
"type": "string",
"description": "filter pods in namespace",
}
},
"required": ["namespace"],
},
}
]
|
上面的完整示例代码中使用的就是继承 BaseTool 实现:
1
2
| class GetClusterNodes(BaseTool):
class GetClusterPodsByNamespaces(BaseTool):
|
继承 BaseTool 的方式其实最终还是需要使用 format_tool_to_openai_function
提取自定义函数中的信息生成一个列表,但使用 BaseTool 管理函数方法是一个更加清晰的方式。
如果不详细描述参数,那么 OpenAI 识别到的参数格式很有可能是这样的:
1
2
3
4
| "function_call": {
"name": "get_cluster_nodes",
"arguments": "{\n\"__arg1\": \"c1\"\n}"
}
|
而如果设置了 properties
或者 args_schema
之后,OpenAI 返回的函数参数就非常符合预期了。
1
2
3
4
| "function_call": {
"name": "get_cluster_nodes",
"arguments": "{\n\"cluster\": \"c1\"\n}"
}
|
如果使用直接拼接列表的形式,那么直接写在函数即可。如果继承 BaseTool,那么就需要实现其同步调用 _run
函数, 异步调用 _arun
函数。
上面的完整示例代码中:
1
2
| function_response = functions_map[function_name]().run(
message["function_call"]["arguments"])
|
直接将返回的参数,传给被调用的函数,这里调用的就是 _run
函数。在 _run
函数中,通过 json.loads(query)
可以获取到符合 args_schema
定义的参数。
1
2
3
4
5
6
7
8
9
10
11
12
| def format(msg: str, function_response: str):
return openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "user", "content": msg},
{
"role": "function",
"name": "get_cluster_nodes",
"content": function_response,
},
],
)["choices"][0]["message"]["content"]
|
代码如上,在获取到函数的响应之后,如果还需要对响应的格式、内容进行二次整理,可以设置一个 function
角色的消息,附加上函数的响应,加上用户的输入,一起发送给 OpenAI。此时,OpenAI 会给出一个更加完整的响应。
但这步不是必须,如果函数的响应已经符合预期,那么可以直接返回。
3. 总结
本篇主要是借助 OpenAI 和 Langchain 实现了一个直接使用自然语言调用函数的示例。
大模型不仅仅可以用来聊天,还可以用来触发一些业务逻辑。在我们开发 Copilot 时,经常需要这种胶水功能,粘合大模型和业务逻辑。
大模型生态的建设有两部分,一个是认知,一个是执行。认知依赖于大模型的参数规模、网络结构、训练数据;执行主要依赖于外部连接的情况。
我认为,即使参与不了大模型的训练,也可以尝试着整理一下行业知识库,还有机会参与到执行部分。围绕执行我们可以将产品的 API 开放出来,增加大模型连接系统的触点;还可以开发一些 SDK、工具包,帮助开发者快速接入大模型,比如整理一个 BaseTool 类库,封装各种 API 、脚本功能;当然,还可以根据大模型的思考方式,重新设计业务流程、执行逻辑。