Function Calling:比Agent更靠谱的工具调用,我踩了4个坑

第6篇写了LangChain Agent------让AI自己决定调哪个工具。当时觉得很好用,但上线后发现问题不断:

arduino 复制代码
用户:查一下订单ORD123的状态
Agent:调了天气API(❌ 调错了)

用户:退货流程是什么?
Agent:调了3次订单查询API,然后说"我找不到退货流程"(❌ 不该调API,应该查知识库)

用户:你好
Agent:调了2个工具,3秒后才回答"你好"(❌ 闲聊不需要调工具)

Agent的核心问题是"太自由"------AI自己决定调什么工具,但经常调错、多调、乱调。

Function Calling是另一种思路:不是让AI自己选工具,而是让AI告诉你要调什么函数,由你来执行。 听起来差不多?实际差远了。

我花了1周从Agent迁移到Function Calling,踩了4个坑。每个坑都让我理解了"AI选工具"和"AI建议工具"的本质区别。


先说结论

维度 Agent(第6篇) Function Calling
谁执行工具 AI自己执行 AI建议,你执行
可控性 ❌ AI可能调错、多调 ✅ 你决定调不调、调什么
可观测性 ❌ 黑盒,不知道AI干了啥 ✅ AI返回函数名+参数,你全看得到
成本 高(AI可能多次调工具) 低(你控制调用次数)
灵活性 高(AI自己组合工具) 中(你按顺序执行)

用Java人的理解:Agent ≈ 反射调用------AI运行时自己选择调哪个方法,你控制不了;Function Calling ≈ 策略模式------AI告诉你用哪个策略,你决定是否执行、怎么执行。前者灵活但危险,后者可控但需要你写胶水代码。

一句话:生产环境用Function Calling,开发验证用Agent。


Function Calling到底是什么

和Agent的区别

javascript 复制代码
Agent流程:
用户提问 → AI思考 → AI调工具1 → AI看结果 → AI调工具2 → AI回答
(全程AI控制,你只看到最终回答)

Function Calling流程:
用户提问 → AI思考 → AI返回:{"function": "query_order", "args": {"order_id": "ORD123"}}
                ↓
            你执行query_order("ORD123") → 拿到结果
                ↓
            把结果喂回AI → AI生成最终回答

关键区别:中间那一步------你拿到了AI的函数调用请求,你可以:

  • 执行它
  • 不执行它(安全检查)
  • 修改参数再执行
  • 记录日志
  • 限制调用次数

这就是Function Calling比Agent强的地方------你拿到了控制权。


坑1:以为Function Calling很复杂,其实就3步

最简示例

python

ini 复制代码
import json
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="qwen-plus")

# 第1步:定义函数(告诉AI你能调哪些函数)
tools = [
    {
        "type": "function",
        "function": {
            "name": "query_order",
            "description": "查询订单状态",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {
                        "type": "string",
                        "description": "订单号,格式:ORD+数字",
                    }
                },
                "required": ["order_id"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "search_knowledge",
            "description": "搜索公司知识库,用于回答产品、流程相关问题",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "搜索关键词",
                    }
                },
                "required": ["query"],
            },
        },
    },
]

# 第2步:AI判断要调哪个函数
response = llm.invoke(
    "帮我查一下订单ORD123的状态",
    tools=tools,
)

# AI返回的不是最终回答,而是函数调用请求
print(response.additional_kwargs.get("tool_calls"))
# [{'function': {'name': 'query_order', 'arguments': '{"order_id": "ORD123"}'}}]

# 第3步:你执行函数,把结果喂回AI
tool_call = response.additional_kwargs["tool_calls"][0]
func_name = tool_call["function"]["name"]
func_args = json.loads(tool_call["function"]["arguments"])

# 你自己实现函数
if func_name == "query_order":
    result = query_order(func_args["order_id"])  # 你的业务逻辑
elif func_name == "search_knowledge":
    result = search_knowledge(func_args["query"])

# 把结果喂回AI,生成最终回答
final_response = llm.invoke(
    f"函数调用结果:{result}\n\n请基于以上结果回答用户的问题:帮我查一下订单ORD123的状态"
)
print(final_response.content)
# "您的订单ORD123已发货,顺丰SF123456,预计6月20日送达。"

