写点什么

如何实现 LLM 的通用 function-calling 能力?

  • 2024-12-10
    福建
  • 本文字数:8095 字

    阅读完需:约 27 分钟

众所周知,LLM 的函数 function-calling 能力很强悍,解决了大模型与实际业务系统的交互问题。其本质就是函数调用。


从 openai 官网摘图:



简而言之:


  1. LLM 起到决策的作用,告知业务系统应该调用什么函数,以及入参是什么。

  2. 业务系统负责实现对应的函数(比如本地实现,或者调用其他系统提供的服务),并且将函数的响应结果再次抛给 LLM。

  3. LLM 根据响应结果,组织自然语言,继续与业务系统进行交互。


在这里,有很多小伙伴会有一个误区:误以为函数调用是有 LLM 本身执行的。其实,LLM 仅仅做决策,而实际的调用是由业务系统完成的。


现阶段,function-calling 能力的实现有两种主流方式:


  1. LLM 本身支持。

  2. 利用 Prompt 模板实现,典型如 ReAct 模板。

 

在实际的应用过程中,我们还要解决另一个重要问题:


function-calling 触发机制是怎样的?也即:何时要使用 function-calling 能力,何时不应该使用?


这个问题的处理方式,对于整体流程的运行至关重要。


此时,我们可以使用特定 Prompt 来解决该问题:


You have access to the following tools:{json.dumps(tools)}You can select one of the above tools or just response user's content and respond with only a JSON object matching the following schema:{{  "tool": <name of the selected tool>,  "tool_input": <parameters for the selected tool, matching the tool's JSON schema>,  "message": <direct response users content>}
复制代码


该 Prompt 告知了 LLM:如果需要使用 function-calling 能力,那么就从 tools(tools 是预定义的 functions)中选取一个最匹配的函数;如果不需要,就用自然语言与用户交互,此时与正常的对话流程无异。输出的格式固定为 json,方便解析。




由此,我们受到启发:只要 LLM 基座够强(能够严格遵循 Prompt 响应诉求),即使 LLM 本身不支持 function-calling,我们也可以自己实现 function-calling,脱离对特定 LLM 的依赖!


拿到 function-calling 的结果后,若要用自然语言的形式输出结果,还要再调用一次 LLM,对结果进行整合。此时可以使用另一个 Prompt:


Please generate a natural language description based on the following question and answer.Question: [Content of the question]Answer: [Content of the answer]Generated Description: The result of [key phrase from the question] is [answer].If necessary, you can polish the description.Only output the Description, with Chinese language.
复制代码


该 Prompt 的作用就是告诉 LLM,你要根据我的问题和答案,用自然语言重新描述一遍。这里指定了中文输出,可根据实际需要进行调整。


以下是一个可运行的完整 Python 脚本:


import requestsimport jsonimport random
# 预置函数定义tools = [    {        "name": "get_current_weather",        "description": "Get the current weather in a given location",        "parameters": {            "type": "object",            "properties": {                "location": {                    "type": "string",                    "description": "The city e.g. Beijing"                },                "unit": {                    "type": "string",                    "enum": [                        "celsius"                    ]                }            },            "required": [                "location"            ]        }    },    {        "name": "calculator",        "description": "计算器",        "parameters": {            "type": "int",            "properties": {                "a": {                    "type": "int",                    "description": "the first number"                },                "b": {                    "type": "int",                    "description": "the second number"                }            },            "required": [                "a",                "b"            ]        }    }]
# 获取天气(随机返回,实际使用可以替换为api调用)def get_current_weather(*args):    # 定义可能的天气状态    weather_conditions = ["sunny", "cloudy", "rainy", "snowy"]    # 定义可能的温度范围    temperature_min = -10  # 最低温度,摄氏度    temperature_max = 35  # 最高温度,摄氏度    # 随机选择一个天气状态    condition = random.choice(weather_conditions)    # 随机生成一个温度    temperature = random.randint(temperature_min, temperature_max)    # 返回一个描述当前天气的字符串    return f"The weather of {args[0].get('location')} is {condition}, and the temperature is {temperature}°C."
def calculator(args):    return sum(value for value in args.values() if isinstance(value, int))
# 函数映射集合functions = {    "get_current_weather": get_current_weather,    "calculator": calculator,}
# 驱动整体流程的入口promptentrance_prompt = f"""You have access to the following tools:{json.dumps(tools)}You can select one of the above tools or just response user's content and respond with only a JSON object matching the following schema:{{  "tool": <name of the selected tool>,  "tool_input": <parameters for the selected tool, matching the tool's JSON schema>,  "message": <direct response users content>}}"""
# 请以自然语言的形式对结果进行描述conformity_prompt = f"""Please generate a natural language description based on the following question and answer.Question: [Content of the question]Answer: [Content of the answer]Generated Description: The result of [key phrase from the question] is [answer].If necessary, you can polish the description.Only output the Description, with Chinese language."""
def extract_json(s):    stack = 0    start = s.find('{')    if start == -1:        return None
    for i in range(start, len(s)):        if s[i] == '{':            stack += 1        elif s[i] == '}':            stack -= 1            if stack == 0:                return s[start:i + 1]    return None
