首页 / 文章

利用 AI 智能体自动化 Laravel 开发:构建、测试、修复,循环迭代

使用 Laravel Boost 和 OpenCode CLI,通过队列任务、调度器、重试限制以及人工审核,在任何内容发布之前执行构建、测试和修复的循环流程。

发布者:REACTAPP.TOP阅读约 14 分钟3315 词

来源: Build → Test → Fix → Repeat: Automating Laravel Development with AI Agents

复制粘贴的循环

算一算,使用人工智能编程助手的典型一天会重复多少次这样的流程:编写提示词,将输出结果复制到编辑器中,运行php artisan test,看到测试失败后,复制错误信息再粘贴给人工智能,要求“请修复这个问题”,然后从复制步骤重新开始。

有些开发者每天要重复这个循环数十次。这虽然被称作人工智能辅助开发,但实际上在这个流程中最忙碌的并非人工智能,而是那个在模型与终端之间传递信息的人——他负责决定何时运行测试、阅读错误信息、判断是否需要进一步修复,以及不断来回复制各种内容。人工智能则会在下一条指令到来之前一直处于闲置状态。

更好的方案是取消人工快递员。一个小型系统可以独立完成构建、测试、修复、重复的循环,直到工作真正完成,仅在最终审核时才需要人工介入。此处使用的工具是Laravel Boost和OpenCode CLI,这两款工具都非常适合当前的Laravel生态系统。讲解过程刻意放慢节奏,因此即使从未使用过这些工具的人也能跟上。

术语表

有一些术语会反复出现:

  • Agent:被赋予特定任务和工具集的AI,相当于拥有明确职责的员工。
  • MCP(模型上下文协议):一种连接方式,让AI能够直接向系统提问,无论是数据库还是日志,从而不必根据你随意输入的内容进行猜测。
  • CLI(命令行界面):通过在终端提示符下输入命令来操作软件,而非在图形界面上进行点击操作。
  • 任务与队列:Laravel处理后台工作的方式,它按顺序逐一执行任务,避免用户一直看到加载中状态。
  • 调度器:负责安排任务顺序、跟踪当前该谁执行任务,以及在任务失败后知道从何处继续的流量控制器。
  • 问题已从“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编码实验变得有趣的地方。