ReAct与Plan&Execute:Agent决策模式实战
发布日期: 2026/08/21 阅读总量: 1

先说我踩的坑

公司客服工单系统接入大模型,第一个版本很天真:让LLM直接返回JSON,指定调哪个工具、传什么参数。上线第一天就出事了。

用户问「我的工单为什么还没处理」,LLM返回:

{"tool": "query_ticket", "params": {"ticket_id": "abc123"}}

看着没问题对吧?但线上跑起来,1000次请求里有183次JSON是坏的:多一个逗号、缺少右括号、被markdown代码块包住、更离谱的是LLM在JSON前面加了一句「好的,我来帮您查询」。18.3%的解析失败率,工单小组差点把我手机打爆。

后来我把架构改成AI Agent模式,试了两种主流方案:ReAct和Plan&Execute。这篇文章把两种方案的实现、压测数据和踩过的坑都写清楚。环境是PHP8.3 + OpenAI gpt-4o-mini,压测用Apache Bench。

问题的本质

LLM直接返回结构化数据这事,能跑通demo,不能上生产。原因有三个:

  • 预训练模型的输出分布不是按JSON结构来的,它按token概率来,json语法小概率出错是必然
  • 复杂任务需要多步推理+多工具协作,一步生成的JSON根本描述不了过程
  • 用户问题里经常有歧义,模型没有机会反问或澄清,一步到位只能靠猜

Agent模式把「一次生成」拆成「循环决策」,模型每步只做一件事:观察当前状态 → 决定下一步动作 → 执行工具 → 再观察。这就是ReAct。Plan&Execute更进一步,先把整个任务拆成计划,再逐步执行。

方案一:ReAct模式

基本原理

ReAct来自Yao et al. 2022年的论文《ReAct: Synergizing Reasoning and Acting in Language Models》。核心是把推理轨迹(Thought)和动作(Action)交替写入prompt:

问题: {用户问题}
可用工具: {工具列表}

思考1: 用户想查工单状态,需要先定位工单ID
动作1: 搜索工单ID [关键词: 4567]
观察1: 找到工单 #T-2024-015,状态为待处理
思考2: 工单存在,状态是待处理,需要查一下超时时间
动作2: 获取工单详情 [工单号: T-2024-015]
观察2: 创建于3天前,SLA时限48小时,已超时
思考3: 已超时,告诉用户并说明预计处理时间
动作3: 最终回答 [工单T-2024-015已超时,预计明天12:00前处理完毕]

每次LLM调用只生成「一个」Thought + Action对,然后执行工具,把结果作为Observation拼回prompt,继续下一轮。循环直到Action类型是「最终回答」。

代码实现

我把它封装成PHP类,核心是循环控制。关键点:维护好对话历史,每轮把Thought/Action/Observation追加进去。

<?php
/**
 * 极简ReAct Agent实现
 * PHP 8.3 + curl,不依赖Composer包,直接能跑
 */

class ReActAgent
{
    private string $apiKey;
    private string $model = 'gpt-4o-mini';
    private array $tools = [];
    private array $messages = [];

    public function __construct(string $apiKey, array $tools)
    {
        $this->apiKey = $apiKey;
        $this->tools = $tools;
    }

    /**
     * 运行Agent,返回最终答案
     */
    public function run(string $userQuery, int $maxSteps = 8): string
    {
        $this->messages = [
            ['role' => 'system', 'content' => $this->buildSystemPrompt()],
            ['role' => 'user', 'content' => $userQuery],
        ];

        for ($step = 0; $step < $maxSteps; $step++) {
            $response = $this->callLLM();
            $parsed = $this->parseAction($response);

            if ($parsed['type'] === 'final') {
                return $parsed['answer'];
            }

            if ($parsed['type'] === 'action') {
                $observation = $this->executeTool($parsed['tool'], $parsed['params']);
                $this->messages[] = [
                    'role' => 'assistant',
                    'content' => "Thought: {$parsed['thought']}\nAction: {$parsed['raw_action']}",
                ];
                $this->messages[] = [
                    'role' => 'user',
                    'content' => "Observation: $observation",
                ];
            }
        }

        return '已达最大步数,未能完成任务';
    }

