feat: 风险关键词红色标记 + 点击显示权重贡献

- 后端: _extract_reason_tokens 返回 [{token, weight}] 格式
- 前端: detect/batch 页面风险关键词使用红色标签样式
- 点击关键词弹窗显示权重值及判定倾向

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
刘正航
2026-04-21 22:55:42 +08:00
parent b5237f9038
commit 50440e84fb
6 changed files with 44 additions and 6 deletions

View File

@@ -119,7 +119,7 @@ class NaiveBayesSpamClassifier:
"trained_at": self.metadata.get("trained_at"),
}
def _extract_reason_tokens(self, text: str, classes: list[str], x_row) -> list[str]:
def _extract_reason_tokens(self, text: str, classes: list[str], x_row) -> list[dict]:
try:
vocab = self.vectorizer.vocabulary_
feature_names = self.vectorizer.get_feature_names_out()
@@ -138,12 +138,12 @@ class NaiveBayesSpamClassifier:
if idx is None:
continue
delta = class_log_prob[spam_idx][idx] - class_log_prob[ham_idx][idx]
scored.append((token, delta))
scored.append({"token": token, "weight": round(delta, 4)})
scored.sort(key=lambda row: abs(row[1]), reverse=True)
return [token for token, _ in scored[:5]]
scored.sort(key=lambda row: abs(row["weight"]), reverse=True)
return scored[:5]
except Exception:
return list(text[:5])
return [{"token": ch, "weight": 0.0} for ch in list(text[:5])]
def model_info(self) -> dict:
return {

View File

@@ -393,6 +393,12 @@ button.btn::after {
color: #ffd487;
}
.tag-danger {
background: rgba(255, 91, 111, 0.18);
border: 1rpx solid rgba(255, 108, 128, 0.42);
color: #ffdce1;
}
.chip-group {
display: flex;
flex-wrap: wrap;

View File

@@ -66,5 +66,18 @@ Page({
'测试报告我已经同步到项目群。'
].join('\n')
})
},
showTokenWeight(e) {
const token = e.currentTarget.dataset.token
const weight = e.currentTarget.dataset.weight
const weightNum = Number(weight || 0)
const direction = weightNum >= 0 ? '倾向垃圾判定' : '倾向正常判定'
wx.showModal({
title: '关键词权重',
content: `关键词"${token}"\n权重贡献:${weightNum >= 0 ? '+' : ''}${weightNum.toFixed(4)}\n(${direction})`,
showCancel: false,
confirmText: '关闭'
})
}
})

View File

@@ -69,6 +69,12 @@
<view class="progress-track">
<view class="{{item.prediction === 'spam' ? 'progress-fill' : 'progress-fill-safe'}}" style="width: {{item.confidence_text}};"></view>
</view>
<view class="field" wx:if="{{item.reason_tokens && item.reason_tokens.length}}">
<text class="field-label">风险关键词</text>
<view class="chip-group">
<text class="tag tag-danger" wx:for="{{item.reason_tokens}}" wx:for-item="tokenItem" wx:key="token" data-token="{{tokenItem.token}}" data-weight="{{tokenItem.weight}}" bindtap="showTokenWeight">{{tokenItem.token}}</text>
</view>
</view>
</view>
</view>
</view>

View File

@@ -98,5 +98,18 @@ Page({
gotoHistory() {
wx.navigateTo({ url: '/pages/history/index' })
},
showTokenWeight(e) {
const token = e.currentTarget.dataset.token
const weight = e.currentTarget.dataset.weight
const weightNum = Number(weight || 0)
const direction = weightNum >= 0 ? '倾向垃圾判定' : '倾向正常判定'
wx.showModal({
title: '关键词权重',
content: `关键词"${token}"\n权重贡献:${weightNum >= 0 ? '+' : ''}${weightNum.toFixed(4)}\n(${direction})`,
showCancel: false,
confirmText: '关闭'
})
}
})

View File

@@ -75,7 +75,7 @@
<view class="field" wx:if="{{result.detect.reason_tokens && result.detect.reason_tokens.length}}">
<text class="field-label">风险关键词</text>
<view class="chip-group">
<text class="tag" wx:for="{{result.detect.reason_tokens}}" wx:key="*this">{{item}}</text>
<text class="tag tag-danger" wx:for="{{result.detect.reason_tokens}}" wx:key="token" data-token="{{item.token}}" data-weight="{{item.weight}}" bindtap="showTokenWeight">{{item.token}}</text>
</view>
</view>