fix: remove user history injection to prevent false positive moderation

- Removed getUserRecentInfractions usage in textBatchProcessor.ts and visionAnalyzer.ts
- Removed buildUserHistoryXml import and calls
- Messages are now evaluated standalone, not influenced by past violations in other channels
- Updated moderation prompts with clearer instructions about user_history usage
- Fixes issue where benign messages like 'tubuh manusia vs gravitasi' were incorrectly flagged due to carryover from previous drone weapons discussion

The user history context was causing the LLM to interpret unrelated current messages
as threats because it conflated them with past violations. Now each message is judged
on its own merit with only channel-specific context.
This commit is contained in:
asepharyana
2026-08-12 20:48:26 +07:00
parent deb5dedf2c
commit f849a87f2f
4 changed files with 12 additions and 56 deletions
@@ -40,7 +40,7 @@ Data konteks tersedia: <user_profiles> (peta ringkasan kepribadian, di pesan USE
Gunakan untuk personalisasi analysis, tapi: Gunakan untuk personalisasi analysis, tapi:
- Profil adalah KONTEKS, bukan bukti. Profil mencurigakan ≠ flag; profil bersih ≠ loloskan pelanggaran. - Profil adalah KONTEKS, bukan bukti. Profil mencurigakan ≠ flag; profil bersih ≠ loloskan pelanggaran.
- Perubahan perilaku mencolok (biasanya teknis tiba-tiba provokatif) layak dicatat di analysis. - Perubahan perilaku mencolok (biasanya teknis tiba-tiba provokatif) layak dicatat di analysis.
- <user_history> (kutipan pesan yang pernah di-flag) = pola pelanggaran lama. Gunakan untuk mendeteksi PENGULANGAN (mis. spam link yang sama, provokasi berulang), tapi JANGAN memflag pesan bersih hanya karena riwayat. - <user_history> (kutipan pesan yang pernah di-flag) = pola pelanggaran lama. Gunakan untuk mendeteksi PENGULANGAN KEKONSISTEN (spam link yang SAMA, provokasi yang MENGULANG KONTEN NYATA YANG SAMA). JANGAN pernah gunakan history untuk "menginterpretasi ulang" pesan bersih yang TERPISAH DARI riwayat. Setiap pesan BARU dinilai TERSAMBUNG (standalone). Jika tidak ada pola pengulangan yang jelas → CLEAN. Contoh: Jika sebelumnya ada pesan dengan link scam.example.com yang di-flag, dan pesan baru juga ada link scam.example.com → FLAG. Tapi jika pesan baru tentang "energi kinetik dari jatuh" tanpa link yang sama → CLEAN walaupun ada history lain.
- JANGAN paksa referensi profil jika tidak relevan — analysis natural lebih baik. - JANGAN paksa referensi profil jika tidak relevan — analysis natural lebih baik.
- Channel culture coding/teknis → pesan teknis lebih wajar; channel santai → slang lebih wajar. Jangan dipakai mengabaikan pelanggaran nyata. - Channel culture coding/teknis → pesan teknis lebih wajar; channel santai → slang lebih wajar. Jangan dipakai mengabaikan pelanggaran nyata.
@@ -35,10 +35,12 @@ export const SYSTEM_RULES = `Kamu adalah asisten moderasi konten untuk server Di
Kata alat kelamin/anatomi seksual (kontol, memek, titten, tit, dick) atau istilah seksual eksplisit WAJIB di-flag sebagai vulgar_language/sexual_content — TANPA pengecualian bercanda, slang, atau "santai". Kata alat kelamin/anatomi seksual (kontol, memek, titten, tit, dick) atau istilah seksual eksplisit WAJIB di-flag sebagai vulgar_language/sexual_content — TANPA pengecualian bercanda, slang, atau "santai".
## Nilai Server — Diskriminasi ## Nilai Server — Diskriminasi
- Seksisme ("dasar perempuan", "logika cewek") → hate_speech (umum) / harassment (terarah). -Ketika sesuatu yang melanggar terjadi di channel, flag jika relevan. Setiap pesan dinilai BERDASARKAN ISINYA SENDIRI, bukan sekadar histori pengguna.
- Ageisme ("dasar bocil", "tau aja lo tua") → hate_speech / harassment. -Seksisme ("dasar perempuan", "logika cewek") → hate_speech (umum) / harassment (terarah).
- Diskriminasi fisik ("gendut", "iteman", "cungkring") → harassment jika terarah. -Ageisme ("dasar bocil", "tau aja lo tua") → hate_speech / harassment.
- Serangan personal, penghinaan, merendahkan = tidak ditoleransi. Perbedaan pendapat wajar. -Diskriminasi fisik ("gendut", "iteman", "cungkring") → harassment jika terarah.
-Serangan personal, penghinaan, merendahkan = tidak ditoleransi. Perbedaan pendapat wajar.
+**PESAN DINILAI SECARA STANDALONE:** Setiap pesan baru dinilai BERDASARKAN ISINYA SENDIRI. <user_history> (jika ada) HANYA untuk mendeteksi POLA PENGULANGAN dengan JAMAK (spam link yang SAMA, provokasi berulang yang MENGANDALKAN KONTEN YANG SAMA). JANGAN gunakan history untuk "menginterpretasi ulang" pesan bersih yang TERPISAH DARI riwayat pelanggaran sebelumnya. Jika pesan tidak mengandung unsur yang BERPANDUAN PADA riwayat → tetap CLEAN.
## LARANGAN BERAT (ZERO TOLERANCE) ## LARANGAN BERAT (ZERO TOLERANCE)
- **LGBT:** Segala promosi, diskusi, pengakuan orientasi, coming out, atau curhat personal tentang LGBT WAJIB di-flag "sexual_deviation". Tidak ada pengecualian. - **LGBT:** Segala promosi, diskusi, pengakuan orientasi, coming out, atau curhat personal tentang LGBT WAJIB di-flag "sexual_deviation". Tidak ada pengecualian.
@@ -19,7 +19,6 @@ import { callModerationLLM } from "./llmCaller.js";
import { analyzeSingleMediaImage } from "./mediaAnalysisClient.js"; import { analyzeSingleMediaImage } from "./mediaAnalysisClient.js";
import { import {
buildReferenceXml, buildReferenceXml,
buildUserHistoryXml,
buildUserProfileRef, buildUserProfileRef,
buildUserProfilesBlock, buildUserProfilesBlock,
escapeXml, escapeXml,
@@ -41,10 +40,7 @@ import { buildTermGlossaryBlock } from "./termGlossary.js";
import { getRecentCorrectedModerations } from "./textCacheStore.js"; import { getRecentCorrectedModerations } from "./textCacheStore.js";
import { extractUrlsFromText, fetchUrlSafely } from "./urlFetcher.js"; import { extractUrlsFromText, fetchUrlSafely } from "./urlFetcher.js";
import { getUserProfile } from "./userProfileStore.js"; import { getUserProfile } from "./userProfileStore.js";
import { import { initializeUserReputation } from "./userReputationStore.js";
getUserRecentInfractions,
initializeUserReputation,
} from "./userReputationStore.js";
import type { MessageImagePart } from "./visionAnalyzer.js"; import type { MessageImagePart } from "./visionAnalyzer.js";
const log = createChildLogger("textBatchProcessor"); const log = createChildLogger("textBatchProcessor");
@@ -218,27 +214,7 @@ export async function runTextOnlyBatch(
if (!userContexts.has(msg.user_id)) { if (!userContexts.has(msg.user_id)) {
const rep = await initializeUserReputation(msg.user_id, msg.guild_id); const rep = await initializeUserReputation(msg.user_id, msg.guild_id);
const repAttrs = formatReputationAttrs(rep); const repAttrs = formatReputationAttrs(rep);
let repXml = `<user_reputation ${repAttrs}/>`; const repXml = `<user_reputation ${repAttrs}/>`;
// Repeat offenders get their last flagged messages as <user_history>
// so the LLM can recognize PATTERNS (same scam link, repeated
// provocation) — history is reference, never proof. Best-effort.
if (rep.total_infractions > 0) {
try {
const history = await getUserRecentInfractions(msg.user_id, 2);
const historyXml = buildUserHistoryXml(
history.map((h) => ({
content: h.content ?? "",
severity: h.severity,
created_at: h.created_at,
})),
);
if (historyXml) {
repXml = `<user_reputation ${repAttrs}>\n${historyXml}\n</user_reputation>`;
}
} catch {
// history is a bonus — fall back to attrs-only reputation
}
}
userContexts.set(msg.user_id, repXml); userContexts.set(msg.user_id, repXml);
} }
if (!userProfiles.has(msg.user_id)) { if (!userProfiles.has(msg.user_id)) {
@@ -66,7 +66,6 @@ import {
} from "./mediaDownloader.js"; } from "./mediaDownloader.js";
import { import {
buildReferenceXml, buildReferenceXml,
buildUserHistoryXml,
buildUserProfileRef, buildUserProfileRef,
escapeXml, escapeXml,
formatReputationAttrs, formatReputationAttrs,
@@ -90,10 +89,7 @@ import {
import { buildTermGlossaryBlock } from "./termGlossary.js"; import { buildTermGlossaryBlock } from "./termGlossary.js";
import { extractUrlsFromText } from "./urlFetcher.js"; import { extractUrlsFromText } from "./urlFetcher.js";
import { getUserProfile } from "./userProfileStore.js"; import { getUserProfile } from "./userProfileStore.js";
import { import { initializeUserReputation } from "./userReputationStore.js";
getUserRecentInfractions,
initializeUserReputation,
} from "./userReputationStore.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Types // Types
@@ -459,27 +455,9 @@ export async function prepareMediaMessage(
? buildUserProfileRef(target.user_id) ? buildUserProfileRef(target.user_id)
: ""; : "";
// Rich reputation — same shape as the text path: attrs + optional // Rich reputation — attrs only, no user history injection (per channel context preference)
// <user_history> with the last flagged messages for repeat offenders.
const repAttrs = formatReputationAttrs(rep); const repAttrs = formatReputationAttrs(rep);
let repXml = `<user_reputation ${repAttrs}/>`; const repXml = `<user_reputation ${repAttrs}/>`;
if (rep.total_infractions > 0) {
try {
const history = await getUserRecentInfractions(target.user_id, 2);
const historyXml = buildUserHistoryXml(
history.map((h) => ({
content: h.content ?? "",
severity: h.severity,
created_at: h.created_at,
})),
);
if (historyXml) {
repXml = `<user_reputation ${repAttrs}>\n${historyXml}\n</user_reputation>`;
}
} catch {
// history is a bonus — fall back to attrs-only reputation
}
}
const isBot = resolveIsBot(target); const isBot = resolveIsBot(target);
const isEdited = resolveIsEdited(target); const isEdited = resolveIsEdited(target);