OpenAI Function Calling

OpenAI Function Calling 逻辑说明

概述

本文件实现纯 OpenAI Function Calling(Tool Calling)模式,不依赖 LangChain,直接使用 OpenAI SDK 与 LLM 交互,支持本地工具和 MCP 远程工具。

核心流程

复制代码
用户输入
    ↓
┌─────────────────────────────────────────────────────┐
│  for turn in range(MAX_TURNS):                       │
│                                                     │
│  ① LLM 推理(携带所有历史消息)                      │
│         ↓                                           │
│  ② 检查 msg.tool_calls                               │
│         ↓                          ↓                 │
│    [有工具调用]                          [无工具调用] │
│         ↓                          ↓                 │
│  ③ 并行执行所有工具                         直接返回 │
│         ↓                                           │
│  ④ 将 ToolMessage 填入消息历史                      │
│         ↓                                           │
│  ⑤ 截断历史(MAX_HISTORY)                         │
│         ↓                                           │
│  ⑥ 回到 ① 继续下一轮推理                           │
└─────────────────────────────────────────────────────┘
    ↓
最终回复

消息循环详解

消息格式(OpenAI 规范)

python 复制代码
# 用户消息
{"role": "user", "content": "上海今天天气怎么样?"}

# LLM 回复(可能含 tool_calls)
{"role": "assistant", "content": "我来查询一下...", "tool_calls": [...]}

# 工具执行结果(role 必须为 "tool")
{
    "role": "tool",
    "tool_call_id": "abc123",
    "name": "get_weather",
    "content": "晴,28°C,湿度 65%"
}

循环终止条件

条件 结果
LLM 回复无 tool_calls 返回 msg.content,正常结束
达到 MAX_TURNS = 10 RuntimeError,异常结束

工具定义与执行

本地工具

工具通过 OpenAI Function Calling 格式定义,存入 tools 列表:

python 复制代码
{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "查询城市天气",
        "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string", "description": "城市名称"}},
            "required": ["city"]
        }
    }
}

执行时通过 tool_map 查找对应函数:

python 复制代码
args = json.loads(call.function.arguments)  # 安全解析,避免 eval
result = tool_map[call.function.name](**args)

MCP 远程工具

MCP 工具通过 Streamable HTTP 协议接入,流程:

复制代码
MCP 服务器 ──→ fetch_mcp_tools() ──→ 格式转换 ──→ 合并到 tools/tool_map
                           │
                           ↓
               ┌─────────────────────────┐
               │  MCP 格式 → OpenAI 格式   │
               │  name, description,      │
               │  parameters 原样保留      │
               └─────────────────────────┘

调用时

python 复制代码
result = call_mcp_tool(tool_name, arguments)  # HTTP POST 到 MCP 服务器

工具查找优先级

复制代码
tool_map 查找
    ↓
[本地工具] 直接执行
    ↓ 未找到
[MCP 工具] HTTP 调用 MCP 服务器
    ↓ 未找到
返回 "未知工具: xxx"

安全机制

防护项 实现位置 说明
代码注入 第97行 json.loads() 替代 eval()
API Key 校验 第10-13行 未设置环境变量时启动即抛异常
工具执行异常 第105-108行 try/except 捕获工具内部错误
消息历史截断 第209-210行 防止内存无限增长
最大调用次数 第78-79行 MAX_TURNS=10 防死循环

错误处理

python 复制代码
# 参数解析失败(JSON 格式错误)
→ "参数解析失败: ..."

# 工具名称不存在
→ "未知工具: xxx"

# 工具执行内部异常
→ "工具执行出错: ..."

所有错误通过 ToolMessage 反馈给 LLM,由 LLM 决定如何整合错误信息。

配置参数

参数 默认值 说明
MAX_TURNS 10 最大推理轮次,防死循环
MAX_HISTORY 20 保留最近 N 条消息,防止内存溢出
MCP_SERVER_URL http://localhost:8080/mcp MCP 服务器地址
MCP_API_KEY 环境变量 MCP_API_KEY MCP 认证密钥(可选)

