perf(ai-moderation): dual-key write-back — clean verdict ikut di-cache global
Analisis pertama tetap berkonteks (chat history) demi akurasi, tapi verdict clean non-actionable (conf>=0.85) juga ditulis di bare key tanpa konteks. Repeat teks sama di channel lain -> exact cache HIT, bukan LLM call baru. Guard sama dgn read path; dedupe LRU per proses; bare row tanpa embedding (tier semantic sudah global).
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
* parallel text+media analysis, LLM calls with retry, and cache handling.
|
* parallel text+media analysis, LLM calls with retry, and cache handling.
|
||||||
*/
|
*/
|
||||||
import { createChildLogger } from "@/shared/logger/index";
|
import { createChildLogger } from "@/shared/logger/index";
|
||||||
|
import { LRUCache } from "lru-cache";
|
||||||
import { config } from "../../shared/config/config.js";
|
import { config } from "../../shared/config/config.js";
|
||||||
import { extractMessageMediaEvidence } from "../message-capture/messageMetadata.js";
|
import { extractMessageMediaEvidence } from "../message-capture/messageMetadata.js";
|
||||||
import type {
|
import type {
|
||||||
@@ -35,6 +36,13 @@ import {
|
|||||||
|
|
||||||
const log = createChildLogger("moderationOrchestrator");
|
const log = createChildLogger("moderationOrchestrator");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bare keys already written this process (dual-key write-back dedupe).
|
||||||
|
* LRU-bounded so a long-lived gateway can't grow it without limit; the DB
|
||||||
|
* upsert underneath is idempotent anyway — this just avoids redundant writes.
|
||||||
|
*/
|
||||||
|
const globalBareKeysWritten = new LRUCache<string, true>({ max: 5000 });
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Types
|
// Types
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -434,20 +442,50 @@ export async function runModerationAnalysis(
|
|||||||
rawContent,
|
rawContent,
|
||||||
makeModerationContextKey(target),
|
makeModerationContextKey(target),
|
||||||
);
|
);
|
||||||
|
const stored = {
|
||||||
|
flags: result.flags ?? [],
|
||||||
|
score: result.score ?? 0,
|
||||||
|
analysis: result.analysis ?? "",
|
||||||
|
categories: result.categories ?? result.flags ?? [],
|
||||||
|
severity: result.severity ?? "none",
|
||||||
|
confidence: result.confidence ?? result.score ?? 0,
|
||||||
|
recommendedAction: result.recommendedAction ?? "none",
|
||||||
|
status: result.status,
|
||||||
|
};
|
||||||
setCachedTextModeration(
|
setCachedTextModeration(
|
||||||
cacheKey,
|
cacheKey,
|
||||||
{
|
stored,
|
||||||
flags: result.flags ?? [],
|
|
||||||
score: result.score ?? 0,
|
|
||||||
analysis: result.analysis ?? "",
|
|
||||||
categories: result.categories ?? result.flags ?? [],
|
|
||||||
severity: result.severity ?? "none",
|
|
||||||
confidence: result.confidence ?? result.score ?? 0,
|
|
||||||
recommendedAction: result.recommendedAction ?? "none",
|
|
||||||
status: result.status,
|
|
||||||
},
|
|
||||||
embeddingsByKey.get(cacheKey),
|
embeddingsByKey.get(cacheKey),
|
||||||
).catch(() => {});
|
).catch(() => {});
|
||||||
|
|
||||||
|
// Dual-key write-back (2026-08-24): the FIRST analysis of a message runs
|
||||||
|
// WITH conversation context (accurate), but its verdict is also stored
|
||||||
|
// under the context-free bare key so repeats in OTHER channels hit the
|
||||||
|
// exact cache instead of paying a new LLM call. Same guard as the read
|
||||||
|
// path — only non-actionable clean verdicts may cross channels. No
|
||||||
|
// embedding on the bare row: the semantic tier is already global, and
|
||||||
|
// writing one would create a duplicate Qdrant point for this content.
|
||||||
|
const bareKey = makeTextModerationCacheKey(rawContent);
|
||||||
|
if (
|
||||||
|
bareKey !== cacheKey &&
|
||||||
|
!globalBareKeysWritten.has(bareKey) &&
|
||||||
|
isGloballyReusableCleanVerdict(
|
||||||
|
{
|
||||||
|
status: stored.status,
|
||||||
|
flags: stored.flags,
|
||||||
|
score: stored.score,
|
||||||
|
analysis: stored.analysis,
|
||||||
|
categories: stored.categories,
|
||||||
|
severity: stored.severity,
|
||||||
|
confidence: stored.confidence,
|
||||||
|
recommendedAction: stored.recommendedAction,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
globalBareKeysWritten.set(bareKey, true);
|
||||||
|
setCachedTextModeration(bareKey, stored, null).catch(() => {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allResults = [
|
const allResults = [
|
||||||
|
|||||||
@@ -524,7 +524,7 @@ export function isSemanticBandAccepted(
|
|||||||
* globally reused — enforcement is context-sensitive by design.
|
* globally reused — enforcement is context-sensitive by design.
|
||||||
*/
|
*/
|
||||||
export function isGloballyReusableCleanVerdict(
|
export function isGloballyReusableCleanVerdict(
|
||||||
verdict: StoredModerationVerdict,
|
verdict: Omit<StoredModerationVerdict, "status"> & { status: string },
|
||||||
analyzedAtMs: number | undefined,
|
analyzedAtMs: number | undefined,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (verdict.status !== "clean") return false;
|
if (verdict.status !== "clean") return false;
|
||||||
|
|||||||
@@ -157,4 +157,13 @@ describe("isGloballyReusableCleanVerdict", () => {
|
|||||||
it("skips the age check when analyzedAt is unknown", () => {
|
it("skips the age check when analyzedAt is unknown", () => {
|
||||||
expect(isGloballyReusableCleanVerdict(makeVerdict(), undefined)).toBe(true);
|
expect(isGloballyReusableCleanVerdict(makeVerdict(), undefined)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects non-standard statuses such as processing write-backs", () => {
|
||||||
|
expect(
|
||||||
|
isGloballyReusableCleanVerdict(
|
||||||
|
{ ...makeVerdict(), status: "processing" },
|
||||||
|
undefined,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user