AI大模型全栈工程师课程笔记 - Function Call

2023-11-23 10:44:36 浏览数 (1)

课程学习自 知乎知学堂 https://www.zhihu.com/education/learning

如果侵权,请联系删除,感谢!

相关参考: https://platform.openai.com/docs/guides/function-calling

1. 课程摘要

大模型,没有真逻辑,只是推测下一个字的概率 gpt 可以根据函数的描述来决定是否调用该函数,但是真正的执行,需要我们自行调用

2. 调用本地函数

2.1 help 函数
代码语言:javascript复制
from openai import OpenAI  # openai 1.3.3
import os

from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv())


client = OpenAI(
    api_key=os.getenv("OPENAI_API_KEY"),
    base_url=os.getenv("OPENAI_API_BASE")
)
def get_completion_with_tools(messages, tools=[], model="gpt-3.5-turbo-1106"):
    response = client.chat.completions.create(  # 注意,以前的 client.ChatCompletion 要换成 client.chat.completions
        model=model,
        messages=messages,
        temperature=0,  # 模型输出的随机性,0 表示随机性最小
        tools=tools,  # 用 JSON 描述函数。可以定义多个。由大模型决定调用谁,
        seed=1024,
        tool_choice='auto',
        # response_format={'type': 'json_object'}  # 如果加了这个,请在prompt中显式指出使用json格式输出
    )
    return response.choices[0].message

def get_completion_with_messages(messages,model="gpt-3.5-turbo-1106"):
    response = client.chat.completions.create(  
        model=model,
        messages=messages,
        temperature=0,  # 模型输出的随机性,0 表示随机性最小
    )
    return response.choices[0].message
2.2 求和

调用python内置 sum 函数

代码语言:javascript复制
# _*_ coding: utf-8 _*_
# @Time : 2023/11/20 0:01
# @Author : Michael
# @File : 03-function-call.py
# @desc :
import json

from agi_class.utils.chat import client, get_completion_with_messages, get_completion_with_tools  # openai 1.3.3  python 3.11.5

tools = [
    {  # 用 JSON 描述函数。可以定义多个。由大模型决定调用谁
        "type": "function",
        "function": {
            "name": "sum",
            "description": "计算一组数的加和",
            "parameters": {
                "type": "object",
                "properties": {
                    "numbers": {
                        "type": "array",
                        "items": {
                            "type": "number"
                        }
                    }
                }
            }
        }
    }
]

prompt = "求一下这些数字的和 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1000"

messages = [
    {"role": "system", "content": "你是一个高级数学老师,会用幽默的方法讲述数学题目"},
    {"role": "user", "content": prompt}
]
response = get_completion_with_tools(messages, tools)
print(response)
# ChatCompletionMessage(content=None, role='assistant', function_call=None,
# tool_calls=[ChatCompletionMessageToolCall(id='call_lxJk0LzId1FBKwdf715LAwkN', function=Function(arguments='{"numbers": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}', name='sum'), type='function'),
#             ChatCompletionMessageToolCall(id='call_8978FU0b9MukKsjop8QNkUzi', function=Function(arguments='{"numbers": [1000]}', name='sum'), type='function')])
messages.append(response)

# 如果返回的是函数调用结果
if response.tool_calls is not None:
    available_functions = {
        "sum": sum,
    }
    for tool_call in response.tool_calls:
        function_name = tool_call.function.name
        function_to_call = available_functions[function_name]
        function_args = json.loads(tool_call.function.arguments)
        function_response = function_to_call(
            function_args.get("numbers"),
        )
        messages.append(
            {
                "tool_call_id": tool_call.id,
                "role": "tool",
                "name": function_name,
                "content": str(function_response),
            }
        )

    # 把函数调用(需要自行调用)的结果,返给gpt,再次调用生成回复
    print(get_completion_with_messages(messages).content)
    # 这些数字的和是55和1000。看起来1000有点孤单,需要更多的朋友来凑热闹!
