## TL;DR

  • 系列方向: 从零搭建 AI Test Agent,逐步实现大模型接入、工具调用、任务规划、状态管理和测试执行,最终形成一个能够辅助软件测试工作的 Agent 项目。
  • 本篇重点: 第三篇已经让 Agent 学会调用测试工具,但一次工具调用还不足以完成复杂测试任务。本篇通过 Agent Loop 实现多轮思考、工具执行、结果反馈,让 Agent 能够根据执行结果继续完成后续操作。

一、为什么有了 Tool Calling,还需要 Agent Loop?

在上一篇文章中,我们已经实现了 Tool Calling。

Agent 可以根据用户输入,判断是否需要调用工具。例如:

用户:帮我检查一下订单 ORDER1002 是否正常。

Agent:
1. 调用 query_order_detail 查询订单
2. 获取订单信息
3. 返回查询结果

看起来已经能够完成任务了,但实际测试工作中,问题往往没有这么简单。

比如我们需要检查一个订单是否存在异常,通常至少需要完成以下操作:

  1. 查询订单详情。
  2. 检查订单金额是否合法。
  3. 检查订单状态是否正常。
  4. 根据前面几步的结果判断是否存在异常。
  5. 整理检查结果并生成报告。

这里有一个关键问题:

Agent 调用完一个工具之后,不能直接结束,而是需要根据工具返回的结果,决定下一步做什么。

这就是 Agent Loop 要解决的问题。

1.1 Tool Calling 和 Agent Loop 有什么区别?

两者不是替代关系,而是不同层面的能力。

对比项Tool CallingAgent Loop
核心作用让模型调用工具让 Agent 连续执行多轮任务
执行方式发起工具调用思考、调用、反馈、继续执行
是否支持多步骤需要额外实现核心机制就是多轮执行
结果处理返回工具结果根据工具结果决定后续动作
适用场景单次查询、单次操作多步骤测试任务

可以简单理解:

Tool Calling 解决的是 Agent 如何使用工具;Agent Loop 解决的是 Agent 如何持续使用工具完成任务。

二、Agent Loop 的执行原理

先看一个比较完整的执行过程。

是

否

接收用户测试任务

构建对话上下文

请求大模型

是否需要调用工具

解析工具调用请求

检查工具名称和参数

执行测试工具

获取工具执行结果

将结果追加到对话上下文

获取最终回答

输出测试结果

这里最重要的是一个循环:

模型提出工具调用
      ↓
Agent 执行工具
      ↓
工具返回执行结果
      ↓
Agent 将结果交给模型
      ↓
模型决定下一步操作
      ↓
继续循环

注意,工具执行结果并不意味着任务已经完成。

例如:

第一轮:查询订单详情
第二轮:检查订单金额
第三轮:检查订单状态
第四轮:生成最终分析

每一轮的操作都由模型结合当前上下文决定。

不过,模型负责提出下一步动作,真正执行工具的仍然是 Python 程序。不能让模型返回一个函数名,就不经过检查直接执行。

三、项目结构设计

本篇继续使用前面创建的 ai-test-agent 项目。

项目结构调整为:

ai-test-agent/
│
├── main.py
├── agent.py
├── llm_client.py
├── tools.py
├── requirements.txt
└── README.md

各文件的职责:

文件作用
main.py启动程序,接收测试任务
agent.py实现 Agent Loop
llm_client.py负责调用大模型
tools.py注册和执行测试工具
requirements.txt项目依赖
README.md项目使用说明

本篇先不引入复杂的任务规划框架,重点把多轮执行机制跑通。

四、实现测试工具

先准备几个简单的订单测试工具。

为了让案例能够独立演示,我们先使用模拟数据。后续接入真实测试环境时,可以将这些函数替换成接口请求、数据库查询或者自动化测试执行器。

4.1 tools.py

# tools.py

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):
    """查询订单详情"""

    order = ORDERS.get(order_id)

    if not order:
        return {
            "success": False,
            "message": "订单不存在"
        }

    return {
        "success": True,
        "data": order
    }


def check_order_amount(order_id):
    """检查订单金额"""

    order = ORDERS.get(order_id)

    if not order:
        return {
            "success": False,
            "passed": False,
            "message": "订单不存在"
        }

    amount = order["amount"]

    if amount < 0:
        return {
            "success": True,
            "passed": False,
            "message": f"订单金额异常:{amount}"
        }

    return {
        "success": True,
        "passed": True,
        "message": "订单金额正常"
    }


def check_order_status(order_id):
    """检查订单状态"""

    order = ORDERS.get(order_id)

    if not order:
        return {
            "success": False,
            "passed": False,
            "message": "订单不存在"
        }

    status = order["status"]

    if status not in ["paid", "pending", "cancelled"]:
        return {
            "success": True,
            "passed": False,
            "message": f"订单状态异常:{status}"
        }

    return {
        "success": True,
        "passed": True,
        "message": "订单状态正常"
    }


