9.2 入门案例:简单函数调用机器人

RAG 让 LLM 能「读」,Function Calling 让 LLM 能「做」。这篇带你从零实现一个​带工具调用能力的聊天机器人​——它能查天气、搜新闻、做算术,还能把结果整理成漂亮的格式返回。

📑 目录


项目目标

最终效果:
用户:「北京今天天气怎么样?顺便帮我算下 23 * 47」
机器人:
🌤 北京今天:晴,气温 12~22°C,适合出行
🧮 23 × 47 = 1081

用户:「搜一下最新的 AI 新闻」
机器人:[调用搜索工具] 🔍 找到以下最新资讯:
1. OpenAI 发布 GPT-5 ... 
2. Google DeepMind 宣布 ...

关键能力:
✅ 自动判断是否需要调用工具
✅ 正确提取函数参数
✅ 执行工具并将结果整合进回答
✅ 多轮工具调用(一个问题可能需要调多个工具)

定义工具集

# bot/tools.py
import json
import random
from datetime import datetime
from typing import Callable

# 工具注册表:名称 → (函数, 描述, 参数Schema)
TOOLS_REGISTRY: dict[str, dict] = {}

def tool(name: str, description: str, params_schema: dict):
    """装饰器:注册一个工具函数"""
    def decorator(func: Callable):
        TOOLS_REGISTRY[name] = {
            "function": func,
            "description": description,
            "parameters": params_schema,
        }
        return func
    return decorator

@tool(
    name="get_weather",
    description="获取指定城市的当前天气",
    params_schema={
        "type": "object",
        "properties": {
            "city": {"type": "string", "description": "城市名"},
        },
        "required": ["city"]
    }
)
def get_weather(city: str) -> str:
    """模拟天气 API(实际替换为真实 API)"""
    weathers = ["晴", "多云", "阴", "小雨"]
    temp_low = random.randint(-5, 20)
    temp_high = random.randint(temp_low + 5, temp_low + 18)
    return json.dumps({"city": city, "weather": random.choice(weathers),
                        "temp_low": temp_low, "temp_high": temp_high,
                        "updated_at": datetime.now().isoformat()}, ensure_ascii=False)

@tool(
    name="calculate",
    description="执行数学计算表达式",
    params_schema={
        "type": "object",
        "properties": {
            "expression": {"type": "string", "description": "数学表达式,如 '23*47'"},
        },
        "required": ["expression"]
    }
)
def calculate(expression: str) -> str:
    try:
        # 安全起见只允许数字和基本运算符
        allowed = set("0123456789+-*/().% ")
        if not all(c in allowed for c in expression):
            return json.dumps({"error": "不允许的表达式字符"})
        result = eval(expression)  # 生产环境用 ast.literal_eval 更安全
        return json.dumps({"expression": expression, "result": result})
    except Exception as e:
        return json.dumps({"error": str(e)})

@tool(
    name="search_news",
    description="搜索最新新闻(按关键词)",
    params_schema={
        "type": "object",
        "properties": {
            "keyword": {"type": "string", "description": "搜索关键词"},
            "limit": {"type": "integer", "description": "返回条数(默认5)"},
        },
        "required": ["keyword"]
    }
)
def search_news(keyword: str, limit: int = 5) -> str:
    """模拟搜索(实际接入搜索 API 或 Tavily)"""
    fake_news = [
        f"{keyword} 相关重大进展报道",
        f"专家解读{keyword}领域新趋势",
        f"{month}{keyword}行业数据发布",
    ]
    return json.dumps([{"title": n, "source": "模拟源"} for n in fake_news[:limit]], ensure_ascii=False)

print(f"已注册 {len(TOOLS_REGISTRY)} 个工具: {list(TOOLS_REGISTRY.keys())}")

核心循环 & 完整实现

# bot/main.py
from openai import OpenAI
import json
from tools import TOOLS_REGISTRY

client = OpenAI()

def build_tools_definition():
    """转换为 OpenAI Function Calling 格式"""
    return [{
        "type": "function",
        "function": {
            "name": name,
            "description": info["description"],
            "parameters": info["parameters"]
        }
    } for name, info in TOOLS_REGISTRY.items()]

def execute_tool(name: str, args: dict):
    """安全执行工具并返回结果"""
    if name not in TOOLS_REGISTRY:
        return json.dumps({"error": f"未知工具: {name}"})
    try:
        func = TOOLS_REGISTRY[name]["function"]
        result = func(**args)
        return result
    except Exception as e:
        return json.dumps({"error": f"工具执行错误: {e}"})

def chat_with_tools(user_message: str, max_turns=5):
    """带 Function Calling 的多轮对话循环"""
    messages = [{"role": "system", "content": "你是一个有帮助的助手。使用可用工具来获取实时信息和执行操作。"}]
    messages.append({"role": "user", "content": user_message})
    
    tools_def = build_tools_definition()
    
    for turn in range(max_turns):
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=messages,
            tools=tools_def,
            temperature=0.2,
        )
        msg = response.choices[0].message
        
        # 情况A:直接回答
        if not msg.tool_calls:
            return msg.content
        
        # 情况B:调用工具
        messages.append(msg)  # 记录 LLM 的决策
        
        for tc in msg.tool_calls:
            func_name = tc.function.name
            func_args = json.loads(tc.function.arguments)
            print(f"  🔧 调用工具: {func_name}({func_args})")
            
            tool_result = execute_tool(func_name, func_args)
            print(f"  📋 结果: {tool_result[:100]}...")
            
            # 结果喂回 LLM
            messages.append({"role": "tool", "tool_call_id": tc.id,
                             "content": tool_result})
    
    return "达到最大轮次限制"

# CLI 交互
if __name__ == "__main__":
    print("🤖 函数调用机器人已启动!(输入 quit 退出)\n")
    while True:
        query = input("你: ")
        if query.lower() == "quit": break
        answer = chat_with_tools(query)
        print(f"🤖: {answer}\n")
# 运行
python -m bot.main

❌ 常见误区

  • ❌ 工具描述写得越详细越好 — 太长的描述反而干扰 LLM 决策,简洁精准最好
  • ❌ 所有工具一次性注册 — 按场景分组加载,减少选择困难
  • ❌ 忽略工具的错误处理 — 工具可能失败,必须有 fallback 和异常捕获
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值