向量开发指南
做一个小插件,让角色在聊到相关话题时想起之前保存的笔记。示例有两条记忆:「角色每周五晚上十点坐夜班火车」和「角色最喜欢茉莉花茶」。发出 回忆:她周五几点出发? 时,插件会搜索笔记,并把最接近的一条加入本轮模型上下文。
准备工作
- 在 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- 在 设置 → 插件 导入
vector-memory.tpg,然后启用插件。也可以参照插件安装说明。 - 回到开启了高级渲染的聊天,从输入框旁的 + 菜单点 保存示例记忆。如果 Tavo 询问是否发送文本,请确认。看到「示例记忆已保存」后,点 搜索示例记忆,预期提示「角色每周五晚上十点坐夜班火车。」
- 开启上下文日志,发送
回忆:她周五几点出发?。在聊天右侧栏打开 日志,查看这一轮的「插件」上下文块,确认其中有火车笔记。
如果菜单里没有示例动作,检查插件是否已启用、当前聊天是否开启了高级渲染。如果搜索不到笔记,先确认写入和搜索发生在同一个聊天。更换默认向量化模型后出现 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')。它与长记忆共用记录,示例和写入约定见访问长记忆向量。