2.3 eval 求表达式的值
代码语言:javascript复制
from math import sqrt
tools = [{
            "type": "function",
            "function": {
                "name": "calculate",
                "description": "计算一个数学表达式的值",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "expression": {
                            "type": "string",
                            "description": "一个python语法的数学表达式",
                        }
                    }
                }
            }
        }]

prompt = "5的平方根加上1再开根号"

messages = [
    {"role": "system", "content": "你是一个计算器,你可以计算任何数学表达式"},
    {"role": "user", "content": prompt}
]
response = get_completion_with_tools(messages, tools)
print(response)
# ChatCompletionMessage(content=None, role='assistant', function_call=None, 
# tool_calls=[ChatCompletionMessageToolCall(id='call_GslHVfRmFrKgJQtDaofoucNs', function=Function(arguments='{"expression": "sqrt(5)"}', name='calculate'), type='function'), 
# 			  ChatCompletionMessageToolCall(id='call_HYp2QqlZnfpi6d1WIuHABxBO', function=Function(arguments='{"expression": "sqrt(sqrt(5) 1)"}', name='calculate'), type='function')])

messages.append(response)

# 如果返回的是函数调用结果
if response.tool_calls is not None:
    available_functions = {
        "calculate": eval,
    }
    for tool_call in response.tool_calls:
        function_name = tool_call.function.name
        function_to_call = available_functions[function_name]
        function_args = json.loads(tool_call.function.arguments)
        function_response = function_to_call(
            function_args.get("expression"),
        )
        messages.append(
            {
                "tool_call_id": tool_call.id,
                "role": "tool",
                "name": function_name,
                "content": str(function_response),
            }
        )

    # 再次调用大模型
    print("=====最终回复=====")
    print(get_completion_with_messages(messages).content)
	# 5的平方根是约等于2.236,然后再加上1得到3.236,最后开根号得到约等于1.799。
2.4 调用高德地图 api
代码语言:javascript复制
import requests

# 调用地图api
map_api_key=os.getenv("A_MAP_KEY")
def get_location_coordinate(location, city="北京"):
    url = f"https://restapi.amap.com/v5/place/text?key={map_api_key}&keywords={location}&region={city}"
    # print(url)
    r = requests.get(url)
    result = r.json()
    if "pois" in result and result["pois"]:
        return result["pois"][0]
    return None


def search_nearby_pois(longitude, latitude, keyword):
    url = f"https://restapi.amap.com/v5/place/around?key={map_api_key}&keywords={keyword}&location={longitude},{latitude}"
    # print(url)
    r = requests.get(url)
    result = r.json()
    ans = ""
    if "pois" in result and result["pois"]:
        for i in range(min(3, len(result["pois"]))):
            name = result["pois"][i]["name"]
            address = result["pois"][i]["address"]
            distance = result["pois"][i]["distance"]
            ans  = f"{name}n{address}n距离:{distance}米nn"
    return ans
代码语言:javascript复制
tools=[{
            "type": "function",
            "function": {

                "name": "get_location_coordinate",
                "description": "根据POI名称,获得POI的经纬度坐标",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "location": {
                            "type": "string",
                            "description": "POI名称,必须是中文",
                        },
                        "city": {
                            "type": "string",
                            "description": "POI所在的城市名,必须是中文",
                        }
                    },
                    "required": ["location", "city"],
                }
            }
        },
            {
            "type": "function",
            "function": {
                "name": "search_nearby_pois",
                "description": "搜索给定坐标附近的poi",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "longitude": {
                            "type": "string",
                            "description": "中心点的经度",
                        },
                        "latitude": {
                            "type": "string",
                            "description": "中心点的纬度",
                        },
                        "keyword": {
                            "type": "string",
                            "description": "目标poi的关键字",
                        }
                    },
                    "required": ["longitude", "latitude", "keyword"],
                }
            }
        }]

prompt = "北京启明国际大厦附近的医院"
messages = [
    {"role": "system", "content": "你是一个地图通,你可以找到任何地址。"},
    {"role": "user", "content": prompt}
]
response = get_completion_with_tools(messages, tools)

