Function Calling 错误处理与重试策略:让 LLM 工具调用从脆弱走向可靠
一、工具调用的脆弱性:Function Calling 为什么比普通 API 更容易失败
LLM 的 Function Calling 机制让大模型具备了调用外部工具的能力,但这个能力在生产环境中异常脆弱。与普通 API 调用不同,Function Calling 的失败点分布在三个完全不同的层面,每一层都有独特的错误模式。
第一层是模型输出层。LLM 生成的函数调用参数可能格式错误:JSON 解析失败、枚举值超出范围、必填参数缺失。GPT-4 在简单函数上的参数格式错误率约 2%,但在参数超过 5 个的复杂函数上,错误率上升到 8%。更棘手的是,模型可能调用不存在的函数,或者传入语义正确但类型不匹配的参数。
第二层是网络传输层。LLM API 本身可能超时、限流或返回 5xx 错误。国内调用 OpenAI API 的超时率在高峰期可达 5%,而函数调用的响应时间比普通 Chat 更长(因为需要等待模型生成结构化输出),进一步增加了超时概率。
第三层是工具执行层。即使模型正确生成了调用参数,工具本身的执行也可能失败:数据库连接超时、第三方 API 返回错误、文件不存在。这些错误的处理逻辑与普通 API 调用类似,但有一个关键区别——当工具执行失败后,我们需要将错误信息回传给 LLM,让它决定是重试还是换一种方式完成任务。
这三层错误叠加起来,一个包含 3 次工具调用的 Agent 任务,成功率可能低至 85%。如果不做系统性的错误处理和重试,生产环境根本无法稳定运行。
二、Function Calling 的错误分类与重试决策模型
不同类型的错误需要不同的处理策略。盲目重试不仅浪费 Token,还可能让情况更糟。
flowchart TD A[Function Calling 错误] --> B{错误类型判断} B -->|模型输出错误| C{参数可修复?} C -->|是| D[自动修正参数后重试] C -->|否| E[将错误反馈给 LLM 重新生成] B -->|网络传输错误| F{是否可重试?} F -->|限流/超时| G[指数退避重试] F -->|认证失败| H[立即报错,不重试] B -->|工具执行错误| I{是否幂等?} I -->|是| J[带退避重试] I -->|否| K[回传 LLM 决策] D --> L[成功?] E --> L G --> L J --> L K --> L L -->|否| M[达到最大重试次数] M --> N[降级处理或报错]模型输出错误分为两类:可自动修正的(如日期格式从2024-1-1修正为2024-01-01)和不可修正的(如调用了不存在的函数)。前者可以通过 Schema 校验后自动修正重试,后者必须将错误信息回传 LLM 让它重新选择。
网络传输错误需要区分可重试和不可重试。限流(429)和超时可以重试,但认证失败(401)重试无意义。重试时必须使用指数退避,避免雪崩。
工具执行错误最复杂。如果工具是幂等的(如查询操作),可以直接重试;如果是非幂等的(如创建订单),重试可能导致重复操作,必须将错误回传 LLM 让它决定下一步。
三、生产级错误处理与重试框架
3.1 错误分类与重试决策
// function_calling.go // Function Calling 的错误处理与重试框架 package agent import ( "context" "encoding/json" "fmt" "math" "time" ) // CallError 函数调用错误,携带错误类型和是否可重试信息 type CallError struct { Type string // model_output / network / tool_execution Retriable bool // 是否可重试 RawError error // 原始错误 Suggestion string // 给 LLM 的修正建议 } func (e *CallError) Error() string { return fmt.Sprintf("[%s] %v (retriable=%v)", e.Type, e.RawError, e.Retriable) } // RetryConfig 重试配置 type RetryConfig struct { MaxRetries int // 最大重试次数 InitialDelay time.Duration // 初始退避时间 MaxDelay time.Duration // 最大退避时间 Multiplier float64 // 退避倍数 } var DefaultRetryConfig = RetryConfig{ MaxRetries: 3, InitialDelay: 1 * time.Second, MaxDelay: 30 * time.Second, Multiplier: 2.0, } // classifyError 对错误进行分类,决定重试策略 func classifyError(err error) *CallError { if err == nil { return nil } // JSON 解析错误 → 模型输出错误,可重试(让 LLM 重新生成) if isJSONError(err) { return &CallError{ Type: "model_output", Retriable: true, RawError: err, Suggestion: "函数调用参数格式错误,请检查 JSON 格式后重新生成", } } // 参数校验错误 → 模型输出错误,尝试自动修正 if isValidationError(err) { return &CallError{ Type: "model_output", Retriable: true, RawError: err, Suggestion: extractValidationSuggestion(err), } } // 限流错误 → 网络错误,可重试 if isRateLimitError(err) { return &CallError{ Type: "network", Retriable: true, RawError: err, } } // 认证错误 → 网络错误,不可重试 if isAuthError(err) { return &CallError{ Type: "network", Retriable: false, RawError: err, } } // 默认归为工具执行错误 return &CallError{ Type: "tool_execution", Retriable: true, RawError: err, } }3.2 带重试的函数调用执行器
// executor.go // 函数调用执行器,封装重试逻辑和错误回传 type FunctionExecutor struct { llmClient LLMClient retryConfig RetryConfig tools map[string]ToolDefinition } // ExecuteWithRetry 执行函数调用,带重试和 LLM 反馈 func (e *FunctionExecutor) ExecuteWithRetry( ctx context.Context, messages []Message, maxRounds int, ) (*FunctionResult, error) { for round := 0; round < maxRounds; round++ { // 第一步:调用 LLM 获取函数调用决策 response, err := e.llmClient.Chat(ctx, messages) if err != nil { callErr := classifyError(err) if callErr.Retriable && round < e.retryConfig.MaxRetries { delay := e.backoffDelay(round) time.Sleep(delay) continue } return nil, fmt.Errorf("LLM 调用失败: %w", err) } // 如果 LLM 没有发起函数调用,直接返回文本响应 if !response.HasFunctionCall() { return &FunctionResult{Content: response.Content}, nil } // 第二步:校验函数调用参数 fnCall := response.FunctionCall if err := e.validateFunctionCall(fnCall); err != nil { // 参数校验失败,将错误反馈给 LLM 重新生成 messages = append(messages, Message{ Role: "assistant", Content: response.ToMessage(), }, Message{ Role: "user", Content: fmt.Sprintf("函数调用参数错误: %v。请修正后重新调用。", err), }) continue } // 第三步:执行工具函数 result, err := e.executeTool(ctx, fnCall) if err != nil { callErr := classifyError(err) if callErr.Retriable && round < e.retryConfig.MaxRetries { // 可重试错误:将错误信息回传 LLM messages = append(messages, Message{ Role: "assistant", Content: response.ToMessage(), }, Message{ Role: "function", Name: fnCall.Name, Content: fmt.Sprintf("执行失败: %v。%s", err, callErr.Suggestion), }) continue } return nil, fmt.Errorf("工具执行失败: %w", err) } return result, nil } return nil, fmt.Errorf("达到最大重试轮次 %d,任务失败", maxRounds) } // backoffDelay 计算指数退避延迟 func (e *FunctionExecutor) backoffDelay(retryCount int) time.Duration { delay := float64(e.retryConfig.InitialDelay) * math.Pow(e.retryConfig.Multiplier, float64(retryCount)) if delay > float64(e.retryConfig.MaxDelay) { return e.retryConfig.MaxDelay } return time.Duration(delay) } // validateFunctionCall 校验函数调用的参数格式 func (e *FunctionExecutor) validateFunctionCall(fnCall *FunctionCall) error { tool, exists := e.tools[fnCall.Name] if !exists { return fmt.Errorf("函数 %s 不存在,可用函数: %v", fnCall.Name, e.availableFunctions()) } // 校验参数是否符合 JSON Schema var params map[string]interface{} if err := json.Unmarshal([]byte(fnCall.Arguments), ¶ms); err != nil { return fmt.Errorf("参数 JSON 解析失败: %w", err) } return tool.ValidateParams(params) }四、架构权衡与适用边界
重试轮次与 Token 消耗的矛盾。每次将错误信息回传 LLM 重新生成,都会消耗额外的 Token。一个 3 轮重试的任务,Token 消耗可能达到单次调用的 3-4 倍。对于使用 GPT-4 等高成本模型的场景,需要设置合理的重试上限(建议不超过 3 轮),并在预算耗尽时降级到更便宜的模型。
自动修正与 LLM 重新生成的选择。对于简单的格式错误(如日期格式、枚举值大小写),自动修正比回传 LLM 更高效,因为避免了额外的模型调用。但自动修正逻辑本身也可能出错,特别是当参数语义复杂时。建议只对确定性高的格式错误做自动修正,其余情况交给 LLM 处理。
幂等性判断的困难。工具是否幂等,决定了执行失败后能否直接重试。但很多工具的幂等性取决于上下文:数据库查询是幂等的,但"创建用户"在用户名冲突时不是。最佳实践是在工具定义中显式声明幂等性标记,让执行器据此决策。
适用边界:该重试框架适用于工具调用失败率超过 5% 的生产环境。对于内部工具调用成功率接近 100% 的简单场景,简单的 try-catch 即可,引入完整的重试框架属于过度设计。
五、总结
Function Calling 的错误处理需要覆盖模型输出、网络传输、工具执行三个层面,每层有不同的错误模式和重试策略。核心实践包括:第一,对错误进行分类,区分可重试和不可重试错误,避免无意义的重试浪费 Token;第二,将工具执行错误回传 LLM,让模型自主决策下一步行动;第三,使用指数退避控制重试节奏,防止雪崩。工程落地时,需要重点权衡重试轮次与 Token 成本,建议将最大重试轮次控制在 3 轮以内,并对确定性高的格式错误优先使用自动修正而非 LLM 重新生成。