4.前端使用Node + MongoDB + Langchain消息管理与聊天历史存储

最近在为一个前端项目搭建 LangChain 聊天后端时,踩了不少坑:如何优雅地与 MongoDB 建立连接、怎么让聊天上下文持久化、怎样配合向量检索做知识增强......本文结合实际代码,完整梳理从零到一的实现流程,帮助你快速搭建可复用的消息管理与聊天历史存储方案。

项目结构与准备工作

仓库核心目录如下,本文重点聚焦 mongodb 中的基础设施:

依赖与环境变量

js 复制代码
pnpm install
pnpm add mongodb @langchain/core @langchain/mongodb @langchain/ollama

.env 中配置以下变量(示例):

yaml 复制代码
MONGODB_URL=mongodb://localhost:27017
MONGODB_DB_NAME=langchain_demo
MONGODB_DB_CHAT_COLLECTION_NAME=chat_message_history
MONGODB_DB_VECTOR_COLLECTION_NAME=vector_collection
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_EMBED_MODEL=nomic-embed-text:latest

1. MongoDB 单例工具:稳定的底座

LangChain 只是调用入口,稳定的数据库层才是关键。MongoDBUtil 定义在 index.ts:1-140,采用单例模式管理连接与集合。

js 复制代码
const mongoUtil = MongoDBUtil.getInstance();
await mongoUtil.connect(); // 默认读取 .env
const users = await mongoUtil.findMany('users', { role: 'admin' });

特性亮点:

2. 聊天历史管理:LangChain + MongoDB

chatHistory.ts 基于 MongoDBChatMessageHistory 封装了 MongoChatHistoryTool

初始化与索引自愈

工具会在首次使用时检查集合是否存在;如果 listIndexes 返回 NamespaceNotFound(代码 26),会自动跳过索引检查,防止冷启动报错。

js 复制代码
const historyTool = new MongoChatHistoryTool({
  collectionName: 'demo_chat_history',
  sessionTTLSeconds: 60 * 60, // 可选:自动过期
});
  • sessionTTLSeconds 会在 updatedAt 上创建 TTL 索引,实现会话自动清理
  • touchSession 负责维护 createdAtupdatedAt 与消息数量,方便后台展示

完整的消息 API

js 复制代码
await historyTool.appendSystemMessage(sessionId, '你是一个知识库助手');
await historyTool.appendHumanMessage(sessionId, '帮我解释 LangChain');
await historyTool.appendAIMessage(sessionId, 'LangChain 帮你连接 LLM 与外部数据源。');

const messages = await historyTool.getMessages(sessionId, { limit: 20 });
const transcript = await historyTool.getMessagesAsBuffer(sessionId, 'User', 'Assistant');

const sessions = await historyTool.listSessions({ limit: 10 });
await historyTool.deleteSession(sessionId);

常用场景总结:

  • appendMessages 支持批量写入,可用于迁移历史记录
  • getMessages 默认返回时间顺序,可通过 { reverse: true } 做反向分页
  • listSessions 自带搜索/分页/排序,易于接入前端会话列表

3. 向量检索:把聊天变成RAG

想让助手回答更贴合业务,需要把内部文档转成向量。MongodbVectorTool 位于 vector.ts,配合 LangChain 中的 MongoDBAtlasVectorSearch 实现 RAG。

初始化与索引

js 复制代码
const vectorTool = new MongodbVectorTool({ collectionName: 'docs_vector' });
await vectorTool.initialize();
await MongodbVectorTool.initSearchIndex(vectorTool.getCollection());

initSearchIndex 会检查 vector_index 是否存在,自动创建 768 维余弦相似度索引。生产环境记得在 Atlas 中打开 Vector Search。

文档写入与检索

js 复制代码
await vectorTool.addTexts(
  ['LangChain 让你把 LLM 接入数据库', 'MongoDB Atlas 原生支持向量检索'],
  [{ source: 'blog' }, { source: 'docs' }]
);

const result = await vectorTool.similaritySearch('怎么把 LLM 连到数据库', 2);

如果你更喜欢自己准备 Document,可以使用静态 toDocuments 帮你打好 pageContentmetadata

4. 构建一个可运行的 Demo

将以上工具结合起来,你可以在 index.ts 中快速跑通:

