全部文章

Agent

 一、Agent的介绍

LLM VS Agent

两者的介绍:

  • LLM:大型语言模型是一种基于深度学习的模型,专注于理解和生成自然语言文本。它的主要目的是处理与语言相关的任务,如文本生成、翻译、问答等。
  • Agent:人工智能代理是一个更广泛的概念,它指的是能够在特定环境中自主行动以实现目标的程序。AI Agent的目的可以是多样的,包括但不限于语言处理,还包括感知、决策和行动等。

LLM是AI Agent可能使用的工具之一,专注于自然语言处理。而AI Agent是一个更全面的概念,它可能包含LLM,但还包括其他组件和能力,以实现在复杂环境中的自主行为。两者在人工智能领域中都有重要的地位,但它们的设计和应用目标不同。

维度​

​LLM​

​Agent​

​本质​

文本理解与生成模型

自主决策的问题解决器

​能力范围​

语言任务(翻译/问答/生成)

规划+记忆+工具使用+环境交互

​技术定位​

Agent的可能组件之一

包含LLM的完整问题解决框架

AI Agent = LLM X (规划、行动、工具、记忆)

 Agent 是什么?

AI 业界对智能体提出了各种定义。个人理解,智能体是一种通用问题解决器。从软件工程的角度看来,智能体是一种基于大语言模型的,具备规划思考能力、记忆能力、使用工具函数的能力,能自主完成给定任务的计算机程序。

image

图片来源:https://lilianweng.github.io/posts/2023-06-23-agent/ , 强烈建议大家通篇阅读。