3步走:

  1. 定义函数列表(告诉AI你有什么工具)
  2. AI返回函数调用请求(AI决定调什么,但不执行)
  3. 你执行函数,把结果喂回AI(你控制执行)

坑2:AI调了不该调的函数------需要加安全校验

翻车现场

bash 复制代码
用户:查一下ORD123的状态
AI返回:{"function": "delete_order", "args": {"order_id": "ORD123"}}

# ❌ AI选了删除订单而不是查询订单!

虽然Function Calling比Agent可控------AI不会直接执行------但如果你无脑执行AI的函数调用请求,一样会出问题。

正确做法:执行前加安全校验层

python

python 复制代码
# 白名单:只允许AI调用这些函数
ALLOWED_FUNCTIONS = {"query_order", "search_knowledge"}

# 危险操作:需要额外确认
DANGEROUS_FUNCTIONS = {"delete_order", "refund_order", "update_order"}

# 参数校验规则
PARAM_VALIDATORS = {
    "query_order": {
        "order_id": lambda x: x.startswith("ORD") and x[3:].isdigit(),  # 必须ORD+数字
    },
    "search_knowledge": {
        "query": lambda x: len(x) >= 2 and len(x) <= 100,  # 2-100字
    },
}


def safe_execute(func_name: str, func_args: dict) -> dict:
    """安全的函数执行:校验+执行"""

    # 1. 检查函数是否在白名单
    if func_name not in ALLOWED_FUNCTIONS:
        return {"error": f"不允许调用函数:{func_name}"}

    # 2. 检查是否是危险操作
    if func_name in DANGEROUS_FUNCTIONS:
        # 实际项目:发通知/写审计日志/要求人工确认
        return {"error": f"危险操作{func_name}需要人工确认"}

    # 3. 参数校验
    validators = PARAM_VALIDATORS.get(func_name, {})
    for param, validator in validators.items():
        value = func_args.get(param)
        if value is None:
            return {"error": f"缺少参数:{param}"}
        if not validator(value):
            return {"error": f"参数{param}格式不正确:{value}"}

    # 4. 执行
    if func_name == "query_order":
        return query_order(func_args["order_id"])
    elif func_name == "search_knowledge":
        return search_knowledge(func_args["query"])

    return {"error": f"未知函数:{func_name}"}


# 完整的Function Calling流程
def chat_with_functions(question: str) -> str:
    """带安全校验的Function Calling"""
    # AI判断要调什么函数
    response = llm.invoke(question, tools=tools)

    tool_calls = response.additional_kwargs.get("tool_calls", [])
    if not tool_calls:
        # 不需要调函数,直接回答
        return response.content

    # 执行每个函数调用
    results = []
    for tool_call in tool_calls:
        func_name = tool_call["function"]["name"]
        func_args = json.loads(tool_call["function"]["arguments"])

        # 安全校验+执行
        result = safe_execute(func_name, func_args)
        results.append(f"{func_name}({func_args}) → {result}")

    # 把结果喂回AI
    final = llm.invoke(
        f"函数调用结果:\n" + "\n".join(results) + f"\n\n请回答用户问题:{question}"
    )
    return final.content

安全校验的4个层次:

层次 校验什么 Java类比
白名单 只允许调预定义函数 API权限控制
危险操作标记 敏感操作需额外确认 @RequiresPermissions
参数校验 参数格式+范围校验 @Valid + JSR303
审计日志 记录每次函数调用 操作审计日志

坑3:多轮对话中AI重复调同一个函数

翻车现场

scss 复制代码
用户:查一下ORD123到哪了
AI:调query_order("ORD123") → "已发货,顺丰SF123456"

用户:那ORD456呢?
AI:调query_order("ORD123") → 又查了一次ORD123!❌

AI没有记住上一轮对话的上下文。 它不知道ORD123已经查过了,也不理解"ORD456呢"指的是另一个订单。

正确做法:维护对话历史+函数调用记录

python

