mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-13 18:31:16 +08:00
fix(vector): prevent incompatible vector reuse
This commit is contained in:
@@ -71,7 +71,12 @@ function createAuthorityVectorGraph() {
|
||||
return { graph, first, second };
|
||||
}
|
||||
|
||||
function createMockTriviumClient({ failBulkUpsert = false, failSearch = false, failBmeVectorApply = false } = {}) {
|
||||
function createMockTriviumClient({
|
||||
failBulkUpsert = false,
|
||||
failSearch = false,
|
||||
failBmeVectorApply = false,
|
||||
failBmeVectorApplyCompatibility = false,
|
||||
} = {}) {
|
||||
const calls = [];
|
||||
return {
|
||||
calls,
|
||||
@@ -156,6 +161,14 @@ function createMockTriviumClient({ failBulkUpsert = false, failSearch = false, f
|
||||
path: "/bme/vector-apply",
|
||||
});
|
||||
}
|
||||
if (failBmeVectorApplyCompatibility) {
|
||||
throw new AuthorityHttpError("BME vector apply dimension mismatch", {
|
||||
status: 400,
|
||||
category: "validation",
|
||||
payload: { details: { category: "vector-dimension-mismatch" } },
|
||||
path: "/bme/vector-apply",
|
||||
});
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
database: payload.database || "st_bme_vectors",
|
||||
@@ -239,6 +252,9 @@ assert.equal(isAuthorityVectorConfig(config), true);
|
||||
|
||||
assert.equal(result.stats.indexed, 2);
|
||||
assert.equal(graph.vectorIndexState.dirty, false);
|
||||
assert.equal(graph.vectorIndexState.manifest.status, "clean");
|
||||
assert.equal(graph.vectorIndexState.manifest.backend, "authority");
|
||||
assert.equal(graph.vectorIndexState.manifest.observedDim, 2);
|
||||
assert.equal(triviumClient.calls.filter(([name]) => name === "bmeVectorApply").length, 1);
|
||||
assert.equal(triviumClient.calls.some(([name]) => name === "purge"), false);
|
||||
assert.equal(triviumClient.calls.some(([name]) => name === "bulkUpsert"), false);
|
||||
@@ -275,6 +291,45 @@ assert.equal(isAuthorityVectorConfig(config), true);
|
||||
assert.equal(triviumClient.calls.some(([name]) => name === "bmeVectorApply"), false);
|
||||
}
|
||||
|
||||
{
|
||||
const { graph } = createAuthorityVectorGraph();
|
||||
const triviumClient = createMockTriviumClient({ failBmeVectorApplyCompatibility: true });
|
||||
const applyConfig = { ...config, bmeVectorApplyReady: true };
|
||||
const result = await syncGraphVectorIndexFromIndex(graph, applyConfig, {
|
||||
chatId: "chat-authority-vector",
|
||||
purge: true,
|
||||
triviumClient,
|
||||
});
|
||||
|
||||
assert.equal(graph.vectorIndexState.dirty, true);
|
||||
assert.equal(result.errorCategory, "validation");
|
||||
assert.equal(triviumClient.calls.filter(([name]) => name === "bmeVectorApply").length, 1);
|
||||
assert.equal(triviumClient.calls.some(([name]) => name === "purge"), false);
|
||||
assert.equal(triviumClient.calls.some(([name]) => name === "bulkUpsert"), false);
|
||||
}
|
||||
|
||||
{
|
||||
const { graph, first, second } = createAuthorityVectorGraph();
|
||||
const triviumClient = createMockTriviumClient();
|
||||
const applyConfig = { ...config, bmeVectorApplyReady: true };
|
||||
await syncGraphVectorIndexFromIndex(graph, applyConfig, {
|
||||
chatId: "chat-authority-vector",
|
||||
purge: true,
|
||||
triviumClient,
|
||||
});
|
||||
const changedModelConfig = { ...applyConfig, model: "other-embedding-model" };
|
||||
const results = await findSimilarNodesByTextFromIndex(
|
||||
graph,
|
||||
"archive door",
|
||||
changedModelConfig,
|
||||
5,
|
||||
[first, second],
|
||||
);
|
||||
assert.deepEqual(results, []);
|
||||
assert.equal(graph.vectorIndexState.dirtyReason, "authority-vector-space-mismatch");
|
||||
assert.equal(graph.vectorIndexState.lastSearchTimings.reason, "authority-vector-space-mismatch");
|
||||
}
|
||||
|
||||
{
|
||||
const { graph } = createAuthorityVectorGraph();
|
||||
const triviumClient = createMockTriviumClient({ failBmeVectorApply: true });
|
||||
|
||||
@@ -99,4 +99,17 @@ const baseConfig = {
|
||||
assert.equal(graph.vectorIndexState.lastSearchTimings.reason, "query-dimension-mismatch");
|
||||
}
|
||||
|
||||
{
|
||||
const graph = createVectorGraph();
|
||||
graph.nodes[0].embedding = [0.1, 0.2, 0.3];
|
||||
embeddingDim = 3;
|
||||
const changedModelConfig = { ...baseConfig, model: "text-embedding-3-large" };
|
||||
await syncGraphVectorIndex(graph, changedModelConfig, { chatId: graph.historyState.chatId });
|
||||
assert.equal(graph.vectorIndexState.manifest.status, "clean");
|
||||
assert.equal(graph.vectorIndexState.manifest.model, "text-embedding-3-large");
|
||||
assert.equal(graph.nodes[0].embedding.length, 3);
|
||||
assert.equal(graph.nodes[0].embedding[0], 1);
|
||||
assert.notDeepEqual(graph.nodes[0].embedding, [0.1, 0.2, 0.3]);
|
||||
}
|
||||
|
||||
console.log("vector-manifest tests passed");
|
||||
|
||||
@@ -651,6 +651,16 @@ function markLocalVectorManifestStale(graph, config, reason = "vector-space-chan
|
||||
: "向量模型配置变化,索引已标记为待重建";
|
||||
}
|
||||
|
||||
function isVectorApplyCompatibilityError(error = null) {
|
||||
const detailCategory = String(error?.payload?.details?.category || error?.details?.category || "").trim();
|
||||
const message = String(error?.message || "").toLowerCase();
|
||||
return detailCategory === "vector-dimension-mismatch" ||
|
||||
detailCategory === "vector-space-mismatch" ||
|
||||
message.includes("dimension mismatch") ||
|
||||
message.includes("vectorspaceid mismatch") ||
|
||||
message.includes("single vector dimension");
|
||||
}
|
||||
|
||||
function markBackendVectorStateDirty(
|
||||
graph,
|
||||
config,
|
||||
@@ -924,6 +934,18 @@ export async function syncGraphVectorIndex(
|
||||
);
|
||||
authorityUpsertMs += nowMs() - applyStartedAt;
|
||||
authorityUpsertDiagnostics = applyResult?.diagnostics || null;
|
||||
const observedDim = Number(applyResult?.manifest?.observedDim || getEmbeddingDimensionFromEntries(graph, desiredEntries) || 0);
|
||||
if (observedDim > 0) {
|
||||
updateVectorManifest(graph, config, {
|
||||
backend: "authority",
|
||||
chatId: effectiveChatId,
|
||||
collectionId,
|
||||
graphRevision: graph?.meta?.revision || graph?.revision || 0,
|
||||
desiredEntries,
|
||||
observedDim,
|
||||
status: "clean",
|
||||
});
|
||||
}
|
||||
authorityLinkDiagnostics = {
|
||||
operation: "bmeVectorApply:links",
|
||||
totalItems: Number(applyResult?.diagnostics?.linkItems || 0),
|
||||
@@ -934,6 +956,7 @@ export async function syncGraphVectorIndex(
|
||||
appliedViaBme = true;
|
||||
} catch (applyError) {
|
||||
if (isAbortError(applyError)) throw applyError;
|
||||
if (isVectorApplyCompatibilityError(applyError)) throw applyError;
|
||||
console.warn("[ST-BME] BME 服务端向量 apply 失败,回退 Authority Trivium 旧路径:", applyError);
|
||||
}
|
||||
}
|
||||
@@ -1016,6 +1039,18 @@ export async function syncGraphVectorIndex(
|
||||
);
|
||||
authorityUpsertMs += nowMs() - applyStartedAt;
|
||||
authorityUpsertDiagnostics = applyResult?.diagnostics || null;
|
||||
const observedDim = Number(applyResult?.manifest?.observedDim || getEmbeddingDimensionFromEntries(graph, entriesToUpsert) || 0);
|
||||
if (observedDim > 0) {
|
||||
updateVectorManifest(graph, config, {
|
||||
backend: "authority",
|
||||
chatId: effectiveChatId,
|
||||
collectionId,
|
||||
graphRevision: graph?.meta?.revision || graph?.revision || 0,
|
||||
desiredEntries,
|
||||
observedDim,
|
||||
status: "clean",
|
||||
});
|
||||
}
|
||||
authorityLinkDiagnostics = {
|
||||
operation: "bmeVectorApply:links",
|
||||
totalItems: Number(applyResult?.diagnostics?.linkItems || 0),
|
||||
@@ -1025,6 +1060,7 @@ export async function syncGraphVectorIndex(
|
||||
appliedViaBme = true;
|
||||
} catch (applyError) {
|
||||
if (isAbortError(applyError)) throw applyError;
|
||||
if (isVectorApplyCompatibilityError(applyError)) throw applyError;
|
||||
console.warn("[ST-BME] BME 服务端向量 apply 失败,回退 Authority Trivium 旧路径:", applyError);
|
||||
}
|
||||
}
|
||||
@@ -1230,7 +1266,7 @@ export async function syncGraphVectorIndex(
|
||||
const hasEmbedding =
|
||||
Array.isArray(node?.embedding) && node.embedding.length > 0;
|
||||
|
||||
if (!force && !currentHash && hasEmbedding) {
|
||||
if (!directScopeChanged && !force && !currentHash && hasEmbedding) {
|
||||
state.hashToNodeId[entry.hash] = entry.nodeId;
|
||||
state.nodeToHash[entry.nodeId] = entry.hash;
|
||||
continue;
|
||||
@@ -1516,6 +1552,24 @@ export async function findSimilarNodesByText(
|
||||
}
|
||||
|
||||
if (isAuthorityVectorConfig(config)) {
|
||||
const state = graph?.vectorIndexState || {};
|
||||
if (config.bmeVectorApplyReady === true || config.bmeVectorManifestReady === true) {
|
||||
const currentDim = Number(state.currentVectorSpace?.observedDim || state.manifest?.observedDim || 0);
|
||||
const currentVectorSpace = currentDim > 0
|
||||
? deriveVectorSpace(config, currentDim)
|
||||
: state.currentVectorSpace;
|
||||
if (!isVectorManifestCompatible(state.manifest, currentVectorSpace)) {
|
||||
recordSearchTimings({
|
||||
success: false,
|
||||
reason: "authority-vector-space-mismatch",
|
||||
resultCount: 0,
|
||||
});
|
||||
state.dirty = true;
|
||||
state.dirtyReason = "authority-vector-space-mismatch";
|
||||
state.lastWarning = "Authority 向量空间不匹配,已切换到非向量召回并等待重建";
|
||||
return [];
|
||||
}
|
||||
}
|
||||
const requestStartedAt = nowMs();
|
||||
try {
|
||||
const queryEmbedStartedAt = nowMs();
|
||||
|
||||
Reference in New Issue
Block a user