对应概念的介绍

  • 规划(Planning):智能体会把大型任务 分解为子任务 ,并规划执行任务的流程;智能体会对任务执行的过程进行思考和反思 ,从而决定是继续执行任务,或判断任务完结并终止运行。
  • 执行(Action: 根据规划和记忆来实施具体行动,这可能会涉及到与外部世界的互动或通过工具来完成任务。
  • 工具使用(Tools:为智能体配备工具 API,比如:计算器、搜索工具、代码执行器、数据库查询工具等。有了这些工具 API,智能体就可以是物理世界交互,解决实际的问题。
  • 记忆(Memory: 短期记忆,是指在执行任务的过程中的上下文,会在子任务的执行过程产生和暂存,在任务完结后被清空。长期记忆是长时间保留的信息,一般是指外部知识库,通常用向量数据库来存储和检索。

 

规划(Planning)

规划,可以为理解观察和思考。如果用人类来类比,当我们接到一个任务,我们的思维模式可能会像下面这样:

1、首先会思考怎么完成这个任务。

2、然后会审视手头上所拥有的工具,以及如何使用这些工具高效地达成目的。

3、再会把任务拆分成子任务。(就像咱们做思维导图一样。)

4、在执行任务的时候,我们会对执行过程进行反思和完善,吸取教训以完善未来的步骤。

5、执行过程中思考任务何时可以终止。

这是人类的规划能力,我们希望智能体也拥有这样的思维模式,因此可以通过LLM 提示工程,为智能体赋予这样的思维模式。在智能体中,最重要的是让 LLM 具备这以下两个能力:

  • 子任务分解: 通过LLM使得智能体可以把大型任务分解为更小的、更可控的子任务,从而能够有效完成复杂的任务。
  • 思维链: 思维链已经是一种比较标准的提示技术。

image

思维链:已经是一种比较标准的提示技术,能显著提升 LLM 完成复杂任务的效果。当我们对 LLM 这样要求「think step by step」,会发现 LLM 会把问题分解成多个步骤,一步一步思考和解决,能使得输出的结果更加准确。这是一种线性的思维方式。

思维树:对 CoT 的进一步扩展,在思维链的每一步,推理出多个分支,拓扑展开成一棵思维树。使用启发式方法评估每个推理分支对问题解决的贡献。选择搜索算法,使用广度优先搜索(BFS)或深度优先搜索(DFS)等算法来探索思维树,并进行前瞻和回溯。

工具使用(Tools/Toolkits)

Agent可以通过学习调用外部API来获取模型权重中所缺少的额外信息,这些信息包括当前信息、代码执行能力和访问专有信息源等。这对于预训练后难以修改的模型权重来说是非常重要的。

掌握使用工具是人类最独特和重要的特质之一。我们通过创造、修改和利用外部工具来突破我们身体和认知的限制。同样地,我们也可以为语言模型(LLM)提供外部工具来显著提升其能力。

image

Langchain工具集链接:https://python.langchain.com/docs/integrations/tools/

记忆(Memory)

生活中的记忆机制:

image

基于LangChain 框架构建智能体

openai和LangChain构建智能体

第一个案例

  1. 安装依赖:需要先在命令行运行以下命令安装库:
     
    # 核心库
    pip install langchain langchain-openai langchain-community
    # 环境变量工具
    pip install python-dotenv
    # 搜索引擎工具依赖
    pip install google-search-results
  2. 准备 API 密钥
    • 注册 OpenAI 账号 获取 OPENAI_API_KEY
    • 注册 SerpAPI 账号 获取 SERPAPI_API_KEY(免费版有调用次数限制)
    • 在代码同目录创建 .env 文件,写入:
       
      OPENAI_API_KEY="你的OpenAI密钥"
      SERPAPI_API_KEY="你的SerpAPI密钥"
      DEEPSEEK_API_KEY="sk-5503fd59b4934e19a77af7c3eed93"
       
  3. 运行说明:代码会先打印代理的思考过程(比如 “我需要获取今天的 NBA 新闻,应该调用搜索引擎”),最后输出整理后的结果。
如果运行中遇到错误,大概率是 API 密钥无效或网络问题(OpenAI 需要科xue上网)。

# 导入必要的工具和类
# LangChain是一个用于构建LLM应用的框架,这里主要用到它的"代理(Agent)"功能
from langchain_community.agent_toolkits.load_tools import load_tools # 用于加载可用的工具
from langchain.agents import initialize_agent  # 用于初始化代理
from langchain.agents import AgentType  # 代理类型枚举
from langchain_openai import ChatOpenAI  # 导入OpenAI的聊天模型接口
from dotenv import load_dotenv  # 用于加载环境变量(存储API密钥等敏感信息)

# 加载.env文件中的环境变量(需要自己创建这个文件)
# .env文件里需要写:OPENAI_API_KEY="你的OpenAI密钥" 和 SERPAPI_API_KEY="你的SerpAPI密钥"
load_dotenv()

# 初始化大语言模型(LLM):这里用的是OpenAI的GPT-4o模型
# model参数指定模型名称,temperature=0表示让输出更确定(0-1之间,值越高越随机)
llm = ChatOpenAI(model="gpt-4o", temperature=0)

# 加载工具:这里加载了"serpapi"(一个搜索引擎API)
# 工具的作用是让LLM能够调用外部资源,弥补自身知识不实时的问题
# 注意:使用serpapi需要先去官网(https://serpapi.com/)注册获取API密钥
tools = load_tools(["serpapi"], llm=llm)

# 初始化代理(Agent):代理是连接LLM和工具的中间层,决定何时调用工具、如何处理结果
# 参数说明:
# - tools:代理可以使用的工具列表
# - llm:核心语言模型
# - agent:代理类型,ZERO_SHOT_REACT_DESCRIPTION表示"零样本反应型"
#   这种代理不需要训练,会根据问题和工具描述直接决定行动
# - verbose=True:显示详细过程(会打印代理的"思考"步骤,方便调试和理解)
agent = initialize_agent(
    tools, 
    llm, 
    agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, 
    verbose=True
)

# 让代理执行任务:获取今天的NBA比赛新闻
# invoke方法接收一个包含"input"键的字典,值是要问的问题
# 因为NBA新闻是实时更新的,LLM自身知识可能过时,所以代理会自动调用serpapi搜索引擎获取最新信息
result = agent.invoke({"input": "今天的NBA比赛有什么新闻信息呢?用中文回答"})

# 打印最终结果(result是一个字典,其中"output"键对应回答内容)
print("\n最终答案:", result["output"])

DeepSeek 和 LangChain构建智能体

# 导入必要的库
# langchain_deepseek 是 DeepSeek 模型的 LangChain 集成
from langchain_deepseek.chat_models import ChatDeepSeek
# 导入 LangChain 的代理相关功能
from langchain.agents import initialize_agent
# 导入 LangChain 的工具加载功能
from langchain_community.agent_toolkits.load_tools import load_tools
# 导入代理类型定义
from langchain.agents import AgentType
# dotenv 用于从 .env 文件加载环境变量
from dotenv import load_dotenv


# 加载环境变量(确保项目根目录有 .env 文件,并且文件中设置了 DEEPSEEK_API_KEY 和 SERPAPI_API_KEY)
# .env 文件示例内容:
# DEEPSEEK_API_KEY=your_deepseek_api_key_here
# SERPAPI_API_KEY=your_serpapi_api_key_here
load_dotenv()

# 初始化 DeepSeek 语言模型
# ChatDeepSeek 是 LangChain 对 DeepSeek API 的封装
llm = ChatDeepSeek(
    model="deepseek-chat",  # 指定使用的模型名称
    api_base="https://api.deepseek.com"  # 指定 API 的基础 URL
    # 注意:api_key 参数未显式设置,会自动从环境变量 DEEPSEEK_API_KEY 读取
)

# 加载搜索引擎工具
# load_tools 函数加载指定的工具集,这里加载了 serpapi 工具
# serpapi 是一个搜索引擎 API,可以获取实时网络搜索结果
tools = load_tools(["serpapi"], llm=llm)

# 初始化代理并添加错误处理
# initialize_agent 创建一个代理,它结合了语言模型和工具
agent = initialize_agent(
    tools,  # 可用的工具列表
    llm,  # 使用的语言模型
    agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,  # 代理类型:零样本反应描述
    # 这种代理类型不需要示例就能理解如何使用工具
    verbose=True,  # 设置为 True 会显示详细的执行过程,便于调试和理解
    handle_parsing_errors=True  # 处理解析错误,当模型输出不符合预期格式时不会崩溃
)

# 调用代理
# 使用代理处理查询
# invoke 方法发送查询给代理,代理会根据问题决定是否使用工具以及如何使用
result = agent.invoke("今天的 NBA 新闻")

# 打印最终答案
# 代理的输出包含在返回字典的 "output" 键中
print("\n最终答案:", result["output"])

 

基于LangChain 和 Function Call构建智能体的区别

 
特性 LangChain 方法 OpenAI 原生方法
抽象层级 高级框架封装 底层原生 API
开发复杂度 较低(框架处理复杂性) 较高(需要手动处理)
控制精度 中等(框架决定流程) 高(完全自主控制)
工具调用方式 自动处理工具选择和调用 手动处理函数调用和结果整合
错误处理 内置错误处理机制 需要手动实现错误处理
适用场景 快速原型开发、标准应用 需要精细控制的复杂应用
  1. 框架 vs 原生

    • LangChain 提供了高级抽象,自动处理工具调用流程

    • OpenAI 原生方法需要手动管理整个函数调用过程

总结:

两种方法都是构建智能体的有效方式:

  • LangChain 方法更适合快速开发和标准化场景

  • OpenAI 原生方法提供更精细的控制,适合需要高度自定义的复杂应用

二、Function Calling

Function Calling 介绍

⼤语⾔模型,例如 DeepSeek,拥有强⼤的知识储备和语⾔理解能⼒,能够进⾏流畅的对话、创作精彩的故事,甚⾄编写代码。然⽽,它们也⾯临着⼀些难以克服的困境,就像⼀个空有知识却⽆法⾏动的巨⼈。

  • 信息滞后:知识库的更新速度有限,⽆法获取最新的实时信息,例如天⽓、股价等。这意味着它可能告诉你昨天的天⽓,或者⼀个⼩时前的股价,⽽这些信息可能已经过时了。就像⼀本印刷好的百科全书,内容固定,⽆法实时更新。
  • 缺乏行动力: 就像被困在虚拟世界中的⼤脑,只能提供信息,⽆法直接与现实世界互动。例如,它可以告诉你如何煮咖啡,但⽆法帮你磨咖啡⾖、煮咖、甚⾄递给你⼀杯咖啡。 这就好⽐⼀位博学的教授,可以讲解复杂的理论知识,但却⽆法在实验室进⾏实际操作。

大模型的三大缺陷:

  • 训练数据不可能涵盖所有信息。垂直、⾮公开数据必有⽋缺。
  • 不知道最新信息。⼤模型的训练周期很⻓,且更新⼀次耗资巨⼤。所以它不可能实时训练。GPT-3.5 的知识截⾄ 2022 年 1 ⽉,GPT-4 是 2023 年 4 ⽉。
  • 没有「真逻辑」。它表现出的逻辑、推理,是训练⽂本的统计规律,⽽不是真正的逻辑。也就是说,它的结果都是有⼀定不确定性的,这对于需要精确和确定结果的领域,如数学等,是灾难性的,基本是不可⽤的。

所以:⼤模型需要连接真实世界,并对接真逻辑系统,以此来控制⼤模型输出的不确定性和幻觉,达到我们想要的结果。于是 OpenAI 在23年6⽉份推出了 FunctionCalling 功能,让 GPT 模型拥有调⽤外部接⼝的能⼒。

OpenAI Function Calling:

https://platform.openai.com/docs/guides/function-calling

image

Function Calling 完整的流程结构介绍:

image

特点介绍:

Function Calling 是⼀种让 Chat Completion 模型调⽤外部函数的能⼒,可以让模型不仅仅根据⾃身的数据库知识进⾏回答,⽽是可以额外挂载⼀个函数库,然后根据⽤户提问去函数库检索,按照实际需求调⽤外部函数并获取函数运⾏结果,再基于函数运⾏结果进⾏回答。

Function Calling 可以被应⽤于各种场景,例如:

  • 调⽤天⽓ API 获取实时天⽓信息: 不再局限于提供过时的天⽓预报,⽽是可以告诉你此时此刻的天⽓状况,就像你打开⼿机上的天⽓应⽤⼀样。
  • 调⽤订票⽹站 API 预订机票: 不再只是告诉你如何订票,⽽是可以直接帮你完成订票操作,就像⼀个专业的旅⾏代理⼀样。
  • 调⽤⽇历 API 安排会议: 不再只是提醒你会议时间,⽽是可以直接帮你安排会议,并发送邀请给参会者,就像⼀个⾼效的私⼈助理⼀样。
  • 调⽤数据库查询信息: 可以访问和查询数据库中的信息,例如产品信息、⽤户信息等,就像⼀个专业的数据库管理员⼀样。
  • 调⽤代码执⾏程序: 可以执⾏代码来完成各种任务,例如数据分析、图像处理等,就像⼀个经验丰富的程序员⼀样。

Function Calling 调用本地函数

定义一个自定义的本地函数,也可以是现有的库中的函数以Python内置的sum函数为例,假设我们想让大模型使用这个函数。我们来看看这个Function Calling怎么来实现。

# 以下代码实现了一个使用DeepSeek API进行函数调用的示例程序
# 功能:接收用户关于年龄的问题,通过调用DeepSeek模型分析问题,必要时调用本地函数计算年龄总和,最后返回自然语言回答

# 导入所需库
# OpenAI兼容库用于调用DeepSeek API
from openai import OpenAI, NotFoundError, AuthenticationError
# os库用于处理操作系统相关功能
import os
# json库用于处理JSON数据格式
import json
# dotenv库用于从.env文件加载环境变量
from dotenv import load_dotenv

# 从.env文件加载环境变量,其中包含DeepSeek API密钥
# .env文件应包含类似这样的内容:DEEPSEEK_API_KEY="你的API密钥"
load_dotenv()

# 初始化DeepSeek客户端
# 客户端需要指定DeepSeek的API端点和API密钥
client = OpenAI(
    api_key=os.getenv("DEEPSEEK_API_KEY"),  # 从环境变量获取API密钥
    base_url="https://api.deepseek.com",  # DeepSeek API端点
)


# 定义获取模型响应的函数
def get_completion(messages, model="deepseek-chat"):
    """
    调用DeepSeek API获取模型响应,并处理可能的错误

    参数:
        messages: 对话历史消息列表,包含系统消息、用户消息和工具返回结果
                  每个消息是一个字典,包含"role"和"content"等键
        model: 使用的模型名称,默认为deepseek-chat

    返回:
        模型的响应消息对象,包含回复内容或工具调用指令
        若发生错误且无法恢复,则返回None
    """
    try:
        # 调用DeepSeek的chat completions API
        response = client.chat.completions.create(
            model=model,  # 指定使用的模型
            messages=messages,  # 传入对话历史
            temperature=0,  # 温度设为0,使输出更确定、更一致
            max_tokens=1024,  # 限制最大令牌数,控制响应长度
            # 定义可用的工具函数
            tools=[
                {
                    "type": "function",
                    "function": {
                        "name": "sum_number",  # 函数名称
                        # 函数描述,帮助模型判断何时需要调用该函数
                        "description": "计算一组数的和",
                        # 函数参数定义,遵循JSON Schema格式
                        "parameters": {
                            "type": "object",
                            "properties": {
                                "numbers": {
                                    "type": "array",
                                    "items": {
                                        "type": "number"  # 数组元素类型为数字
                                    },
                                    "description": "需要求和的数字列表"  # 添加参数描述
                                }
                            },
                            "required": ["numbers"]  # 明确指定numbers是必填参数
                        }
                    }
                }
            ],
            tool_choice="auto"  # 让模型根据问题自动决定是否调用工具
        )
        # 返回响应中的第一条消息
        return response.choices[0].message

    # 处理API密钥验证失败的错误
    except AuthenticationError:
        print("API密钥验证失败,请检查你的API密钥是否正确")
        return None

    # 处理其他可能的异常
    except Exception as e:
        print(f"调用API时发生错误: {e}")
        return None


# 定义求和函数
def sum_number(numbers):
    """
    计算一组数字的和

    参数:
        numbers: 数字列表

    返回:
        数字列表中所有数字的和
    """
    return sum(numbers)


# 用户的问题:需要计算年龄总和
# 问题中包含三个年龄,但实际需要计算的是"我"和"舅舅"的年龄总和
prompt = "我今年18岁,我的舅舅今年38岁,我的爷爷今年72岁,我和舅舅一共多少岁了?"
# 另一个测试问题:乘法运算(当前工具无法直接处理,模型会尝试直接回答)
# prompt = "30*8等于多少?"

# 构建对话消息列表
# 消息列表是维护对话状态的关键,包含了所有对话历史
messages = [
    # 系统消息:定义模型的角色、能力和行为准则
    {"role": "system", "content": "你是一个数学家,你可以计算任何算式。如果需要计算一组数的和,可以调用sum_number函数。"},
    # 用户消息:包含用户当前的问题
    {"role": "user", "content": prompt}
]

# 第一次调用模型,获取初步响应
# 模型会分析问题,决定是直接回答还是调用工具
response = get_completion(messages)

# 检查响应是否有效
if response is None:
    print("无法获取模型响应,请检查你的配置")
else:
    # 将模型的响应添加到对话历史中,保持上下文连贯性
    # 这一步很重要,确保后续调用能理解完整的对话历史
    messages.append({
        "role": "assistant",
        "content": response.content if response.content else "",
        "tool_calls": response.tool_calls if hasattr(response, 'tool_calls') else None
    })

    print("=====DeepSeek回复=====")
    print(response)

    # 检查模型是否要求调用工具函数
    # tool_calls不为None表示模型决定调用工具
    if hasattr(response, 'tool_calls') and response.tool_calls is not None:
        # 获取第一个工具调用信息(当前示例中只会有一个)
        tool_call = response.tool_calls[0]

        # 检查是否要调用sum_number函数
        if tool_call.function.name == "sum_number":
            try:
                # 解析函数参数(从JSON字符串转换为Python字典)
                # 模型返回的参数是JSON格式的字符串
                args = json.loads(tool_call.function.arguments)

                # 验证参数是否正确
                # 确保包含numbers字段且其类型为列表
                if "numbers" not in args or not isinstance(args["numbers"], list):
                    raise ValueError("函数参数不正确,需要包含numbers数组")

                # 调用自定义的sum_number函数计算结果
                result = sum_number(args["numbers"])

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

                # 将函数调用结果添加到对话历史中
                # 这样模型就能基于这个结果生成自然语言回答
                messages.append(
                    {
                        "role": "tool",  # 角色为tool,表示这是工具返回的结果
                        "tool_call_id": tool_call.id,  # 工具调用ID,用于关联调用和结果
                        "name": "sum_number",  # 对应的函数名称
                        "content": str(result)  # 函数返回的结果,转换为字符串
                    }
                )

                print("=====对话历史=====")
                print(messages)

                # 再次调用模型,让其基于工具返回的结果生成最终的自然语言回答
                final_response = get_completion(messages)
                if final_response:
                    print("=====最终回复=====")
                    print(final_response.content)

            # 处理JSON解析错误
            except json.JSONDecodeError:
                print("解析函数参数失败,JSON格式错误")

            # 处理参数验证错误
            except ValueError as e:
                print(f"参数错误: {e}")

            # 处理其他可能的异常
            except Exception as e:
                print(f"处理函数调用时发生错误: {e}")
    else:
        # 如果模型没有调用工具,直接输出回复
        print("=====最终回复=====")
        print(response.content)
=====DeepSeek回复=====
ChatCompletionMessage(content='我来帮您计算您和舅舅的年龄总和。', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=[ChatCompletionMessageFunctionToolCall(id='call_00_Yj5EJcnB3u6JZffeqKtPhb8E', function=Function(arguments='{"numbers": [18, 38]}', name='sum_number'), type='function', index=0)])
=====函数返回=====
56
=====对话历史=====
[{'role': 'system', 'content': '你是一个数学家,你可以计算任何算式。如果需要计算一组数的和,可以调用sum_number函数。'}, {'role': 'user', 'content': '我今年18岁,我的舅舅今年38岁,我的爷爷今年72岁,我和舅舅一共多少岁了?'}, {'role': 'assistant', 'content': '我来帮您计算您和舅舅的年龄总和。', 'tool_calls': [ChatCompletionMessageFunctionToolCall(id='call_00_Yj5EJcnB3u6JZffeqKtPhb8E', function=Function(arguments='{"numbers": [18, 38]}', name='sum_number'), type='function', index=0)]}, {'role': 'tool', 'tool_call_id': 'call_00_Yj5EJcnB3u6JZffeqKtPhb8E', 'name': 'sum_number', 'content': '56'}]
=====最终回复=====
根据您提供的信息:
- 您今年18岁
- 您的舅舅今年38岁

您和舅舅的年龄总和是 **56岁**。
=====DeepSeek回复=====
ChatCompletionMessage(content='我来计算 30*8 的结果。\n\n30 × 8 = 240\n\n所以 30*8 等于 240。', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=None)
=====最终回复=====
我来计算 30*8 的结果。

30 × 8 = 240

所以 30*8 等于 240。

JSON Schema是一个用于描述JSON数据格式和结构的元数据标准。它用于验证、注释以及操控 JSON文档。JSON Schema本身是用JSON格式表示的,提供了一种灵活的方式来校验数据的结构,包括对象属性的类型、数组长度、数字和字符串的值范围等等。

参考⽹址:https://json-schema.org/learn/getting-started-step-by-step

Function 的调用

这里以一个查询某个地点附近某些信息的需求为例。

定义本地函数

这里我们需要定义自己的本地函数,不再使用Python的库函数了。

下面的代码,我们定义了两个函数。

  • get_location_coordinate用于查询某个地点的地理坐标。
  • search_nearby_pois用于查询地理坐标附近的某些信息(取决于用户输入的Keyword)

这是用的高德地图的开放接口,在使用本例之前,你需要先去高德地图开放接口的官网申请一个key,免费的。这里就不过多介绍了。

https://console.amap.com/dev/key/app

image

完整的案例代码

# 导入所需库
from openai import OpenAI
import os
from dotenv import load_dotenv
import requests
import json

# 加载环境变量,从.env文件中读取配置
load_dotenv()

# 初始化DeepSeek客户端
client = OpenAI(
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com/v1"
)

# 从环境变量获取高德地图API密钥
amap_key = os.getenv("AMAP_KEY")


def get_location_coordinate(location, city="北京"):
    """
    根据POI名称和城市名,获取该地点的经纬度坐标

    参数:
        location (str): POI名称,中文
        city (str): 城市名,中文,默认为北京

    返回:
        dict: 包含地点详细信息的字典,包括经纬度;如果未找到则返回None
    """
    # 构建高德地图地点搜索API的URL
    url = f"https://restapi.amap.com/v5/place/text?key={amap_key}&keywords={location}&region={city}"
    print(f"请求URL: {url}")

    # 发送GET请求
    r = requests.get(url)
    # 解析JSON响应
    result = r.json()

    # 检查响应中是否包含有效POI信息
    if "pois" in result and result["pois"]:
        return result["pois"][0]
    return None


def search_nearby_pois(longitude, latitude, keyword):
    """
    根据给定的经纬度坐标,搜索附近的POI(兴趣点)

    参数:
        longitude (str): 中心点的经度
        latitude (str): 中心点的纬度
        keyword (str): 目标POI的关键字,如"酒店"、"咖啡馆"等

    返回:
        str: 格式化的附近POI信息,包括名称、地址和距离;如果未找到则返回空字符串
    """
    # 构建高德地图周边搜索API的URL
    url = f"https://restapi.amap.com/v5/place/around?key={amap_key}&keywords={keyword}&location={longitude},{latitude}"
    print(f"请求URL: {url}")

    # 发送GET请求
    r = requests.get(url)
    # 解析JSON响应
    result = r.json()

    # 构建返回结果字符串
    ans = ""
    # 检查响应中是否包含有效POI信息
    if "pois" in result and result["pois"]:
        # 最多返回3个结果
        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}米\n\n"
    return ans


def get_completion(messages, model="deepseek-chat"):
    """
    调用DeepSeek API获取对话完成结果,支持工具调用

    参数:
        messages (list): 对话历史消息列表
        model (str): 要使用的模型名称,默认为"deepseek-chat"

    返回:
        对象: DeepSeek API返回的消息对象
    """
    response = client.chat.completions.create(
        model=model,
        messages=messages,
        temperature=0,  # 温度设为0,使输出更确定
        max_tokens=1024,  # 最大令牌数限制
        # 定义可调用的工具函数
        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"],  # 必需的参数
                    }
                }
            }
        ]
    )
    return response.choices[0].message