# 结果包装器,type为func表示是函数调用返回的结果,default表示是自然语言结果。对于func返回的结果,会用LLM再次总结class ResultWrapper:    def __init__(self, type, result):        self.type = type        self.result = result
# 解析LLM返回的结果,如果有json则去解析jsondef parse_result(res):    json_str = extract_json(res["message"]["content"])    if json_str is not None:        obj = json.loads(json_str)        if "tool" in obj:            if obj["tool"] in functions:                fun = functions[obj["tool"]]                return ResultWrapper("func", fun(obj["tool_input"]))            else:                return ResultWrapper("default", obj["message"])        else:            return ResultWrapper("default", res["message"]["content"])    else:        return ResultWrapper("default", res["message"]["content"])
def invokeLLM(messages):    url = "${domain}/v1/chat/completions" #需替换域名    model = ""    payload = {        "model": model,        "messages": messages,    }    payload = json.dumps(payload)    headers = {        'Content-Type': 'application/json'    }    print("PAYLOAD: ", payload)    response = requests.request("POST", url, headers=headers, data=payload)    print("RESPONSE: ", response.text)    print("=======================================================================")    resp = json.loads(response.text)    return resp["choices"][0]

if __name__ == '__main__':    while True:        messages = [            {                "role": "system",                "content": entrance_prompt            }        ]        user_input = input('Enter a string: ')        messages.append({            "role": "user",            "content": user_input        })        result_wrapper = parse_result(invokeLLM(messages))        if result_wrapper.type == "func":            messages = [                {                    "role": "user",                    "content": f"{conformity_prompt}\n\nThe question:{user_input}\nThe answer:{result_wrapper.result}"                }            ]            print("FINAL RESULT WITH FUNCTION CALL: ", parse_result(invokeLLM(messages)).result)        else:            print("FINAL RESULT: ", result_wrapper.result
复制代码


实验效果:


Enter a string: 你好PAYLOAD: {"model": "", "messages": [{"role": "system", "content": "You have access to the following tools:\n[{\"name\": \"get_current_weather\", \"description\": \"Get the current weather in a given location\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\", \"description\": \"The city e.g. Beijing\"}, \"unit\": {\"type\": \"string\", \"enum\": [\"celsius\"]}}, \"required\": [\"location\"]}}, {\"name\": \"calculator\", \"description\": \"\\u8ba1\\u7b97\\u5668\", \"parameters\": {\"type\": \"int\", \"properties\": {\"a\": {\"type\": \"int\", \"description\": \"the first number\"}, \"b\": {\"type\": \"int\", \"description\": \"the second number\"}}, \"required\": [\"a\", \"b\"]}}]\nYou can select one of the above tools or just response user's content and respond with only a JSON object matching the following schema:\n{\n \"tool\": <name of the selected tool>,\n \"tool_input\": <parameters for the selected tool, matching the tool's JSON schema>,\n \"message\": <direct response users content>\n}"}, {"role": "user", "content": "\u4f60\u597d"}]}RESPONSE: {"model":"","object":"","choices":[{"index":0,"message":{"role":"assistant","content":"```json\n{\"tool\": null, \"tool_input\": null, \"message\": \"你好,有什么可以帮您的吗?\"}\n```","function_call":null},"finish_reason":"stop"}],"queueTime":0.0020923614501953125,"costTime":0.7685532569885254,"usage":{"prompt_token":244,"completion_token":29,"total_tokens":273}}=======================================================================FINAL RESULT: 你好,有什么可以帮您的吗?

Enter a string: 厦门天气如何?PAYLOAD: {"model": "", "messages": [{"role": "system", "content": "You have access to the following tools:\n[{\"name\": \"get_current_weather\", \"description\": \"Get the current weather in a given location\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\", \"description\": \"The city e.g. Beijing\"}, \"unit\": {\"type\": \"string\", \"enum\": [\"celsius\"]}}, \"required\": [\"location\"]}}, {\"name\": \"calculator\", \"description\": \"\\u8ba1\\u7b97\\u5668\", \"parameters\": {\"type\": \"int\", \"properties\": {\"a\": {\"type\": \"int\", \"description\": \"the first number\"}, \"b\": {\"type\": \"int\", \"description\": \"the second number\"}}, \"required\": [\"a\", \"b\"]}}]\nYou can select one of the above tools or just response user's content and respond with only a JSON object matching the following schema:\n{\n \"tool\": <name of the selected tool>,\n \"tool_input\": <parameters for the selected tool, matching the tool's JSON schema>,\n \"message\": <direct response users content>\n}"}, {"role": "user", "content": "\u53a6\u95e8\u5929\u6c14\u5982\u4f55\uff1f"}]}RESPONSE: {"model":"","object":"","choices":[{"index":0,"message":{"role":"assistant","content":"```json\n{\"tool\": \"get_current_weather\", \"tool_input\": {\"location\": \"Xiamen\", \"unit\": \"celsius\"}, \"message\": \"\"}\n```","function_call":null},"finish_reason":"stop"}],"queueTime":0.0021338462829589844,"costTime":0.9370713233947754,"usage":{"prompt_token":247,"completion_token":36,"total_tokens":283}}=======================================================================PAYLOAD: {"model": "", "messages": [{"role": "user", "content": "\nPlease generate a natural language description based on the following question and answer.\nQuestion: [Content of the question]\nAnswer: [Content of the answer]\nGenerated Description: The result of [key phrase from the question] is [answer].\nIf necessary, you can polish the description.\nOnly output the Description, with Chinese language.\n\n\nThe question:\u53a6\u95e8\u5929\u6c14\u5982\u4f55\uff1f\nThe answer:The weather of Xiamen is cloudy, and the temperature is 35\u00b0C."}]}RESPONSE: {"model":"","object":"","choices":[{"index":0,"message":{"role":"assistant","content":"厦门天气情况是:多云,气温35°C。","function_call":null},"finish_reason":"stop"}],"queueTime":0.008246660232543945,"costTime":0.3240656852722168,"usage":{"prompt_token":143,"completion_token":12,"total_tokens":155}}=======================================================================FINAL RESULT WITH FUNCTION CALL: 厦门天气情况是:多云,气温35°C。

Enter a string: 383加上135721等于多少?PAYLOAD: {"model": "", "messages": [{"role": "system", "content": "You have access to the following tools:\n[{\"name\": \"get_current_weather\", \"description\": \"Get the current weather in a given location\", \"parameters\": {\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\", \"description\": \"The city e.g. Beijing\"}, \"unit\": {\"type\": \"string\", \"enum\": [\"celsius\"]}}, \"required\": [\"location\"]}}, {\"name\": \"calculator\", \"description\": \"\\u8ba1\\u7b97\\u5668\", \"parameters\": {\"type\": \"int\", \"properties\": {\"a\": {\"type\": \"int\", \"description\": \"the first number\"}, \"b\": {\"type\": \"int\", \"description\": \"the second number\"}}, \"required\": [\"a\", \"b\"]}}]\nYou can select one of the above tools or just response user's content and respond with only a JSON object matching the following schema:\n{\n \"tool\": <name of the selected tool>,\n \"tool_input\": <parameters for the selected tool, matching the tool's JSON schema>,\n \"message\": <direct response users content>\n}"}, {"role": "user", "content": "383\u52a0\u4e0a135721\u7b49\u4e8e\u591a\u5c11\uff1f"}]}RESPONSE: {"model":"","object":"","choices":[{"index":0,"message":{"role":"assistant","content":"```json\n{\"tool\": \"calculator\", \"tool_input\": {\"a\": 383, \"b\": 135721}, \"message\": null}\n```","function_call":null},"finish_reason":"stop"}],"queueTime":0.0021514892578125,"costTime":0.9161381721496582,"usage":{"prompt_token":252,"completion_token":35,"total_tokens":287}}=======================================================================PAYLOAD: {"model": "", "messages": [{"role": "user", "content": "\nPlease generate a natural language description based on the following question and answer.\nQuestion: [Content of the question]\nAnswer: [Content of the answer]\nGenerated Description: The result of [key phrase from the question] is [answer].\nIf necessary, you can polish the description.\nOnly output the Description, with Chinese language.\n\n\nThe question:383\u52a0\u4e0a135721\u7b49\u4e8e\u591a\u5c11\uff1f\nThe answer:136104"}]}RESPONSE: {"model":"","object":"","choices":[{"index":0,"message":{"role":"assistant","content":"383加上135721等于136104。","function_call":null},"finish_reason":"stop"}],"queueTime":0.0064160823822021484,"costTime":0.28981900215148926,"usage":{"prompt_token":134,"completion_token":11,"total_tokens":145}}=======================================================================FINAL RESULT WITH FUNCTION CALL: 383加上135721等于136104。
复制代码


在这个例子中,预置了两个函数,分别为天气查询和计算器,实验效果中进行了三轮,其中第一次属于未命中函数调用的闲聊场景,后两次分别命中了天气查询和计算器。


在实际的工作中,可能需要预置非常多函数能力,此时可能需要考虑到 LLM 的输入 token 限制,必要时需要进行模块划分,将一次 LLM 决策转化为多次决策,更通用一点的说法就是意图层级识别。


文章转载自:xiaoxi666

原文链接:https://www.cnblogs.com/xiaoxi666/p/18594503

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
如何实现LLM的通用function-calling能力?_LLM_快乐非自愿限量之名_InfoQ写作社区