    private function buildSystemPrompt(): string
    {
        $toolDesc = [];
        foreach ($this->tools as $tool) {
            $toolDesc[] = "{$tool['name']}: {$tool['description']} 参数: " . json_encode($tool['params']);
        }

        return <<<PROMPT
你是工单处理助手。请严格按照以下格式循环推理:

可用工具:
- query_ticket_status: 查询工单状态
  参数: {"ticket_id": "字符串"}
- get_ticket_detail: 获取工单详情
  参数: {"ticket_id": "字符串"}
- escalate_ticket: 升级工单
  参数: {"ticket_id": "字符串", "reason": "字符串"}

必须使用如下格式:
Thought: 你的推理过程
Action: 工具名: {"参数名": "参数值"}
Observation: 工具返回结果(系统填充)

当得到足够信息时,输出:
Thought: 我得到了足够的信息
Final Answer: 给用户的最终答复
PROMPT;
    }

    private function callLLM(): string
    {
        $ch = curl_init('https://api.openai.com/v1/chat/completions');
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                'Authorization: Bearer ' . $this->apiKey,
            ],
            CURLOPT_POSTFIELDS => json_encode([
                'model' => $this->model,
                'messages' => $this->messages,
                'temperature' => 0,
                'max_tokens' => 300,
            ]),
        ]);

        $result = curl_exec($ch);
        curl_close($ch);
        $data = json_decode($result, true);

        return $data['choices'][0]['message']['content'] ?? '';
    }

    private function parseAction(string $llmOutput): array
    {
        // 尝试解析 Final Answer
        if (preg_match('/Final Answer:\s*(.+)/s', $llmOutput, $m)) {
            return ['type' => 'final', 'answer' => trim($m[1])];
        }

        // 解析 Thought 和 Action
        if (preg_match('/Thought:\s*(.+?)\s*Action:\s*(\w+):\s*(\{.+?\})/s', $llmOutput, $m)) {
            $params = json_decode($m[3], true);
            if (json_last_error() !== JSON_ERROR_NONE) {
                return ['type' => 'final', 'answer' => '抱歉,我无法解析您的请求,请重新描述。'];
            }
            return [
                'type' => 'action',
                'thought' => $m[1],
                'tool' => $m[2],
                'raw_action' => trim($m[0]),
                'params' => $params,
            ];
        }

        return ['type' => 'final', 'answer' => $llmOutput];
    }

    private function executeTool(string $toolName, array $params): string
    {
        foreach ($this->tools as $tool) {
            if ($tool['name'] === $toolName) {
                try {
                    return call_user_func($tool['callback'], $params);
                } catch (\Throwable $e) {
                    return '工具执行失败: ' . $e->getMessage();
                }
            }
        }
        return "未知工具: $toolName";
    }
}

// 使用示例
$tools = [
    [
        'name' => 'query_ticket_status',
        'description' => '根据工单ID查询工单状态',
        'params' => ['ticket_id' => 'string'],
        'callback' => function (array $params) {
            // 模拟数据库查询
            $tickets = [
                'T-2024-015' => ['status' => 'pending', 'created_at' => '2024-03-01 10:00:00'],
                'T-2024-016' => ['status' => 'resolved', 'created_at' => '2024-03-01 09:30:00'],
            ];
            $id = $params['ticket_id'] ?? '';
            return json_encode($tickets[$id] ?? ['error' => '工单不存在']);
        },
    ],
];

$agent = new ReActAgent(getenv('OPENAI_API_KEY'), $tools);
$answer = $agent->run('我工单T-2024-015怎么还没处理?');
echo $answer;

压测结果

用Apache Bench跑1000个请求,并发50:

ab -n 1000 -c 50 -T 'application/json' -p request.json https://api.example.com/react-agent
# ReAct模式压测结果(1000次请求)
# 成功率: 96.5%
# 平均响应时间: 3.2s
# P95响应时间: 5.8s
# 工具调用成功率: 96.5%
# 平均LLM调用次数: 3.4次
# 最大LLM调用次数: 7次

比裸JSON方式好用,但有个问题:响应慢。每次工具调用都走一次LLM,一个简单问题得3次来回。平均3.2秒,放网页端勉强能接受,但用户反馈「转圈圈」。

方案二:Plan & Execute模式

基本原理

Plan&Execute的思路是两阶段:先规划、后执行。先把用户问题丢给规划器(Planner),让它一次性输出完整步骤列表,然后一个执行器(Executor)按步骤调用工具,不需要每步都问LLM。