messages.append(response)  # 把大模型的回复加入到对话中
print("=====GPT回复=====")
print(response)

# 如果返回的是函数调用结果,则打印出来
while response.tool_calls is not None:
    # 1106 版新模型支持一次返回多个函数调用请求
    for tool_call in response.tool_calls:
        args = json.loads(tool_call.function.arguments)
        print(f'参数:{args}')

        if tool_call.function.name == "get_location_coordinate":
            print("Call: get_location_coordinate")
            result = get_location_coordinate(**args)
        elif tool_call.function.name == "search_nearby_pois":
            print("Call: search_nearby_pois")
            result = search_nearby_pois(**args)

        print("=====函数返回=====")
        print(result)

        messages.append({
            "tool_call_id": tool_call.id,  # 用于标识函数调用的 ID
            "role": "tool",
            "name": tool_call.function.name,
            "content": str(result)  # 数值result 必须转成字符串
        })

    response = get_completion_with_tools(messages, tools)

    messages.append(response)  # 把大模型的回复加入到对话中

print('====所有 messages=====')
print(messages)
print("=====最终回复=====")
print(response.content)

输出:

代码语言:javascript复制
=====GPT回复=====
ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_rJTZh2IMtp280dITbPocNf3A', function=Function(arguments='{"location":"北京启明国际大厦","city":"北京"}', name='get_location_coordinate'), type='function')])

参数:{'location': '北京启明国际大厦', 'city': '北京'}
Call: get_location_coordinate
=====函数返回=====
{'parent': '', 'address': '望京利泽中园101号', 'distance': '', 'pcode': '110000', 'adcode': '110105', 'pname': '北京市', 'cityname': '北京市', 'type': '商务住宅;楼宇;商务写字楼', 'typecode': '120201', 'adname': '朝阳区', 'citycode': '010', 'name': '启明国际大厦', 'location': '116.475071,40.012434', 'id': 'B000A87P48'}

参数:{'longitude': '116.475071', 'latitude': '40.012434', 'keyword': '医院'}
Call: search_nearby_pois
=====函数返回=====
东湖社区卫生服务中心
望京花园东区204号楼
距离:480米

国宗济世中医院门诊
河荫中路与广顺北大街交叉口东80米
距离:1030米

北京固生堂京广顺中医医院
望京西园二区京电底商综合楼2
距离:1045米

====所有 messages=====
[{'role': 'system', 'content': '你是一个地图通,你可以找到任何地址。'}, 
{'role': 'user', 'content': '北京启明国际大厦附近的医院'}, 
ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_rJTZh2IMtp280dITbPocNf3A', function=Function(arguments='{"location":"北京启明国际大厦","city":"北京"}', name='get_location_coordinate'), type='function')]), {'tool_call_id': 'call_rJTZh2IMtp280dITbPocNf3A', 'role': 'tool', 'name': 'get_location_coordinate', 'content': "{'parent': '', 'address': '望京利泽中园101号', 'distance': '', 'pcode': '110000', 'adcode': '110105', 'pname': '北京市', 'cityname': '北京市', 'type': '商务住宅;楼宇;商务写字楼', 'typecode': '120201', 'adname': '朝阳区', 'citycode': '010', 'name': '启明国际大厦', 'location': '116.475071,40.012434', 'id': 'B000A87P48'}"}, 
ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_AnwdLAUJWvTWDJ4LN3LW6sFc', function=Function(arguments='{"longitude":"116.475071","latitude":"40.012434","keyword":"医院"}', name='search_nearby_pois'), type='function')]), {'tool_call_id': 'call_AnwdLAUJWvTWDJ4LN3LW6sFc', 'role': 'tool', 'name': 'search_nearby_pois', 'content': '东湖社区卫生服务中心n望京花园东区204号楼n距离:480米nn国宗济世中医院门诊n河荫中路与广顺北大街交叉口东80米n距离:1030米nn北京固生堂京广顺中医医院n望京西园二区京电底商综合楼2n距离:1045米nn'}, 
ChatCompletionMessage(content='北京启明国际大厦附近有以下医院:n1. 东湖社区卫生服务中心n   地址:望京花园东区204号楼n   距离:480米nn2. 国宗济世中医院门诊n   地址:河荫中路与广顺北大街交叉口东80米n   距离:1030米nn3. 北京固生堂京广顺中医医院n   地址:望京西园二区京电底商综合楼2n   距离:1045米', role='assistant', function_call=None, tool_calls=None)]
=====最终回复=====
北京启明国际大厦附近有以下医院:
1. 东湖社区卫生服务中心
   地址:望京花园东区204号楼
   距离:480米

