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

# 聊天历史

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

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

例如，如果你问一个后续问题，比如“你能详细说明第二点吗？”，如果没有之前消息的上下文，就无法理解。

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

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

$message = Agent::make()
    ->chat(new UserMessage("你叫什么名字？"))
    ->getMessage();

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

很明显，代理对我没有任何上下文。现在我先介绍自己，然后再问它我的名字：

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

$agent = Agent::make()

$message = $agent->chat(new UserMessage("嗨，我叫 Valerio！"))->getMessage();
echo $message->getContent();
// 嗨，Valerio，很高兴认识你，今天我能如何帮助你？

$message = $agent->chat(new UserMessage("你还记得我的名字吗？"))->getMessage();
echo $message->getContent();
// 当然，你的名字是 Valerio！
```

## 聊天历史的工作方式

Neuron Agent 会把你的应用与 LLM 之间交换的消息列表放入一个名为“聊天历史”的对象中。这是框架的关键部分，因为聊天历史需要根据底层 LLM 的上下文窗口进行管理。

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

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

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

例如，如果你的模型支持 200K 的上下文窗口，你应该将聊天历史实例化为 190K。

```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, "嗨，我的公司叫 Inspector.dev"),
        new Message(MessageRole::ASSISTANT, "太好了，今天我能如何帮助你？"),
        new Message(MessageRole::USER, "我工作的公司叫什么名字？"),
    ])
    ->getMessage();
    
echo $message->getContent();
// 你为 Inspector.dev 工作
```

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

## 注册聊天历史

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

```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'
    ];
    
    /**
     * return 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());
    }

    ...
}
```
