Function Calling的正确打开方式:API设计与Agent工具调用

Function Calling的正确打开方式:API设计与Agent工具调用

引言:大模型为什么需要“手”?

如果你尝试过构建生产级别的AI应用,一定会发现一个残酷的现实:大模型虽然“聪明”,但它没有“手”

它能为你写出一篇完美的库存分析报告,却无法直接登录后台去扣减一个库存;它能规划出最优的旅游路线,却没有办法帮你订一张机票。这种“能说不能做”的局限,正是Function Calling要解决的核心问题。

Function Calling(函数调用)是LLM与外部系统交互的核心能力。它的本质是让LLM输出一个结构化的工具调用指令,而不是普通文本。简单来说,LLM扮演的是一个**“调度员”**的角色——当你给它提供了一系列工具的描述后,它会根据用户意图判断是否需要调用外部工具,并返回一段特定格式的JSON,告诉调用方:“我建议你调用这个函数,参数是这些。”

真正的代码执行、权限校验和错误处理,依然牢牢掌握在你的程序手中。这种**“意图在模型,执行在代码”**的边界划分,正是AI系统安全性的核心保障。本文将从架构设计和代码实现两个维度,系统阐述Function Calling的正确打开方式。

一、理解本质:Function Calling不是让LLM直接执行代码

首先需要纠正一个常见的误区:Function Calling并不是让LLM直接运行你的代码。LLM本身不具备执行能力,它只负责“决策”。

完整的Function Calling流程包含五个步骤:

用户输入 ↓ ┌─────────────────────────────┐ │ LLM + 可用工具Schema定义 │ ← 开发者预先注册工具 └──────────────┬──────────────┘ ↓ LLM推理判断:是否需要调用工具? ↓ 返回 tool_calls JSON(name + arguments) ↓ ┌──────────────────────────────┐ │ 开发者解析 tool_calls │ │ 执行真实的工具函数 │ └──────────────┬───────────────┘ ↓ 将执行结果作为 tool 消息发回给LLM ↓ LLM生成自然语言最终回复 ↓ 返回给用户

这种设计的精妙之处在于:模型只负责“决定做什么”,程序负责“实际怎么做”。模型输出的永远是结构化的调用请求,而非可执行代码,这从架构层面隔离了安全风险。

二、API设计:如何定义高质量的Tool Schema

工具定义的质量直接决定了LLM调用工具的准确性。一个完整的工具定义包含三个核心字段:

  • name:函数唯一标识
  • description:用自然语言描述函数作用——这是模型选择工具的关键依据
  • parameters:详细定义参数的名称、类型、描述及是否必需

2.1 description是灵魂

LLM完全靠description来判断“什么时候该用这个工具”。描述越清晰,调用越准确。

❌ 差的描述:

{"description":"获取天气信息"}

✅ 好的描述:

{"description":"获取指定城市的当前天气和未来24小时预报。当用户询问天气、穿衣建议、是否适合出行时使用。返回:温度、湿度、风力、空气质量。"}

2.2 参数设计原则

原则一:窄工具,显式参数

模糊的参数让模型难以正确填充。对比以下两种设计:

❌ 不推荐——参数模糊:

{"name":"lookup_customer","parameters":{"properties":{"query":{"type":"string"}}}}

✅ 推荐——参数明确:

{"name":"lookup_customer_by_email","parameters":{"properties":{"customer_email":{"type":"string"}}}}

原则二:读写分离

工具应分为两类:

  • 读工具:检索信息(查询订单状态、搜索知识库)——可频繁调用,无需确认
  • 写工具:变更状态(创建工单、发送邮件、更新记录)——需要用户明确确认

2.3 完整的工具定义示例

以下是一个电商场景的工具定义,包含了订单查询和RAG文档检索两种能力:

