ARTICLE · 1313971
AI Agent 软件测试实战(03):Tool Calling 实战,让 Agent 学会动态调用测试工具
TL;DR
系列方向: 从软件测试实际场景出发,使用 Python 和大模型技术,从零搭建一个 AI Test Agent。逐步实现任务理解、工具调用、任务规划、测试执行、结果分析、记忆管理和工程集成,最终形成一个可以融入软件测试工作流的智能测试助手。
本篇重点: 前两篇已经实现了 Agent 基础架构和大模型任务理解,但工具调用仍然依赖固定的 Python 逻辑。本篇将正式引入 Tool Calling,让大模型根据工具描述和参数 Schema,自主选择合适的测试工具、生成调用参数,再由 Agent 完成安全校验和实际执行。
一、为什么 Agent 需要 Tool Calling?
上一篇我们已经让大模型参与了测试任务理解。
例如,测试人员输入:
帮我检查订单接口,看看订单金额和状态是否正常。大模型能够返回:
{”tool_name”: ”check_order”,”parameters”: {},”reason”: ”用户希望检查订单金额和状态。”}
然后 Agent 根据 tool_name 查找工具并执行。
看起来已经实现了工具调用,但仔细想一下,这里面还有一个问题:
我们仍然需要提前告诉程序应该如何根据模型返回的名称进行处理。
如果后面增加十几个测试工具,是否还要不断修改任务判断逻辑?
例如:
检查订单接口。 查询订单详情。 检查订单金额。 检查用户登录。 分析自动化测试失败日志。 查询数据库中的测试数据。
如果每增加一个工具,就要修改一遍 Agent 的判断代码,项目很快就会变得臃肿。
这时候就需要 Tool Calling。
1.1 Tool Calling 到底是什么?
Tool Calling 可以理解为:开发人员提前向大模型提供一组工具定义,包括工具名称、用途和参数结构,大模型根据用户需求选择合适的工具,并返回结构化的调用请求。
需要注意:
大模型通常不会直接执行我们的 Python 函数。
它返回的是一个工具调用请求,真正执行函数的仍然是我们自己的程序。
例如:
测试人员输入:
帮我检查订单 ORDER1001 的金额是否正常。模型根据工具定义判断需要调用:
{”name”: ”check_order_amount”,”arguments”: {”order_id”: ”ORDER1001”}}
Agent 收到请求之后:
检查工具是否已经注册。 校验参数是否合法。 调用对应的 Python 函数。 获取执行结果。 将结果返回给大模型。 生成最终的测试说明。
这才是一个比较完整的工具调用过程。
1.2 普通任务理解与 Tool Calling 的区别
两者并不是完全不同的技术。
第二篇解决的是模型如何理解测试人员的需求,第三篇进一步解决模型如何按照约定的接口提出工具调用请求。
二、本篇准备实现什么?
这次继续使用之前的 ai-test-agent 项目。
为了更直观地验证 Tool Calling,我们不再只保留一个订单检查函数,而是准备三个不同的测试工具。
用户可以使用不同的自然语言描述需求,Agent 根据实际情况选择工具。
例如:
场景一:查询订单
帮我查询一下 ORDER1001 的详细信息。预期调用:
query_order_detail场景二:检查订单金额
检查 ORDER1001 的订单金额是否大于 0。预期调用:
check_order_amount场景三:检查订单状态
看看 ORDER1002 当前的订单状态是否正常。预期调用:
check_order_status这里我们要验证的不是模型能否返回一段漂亮的文字,而是它能否根据工具描述选择正确的工具,并且生成正确的参数。
三、理解 Tool Calling 的执行流程
在动手之前,先看一下整个调用过程。
flowchart TDA[测试人员输入需求] --> B[Agent 接收任务]B --> C[向大模型发送工具定义]C --> D[大模型选择工具]D --> E[返回 Tool Call 请求]E --> F[Agent 校验工具和参数]F --> G[执行 Python 测试工具]G --> H[返回工具执行结果]H --> I[再次请求大模型]I --> J[生成最终测试说明]
这里有两个容易混淆的地方。
第一,大模型选择工具,不代表工具已经执行。
第二,工具执行成功,也不代表测试检查通过。
例如,订单金额检查工具成功运行,但发现订单金额为负数。
这时应该返回:
工具执行成功。 业务检查失败。
这两个结果需要分开管理。
在真实测试工程中,这个区别尤其重要。
四、准备项目结构
本篇继续沿用前两篇的项目。
目录调整为:
ai-test-agent/├── agent.py├── tools.py├── llm_client.py├── README.md└── requirements.txt
本次仍然使用 Python 标准库,不需要额外安装 LangChain 或其他 Agent 框架。
模型服务需要支持兼容 OpenAI Chat Completions 格式的 Tool Calling。
注意:不同服务商对工具调用参数、模型名称和返回格式可能存在差异,使用前需要确认所选模型支持 tools 和 tool_calls。
五、第一步:准备测试数据和工具函数
先实现三个测试工具。
修改 tools.py:
from typing import Any# 模拟订单数据库ORDERS = {”ORDER1001”: {”order_id”: ”ORDER1001”,”amount”: 199.00,”status”: ”paid”,”user_id”: ”USER001”},”ORDER1002”: {”order_id”: ”ORDER1002”,”amount”: -20.00,”status”: ”unknown”,”user_id”: ”USER002”}}def query_order_detail(order_id: str) -> dict[str, Any]:”””查询订单详情”””order = ORDERS.get(order_id)if order is None:return {”success”: False,”error”: f”订单不存在:{order_id}”}return {”success”: True,”data”: order}def check_order_amount(order_id: str) -> dict[str, Any]:”””检查订单金额”””order = ORDERS.get(order_id)if order is None:return {”success”: False,”error”: f”订单不存在:{order_id}”}amount = order[”amount”]passed = (isinstance(amount, (int, float))and not isinstance(amount, bool)and amount > 0)return {”success”: True,”passed”: passed,”order_id”: order_id,”amount”: amount,”message”: (”订单金额正常”if passedelse f”订单金额异常:{amount}”)}def check_order_status(order_id: str) -> dict[str, Any]:”””检查订单状态”””order = ORDERS.get(order_id)if order is None:return {”success”: False,”error”: f”订单不存在:{order_id}”}status = order[”status”]allowed_status = {”created”,”paid”,”cancelled”}passed = status in allowed_statusreturn {”success”: True,”passed”: passed,”order_id”: order_id,”status”: status,”message”: (”订单状态正常”if passedelse f”订单状态异常:{status}”)}
这里使用了两条模拟订单数据:
这样设计的目的,是让后续验证同时覆盖正常和异常场景。
实际项目中,这里的 ORDERS 可以替换为数据库查询、HTTP 接口调用或者现有自动化测试程序。
另外,工具返回的 success 表示工具是否成功完成执行,passed 才表示业务检查是否通过。
六、第二步:定义工具 Schema
这是本篇最重要的部分之一。
在 Tool Calling 中,我们需要告诉模型:
有哪些工具? 每个工具解决什么问题? 每个工具需要什么参数? 参数的数据类型是什么? 哪些参数是必填的?
这些信息通常通过 JSON Schema 描述。
在 tools.py 末尾增加:
TOOL_DEFINITIONS = [{”type”: ”function”,”function”: {”name”: ”query_order_detail”,”description”: ”查询指定订单的详细信息,包括订单编号、金额、状态和用户编号。”,”parameters”: {”type”: ”object”,”properties”: {”order_id”: {”type”: ”string”,”description”: ”需要查询的订单编号”}},”required”: [”order_id”],”additionalProperties”: False}}},{”type”: ”function”,”function”: {”name”: ”check_order_amount”,”description”: ”检查指定订单的金额是否为有效正数,适用于订单金额异常检查。”,”parameters”: {”type”: ”object”,”properties”: {”order_id”: {”type”: ”string”,”description”: ”需要检查的订单编号”}},”required”: [”order_id”],”additionalProperties”: False}}},{”type”: ”function”,”function”: {”name”: ”check_order_status”,”description”: ”检查指定订单的状态是否属于 created、paid 或 cancelled。”,”parameters”: {”type”: ”object”,”properties”: {”order_id”: {”type”: ”string”,”description”: ”需要检查的订单编号”}},”required”: [”order_id”],”additionalProperties”: False}}}]# 工具注册表TOOL_REGISTRY = {”query_order_detail”: query_order_detail,”check_order_amount”: check_order_amount,”check_order_status”: check_order_status}
这段代码实际上完成了两件事情。
第一件事情:告诉大模型工具有哪些。
例如:
{”name”: ”check_order_amount”,”description”: ”检查指定订单的金额是否为有效正数”,”parameters”: {”type”: ”object”,”properties”: {”order_id”: {”type”: ”string”}},”required”: [”order_id”]}}
模型看到这些描述之后,就能根据用户需求判断是否应该使用这个工具。
第二件事情:告诉 Agent 工具如何执行。
TOOL_REGISTRY = {”query_order_detail”: query_order_detail,”check_order_amount”: check_order_amount,”check_order_status”: check_order_status}
这里是程序自己的工具注册表。
模型只能提出调用建议,不能通过返回任意函数名称来执行代码。
这一点非常重要。
不要使用 eval() 或 exec() 根据模型返回内容动态执行 Python 代码。
七、第三步:改造大模型客户端
第二篇的模型客户端只支持普通文本请求。
现在需要增加 Tool Calling 能力。
修改 llm_client.py:
import jsonimport osimport urllib.requestimport urllib.errorclass LLMClient:def __init__(self):self.api_url = os.getenv(”AI_API_URL”)self.api_key = os.getenv(”AI_API_KEY”)self.model = os.getenv(”AI_MODEL”)if not self.api_url:raise ValueError(”缺少 AI_API_URL”)if not self.api_key:raise ValueError(”缺少 AI_API_KEY”)if not self.model:raise ValueError(”缺少 AI_MODEL”)def chat(self, messages, tools=None):payload = {”model”: self.model,”messages”: messages,”temperature”: 0.1}if tools:payload[”tools”] = toolspayload[”tool_choice”] = ”auto”data = json.dumps(payload).encode(”utf-8”)request = urllib.request.Request(self.api_url,data=data,headers={”Content-Type”: ”application/json”,”Authorization”: f”Bearer {self.api_key}”},method=”POST”)try:with urllib.request.urlopen(request,timeout=30) as response:result = json.loads(response.read().decode(”utf-8”))return result[”choices”][0][”message”]except urllib.error.HTTPError as e:raise RuntimeError(f”模型接口 HTTP 错误:{e.code}”) from eexcept urllib.error.URLError as e:raise RuntimeError(f”模型接口连接失败:{e.reason}”) from eexcept (KeyError, IndexError, TypeError, ValueError) as e:raise RuntimeError(f”模型响应异常:{e}”) from e
与上一篇相比,主要变化是:
def chat(self, messages, tools=None):现在模型客户端可以接收工具定义。
核心请求参数:
payload[”tools”] = toolspayload[”tool_choice”] = ”auto”
其中:
tools:告诉模型有哪些工具可以使用。 tool_choice:让模型根据当前任务自行判断是否需要调用工具。
这里的 auto 不意味着模型一定会调用工具,它也可以选择直接返回文本。
这正是我们希望获得的灵活性。
八、第四步:实现动态工具调用
接下来修改 agent.py。
这次 Agent 不再自己通过关键词判断工具名称,而是读取大模型返回的 tool_calls。
完整代码:
import jsonfrom llm_client import LLMClientfrom tools import TOOL_DEFINITIONS, TOOL_REGISTRYSYSTEM_PROMPT = ”””你是一个软件测试 AI Agent。你的职责是根据用户的测试需求,选择合适的测试工具。当前提供的工具包括:1. 查询订单详情2. 检查订单金额3. 检查订单状态使用规则:1. 根据用户需求选择最合适的工具。2. 如果需要订单编号,必须从用户输入中提取。3. 不允许编造订单编号。4. 如果用户没有提供必要的参数,先向用户询问。5. 不允许调用工具列表之外的工具。6. 不要编造工具执行结果。7. 工具返回的数据才是真实执行依据。”””class AITestAgent:def __init__(self):self.llm = LLMClient()self.tools = TOOL_REGISTRYdef validate_arguments(self,tool_name: str,arguments: dict):if not isinstance(arguments, dict):raise ValueError(”工具参数必须是 JSON 对象”)allowed_parameters = {”order_id”}if set(arguments) != allowed_parameters:raise ValueError(”工具参数缺失或包含不允许的字段”)order_id = arguments.get(”order_id”)if not isinstance(order_id, str):raise ValueError(”order_id 必须是字符串”)if not order_id.strip():raise ValueError(”order_id 不能为空”)return {”order_id”: order_id.strip()}def execute_tool(self,tool_name: str,arguments: dict):# 只允许调用已注册工具tool = self.tools.get(tool_name)if tool is None:return {”success”: False,”error”: f”工具不存在:{tool_name}”}# 参数校验try:arguments = self.validate_arguments(tool_name,arguments)except ValueError as e:return {”success”: False,”error”: str(e)}# 执行工具try:return tool(**arguments)except Exception as e:return {”success”: False,”error”: f”工具执行异常:{e}”}def run(self, task: str):messages = [{”role”: ”system”,”content”: SYSTEM_PROMPT},{”role”: ”user”,”content”: task}]print(f”\n收到测试任务:{task}”)# 第一步:请求大模型response = self.llm.chat(messages,TOOL_DEFINITIONS)tool_calls = response.get(”tool_calls”)# 模型不调用工具,直接返回文本if not tool_calls:print(”\nAI 回复:”)print(response.get(”content”))return# 第二步:处理模型提出的工具调用请求print(”\n模型选择了以下工具:”)messages.append(response)tool_results = []for call in tool_calls:function_info = call.get(”function”, {})tool_name = function_info.get(”name”)raw_arguments = function_info.get(”arguments”,”{}”)print(f”工具名称:{tool_name}”)# 解析模型生成的参数try:arguments = json.loads(raw_arguments)if not isinstance(arguments, dict):raise ValueError(”工具参数必须是 JSON 对象”)except (json.JSONDecodeError, ValueError):result = {”success”: False,”error”: ”模型返回的工具参数格式错误”}else:print(f”工具参数:{arguments}”)# 第三步:执行工具result = self.execute_tool(tool_name,arguments)print(f”工具结果:{result}”)tool_results.append(result)# 第四步:将执行结果返回给模型messages.append({”role”: ”tool”,”tool_call_id”: call[”id”],”content”: json.dumps(result,ensure_ascii=False)})# 第五步:让模型根据实际工具结果生成说明final_response = self.llm.chat(messages)print(”\nAI 测试分析结果:”)print(final_response.get(”content”))if __name__ == ”__main__”:agent = AITestAgent()task = input(”请输入测试任务:”)agent.run(task)
这段代码是本篇的核心。
我们重点看四个地方。
8.1 获取模型返回的 tool_calls
tool_calls = response.get(”tool_calls”)如果模型认为需要调用工具,就会返回工具调用请求。
如果不需要调用工具,也可能直接返回文本。
这说明 Tool Calling 并不是强制执行工具,而是让模型根据任务决定是否需要使用工具。
8.2 解析工具名称和参数
function_info = call.get(”function”, {})tool_name = function_info.get(”name”)raw_arguments = function_info.get(”arguments”,”{}”)
例如模型返回:
{”name”: ”check_order_amount”,”arguments”: ”{\”order_id\”:\”ORDER1001\”}”}
程序提取出:
tool_name = ”check_order_amount”arguments = {”order_id”: ”ORDER1001”}
8.3 动态调用 Python 函数
tool = self.tools.get(tool_name)return tool(**arguments)
这里就是 Tool Calling 与实际 Python 工具之间的连接点。
模型只需要返回工具名称和参数,Agent 就可以从注册表中找到对应函数。
以后增加新的工具,只要按照统一规则注册,不需要再为每个工具单独编写一套判断分支。
8.4 将执行结果返回大模型
messages.append({”role”: ”tool”,”tool_call_id”: call[”id”],”content”: json.dumps(result,ensure_ascii=False)})
这一步也很关键。
模型之前只是提出调用请求,并不知道真实执行结果。
现在我们把 Python 工具的执行结果返回给模型,它才能基于实际结果生成最终说明。
注意,这里的 tool_call_id 必须对应之前模型返回的工具调用 ID。
这也是 Chat Completions 工具调用协议中的重要部分。
九、实际验证:让 Agent 自己选择测试工具
代码完成后,我们开始验证。
先确保模型服务配置正确:
python agent.py场景一:查询订单详情
输入:
帮我查询一下 ORDER1001 的详细信息。模型预期选择:
{”name”: ”query_order_detail”,”arguments”: {”order_id”: ”ORDER1001”}}
工具返回:
{”success”: true,”data”: {”order_id”: ”ORDER1001”,”amount”: 199.0,”status”: ”paid”,”user_id”: ”USER001”}}
最终 AI 可以根据真实工具结果生成类似说明:
订单 ORDER1001 查询成功。订单金额:199.00订单状态:paid用户编号:USER001当前订单信息正常。
场景二:检查订单金额
输入:
检查 ORDER1002 的订单金额是否正常。模型应该选择:
check_order_amount参数:
{”order_id”: ”ORDER1002”}
工具执行结果:
{”success”: true,”passed”: false,”order_id”: ”ORDER1002”,”amount”: -20.0,”message”: ”订单金额异常:-20.0”}
最终 AI 可以生成:
订单 ORDER1002 的金额检查未通过。实际金额:-20.00检查结果:失败。异常原因:订单金额小于等于 0,不符合订单金额校验规则。
这里有个细节:
工具执行成功,但业务检查失败。
Agent 不能将它们混为一谈。
场景三:检查订单状态
输入:
看看 ORDER1002 当前订单状态是否符合业务规则。模型应该选择:
check_order_status工具返回:
{”success”: true,”passed”: false,”order_id”: ”ORDER1002”,”status”: ”unknown”,”message”: ”订单状态异常:unknown”}
最终 AI 根据工具结果生成分析说明。
场景四:缺少必要参数
输入:
帮我检查一下订单金额。这里没有提供订单编号。
按照 Prompt 的要求,模型应该优先询问:
请提供需要检查的订单编号。Agent 不应该允许模型随意编造订单编号并调用工具。
如果模型仍然尝试调用工具,程序也必须通过参数校验进行拦截。
场景五:调用不存在的工具
假设模型错误地返回:
{”name”: ”delete_order”,”arguments”: {”order_id”: ”ORDER1001”}}
由于注册表中不存在 delete_order,Agent 会返回:
{”success”: false,”error”: ”工具不存在:delete_order”}
不会执行任何删除操作。
这也是为什么不能把模型返回的工具名称直接交给 Python 动态执行。
以上 JSON 和最终回复均为预期示例,实际调用结果需要在配置好的模型服务中验证。
十、工程实践:Tool Calling 不能只考虑能不能调用
到这里,我们已经完成了基本的动态工具调用。
但如果准备将它用于真正的软件测试项目,还需要考虑几个工程问题。
10.1 工具参数必须进行二次校验
虽然模型可以按照 Schema 生成参数,但这不代表参数一定正确。
例如:
{”order_id”: ””}
或者:
{”order_id”: 12345}
都可能出现。
因此,不能只依赖模型的参数生成能力。
程序必须再次检查:
参数是否存在。 数据类型是否正确。 字段是否在允许范围内。 参数是否满足业务规则。
模型负责提出请求,程序负责验证请求。
10.2 工具权限需要进行限制
未来 Agent 可能拥有数据库查询、接口调用、文件读取等能力。
这些工具并不是都可以无限制使用。
例如:
特别是涉及数据修改、环境操作和生产系统时,应该设置明确的权限边界。
10.3 工具调用需要设置超时和次数限制
不能让 Agent 无限调用工具。
实际项目中建议增加:
单次工具调用超时时间。 单个任务最大工具调用次数。 最大输入和输出长度。 异常重试次数。 日志记录和审计。
本篇代码只演示基础工具调用,尚未实现这些完整的生产级控制。
10.4 工具描述也需要认真设计
工具描述不是随便写一句话就可以。
例如:
检查订单这个描述过于简单。
模型无法明确知道它究竟检查什么。
更好的描述是:
检查指定订单的金额是否为有效正数,适用于订单金额异常检查。
工具越多,描述越重要。
如果多个工具的职责非常相似,模型就可能选错。
因此,工具设计时需要考虑功能边界,尽量避免职责重叠。
十一、本篇总结:Agent 开始具备工具调用能力
回顾一下这三篇文章的变化。
现在的 AI Test Agent 已经具备:
接收自然语言测试需求。 将测试需求交给大模型理解。 根据工具定义选择测试工具。 自动生成工具调用参数。 通过注册表执行对应的 Python 函数。 将真实执行结果返回给大模型。 生成最终测试说明。
不过,目前仍然只是单轮工具调用。
例如:
先查询订单,再检查金额,最后根据检查结果分析异常。
这种需要多个步骤、多个工具连续协作的任务,还没有完整实现。
这也是下一步要去学习和实践的东西。















































