与 LangChain 版本对比

特性 本文件 LangChain 版本
依赖 openai langchain-core + langchain-openai
代码量 约 220 行 约 170 行
工具定义 手动拼接 dict @tool 装饰器
消息构建 手动管理 自动 Message 对象
并行执行 需手动改造 asyncio.gather
MCP 集成 需手动实现 需 MCP 扩展

选择建议

  • 追求轻量、掌控力 → 本文件
  • 追求开发效率、复杂流程 → LangChain 版本
python 复制代码
"""
直接使用 OpenAI Function Calling(Tool Calling),无 LangChain 依赖
依赖: openai
"""

import json
import os
from openai import OpenAI

api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
    raise ValueError("OPENAI_API_KEY 环境变量未设置")
client = OpenAI(api_key=api_key)

# ── 1. 定义工具 ───────────────────────────────────────────────────

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查询城市天气",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名称"}
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search_web",
            "description": "搜索网页获取实时信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string"},
                    "top_k": {"type": "integer", "default": 3}
                },
                "required": ["query"]
            }
        }
    }
]


# ── 2. 工具执行函数 ────────────────────────────────────────────────

def get_weather(city: str) -> str:
    weather_data = {
        "上海": "晴,28°C,湿度 65%",
        "北京": "多云,24°C,湿度 40%",
        "东京": "小雨,22°C,湿度 80%",
    }
    return weather_data.get(city, f"未收录城市 {city} 的数据")


def search_web(query: str, top_k: int = 3) -> list[dict]:
    return [
        {"title": f"结果{i+1} for {query}", "url": f"https://example.com/{i}"}
        for i in range(top_k)
    ]


tool_map = {
    "get_weather": get_weather,
    "search_web": search_web,
}


# ── 2.5 MCP 工具集成 ────────────────────────────────────────────────

MCP_SERVER_URL = "http://localhost:8080/mcp"  # MCP 服务地址
MCP_API_KEY = os.getenv("MCP_API_KEY", "")    # MCP 认证密钥(可选)


def fetch_mcp_tools() -> list[dict]:
    """从 MCP 服务器获取工具列表(Streamable HTTP 协议)

    真实项目中替换为实际 HTTP 请求:
        response = requests.post(
            f"{MCP_SERVER_URL}/tools/list",
            headers={"Authorization": f"Bearer {MCP_API_KEY}"} if MCP_API_KEY else {},
        )
        return response.json()["tools"]
    """
    # ── Mock: 模拟 MCP 服务器返回的工具列表 ──────────────────────
    return [
        {
            "name": "mcp_calculator",
            "description": "数学计算器,支持加减乘除",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {"type": "string", "description": "数学表达式,如 2+3*5"}
                },
                "required": ["expression"]
            }
        },
        {
            "name": "mcp_currency_convert",
            "description": "货币换算",
            "parameters": {
                "type": "object",
                "properties": {
                    "amount": {"type": "number", "description": "金额"},
                    "from_currency": {"type": "string", "description": "源货币,如 USD"},
                    "to_currency": {"type": "string", "description": "目标货币,如 CNY"}
                },
                "required": ["amount", "from_currency", "to_currency"]
            }
        }
    ]


def call_mcp_tool(tool_name: str, arguments: dict) -> str:
    """调用 MCP 工具(Streamable HTTP 协议)

    真实项目中替换为实际 HTTP 请求:
        response = requests.post(
            f"{MCP_SERVER_URL}/tools/call",
            json={"name": tool_name, "arguments": arguments},
            headers={"Authorization": f"Bearer {MCP_API_KEY}"} if MCP_API_KEY else {},
        )
        return response.json()["result"]
    """
    # ── Mock: 模拟 MCP 工具执行 ────────────────────────────────
    if tool_name == "mcp_calculator":
        try:
            return str(eval(arguments["expression"]))
        except Exception:
            return "表达式计算错误"
    elif tool_name == "mcp_currency_convert":
        rates = {"USD_CNY": 7.2, "CNY_USD": 0.14, "USD_JPY": 149.5, "JPY_USD": 0.0067}
        key = f"{arguments['from_currency']}_{arguments['to_currency']}"
        if key in rates:
            return str(arguments["amount"] * rates[key])
        return f"暂不支持 {arguments['from_currency']} → {arguments['to_currency']}"
    return f"未知 MCP 工具: {tool_name}"


