利用 AI 智能体自动化 Laravel 开发:构建、测试、修复,循环迭代
使用 Laravel Boost 和 OpenCode CLI,通过队列任务、调度器、重试限制以及人工审核,在任何内容发布之前执行构建、测试和修复的循环流程。
来源: Build → Test → Fix → Repeat: Automating Laravel Development with AI Agents
复制粘贴的循环
算一算,使用人工智能编程助手的典型一天会重复多少次这样的流程:编写提示词,将输出结果复制到编辑器中,运行php artisan test,看到测试失败后,复制错误信息再粘贴给人工智能,要求“请修复这个问题”,然后从复制步骤重新开始。
有些开发者每天要重复这个循环数十次。这虽然被称作人工智能辅助开发,但实际上在这个流程中最忙碌的并非人工智能,而是那个在模型与终端之间传递信息的人——他负责决定何时运行测试、阅读错误信息、判断是否需要进一步修复,以及不断来回复制各种内容。人工智能则会在下一条指令到来之前一直处于闲置状态。
更好的方案是取消人工快递员。一个小型系统可以独立完成构建、测试、修复、重复的循环,直到工作真正完成,仅在最终审核时才需要人工介入。此处使用的工具是Laravel Boost和OpenCode CLI,这两款工具都非常适合当前的Laravel生态系统。讲解过程刻意放慢节奏,因此即使从未使用过这些工具的人也能跟上。
术语表
有一些术语会反复出现:
- Agent:被赋予特定任务和工具集的AI,相当于拥有明确职责的员工。
- MCP(模型上下文协议):一种连接方式,让AI能够直接向系统提问,无论是数据库还是日志,从而不必根据你随意输入的内容进行猜测。
问题已从“AI能否编写代码”转向其他方向
任何使用过Claude Code、Cursor或类似工具处理Laravel项目的人都知道,AI能够生成迁移文件、模型、策略和测试代码。剩下的问题在于其周围的手动工作流程:
Developer → AI → Developer → Test → Developer → AI → Developer → Test → ...
每个箭头都表示有人手动将上下文从一步推进到下一步,决定是时候进行测试了,或者有错误需要反馈给人工智能。
对比自动化版本:
Developer → Orchestrator → Build → Test → (failed? → Fix → Test again) → Review → Done
这个差距看似很小,但实际上影响巨大。现在有了协调机制,这一层能够掌控任务的执行顺序、决定何时重试以及何时停止,而无需每三十秒就有人介入干预。
Laravel也在朝这个方向发展。Laravel Boost的诞生就是为了让AI编码工具能够获取关于Laravel应用程序的真实上下文,而非仅凭通用模式进行猜测。
两种主要工具
Laravel Boost:AI与应用程序之间的桥梁
一个AI可能在Laravel技术层面表现优异,但对于特定项目却可能一无所知。它不知道你使用的是哪个版本的Laravel,数据库的实际结构如何,是否使用了Livewire或Inertia,以及团队约定的命名规范是什么。
Boost 是一个 Composer 包,它能让 AI 了解项目的真实状况。它通过 MCP 实现功能,因此 AI 可以直接查询应用程序:查看数据库结构、浏览现有路由、查看最新的错误信息、执行 Artisan 命令,以及查阅与实际安装的包版本对应的 Laravel 文档。
安装只需两条命令即可完成:
composer require laravel/boost --dev
php artisan boost:install
第二个是交互式的。它会询问你使用的是哪种IDE或AI工具,然后安装相应的资源。
需要明确一点:Boost只是翻译上下文,并不会做出决策。它不会说“现在运行测试”或“失败时启动修复代理”。它的唯一职责就是防止代理对应用程序的实际情况一无所知。
OpenCode:代理执行任务的地方
如果Boost负责翻译上下文,那么OpenCode就是工作场所:一个专为AI编程打造的命令行界面。与普通聊天机器人不同,它允许你定义多个具有不同职责的代理,比如一个负责编写代码,另一个负责读取测试结果,每个代理都有其自身的指令和限制。
OpenCode还可以连接到MCP服务器,其中包括Boost。一旦连接成功,Boost提供的所有功能都会可供在OpenCode中运行的代理使用。
实操:将 Boost 与 OpenCode 相连
在实现自动化之前,先在终端中手动尝试操作。
1. 指定 OpenCode 的 Boost 地址
安装了 Boost 后,OpenCode 需要知道其 MCP 服务器的位置。该信息会被保存在项目根目录下的 opencode.json 文件中:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"laravel-boost": {
"type": "local",
"command": ["php", "artisan", "boost:mcp"],
"enabled": true
}
}
}
具体命令可能因 Boost 版本不同而有所差异,因此请遵循 php artisan boost:install 生成的命令,而非盲目复制此内容。这是各版本之间最容易发生变化的部分之一。
请确认 OpenCode 能够访问服务器:
opencode mcp list
如果 laravel-boost 显示为已连接状态,那么这些代理就能访问 Boost 提供的所有功能。
2. 创建具有不同职责的代理
不要让一个代理承担所有任务,而应划分职责。在项目中创建一个 .opencode/agent/builder.md 文件:
---
description: Build Agent for new features
mode: primary
tools:
write: true
edit: true
---
You are the Build Agent.
Use Laravel Boost tools to understand the application structure
before writing any code. Never assume the database structure -
always check the schema first. Write tests for every new behavior.
At the end of your work, return ONLY JSON in this format:
{"status": "success", "files_changed": [...], "summary": "..."}
tools.write: true 这一行允许该代理修改文件。后续的代理将设置为 write: false,从而变为只读状态且无法触碰任何内容,其背后的原因稍后会说明。
列出已注册的代理:
opencode agent list
3. 先手动运行它
opencode run --agent builder "Add a task assignment feature to TaskFlow according to the requirements in TASK.md"
关于该命令的一些说明:
--agent用于指定要运行的代理,可以是构建器、测试器或其他已定义的类型。- 添加
--format json后,输出将为原始数据而非供人阅读的格式,这正是后续 PHP 调用所需要使用的形式。 --session和--continue允许代理记住之前的对话内容,而无需每次都重新开始。
在实现自动化之前,先手动运行几次以熟悉其用法。
具体案例:TaskFlow 中的任务分配
一个实际的示例有助于更好地理解。假设存在一个名为 TaskFlow 的简单任务管理应用,它需要具备以下功能:用户可以将任务分配给另一位用户,但仅限于双方都有权限访问的项目范围内。具体需求可拆解为:
1. A task has an assignee column (belongs to a User).
2. A user can only assign tasks within a project they're a member of.
3. A user must not assign or access tasks from other projects.
4. All of the above must be covered by automated tests.
这是一个常见的需求,也正是它成为绝佳演示案例的原因。这类场景往往需要两三次尝试,才能确保“谁可以访问什么”这一问题——在技术上称为授权——得到妥善解决。而这正是自动化循环发挥作用的地方。
为何该流程需要日志本
一旦任务在后台运行,就没人能仅凭记忆来回忆“那个错误到底是什么”。所有内容都必须被记录下来,而在这里,这意味着要使用数据库表作为进度日志本:
Schema::create('ai_tasks', function (Blueprint $table) {
$table->id();
$table->string('type');
$table->string('status')->default('pending');
$table->text('prompt');
$table->json('result')->nullable();
$table->unsignedTinyInteger('iteration')->default(0);
$table->timestamps();
});
模型:
class AiTask extends Model
{
protected $fillable = [
'type', 'status', 'prompt', 'result', 'iteration',
];
protected function casts(): array
{
return ['result' => 'array'];
}
}
status列起着关键作用。它会经历以下这些阶段:
pending → building → testing → (fixing → testing)* → reviewing → completed / failed
为何不使用简单的 done 标志呢?因为调度器需要知道任务处于哪个阶段,而不仅仅是它是否已完成。阶段跟踪还能在某个工作节点运行过程中崩溃时让任务继续执行,并在任何地方显示实时进度。
将 OpenCode 集成到 PHP 中
人们常常跳过这一步:不要将 OpenCode 命令分散在多个文件中。如果后续更换工具,也不必修改数十个地方。应创建一个入口点,即一个接口:
interface AgentRunner
{
public function run(string $agent, string $prompt): array;
}
接着是实际实现部分,即从 PHP 调用 OpenCode 的代码:
class OpenCodeAgentRunner implements AgentRunner
{
public function run(string $agent, string $prompt): array
{
$result = Process::timeout(600)->run([
'opencode', 'run',
'--agent', $agent,
'--format', 'json',
$prompt,
]);
if (! $result->successful()) {
return [
'status' => 'error',
'error' => $result->errorOutput(),
];
}
return json_decode($result->output(), true) ?? [
'status' => 'error',
'error' => 'Agent output was not valid JSON',
];
}
}
在服务提供商处进行绑定:
$this->app->bind(AgentRunner::class, OpenCodeAgentRunner::class);
采用这种模式后,每个任务都只与AgentRunner通信,而无需关心其背后是OpenCode还是其他系统。
每个阶段一个任务
对于刚接触Laravel Jobs的人来说:任务是通过队列在后台运行的作业,因此无需有人等待其完成。
构建任务
class BuildFeature implements ShouldQueue
{
public function __construct(protected AiTask $task) {}
public function handle(AgentRunner $agent): void
{
$result = $agent->run(
agent: 'builder',
prompt: $this->task->prompt,
);
$this->task->update([
'status' => 'testing',
'result' => $result,
]);
RunTests::dispatch($this->task);
}
}
在这里,构建代理会运行,其结果会被保存,随后流程会直接进入下一个任务(RunTests),无需手动点击操作。
测试任务
这个任务被刻意设计得非常简单。它无需理解代码,只需运行测试套件并读取测试结果即可。
class RunTests implements ShouldQueue
{
public function __construct(protected AiTask $task) {}
public function handle(): void
{
$result = Process::timeout(300)->run('php artisan test --compact');
if ($result->successful()) {
$this->task->update(['status' => 'reviewing']);
ReviewFeature::dispatch($this->task);
return;
}
$this->task->update(['status' => 'failed']);
AnalyzeFailure::dispatch($this->task, $result->output());
}
}
流程很简单:
Test
├── passed → move to Review
└── failed → move to Analyze
不要将原始错误直接传递给代理
这一环节最容易被忽视,却对质量影响最大。仅仅告知代理测试失败并要求其修复远远不够。代理必须从零开始,重新读取所有文件并推测问题所在。
首先需对故障进行结构化分析:
{
"status": "failed",
"failed_tests": [
{
"name": "user_cannot_assign_task_from_other_project",
"error": "Expected response status code [403] but received 200."
}
]
}
然后将其交给专门负责诊断的代理,可称其为“调试代理”,并给出严格限制其操作范围的指令:
You are the Debug Agent.
Analyze the failed tests below. DO NOT modify any files.
Inspect: relevant models, policies, migrations, and tests.
Return JSON with:
- root_cause
- affected_files
- recommended_fix
- risk_of_regression
为何要将诊断与修复分开?是为了在理解问题与执行修复之间制造隔阂。如果只是让智能体直接去修复问题,那么记录某个文件为何发生变化的线索就会消失,最终审核也会变得极为难以验证。
对于TaskFlow而言,调试智能体的输出很可能会类似如下形式:
{
"status": "analyzed",
"root_cause": "TaskPolicy does not verify project membership before allowing assignment",
"affected_files": ["app/Policies/TaskPolicy.php"],
"recommended_fix": "Add a project membership check inside the assign() method",
"next_action": "fix"
}
这确实是一个可信的发现,因为这是最常见的授权漏洞之一:相关策略仅检查用户是否已登录,而不会确认其是否真正属于该项目。
修复后再测试,切勿修完就了事
class FixFeature implements ShouldQueue
{
public function __construct(protected AiTask $task, protected array $analysis) {}
public function handle(AgentRunner $agent): void
{
$result = $agent->run(
agent: 'fixer',
prompt: json_encode($this->analysis),
);
$this->task->increment('iteration');
$this->task->update([
'status' => 'testing',
'result' => $result,
]);
RunTests::dispatch($this->task);
}
}
请注意,修复之后状态会恢复为 testing,而不会直接变为 completed。原则很明确:仅因为文件发生了变化,代理程序就无权自行宣布任务已完成。唯一有效的正确性证明是真正重新运行过的测试套件,而非代理程序自身的声明。
作为流量控制器的协调器
起初,将任务按线性顺序依次执行似乎很有吸引力:
Bus::chain([
new BuildFeature($task),
new RunTests($task),
new ReviewFeature($task),
])->dispatch();
但这仅仅是一条线。一旦出现分支,比如在测试失败时返回进行修复,线性链就不够用了。这时需要的就像一个交通调度员,每次被调用时都会检查当前状况,并据此选择下一步行动:
class AgentOrchestrator
{
public function next(AiTask $task): void
{
match ($task->status) {
'pending' => BuildFeature::dispatch($task),
'testing' => RunTests::dispatch($task),
'failed' => AnalyzeFailure::dispatch($task),
'fixing' => RunTests::dispatch($task),
'reviewing' => ReviewFeature::dispatch($task),
'completed', 'stopped' => null,
default => throw new LogicException("Unknown status: {$task->status}"),
};
}
}
思维方式的转变意义重大。协调器不再询问下一个任务是什么,而是会判断在当前状态下哪种转换是有效的。这样更便于调试,而且无需打乱整个流程即可添加安全审查之类的新阶段。
限制重试次数
如果没有限制,可能会出现如下情况:
Fix → Test fails → Fix → Test fails → Fix → Test fails → ...
那样会浪费时间,而且每次调用人工智能都会产生费用。解决办法是设定上限:
class RunTests implements ShouldQueue
{
protected const MAX_ITERATIONS = 5;
public function handle(): void
{
if ($this->task->iteration >= self::MAX_ITERATIONS) {
$this->task->update(['status' => 'needs_human_review']);
return;
}
// ... run tests as usual
}
}
如果五次修复尝试仍不成功,系统会停止并将该任务标记为需要人工处理,而不会无限尝试。这并不一定意味着人工智能不擅长这项工作。有时需求本身就不明确,或者某些决策确实需要人类来决定。
测试通过并不等同于功能已完成
一旦看到系统能够自动运行就觉得令人兴奋,就很容易忘记这一点。假设智能体写了类似这样的内容:
public function assign(User $user, Task $task): bool
{
return $user->isAdmin();
}
所有测试都可能通过,因为这些测试仅覆盖了当时人们设想的场景。实际需求规定用户必须是项目成员,而非管理员。尽管所有测试结果均为通过,但逻辑本身是错误的。
这就是该流程存在安全隐患的原因:
Build → Tests pass → Deploy immediately
更安全的那一个看起来像这样:
Build → Test → (if failed: Debug → Fix → Test again) → Human review → Deploy
对于任何涉及权限控制的内容,审核阶段至少应由一个人参与。像这样的安全敏感变更,无论自动化流程看起来多么完善,要求在合并之前进行人工审核都是明智的。
并行代理与文件冲突
同时运行安全代理和研究代理的做法会带来一个问题:如果两个代理几乎同时在同一文件上写入数据,就会出现竞态条件,导致两个进程互相覆盖彼此的修改结果。
Agent A → writes Task.php
Agent B → reads Task.php (nearly at the same time)
Agent A → writes Task.php again
基本原则是根据谁可以编写内容、谁只能阅读内容来划分职责:
Research Agent → read-only
Review Agent → read-only
Security Agent → read-only
Build Agent → write access
Fix Agent → write access
这也解释了之前builder.md中出现的tools.write: true这一行。它用于控制哪些代理可以修改文件,哪些只能查看。
抵制超级代理
一旦这种机制可行,最大的诱惑就是让一个代理同时负责规划、构建、测试、调试、修复、审查和部署工作。但这往往适得其反。一个代理承担的责任越多,需要处理的信息就越多,掌握的工具也越多,出错的可能性也就越大。按职责划分会更有效:
Planner → designs the approach
Builder → writes the new feature's code
Tester → runs the test suite
Debugger → diagnoses failures (read-only)
Fixer → executes the fix
Reviewer → final quality check (read-only)
其原理与避免创建一个处理应用程序中所有事务的庞大类相同。同样也不应构建一个能完成所有工作的代理。
实际使用前的最低要求清单
在真实项目中进行实验时,这些措施应从一开始就落实,而非事后添加:
- 设置重试次数上限,防止修复循环无限运行
- 为代理执行的每条命令设定时间限制
- 根据代理的角色设置文件访问权限,仅为只读或写入
- 在状态进入审核阶段之前,必须先有通过测试的验证
- 任何内容在投入生产前都需要人工审批
- 记录代理做出的每一个决策,而不仅仅是最终结果
- 保护如
.env这样的敏感文件,避免代理意外读取它们
最后一点常常被低估。一旦代理能够通过上述的 Process::run() 方法在系统中执行命令,它实际上就可以在该服务器上运行几乎任何程序。访问限制是从一开始就必须具备的要求,而非可选项。
Boost的实际作用
以下是完整情况:
Developer (us)
│
▼
Orchestrator (traffic controller, running on Laravel Queue)
│
├── Build Agent
├── Test Agent
├── Debug Agent
└── Fix Agent
│
▼
OpenCode (where the agent does its work)
│
▼
Laravel Boost (context translator, via MCP)
│
▼
Our Laravel application
Boost不会主动决定运行测试,也不会在出现故障时启动修复代理。它只做一件事,而且是一件非常重要的事:确保项目中的每个代理都能准确了解应用程序的实时状态,而不会依据通用模式进行猜测。由Laravel Queue构建的协调器负责决定谁在何时采取行动,以及何时停止所有操作。
现在需要思考的问题
几年前,一个有趣的问题是人工智能能否编写出正确的Laravel代码。显然它可以,并且还在不断进步。如今更关键的问题是,人工智能能否自行构建一个包含构建、测试、出错、分析原因、修复以及再次测试的反馈循环,直到真正解决问题,而无需在每一步都依赖人工干预。
只要注意三点,TaskFlow场景下的答案便是肯定的。首先需要对进度进行清晰记录,其次要为重试设置明确的次数限制,最后绿色测试结果应被视为一个信号,而非任务完成的标志。
Boost有助于客服人员理解应用程序的上下文。OpenCode为他们提供了工作场所。Laravel Queue则成为协调所有任务的骨干。而开发者们也能提升职责层级,从每一步都亲自操作的人转变为最终结果的审核者。
Vibe编码功能能加快编写速度。这样的编排方式能让编码过程自动运行,而这正是AI编码实验变得有趣的地方。