夜雨聆风学习资料网

ARTICLE · 1313971

AI Agent 软件测试实战(03):Tool Calling 实战,让 Agent 学会动态调用测试工具

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 收到请求之后:

  1. 检查工具是否已经注册。
  2. 校验参数是否合法。
  3. 调用对应的 Python 函数。
  4. 获取执行结果。
  5. 将结果返回给大模型。
  6. 生成最终的测试说明。

这才是一个比较完整的工具调用过程。

1.2 普通任务理解与 Tool Calling 的区别

对比维度
第二篇任务理解
第三篇 Tool Calling
工具定义
主要依靠 Prompt 描述
独立定义工具 Schema
工具选择
返回工具名称
返回标准工具调用请求
参数处理
自己解析 JSON
模型生成调用参数
工具执行
Agent 根据名称判断
Agent 根据注册表动态分发
扩展方式
修改任务理解逻辑
增加工具定义和注册函数

两者并不是完全不同的技术。

第二篇解决的是模型如何理解测试人员的需求,第三篇进一步解决模型如何按照约定的接口提出工具调用请求。

二、本篇准备实现什么?

这次继续使用之前的 ai-test-agent 项目。

为了更直观地验证 Tool Calling,我们不再只保留一个订单检查函数,而是准备三个不同的测试工具。

工具名称
功能
输入参数
query_order_detail
查询订单详情
order_id
check_order_amount
检查订单金额
order_id
check_order_status
检查订单状态
order_id

用户可以使用不同的自然语言描述需求,Agent 根据实际情况选择工具。

例如:

场景一:查询订单

帮我查询一下 ORDER1001 的详细信息。

预期调用:

query_order_detail

场景二:检查订单金额

检查 ORDER1001 的订单金额是否大于 0。

预期调用:

check_order_amount

场景三:检查订单状态

看看 ORDER1002 当前的订单状态是否正常。

预期调用:

check_order_status

这里我们要验证的不是模型能否返回一段漂亮的文字,而是它能否根据工具描述选择正确的工具,并且生成正确的参数。

三、理解 Tool Calling 的执行流程

在动手之前,先看一下整个调用过程。

flowchart TD    A[测试人员输入需求] --> 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 passed            else 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_status    return {        ”success”: True,        ”passed”: passed,        ”order_id”: order_id,        ”status”: status,        ”message”: (            ”订单状态正常”            if passed            else f”订单状态异常:{status}”        )    }

这里使用了两条模拟订单数据:

订单编号
金额
状态
预期情况
ORDER1001
199.00
paid
正常
ORDER1002
-20.00
unknown
异常

这样设计的目的,是让后续验证同时覆盖正常和异常场景。

实际项目中,这里的 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”] = tools            payload[”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 e        except urllib.error.URLError as e:            raise RuntimeError(                f”模型接口连接失败:{e.reason}”            ) from e        except (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_REGISTRY    def 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 开始具备工具调用能力

回顾一下这三篇文章的变化。

阶段
已完成的能力
第一篇
搭建 Agent 基础架构
第二篇
接入大模型理解测试任务
第三篇
根据模型决策动态调用测试工具

现在的 AI Test Agent 已经具备:

  1. 接收自然语言测试需求。
  2. 将测试需求交给大模型理解。
  3. 根据工具定义选择测试工具。
  4. 自动生成工具调用参数。
  5. 通过注册表执行对应的 Python 函数。
  6. 将真实执行结果返回给大模型。
  7. 生成最终测试说明。

不过,目前仍然只是单轮工具调用。

例如:

先查询订单,再检查金额,最后根据检查结果分析异常。

这种需要多个步骤、多个工具连续协作的任务,还没有完整实现。

这也是下一步要去学习和实践的东西。

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料