LLM 能力集成:Function Calling 与工具调用的工程实践
LLM 能力集成:Function Calling 与工具调用的工程实践

一、LLM 的行动力缺失:为什么模型只会"说"不会"做"
大语言模型在文本生成方面表现出色,但面对"查询用户订单状态""发送邮件通知""调用支付接口"等需要与外部系统交互的任务时,模型只能输出文本建议,无法直接执行操作。Function Calling 机制正是为弥补这一鸿沟而生——它让模型能够识别何时需要调用工具、选择哪个工具、提取哪些参数,并将执行结果回传给模型继续推理。然而,从 Demo 到生产级实现之间,存在参数校验、错误恢复、并发安全和工具编排等多重工程挑战。
二、Function Calling 的执行流程与协议设计
Function Calling 的核心流程分为四步:工具定义注册、模型决策调用、参数提取校验、结果回传续推。模型并不直接执行函数,而是输出一个结构化的调用意图(包含函数名和参数),由应用层负责实际执行并将结果注入上下文。
sequenceDiagram
participant U as 用户
participant A as 应用层
participant L as LLM
participant T as 工具服务
U->>A: 发送消息
A->>L: 消息 + 工具定义
L->>A: 返回 tool_calls 意图
alt 参数校验通过
A->>T: 执行函数调用
T->>A: 返回执行结果
A->>L: 工具结果 + 历史上下文
L->>A: 最终回复
A->>U: 展示结果
else 参数校验失败
A->>L: 校验错误信息 + 历史上下文
L->>A: 修正后的调用意图
end
关键设计点在于工具定义的 JSON Schema 规范。Schema 的描述质量直接影响模型的参数提取准确率——模糊的描述会导致模型在参数提取时产生幻觉,遗漏必填参数或填入类型不匹配的值。
三、生产级 Function Calling 的工程实现
3.1 工具注册与 Schema 定义
from dataclasses import dataclass
from typing import Callable, Dict, Any, List, Optional
import jsonschema
@dataclass
class ToolDefinition:
"""工具定义:将函数签名、Schema 与执行逻辑绑定"""
name: str
description: str
parameters: Dict[str, Any] # JSON Schema
handler: Callable[..., Any]
timeout_seconds: int = 30
max_retries: int = 2
class ToolRegistry:
"""工具注册中心:统一管理工具定义与执行"""
def __init__(self):
self._tools: Dict[str, ToolDefinition] = {}
def register(self, tool: ToolDefinition):
"""注册工具:校验 Schema 合法性后存入注册表"""
# 校验 Schema 是否为合法的 JSON Schema
try:
jsonschema.Draft7Validator.check_schema(tool.parameters)
except jsonschema.SchemaError as e:
raise ValueError(f"工具 {tool.name} 的参数 Schema 不合法: {e}")
self._tools[tool.name] = tool
def get_openai_tools_format(self) -> List[Dict[str, Any]]:
"""生成 OpenAI API 所需的 tools 参数格式"""
return [
{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.parameters,
},
}
for tool in self._tools.values()
]
async def execute(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
"""执行工具调用:校验参数 → 执行 → 返回结果"""
tool = self._tools.get(tool_name)
if tool is None:
raise ToolNotFoundError(f"未注册的工具: {tool_name}")
# 参数校验:在执行前拦截非法参数,避免下游服务异常
try:
jsonschema.validate(arguments, tool.parameters)
except jsonschema.ValidationError as e:
raise ToolArgumentError(
f"工具 {tool_name} 参数校验失败: {e.message}"
)
# 执行工具调用,带超时与重试
import asyncio
for attempt in range(tool.max_retries + 1):
try:
result = await asyncio.wait_for(
tool.handler(**arguments),
timeout=tool.timeout_seconds,
)
return result
except asyncio.TimeoutError:
if attempt == tool.max_retries:
raise ToolTimeoutError(
f"工具 {tool_name} 执行超时 ({tool.timeout_seconds}s)"
)
except Exception as e:
if attempt == tool.max_retries:
raise ToolExecutionError(f"工具 {tool_name} 执行失败: {e}")
class ToolNotFoundError(Exception): pass
class ToolArgumentError(Exception): pass
class ToolTimeoutError(Exception): pass
class ToolExecutionError(Exception): pass
3.2 多轮工具调用的编排器
import openai
from typing import List, Dict, Any
class FunctionCallingOrchestrator:
"""多轮工具调用编排器:处理模型的连续调用意图"""
def __init__(
self,
client: openai.AsyncOpenAI,
registry: ToolRegistry,
model: str = "gpt-4o",
max_rounds: int = 5,
):
self.client = client
self.registry = registry
self.model = model
self.max_rounds = max_rounds
async def run(self, messages: List[Dict[str, Any]]) -> str:
"""执行多轮工具调用,直到模型输出最终文本回复"""
tools = self.registry.get_openai_tools_format()
for round_idx in range(self.max_rounds):
response = await self.client.chat.completions.create(
model=self.model,
messages=messages,
tools=tools if tools else None,
tool_choice="auto",
)
choice = response.choices[0]
# 模型未调用工具,返回文本回复
if not choice.message.tool_calls:
return choice.message.content or ""
# 将模型的工具调用意图加入消息历史
messages.append(choice.message)
# 依次执行每个工具调用
for tool_call in choice.message.tool_calls:
try:
import json
arguments = json.loads(tool_call.function.arguments)
result = await self.registry.execute(
tool_call.function.name, arguments
)
# 将工具结果加入消息历史
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(
{"result": result}, ensure_ascii=False
),
})
except (ToolArgumentError, ToolNotFoundError) as e:
# 参数错误或工具不存在:将错误信息回传模型,让其修正
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(
{"error": str(e)}, ensure_ascii=False
),
})
except (ToolTimeoutError, ToolExecutionError) as e:
# 执行失败:记录日志,回传错误信息,模型可选择降级方案
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(
{"error": f"执行失败: {str(e)}"}, ensure_ascii=False
),
})
return "工具调用轮次已达上限,请简化请求后重试。"
四、Function Calling 的边界与权衡
Function Calling 的首要风险是参数幻觉。模型可能为必填参数编造不存在的值,或为枚举类型填入 Schema 之外的选项。生产环境中必须在执行前进行严格的 JSON Schema 校验,拒绝非法参数并将错误信息回传模型修正。但校验本身也有成本——过于严格的 Schema 会限制模型的灵活性,过于宽松则失去防护意义。
多轮调用的状态管理是另一个难点。当模型连续调用多个工具时,前一个工具的输出可能影响后一个工具的参数。编排器需要维护完整的消息历史,但上下文窗口有限,长链调用可能触及 Token 上限。截断历史会丢失关键上下文,保留全部则增加延迟和成本。
在安全性方面,工具调用等同于让模型拥有了执行代码的能力。必须建立严格的权限控制——不同用户角色可调用的工具集不同,敏感操作(如删除数据、发送邮件)需要二次确认。缺少权限控制的 Function Calling 等于将系统接口暴露给不可控的输入源。
五、总结
Function Calling 让 LLM 从"只会说"进化为"能做事",但生产级实现需要解决参数校验、错误恢复、多轮编排和权限控制等工程问题。核心实践包括:用 JSON Schema 约束工具参数并在执行前校验,用编排器管理多轮调用的消息历史,用错误回传机制让模型自我修正,用权限控制防止越权操作。在工具设计上,优先选择幂等操作,避免模型重试导致重复执行。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)