2. 国宗济世中医院门诊
   地址:河荫中路与广顺北大街交叉口东80米
   距离:1030米

3. 北京固生堂京广顺中医医院
   地址:望京西园二区京电底商综合楼2
   距离:1045米

试了很多次,发现并没有调用 search_nearby_pois 函数,调整成老师写的 tools 里面的描述才可以,模型对 tools 里面的自然语言描述比较敏感

2.5 输出 JSON

create 函数中加入参数 response_format={'type': 'json_object'} # 如果加了这个,请在prompt中显式指出使用json格式输出,否则会报错

代码语言:javascript复制
tools = [{
            "type": "function",
            "function": {
                "name": "add_contact",
                "description": "添加联系人",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "name": {
                            "type": "string",
                            "description": "联系人姓名"
                        },
                        "address": {
                            "type": "string",
                            "description": "联系人地址"
                        },
                        "tel": {
                            "type": "string",
                            "description": "联系人电话"
                        },
                    }
                }
            }
        }]

prompt = "帮我把这个文件快递给张三,地址是北京市朝阳区亮启明国际大厦,电话123456789。使用json格式输出"
messages = [
    {"role": "system", "content": "你是一个联系人录入员。"},
    {"role": "user", "content": prompt}
]
response = get_completion_with_tools(messages, tools)
print("====GPT回复====")
print(response)
args = json.loads(response.tool_calls[0].function.arguments)
print("====函数参数====")
print(args)

输出:

代码语言:javascript复制
====GPT回复====
ChatCompletionMessage(content=None, role='assistant', function_call=None, 
	tool_calls=[ChatCompletionMessageToolCall(id='call_baWP1OICnj0LEfq9mGyxkyP4', function=Function(arguments='{"name": "张三", "address": "北京市朝阳区亮启明国际大厦", "tel": "123456789"}', name='add_contact'), type='function'), ChatCompletionMessageToolCall(id='call_7qwlc196ik6XywFI7vAdOLFp', function=Function(arguments='{"name": "快递公司", "address": "北京市朝阳区", "tel": "快递公司电话"}', name='add_contact'), type='function')])
====函数参数====
{'name': '张三', 'address': '北京市朝阳区亮启明国际大厦', 'tel': '123456789'}
2.6 查询数据库

进行多表的join查询

代码语言:javascript复制
# 查询数据库

#  描述数据库表结构
database_schema_string = """
CREATE TABLE course_selection (
    id INT PRIMARY KEY NOT NULL, -- 主键,不允许为空
    student_id INT NOT NULL, -- 学生ID,不允许为空
    course_id STR NOT NULL, -- 课程ID,不允许为空
    registration_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 报名时间,默认为当前时间
    status INT NOT NULL, -- 选课状态,整数类型,不允许为空。0代表未选中,1代表已选中,2代表已取消选课
    credits INT NOT NULL, -- 学分,不允许为空
    FOREIGN KEY (student_id) REFERENCES students(id), -- 外键关联,学生ID关联到students表的id字段
    FOREIGN KEY (course_id) REFERENCES courses(id) -- 外键关联,课程ID关联到courses表的id字段
);
"""
course_table_schema = """
CREATE TABLE courses (
    id STR PRIMARY KEY NOT NULL, -- 课程ID,不允许为空
    name VARCHAR(255) NOT NULL, -- 课程名称,不允许为空
    professor VARCHAR(255) NOT NULL -- 授课教授,不允许为空
);
"""

