【AI Agent工程实战系列②】工具调用的正确姿势——不只是写个函数那么简单

张开发
2026/4/19 23:34:56 15 分钟阅读

分享文章

【AI Agent工程实战系列②】工具调用的正确姿势——不只是写个函数那么简单
先模拟一个场景我们有一个Agent负责处理内部的IT工单,工具列表里有两个长得很像的工具:defget_user_info(user_id:str)-dict:"""获取用户的基本信息"""...defget_user_permissions(user_id:str)-dict:"""获取用户的权限列表"""...某天一个工程师提交了工单:“帮我查一下张三有没有生产环境的部署权限。”Agent调用了get_user_info,拿到了张三的姓名、邮箱、部门,然后基于这些信息"推理"出:张三是高级工程师,所以应该有部署权限——并把这个结论告诉了工单提交者。实际上张三没有这个权限。工程师拿着这个错误结论去操作,碰壁之后才发现问题。工具描述改一个字就能修复这个问题。但如果不知道根因在哪,你可能永远找不到这一个字。工具调用的完整生命周期大多数教程把工具调用简化成这样:LLM → 选工具 → 调用 → 返回结果实际上每个箭头背后都有一堆可能出错的地方:九个环节,每个都有独立的失败模式。我们逐一处理。第一关:工具描述设计——最被低估的工程工作工具描述(Tool Description)是LLM选择工具和提取参数的唯一依据。写得好,工具调用准确率能从60%提升到95%以上。写得差,再好的模型也救不了你。坏的工具描述长什么样# 反例:模糊、歧义、缺少关键信息tools=[{"name":"get_order","description":"获取订单信息","parameters":{"order_id":{"type":"string"}}},{"name":"search_orders","description":"搜索订单","parameters":{"query":{"type":"string"}}},{"name":"get_user_orders","description":"获取用户订单","parameters":{"user_id":{"type":"string"}}}]这三个工具的描述,对LLM来说几乎是一样的。当用户说"帮我查一下我的订单",LLM不知道该选哪个。好的工具描述长什么样tools=[{"name":"get_order_by_id","description":("通过精确的订单号查询单个订单的详细信息,包括订单状态、""商品列表、支付信息和物流追踪号。""【使用场景】用户提供了具体的订单号(如'ORDER-20240315-001')。""【不适用场景】用户只说'我的订单'但没有提供订单号;""需要查询多个订单时。"),"parameters":{"type":"object","properties":{"order_id":{"type":"string","description":("订单号,格式为'ORDER-YYYYMMDD-XXX',""例如'ORDER-20240315-001'。""如果用户没有提供订单号,不要猜测,""应该向用户询问订单号。"),"pattern":"^ORDER-\\d{8}-\\d{3}$"}},"required":["order_id"]}},{"name":"get_user_all_orders","description":("获取指定用户的所有订单列表,按时间倒序排列,最多返回50条。""【使用场景】用户想查看自己的全部订单历史,""或者说'我的所有订单'/'最近的订单'但没有提供订单号。""【不适用场景】用户已经提供了具体订单号;""需要搜索特定条件的订单时。"),"parameters":{"type":"object","properties":{"user_id":{"type":"string","description":"用户ID,从当前会话的用户上下文中获取,不要向用户询问"},"limit":{"type":"integer","description":"返回订单数量,默认10,最大50","default":10,"minimum":1,"maximum":50}},"required":["user_id"]}}]好的工具描述有五个要素:① 说清楚工具做什么(精确,不模糊) ② 说清楚什么时候用(适用场景) ③ 说清楚什么时候不用(不适用场景)——这是最容易忽略的 ④ 每个参数的格式和示例(不要让LLM猜) ⑤ 参数的边界条件(required/optional/default/range)工具描述的A/B测试工具描述不是写一次就完事的,它需要迭代。我们的做法是:importjsonfromtypingimportList,Dictfromdataclassesimportdataclass@dataclassclassToolSelectionTestCase:"""工具选择测试用例"""user_input:strexpected_tool:strexpected_params:Dict description:str# 这个用例测试什么边界# 工具选择测试套件TOOL_SELECTION_TEST_CASES=[ToolSelectionTestCase(user_input="帮我查一下ORDER-20240315-001的状态",expected_tool="get_order_by_id",expected_params={"order_id":"ORDER-20240315-001"},description="明确提供订单号,应选择精确查询"),ToolSelectionTestCase(user_input="我最近买了什么",expected_tool="get_user_all_orders",expected_params={"user_id":"{current_user_id}","limit":10},description="没有订单号,应选择用户订单列表"),ToolSelectionTestCase(user_input="我的订单",# 极度模糊expected_tool="get_user_all_orders",expected_params={"user_id":"{current_user_id}"},description="模糊输入,应默认查用户订单列表而不是要求提供订单号"),ToolSelectionTestCase(user_input="帮我查一下订单123",# 格式不对的订单号expected_tool="clarify_intent",# 应该请求澄清,而不是乱猜expected_params={},description="订单号格式不对,应请求澄清"),]defevaluate_tool_selection(agent,test_cases:List[ToolSelectionTestCase])-dict:""" 评估工具描述的质量 通过测试用例集测量工具选择准确率 """results={"total":len(test_cases),"correct_tool":0,"correct_params":0,"failures":[]}forcaseintest_cases:# 只运行工具选择步骤,不实际执行工具selected_tool,selected_params=agent.plan_tool_call(case.user_input)tool_correct=selected_tool==case.expected_tool params_correct=_params_match(selected_params,case.expected_params)iftool_correct:results["correct_tool"]+=1iftool_correctandparams_correct:results["correct_params"]+=1ifnottool_correctornotparams_correct:results["failures"].append({"input":case.user_input,"description":case.description,"expected_tool":case.expected_tool,"selected_tool":selected_tool,"expected_params":case.expected_params,"selected_params":selected_params,})results["tool_accuracy"]=results["correct_tool"]/results["total"]results["param_accuracy"]=results["correct_params"]/results["total"]returnresultsdef_params_match(actual:dict,expected:dict)-bool:"""参数匹配检查(允许占位符变量)"""forkey,expected_valinexpected.items():ifkeynotinactual:

更多文章