TOOL_REGISTRY = {
    "query_order_detail": query_order_detail,
    "check_order_amount": check_order_amount,
    "check_order_status": check_order_status
}


def execute_tool(tool_name, arguments):
    """执行已注册的工具"""

    tool = TOOL_REGISTRY.get(tool_name)

    if tool is None:
        return {
            "success": False,
            "message": f"不支持的工具:{tool_name}"
        }

    if not isinstance(arguments, dict):
        return {
            "success": False,
            "message": "工具参数必须是 JSON 对象"
        }

    if set(arguments) != {"order_id"}:
        return {
            "success": False,
            "message": "工具参数不正确,需要且仅需要 order_id"
        }

    if not isinstance(arguments["order_id"], str):
        return {
            "success": False,
            "message": "order_id 必须是字符串"
        }

    try:
        return tool(**arguments)
    except Exception as exc:
        return {
            "success": False,
            "message": f"工具执行异常:{str(exc)}"
        }

这里有两个容易混淆的字段:

  • success:代表工具是否成功执行。
  • passed:代表业务检查是否通过。

例如订单金额为 -20:

{
  "success": true,
  "passed": false,
  "message": "订单金额异常:-20.0"
}

这意味着工具正常执行了,但是检查发现了业务问题。

在测试平台中,这两个状态一定要区分,否则后续统计测试成功率、失败率时容易出现问题。

五、实现大模型客户端

Agent Loop 需要反复调用大模型,因此我们将模型请求单独封装。

5.1 llm_client.py

下面使用 OpenAI Chat Completions 兼容接口作为示例。

需要根据实际使用的模型服务商配置 API 地址、模型名称和密钥。

# llm_client.py

import os
import json
import urllib.request
import urllib.error


class LLMClient:

    def __init__(self):
        self.api_key = os.getenv("LLM_API_KEY")
        self.base_url = os.getenv(
            "LLM_BASE_URL",
            "https://api.openai.com/v1/chat/completions"
        )
        self.model = os.getenv(
            "LLM_MODEL",
            "gpt-4o-mini"
        )

        if not self.api_key:
            raise ValueError("请先配置 LLM_API_KEY")

    def chat(self, messages, tools=None):

        payload = {
            "model": self.model,
            "messages": messages,
            "temperature": 0
        }

        if tools:
            payload["tools"] = tools
            payload["tool_choice"] = "auto"

        request = urllib.request.Request(
            self.base_url,
            data=json.dumps(payload).encode("utf-8"),
            headers={
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json"
            },
            method="POST"
        )

        try:
            with urllib.request.urlopen(
                request,
                timeout=60
            ) as response:
                result = json.loads(
                    response.read().decode("utf-8")
                )

            return result["choices"][0]["message"]

        except urllib.error.HTTPError as exc:
            detail = exc.read().decode(
                "utf-8",
                errors="replace"
            )
            raise RuntimeError(
                f"模型接口请求失败:HTTP {exc.code},{detail}"
            ) from exc

        except urllib.error.URLError as exc:
            raise RuntimeError(
                f"模型服务连接失败:{exc}"
            ) from exc

这里没有使用第三方 SDK,而是使用 Python 自带的 urllib.request。

这样做的好处是依赖比较少,也方便理解 Agent 到底向模型发送了什么数据。

需要注意:不同模型服务商对 Tool Calling 的字段支持可能存在差异,实际接入时需要检查其接口协议。

六、核心实现:构建 Agent Loop

接下来是本篇最重要的部分。

Agent Loop 主要完成以下事情:

  1. 接收用户任务。
  2. 将任务放入对话上下文。
  3. 调用大模型。
  4. 判断模型是否提出工具调用。
  5. 执行工具并保存结果。
  6. 将结果反馈给模型。
  7. 继续下一轮,直到模型给出最终回答。

6.1 agent.py

# agent.py

import json

from llm_client import LLMClient
from tools import execute_tool


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": "检查订单状态是否正常",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {
                        "type": "string",
                        "description": "订单编号"
                    }
                },
                "required": ["order_id"],
                "additionalProperties": False
            }
        }
    }
]