js 复制代码
import MongoChatHistoryTool from './utils/mongodb/chatHistory.js';
import MongodbVectorTool from './utils/mongodb/vector.js';
import MongoDBUtil from './utils/mongodb/index.js';

async function bootstrap() {
  const mongo = MongoDBUtil.getInstance();
  await mongo.connect();

  const sessionId = 'session-123';
  const historyTool = new MongoChatHistoryTool();

  await historyTool.appendSystemMessage(sessionId, '你是一位乐于助人的助手。');
  await historyTool.appendHumanMessage(sessionId, '嗨,你能解释一下 LangChain 吗?');
  await historyTool.appendAIMessage(sessionId, 'LangChain 能将语言模型与外部数据源相连接。');

  console.log(await historyTool.getMessages(sessionId));
  console.log(await historyTool.getMessagesAsBuffer(sessionId, 'User', '助手'));

  const vectorTool = new MongodbVectorTool({});
  await vectorTool.initialize();
  await MongodbVectorTool.initSearchIndex(vectorTool.getCollection());

  await vectorTool.addTexts(['LangChain 是一个用于构建具备情境感知功能的应用程序的框架。'], [{ tag: 'intro' }]);
  console.log(await vectorTool.similaritySearch('什么是 LangChain?', 1));

  await historyTool.deleteSession(sessionId);
  await mongo.disconnect();
}

bootstrap().catch(console.error);

5.完整代码

index.ts

js 复制代码
import { MongoClient, Db, Collection, ObjectId, UUID } from 'mongodb';
import type { DeleteOptions, DeleteResult, Document, Filter, OptionalUnlessRequiredId, WithId } from 'mongodb';
import 'dotenv/config';
import { Document as LangChainDocument } from '@langchain/core/documents';

/**
 * MongoDB 工具类
 * 提供基本的数据库连接和操作功能
 */
class MongoDBUtil {
  private static instance: MongoDBUtil;
  private client: MongoClient | null = null;
  private db: Db | null = null;

  private constructor() {}

  /**
   * 获取单例实例
   * @returns MongoDBUtil 实例
   */
  public static getInstance(): MongoDBUtil {
    if (!MongoDBUtil.instance) {
      MongoDBUtil.instance = new MongoDBUtil();
    }
    return MongoDBUtil.instance;
  }

  /**
   * 连接到 MongoDB 数据库
   * @param uri MongoDB 连接字符串
   * @param dbName 数据库名称
   * @returns Promise<void>
   */
  public async connect(uri: string = process.env.MONGODB_URL || '', dbName: string = process.env.MONGODB_DB_NAME || ''): Promise<void> {
    try {
      this.client = new MongoClient(uri);
      await this.client.connect();
      this.db = this.client.db(dbName);
      console.log('Connected to MongoDB');
    } catch (error) {
      console.error('Failed to connect to MongoDB:', error);
      throw error;
    }
  }

  /**
   * 断开数据库连接
   * @returns Promise<void>
   */
  public async disconnect(): Promise<void> {
    try {
      if (this.client) {
        await this.client.close();
        this.client = null;
        this.db = null;
        console.log('Disconnected from MongoDB');
      }
    } catch (error) {
      console.error('Failed to disconnect from MongoDB:', error);
      throw error;
    }
  }

  /**
   * 获取集合引用
   * @param collectionName 集合名称
   * @returns Collection 对象
   */
  public getCollection<T extends Document>(collectionName: string): Collection<T> {
    if (!this.db) {
      throw new Error('Database not connected. Please call connect() first.');
    }
    return this.db.collection<T>(collectionName);
  }

  /**
   * @description: 将任意对象数组转换为 Document 数组
   * @template T
   * @param {T[]} items
   * @param {(item: T, index: number) => Record<string, unknown>} metadataSelector
   * @return {Document[]}
   */
  static toDocuments<T extends Record<string, unknown>>(
    items: T[],
    metadataSelector?: (item: T, index: number) => Record<string, unknown>
  ): Document[] {
    return items.map(
      (item, index) =>
        new LangChainDocument({
          pageContent: JSON.stringify(item),
          metadata: metadataSelector?.(item, index) ?? {},
          id: new ObjectId().toString(),
        })
    );
  }