# 测试查询:北京故宫附近的酒店
prompt = "北京故宫附近的酒店"
# 也可以测试其他查询,如"上海陆家嘴附近的咖啡"
# prompt = "上海陆家嘴附近的咖啡"

# 初始化对话消息列表
messages = [
    {"role": "system", "content": "你是一个地图通,你可以找到任何地址。"},
    {"role": "user", "content": prompt}
]

# 获取初始的AI响应
response = get_completion(messages)

# 处理可能出现的content为None的情况
if response.content is None:
    response.content = ""

# 将AI的回复加入到对话历史中
messages.append(response)
print("=====DeepSeek回复=====")
print(response)

# 如果AI返回了工具调用请求,则执行相应的函数
while response.tool_calls is not None:
    # 处理可能的多个工具调用
    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("调用函数: get_location_coordinate")
            result = get_location_coordinate(**args)
        elif tool_call.function.name == "search_nearby_pois":
            print("调用函数: 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)  # 将结果转换为字符串
        })

    # 根据新的对话历史获取AI的下一次响应
    response = get_completion(messages)

    # 处理可能的content为None的情况
    if response.content is None:
        response.content = ""

    # 将新的响应添加到对话历史
    messages.append(response)

# 打印最终的回答结果
print("=====最终回复=====")
print(response.content)

完整的输出结果

D:\install\tools\Anaconda312\python.exe D:\learn\learncode\Agent\lzs\03多函数调用.py 
=====DeepSeek回复=====
ChatCompletionMessage(content='', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=[ChatCompletionMessageFunctionToolCall(id='call_00_ivcf3haft0GxTXQqAEKMDpiI', function=Function(arguments='{"location": "故宫", "city": "北京"}', name='get_location_coordinate'), type='function', index=0)])
函数参数: {'location': '故宫', 'city': '北京'}
调用函数: get_location_coordinate
请求URL: https://restapi.amap.com/v5/place/text?key=2ede45246000c50dd3cb207e1b0bf068&keywords=故宫&region=北京
=====函数返回=====
{'name': '故宫博物院(暂停开放)', 'id': 'B000A8UIN8', 'location': '116.397029,39.917839', 'type': '风景名胜;风景名胜;世界遗产|科教文化服务;博物馆;博物馆', 'typecode': '110201|140100', 'pname': '北京市', 'cityname': '北京市', 'adname': '东城区', 'address': '景山前街4号', 'pcode': '110000', 'citycode': '010', 'adcode': '110101', 'distance': '', 'parent': ''}
函数参数: {'longitude': '116.397029', 'latitude': '39.917839', 'keyword': '酒店'}
调用函数: search_nearby_pois
请求URL: https://restapi.amap.com/v5/place/around?key=2ede45246000c50dd3cb207e1b0bf068&keywords=酒店&location=116.397029,39.917839
=====函数返回=====
北京东华庭院四合院
北池子大街50号
距离:519米