class AITestAgent:

    def __init__(self, max_rounds=8):

        self.llm = LLMClient()
        self.max_rounds = max_rounds

    def run(self, task):

        messages = [
            {
                "role": "system",
                "content": """
你是一个软件测试 Agent。

你需要根据用户的测试任务,
选择合适的测试工具完成检查。

规则:
1. 需要查询订单时调用查询工具。
2. 需要检查金额时调用金额检查工具。
3. 需要检查状态时调用状态检查工具。
4. 根据工具返回结果进行分析。
5. 不要编造工具执行结果。
6. 所有必要检查完成后,输出清晰的测试报告。
7. 工具执行失败时,应如实说明,不要把失败说成通过。
"""
            },
            {
                "role": "user",
                "content": task
            }
        ]

        execution_history = []

        for round_num in range(1, self.max_rounds + 1):

            print(f"\n========== Agent 第 {round_num} 轮 ==========")

            response = self.llm.chat(
                messages,
                TOOL_DEFINITIONS
            )

            tool_calls = response.get("tool_calls")

            # 模型没有继续请求工具,说明本轮给出了最终回答
            if not tool_calls:

                final_answer = response.get("content") or ""

                return {
                    "success": True,
                    "answer": final_answer,
                    "history": execution_history
                }

            # 保存模型提出的工具调用请求
            messages.append(response)

            for tool_call in tool_calls:

                function_info = tool_call["function"]

                tool_name = function_info["name"]

                arguments = json.loads(
                    function_info["arguments"]
                )

                print(f"调用工具:{tool_name}")
                print(f"工具参数:{arguments}")

                # 执行工具
                result = execute_tool(
                    tool_name,
                    arguments
                )

                print(f"执行结果:{result}")

                execution_history.append({
                    "round": round_num,
                    "tool": tool_name,
                    "arguments": arguments,
                    "result": result
                })

                # 将工具结果反馈给模型
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call["id"],
                    "content": json.dumps(
                        result,
                        ensure_ascii=False
                    )
                })

        return {
            "success": False,
            "reason": "达到最大执行轮数,Agent 未能生成最终回答",
            "history": execution_history
        }

6.2 代码中最关键的地方

真正实现循环的代码其实并不复杂:

for round_num in range(1, self.max_rounds + 1):

    response = self.llm.chat(
        messages,
        TOOL_DEFINITIONS
    )

    tool_calls = response.get("tool_calls")

    if not tool_calls:
        return response.get("content")

    messages.append(response)

    for tool_call in tool_calls:

        result = execute_tool(
            tool_call["function"]["name"],
            json.loads(tool_call["function"]["arguments"])
        )

        messages.append({
            "role": "tool",
            "tool_call_id": tool_call["id"],
            "content": json.dumps(result, ensure_ascii=False)
        })

它体现了 Agent Loop 的核心机制:

每次工具执行完成后,都要把执行结果放回 messages,再请求模型。

如果不把工具结果反馈给模型,那么模型就不知道前面发生了什么,也就无法根据实际执行情况决定下一步操作。

另外,这里的 messages.append(response) 用于保留模型发出的工具调用消息。工具返回消息通过 tool_call_id 与对应调用关联。

当前代码支持模型在一轮中返回多个工具调用,并逐个执行后再统一反馈结果。

七、启动项目,执行一次完整测试

7.1 main.py

# main.py

from agent import AITestAgent


if __name__ == "__main__":

    agent = AITestAgent(max_rounds=8)

    task = """
请检查订单 ORDER1002 是否存在异常。

要求:
1. 查询订单详情。
2. 检查订单金额。
3. 检查订单状态。
4. 根据检查结果生成测试报告。
"""

    result = agent.run(task)

    print("\n========== 最终测试报告 ==========")

    if result["success"]:
        print(result["answer"])
    else:
        print("Agent 执行未正常完成:")
        print(result["reason"])

    print("\n========== 工具执行记录 ==========")

    for item in result["history"]:
        print(item)

7.2 requirements.txt

# 本项目核心代码使用 Python 标准库

本篇没有额外安装 Python 第三方依赖。

7.3 配置模型服务

Mac/Linux:

export LLM_API_KEY="你的API_KEY"
export LLM_BASE_URL="你的模型接口地址"
export LLM_MODEL="你的模型名称"

Windows PowerShell:

$env:LLM_API_KEY="你的API_KEY"
$env:LLM_BASE_URL="你的模型接口地址"
$env:LLM_MODEL="你的模型名称"

启动:

python main.py

这里的模型地址必须是支持 Chat Completions 和 Tool Calling 的兼容接口。不能简单地把任意大模型 API 地址填进去就认为可以正常运行。

八、预期执行过程

我们使用 ORDER1002 作为测试对象。

模拟数据如下:

{
  "order_id": "ORDER1002",
  "amount": -20.00,
  "status": "unknown",
  "user_id": "USER002"
}

根据前面的测试工具,预期需要完成三项检查。

检查项目检查结果原因
订单查询成功订单存在
金额检查不通过金额为 -20.00
状态检查不通过状态为 unknown

Agent 的执行过程可能类似:

========== Agent 第 1 轮 ==========

调用工具:query_order_detail

执行结果:
{
    "success": true,
    "data": {
        "order_id": "ORDER1002",
        "amount": -20.0,
        "status": "unknown",
        "user_id": "USER002"
    }
}

========== Agent 第 2 轮 ==========

