🛠️ AI Agent 工具调用实战:Function Calling、Tool Use、API 集成从入门到生产
从裸Prompt到可靠工具调用,Agent 落地的核心工程实践
2026年7月 · 测试开发手记
"AI Agent 能不能干活,90% 看工具调用是否稳。Function Calling 不是调个API就完事——参数校验、错误重试、上下文注入,每一个环节都可能让 Agent 从'智能'变成'智障'。"
🏷️ AI测试🤖 Agent⚙️ Function Calling💡 Tool Use⏱ 阅读约 14 分钟💥 痛点:Agent 一调用工具就翻车
我去年开始折腾 AI Agent,最头疼的就是工具调用。明明 LLM 已经理解了我的意图,但一让它调外部 API 就各种翻车:参数格式不对、漏传必填字段、返回结果不会解析、甚至连续调用同一个工具导致死循环。
后来我专门花了两周时间,把 Function Calling 的整个链路拆开来看——从 Prompt 定义、参数 Schema、到执行结果注入、错误重试,一步一步调优。今天就把这些实战经验掰开揉碎讲清楚。
🧑💻 测试开发视角: 我们团队用 Agent 做自动化测试数据生成,一开始工具调用成功率只有 62%——一半以上的调用要么参数错误要么超时。后来加了参数校验 + 重试 + 上下文压缩,成功率飙到 97%。工程化不是锦上添花,是生存刚需。 🔍 核心功能拆解:工具调用的四个关键环节
1️⃣ 函数定义与 Schema 设计
一切从定义开始。OpenAI 的 Function Calling、Anthropic 的 Tool Use、以及开源模型(Qwen、DeepSeek)都支持 JSON Schema 描述工具。但细节决定成败:
① 参数描述要带例子 —— LLM 对自然语言敏感,比如 "location: 城市名称,如'北京'、'上海'" 比 "location: string" 准确率高 30%。
② 必填参数用 required 标记 —— 不标记的话 LLM 经常漏传,尤其是可选参数一多,模型会偷懒。
③ 枚举值用 enum 限制 —— 比如天气查询的 unit 字段,enum: ["celsius", "fahrenheit"],避免模型传 "C" 或 "摄氏度"。
下面是我在项目里用的一个真实工具定义(简化版):
{ "name": "get_weather", "description": "查询指定城市的实时天气,支持摄氏/华氏", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "城市名称,例如 '北京'、'上海'、'London'" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "温度单位,默认 celsius" } }, "required": ["location"] }}注意 description 里的具体例子,这是让 LLM 准确理解的关键。我对比过,不加例子的调用失败率高 22%。
2️⃣ 参数校验 & 自动修复
LLM 生成的参数不可能 100% 符合 Schema。我做了个轻量校验层,在调用真实工具前做一次强制校验:
import jsonfrom jsonschema import validate, ValidationErrordef validate_tool_args(schema, arguments): """校验 LLM 生成的参数,失败时返回修复建议""" try: # 尝试解析 JSON(LLM 可能返回字符串) if isinstance(arguments, str): arguments = json.loads(arguments) validate(instance=arguments, schema=schema) return arguments, None # 校验通过 except ValidationError as e: # 自动修复:补默认值 / 类型转换 fixed = fix_arguments(schema, arguments, e) return fixed, f"自动修复参数: {e.message}" except json.JSONDecodeError: # 最坏情况:强制用空对象,让 LLM 重试 return {}, "参数 JSON 解析失败,请重新生成"def fix_arguments(schema, args, error): """简单修复:必填缺失则补空字符串,类型错误则强转""" path = list(error.path) if not path: return args field = path[0] props = schema.get("properties", {}) if field not in props: return args # 如果是必填缺失,补默认值 if error.validator == 'required': for req in error.schema.get('required', []): if req not in args: args[req] = "" # 或从 description 提取示例 return args这个校验层上线后,工具调用成功率从 68% 直接升到 89%。关键点:不要直接 reject,要自动修复 + 日志告警,既保证流程不中断,又能追踪 LLM 的常见错误模式。
3️⃣ 工具执行 & 结果注入
工具执行后,结果怎么喂回 LLM 是个大学问。我见过直接把整个 JSON 响应塞进上下文的,结果 Token 爆炸,模型反而看不懂。
我的做法是 结构化摘要:
# 原始 API 返回raw_response = { "location": "Beijing", "temperature": 28, "humidity": 65, "wind": {"speed": 12, "direction": "NW"}, "forecast": [{"day": "Mon", "temp": 30}, ...] # 7天预报}# 注入给 LLM 的摘要(只保留关键信息)tool_result_summary = f"""[工具: get_weather]城市: 北京当前温度: 28°C湿度: 65%风速: 12 km/h 西北风简要预报: 周一 30°C, 周二 27°C, 周三 25°C"""这样 LLM 能快速抓住重点,不需要从原始 JSON 里自己提取。而且 Token 消耗降低 70%,响应速度明显提升。
4️⃣ 错误重试 & 降级策略
工具调用不可能永远成功——网络超时、API 限流、参数非法。我设计了一个三级降级策略:
第一级:自动重试 —— 最多 3 次,每次等待 1s/2s/4s(指数退避)。只重试可恢复错误(5xx、超时)。
第二级:参数修正重试 —— 如果返回 400 或参数校验失败,让 LLM 重新生成参数(带错误信息)。
第三级:降级响应 —— 如果所有重试都失败,返回一个默认的 "服务暂不可用" 消息,不让 Agent 卡死。
def call_with_retry(tool_func, args, max_retries=3): for attempt in range(max_retries): try: result = tool_func(**args) return result, None except (TimeoutError, ConnectionError) as e: wait = 2 ** attempt print(f"重试 {attempt+1}/{max_retries}, 等待 {wait}s: {e}") time.sleep(wait) except ValueError as e: # 参数错误,需要 LLM 修正 return None, f"参数错误: {e}" return None, "所有重试失败,服务不可用"这个策略让 Agent 在真实生产环境中的 工具调用可用性达到 99.3%(压测数据)。
🧑💻 测试开发视角: 我们压测了一个星期,发现最坑的不是 LLM 生成参数错误,而是 工具返回结果太大 导致 LLM 上下文溢出。后来加了 max_tokens 限制 + 结果摘要,才彻底解决。生产环境一定要给工具结果设上限,否则 Agent 会 silently 挂掉。 ⚡ 实战技巧:让 Agent 工具调用更稳
技巧1:用 system prompt 约束工具选择
我见过 Agent 在多个工具之间反复横跳,甚至同时调用两个冲突的工具。在 system prompt 里加一句:
"每次只调用一个工具,等待结果后再决定下一步。不要同时发起多个工具调用。"效果立竿见影——多工具并发调用次数下降 80%。
技巧2:工具描述里加使用条件
比如一个 "发送邮件" 工具,在 description 里写:"仅当用户明确要求发送邮件时调用。不要主动发送未经确认的邮件。" 这样能避免 Agent 自作主张。
技巧3:用 mock 模式做回归测试
我们团队写了个 ToolCallRecorder,把每次工具调用的请求/响应都录下来,然后回放做回归。这样改 Prompt 或升级模型后,能快速发现工具调用行为是否退化。
# 伪代码:录制模式class ToolCallRecorder: def __init__(self, mode="live"): self.recordings = [] self.mode = mode def call(self, tool_name, args): if self.mode == "record": result = real_tool_call(tool_name, args) self.recordings.append((tool_name, args, result)) return result elif self.mode == "replay": # 匹配最近的录制 for name, a, r in self.recordings: if name == tool_name and a == args: return r raise ValueError("未找到录制数据")这个录制器让我们在迭代 Prompt 时,工具调用回归测试从 3 天缩短到 2 小时。
技巧4:给工具调用加超时熔断
如果某个工具连续失败 5 次,自动熔断 30 秒,避免 Agent 死循环。用 circuit breaker 模式实现:
class CircuitBreaker: def __init__(self, threshold=5, reset_timeout=30): self.failures = 0 self.threshold = threshold self.reset_timeout = reset_timeout self.last_failure_time = 0 def call(self, func, *args, **kwargs): if self.failures >= self.threshold: if time.time() - self.last_failure_time < self.reset_timeout: return None, "熔断中" else: self.failures = 0 # 半开 try: result = func(*args, **kwargs) self.failures = 0 return result, None except Exception as e: self.failures += 1 self.last_failure_time = time.time() return None, str(e)📌 踩坑经验总结
1. 不要相信 LLM 的参数格式 —— 即使 GPT-4o 也经常把数字写成字符串,或者漏掉必填字段。必须加校验层。
2. 工具返回结果一定要截断 —— 我们遇到过 LLM 把整个数据库表结构当成工具结果塞进上下文,导致 Token 爆炸。设置 max_result_length=2000 tokens。
3. 工具描述里不要用否定句 —— LLM 对 "不要调用天气工具" 的理解远不如 "只有用户问天气时才调用天气工具"。
4. 并发调用要加锁 —— 多个 Agent 实例同时调用同一个写工具(如数据库写入)会导致数据不一致。用 Redis 分布式锁。
5. 一定要有 fallback 回复 —— 当所有工具都失败时,Agent 应该能说 "我现在无法获取这个信息,请稍后再试",而不是沉默或报错。
AI Agent 的工具调用,本质上是一个 工程问题 而不是模型问题。把校验、重试、熔断、摘要这些基础设施做扎实,Agent 才能真正从 demo 走向生产。
2026年7月 · 测试开发手记 · 欢迎交流