def load_mcp_tools():
    """从 MCP 服务器加载工具并合并到全局 tools 和 tool_map"""
    mcp_tools = fetch_mcp_tools()
    for tool in mcp_tools:
        # 转换 MCP 格式 → OpenAI Function Calling 格式
        openai_tool = {
            "type": "function",
            "function": {
                "name": tool["name"],
                "description": tool["description"],
                "parameters": tool["parameters"]
            }
        }
        tools.append(openai_tool)
        # 用默认参数捕获当前值,避免闭包陷阱
        tool_map[tool["name"]] = lambda name=tool["name"], args={}: call_mcp_tool(name, args)


# 启动时加载 MCP 工具(可延迟到需要时再加载)
load_mcp_tools()


# ── 3. 对话循环 ───────────────────────────────────────────────────

def run(user_query: str) -> str:
    messages = [{"role": "user", "content": user_query}]
    MAX_TURNS = 10
    MAX_HISTORY = 20

    for _ in range(MAX_TURNS):
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tools,
            tool_choice="auto",
        )

        msg = response.choices[0].message
        messages.append({"role": msg.role, "content": msg.content or ""})

        if not msg.tool_calls:
            return msg.content

        for call in msg.tool_calls:
            try:
                args = json.loads(call.function.arguments)
            except json.JSONDecodeError as e:
                result = f"参数解析失败: {e}"
            else:
                tool_name = call.function.name
                if tool_name not in tool_map:
                    result = f"未知工具: {tool_name}"
                else:
                    try:
                        result = tool_map[tool_name](**args)
                    except Exception as e:
                        result = f"工具执行出错: {e}"

            messages.append({
                "role": "tool",
                "tool_call_id": call.id,
                "name": call.function.name,
                "content": str(result),
            })

        # 每轮结束后截断历史
        if len(messages) > MAX_HISTORY:
            messages = messages[-MAX_HISTORY:]

    raise RuntimeError("工具调用超过最大次数限制,可能陷入死循环")


if __name__ == "__main__":
    print(run("上海今天天气怎么样?顺便搜一下上海旅游攻略"))
相关推荐
小小高不懂写代码1 小时前
Transformer与注意力机制
前端·人工智能
码流怪侠1 小时前
【GitHub】 Headroom 深度解析:AI Agent 上下文压缩层的完整技术拆解
人工智能·github·agent
qq_411262421 小时前
ESP32-S3 AI相机硬件组成与通信配置说明
人工智能·数码相机
闲人小吴1 小时前
Loop Engineering:当杠杆点从「写 Prompt」移到「设计循环」
人工智能
Yobeeo1 小时前
记忆与存档——Checkpointer 与状态持久化 — LangGraph 实战——构建跨平台爆款图文 Agent 第3篇
人工智能
Yobeeo1 小时前
Agent 的思考循环——条件路由与工具调用 — LangGraph 实战——构建跨平台爆款图文 Agent 第2篇
人工智能
前端不太难1 小时前
Agent First:鸿蒙 App 的下一代 AI Runtime 架构
人工智能·架构·harmonyos
Yobeeo1 小时前
图的力量——LangGraph 思维模型与第一个图 — LangGraph 实战——构建跨平台爆款图文 Agent 第1篇
人工智能
大鱼>1 小时前
能源物联网:AI如何重构电力系统的每一个环节
人工智能·物联网·能源