向量开发指南

做一个小插件,让角色在聊到相关话题时想起之前保存的笔记。示例有两条记忆:「角色每周五晚上十点坐夜班火车」和「角色最喜欢茉莉花茶」。发出 回忆:她周五几点出发? 时,插件会搜索笔记,并把最接近的一条加入本轮模型上下文。

准备工作

  • 在 Tavo 的 模型与服务 → 高级服务 → 向量化 API 添加连接,选择向量化模型并将连接设为默认。
  • 准备一个能正常聊天的会话,开启高级渲染。稍后在同一个聊天里保存、搜索和验证召回。

示例在保存笔记和搜索时把相应文本发送给默认向量化服务。Tavo 将笔记原文和向量保存在本地集合中。

四步完成一次记忆召回

下面四段代码沿用后面完整示例中的变量。

1. 生成向量

调用 tavo.embedding.generate 生成向量,调用前必须配置好向量 API。 返回值会带有向量值 embedded.vectors 与校验码 embedded.embeddingSpaceId

const embedded = await tavo.embedding.generate(
  notes.map(note => note.text),
  { timeout },
);

2. 保存记忆

用 tavo.vector.collection() 取得当前聊天的集合,再用 collection.upsert() 写入每条笔记的原文和向量。第一个参数传入上一步返回的 embeddingSpaceId。

const collection = tavo.vector.collection(collectionName);
await collection.upsert(
  embedded.embeddingSpaceId,
  notes.map((note, index) => ({ ...note, vector: embedded.vectors[index] })),
);

3. 检索记忆

先把查询文本向量化,再把查询向量和这次返回的 embeddingSpaceId 交给 collection.search()。limit: 1 取最接近的一条笔记。

async function searchNotes(collection, query) {
  const embedded = await tavo.embedding.generate(query, { timeout });
  return collection.search(embedded.embeddingSpaceId, embedded.vectors[0], { limit: 1 });
}

4. 注入上下文

在 generation:prepare 中,用 回忆: 后面的问题检索记忆。调用 event.addContext() 把命中的笔记加入本轮模型请求。

tavo.plugin.on('generation:prepare', async event => {
  if (!event.text.startsWith(recallPrefix)) return;
  const query = event.text.slice(recallPrefix.length).trim();
  const collection = tavo.vector.collection(collectionName);
  const hits = await searchNotes(collection, query);
  event.addContext({ text: "相关记忆:\n" + hits[0].text });
});

复制并运行示例

在同一个文件夹中创建 manifest.json 和 entry.js,把以下两段代码分别复制进去。

manifest.json

{
  "specVersion": 2,
  "id": "com.example.vector-memory",
  "name": "向量记忆示例",
  "version": "1.0.0",
  "entry": "entry.js",
  "author": "Example",
  "description": "保存示例笔记,在相关聊天中找回它们。",
  "localization": {
    "defaultLocale": "zh-CN"
  },
  "permissions": [
    "generate"
  ],
  "contributes": {
    "inputActions": [
      {
        "id": "save-memory",
        "label": "保存示例记忆"
      },
      {
        "id": "search-memory",
        "label": "搜索示例记忆"
      }
    ]
  }
}

entry.js

const collectionName = 'example_vector_memory_notes';
const notes = [
  { id: 'train', text: "角色每周五晚上十点坐夜班火车。" },
  { id: 'tea', text: "角色最喜欢茉莉花茶。" },
];
const exampleQuery = "她周五几点出发?";
const recallPrefix = "回忆:";
const timeout = 5000;

async function saveNotes(collection) {
  const embedded = await tavo.embedding.generate(
    notes.map(note => note.text),
    { timeout },
  );
  if (!embedded) return null;

  return collection.upsert(
    embedded.embeddingSpaceId,
    notes.map((note, index) => ({ ...note, vector: embedded.vectors[index] })),
  );
}

async function searchNotes(collection, query) {
  const info = await collection.info();
  if (!info || info.count === 0) return [];

  const embedded = await tavo.embedding.generate(query, { timeout });
  if (!embedded) return null;

  return collection.search(embedded.embeddingSpaceId, embedded.vectors[0], { limit: 1 });
}

tavo.plugin.onInputAction('save-memory', async () => {
  const collection = tavo.vector.collection(collectionName);
  const saved = await saveNotes(collection);
  if (saved) await tavo.utils.toast("示例记忆已保存");
});

tavo.plugin.onInputAction('search-memory', async () => {
  const collection = tavo.vector.collection(collectionName);
  const hits = await searchNotes(collection, exampleQuery);
  if (hits === null) return;
  await tavo.utils.toast(hits[0]?.text ?? "未找到相关记忆");
});

tavo.plugin.on('generation:prepare', async event => {
  if (!event.text.startsWith(recallPrefix)) return;
  const query = event.text.slice(recallPrefix.length).trim();
  if (!query) return;

  try {
    const collection = tavo.vector.collection(collectionName);
    const hits = await searchNotes(collection, query);
    const note = hits?.[0]?.text;
    if (note) event.addContext({ text: "相关记忆:\n" + note });
  } catch (error) {
    console.warn("本轮跳过记忆召回", error?.code ?? 'unknown_error');
  }
});

在这两个文件所在的目录运行打包命令。包内的两个文件都要位于根目录。

zip vector-memory.tpg manifest.json entry.js
  1. 在 设置 → 插件 导入 vector-memory.tpg,然后启用插件。也可以参照插件安装说明。
  2. 回到开启了高级渲染的聊天,从输入框旁的 + 菜单点 保存示例记忆。如果 Tavo 询问是否发送文本,请确认。看到「示例记忆已保存」后,点 搜索示例记忆,预期提示「角色每周五晚上十点坐夜班火车。」
  3. 开启上下文日志,发送 回忆:她周五几点出发?。在聊天右侧栏打开 日志,查看这一轮的「插件」上下文块,确认其中有火车笔记。

如果菜单里没有示例动作,检查插件是否已启用、当前聊天是否开启了高级渲染。如果搜索不到笔记,先确认写入和搜索发生在同一个聊天。更换默认向量化模型后出现 embedding_space_mismatch 时,可调用 await collection.rebuild({ batchSize: 16 }),用已保存的文本重建集合。重建中断后可再次调用,继续补齐缺少的向量。

用文本方法简化代码

upsertText() 封装了「生成向量 → upsert()」,searchText() 封装了「生成查询向量 → search()」。两个方法会自动传递向量和 embeddingSpaceId,调用失败时抛出错误。将示例中同名的两个函数替换为下面这段,两个菜单动作和生成前召回就会使用文本接口。

async function saveNotes(collection) {
  return collection.upsertText(notes, { timeout });
}

async function searchNotes(collection, query) {
  return collection.searchText(query, { limit: 1, timeout });
}

完整参数、集合范围和重建时缺失文本的处理方式见 TavoJS API 的本地向量集合。

如果需要直接读写当前聊天的长记忆向量,可使用 tavo.vector.collection('_memories')。它与长记忆共用记录,示例和写入约定见访问长记忆向量。

目录