北京京古四合院
北池子大街三条6号
距离:521米

北京福禄久四合院
福禄巷9号
距离:666米


=====最终回复=====
根据搜索结果,北京故宫附近有以下几家酒店:

1. **北京东华庭院四合院** - 距离故宫519米
   - 地址:北池子大街50号

2. **北京京古四合院** - 距离故宫521米  
   - 地址:北池子大街三条6号

3. **北京福禄久四合院** - 距离故宫666米
   - 地址:福禄巷9号

这几家酒店都是四合院风格的住宿,距离故宫博物院非常近,步行即可到达。如果您需要更多酒店选择或者特定类型的酒店,请告诉我您的具体需求。

进程已结束,退出代码为 0

当然我们也可以通过一个12306的爬虫案例来演示下多Function Calling的效果。

案例涉及到的爬虫网站:https://spidertools.cn/#/curl2Request

image

完整的案例代码

# 导入项目依赖库
from openai import OpenAI  # OpenAI API客户端,用于调用大模型
import os  # 用于读取环境变量
import json  # 用于解析JSON数据(工具调用参数、API响应)
import requests  # 用于发送HTTP请求(调用12306接口查票)
from dotenv import load_dotenv  # 从.env文件加载环境变量(如DeepSeek API密钥)
import pandas as pd  # 用于格式化输出车票数据(表格形式)
from datetime import datetime  # 用于处理日期(获取当前日期、计算查询日期)

# 加载.env文件中的环境变量(需确保文件中包含DEEPSEEK_API_KEY)
load_dotenv()

# 初始化DeepSeek客户端
client = OpenAI(
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com/v1"
)


def check_tick(date, start, end):
    """
    调用12306官方接口,查询指定日期、出发站-终点站的列车车票信息

    参数:
        date (str): 查询日期,格式需为"YYYY-MM-DD"(如2024-10-01)
        start (str): 出发站编码(12306内部编码,如北京=BJP、上海=SHH)
        end (str): 终点站编码(同出发站编码规则,如天津=TJP、广州=GZQ)

    返回:
        pd.DataFrame: 包含列车信息的表格,列包括【车次、出发时间、到站时间、是否可以预定】
                     (G字头高铁额外包含特等座/一等座/二等座状态,但原代码暂未在返回中显示)
    """
    # 12306车票查询接口(官方公开接口,参数需严格匹配格式)
    # 注意:URL需拼接为完整字符串,原代码的换行会导致语法错误,此处已修复
    url = (
        f'https://kyfw.12306.cn/otn/leftTicket/queryG?'
        f'leftTicketDTO.train_date={date}&'
        f'leftTicketDTO.from_station={start}&'
        f'leftTicketDTO.to_station={end}&'
        f'purpose_codes=ADULT'  # purpose_codes=ADULT表示查询成人票
    )

    # 请求头(模拟浏览器请求,避免被12306反爬机制拦截)
    headers = {
        "Accept": "*/*",  # 接受所有响应类型
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",  # 语言偏好
        "Cache-Control": "no-cache",  # 禁用缓存
        "Connection": "keep-alive",  # 保持长连接
        "If-Modified-Since": "0",  # 强制获取最新数据
        "Pragma": "no-cache",  # 禁用缓存(兼容旧协议)
        "Referer": "https://kyfw.12306.cn/otn/leftTicket/init?linktypeid=dc",  # Referer验证(模拟从12306首页跳转)
        "Sec-Fetch-Dest": "empty",  # Fetch API目标类型
        "Sec-Fetch-Mode": "cors",  # 跨域请求模式
        "Sec-Fetch-Site": "same-origin",  # 同源请求
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
        # 浏览器标识
        "X-Requested-With": "XMLHttpRequest",  # 模拟AJAX请求
        "sec-ch-ua": "\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"",  # Chrome浏览器UA信息
        "sec-ch-ua-mobile": "?0",  # 非移动设备
        "sec-ch-ua-platform": "\"macOS\""  # 操作系统
    }

    # Cookie(12306身份验证关键,原代码中部分字段为空,需注意:
    # 实际使用时需补充实时Cookie(如JSESSIONID、BIGipServerotn),否则请求会失败)
    cookies = {
        "_uab_collina": "",  # 客户端标识(需实时获取)
        "JSESSIONID": "",  # 会话ID(核心,过期需重新获取)
        "_jc_save_wfdc_flag": "dc",  # 12306固定标识
        "_jc_save_fromStation": "%%u6C99%",  # 出发站缓存(原代码值不完整,需替换为编码,如%B1%B1%BE%A9%B3%C7=BJP)
        "guidesStatus": "off",  # 引导状态
        "highContrastMode": "",  # 高对比度模式
        "cursorStatus": "off",  # 光标状态
        "BIGipServerotn": "..0000",  # 负载均衡标识(需实时获取)
        "BIGipServerpassport": "..0000",  # 通行证负载标识(需实时获取)
        "route": "",  # 路由标识(需实时获取)
        "_jc_save_toStation": "%u4E0Au6D77%2CSHH",  # 终点站缓存(示例:上海=SHH,编码需匹配)
        "_jc_save_fromDate": "",  # 出发日期缓存
        "_jc_save_toDate": ""  # 到达日期缓存
    }

    # 创建会话对象(保持请求上下文,模拟浏览器会话)
    session = requests.session()
    # 发送GET请求查询车票(携带 headers 和 cookies 避免反爬)
    res = session.get(url, headers=headers, cookies=cookies)
    # 解析响应为JSON格式(12306接口返回数据为JSON结构)
    data = res.json()

    # 提取车票核心数据(12306返回的result字段是列表,每个元素代表一趟列车)
    result = data["data"]["result"]
    # 用于存储格式化后的列车信息
    train_list = []

    # 遍历每一趟列车,解析关键信息
    for train_info in result:
        # 12306返回的列车信息用"|"分隔,先替换"有/无"为"Yes/No"(统一状态格式),再拆分
        train_detail = train_info.replace('有', 'Yes').replace('无', 'No').split('|')

        # 提取车次(如G1、D234,在拆分后的列表第3位,索引从0开始)
        train_number = train_detail[3]

        # 区分G字头高铁和其他车次(高铁额外提取座位信息,非高铁只提取基础信息)
        if 'G' in train_number:  # G字头高铁
            departure_time = train_detail[8]  # 出发时间(列表第8位)
            arrival_time = train_detail[9]  # 到达时间(列表第9位)
            president_seat = train_detail[25]  # 特等座状态(列表第25位)
            first_class = train_detail[31]  # 一等座状态(列表第31位)
            second_class = train_detail[30]  # 二等座状态(列表第30位)

            # 构造单趟高铁的信息字典(原代码暂未将座位状态加入返回,此处保留字段便于后续扩展)
            train_dict = {
                '车次': train_number,
                '出发时间': departure_time,
                '到站时间': arrival_time,
                "是否可以预定": train_detail[11],  # 预定状态(列表第11位)
                # "特等座": president_seat,  # 如需显示座位状态,可取消注释
                # "一等座": first_class,
                # "二等座": second_class
            }
            train_list.append(train_dict)

        else:  # 非G字头车次(如D字头动车、Z字头直达车等)
            departure_time = train_detail[8]  # 出发时间
            arrival_time = train_detail[9]  # 到达时间

            # 构造非高铁的信息字典
            train_dict = {
                '车次': train_number,
                '出发时间': departure_time,
                '到站时间': arrival_time,
                "是否可以预定": train_detail[11]
            }
            train_list.append(train_dict)

    # 将列车列表转换为DataFrame(表格形式,便于阅读和后续处理)
    train_df = pd.DataFrame(train_list)
    return train_df


def check_date():
    """
    获取当前系统日期(无参数,仅返回当前日期)

    返回:
        datetime.date: 当前日期对象(格式如2024-09-03),需后续转换为"YYYY-MM-DD"字符串用于查票
    """
    # 获取当前本地时间的日期部分(剥离时分秒)
    today = datetime.now().date()
    return today