  /**
   * 插入单个文档
   * @param collectionName 集合名称
   * @param document 要插入的文档
   * @returns Promise<any>
   */
  public async insertOne<T extends Document>(collectionName: string, document: OptionalUnlessRequiredId<T>): Promise<any> {
    try {
      const collection = this.getCollection<T>(collectionName);
      const result = await collection.insertOne(document);
      return result;
    } catch (error) {
      console.error(`Failed to insert document into ${collectionName}:`, error);
      throw error;
    }
  }

  /**
   * 插入多个文档
   * @param collectionName 集合名称
   * @param documents 要插入的文档数组
   * @returns Promise<any>
   */
  public async insertMany<T extends Document>(collectionName: string, documents: OptionalUnlessRequiredId<T>[]): Promise<any> {
    try {
      const collection = this.getCollection<T>(collectionName);
      const result = await collection.insertMany(documents);
      return result;
    } catch (error) {
      console.error(`Failed to insert documents into ${collectionName}:`, error);
      throw error;
    }
  }

  /**
   * 查找单个文档
   * @param collectionName 集合名称
   * @param query 查询条件
   * @returns Promise<T | null>
   */
  public async findOne<T extends Document>(collectionName: string, query: any = {}): Promise<WithId<T> | null> {
    try {
      const collection = this.getCollection<T>(collectionName);
      const result = await collection.findOne(query);
      return result;
    } catch (error) {
      console.error(`Failed to find document in ${collectionName}:`, error);
      throw error;
    }
  }

  /**
   * 查找多个文档
   * @param collectionName 集合名称
   * @param query 查询条件
   * @param options 查询选项(如限制数量、排序等)
   * @returns Promise<T[]>
   */
  public async findMany<T extends Document>(collectionName: string, query: any = {}, options?: any): Promise<WithId<T>[]> {
    try {
      const collection = this.getCollection<T>(collectionName);
      const cursor = collection.find(query, options);
      const results = await cursor.toArray();
      return results;
    } catch (error) {
      console.error(`Failed to find documents in ${collectionName}:`, error);
      throw error;
    }
  }

  /**
   * 更新单个文档
   * @param collectionName 集合名称
   * @param query 查询条件
   * @param update 更新内容
   * @returns Promise<any>
   */
  public async updateOne<T extends Document>(collectionName: string, query: any, update: any): Promise<any> {
    try {
      const collection = this.getCollection<T>(collectionName);
      const result = await collection.updateOne(query, update);
      return result;
    } catch (error) {
      console.error(`Failed to update document in ${collectionName}:`, error);
      throw error;
    }
  }

  /**
   * 更新多个文档
   * @param collectionName 集合名称
   * @param query 查询条件
   * @param update 更新内容
   * @returns Promise<any>
   */
  public async updateMany<T extends Document>(collectionName: string, query: any, update: any): Promise<any> {
    try {
      const collection = this.getCollection<T>(collectionName);
      const result = await collection.updateMany(query, update);
      return result;
    } catch (error) {
      console.error(`Failed to update documents in ${collectionName}:`, error);
      throw error;
    }
  }

  /**
   * 删除单个文档
   * @param collectionName 集合名称
   * @param query 查询条件
   * @returns Promise<any>
   */
  public async deleteOne<T extends Document>(collectionName: string, query?: Filter<T>): Promise<DeleteResult> {
    try {
      const collection = this.getCollection<T>(collectionName);
      const result = await collection.deleteOne(query);
      return result;
    } catch (error) {
      console.error(`Failed to delete document from ${collectionName}:`, error);
      throw error;
    }
  }

  /**
   * 删除多个文档
   * @param collectionName 集合名称
   * @param query 查询条件
   * @returns Promise<any>
   */
  public async deleteMany<T extends Document>(collectionName: string, query?: Filter<T>): Promise<DeleteResult> {
    try {
      const collection = this.getCollection<T>(collectionName);
      const result = await collection.deleteMany(query);
      return result;
    } catch (error) {
      console.error(`Failed to delete documents from ${collectionName}:`, error);
      throw error;
    }
  }

  /**
   * 检查数据库连接状态
   * @returns boolean
   */
  public isConnected(): boolean {
    return !!this.client && !!this.db;
  }
}

export default MongoDBUtil;
export { MongoChatHistoryTool } from './chatHistory.js';

vector.ts

js 复制代码
import { OllamaEmbeddings } from '@langchain/ollama';
import MongoDBUtil from './index.js';
import 'dotenv/config';
import type { Collection, Filter } from 'mongodb';
import { MongoDBAtlasVectorSearch } from '@langchain/mongodb';
import { Document, type DocumentInterface } from '@langchain/core/documents';