tools = [{
	# OpenAI 官方示例 https://github.com/openai/openai-cookbook/blob/main/examples/How_to_call_functions_with_chat_models.ipynb
    "type": "function",
    "function": {
        "name": "ask_database",
        "description": "Use this function to answer user questions about student course. 
                            Output should be a fully formed SQL query.",
        "parameters": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": f"""
                            SQL query extracting info to answer the user's question.
                            SQL should be written using this database schema:
                            {database_schema_string}n
                            {course_table_schema}n
                            The query should be returned in plain text, not in JSON.
                            The query should only contain grammars supported by SQLite.
                            """,
                }
            },
            "required": ["query"],
        }
    }
}]

import sqlite3

# 创建数据库连接
conn = sqlite3.connect(':memory:')
cursor = conn.cursor()

# 创建orders表
cursor.execute(database_schema_string)
cursor.execute(course_table_schema)

# 插入明确的模拟记录
mock_data = [
    (101, 1, 'C001', '2023-01-01 10:00:00', 1, 4),
    (102, 1, 'C002', '2023-01-02 11:30:00', 1, 3),
    (100, 1, 'C003', '2023-01-02 11:30:00', 2, 3),
    (103, 2, 'C001', '2023-01-03 09:45:00', 1, 4),
    (104, 2, 'C003', '2023-01-04 14:15:00', 1, 3),
    (105, 3, 'C002', '2023-01-05 13:20:00', 1, 3)
]

for record in mock_data:
    cursor.execute('''
    INSERT INTO course_selection (id, student_id, course_id, registration_time, status, credits)
    VALUES (?, ?, ?, ?, ?, ?)
    ''', record)

mock_data = [
    ('C001', '数据库基础', '刘教授'),
    ('C002', '计算机网络', '王教授'),
    ('C003', '数据结构', '陈教授')
]

for record in mock_data:
    cursor.execute('''
    INSERT INTO courses (id, name, professor)
    VALUES (?, ?, ?)
    ''', record)

# 提交事务
conn.commit()


def ask_database(query):
    cursor.execute(query)
    records = cursor.fetchall()
    return records


prompt = "学生id是1的学生,现在有哪些课程选中了,分别是什么课程名、教师是谁,这些课程总共多少学分"

messages = [
    {"role": "system", "content": "基于 course_selection, courses 表回答用户问题"},
    {"role": "user", "content": prompt}
]
response = get_completion_with_tools(messages, tools)
messages.append(response)

print("====Function Calling====")
print(response)

while response.tool_calls is not None:
    for tool_call in response.tool_calls:
        if tool_call.function.name == "ask_database":
            arguments = tool_call.function.arguments
            args = json.loads(arguments)
            print("====SQL====")
            print(args["query"])
            result = ask_database(args["query"])
            print("====DB Records====")
            print(result)
    
            messages.append({
                "tool_call_id": tool_call.id,
                "role": "tool",
                "name": "ask_database",
                "content": str(result)
            })
            response = get_completion_with_tools(messages, tools)
        print("====GPT回复====")
        print(response.content)

输出:

代码语言:javascript复制
====Function Calling====
ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_JrLGU5uI5RvpfPwdOXF8UI8E', function=Function(arguments='{"query":"SELECT c.name, c.professor, cs.credits FROM course_selection cs JOIN courses c ON cs.course_id = c.id WHERE cs.student_id = 1 AND cs.status = 1;"}', name='ask_database'), type='function')])
====SQL====
SELECT c.name, c.professor, cs.credits FROM course_selection cs JOIN courses c 
ON cs.course_id = c.id WHERE cs.student_id = 1 AND cs.status = 1;
====DB Records====
[('数据库基础', '刘教授', 4), ('计算机网络', '王教授', 3)]
====GPT回复====
学生ID为1的学生已选中了以下课程:
1. 课程名:数据库基础,教师:刘教授,学分:4
2. 课程名:计算机网络,教师:王教授,学分:3

这些课程总共的学分是 7。

3. 支持Function Call的国产模型

  • 文心
  • MiniMax
  • ChatGLM3-6B
  • 星火3.0

0 人点赞