用上面那个例子:

规划器输出:
1. 调用 query_ticket_status 查询工单T-2024-015的状态
2. 如果状态是pending,调用 get_ticket_detail 获取创建时间和SLA信息
3. 根据SLA信息计算是否超时
4. 汇总最终答复

执行器按步骤调用工具,收集每步结果。关键优化:局部步骤如果依赖前一步的输出(比如第2步依赖第1步的工单ID),要按顺序执行。不依赖的步骤可以并行。

代码实现

<?php
/**
 * 极简 Plan & Execute Agent 实现
 * 规划器(Planner) + 执行器(Executor)
 */

class PlanExecuteAgent
{
    private string $apiKey;
    private array $tools;
    private array $messages = [];

    public function __construct(string $apiKey, array $tools)
    {
        $this->apiKey = $apiKey;
        $this->tools = $tools;
    }

    /**
     * 运行Agent
     */
    public function run(string $userQuery): string
    {
        // Step 1: 规划
        $plan = $this->createPlan($userQuery);
        if (isset($plan['error'])) {
            return $plan['error'];
        }

        // Step 2: 执行
        $results = [];
        foreach ($plan['steps'] as $index => $step) {
            $toolName = $step['tool'];
            $params = $step['params'];

            // 支持变量替换: 前序步骤的结果可以引用
            foreach ($params as $key => $value) {
                if (is_string($value) && preg_match('/\{\{step(\d+)_(\w+)\}\}/', $value, $m)) {
                    $prevResult = $results[$m[1] - 1] ?? [];
                    $params[$key] = $prevResult[$m[2]] ?? $value;
                }
            }

            $result = $this->executeTool($toolName, $params);
            $results[$index] = $result;

            // Step 3: 检查是否需要重规划
            $needsReplan = $this->checkNeedReplan($step, $result);
            if ($needsReplan['replan']) {
                $plan = $this->replan($userQuery, $plan, $results, $needsReplan['reason']);
                // 重新执行剩余步骤(示例简化:重置循环)
                $results = [];
                foreach ($plan['steps'] as $i => $s) {
                    $results[$i] = $this->executeTool($s['tool'], $s['params']);
                }
                break;
            }
        }

        // Step 4: 生成最终回答
        return $this->generateFinalAnswer($userQuery, $results);
    }

    private function createPlan(string $query): array
    {
        $prompt = <<<PROMPT
你是任务规划器。将用户问题拆解为可执行的步骤列表。

用户问题: $query

可用工具:
- query_ticket_status: 查询工单状态,参数: {"ticket_id": "string"}
- get_ticket_detail: 获取工单详情,参数: {"ticket_id": "string"}
- escalate_ticket: 升级工单,参数: {"ticket_id": "string", "reason": "string"}
- calculate_sla: 计算SLA超时时间,参数: {"created_at": "string", "sla_hours": "int"}

请严格输出JSON数组,不要输出任何其他内容:
[
  {"step": 1, "tool": "工具名", "params": {"参数名": "参数值"}, "description": "这一步做什么"},
  {"step": 2, "tool": "工具名", "params": {"参数名": "参数值"}, "description": "这一步做什么"}
]

规则:
1. 参数不能凭空编造,如果不知道参数值,用占位符 {{stepN_字段名}} 引用前序步骤输出
2. 步骤数量控制在1-5个
3. 尽量让每一步独立,减少步骤间依赖
PROMPT;

        $response = $this->callLLM($prompt);
        $plan = json_decode($response, true);

        if (json_last_error() !== JSON_ERROR_NONE || !isset($plan[0])) {
            // 解析失败,尝试提取JSON数组
            preg_match('/\[.*\]/s', $response, $m);
            if (empty($m)) {
                return ['error' => '规划器解析失败'];
            }
            $plan = json_decode($m[0], true);
            if (json_last_error() !== JSON_ERROR_NONE) {
                return ['error' => '规划器输出格式错误'];
            }
        }

        return ['steps' => $plan];
    }