export interface MongodbToolConfig {
  /** 指定集合名称,不同 collection 互相隔离 */
  collectionName?: string;
  /** 自定义 Embeddings 实例,默认走 Ollama */
  embeddings?: OllamaEmbeddings;
  /** 默认检索条数 */
  defaultK?: number;
  /** Chroma 服务地址,优先使用 URL */
  url?: string;
  /** 本地持久化目录(与 url 二选一) */
  persistDirectory?: string;
}
export class MongodbVectorTool {
  private readonly config: MongodbToolConfig;
  private readonly defaultK: number;
  /** 实际使用的 Embeddings 实例 */
  private readonly embeddings: OllamaEmbeddings;
  /** mongodb 实例 */
  private readonly mongoClient: MongoDBUtil;
  /** 集合 */
  private collection: Collection | null = null;
  /** 向量存储 */
  private vectorStore: MongoDBAtlasVectorSearch | null = null;

  constructor(config: MongodbToolConfig) {
    this.config = config;
    this.defaultK = config.defaultK ?? 4;
    this.embeddings =
      config.embeddings ??
      new OllamaEmbeddings({
        model: process.env.OLLAMA_EMBED_MODEL ?? 'nomic-embed-text:latest',
        baseUrl: process.env.OLLAMA_BASE_URL ?? 'http://localhost:11434',
      });
    this.mongoClient = MongoDBUtil.getInstance();
  }

  /**
   * 初始化向量工具
   * @returns Promise<void>
   */
  async initialize(): Promise<void> {
    // 确保数据库连接
    if (!this.mongoClient.isConnected()) {
      await this.mongoClient.connect();
    }

    // 获取集合引用
    this.collection = this.mongoClient.getCollection(
      this.config.collectionName || process.env.MONGODB_DB_VECTOR_COLLECTION_NAME || 'vector_collection'
    );
    // 初始化向量存储
    this.vectorStore = new MongoDBAtlasVectorSearch(this.embeddings, {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      collection: this.collection as any,
      indexName: 'vector_index',
      textKey: 'text',
      embeddingKey: 'embedding',
    });
  }
  getCollection() {
    return this.collection;
  }
  static async initSearchIndex(
    collection: Collection | null,
    definition = {
      fields: [
        {
          type: 'vector',
          numDimensions: 768,
          path: 'embedding',
          similarity: 'cosine',
        },
      ],
    }
  ) {
    if (collection) {
      const indexes = await collection.listSearchIndexes('vector_index').toArray();
      if (indexes.length === 0) {
        const index = {
          name: 'vector_index',
          type: 'vectorSearch',
          definition: definition,
        };
        collection.createSearchIndex(index);
      }
    }
  }
  /**
   * 确保向量工具已初始化
   * @returns Promise<void>
   */
  private async ensureInitialized(): Promise<void> {
    if (!this.vectorStore || !this.collection) {
      await this.initialize();
    }
  }

  /**
   * @description: 将任意对象数组转换为 Document 数组
   * @template T
   * @param {T[]} items
   * @param {(item: T, index: number) => Record<string, unknown>} metadataSelector
   * @return {Document[]}
   */
  static toDocuments<T extends Record<string, unknown>>(
    items: T[],
    metadataSelector?: (item: T, index: number) => Record<string, unknown>
  ): Document[] {
    return items.map(
      (item, index) =>
        new Document({
          pageContent: JSON.stringify(item),
          metadata: metadataSelector?.(item, index) ?? {},
        })
    );
  }
  /**
   * @description: 添加文档
   * @param {Document[]} documents
   * @return {Promise<void>}
   */
  async addDocuments(documents: Document[]): Promise<void> {
    if (!documents.length) return;
    await this.ensureInitialized();
    if (!this.vectorStore) {
      throw new Error('Vector store not initialized');
    }
    await this.vectorStore.addDocuments(documents);
  }

  /**
   * @description: 添加文本
   * @param {string[]} texts
   * @param {Record<string, unknown>[]} metadatas
   * @return {Promise<void>}
   */
  async addTexts(texts: string[], metadatas?: Record<string, unknown>[]): Promise<void> {
    const documents = texts.map(
      (text, index) =>
        new Document({
          pageContent: text,
          metadata: metadatas?.[index] ?? {},
        })
    );
    await this.addDocuments(documents);
  }