python 复制代码
class FunctionCallingChat:
    """带上下文的Function Calling聊天"""

    def __init__(self, llm, tools):
        self.llm = llm
        self.tools = tools
        self.messages = []  # 对话历史
        self.call_history = []  # 函数调用记录

    def chat(self, user_input: str) -> str:
        # 添加用户消息
        self.messages.append({"role": "user", "content": user_input})

        # AI判断(带对话历史)
        response = self.llm.invoke(
            self.messages,
            tools=self.tools,
        )

        tool_calls = response.additional_kwargs.get("tool_calls", [])

        if not tool_calls:
            answer = response.content
            self.messages.append({"role": "assistant", "content": answer})
            return answer

        # 执行函数调用
        function_results = []
        for tool_call in tool_calls:
            func_name = tool_call["function"]["name"]
            func_args = json.loads(tool_call["function"]["arguments"])

            # 检查是否重复调用
            call_key = f"{func_name}({func_args})"
            if call_key in self.call_history:
                function_results.append(f"{func_name}({func_args}) → [已查过,结果不变]")
                continue

            result = safe_execute(func_name, func_args)
            function_results.append(f"{func_name}({func_args}) → {result}")
            self.call_history.append(call_key)

        # 喂回AI
        context = f"函数调用结果:\n" + "\n".join(function_results)
        self.messages.append({"role": "assistant", "content": context})

        final = self.llm.invoke(
            self.messages + [{"role": "user", "content": f"请基于函数结果回答:{user_input}"}]
        )
        self.messages.append({"role": "assistant", "content": final.content})
        return final.content


# 使用
chat = FunctionCallingChat(llm, tools)
print(chat.chat("查一下ORD123到哪了"))     # 查ORD123
print(chat.chat("那ORD456呢?"))            # 查ORD456(不会重复查ORD123)
print(chat.chat("再帮我查一次ORD123"))       # 允许显式重复查询

坑4:Function Calling和Agent到底用哪个?选择困难症

决策指南

场景 用Function Calling 用Agent
函数少(1-5个),逻辑简单 ✅ 够用 杀鸡用牛刀
函数多(5+),需要多步组合 ⚠️ 可以但要写更多胶水代码 ✅ Agent自己组合更灵活
生产环境,安全要求高 ✅ 可控可审计 ❌ 黑盒不安全
需要限制调用次数/成本 ✅ 你控制执行 ❌ AI可能多调
研究实验、快速验证 ⚠️ 写胶水代码慢 ✅ 快速出Demo
需要AI自动重试/换策略 ❌ 你得自己写 ✅ Agent自带

我的真实推荐:

阶段 用什么 原因
开发验证 Agent 快速出Demo
上生产 Function Calling 可控+安全+可审计
复杂多步 LangGraph + Function Calling 第9篇的流程控制+本文的安全执行

最佳组合:LangGraph编排流程 + Function Calling执行函数。 LangGraph控制"先做什么再做什么",Function Calling控制"安全地调什么函数"。两者互补。


完整代码:带安全校验的Function Calling客服

python

python 复制代码
"""
Function Calling客服系统
依赖:pip install langchain langchain-openai
"""
import json
from langchain_openai import ChatOpenAI

# ============ 函数定义 ============

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "query_order",
            "description": "查询订单状态,输入订单号",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {"type": "string", "description": "订单号,ORD+数字"}
                },
                "required": ["order_id"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "search_knowledge",
            "description": "搜索公司知识库,回答产品、流程、政策相关问题",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "搜索关键词"}
                },
                "required": ["query"],
            },
        },
    },
]

# ============ 业务函数实现 ============

def query_order(order_id: str) -> dict:
    """查询订单(模拟)"""
    orders = {
        "ORD123": {"status": "已发货", "tracking": "SF123456", "eta": "6月20日"},
        "ORD456": {"status": "待支付", "tracking": None, "eta": None},
    }
    return orders.get(order_id, {"error": "订单不存在"})