tools=[{"type":"function","function":{"name":"get_order","description":"通过订单号查询订单详细信息,返回物流信息、下单时间、商品清单等","parameters":{"type":"object","properties":{"order_number":{"type":"string","description":"订单号,格式如ORD20260123001"}},"required":["order_number"]}}},{"type":"function","function":{"name":"retrieving_documents","description":"通过商品文档ID查询商品参数或功能使用说明,RAG检索增强","parameters":{"type":"object","properties":{"product_id":{"type":"string","description":"商品文档ID"},"question":{"type":"string","description":"用户想要了解的具体问题"}},"required":["product_id","question"]}}}]

三、代码实现:从注册到执行的完整链路

3.1 工具注册中心

一个优雅的工具注册中心应该具备高度的可扩展性。以下实现用ToolRegistry维护函数名与执行逻辑的映射:

# tool_registry.py - 工具注册中心fromtypingimportDict,Any,Callable,List,OptionalimportinspectimportjsonfromfunctoolsimportwrapsclassToolRegistry:""" 工具注册中心:管理所有可调用工具 职责: 1. 注册工具(名称 + 描述 + 参数Schema + 执行函数) 2. 生成OpenAI兼容的工具定义列表 3. 根据工具名称执行对应的函数 """def__init__(self):self._tools:Dict[str,Dict[str,Any]]={}defregister(self,name:str,description:str,parameters:Dict[str,Any])->Callable:""" 装饰器:将函数注册为可调用工具 Args: name: 工具唯一名称 description: 工具描述(LLM据此判断何时调用) parameters: JSON Schema格式的参数定义 """defdecorator(func:Callable)->Callable:self._tools[name]={"name":name,"description":description,"parameters":parameters,"func":func}@wraps(func)defwrapper(*args,**kwargs):returnfunc(*args,**kwargs)returnwrapperreturndecoratordefget_tool_definitions(self)->List[Dict[str,Any]]:"""获取所有工具的OpenAI格式定义"""definitions=[]fortoolinself._tools.values():definitions.append({"type":"function","function":{"name":tool["name"],"description":tool["description"],"parameters":tool["parameters"]}})returndefinitionsdefexecute(self,tool_name:str,arguments:Dict[str,Any])->str:""" 执行指定的工具 Returns: 工具执行结果(字符串格式,便于LLM理解) """iftool_namenotinself._tools:returnf"错误:未找到工具 '{tool_name}'"try:tool=self._tools[tool_name]result=tool["func"](**arguments)# 确保返回字符串ifisinstance(result,dict):returnjson.dumps(result,ensure_ascii=False)returnstr(result)exceptExceptionase:returnf"工具执行失败:{str(e)}"defget_tool_names(self)->List[str]:"""获取所有已注册工具的名称"""returnlist(self._tools.keys())

3.2 注册业务工具

以天气查询和订单查询为例:

# tools/business_tools.py - 注册业务工具fromtool_registryimportToolRegistryimportjsonfromdatetimeimportdatetime registry=ToolRegistry()@registry.register(name="get_weather",description="获取指定城市的当前天气信息。当用户询问天气、温度、穿衣建议时使用。",parameters={"type":"object","properties":{"city":{"type":"string","description":"城市名称,如:北京、上海、深圳"},"unit":{"type":"string","enum":["celsius","fahrenheit"],"description":"温度单位,默认为celsius"}},"required":["city"]})defget_weather(city:str,unit:str="celsius")->dict:"""模拟天气查询API"""# 实际项目中替换为真实API调用weather_data={"北京":{"temp":22,"condition":"晴","humidity":45},"上海":{"temp":28,"condition":"多云","humidity":60},"深圳":{"temp":30,"condition":"阵雨","humidity":75},}data=weather_data.get(city,{"temp":20,"condition":"未知","humidity":50})unit_symbol="°C"ifunit=="celsius"else"°F"temp=data["temp"]ifunit=="celsius"elsedata["temp"]*9/5+32return{"city":city,"temperature":f"{temp:.1f}{unit_symbol}","condition":data["condition"],"humidity":f"{data['humidity']}%","update_time":datetime.now().strftime("%Y-%m-%d %H:%M")}@registry.register(name="query_order",description="查询用户订单信息,返回订单状态、物流信息、商品清单等",parameters={"type":"object","properties":{"order_id":{"type":"string","description":"订单编号,格式如ORD20260123001"}},"required":["order_id"]})defquery_order(order_id:str)->dict:"""模拟订单查询"""# 实际项目中查询数据库orders={"ORD20260123001":{"status":"已发货","total_amount":299.00,"items":[{"name":"智能手环","quantity":1,"price":299.00}],"logistics":"顺丰速运 SF1234567890","estimated_delivery":"2026-01-25"}}result=orders.get(order_id)ifresult:result["order_id"]=order_idreturnresultreturn{"error":f"未找到订单{order_id}","order_id":order_id}