  /**
   * @description: 相似度检索
   * @param {string} query
   * @param {number} k
   * @return {Promise<DocumentInterface[]>}
   */
  async similaritySearch(query: string, k: number = this.defaultK): Promise<DocumentInterface[]> {
    await this.ensureInitialized();
    if (!this.vectorStore) {
      throw new Error('Vector store not initialized');
    }
    return this.vectorStore.similaritySearch(query, k);
  }

  /**
   * @description: 相似度检索并返回得分
   * @param {string} query
   * @param {number} k
   * @return {Promise<[DocumentInterface, number][]>}
   */
  async similaritySearchWithScore(query: string, k: number = this.defaultK): Promise<[DocumentInterface, number][]> {
    await this.ensureInitialized();
    if (!this.vectorStore) {
      throw new Error('Vector store not initialized');
    }
    console.log(this.vectorStore);
    return this.vectorStore.similaritySearchWithScore(query, k);
  }

  /**
   * @description: 删除集合
   * @return {Promise<void>}
   */
  async deleteCollection(query: Filter<Document>): Promise<void> {
    await this.ensureInitialized();
    if (!this.mongoClient) {
      throw new Error('MongoDBUtil not initialized');
    }
    try {
      await this.mongoClient.deleteOne(this.config.collectionName || process.env.MONGODB_DB_VECTOR_COLLECTION_NAME || 'vector_collection', query);
    } catch (error) {
      console.error('Error deleting collection:', error);
      throw error;
    }
  }
}

export default MongodbVectorTool;

chatHistory.ts

js 复制代码
import { MongoDBChatMessageHistory } from '@langchain/mongodb';
import MongoDBUtil from './index.js';
import { MongoServerError, type Collection, type Document } from 'mongodb';
import {
  AIMessage,
  BaseMessage,
  type BaseMessageLike,
  HumanMessage,
  type MessageContent,
  SystemMessage,
  coerceMessageLikeToMessage,
  getBufferString,
} from '@langchain/core/messages';

// 定义排序顺序类型,用于指定升序或降序
type SortOrder = 'asc' | 'desc';

// MongoDB聊天历史选项接口
export interface MongoChatHistoryOptions {
  // 可选的集合名称,默认为'chat_message_history'
  collectionName?: string;
  // 会话TTL(生存时间)秒数,用于设置会话过期时间
  sessionTTLSeconds?: number;
}

// 列出会话的选项接口
export interface ListSessionsOptions {
  // 限制返回的会话数量
  limit?: number;
  // 跳过的会话数量(用于分页)
  skip?: number;
  // 排序顺序(升序或降序)
  order?: SortOrder;
  // 特定会话ID列表,如果指定则只返回这些会话
  sessionIds?: string[];
}

// 消息查询选项接口
export interface MessageQueryOptions {
  // 限制返回的消息数量
  limit?: number;
  // 是否反转消息顺序
  reverse?: boolean;
}

// 聊天会话摘要接口
export interface ChatSessionSummary {
  // 会话ID
  sessionId: string;
  // 消息数量
  messageCount: number;
  // 会话创建时间
  createdAt?: Date;
  // 会话更新时间
  updatedAt?: Date;
}

// 默认集合名称常量
const DEFAULT_COLLECTION_NAME = 'chat_message_history';

/**
 * MongoDB聊天历史工具类
 * 提供了对聊天历史的增删查改功能,支持会话管理和消息操作
 */
class MongoChatHistoryTool {
  // 工具配置选项
  private readonly options: MongoChatHistoryOptions;
  // MongoDB工具实例
  private readonly mongo: MongoDBUtil;
  // MongoDB集合实例
  private collection: Collection<Document> | null = null;
  // 缓存的历史记录实例映射,用于提高性能
  private readonly histories = new Map<string, MongoDBChatMessageHistory>();
  // 索引是否已准备好的标志
  private indexesReady = false;

  /**
   * 构造函数
   * @param options 配置选项
   */
  constructor(options: MongoChatHistoryOptions = {}) {
    this.options = options;
    this.mongo = MongoDBUtil.getInstance();
  }

  /**
   * 获取集合名称
   * 优先级:选项中指定 > 环境变量 > 默认值
   */
  private get collectionName(): string {
    return this.options.collectionName ?? process.env.MONGODB_DB_CHAT_COLLECTION_NAME ?? DEFAULT_COLLECTION_NAME;
  }

