> For the complete documentation index, see [llms.txt](https://docs.neuron-ai.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.neuron-ai.dev/neuron-v3-zh/agent/middleware.md).

# 中间件

中间件是基础 Workflow 组件的一个特性。因此，你也可以将自定义中间件附加到 Agent 和 RAG 上，以便介入它们的执行周期。

### Agent 工作流

Agent 类是 Workflow 组件的扩展。Workflow 是 Neuron 中拼图的基础部分。Agent 和 RAG 组件的许多特性都继承自底层 Workflow 的能力。

下面是用于创建 Agent 实现的工作流的一个简单示意图：

<figure><img src="/files/0f88d870f56e4b96c46f96843ffded5ce89ed2f7" alt=""><figcaption></figcaption></figure>

基于这种架构，你可以自由使用中间件来钩入 agent 工作流、使用中断来保持人工介入，或者查看下面我们为常见用例提供的一组内置组件。

### 工具审批（人工介入）

{% hint style="info" %}
在使用 ToolApproval 之前，你应该先熟悉工作流 [持久化](/neuron-v3-zh/gong-zuo-liu/persistence.md) 和 [中断](/neuron-v3-zh/gong-zuo-liu/human-in-the-loop.md).
{% endhint %}

在 Neuron 中，Agent 实体建立在 Workflow 组件之上。这意味着它可以在执行关键操作之前被中断以请求确认。 `ToolApproval` 中间件会在工具调用执行前暂停 agent 执行，以便人工批准或拒绝这些调用。

```php
use NeuronAI\Agent\Agent;
use NeuronAI\Agent\Middleware\ToolApproval;
use NeuronAI\Workflow\Middleware\WorkflowMiddleware;
use NeuronAI\Workflow\NodeInterface;

class MyAgent extends Agent
{
    protected function provider(): AIProviderInterface
    {...}
    
    /**
     * 注册工具
     */
    protected function tools(): array
    {
        return [
            BuyTicketTool::make(),
        ];
    }

    /**
     * 将中间件附加到节点。
     */
    protected function middleware(): array
    {
        return [
            ToolNode::class => [
                new ToolApproval(
                    // 提供需要审批的工具类或名称列表
                    tools: [BuyTicketTool::class]
                )
            ],
        ];
    }
}
```

一旦 agent 试图在 `ToolApproval` 中间件中调用列表里的某个工具，它就会抛出工作流中断异常。你必须捕获这个异常，并向用户展示界面以收集他们的反馈。中断异常将包含一个 `ApprovalRequest` 以及需要用户反馈的操作实例。

```php
use NeuronAI\Workflow\Interrupt\WorkflowInterrupt;
use NeuronAI\Workflow\Persistence\FilePersistence;

$persistence = new FilePersistence(__DIR__);

try {

    $response = new MyAgent($presistence)
        ->chat(new UserMessage("意大利的天气怎么样？"))
        ->getMessage();
        
} catch (WorkflowInterrupt $interrupt) {
    $approvalRequest = json_encode($interrupt->getRequest());
    $resumeToken = $interrupt->getResumeToken();
    
    // 存储请求和 resumeToken，以便收集用户反馈，并在之后重新启动 agent 工作流
}
```

resume token 会自动生成，并可在中断异常中获取。

你应该将 `审批请求` 以及 `resume token` 一起存储，以便在之后从中断处准确恢复 agent 工作流。你可以使用数据库或任何其他适合你的应用的持久化层。审批请求是可 JSON 序列化的，因此你可以轻松地将其结构存入存储中。

一旦用户批准/拒绝了这些操作，你就可以使用编辑后的请求来恢复 agent。

```php
$persistence = new FilePersistence(__DIR__);

// 在用户交互后检索请求和 token，以重新启动工作流
$approvalRequest = ApprovalRequest::fromArray(...);
$resumeToken = ...

$response = new MyAgent($persistence, $resumeToken)
        ->chat(interrupt: $approvalRequest)
        ->getMessage();
```

要更好地理解如何管理中断流程，你可以查看这个示例：

{% embed url="<https://github.com/neuron-core/neuron-ai/blob/main/examples/agent/tool-approval.php>" %}

或者参考完整的 [工作流文档](/neuron-v3-zh/gong-zuo-liu/human-in-the-loop.md).

### 条件审批

上面的示例是一个经典的开/关审批流程。如果某个工具被列在 `ToolApproval` 中间件中，agent 就会中断执行；否则，该工具将照常执行。

中间件也支持为工具关联一个回调，以定义你自定义的审批条件。该回调接收工具实例并返回 `true` 如果该工具需要审批，或者返回 `false` 以跳过中断并按原样运行该工具。

```php
class MyAgent extends Agent
{
    ...
    
    /**
     * 注册工具
     */
    protected function tools(): array
    {
        return [
            BuyTicketTool::make(),
        ];
    }

    /**
     * 将中间件附加到节点。
     */
    protected function middleware(): array
    {
        return [
            ToolNode::class => [
                new ToolApproval(
                    tools: [
                        // 如果金额大于 100，则请求审批
                        BuyTicketTool::class => function (array $args): bool {
                            return $args['amount'] > 100;
                        }
                    ]
                )
            ],
        ];
    }
}
```

在上面的示例中，我们只在票价高于 100 时才需要人工审批，否则回调返回 false，这意味着不需要中断。

### 上下文摘要

这个中间件旨在包装 agent 实际调用 LLM 的节点，在接近 token 限制时自动汇总对话历史。根据你要执行的调用类型，在 Neuron 中有三个可能负责这项任务的节点： `ChatNode`, `StreamingNode`，以及 `StructuredNode`。你应该将中间件附加到所有这些节点上，以确保无论 agent 以哪种模式运行都能正常工作。

```php
use NeuronAI\Agent\Agent;
use NeuronAI\Agent\Middleware\Summarization;
use NeuronAI\Agent\Nodes\ChatNode;
use NeuronAI\Agent\Nodes\StreamingNode;
use NeuronAI\Agent\Nodes\StructuredOutputNode;

class MyAgent extends Agent
{
    ...

    /**
     * 将中间件附加到节点。
     */
    protected function middleware(): array
    {
        $summarization = new Summarization(
            provider: $this->resolveProvider(), // 或使用专用的 provider 实例
            maxTokens: 10000,
            messagesToKeep: 5,
        );
        
        return [
            ChatNode::class => [$summarization],
            StreamingNode::class => [$summarization],
            StructuredOutputNode::class => [$summarization]
        ];
    }
}
```

`maxTokens` 和 `messagesToKeep` 共同决定了必须执行摘要的阈值。在上面的示例中，如果上下文达到 3 万个 token，那么聊天历史中至少要有 10 条消息，摘要才会开始。随着聊天历史中不断添加新消息，最终会同时跨过这两个阈值，从而触发摘要。

### 工具搜索

默认情况下，每次调用 provider 时，所有工具都会被加载并传递给后端 LLM。一个复杂的生产级 agent，如果连接了电子邮件、日历、云盘、CRM 以及多个 MCP 服务器，很容易达到数百个工具，每个工具都带有名称、描述、参数模式和使用提示。

这会在每一轮消耗数千个 token，但更痛苦的问题在于质量：当模型一次看到太多工具时，描述会互相混淆，名称相近的工具会争夺注意力，agent 也开始做出细微但错误的选择，把参数混淆，或者胡乱生成参数，因为它试图在同一时间把太多签名保留在工作记忆中。

工具搜索将工具目录重新定义为 agent 按需查询的对象，而不是它在每次请求中都携带的东西。

你可以使用全局中间件 `ToolSearchMiddleware` 来在你的 agent 上启用动态工具选择：

```php
class MyAgent extends Agennt
{
    ...
    
    /**
     * 定义全局中间件。
     */
    protected function globalMiddleware(): array
    {
        return [
            new ToolSearchMiddleware([
                MyCustomTool::make(),
                ...CalculatorToolkit::make()->tools()
                ...MCPConnector::make([...])->tools()
            ]),
        ];
    }
    
    /**
     * 向 agent 提供核心工具。
     */
    protected function tools(): array
    {
        return [
            // 模型始终可用的一组核心工具
            TavilySearchTool::make(...),
        ];
    }
}
```

{% embed url="<https://www.youtube.com/watch?v=qYmidHAXEYM>" %}

中间件会自动将 `ToolSearch` 工具注入到每次请求时模型可用的默认工具列表中，并将你提供的工具列表保存在内部数组中。

agent 以一个最小的工具集开始一轮对话，通常只有 `ToolSearch` 它自身以及你始终希望可用的任何核心工具，而当它需要当前没有的能力时，它会调用 `ToolSearch` 并使用自然语言查询，返回一个按优先级排序的工具描述符列表及其完整模式。此时，位于 agent 和下一次推理调用之间的中间件会检查搜索结果，提取工具标识符，在底层注册表中查找它们，并将其完整定义添加到下一次发送给模型的工具数组中。

从模型的角度看，下一轮只是带着更丰富的工具列表到来，它可以直接使用正确的模式校验调用其中任何一个新暴露的工具，就像这些工具从一开始就在那里一样。