3.3 LLM客户端集成

实现带Function Calling的LLM客户端:

# llm_client.py - 带Function Calling的LLM客户端importjsonfromtypingimportList,Dict,Any,OptionalimporthttpxclassFunctionCallingLLMClient:""" 支持Function Calling的LLM客户端 完整流程: 1. 发送用户消息 + 工具定义给LLM 2. 如果LLM返回tool_calls,执行对应的工具 3. 将工具结果发回LLM 4. 返回LLM的最终回复 """def__init__(self,base_url:str="http://localhost:11434/v1",model:str="qwen2.5:7b",tool_registry=None):self.base_url=base_url self.model=model self.tool_registry=tool_registry self.client=httpx.Client(timeout=30.0)defchat_with_tools(self,user_message:str,tools:Optional[List[Dict]]=None,system_prompt:Optional[str]=None)->Dict[str,Any]:""" 带工具调用的完整对话 Returns: { "role": "assistant", "content": "最终回复内容", "tool_calls_executed": ["tool1", "tool2"], "iterations": 2 } """messages=[]ifsystem_prompt:messages.append({"role":"system","content":system_prompt})messages.append({"role":"user","content":user_message})# 如果没有传入工具定义,从注册中心获取iftoolsisNoneandself.tool_registry:tools=self.tool_registry.get_tool_definitions()iterations=0max_iterations=5executed_tools=[]whileiterations<max_iterations:iterations+=1# 调用LLMresponse=self._call_llm(messages,tools)ifnotresponse:return{"role":"assistant","content":"系统调用失败,请稍后重试","tool_calls_executed":executed_tools,"iterations":iterations}message=response.get("choices",[{}])[0].get("message",{})messages.append(message)# 检查是否有工具调用tool_calls=message.get("tool_calls")ifnottool_calls:# 无工具调用,返回最终回复return{"role":"assistant","content":message.get("content",""),"tool_calls_executed":executed_tools,"iterations":iterations}# 执行所有工具调用fortool_callintool_calls:function=tool_call.get("function",{})tool_name=function.get("name","")arguments=function.get("arguments","{}")# 解析参数try:args=json.loads(arguments)ifisinstance(arguments,str)elseargumentsexcept:args={}# 执行工具ifself.tool_registry:result=self.tool_registry.execute(tool_name,args)else:result=f"错误:未找到工具执行器,无法执行{tool_name}"executed_tools.append(tool_name)# 将工具结果添加到消息历史messages.append({"role":"tool","name":tool_name,"content":result,"tool_call_id":tool_call.get("id","")})# 超过最大迭代次数return{"role":"assistant","content":"抱歉,处理您的请求需要的步骤过多,请简化后重试。","tool_calls_executed":executed_tools,"iterations":iterations}def_call_llm(self,messages:List[Dict],tools:Optional[List[Dict]]=None)->Dict:"""调用LLM API"""payload={"model":self.model,"messages":messages,"temperature":0.3}iftools:payload["tools"]=tools payload["tool_choice"]="auto"try:response=self.client.post(f"{self.base_url}/chat/completions",json=payload)response.raise_for_status()returnresponse.json()exceptExceptionase:print(f"LLM调用失败:{e}")return{}

3.4 完整调用示例