  /**
   * 获取TTL(生存时间)设置
   */
  private get ttl(): number | undefined {
    return this.options.sessionTTLSeconds;
  }

  /**
   * 确保初始化完成
   * 连接数据库并设置集合和索引
   */
  private async ensureInitialized(): Promise<void> {
    if (!this.mongo.isConnected()) {
      await this.mongo.connect();
    }
    if (!this.collection) {
      this.collection = this.mongo.getCollection<Document>(this.collectionName);
    }
    if (!this.indexesReady && this.collection) {
      await this.ensureIndexes();
      this.indexesReady = true;
    }
  }

  /**
   * 确保必要的索引已创建
   * 创建sessionId索引和TTL索引(如果设置了TTL)
   */
  private async ensureIndexes(): Promise<void> {
    if (!this.collection) {
      return;
    }
    let indexInfos: Document[] = [];
    try {
      indexInfos = await this.collection.listIndexes().toArray();
    } catch (error) {
      if (error instanceof MongoServerError && error.code === 26) {
        // NamespaceNotFound表示集合尚不存在,继续创建索引
        indexInfos = [];
      } else {
        throw error;
      }
    }
    const hasSessionIndex = indexInfos.some((idx) => idx.name === 'sessionId_1');
    if (!hasSessionIndex) {
      await this.collection.createIndex({ sessionId: 1 }, { unique: true });
    }
    if (this.ttl != null) {
      const ttlIndexName = 'updatedAt_ttl';
      const hasTTL = indexInfos.some((idx) => idx.name === ttlIndexName);
      if (!hasTTL) {
        await this.collection.createIndex({ updatedAt: 1 }, { name: ttlIndexName, expireAfterSeconds: this.ttl });
      }
    }
  }

  /**
   * 获取指定会话的历史记录实例
   * 如果不存在则创建新的实例并缓存
   * @param sessionId 会话ID
   */
  private async getHistory(sessionId: string): Promise<MongoDBChatMessageHistory> {
    if (!sessionId) {
      throw new Error('Session id required');
    }
    await this.ensureInitialized();
    if (!this.collection) {
      throw new Error('MongoDB collection unavailable');
    }
    let history = this.histories.get(sessionId);
    if (!history) {
      history = new MongoDBChatMessageHistory({ collection: this.collection as any, sessionId });
      this.histories.set(sessionId, history);
    }
    return history;
  }

  /**
   * 更新会话的时间戳和消息计数
   * @param sessionId 会话ID
   */
  private async touchSession(sessionId: string): Promise<void> {
    if (!this.collection) {
      return;
    }
    const now = new Date();
    await this.collection.updateOne({ sessionId }, [
      {
        $set: {
          updatedAt: now,
          createdAt: { $ifNull: ['$createdAt', now] },
          messageCount: { $size: '$messages' },
        },
      },
    ]);
  }

  /**
   * 向指定会话追加消息
   * @param sessionId 会话ID
   * @param message 要添加的消息
   */
  async appendMessage(sessionId: string, message: BaseMessageLike): Promise<void> {
    const history = await this.getHistory(sessionId);
    const normalized = coerceMessageLikeToMessage(message);
    await history.addMessage(normalized);
    await this.touchSession(sessionId);
  }

  /**
   * 向指定会话追加人类消息
   * @param sessionId 会话ID
   * @param content 消息内容
   */
  async appendHumanMessage(sessionId: string, content: string | MessageContent): Promise<void> {
    await this.appendMessage(sessionId, new HumanMessage(content));
  }

  /**
   * 向指定会话追加AI消息
   * @param sessionId 会话ID
   * @param content 消息内容
   */
  async appendAIMessage(sessionId: string, content: string | MessageContent): Promise<void> {
    await this.appendMessage(sessionId, new AIMessage(content));
  }

  /**
   * 向指定会话追加系统消息
   * @param sessionId 会话ID
   * @param content 消息内容
   */
  async appendSystemMessage(sessionId: string, content: string | MessageContent): Promise<void> {
    await this.appendMessage(sessionId, new SystemMessage(content));
  }

  /**
   * 向指定会话批量追加消息
   * @param sessionId 会话ID
   * @param messages 消息数组
   */
  async appendMessages(sessionId: string, messages: BaseMessageLike[]): Promise<void> {
    for (const message of messages) {
      await this.appendMessage(sessionId, message);
    }
  }