    private function callLLM(string $prompt): string
    {
        $ch = curl_init('https://api.openai.com/v1/chat/completions');
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                'Authorization: Bearer ' . $this->apiKey,
            ],
            CURLOPT_POSTFIELDS => json_encode([
                'model' => 'gpt-4o-mini',
                'messages' => [
                    ['role' => 'system', 'content' => $prompt],
                ],
                'temperature' => 0,
                'max_tokens' => 500,
            ]),
        ]);

        $result = curl_exec($ch);
        curl_close($ch);
        $data = json_decode($result, true);

        return $data['choices'][0]['message']['content'] ?? '';
    }

    private function executeTool(string $toolName, array $params): array
    {
        foreach ($this->tools as $tool) {
            if ($tool['name'] === $toolName) {
                $output = call_user_func($tool['callback'], $params);
                return ['output' => $output, 'tool' => $toolName];
            }
        }
        return ['output' => ['error' => "未知工具: $toolName"], 'tool' => $toolName];
    }

    private function checkNeedReplan(array $step, array $result): array
    {
        // 如果工具返回错误,需要重规划
        if (isset($result['output']['error'])) {
            return ['replan' => true, 'reason' => '工具执行失败: ' . $result['output']['error']];
        }

        // 如果结果中缺少后续步骤需要的字段,需要重规划
        $requiredFields = $step['required_outputs'] ?? [];
        foreach ($requiredFields as $field) {
            if (!isset($result['output'][$field])) {
                return ['replan' => true, 'reason' => "缺少字段: $field"];
            }
        }

        return ['replan' => false];
    }

    private function replan(string $query, array $oldPlan, array $results, string $reason): array
    {
        // 简化实现:带上下文重新规划
        $context = "原计划:\n" . json_encode($oldPlan) . "\n执行结果:\n" . json_encode($results) . "\n失败原因: $reason";
        $prompt = "原计划执行失败,请重新规划:\n$context\n\n" . "用户原始问题: $query\n请输出新的步骤列表,保持JSON数组格式。";

        $response = $this->callLLM($prompt);
        $newPlan = json_decode($response, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            preg_match('/\[.*\]/s', $response, $m);
            if (!empty($m)) {
                $newPlan = json_decode($m[0], true);
            }
        }

        return ['steps' => $newPlan ?? []];
    }

    private function generateFinalAnswer(string $query, array $results): string
    {
        $prompt = <<<PROMPT
根据以下工具执行结果,生成给用户的最终答复。
用户问题: $query

执行结果:
PROMPT;

        foreach ($results as $i => $result) {
            $prompt .= "\n步骤" . ($i + 1) . " ({$result['tool']}): " . json_encode($result['output']);
        }

        $prompt .= "\n\n请用友好的语气回答用户,不要提到「工具」或「步骤」这些词。";

        return $this->callLLM($prompt);
    }
}

// 使用示例
$tools = [
    [
        'name' => 'query_ticket_status',
        'callback' => function (array $params) {
            $tickets = [
                'T-2024-015' => ['status' => 'pending'],
                'T-2024-016' => ['status' => 'resolved'],
            ];
            return $tickets[$params['ticket_id']] ?? ['error' => '工单不存在'];
        },
    ],
    [
        'name' => 'get_ticket_detail',
        'callback' => function (array $params) {
            $details = [
                'T-2024-015' => ['created_at' => '2024-03-01 10:00:00', 'sla_hours' => 48],
            ];
            return $details[$params['ticket_id']] ?? ['error' => '工单不存在'];
        },
    ],
];

$agent = new PlanExecuteAgent(getenv('OPENAI_API_KEY'), $tools);
$answer = $agent->run('我工单T-2024-015怎么还没处理?');
echo $answer;

这里要注意:我用了两种不同的LLM调用方式。ReAct是循环交互,每轮一个请求。Plan&Execute是两段式:一次规划请求 + 多次工具调用(不经过LLM) + 一次总结请求。工具调用不走LLM,这就是关键性能差异。

执行阶段优化:DAG并行

如果步骤之间没有依赖关系,可以并行执行。我用了一个简单的DAG调度器,把独立步骤并发跑。压测数据对比:

# 串行执行
# 4个独立步骤耗时: 320ms
# 并发执行
# 4个独立步骤耗时: 80ms

两种模式对比

维度ReActPlan & Execute
LLM调用次数3-7次2次(规划+总结)
平均延迟3.2s1.1s
P95延迟5.8s1.9s
工具调用成功率96.5%92.3%
重规划/重试率3.5%(工具调用失败后重试)18%(规划偏差导致)
多工具协作能力强,每步观察后动态调整中,规划错了很被动
适合场景需要频繁根据中间结果调整的复杂任务流程固定、步骤清晰的标准化任务