def get_completion(messages, model="deepseek-chat"):
    """
    调用DeepSeek大模型API,支持工具调用(触发check_tick或check_date函数)

    参数:
        messages (list): 对话历史列表,包含system/user/tool角色的消息
        model (str): 调用的大模型,默认deepseek-chat(DeepSeek的通用模型)

    返回:
        ChatCompletionMessage: 大模型的响应对象,包含content(自然语言回复)或tool_calls(工具调用指令)
    """
    response = client.chat.completions.create(
        model=model,  # 指定模型
        messages=messages,  # 对话历史
        temperature=0,  # 温度=0,输出更确定(避免随机结果)
        max_tokens=1024,  # 最大生成 tokens 限制(防止输出过长)
        tools=[  # 定义可调用的工具函数列表
            # 工具1:check_tick(查票函数)
            {
                "type": "function",
                "function": {
                    "name": "check_tick",
                    "description": "给定日期、出发站编码、终点站编码,查询12306列车票信息",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "date": {
                                "type": "string",
                                "description": "查询日期,必须是'YYYY-MM-DD'格式(如2024-10-01)"
                            },
                            "start": {
                                "type": "string",
                                "description": "出发站的12306内部编码(如北京=BJP、上海=SHH)"
                            },
                            "end": {
                                "type": "string",
                                "description": "终点站的12306内部编码(如天津=TJP、广州=GZQ)"
                            }
                        },
                        # 补充原代码缺失的"required"字段(大模型需明确必传参数,否则可能漏传)
                        "required": ["date", "start", "end"]
                    }
                }
            },
            # 工具2:check_date(获取当前日期函数)
            {
                "type": "function",
                "function": {
                    "name": "check_date",
                    "description": "获取当前系统日期(无需参数),用于计算查询日期(如后天)",
                    "parameters": {
                        "type": "object",
                        "properties": {}  # 无参数,properties为空
                    },
                    "required": []  # 无必传参数
                }
            }
        ]
    )
    # 返回大模型响应中的第一条消息(通常只有一条)
    return response.choices[0].message


# ---------------------- 主程序逻辑 ----------------------
# 用户查询需求(自然语言):查询后天从北京到上海的车票
user_prompt = "查询后天 北京到上海的票"

# 初始化对话历史(包含系统指令和用户需求)
messages = [
    # system角色:定义大模型的身份和能力
    {"role": "system", "content": "你是12306票务查询助手,可调用工具查询列车票,需先确认日期和站点编码"},
    {"role": "user", "content": user_prompt}  # user角色:用户的查询需求
]

# 第一次调用大模型:获取初始响应(可能触发工具调用,如先查当前日期)
response = get_completion(messages)

# 处理API的潜在bug:部分情况下response.content为None,需设为空字符串避免报错
if response.content is None:
    response.content = ""

# 将大模型的初始响应加入对话历史(保持上下文连续)
messages.append(response)
print("=====DeepSeek 初始响应(可能包含工具调用指令)=====")
print(response)

# 循环处理工具调用:如果大模型返回tool_calls,就执行对应函数,直到无工具调用(得到最终回复)
while response.tool_calls is not None:
    # 支持大模型一次返回多个工具调用
    for tool_call in response.tool_calls:
        # 解析工具调用的参数(JSON字符串转字典)
        tool_args = json.loads(tool_call.function.arguments)
        print(f"\n----- 当前工具调用参数 -----")
        print(f"工具名称:{tool_call.function.name}")
        print(f"参数:{tool_args}")

        # 根据工具名称执行对应函数
        if tool_call.function.name == "check_tick":
            print(f"执行工具:调用 check_tick(查询车票)")
            # 解包参数调用查票函数(需确保参数包含date/start/end)
            tool_result = check_tick(**tool_args)

        elif tool_call.function.name == "check_date":
            print(f"执行工具:调用 check_date(获取当前日期)")
            # 调用获取当前日期函数(无参数)
            tool_result = check_date()

        # 打印工具执行结果
        print("\n===== 工具执行返回结果 =====")
        print(tool_result)

        # 将工具调用结果加入对话历史(供大模型后续分析,需包含tool_call_id关联调用)
        messages.append({
            "tool_call_id": tool_call.id,  # 工具调用唯一ID(关联调用和结果)
            "role": "tool",  # role角色:工具返回的结果
            "name": tool_call.function.name,  # 工具名称(标识是哪个工具的结果)
            "content": str(tool_result)  # 工具结果转字符串(大模型仅接受文本格式)
        })

    # 再次调用大模型:基于更新后的对话历史(包含工具结果),获取下一轮响应
    response = get_completion(messages)

    # 处理content为None的bug
    if response.content is None:
        response.content = ""

    # 打印第二轮响应(可能继续触发工具调用,或返回最终结果)
    print("\n=====DeepSeek 后续响应(基于工具结果)=====")
    print(response)

    # 将第二轮响应加入对话历史
    messages.append(response)

# 打印最终回复(无工具调用时,大模型返回的自然语言回答)
print("\n===== 最终票务查询结果 =====")
print(response.content)

完整的输出结果

点击查看打印结果
D:\install\tools\Anaconda312\python.exe D:\learn\learncode\Agent\lzs\04-12306爬虫案例.py 
=====DeepSeek 初始响应(可能包含工具调用指令)=====
ChatCompletionMessage(content='', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=[ChatCompletionMessageFunctionToolCall(id='call_00_xQ5UQHwQXA8iHA8SAeO9AeDy', function=Function(arguments='{}', name='check_date'), type='function', index=0)])

----- 当前工具调用参数 -----
工具名称:check_date
参数:{}
执行工具:调用 check_date(获取当前日期)

===== 工具执行返回结果 =====
2025-09-04

=====DeepSeek 后续响应(基于工具结果)=====
ChatCompletionMessage(content='', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=[ChatCompletionMessageFunctionToolCall(id='call_00_KtDh1eoO9IIoH0DcXhiMyseK', function=Function(arguments='{"date": "2025-09-06", "start": "BJP", "end": "SHH"}', name='check_tick'), type='function', index=0)])

----- 当前工具调用参数 -----
工具名称:check_tick
参数:{'date': '2025-09-06', 'start': 'BJP', 'end': 'SHH'}
执行工具:调用 check_tick(查询车票)

===== 工具执行返回结果 =====
       车次   出发时间   到站时间 是否可以预定
0    G103  06:20  11:58      Y
1      G1  07:00  11:29      Y
2    G105  07:17  13:03      Y
3    G107  07:25  13:12      Y
4      G3  07:40  12:32      Y
5    G109  07:45  13:49      Y
6      G3  08:00  12:32      Y
7    G111  08:16  14:11      Y
8    G113  08:39  15:01      Y
9      G5  09:00  13:37      Y
10   G117  09:20  14:55      Y
11   G119  09:24  15:31      Y
12     G7  10:00  14:35      Y
13   G121  10:05  15:41      Y
14   G123  10:14  16:26      Y
15   G125  10:48  16:50      Y
16     G9  11:00  15:37      Y
17   G129  11:18  17:38      Y
18   G131  11:27  17:22      Y
19   G133  11:50  18:02      Y
20    G11  12:00  16:38      Y
21   1461  12:01  06:45      Y
22  G2573  12:08  19:58      Y
23   G135  12:12  18:21      Y
24   G137  12:47  18:59      Y
25    G13  13:00  17:35      Y
26   G139  13:04  19:06      Y
27   G141  13:34  20:08      Y
28    G15  14:00  18:32      Y
29   G143  14:09  20:08      Y
30   G145  14:14  20:12      Y
31   G147  14:27  20:43      Y
32    G17  15:00  19:34      Y
33   G149  15:08  21:10      Y
34   G151  15:49  22:12      Y
35    G19  16:00  20:28      Y
36   G153  16:30  22:27      Y
37   G157  16:53  23:12      Y
38    G21  17:00  21:18      Y
39   G159  17:19  23:18      Y
40   G161  17:33  23:44      Y
41    G23  18:00  22:43      Y
42    G25  18:04  22:58      Y
43    G27  19:00  23:35      Y
44    D17  19:13  07:31      Y
45     D9  19:36  08:00      Y
46   T109  20:03  11:02      Y
47     D5  21:21  09:27      Y

=====DeepSeek 后续响应(基于工具结果)=====
ChatCompletionMessage(content='根据查询结果,后天(2025年9月6日)从北京到上海有以下列车票信息:\n\n**高铁列车(G字头):**\n- G103:06:20出发,11:58到达\n- G1:07:00出发,11:29到达  \n- G105:07:17出发,13:03到达\n- G107:07:25出发,13:12到达\n- G3:07:40出发,12:32到达\n- ...(共40多趟高铁列车)\n\n**动车列车(D字头):**\n- D17:19:13出发,次日07:31到达\n- D9:19:36出发,次日08:00到达\n- D5:21:21出发,次日09:27到达\n\n**特快列车(T字头):**\n- T109:20:03出发,次日11:02到达\n\n**普快列车:**\n- 1461:12:01出发,次日06:45到达\n\n所有车次目前都可以预定(显示"Y")。您可以根据出行时间需求选择合适的车次。', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=None)

===== 最终票务查询结果 =====
根据查询结果,后天(2025年9月6日)从北京到上海有以下列车票信息:

**高铁列车(G字头):**
- G103:06:20出发,11:58到达
- G1:07:00出发,11:29到达  
- G105:07:17出发,13:03到达
- G107:07:25出发,13:12到达
- G3:07:40出发,12:32到达
- ...(共40多趟高铁列车)

**动车列车(D字头):**
- D17:19:13出发,次日07:31到达
- D9:19:36出发,次日08:00到达
- D5:21:21出发,次日09:27到达

**特快列车(T字头):**
- T109:20:03出发,次日11:02到达

**普快列车:**
- 1461:12:01出发,次日06:45到达

所有车次目前都可以预定(显示"Y")。您可以根据出行时间需求选择合适的车次。

进程已结束,退出代码为 0

自动生成tools工具描述

上面案例中我们在函数仓库中对每个Function定义的时候都需要写比较复杂得JSON Schema,其实我们也可以通过自定义的方法来快速的生成,比如这个auto_functions,

from typing import List, Callable, Any
import inspect