  /**
   * 获取指定会话的消息
   * @param sessionId 会话ID
   * @param options 查询选项
   */
  async getMessages(sessionId: string, options: MessageQueryOptions = {}): Promise<BaseMessage[]> {
    const history = await this.getHistory(sessionId);
    const messages = await history.getMessages();
    const ordered = options.reverse ? [...messages].reverse() : messages;
    if (options.limit == null || options.limit < 0) {
      return ordered;
    }
    if (options.reverse) {
      return ordered.slice(0, options.limit);
    }
    return ordered.slice(Math.max(0, ordered.length - options.limit));
  }

  /**
   * 将指定会话的消息转换为缓冲字符串
   * @param sessionId 会话ID
   * @param humanPrefix 人类消息前缀
   * @param aiPrefix AI消息前缀
   */
  async getMessagesAsBuffer(sessionId: string, humanPrefix?: string, aiPrefix?: string): Promise<string> {
    const messages = await this.getMessages(sessionId);
    return getBufferString(messages, humanPrefix, aiPrefix);
  }

  /**
   * 列出聊天会话
   * @param options 列表选项
   */
  async listSessions(options: ListSessionsOptions = {}): Promise<ChatSessionSummary[]> {
    await this.ensureInitialized();
    if (!this.collection) {
      return [];
    }
    const { limit = 20, skip = 0, order = 'desc', sessionIds } = options;
    const pipeline: Document[] = [];
    if (sessionIds?.length) {
      pipeline.push({ $match: { sessionId: { $in: sessionIds } } });
    }
    pipeline.push({
      $addFields: {
        messageCount: { $ifNull: ['$messageCount', { $size: '$messages' }] },
      },
    });
    pipeline.push({ $sort: { updatedAt: order === 'asc' ? 1 : -1, sessionId: 1 } });
    if (skip > 0) {
      pipeline.push({ $skip: skip });
    }
    if (limit > 0) {
      pipeline.push({ $limit: limit });
    }
    pipeline.push({
      $project: {
        _id: 0,
        sessionId: 1,
        messageCount: 1,
        createdAt: 1,
        updatedAt: 1,
      },
    });
    const summaries = await this.collection.aggregate(pipeline).toArray();
    return summaries.map((summary) => ({
      sessionId: summary.sessionId as string,
      messageCount: (summary.messageCount as number) ?? 0,
      createdAt: summary.createdAt,
      updatedAt: summary.updatedAt,
    }));
  }

  /**
   * 删除指定会话
   * @param sessionId 会话ID
   */
  async deleteSession(sessionId: string): Promise<void> {
    await this.ensureInitialized();
    if (!this.collection) {
      return;
    }
    await this.collection.deleteOne({ sessionId });
    this.histories.delete(sessionId);
  }

  /**
   * 清空指定会话的消息
   * @param sessionId 会话ID
   */
  async clear(sessionId: string): Promise<void> {
    const history = await this.getHistory(sessionId);
    await history.clear();
    this.histories.delete(sessionId);
  }
}

export { MongoChatHistoryTool };
export default MongoChatHistoryTool;
相关推荐
Polaris_o9 小时前
轻松上手Bootstrap框架
前端
1024小神9 小时前
微信小程序前端扫码动画效果绿色光效移动,四角椭圆
前端
C_心欲无痕9 小时前
网络相关 - 强缓存与协商缓存讲解
前端·网络·网络协议·缓存
用户904706683579 小时前
初来乍到公司,git不会用,怎么在团队里写代码?
前端
我的写法有点潮9 小时前
如何取消Vue Watch监听
前端·javascript·vue.js
童心虫鸣9 小时前
如何在Vue中传递函数作为Prop
前端·vue.js
用户6600676685399 小时前
用 React + TailwindCSS 打造高体验登录页面
前端·react.js
HashTang9 小时前
【AI 编程实战】第 6 篇:告别复制粘贴 - 设计一个优雅的 HTTP 模块
前端·uni-app·ai编程
用户6600676685399 小时前
避开 React 性能陷阱:useMemo 与 useCallback 的正确打开方式
前端·react.js
xkxnq9 小时前
第一阶段:Vue 基础入门(第 6 天)
前端·javascript·vue.js