def search_knowledge(query: str) -> dict:
    """搜索知识库(模拟)"""
    kb = {
        "退货": "退货流程:登录→我的订单→申请退货→填原因→提交,3个工作日审核。",
        "退款": "退款3-5个工作日到账,按原支付方式返还。",
        "换货": "换货请联系人工客服,线上暂不支持自助换货。",
    }
    for key, value in kb.items():
        if key in query:
            return {"result": value}
    return {"result": "未找到相关信息"}

# ============ 安全执行 ============

ALLOWED = {"query_order", "search_knowledge"}
VALIDATORS = {
    "query_order": {"order_id": lambda x: x.startswith("ORD")},
    "search_knowledge": {"query": lambda x: 2 <= len(x) <= 100},
}

def safe_execute(name: str, args: dict) -> dict:
    if name not in ALLOWED:
        return {"error": f"不允许调用:{name}"}
    for param, validator in VALIDATORS.get(name, {}).items():
        if not validator(args.get(param, "")):
            return {"error": f"参数{param}校验失败"}
    if name == "query_order":
        return query_order(args["order_id"])
    elif name == "search_knowledge":
        return search_knowledge(args["query"])
    return {"error": "未知函数"}

# ============ 主流程 ============

def chat(question: str, llm, messages: list) -> str:
    messages.append({"role": "user", "content": question})
    response = llm.invoke(messages, tools=TOOLS)

    tool_calls = response.additional_kwargs.get("tool_calls", [])
    if not tool_calls:
        messages.append({"role": "assistant", "content": response.content})
        return response.content

    results = []
    for tc in tool_calls:
        name = tc["function"]["name"]
        args = json.loads(tc["function"]["arguments"])
        result = safe_execute(name, args)
        results.append(f"{name} → {result}")

    context = "函数结果:\n" + "\n".join(results)
    final = llm.invoke(
        messages + [{"role": "user", "content": f"{context}\n\n回答:{question}"}]
    )
    return final.content


# ============ 测试 ============

if __name__ == "__main__":
    llm = ChatOpenAI(model="qwen-plus")
    messages = []

    print(chat("帮我查一下订单ORD123到哪了", llm, messages))
    # → 您的订单ORD123已发货,顺丰SF123456,预计6月20日送达。

    print(chat("退货流程是什么?", llm, messages))
    # → 退货流程:登录→我的订单→申请退货→填原因→提交,3个工作日审核。

    print(chat("帮我删除订单ORD999", llm, messages))
    # → 抱歉,当前不支持删除订单的操作。

4个坑的总结

# 错误做法 正确做法 一句话
1 以为很复杂 不敢用 3步走:定义→AI建议→你执行 AI建议你执行=可控
2 AI调了不该调的函数 无脑执行AI的请求 安全校验:白名单+参数校验+危险操作确认 执行前必须校验
3 多轮对话重复调函数 不维护对话历史 维护messages+调用记录 上下文是AI的记忆
4 和Agent选哪个 选择困难 生产用Function Calling,验证用Agent 可控比灵活重要

你用过Function Calling吗?和Agent比体验如何?评论区聊聊 👇

点赞关注「荣码」,大模型转型系列持续更新~

相关推荐
这就是佬们吗1 小时前
AI 的记忆进化史:从金鱼记忆到学会做梦
人工智能·python·embedding
Csvn1 小时前
🐍 Day 6: Python 异常处理 — 防御式编程的核心
后端·python
朝发如雪1 小时前
快速掌握Linux(2)(时间相关函数)(文件IO)
linux·python·算法
苏子寒1 小时前
Nano-VLLM全代码解析笔记(2)-block_manager
人工智能·笔记·python·机器学习·nlp·vllm
杨丰玮4181 小时前
暑假Java知识点回顾:类与对象知识总结
java·开发语言
盐焗鹌鹑蛋1 小时前
【Linux】ELF与库加载
java·linux·服务器
小猴子爱上树2 小时前
跨境电商图片翻译工具,跨马批量处理视频字幕与抠图
python·音视频
WarPigs2 小时前
Unity未激活的物体无法响应事件?
java·unity·游戏引擎
smj2302_796826522 小时前
解决leetcode第4033题有效K个不同元素子数组I
数据结构·python·算法·leetcode