def auto_functions(func_list: List[Callable]) -> List[dict]:
    """
    自动生成函数描述字典列表,用于构建工具调用规范
    
    参数:
        func_list: 包含多个函数的列表
        
    返回:
        符合OpenAI工具调用规范的字典列表
    """
    tools_description = []
    
    for func in func_list:
        # 获取函数的签名信息
        sig = inspect.signature(func)
        func_params = sig.parameters
        
        # 构建参数描述结构
        parameters = {
            'type': 'object',
            'properties': {},
            'required': []  # 存储必填参数名
        }
        
        for param_name, param in func_params.items():
            # 获取参数类型(优先使用注解,默认为'string')
            param_type = 'string'
            if param.annotation is not param.empty:
                # 提取基本类型名称(如<class 'str'> -> 'str')
                type_name = param.annotation.__name__
                # 映射Python类型到JSON类型
                param_type = {
                    'str': 'string',
                    'int': 'integer',
                    'float': 'number',
                    'bool': 'boolean',
                    'list': 'array',
                    'dict': 'object'
                }.get(type_name, type_name.lower())
            
            # 构建参数描述
            param_description = ""
            if func.__doc__:
                # 从函数文档中提取参数描述(简单实现)
                doc_lines = func.__doc__.split('\n')
                for line in doc_lines:
                    if f":param {param_name}:" in line:
                        param_description = line.split(":", 2)[-1].strip()
                        break
            
            # 添加到参数属性
            parameters['properties'][param_name] = {
                'description': param_description,
                'type': param_type
            }
            
            # 检查是否为必填参数(无默认值)
            if param.default == param.empty:
                parameters['required'].append(param_name)
        
        # 构建函数描述字典
        func_dict = {
            "type": "function",
            "function": {
                "name": func.__name__,
                "description": (func.__doc__ or "").split('\n')[0].strip(),
                "parameters": parameters
            }
        }
        tools_description.append(func_dict)
    
    return tools_description

def machine_learning_1() -> str:
    """
    解释机器学习是什么
    
    返回:
        机器学习的基础定义
    """
    return """机器学习是人工智能的一个分支,研究计算机如何自动从数据中学习,
提升性能并做出预测。它通过算法让计算机提炼知识,优化任务执行,而无需明确编程。"""

def check_ticket(date: str, start: str, end: str) -> str:
    """
    查询是否可以订票
    
    参数:
        date: 出发日期(格式:YYYY-MM-DD)
        start: 出发站名称
        end: 终点站名称
        
    返回:
        可预订车次信息的JSON字符串
    """
    # 实际实现中这里会有查询逻辑
    return f"日期:{date}, 从{start}到{end}的车次信息"

# 函数列表
functions_list = [machine_learning_1, check_ticket]

# 生成工具描述
tools = auto_functions(functions_list)

# 打印结果
import json
print(json.dumps(tools, indent=2, ensure_ascii=False))

数据的结果为

[
    {
        "type": "function",
        "function": {
            "name": "machine_learning_1",
            "description": "解释机器学习是什么",
            "parameters": {
                "type": "object",
                "properties": {},
                "required": []
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "check_tick",
            "description": "查询是否可以订票\n :param date: 日期\n :param start: 出发站\n :param end:终点站\n :return: 可以预定的车次信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "date": {
                        "description": "",
                        "type": "Any"
                    },
                    "start": {
                        "description": "",
                        "type": "Any"
                    },
                    "end": {
                        "description": "",
                        "type": "Any"
                    }
                },
                "required": []
            }
        }
    }
]

通过JSON格式化的工具来查看

image

from openai import OpenAI
import os
from dotenv import load_dotenv

# 加载环境变量(包含OpenAI API密钥)
load_dotenv()

# 创建OpenAI客户端实例
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

def get_completion(user_input: str, model: str = "gpt-4", temperature: float = 0) -> str:
    """
    使用OpenAI API将函数定义转换为JSON Schema格式
    
    参数:
        user_input: 包含函数定义的用户输入字符串
        model: 使用的OpenAI模型,默认为gpt-4
        temperature: 控制生成结果的随机性,0表示最确定性
        
    返回:
        OpenAI API返回的JSON Schema格式字符串
    """
    # 示例JSON Schema结构,用于指导模型转换
    examples = """
    {
        "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"]
            }
        }
    }
    """
    
    # 构建提示词,指导模型进行转换
    prompt = f"""
    任务:将用户提供的函数定义转换为JSON Schema格式
    
    示例格式:
    {examples}
    
    用户提供的函数定义:
    {user_input}
    
    转换要求:
    1. 只返回JSON Schema格式的结果,不要包含任何额外文本
    2. 确保参数描述清晰准确
    3. 正确处理默认值参数(有默认值的参数不是必需的)
    4. 使用合理的参数类型(string, integer, boolean等)
    """
    
    # 创建消息列表(目前只包含用户消息)
    messages = [{"role": "user", "content": prompt}]
    
    # 调用OpenAI API
    response = client.chat.completions.create(
        model=model,
        messages=messages,
        temperature=temperature  # 控制生成结果的随机性
    )
    
    # 返回模型生成的响应内容
    return response.choices[0].message.content

# 示例函数定义(用户输入)
user_input = """
def get_location_coordinate(location, city="北京"):
    \"\"\"
    根据POI名称获取经纬度坐标
    
    参数:
        location: POI名称(字符串)
        city: 所在城市(字符串),默认为"北京"
    
    返回:
        包含POI信息的字典,如果未找到则返回None
    \"\"\"
    import requests
    amap_key = "your_api_key_here"  # 实际使用中应从环境变量获取
    url = f"https://restapi.amap.com/v5/place/text?key={amap_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
"""

# 调用函数并打印结果
result = get_completion(user_input)
print("生成的JSON Schema:")
print(result)

输出的效果如下

改写后的JSON schema形式如下:
{
    "type": "function",
    "function": {
        "name": "get_location_coordinate",
        "description": "根据POI名称和城市名,获取POI的经纬度坐标",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "POI名称,必须是中文"
                },
                "city": {
                    "type": "string",
                    "description": "POI所在的城市名,必须是中文,默认为北京",
                    "default": "北京"
                }
            },
            "required": [
                "location"
            ]
        }
    }
}

Function Calling 连接数据库

操作数据库也是一个非常常见的需求,我们来看看通过 Function Calling如何实现数据库的操作

from openai import OpenAI
import os
import pymysql
import json
from dotenv import load_dotenv
from mysql.connector import Error

# 加载环境变量(包含OpenAI API密钥)
load_dotenv()

# 创建OpenAI客户端实例
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

# 数据库表结构定义
database_schema_string = """
CREATE TABLE goods(
    id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT NOT NULL,
    name VARCHAR(150) NOT NULL,
    cate_name VARCHAR(40) NOT NULL,
    brand_name VARCHAR(40) NOT NULL,
    price DECIMAL(10,3) NOT NULL DEFAULT 0,
    is_show BIT NOT NULL DEFAULT 1,
    is_saleoff BIT NOT NULL DEFAULT 0
);
"""

# 示例数据插入语句(实际应用中应执行这些语句)
sample_data = """
INSERT INTO goods VALUES(0, 'r510vc 15.6英寸笔记本', '笔记本', '华硕', '3399', DEFAULT, DEFAULT);
INSERT INTO goods VALUES(0, 'y400n 14.0英寸笔记本电脑', '笔记本', '联想', '4999', DEFAULT, DEFAULT);
INSERT INTO goods VALUES(0, 'g150th 15.6英寸游戏本', '游戏本', '雷神', '8499', DEFAULT, DEFAULT);
INSERT INTO goods VALUES(0, 'x550cc 15.6英寸笔记本', '笔记本', '华硕', '2799', DEFAULT, DEFAULT);
INSERT INTO goods VALUES(0, 'x240 超极本', '超级本', '联想', '4880', DEFAULT, DEFAULT);
INSERT INTO goods VALUES(0, 'u330p 13.3英寸超极本', '超级本', '联想', '4299', DEFAULT, DEFAULT);
INSERT INTO goods VALUES(0, 'svp13226scb 触控超极本', '超级本', '索尼', '7999', DEFAULT, DEFAULT);
INSERT INTO goods VALUES(0, 'ipad mini 7.9英寸平板电脑', '平板电脑', '苹果', '1998', DEFAULT, DEFAULT);
INSERT INTO goods VALUES(0, 'ipad air 9.7英寸平板电脑', '平板电脑', '苹果', '3388', DEFAULT, DEFAULT);
INSERT INTO goods VALUES(0, 'ipad mini 配备 retina 显示屏', '平板电脑', '苹果', '2788', DEFAULT, DEFAULT);
INSERT INTO goods VALUES(0, 'ideacentre c340 20英寸一体电脑', '台式机', '联想', '3499', DEFAULT, DEFAULT);
INSERT INTO goods VALUES(0, 'vostro 3800-r1206 台式电脑', '台式机', '戴尔', '2899', DEFAULT, DEFAULT);
INSERT INTO goods VALUES(0, 'imac me086ch/a 21.5英寸一体电脑', '台式机', '苹果', '9188', DEFAULT, DEFAULT);
INSERT INTO goods VALUES(0, 'at7-7414lp 台式电脑 linux', '台式机', '宏碁', '3699', DEFAULT, DEFAULT);
INSERT INTO goods VALUES(0, 'z220sff f4f06pa工作站', '服务器/工作站', '惠普', '4288', DEFAULT, DEFAULT);
INSERT INTO goods VALUES(0, 'poweredge ii服务器', '服务器/工作站', '戴尔', '5388', DEFAULT, DEFAULT);
INSERT INTO goods VALUES(0, 'mac pro专业级台式电脑', '服务器/工作站', '苹果', '28888', DEFAULT, DEFAULT);
INSERT INTO goods VALUES(0, 'hmz-t3w 头戴显示设备', '笔记本配件', '索尼', '6999', DEFAULT, DEFAULT);
INSERT INTO goods VALUES(0, '商务双肩背包', '笔记本配件', '索尼', '99', DEFAULT, DEFAULT);
INSERT INTO goods VALUES(0, 'x3250 m4机架式服务器', '服务器/工作站', 'ibm', '6888', DEFAULT, DEFAULT);
"""

