第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步走:
- 定义函数列表(告诉AI你有什么工具)
- AI返回函数调用请求(AI决定调什么,但不执行)
- 你执行函数,把结果喂回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比体验如何?评论区聊聊 👇
点赞关注「荣码」,大模型转型系列持续更新~