guard.js
3.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/**
* `guardGeneration` —— 生成链路**前置闸**编排(§4 职责)。
*
* 这是一期产品要点:护栏发生在生成产物落库/回流**之前**,不是事后扫描。
* 流程:①先跑本地 `rules`(敏感词/长度/格式硬红线);②若注入了 `llmGuardPort`,
* 再调它做基础安全类目(暴力/色情/仇恨/未成年)分类;③`decision.js` 取最严合并。
*
* 基础安全类目判定**只来自 `llmGuardPort`**,本包不自训/自写分类模型(验收 4/7)。
* 未注入 `llmGuardPort` 时优雅降级为 `rules-only` 且如实标注,不假装做了 LLM 护栏
* (验收 1,§5 review 关注点 4)。
*
* @typedef {object} GuardPort
* @property {(args: { text:string, context?:any, region?:string }) =>
* Promise<{ categories?:string[], unsafe?:boolean, reasons?:string[] }> | { categories?:string[], unsafe?:boolean, reasons?:string[] }
* } classify 薄封装厂商 moderation 端点 或 轻量提示词裁判
*
* @typedef {object} GuardResult
* @property {'allow'|'flag'|'block'} decision
* @property {string[]} reasons
* @property {string[]} categories
* @property {'rules+llm'|'rules-only'} guardLevel
*/
import { ModerationError, ModerationErrorCode } from './errors.js'
import { resolveRules, runRulesCheck } from './rules-check.js'
import { mergeDecisions, guardLevelOf } from './decision.js'
/**
* 运行生成前置护栏。
*
* @param {object} deps
* @param {import('./rules-check.js').Rules} deps.rules 注入的规则
* @param {GuardPort} [deps.llmGuardPort] 可选注入的 LLM 护栏 Port
* @param {object} [args]
* @param {string} [args.text]
* @param {any} [args.context] 生成上下文(喂给 llmGuardPort)
* @param {string} [args.policy] 二期分级位(一期走默认 policy)
* @param {string} [args.region] 二期区域位(一期透传给 Port,可忽略)
* @returns {Promise<GuardResult>}
*/
export async function runGuardGeneration({ rules, llmGuardPort }, { text, context, policy, region } = {}) {
if (typeof text !== 'string') {
throw new ModerationError({
code: ModerationErrorCode.BAD_REQUEST,
retryable: false,
message: 'guardGeneration 需要 string 类型的 text',
})
}
const resolved = resolveRules(rules, policy)
// ① 本地规则(前置硬红线)。
const local = runRulesCheck({ text, kind: 'generated', rules: resolved })
/** @type {Array<{decision?:string, reasons?:string[], categories?:string[]}>} */
const sources = [{ decision: local.decision, reasons: local.reasons, categories: [] }]
const hasLlm = Boolean(llmGuardPort && typeof llmGuardPort.classify === 'function')
// ② LLM 基础安全类目(仅当注入了 Port)。
if (hasLlm && llmGuardPort) {
let cls
try {
cls = await llmGuardPort.classify({ text, context, region })
} catch (err) {
throw new ModerationError({
code: ModerationErrorCode.GUARD_PORT_FAILED,
retryable: true,
cause: err,
message: `llmGuardPort.classify 调用失败: ${err instanceof Error ? err.message : String(err)}`,
})
}
if (!cls || typeof cls !== 'object') {
throw new ModerationError({
code: ModerationErrorCode.GUARD_PORT_BAD_SHAPE,
retryable: false,
message: 'llmGuardPort.classify 必须返回 { categories, unsafe, reasons }',
})
}
const categories = Array.isArray(cls.categories) ? cls.categories : []
const llmReasons = Array.isArray(cls.reasons) ? cls.reasons : []
// unsafe=true → block(前置阻断回流);否则该路 allow。
sources.push({
decision: cls.unsafe ? 'block' : 'allow',
reasons: llmReasons,
categories,
})
}
// ③ 取最严合并。
const merged = mergeDecisions(sources)
return {
decision: merged.decision,
reasons: merged.reasons,
categories: merged.categories,
guardLevel: guardLevelOf(hasLlm),
}
}