# 数据库连接配置
DB_CONFIG = {
    'host': '127.0.0.1',
    'port': 3320,
    'user': 'root',
    'password': '123456',
    'database': 'func'
}

# 全局数据库连接对象
connection = None
cursor = None

try:
    print("正在连接数据库...")
    # 创建数据库连接
    connection = pymysql.connect(**DB_CONFIG)
    print("数据库连接成功")
    
    # 创建游标对象
    cursor = connection.cursor()
    print("游标创建成功")
    
    # 在实际应用中,这里应该执行建表语句和插入数据
    # cursor.execute(database_schema_string)
    # cursor.execute(sample_data)
    # connection.commit()
    
except Error as err:
    print(f"数据库连接错误: {err}")
    # 如果连接失败,退出程序
    exit(1)

def get_sql_completion(messages: list, model: str = "gpt-4o") -> dict:
    """
    使用OpenAI API生成SQL查询语句
    
    参数:
        messages: 包含对话历史的消息列表
        model: 使用的OpenAI模型,默认为gpt-4o
        
    返回:
        OpenAI API返回的完整响应消息
    """
    # 定义函数调用工具规范
    tools = [
        {
            "type": "function",
            "function": {
                "name": "ask_database",
                "description": "使用此函数回答用户关于商品数据的问题,输出应为完整的SQL查询语句",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "query": {
                            "type": "string",
                            "description": f"""
根据用户问题生成的SQL查询语句。
使用以下数据库架构:
{database_schema_string}
注意事项:
1. 只返回纯文本SQL查询,不要包含任何额外文本
2. 仅使用MySQL支持的语法
3. 使用反引号包裹表名和列名防止冲突
4. 优先使用COUNT(), SUM()等聚合函数
                            """,
                        }
                    },
                    "required": ["query"],
                }
            }
        }
    ]
    
    # 调用OpenAI API
    response = client.chat.completions.create(
        model=model,
        messages=messages,
        temperature=0,  # 确保输出确定性
        tools=tools,
        tool_choice={"type": "function", "function": {"name": "ask_database"}}
    )
    
    return response.choices[0].message

def ask_database(query: str):
    """
    执行SQL查询并返回结果
    
    参数:
        query: 要执行的SQL查询语句
        
    返回:
        查询结果列表
    """
    try:
        print(f"执行SQL查询: {query}")
        # 执行SQL语句
        cursor.execute(query)
        # 获取所有结果
        records = cursor.fetchall()
        print(f"查询成功,返回 {len(records)} 条记录")
        return records
    except Exception as e:
        print(f"SQL执行错误: {e}")
        return f"SQL执行错误: {str(e)}"

def main():
    """主函数,处理用户查询流程"""
    # 用户查询
    prompt = "类型是笔记本的有多少台"
    
    # 初始化消息列表
    messages = [
        {"role": "system", "content": "你是一个SQL专家,帮助用户查询商品数据库"},
        {"role": "user", "content": prompt}
    ]
    
    print("\n==== 开始处理用户查询 ====")
    print(f"用户问题: {prompt}")
    
    # 第一步:获取SQL查询
    response = get_sql_completion(messages)
    print("\n==== 初始响应 ====")
    print(response)
    
    # 添加到消息历史
    if response.content is None:
        response.content = ""
    messages.append(response)
    
    # 检查是否有函数调用
    if response.tool_calls:
        print("\n==== 检测到函数调用 ====")
        tool_call = response.tool_calls[0]
        
        if tool_call.function.name == "ask_database":
            # 解析函数参数
            arguments = tool_call.function.arguments
            args = json.loads(arguments)
            
            print("\n==== 生成的SQL查询 ====")
            print(args["query"])
            
            # 执行SQL查询
            result = ask_database(args["query"])
            
            print("\n==== 数据库查询结果 ====")
            print(result)
            
            # 将结果添加到消息历史
            messages.append({
                "tool_call_id": tool_call.id,
                "role": "tool",
                "name": "ask_database",
                "content": str(result)
            })
            
            # 第二步:获取最终回答
            response = get_sql_completion(messages)
            print("\n==== 最终回复 ====")
            print(response.content)
    else:
        print("\n==== 直接回复 ====")
        print(response.content)

if __name__ == "__main__":
    try:
        main()
    finally:
        # 确保关闭数据库连接
        if cursor:
            cursor.close()
            print("游标已关闭")
        if connection:
            connection.close()
            print("数据库连接已关闭")

输出结果

<pymysql.cursors.Cursor object at 0x000002124A2F4E50>ChatCompletionMessage(content=None, refusal=None,role='assistant', audio=None, function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_gurapPqrFTm1iCBy3B04uqnJ', function=Function(arguments='{"query":"SELECT COUNT(*) FROMgoods WHERE cate_name = \'笔记本\';"}', name='ask_database',parameters=None), type='function')])
====Function Calling====
ChatCompletionMessage(content='', refusal=None, role='assistant',audio=None, function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_gurapPqrFTm1iCBy3B04uqnJ', function=Function(arguments='{"query":"SELECT COUNT(*) FROMgoods WHERE cate_name = \'笔记本\';"}', name='ask_database',parameters=None), type='function')])
====SQL====
SELECT COUNT(*) FROM goods WHERE cate_name = '笔记本';
====数据库查询结果====
((3,),)
====最终回复====
类型是笔记本的有3台。

国产大模型

  • Function Calling 会成为所有大模型的标配,支持它的越来越多
  • 不支持的大模型,某种程度上是不大可用的

百度文心大模型

官方文档:https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html

百度文心系列大模型有四个。按发布时间从早到晚是:

  • ERNIE-Bot - 支持 Function Calling
  • ERNIE-Bot-turbo
  • ERNIE-Bot 4.0
  • ERNIE-Bot 3.5 - 支持 Function Calling

参数大体和 OpenAI 一致。

ChatGLM

质谱大模型:官方文档:https://github.com/THUDM/ChatGLM3/tree/main/tools_using_demo

https://open.bigmodel.cn/console/overview

  • 最著名的国产开源大模型,生态最好
  • 早就使用 tools 而不是 function 来做参数,其它和 OpenAI 1106 版之前完全一样
import json
from zhipuai import ZhipuAI
from dotenv import load_dotenv

# 加载环境变量(包含智谱AI API密钥)
load_dotenv()

# 创建智谱AI客户端实例
client = ZhipuAI(api_key=os.getenv("ZHIPUAI_API_KEY"))

# 定义可用的函数工具列表
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_flight_number",
            "description": "根据始发地、目的地和日期,查询对应日期的航班号",
            "parameters": {
                "type": "object",
                "properties": {
                    "departure": {
                        "description": "出发地城市名称",
                        "type": "string"
                    },
                    "destination": {
                        "description": "目的地城市名称",
                        "type": "string"
                    },
                    "date": {
                        "description": "日期,格式为YYYY-MM-DD",
                        "type": "string",
                    }
                },
                "required": ["departure", "destination", "date"]
            },
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_ticket_price",
            "description": "查询某航班在某日的票价",
            "parameters": {
                "type": "object",
                "properties": {
                    "flight_number": {
                        "description": "航班号,如CA1234",
                        "type": "string"
                    },
                    "date": {
                        "description": "日期,格式为YYYY-MM-DD",
                        "type": "string",
                    }
                },
                "required": ["flight_number", "date"]
            },
        }
    },
    {
        "type": "function",
        "function": {
            "name": "sum_number",
            "description": "计算一组数的和",
            "parameters": {
                "type": "object",
                "properties": {
                    "numbers": {
                        "type": "array",
                        "items": {
                            "type": "number"
                        },
                        "description": "需要求和的一组数字"
                    }
                },
                "required": ["numbers"]
            }
        }
    }
]

def get_flight_number(date: str, departure: str, destination: str) -> dict:
    """
    根据日期、出发地和目的地查询航班号(模拟实现)
    
    参数:
        date: 日期字符串(YYYY-MM-DD)
        departure: 出发地城市名称
        destination: 目的地城市名称
        
    返回:
        包含航班号的字典
    """
    # 模拟航班数据
    flight_number = {
        "北京": {
            "上海": "CA1234",
            "广州": "CZ8321",
        },
        "上海": {
            "北京": "MU1233",
            "广州": "FM8123",
        },
        "广州": {
            "北京": "CZ3101",
            "上海": "CZ3537",
        }
    }
    
    # 检查城市是否存在
    if departure not in flight_number:
        return {"error": f"不支持从{departure}出发的航班查询"}
    
    if destination not in flight_number[departure]:
        return {"error": f"不支持从{departure}到{destination}的航班查询"}
    
    return {"flight_number": flight_number[departure][destination]}

def get_ticket_price(date: str, flight_number: str) -> dict:
    """
    根据日期和航班号查询票价(模拟实现)
    
    参数:
        date: 日期字符串(YYYY-MM-DD)
        flight_number: 航班号
        
    返回:
        包含票价的字典
    """
    # 模拟票价数据(实际应用中应该查询数据库或API)
    price_mapping = {
        "CA1234": "980",
        "CZ8321": "1200",
        "MU1233": "950",
        "FM8123": "1100",
        "CZ3101": "1350",
        "CZ3537": "1050"
    }
    
    if flight_number not in price_mapping:
        return {"error": f"未找到航班{flight_number}的票价信息"}
    
    return {"ticket_price": price_mapping[flight_number]}