数据说明:压测请求是真实的1000条工单查询+处理请求,混合了简单查询(一步)、中等流程(两步)、复杂工单(四步含升级操作)。Plan&Execute的18%重规划率全来自规划器理解错用户意图——比如用户说「我的工单被驳回的」,规划器直接去查状态,没查驳回原因。

混合方案:Plan & Execute + 局部ReAct

真实生产环境我最后用了一个混合方案:Plan&Execute做顶层调度,但在每个步骤内部允许ReAct式的小循环。比如「查工单详情」这步,如果发现工单状态是"被驳回",就进入ReAct循环去查驳回原因、看申诉流程。

/**
 * 混合Agent: Plan&Execute做顶层调度
 * 每个步骤可以配置是否启用ReAct子循环
 */
class HybridAgent
{
    private PlanExecuteAgent $planner;
    private ReActAgent $reactor;

    public function __construct(string $apiKey, array $tools)
    {
        $this->planner = new PlanExecuteAgent($apiKey, $tools);
        $this->reactor = new ReActAgent($apiKey, $tools);
    }

    public function run(string $query): string
    {
        // 先规划
        $plan = $this->planner->createPlan($query);

        $results = [];
        foreach ($plan['steps'] as $index => $step) {
            // 判断这步是否需要ReAct子循环
            if (in_array($step['tool'], ['query_ticket_status', 'get_ticket_detail'])) {
                // 用ReAct处理需要动态判断的步骤
                $subQuery = "执行工具 {$step['tool']},参数是 " . json_encode($step['params']);
                $results[$index] = [
                    'output' => $this->reactor->run($subQuery, 3), // 限制最多3步
                    'tool' => $step['tool'],
                ];
            } else {
                // 普通步骤直接执行
                $results[$index] = $this->planner->executeTool($step['tool'], $step['params']);
            }
        }

        return $this->planner->generateFinalAnswer($query, $results);
    }
}

混合方案的压测数据:

# 混合模式压测结果(1000次请求)
# 成功率: 97.8%
# 平均响应时间: 1.8s
# P95响应时间: 2.6s
# 重规划/重试率: 5.2%

数据背后的原理分析

为什么ReAct更稳但更慢?

ReAct每轮循环都把Observation作为上下文拼回prompt。这有两个维度的影响:

正确性维度:模型每步都能「抬头看路」,发现走错了能及时纠正。比如规划器可能第一步查了状态,第二步发现工单ID不对,ReAct模式下模型会重新提取工单号再查一次。这是ReAct成功率96.5%的原因。

性能维度:OpenAI API按token计费,每轮循环prompt长度都在增长。我统计过:3步的ReAct任务,prompt从初始的1200 tokens涨到第3轮的3100 tokens。这不光是钱的问题,输入token越多,首字延迟越高。3轮循环累计延迟 = 每一轮的输入token处理时间 + 输出token生成时间。这构成了3.2秒平均延迟的大头。

为什么Plan&Execute快但容易规划错?

规划器只看到用户问题,没看到工具执行后的真实返回值。它靠「经验」猜这个工单查询后可能会返回什么字段,然后基于这个猜的字段去设计后续步骤。工具返回的字段跟规划不一致,后续步骤就崩了。

我实际统计了100条Plan&Execute失败案例,分布在三类:

  • 规划器编造参数(如用户没提供工单号,它编一个):占42%
  • 规划的步骤之间依赖关系错了(第2步用了第3步的输出):占31%
  • 工具返回的数据结构跟规划器预期不一致:占27%

避坑指南

这里写我实际踩过的坑,按严重程度排序:

坑1:LLM输出解析必须用「提取」而不是「验证」

第一个版本我验证了json_decode失败就直接返回错误。后来发现LLM返回的内容经常是「好的,我来帮您。`{...}`」这种带前后缀的。改成用正则提取JSON部分再解析,解析失败率从18.3%降到1.2%。

正则提取JSON要小心:`preg_match('/\{.*\}/s', $response, $m)` 遇到嵌套JSON会匹配到第一个`}`就停。用`/\[.*\]/s`或`/\{.*\}/s`加贪婪模式,但如果响应里有一个以上JSON对象会匹配到拼接的脏数据。稳妥做法是先找最后一个`}`或`]`的位置,再往回找对应开头。

