LLM是不存在真正逻辑的且并不是知晓万事万物的(至少目前是这样)在很多更垂直的环境下LLM并不能很好的赋能。
function calling的实现使LLM可以对接真正的世界以及真正有逻辑的系统,这将很大程度上改变LLM的可用范围(当然安全问题依旧是不容忽视的事)。
环境简述
model: gpt-3.5-turbo
api: 理杏仁开放平台
tools
首先我们需要分析需求涉及到什么逻辑功能(函数),并且将该功能的用途,参数及参数的用途进行格式化整理(json/yaml格式)。
针对理杏仁的使用,以下为一个简单的需求:
- 可以获取到某天某指数的PE-TTM的当前分位点
针对上述需求可以拆分出如下几点:
- 针对需求有两个api是我们将会调用的:基础信息(获取所有指数),基本面数据(获取日期的指数数据)
- 我们需要保证user指定的指数是存在的,基础信息api,从中拿到指数代码,当无法找到时结束当前对话【get_code】
- 我们需要指定日期,user可能会使用类似今天昨天等词汇,我们需要自构建函数可以处理日期【date】
- 当我们有了指数代码和日期后便可以调用基本面数据api获得对应的数据【get_cvpos】
step1: 构建函数
针对上述分析,我们需要三个函数,其中两个函数主要工作是调用api(get_code
和 get_cvpos
)另外一个是获取当前日期的函数(date
)
具体函数内容如下:
def get_code(self, name: str):
print(f"name: {name}")
response = requests.post(f"{self.url}", json=self.base_json).json()
for item in response["data"]:
if item["name"] == name:
return {"stockcode": item["stockCode"]}
return {"stockcode": f"未找到{name}, 请检查输入!"}
def get_cvpos(self, stockcode: str, date: str):
print(f"stockcode: {stockcode}, date: {date}")
data = self.base_json
data.update(
{
"date": date,
"stockCodes": [stockcode],
"metricsList": ["pe_ttm.y5.mcw.cvpos"]
}
)
response = requests.post(url=f"{self.url}/fundamental", json=data)
response = response.json()
if not response["data"]:
return {"cvpos": f"所指定日期暂无数据,请确定{date}是否为交易日"}
return {"cvpos": response["data"][0]["pe_ttm.y5.mcw.cvpos"]}
def date(self, delta: str = 0):
print(f"delta: {delta}")
day = datetime.date.today() + datetime.timedelta(int(delta))
return {"date": day}
有了函数之后我们需要通过用户的输入来判断我们什么时间需要调用哪一个函数,且需要判断写入什么样的参数。从日常语言转换为结构化数据是一个复杂的过程,当然我们可以通过各种方式约束用户的描述(例如表单等)但随着需要的参数越来越多逻辑就可能会越来越复杂。
step2: 传递tools
function calling就可以很好的实现自然语言到结构化数据的转变,我们只需要将每个函数的作用及调用规则告诉模型即可,模型会根据user的问题返回需要调用的函数及作为参数的结构化数据。
针对上述需求我们构建出如下 TOOLS
来描述函数:
TOOLS = [
{
"type": "function",
"function": {
"name": "get_code",
"description": "获取某个指数的代码",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "指数名称, 例如: 沪深300, 上证50",
}
},
"required": ["name"],
}
}
},
{
"type": "function",
"function": {
"name": "date",
"description": "获取前天/昨天/今天的日期, 如果直接指定了日期则无需调用该function",
"parameters": {
"type": "object",
"properties": {
"delta": {
"type": "string",
"description": "今天为0, 昨天为-1, 前天为-2"
}
}
}
}
},
{
"type": "function",
"function": {
"name": "get_cvpos",
"description": "获取某天给定代码的指数指标",
"parameters": {
"type": "object",
"properties": {
"stockcode": {
"type": "string",
"description": "所给定的指数代码"
},
"date": {
"type": "string",
"description": "日期, 格式为2024-06-25"
}
},
"required": ["code", "date"],
}
}
}
]
TOOLS的内容最终也会作为prompt的一部分交给大模型,因此使用时也需要考虑token消耗带来的费用。
我们只需要将TOOLS通过tools参数传递给模型即可。
response = client.chat.completions.create(
model=model,
messages=messages,
temperature=0,
seed=1024,
tool_choice="auto",
tools = TOOLS
)
step3: 模型响应内容及函数调用
user提问:可以告诉我沪深300的代码吗?
正常情况下模型将会返回如下结果:
{
"content": null,
"role": "assistant",
"function_call": null,
"tool_calls": [
{
"id": "call_WeIPPDsWwzOLH90IV0dSEp1q",
"function": {
"arguments": "{\"name\":\"沪深300\"}",
"name": "get_code"
},
"type": "function"
}
]
}
在返回的结果中我们可以清楚的知道对于user的提问需要要调用get_code函数且入参为{“name”: “沪深300”},此时我们便可以轻松使用 getattr() 的方式来调用函数。
messages完整流程
user的一次对话可能会不止一次调用模型,因此需要使用列表存储过程中产生的所有message来保证对话的连续。如下:
messages = [
{"role": "system", "content": "你是一个指数查询器(此处的指数是指金融上的指数)"},
{"role": "user", "content": "xxxx"}
]
response = self.get_completion(messages)
messages.append(response)
while (response.tool_calls is not None):
for tool_call in response.tool_calls:
print(f"=== start function {tool_call.function.name} ===")
args = json.loads(tool_call.function.arguments)
result = getattr(self.tools, tool_call.function.name)(**args)
messages.append({
"tool_call_id": tool_call.id, # 用于标识函数调用的 ID
"role": "tool",
"name": tool_call.function.name,
"content": str(result) # 数值result 必须转成字符串
})
response = self.get_completion(messages)
messages.append(response)
print("===== 最终回复 =====")
print(response.content)
结果展示
user提供的prompt为:请告诉我昨天沪深300的数据
注:该文章完成时间为2024年7月16日,因此昨天应为2024-07-15
1. 最终回复
昨天(2024年7月15日)沪深300指数的收盘点位为0.4026。
2. 完整打印
{
"role": "system",
"content": "你是一个指数查询器(此处的指数是指金融上的指数)"
}
{
"role": "user",
"content": "请告诉我昨天沪深300的数据"
}
{
"content": null,
"role": "assistant",
"function_call": null,
"tool_calls": [
{
"id": "call_fLWvI0BVRsuzUEj4TTwvAnke",
"function": {
"arguments": "{\"delta\":\"-1\"}",
"name": "date"
},
"type": "function"
}
]
}
{
"tool_call_id": "call_fLWvI0BVRsuzUEj4TTwvAnke",
"role": "tool",
"name": "date",
"content": "{'date': datetime.date(2024, 7, 15)}"
}
{
"content": null,
"role": "assistant",
"function_call": null,
"tool_calls": [
{
"id": "call_eDB0nZu5V5xLAZWs3osXdAXS",
"function": {
"arguments": "{\"name\":\"沪深300\"}",
"name": "get_code"
},
"type": "function"
}
]
}
{
"tool_call_id": "call_eDB0nZu5V5xLAZWs3osXdAXS",
"role": "tool",
"name": "get_code",
"content": "{'stockcode': '000300'}"
}
{
"content": null,
"role": "assistant",
"function_call": null,
"tool_calls": [
{
"id": "call_XtS9TkIngpdCVlRLNx5kYmeh",
"function": {
"arguments": "{\"stockcode\":\"000300\",\"date\":\"2024-07-15\"}",
"name": "get_cvpos"
},
"type": "function"
}
]
}
{
"tool_call_id": "call_XtS9TkIngpdCVlRLNx5kYmeh",
"role": "tool",
"name": "get_cvpos",
"content": "{'cvpos': 0.40264026402640263}"
}
{
"content": "昨天(2024年7月15日)沪深300的收盘指数为0.4026。",
"role": "assistant",
"function_call": null,
"tool_calls": null
}