Function Calling 讲透:AI 从“会说”到“会做”就这一步

5 阅读6分钟

博客配套代码发布于github:03 Function calling 与工具调用

本系列所有博客均配套Gihub开源代码,开箱即用,仅需配置API_KEY。

如果该Agent教学系列帮到了你,欢迎给我个Star⭐,非常感谢!

知识点Function calling原理、工具函数封装、API接入实践、多轮调用流程、Agent能力扩展


一、什么是function callings

回想一下,所谓ai,虽然它的确看起来牛哄哄,什么都懂什么都会,但它的局限性非常大。它无法知道最新发生的某件事,无法知道你公司数据库的重要信息,也没办法动手帮你干事情。这时候,而解决方法也显而易见:为它加上手,也就是function calling

function是函数,calling是调用,即从外部调用函数给agent

上面看起来是局限的事情,有了调用函数的方法后立刻变得简单了起来。不知道某事就为它加入个可联网搜索的api接口,不知道你公司数据库信息就用函数链接数据库,没法动手做事就写代码让它动起来...

总而言之,当我们为它配备一个函数库,原先那些它无法解决的事情就会通过调用函数库的方式来完成回应。此时的大模型处理与解决问题的能力可以说是又上一层楼。

function calling就是Agent智能体开发的基础。

二、自定义函数调用

我们先来尝试一下,自己写个假的小函数作为工具让Agent调用:

def get_weather(location):
    # 模拟获得天气信息
    return f"{location}当前天气:23℃,晴,风力2级"

封装完了之后,我们还需要将其改一下agent接受的固定格式:

# 以下格式为固定写法,一般仅需改description与name
get_weather_func = {
    "name": "get_weather",  # 函数名称
    "description": "获取指定城市的天气情况",  # 对该函数的描述
    "parameters": {
        "type": "object",
        "properties": {
            "location": {
                "type": "string",  # 该参数类型
                "description": "城市名称,如北京、上海等"  # 对该参数的描述
            }
        },
        "required": ["location"] # 声明必填
    }
}

看起来很复杂,但里面格式都是固定的。其中需要更改的地方只有name,description,parameters参数location。最后的required就是要哪些参数加进去。

封装好get_weather_func后就可以将它扔给tools作为一个工具了。

# 固定写法,有多少个tool就往里追加多少个
tools = [
    {
        "type": "function",
        "function": get_weather_func
    }
]

紧接着,我们就需要让response接收这个工具:

def chat_loop(agent_client, tools):
    messages = [
        {"role": "system", "content": "你是一个善解人意会热心回答人问题的助手。如果你感觉你回答不了当前问题,就会调用函数来回答。"},
        {"role": "user", "content": "北京今天的天气怎么样?"}
    ]
    response = agent_client.chat.completions.create(
        model="deepseek-chat",
        messages=messages,
        tools=tools,  # 调用工具
        tool_choice="auto"  # 模型自主选择是否调用工具
    )
    message = response.choices[0].message

对比起之前,很明显能注意到这里我们加入了tools=tools,tool_choice="auto"。这里是让agent知道可以调用agent,auto是让agent智能选择它是否要调用工具来解决问题。

再接着想想:

ai调用工具与否肯定按两套方式输出:没调直接输出,调用了就按调用后的输出。但是ai返回肯定是第一次只能返回它是否要调用,第二次才能返回它依靠调用后的工具函数输出的结果。所以这里我们要二次调用才能最终生成答案。因此代码应构建如下:

    # 如果有该参数,证明ai调用了工具
    if message.tool_calls:
        # 对每个可能要调用的工具进行循环
        for tool_call in message.tool_calls:
            if tool_call.function.name == "get_weather":
                # 解析参数
                args = json.loads(tool_call.function.arguments)  # 获取用户关键词的参数
                location = args.get("location", "未知地点")
​
                # 调用真实参数
                weather_info = get_weather(location)
​
                # 将函数执行结果以"tool"角色传给模型,等待后面二次调用
                messages.append(message)  # 先添加模型的原始响应
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "name": tool_call.function.name,
                    "content": weather_info
                })
        # 第二次调用,让模型基于工具返回的结果再生成最终答案
        final_res = agent_client.chat.completions.create(
            model="deepseek-chat",
            messages=messages
        )
        print('已调用工具...')
        print(f'回答:{final_res.choices[0].message.content}')
    else:
        # 模型没有要调用工具,直接返回
        print('未调用工具...')
        print(f'回答:{response.choices[0].message.content}')

(完整可运行代码于github配套提供)

返回结果:

img

没有问题。

总结一下我们的流程:

User Input
    ↓
LLM (第一次调用) → 决定调用哪个工具
    ↓
执行函数(get_weather)
    ↓
把结果以 "tool" 角色塞回去
    ↓
LLM (第二次调用) → 生成最终回答

三、API调用

上面的毕竟是我们自己虚构的函数,这里我们接入一个真实存在的api来测试下。

与上述的流程大同小异,只不过需要注意下你新加工具的api的文档使用方法,传入api与使用时的格式也要小心。

ipapi这个网站能获取ip地址与对应城市,并且不需要api-key,直接向对应地址发起请求即可:

def get_addr():
    res_ip = requests.get('https://ipapi.co/ip/').text
    res_city = requests.get('https://ipapi.co/city/').text
    return f'ip地址:{res_ip},所在城市:{res_city}'

另外有些小地方也需要注意下。这个get_addr特殊在它不需要加入参数。

if tool_call.function.name == "get_addr":
    # 解析参数
    # args = json.loads(tool_call.function.arguments)  # 获取用户关键词的参数
    # location = args.get("location")
    # 调用真实参数
    # your_info = get_weather(location)
​
    # 这里无参数,直接调用函数就行
    your_info = get_addr()

get_addr_func = {
    "name": "get_addr",  # 函数名称
    "description": "获取用户的ip地址与城市",  # 对该函数的描述
    "parameters": {
        "type": "object",
        "properties": {}, # 参数为空,那么这两个地方也是空的
        "required": []  
    }
}

运行结果如下:img

完成。

俩个案例做下来,有没有发现这个过程是不是有点繁琐,而且里面的很多工具看起来都是可以封装的?

猜对了,这就是langchain的做法。下篇我们很快会开始了解langchain,这篇我们本质上就是在学function calling的原理,造轮子的底层逻辑。在框架langchain中,它就已经给我们封装好了这些复杂的东西,不需要我们再手动去敲,直接上手去做即可。

四、总结

知识点概括Function calling原理、工具函数封装、API接入实践、多轮调用流程、Agent能力扩展

至此我们可以自豪的说,我们已经知道如何真正构建一个正儿八经的智能体了,它有记忆提示词,可以调用外部工具函数,能够多轮问答

但,有没有觉得这些工作还是有点繁琐?是的,基础篇我们已经搞定,接下来就是框架篇了。langchian/langgraph会为我们的agent开发更省功夫,并给出更系统化、标准化、自动化的使用方法。同时还有rag让我们的agent更为智能。

下篇内容,我们会开始深入讲解langchain与其详细用法,更快捷的构建一个agent。

🚀一键跳转[Ai Agent] 04 一文吃透LangChain:Prompt、LLM、Chain、Memory 全流程实战

📌 项目代码 + 后续案例合集 全部发布在 Github 仓库 agent-craft ,持续更新中,欢迎Star⭐!