坑2:temperature必须设0,不然同样输入两次结果不一样

ReAct模式里,如果temperature不设0,同一个Observation可能让模型走两条不同分支,一次成功一次失败。压测时我做过对比:temperature=0.7时,同一个请求跑两次,一次3步解决,一次7步还没绕出来(触发了max_steps)。

坑3:工具调用的参数校验必须做在Agent里,不能信LLM

LLM生成的参数会超出你定义的schema。比如工单ID我定义的是`T-2024-015`这种格式,LLM可能输出`T2024015`或者`工单T-2024-015`。必须在executeTool入口做严格校验,不合法就返回一个明确的错误Observation给LLM,让它自己修正。不要尝试隐式转换。

坑4:别把所有工具都暴露给LLM

模型会试图用工具做它不该做的事。比如我暴露了一个`delete_ticket`工具,结果有用户问「能帮我删掉这个工单吗」,Agent真的去调用了。生产环境要做两件事:

  • 工具按用户角色做权限过滤,普通用户看不到删除类工具
  • 危险操作加二次确认,Agent需要调用`confirm_action`工具让用户确认后再执行

坑5:Step的依赖关系别靠LLM自己管理

Plan&Execute里我一开始让LLM自己写步骤之间的依赖,结果它经常写错。后来我人工定义了工具之间的依赖图谱,规划器只负责选择工具和填充参数,依赖关系由代码保证。比如`get_ticket_detail`必须在`query_ticket_status`之后执行,这个逻辑我不让LLM决定。

# 工具依赖配置,人工定义
tool_dependencies:
  query_ticket_status:
    depends_on: []
  get_ticket_detail:
    depends_on: [query_ticket_status]
  escalate_ticket:
    depends_on: [get_ticket_detail]
  calculate_sla:
    depends_on: [get_ticket_detail]

坑6:线上环境的API key管理用环境变量,别写死在代码里

这个看起来基础,但我确实见过同事把API key直接写在Agent类里提交到Git仓库。用`env('OPENAI_API_KEY')`或者部署平台密钥管理,我习惯在PHP里用getenv(),Laravel项目用config/services.php。

坑7:ReAct模式的max_steps设太小会误杀长任务

我一开始设max_steps=5,结果两步工具调用加一轮总结就要3个循环,偶尔一个工具调用失败了重试一次就到5了,直接返回「已达最大步数」。设到8之后,成功率从89%涨到96.5%。代价是极端情况延迟可能到8秒。按业务需求取舍。

生产建议

给要上Agent的人一个落地清单:

  • 先用ReAct跑通业务逻辑,如果发现80%以上的请求工具调用步数固定、不需要动态调整,再换Plan&Execute
  • 不要用单一模式部署所有Agent,按任务复杂度选:简单任务(一步工具调用)直接走LLM+工具映射,不要走Agent;中等任务Plan&Execute,复杂任务用混合架构
  • Prompt里的工具描述直接影响模型选择哪个工具。描述写得越具体,选对率越高。对比过:「查询工单状态」正确率72%,「根据工单ID精确查询工单当前状态,返回status字段,值为pending/resolved/escalated」正确率91%
  • 所有Agent调用建议用Redis做缓存,相同问题+相同工具结果直接返回。我用缓存后重复问询的响应时间从2.5s降到120ms
  • 给Agent加监控。每次LLM调用的token数、工具调用的成功率、每步耗时都记录下来,画成图表。没有监控你就不知道线上一半的请求卡在哪一步

代码仓库

完整代码我放在了公司内网GitLab的agent-patterns仓库里,包含:

  • src/ReActAgent.php — ReAct完整实现
  • src/PlanExecuteAgent.php — Plan&Execute完整实现
  • src/HybridAgent.php — 混合实现
  • src/ToolRegistry.php — 工具注册和依赖校验
  • tests/AgentTest.php — 单元测试(PHPUnit 10.5)
  • benchmark/ab_runner.sh — 压测脚本

线上跑了两个月,每天处理约800个工单查询请求。混合方案的P95延迟稳定在2.6秒左右,工具调用成功率和工单处理准确率都达标了。如果你也在做类似的东西,可以按这个思路选型,先跑通再优化。