> 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/chat-history-and-memory.md).

# 聊天历史

Neuron AI 为你提供了一个内置系统，用于管理你与智能体进行的聊天会话记忆。

在许多问答应用中，你可以与 LLM 进行来回对话，这意味着应用需要某种对过去问题和答案的“记忆”，以及将这些内容纳入当前思考的逻辑。

例如，如果你问一个跟进问题，比如“你能详细说明第二点吗？”，如果没有前文上下文，这是无法理解的。

在下面的示例中，你可以看到智能体一开始并不知道我的名字：

```php
use NeuronAI\Agent\Agent;
use NeuronAI\Chat\Messages\UserMessage;

$message = Agent::make()
    ->chat(new UserMessage("What's my name?"))
    ->getMessage();

echo $message->getContent();
// 抱歉，我不知道你的名字。你想告诉我更多关于你自己的信息吗？
```

显然，智能体对我没有任何上下文。现在我先在第一条消息中介绍自己，然后再问它我的名字：

```php
use NeuronAI\Agent\Agent;
use NeuronAI\Chat\Messages\UserMessage;

$agent = Agent::make()

$message = $agent->chat(new UserMessage("Hi, my name is Valerio!"))->getMessage();
echo $message->getContent();
// 嗨，Valerio，很高兴认识你，我今天能帮你什么？

$message = $agent->chat(new UserMessage("Do you remember my name?"))->getMessage();
echo $message->getContent();
// 当然，你的名字是 Valerio！
```

## 聊天历史如何工作

Neuron Agent 会将你与 LLM 之间交换的消息列表放入一个名为 Chat History 的对象中。它是该框架的关键部分，因为聊天历史需要根据底层 LLM 的上下文窗口来管理。

将过去的消息发送回 LLM 以保持对话上下文非常重要，但如果消息列表增长到足以超出模型的上下文窗口，请求将被 AI 提供方拒绝，因为它超出了 LLM 的最大能力。