# main.py - 完整使用示例fromtool_registryimportToolRegistryfromtools.business_toolsimportregistryasbusiness_registryfromllm_clientimportFunctionCallingLLMClient# 创建客户端client=FunctionCallingLLMClient(base_url="http://localhost:11434/v1",model="qwen2.5:7b",tool_registry=business_registry)# 场景1:天气查询result=client.chat_with_tools(user_message="北京今天天气怎么样?适合出门吗?",system_prompt="你是一个专业的天气助手,基于工具查询结果回答问题。")print(f"回答:{result['content']}")print(f"调用的工具:{result['tool_calls_executed']}")# 场景2:订单查询result=client.chat_with_tools(user_message="帮我查一下订单ORD20260123001的状态",system_prompt="你是电商客服助手。")print(f"回答:{result['content']}")# 场景3:多步骤调用result=client.chat_with_tools(user_message="帮我比较一下北京和上海今天的天气哪个更适合户外活动",)print(f"回答:{result['content']}")print(f"调用的工具:{result['tool_calls_executed']}")

四、分层设计:FC、Skill与MCP

在企业级Agent架构中,Function Calling并非孤立存在,而是遵循清晰的分层逻辑:

层级组件职责粒度
上层MCP(模型能力中台)统一注册、网关、治理平台层
中层Skill(技能)业务编排、FC聚合业务层
底层FunctionCall(FC)原子能力执行原子层

核心规则:自上而下依赖,禁止反向依赖

  • FunctionCall是“螺丝钉”:不可拆分的最小原子能力,如“查询员工基础信息”“数学表达式计算”
  • Skill是“成品工具”:将多个FC按业务逻辑编排,如“员工薪资查询Skill”内部编排三个FC
  • MCP是“工具仓库+管理员”:统一能力注册中心、调用网关与权限治理平台

这种分层设计的价值在于:给大模型暴露粗粒度的Skill,而非几十个零散FC,减少大模型的决策成本,提升调用准确性

五、最佳实践与常见陷阱

5.1 六大最佳实践

1. 描述即契约function.description是模型理解的唯一依据,务必清晰、准确。所有工具的name和description必须使用英文,这是目前所有LLM API的标准格式要求。

2. 安全第一:敏感操作(如转账、删除)需增加人工确认环节。绝不要让LLM访问任何未经脱敏的敏感数据,所有函数调用应经过权限审计。

3. 避免过早触发:等待用户意图明确后再调用工具,尤其是写操作。建议在系统Prompt中明确策略:仅当所有必需参数齐全时才调用写工具,参数缺失时先询问用户。

4. 保持工具结果简洁:返回结果会被注入模型上下文,大payload增加Token消耗。只返回回答所需的关键字段。

5. 处理并行调用:如果一次触发了多个工具调用,利用并发机制执行以缩短响应时间。

6. 记录日志:生产系统应记录工具调用生命周期:触发时间、payload、执行结果、状态,便于调试。

5.2 常见陷阱

陷阱后果解决方案
过度设计为边缘场景设计复杂接口“最小可行函数”原则,先核心功能后迭代
语义漂移函数描述与实际实现不符建立函数版本管理和兼容性策略
执行失控未设置超时和重试机制每个工具调用设超时,失败时反馈错误让模型自愈
参数校验缺失模型生成的非法参数导致崩溃执行前严格校验类型和范围

结语

Function Calling的出现,标志着大模型从“文本生成器”向“操作系统核心”的转变。它不仅是Agent开发的核心基石,更是连接“不确定的大模型输出”与“确定的业务逻辑”之间的唯一桥梁。

正确打开Function Calling的方式,不是简单地“给LLM塞一堆函数定义”,而是:

  1. 设计高质量的Tool Schema——description是灵魂,参数要显式明确
  2. 构建健壮的注册与执行系统——意图在模型,执行在代码
  3. 遵循分层架构——FC为原子层,Skill为业务层,MCP为治理层
  4. 严守安全与可靠性底线——权限、超时、日志、降级缺一不可

正如行业实践所示,Function Calling架构使系统维护成本降低40%,新功能上线周期从2周缩短至2天。对于开发者而言,掌握Function Calling设计模式已成为构建下一代智能应用的关键能力。