第2126篇:Function Calling的工程实践——让LLM真正"干活"而不只是"说话"
第2126篇:Function Calling的工程实践——让LLM真正"干活"而不只是"说话"
适读人群:构建AI Agent和工具调用系统的工程师 | 阅读时长:约20分钟 | 核心价值:掌握Function Calling的完整实现,处理并行调用、调用链、错误恢复等生产级问题
"我们让AI帮用户查订单,结果AI只是说'我帮您查一下',然后继续聊天,什么都没查。"
这是没有正确实现Function Calling的典型现象。早期的LLM集成,很多团队是这样做的:在Prompt里告诉AI"你可以查询数据库,格式是{action: query, table: orders}",然后期待AI输出这个格式,再解析执行。
这种方式脆弱、不可靠——LLM输出的格式经常不标准,解析失败率高。OpenAI的Function Calling(现在叫Tool Use)从协议层解决了这个问题:LLM直接输出结构化的工具调用请求,开发者只需要按格式执行。
Function Calling的工作原理
/**
* Function Calling的完整流程
*
* ===== 第一轮:用户提问 =====
*
* 用户:我的订单 #ORD-2024001 现在是什么状态?
*
* 开发者:发送给LLM的请求包含:
* 1. 用户消息
* 2. 工具定义(描述LLM可以调用哪些函数)
*
* LLM响应:不是直接回答,而是:
* {
* "tool_calls": [{
* "id": "call_abc123",
* "type": "function",
* "function": {
* "name": "get_order_status",
* "arguments": "{\"order_id\": \"ORD-2024001\"}"
* }
* }]
* }
*
* ===== 第二轮:执行工具,返回结果 =====
*
* 开发者:
* 1. 解析LLM的tool_calls
* 2. 调用实际的 get_order_status("ORD-2024001")
* 3. 把执行结果发回给LLM
*
* LLM收到工具结果后,给出最终回答:
* "您的订单 #ORD-2024001 目前已发货,预计明天送达。"
*
* ===== 关键设计原则 =====
*
* 1. LLM决定调什么、何时调(智能)
* 2. 开发者控制怎么调(安全)
* 3. 工具定义越精确,LLM调用越准确
* 4. 工具结果要结构化,方便LLM理解和组合
*/工具定义与注册
/**
* 工具注册中心
*
* 集中管理所有可被LLM调用的工具
* 工具的定义决定了LLM能不能正确理解和调用它
*/
@Service
@Slf4j
public class ToolRegistry {
private final Map<String, RegisteredTool> tools = new LinkedHashMap<>();
/**
* 注册工具
*
* 工具定义的质量直接影响LLM的调用准确率
* 名称和描述要清晰、准确
*/
public void register(RegisteredTool tool) {
tools.put(tool.getName(), tool);
log.info("工具已注册: name={}", tool.getName());
}
/**
* 获取所有工具的定义(发给LLM的格式)
*/
public List<ToolSpecification> getAllToolSpecs() {
return tools.values().stream()
.map(RegisteredTool::toToolSpecification)
.toList();
}
public RegisteredTool getTool(String name) {
return tools.get(name);
}
/**
* 订单查询工具定义
*
* 注意description的写法:要让LLM知道什么时候该用这个工具
*/
public static RegisteredTool orderStatusTool(OrderService orderService) {
return RegisteredTool.builder()
.name("get_order_status")
.description("查询订单状态和物流信息。当用户询问订单进度、物流状态、" +
"预计送达时间时,使用此工具。需要提供订单号。")
.parameters(ToolParameters.builder()
.addRequired("order_id", "string",
"订单编号,格式为ORD-开头的字符串,例如:ORD-2024001")
.build())
.executor(args -> {
String orderId = args.get("order_id").toString();
OrderStatus status = orderService.getOrderStatus(orderId);
return Map.of(
"order_id", orderId,
"status", status.getStatus(),
"location", status.getCurrentLocation(),
"estimatedDelivery", status.getEstimatedDelivery().toString(),
"trackingUrl", status.getTrackingUrl()
);
})
.build();
}
/**
* 退款申请工具
*/
public static RegisteredTool refundTool(RefundService refundService) {
return RegisteredTool.builder()
.name("submit_refund_request")
.description("提交退款申请。当用户明确表示要退款时使用。" +
"调用前必须确认用户已同意退款条款。")
.parameters(ToolParameters.builder()
.addRequired("order_id", "string", "要退款的订单号")
.addRequired("reason", "string", "退款原因,如:质量问题/不想要了/发错货")
.addOptional("additional_notes", "string", "补充说明")
.build())
.executor(args -> {
String orderId = args.get("order_id").toString();
String reason = args.get("reason").toString();
String notes = args.getOrDefault("additional_notes", "").toString();
RefundResult result = refundService.submitRefund(orderId, reason, notes);
return Map.of(
"success", result.isSuccess(),
"refund_id", result.getRefundId(),
"expected_days", result.getExpectedDays(),
"message", result.getMessage()
);
})
.build();
}
@Data
@Builder
public static class RegisteredTool {
private String name;
private String description;
private ToolParameters parameters;
private Function<Map<String, Object>, Object> executor;
public ToolSpecification toToolSpecification() {
return ToolSpecification.builder()
.name(name)
.description(description)
.parameters(parameters.toJsonSchema())
.build();
}
public Object execute(Map<String, Object> args) {
return executor.apply(args);
}
}
}工具调用执行引擎
/**
* 工具调用执行引擎
*
* 核心逻辑:
* 1. 调用LLM,携带工具定义
* 2. 如果LLM返回工具调用请求,执行工具
* 3. 把结果发回LLM,得到最终回答
* 4. 支持多轮工具调用(一次对话可能调多个工具)
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class ToolCallingEngine {
private final ChatLanguageModel llm;
private final ToolRegistry toolRegistry;
// 最大工具调用轮数(防止无限循环)
private static final int MAX_TOOL_CALL_ROUNDS = 5;
/**
* 带工具调用的对话处理
*/
public String chat(String userMessage, List<ChatMessage> history) {
List<ChatMessage> messages = new ArrayList<>(history);
messages.add(UserMessage.from(userMessage));
// 获取所有工具的定义
List<ToolSpecification> toolSpecs = toolRegistry.getAllToolSpecs();
int round = 0;
while (round < MAX_TOOL_CALL_ROUNDS) {
round++;
// 调用LLM(带工具定义)
Response<AiMessage> response = llm.generate(messages, toolSpecs);
AiMessage aiMessage = response.content();
messages.add(aiMessage);
// 检查LLM是否要调用工具
if (!aiMessage.hasToolExecutionRequests()) {
// 没有工具调用,这就是最终回答
log.debug("工具调用完成,共{}轮", round);
return aiMessage.text();
}
// 执行所有工具调用(可能有并行调用)
List<ToolExecutionResultMessage> toolResults =
executeToolCalls(aiMessage.toolExecutionRequests());
// 把工具结果加到消息历史
messages.addAll(toolResults);
log.debug("第{}轮工具调用,执行了{}个工具", round, toolResults.size());
}
// 超过最大轮数,返回提示
log.warn("工具调用超过最大轮数: userMessage={}", userMessage);
return "抱歉,处理您的请求时遇到了问题,请稍后重试。";
}
/**
* 执行工具调用列表
*
* 支持并行执行(LLM可能同时请求多个工具)
*/
private List<ToolExecutionResultMessage> executeToolCalls(
List<ToolExecutionRequest> requests) {
return requests.parallelStream()
.map(this::executeSingleToolCall)
.toList();
}
/**
* 执行单个工具调用
*/
private ToolExecutionResultMessage executeSingleToolCall(ToolExecutionRequest request) {
String toolName = request.name();
String toolId = request.id();
log.debug("执行工具: name={}, id={}", toolName, toolId);
// 查找工具
ToolRegistry.RegisteredTool tool = toolRegistry.getTool(toolName);
if (tool == null) {
log.error("工具不存在: name={}", toolName);
return ToolExecutionResultMessage.from(
toolId, toolName,
"{\"error\": \"工具不存在: " + toolName + "\"}"
);
}
// 解析参数
Map<String, Object> args;
try {
args = new ObjectMapper().readValue(
request.arguments(),
new TypeReference<Map<String, Object>>() {}
);
} catch (Exception e) {
log.error("工具参数解析失败: {}", request.arguments(), e);
return ToolExecutionResultMessage.from(
toolId, toolName,
"{\"error\": \"参数格式错误\"}"
);
}
// 执行工具
try {
long startMs = System.currentTimeMillis();
Object result = tool.execute(args);
long latencyMs = System.currentTimeMillis() - startMs;
log.debug("工具执行完成: name={}, latency={}ms", toolName, latencyMs);
String resultJson = new ObjectMapper().writeValueAsString(result);
return ToolExecutionResultMessage.from(toolId, toolName, resultJson);
} catch (Exception e) {
log.error("工具执行异常: name={}", toolName, e);
return ToolExecutionResultMessage.from(
toolId, toolName,
"{\"error\": \"工具执行失败,请稍后重试\"}"
);
}
}
}工具调用的安全控制
/**
* 工具调用安全层
*
* LLM可能被诱导调用不应该调用的工具
* 或者以不合理的参数调用工具
*
* 安全原则:
* 1. 高风险操作(删除、支付)需要用户明确确认
* 2. 工具调用要有权限校验
* 3. 参数要做范围和格式校验
* 4. 敏感操作要记录审计日志
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class SecureToolCallWrapper {
private final AiAuditService auditService;
/**
* 执行工具调用前的安全检查
*/
public ToolCallSafetyResult checkSafety(
String userId,
ToolExecutionRequest request,
Map<String, Object> parsedArgs) {
String toolName = request.name();
// 1. 高风险工具需要用户确认
if (isHighRiskTool(toolName)) {
return ToolCallSafetyResult.requiresConfirmation(
buildConfirmationMessage(toolName, parsedArgs)
);
}
// 2. 检查参数异常
if (hasAbnormalParameters(toolName, parsedArgs)) {
log.warn("工具参数异常: userId={}, tool={}, args={}",
userId, toolName, parsedArgs);
return ToolCallSafetyResult.blocked("参数包含不合规内容");
}
// 3. 速率限制(防止LLM被诱导反复调用工具)
if (isRateLimitExceeded(userId, toolName)) {
return ToolCallSafetyResult.blocked("调用频率超限,请稍后再试");
}
return ToolCallSafetyResult.allowed();
}
private boolean isHighRiskTool(String toolName) {
return List.of("submit_refund_request", "delete_account",
"transfer_funds", "cancel_order").contains(toolName);
}
private String buildConfirmationMessage(String toolName, Map<String, Object> args) {
return switch (toolName) {
case "submit_refund_request" ->
String.format("您正在申请退款,订单号:%s,原因:%s。\n请回复\"确认\"以继续,或回复\"取消\"放弃。",
args.get("order_id"), args.get("reason"));
case "cancel_order" ->
String.format("您正在取消订单:%s。此操作不可撤销,确认取消吗?",
args.get("order_id"));
default -> "请确认是否执行此操作(回复\"确认\"或\"取消\")";
};
}
private boolean hasAbnormalParameters(String toolName, Map<String, Object> args) {
// 检查是否有SQL注入、命令注入等
return args.values().stream()
.filter(v -> v instanceof String)
.map(Object::toString)
.anyMatch(v -> v.contains("'; DROP") || v.contains("../") ||
v.contains("<script>"));
}
private boolean isRateLimitExceeded(String userId, String toolName) {
// 实现限流逻辑
return false;
}
@Data
@Builder
public static class ToolCallSafetyResult {
private SafetyStatus status;
private String message;
public static ToolCallSafetyResult allowed() {
return ToolCallSafetyResult.builder().status(SafetyStatus.ALLOWED).build();
}
public static ToolCallSafetyResult blocked(String reason) {
return ToolCallSafetyResult.builder()
.status(SafetyStatus.BLOCKED).message(reason).build();
}
public static ToolCallSafetyResult requiresConfirmation(String confirmMessage) {
return ToolCallSafetyResult.builder()
.status(SafetyStatus.REQUIRES_CONFIRMATION).message(confirmMessage).build();
}
public enum SafetyStatus { ALLOWED, BLOCKED, REQUIRES_CONFIRMATION }
}
}实践建议
工具描述是Function Calling准确率的关键
很多工具调用失败,不是代码问题,是description写得不够清晰。LLM是根据你的描述来判断"什么时候该调哪个工具"的。好的描述应该包含:(1)什么场景下使用;(2)输入参数的具体格式(比如"订单号格式:ORD-YYYYXXXX");(3)工具的边界(比如"只能查询30天内的订单")。把工具描述当成写给LLM的API文档,越精确越好。
并行工具调用是容易忽略的性能提升
当LLM需要同时查询多个信息时(比如"帮我查订单#001的状态和退款申请进度"),它会在一次响应中返回多个tool_calls。很多实现是串行执行这些调用,但它们完全可以并行执行。对于I/O密集型工具(数据库查询、外部API),并行执行能把延迟从3秒降到1秒。用CompletableFuture或ParallelStream处理多个工具调用是值得的。
高风险操作的二次确认不可省略
LLM在某些情况下会过于"积极"地帮用户执行操作(特别是Prompt被诱导时)。对于退款、取消订单、删除数据等不可逆操作,一定要在代码层面做二次确认,不能完全信任LLM的判断。这不是怀疑LLM能力,而是对高风险操作应该有的工程保护。