聊天历史会自动截断消息列表，确保其不会超过上下文窗口，从而避免意外错误。你可以考虑实现更复杂的上下文管理策略，例如 [摘要](/neuron-v3-zh/agent/middleware.md#summarization).

在截断时，聊天历史会尽量减少上下文丢失。内部裁剪器可以识别出一个比最初识别的位置更不激进的截断点。因此，为了确保智能体对话保持在限制范围内， **你应该在智能体聊天历史中将上下文窗口配置为比底层模型实际限制小 5% 到 10% 的余量**.

例如，如果你的模型支持 20 万上下文窗口，你应将聊天历史实例化为 19 万。

```php
namespace App\Neuron;

use NeuronAI\Agent\Agent;
use NeuronAI\Providers\AIProviderInterface;
use NeuronAI\\Chat\\History\\ChatHistoryInterface;
use NeuronAI\\Chat\\History\\InMemoryChatHistory;

class MyAgent extends Agent
{
    protected function provider(): AIProviderInterface
    {
        ...
    }
    
    protected function chatHistory(): ChatHistoryInterface
    {
        return new InMemoryChatHistory(
            contextWindow: 190000
        );
    }
}
```

## 如何喂入一段先前的对话

有时你已经有了一段用户与助手对话的表示，并且需要一种方式将先前消息提供给智能体。

你只需将一个消息数组传给 `chat()` 方法。该对话会自动加载到智能体记忆中，你可以继续在其基础上迭代。

```php
use NeuronAI\\Chat\\Enums\\MessageRole;
use NeuronAI\Chat\Messages\Message;

$message = MyAgent::make()
    ->chat([
        new Message(MessageRole::USER, "Hi, my company is called Inspector.dev"),
        new Message(MessageRole::ASSISTANT, "Great, how can I assist you today?"),
        new Message(MessageRole::USER, "What's the name of the company I work for?"),
    ])
    ->getMessage();
    
echo $message->getContent();
// 你为 Inspector.dev 工作
```

列表中的最后一条消息将被视为最新消息。

## 注册聊天历史

默认情况下，Neuron Agent 使用“内存中”的聊天历史。这意味着它只会在当前执行周期中保留消息。不过，如果你希望跨会话持久化消息，可以通过实现 `chatHistory` 方法在 Agent 类中的该方法。

```php
namespace App\Neuron;

use NeuronAI\Agent\Agent;
use NeuronAI\Providers\AIProviderInterface;
use NeuronAI\\Chat\\History\\ChatHistoryInterface;
use NeuronAI\\Chat\\History\\InMemoryChatHistory;

class MyAgent extends Agent
{
    protected function provider(): AIProviderInterface
    {
        ...
    }
    
    protected function chatHistory(): ChatHistoryInterface
    {
        return new InMemoryChatHistory(
            contextWindow: 50000
        );
    }
}
```

## 可用的聊天历史实现

### InMemoryChatHistory

它只是将消息列表存储到一个数组中。它仅在当前执行期间保留在内存中。如果你没有显式注册其他组件，它会作为默认实现使用。

```php
namespace App\Neuron;

use NeuronAI\Agent\Agent;
use NeuronAI\\Chat\\History\\ChatHistoryInterface;
use NeuronAI\\Chat\\History\\InMemoryChatHistory;
use NeuronAI\Providers\AIProviderInterface;

class MyAgent extends Agent
{
    ...
    
    protected function chatHistory(): ChatHistoryInterface
    {
        return new InMemoryChatHistory(
            contextWindow: 150000
        );
    }
}
```

### FileChatHistory

这个组件使你能够将与智能体的正在进行的对话持久化到文件中，并在之后继续它。要创建一个 `FileChatHistory` 你需要传入 `目录` 中你希望存储对话的位置，以及当前对话的唯一 `键` 。

```php
namespace App\Neuron;

use NeuronAI\Agent\Agent;
use NeuronAI\\Chat\\History\\ChatHistoryInterface;
use NeuronAI\\Chat\\History\\FileChatHistory;
use NeuronAI\Providers\AIProviderInterface;

class MyAgent extends Agent
{
    ...
    
    protected function chatHistory(): ChatHistoryInterface
    {
        return new FileChatHistory(
            directory: '/home/app/storage/neuron',
            key: 'THREAD_ID',
            contextWindow: 150000
        );
    }
}
```

该 `键` 该参数允许你将不同的对话存储到不同的文件中。你可以为每个用户使用唯一键，或者使用线程 ID，让用户能够保存多段对话。

### SQLChatHistory

此组件允许你将正在进行的对话存储到 SQL 数据库中。在使用此组件之前，你必须先在数据库中创建用于存储消息的表。以下是 SQL 脚本：

```sql
CREATE TABLE IF NOT EXISTS chat_history (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  thread_id VARCHAR(255) NOT NULL,
  messages LONGTEXT NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
 
  UNIQUE KEY uk_thread_id (thread_id),
  INDEX idx_thread_id (thread_id)
);
```

你可以通过添加更多列来自定义这个表，最终还可以增加与用户的关联或类似用例。你也可以在创建实例时传入自定义表名来进行定制。

要创建一个 `SQLChatHistory` 你需要传入 `thread_id` 以区分不同的对话线程，以及 `PDO` 到数据库的连接。

```php
namespace App\Neuron;

use NeuronAI\Agent\Agent;
use NeuronAI\\Chat\\History\\ChatHistoryInterface;
use NeuronAI\\Chat\\History\\SQLChatHistory;
use NeuronAI\Providers\AIProviderInterface;

class MyAgent extends Agent
{
    ...
    
    protected function chatHistory(): ChatHistoryInterface
    {
        return new SQLChatHistory(
            thread_id: 'THREAD_ID',
            pdo: new \\PDO("mysql:host=localhost;dbname=DB_NAME;charset=utf8mb4", "DB_USER", "DB_PASS"),
            table: 'chat_history',
            contextWindow: 150000
        );
    }
}
```

如果你的应用构建在某个框架之上，你可以轻松地从 ORM 获取 PDO 连接。以下是 Laravel 或 Symfony 应用中的几个示例。

#### Laravel

```php
namespace App\Neuron;

use NeuronAI\Agent\Agent;
use NeuronAI\\Chat\\History\\ChatHistoryInterface;
use NeuronAI\\Chat\\History\\SQLChatHistory;
use NeuronAI\Providers\AIProviderInterface;

class MyAgent extends Agent
{
    ...
    
    protected function chatHistory(): ChatHistoryInterface
    {
        return new SQLChatHistory(
            thread_id: 'CHAT_THREAD_ID',
            pdo: \\DB::connection()->getPdo(),
            table: 'chat_history',
            contextWindow: 150000
        );
    }
}
```

#### Symfony

你可以将你的智能体注册为一个服务，并使用一个 `Doctrine\\DBAL\\Connection` 作为构造函数依赖项：

```php
namespace App\Neuron;

use Doctrine\\DBAL\\Connection;
use NeuronAI\Agent\Agent;
use NeuronAI\\Chat\\History\\ChatHistoryInterface;
use NeuronAI\\Chat\\History\\SQLChatHistory;
use NeuronAI\Providers\AIProviderInterface;

class MyAgent extends Agent
{
    public function __construct(protected Connection $connection)
    {
        parent::__construct();
    }
    
    protected function chatHistory(): ChatHistoryInterface
    {
        return new SQLChatHistory(
            thread_id: 'CHAT_THREAD_ID',
            pdo: $this->connection->getNativeConnection(),
            table: 'chat_history',
            contextWindow: 150000
        );
    }
}
```

### EloquentChatHistory

你应该创建自己的 Eloquent 模型，并将类名字符串作为构造参数传入。该模型可以具有自定义关联、作用域、属性等，但基本结构必须基于以下迁移脚本：

```bash
php artisan make:migration create_chat_messages_table --create=chat_messages
```

```php
Schema::create('chat_messages', function (Blueprint $table) {
     $table->id();
     $table->string('thread_id')->index();
     $table->string('role');
     $table->json('content');
     $table->json('meta')->nullable();
     $table->timestamps();

     $table->index(['thread_id', 'id']); // 用于高效排序和裁剪
});
```

#### ChatMessage 模型示例

```php
class ChatMessage extends Model
{
    protected $fillable = [
        'thread_id', 'role', 'content', 'meta'
    ];
    
    protected $casts = [
        'content' => 'array', 
        'meta' => 'array'
    ];
    
    /**
     * 返回 BelongsTo<Conversation, $this>
     */
    public function conversation(): BelongsTo
    {
        return $this->belongsTo(Conversation::class, 'thread_id');
    }
}
```

在你的智能体中使用：

```php
namespace App\Neuron;

use NeuronAI\Agent\Agent;
use NeuronAI\\Chat\\History\\ChatHistoryInterface;
use NeuronAI\\Chat\\History\\EloquentChatHistory;

class MyAgent extends Agent
{
    ...
    
    protected function chatHistory(): ChatHistoryInterface
    {
        return new EloquentChatHistory(
            thread_id: 'THREAD_ID',
            modelClass: ChatMessage::class,
            contextWindow: 150000
        );
    }
}
```

## 实现自定义聊天历史

你可以通过仅实现以下内容来创建聊天历史的自定义实现，以支持不同的持久化层： `AbstractChatHistory`。它允许你继承内部历史管理的多个行为，因此你只需实现几个方法，就能将消息保存到你想使用的存储系统中。

```php
abstract class AbstractChatHistory implements ChatHistoryInterface
{
    /**
     * @param Message[] $messages
     */
    protected function setMessages(array $messages): void
    {
        // 每次历史更新时处理一次性保存整个历史。
    }

    protected function onNewMessage(Message $message): void
    {
        // 处理单条消息的添加
    }

    protected function onTrimHistory(int $index): void
    {
        // 当触发截断时， 
        // 从零到 $index 位置的消息必须被移除。
    }

    protected function clear(): void
    {
        // 移除所有消息。
    }
}
```

该抽象类已经实现了一些实用方法，可根据 AI 提供方的响应计算 token 使用量，并根据上下文窗口大小自动截断对话。你只需专注于与底层存储的交互，以添加和删除消息，或清空整个历史。

我们强烈建议你查看其他实现，例如 `FileChatHistory` 以了解如何创建你自己的实现。

### 序列化/反序列化消息

当 ChatHistory 需要存储消息时，必须先对其进行序列化。同样，当 ChatHistory 组件被实例化时，它应该从底层存储（数据库、缓存等）中加载所有先前消息，并将它们反序列化为原始消息类型。

为了以一致的方式序列化/反序列化消息， `AbstractChatHistory` 为你提供了 `serializeMessage()` 和 `deserializeMessage()` 这些方法。以下是在一个假设的数据库聊天历史实现中如何使用它们的示例：

```php
<?php

namespace NeuronAI\\Chat\\History;

use NeuronAI\Chat\Messages\Message;

class DatabaseChatHistory extends AbstractChatHistory
{
    public function __construct(protected \\PDO $db) 
    {
        // 从底层存储中检索当前对话
        $messages = $this->db->select(...);
        
        // 正确反序列化，以使用正确的数据正确初始化消息类型。
        $this->history = $this->deserializeMessages($messages);
    }

    protected function onNewMessage(Message $message): void
    {
        // 存储序列化后的版本。
        $this->db->insert($message->jsonSerialize());
    }

    ...
}
```