def parse_function_call(model_response, messages: list):
    """
    处理函数调用结果,根据模型返回参数调用对应函数
    
    参数:
        model_response: 模型响应对象
        messages: 消息历史列表
        
    返回:
        处理后的模型响应消息
    """
    # 检查是否有函数调用
    if model_response.choices[0].message.tool_calls:
        tool_call = model_response.choices[0].message.tool_calls[0]
        args = tool_call.function.arguments
        
        function_result = {}
        
        # 根据函数名调用对应的函数
        if tool_call.function.name == "get_flight_number":
            function_result = get_flight_number(**json.loads(args))
            
        elif tool_call.function.name == "get_ticket_price":
            function_result = get_ticket_price(**json.loads(args))
            
        elif tool_call.function.name == "sum_number":
            args_dict = json.loads(args)
            function_result = {"sum": sum(args_dict["numbers"])}
        
        print("函数执行结果:", function_result)
        
        # 将函数执行结果添加到消息历史
        messages.append({
            "role": "tool",
            "content": json.dumps(function_result),
            "tool_call_id": tool_call.id
        })
        
        # 再次调用模型,将函数结果输入模型
        response = client.chat.completions.create(
            model="glm-4-flash",
            messages=messages,
            tools=tools,
        )
        
        # 将模型回复添加到消息历史
        messages.append(response.choices[0].message.model_dump())
        
        return response.choices[0].message
    
    # 如果没有函数调用,直接返回原响应
    return model_response.choices[0].message

def main():
    """主函数,处理用户查询流程"""
    # 初始化消息列表
    messages = []
    
    # 添加系统提示
    messages.append({
        "role": "system", 
        "content": "不要假设或猜测传入函数的参数值。如果用户的描述不明确,请要求用户提供必要信息"
    })
    
    # 添加用户查询
    user_query = "帮我查询1月23日,北京到广州的航班?"
    messages.append({"role": "user", "content": user_query})
    
    print(f"用户查询: {user_query}")
    
    # 第一次调用模型
    response = client.chat.completions.create(
        model="glm-4-flash",
        messages=messages,
        tools=tools,
    )
    
    # 将模型回复添加到消息历史
    messages.append(response.choices[0].message.model_dump())
    
    print("\n==== 第一次模型响应 ====")
    print(response.choices[0].message)
    
    # 处理函数调用
    result = parse_function_call(response, messages)
    
    print("\n==== 最终结果 ====")
    print(result)
    
    # 打印完整对话历史
    print("\n==== 完整对话历史 ====")
    for i, msg in enumerate(messages):
        print(f"{i+1}. {msg['role']}: {msg.get('content', 'No content')}")

if __name__ == "__main__":
    main()

多表查询

接下来我们看看如何通过 Function Calling 实现多表查询操作

from openai import OpenAI
import os
import pymysql
import json
from dotenv import load_dotenv
from mysql.connector import Error

# 加载环境变量(包含OpenAI API密钥)
load_dotenv()

# 创建OpenAI客户端实例
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

# 数据库表结构定义(仅包含表结构,不包含数据)
database_schema_string = """
-- 创建班级表
CREATE TABLE Classes (
    class_id INT PRIMARY KEY,
    class_name VARCHAR(100) NOT NULL
);

-- 创建学生表
CREATE TABLE Students (
    student_id INT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    class_id INT,
    FOREIGN KEY (class_id) REFERENCES Classes(class_id)
);

-- 创建成绩表
CREATE TABLE Scores (
    score_id INT PRIMARY KEY,
    student_id INT,
    subject VARCHAR(100) NOT NULL,
    score FLOAT NOT NULL,
    FOREIGN KEY (student_id) REFERENCES Students(student_id)
);
"""

# 示例数据插入语句(实际应用中应执行这些语句)
sample_data = """
INSERT INTO Classes (class_id, class_name) VALUES
(1, '一班'),
(2, '二班'),
(3, '三班'),
(4, '四班'),
(5, '五班');

INSERT INTO Students (student_id, name, class_id) VALUES
(1, '张三', 1),
(2, '李四', 1),
(3, '王五', 2),
(4, '赵六', 3),
(5, '钱七', 4);

INSERT INTO Scores (score_id, student_id, subject, score) VALUES
(1, 1, '数学', 85.5),
(2, 1, '英语', 90.0),
(3, 2, '数学', 78.0),
(4, 3, '英语', 88.5),
(5, 4, '数学', 92.0);
"""

# 数据库连接配置
DB_CONFIG = {
    'host': '127.0.0.1',
    'port': 3320,
    'user': 'root',
    'password': '123456',
    'database': 'func'
}

# 全局数据库连接对象
connection = None
cursor = None

try:
    print("正在连接数据库...")
    # 创建数据库连接
    connection = pymysql.connect(**DB_CONFIG)
    print("数据库连接成功")
    
    # 创建游标对象
    cursor = connection.cursor()
    print("游标创建成功")
    
    # 在实际应用中,这里应该执行建表语句和插入数据
    # 注意:这里只是示例,实际执行需要取消注释
    # cursor.execute("DROP TABLE IF EXISTS Scores, Students, Classes;")
    # cursor.execute(database_schema_string)
    # for statement in sample_data.split(';'):
    #     if statement.strip():
    #         cursor.execute(statement)
    # connection.commit()
    
except Error as err:
    print(f"数据库连接错误: {err}")
    # 如果连接失败,退出程序
    exit(1)

def get_sql_completion(messages: list, model: str = "gpt-3.5-turbo") -> dict:
    """
    使用OpenAI API生成SQL查询语句
    
    参数:
        messages: 包含对话历史的消息列表
        model: 使用的OpenAI模型,默认为gpt-3.5-turbo
        
    返回:
        OpenAI API返回的完整响应消息
    """
    # 定义函数调用工具规范
    tools = [
        {
            "type": "function",
            "function": {
                "name": "ask_database",
                "description": "使用此函数回答用户关于学生成绩数据的问题,输出应为完整的SQL查询语句",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "query": {
                            "type": "string",
                            "description": f"""
根据用户问题生成的SQL查询语句。
使用以下数据库架构:
{database_schema_string}
注意事项:
1. 只返回纯文本SQL查询,不要包含任何额外文本
2. 仅使用MySQL支持的语法
3. 使用反引号包裹表名和列名防止冲突
4. 使用JOIN连接相关表
5. 使用WHERE子句进行筛选
                            """,
                        }
                    },
                    "required": ["query"],
                }
            }
        }
    ]
    
    # 调用OpenAI API
    response = client.chat.completions.create(
        model=model,
        messages=messages,
        temperature=0,  # 确保输出确定性
        tools=tools,
        tool_choice={"type": "function", "function": {"name": "ask_database"}}
    )
    
    return response.choices[0].message

def ask_database(query: str):
    """
    执行SQL查询并返回结果
    
    参数:
        query: 要执行的SQL查询语句
        
    返回:
        查询结果列表
    """
    try:
        print(f"执行SQL查询: {query}")
        # 执行SQL语句
        cursor.execute(query)
        # 获取所有结果
        records = cursor.fetchall()
        print(f"查询成功,返回 {len(records)} 条记录")
        return records
    except Exception as e:
        print(f"SQL执行错误: {e}")
        return f"SQL执行错误: {str(e)}"

def main():
    """主函数,处理用户查询流程"""
    # 用户查询
    prompt = "查询一班的学生数学成绩是多少?"
    
    # 初始化消息列表
    messages = [
        {"role": "system", "content": "你是一个SQL专家,帮助用户查询学生成绩数据库"},
        {"role": "user", "content": prompt}
    ]
    
    print("\n==== 开始处理用户查询 ====")
    print(f"用户问题: {prompt}")
    
    # 第一步:获取SQL查询
    response = get_sql_completion(messages)
    print("\n==== 初始响应 ====")
    print(response)
    
    # 添加到消息历史
    messages.append(response)
    
    # 检查是否有函数调用
    if response.tool_calls:
        print("\n==== 检测到函数调用 ====")
        tool_call = response.tool_calls[0]
        
        if tool_call.function.name == "ask_database":
            # 解析函数参数
            arguments = tool_call.function.arguments
            args = json.loads(arguments)
            
            print("\n==== 生成的SQL查询 ====")
            print(args["query"])
            
            # 执行SQL查询
            result = ask_database(args["query"])
            
            print("\n==== 数据库查询结果 ====")
            print(result)
            
            # 将结果添加到消息历史
            messages.append({
                "tool_call_id": tool_call.id,
                "role": "tool",
                "name": "ask_database",
                "content": str(result)
            })
            
            # 第二步:获取最终回答
            response = get_sql_completion(messages)
            print("\n==== 最终回复 ====")
            print(response.content)
    else:
        print("\n==== 直接回复 ====")
        print(response.content)

if __name__ == "__main__":
    try:
        main()
    finally:
        # 确保关闭数据库连接
        if cursor:
            cursor.close()
            print("游标已关闭")
        if connection:
            connection.close()
            print("数据库连接已关闭")

完整输出结果

====SQL====
SELECT s.name, sc.score FROM Students s JOIN Scores sc ON s.student_id = sc.student_id WHERE s.class_id = 1 AND sc.subject= '数学';
====MySQL数据库查询结果====
(('张三', 85.5), ('李四', 78.0))
====最终回复====
ChatCompletionMessage(content='一班的学生`数学`成绩如下:\n- 张三:85.5\n- 李四: 78.0', refusal=None, role='assistant', audio=None,function_call=None, tool_calls=None, annotations=[])

 

posted @ 2025-09-02 09:39  指尖下的世界  阅读(5)  评论(0)    收藏  举报