【AI Agent 软件测试实战(04):构建 Agent Loop,让 AI 学会多步骤测试任务执行】
## 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. 返回查询结果
看起来已经能够完成任务了,但实际测试工作中,问题往往没有这么简单。
比如我们需要检查一个订单是否存在异常,通常至少需要完成以下操作:
- 查询订单详情。
- 检查订单金额是否合法。
- 检查订单状态是否正常。
- 根据前面几步的结果判断是否存在异常。
- 整理检查结果并生成报告。
这里有一个关键问题:
Agent 调用完一个工具之后,不能直接结束,而是需要根据工具返回的结果,决定下一步做什么。
这就是 Agent Loop 要解决的问题。
1.1 Tool Calling 和 Agent Loop 有什么区别?
两者不是替代关系,而是不同层面的能力。
| 对比项 | Tool Calling | Agent 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 主要完成以下事情:
- 接收用户任务。
- 将任务放入对话上下文。
- 调用大模型。
- 判断模型是否提出工具调用。
- 执行工具并保存结果。
- 将结果反馈给模型。
- 继续下一轮,直到模型给出最终回答。
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 真正用到软件测试工作中。
更多推荐


所有评论(0)