调用工具:check_order_amount

执行结果:
{
    "success": true,
    "passed": false,
    "message": "订单金额异常:-20.0"
}

========== Agent 第 3 轮 ==========

调用工具:check_order_status

执行结果:
{
    "success": true,
    "passed": false,
    "message": "订单状态异常:unknown"
}

========== Agent 第 4 轮 ==========

模型根据已有执行结果生成最终测试报告。

以上是基于示例任务的预期执行过程,并非声称已经在某个模型服务上实际运行。实际调用轮数和工具调用顺序由模型决定,也可能在一轮中请求多个工具。

8.1 预期测试报告

订单测试报告

订单编号:ORDER1002

一、订单查询
执行结果:成功
说明:成功获取订单详情。

二、金额检查
执行结果:失败
实际金额:-20.00
异常原因:订单金额小于 0。

三、订单状态检查
执行结果:失败
实际状态:unknown
异常原因:订单状态不在允许范围内。

四、测试总结
检查项目:3
通过项目:1
失败项目:2

最终结论:订单存在异常。

这里需要注意,Agent 最终报告是对工具结果的归纳,不应该让大模型凭空判断测试是否通过。真正的检查结果仍然来自 Python 工具。

九、Agent Loop 还有哪些问题?

到这里,一个最基础的 Agent Loop 就完成了。

但从工程角度看,这套实现还有不少需要改进的地方。

9.1 最大执行轮数限制

当前代码通过:

AITestAgent(max_rounds=8)

限制执行轮数。

为什么需要这个限制?

因为大模型不一定每次都能正确完成任务。如果模型持续提出工具调用,或者陷入重复调用,就可能一直消耗 Token 和接口资源。

最大轮数是一种基础保护机制。

后续还可以增加:

  • 最大工具调用次数。
  • 单次工具执行超时时间。
  • 整个任务的总执行时间。
  • 单任务 Token 消耗上限。

9.2 工具重复调用

例如模型已经查询过 ORDER1002,下一轮又重复查询一次。

这在简单 Demo 中问题不大,但在真实项目里可能带来额外成本。

特别是当工具涉及创建订单、提交支付、修改数据等操作时,重复执行可能产生实际副作用。

因此,工程中不能只依靠模型记住自己调用过什么,还应该在 Python 层记录执行历史,并针对有副作用的工具设计幂等机制。

9.3 缺少任务状态管理

当前 Agent 主要依靠 messages 保存上下文。

例如:

已经查询订单
已经检查金额
已经检查状态

这些信息都在对话上下文中,但还没有形成独立的任务状态。

当任务变得复杂时,仅靠对话记录会越来越难管理。

比如:

任务 A:登录系统
任务 B:获取 Token
任务 C:查询订单
任务 D:检查订单金额
任务 E:生成测试报告

如果任务 C 失败,后面的任务是否继续执行?哪些任务已经完成?哪些任务需要重试?

这些问题就不能只依靠简单的对话循环解决。

9.4 缺少执行记录持久化

当前的 execution_history 只保存在 Python 内存中。

程序结束后,记录也就消失了。

如果后续要把 Agent 接入自动化测试平台,就需要考虑将执行记录保存到数据库,例如 SQLite 或 MySQL。

这样才能支持:

  • 历史任务查询。
  • 测试执行过程追踪。
  • 失败原因分析。
  • 测试报告回溯。
  • Agent 执行效果统计。

这些都是从 Demo 走向工程应用时需要补充的能力。

十、这一篇真正实现了什么?

回顾一下前面几篇的进度:

篇章实现能力
01搭建 AI Test Agent 基础架构
02接入大模型,理解自然语言测试任务
03接入 Tool Calling,动态调用测试工具
04构建 Agent Loop,实现多轮工具执行

到第四篇为止,我们已经具备了一个基础 Agent 的核心执行能力。

但我认为,这还不能算是一个真正成熟的测试 Agent。

原因很简单:

现在的 Agent 虽然能够连续调用工具,但它还没有一个清晰、独立的任务计划,也没有完整的任务状态管理机制。

它更像是一个能够根据模型判断不断执行操作的程序。

而我们下一步要解决的,就是让它从简单的循环执行,逐步走向有计划、有状态、有执行记录的测试任务管理。

写在最后:

这一篇看起来只是增加了一个循环,但它实际上改变了 Agent 的工作方式。

以前我们可能是写好一套固定流程,让程序按照顺序执行;现在则是由模型结合执行结果决定下一步调用什么工具。

不过,我并不认为把流程交给模型就意味着可以完全放开程序控制。对于测试系统来说,工具权限、执行次数、超时和结果记录仍然应该由程序控制。

AI 负责理解和决策,程序负责约束和执行。两者配合,才更有机会把 AI Agent 真正用到软件测试工作中。

Logo

更多推荐