package main import "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/guardrail" // 组合根的默认护栏策略(design/04 §5)。本条修的就是「护栏是空壳」: // 此前 `guardrail.New()` 不带任何 Option,管线里一个检查都没有, // 输入/输出两侧的 guardrail span 每次都只是走过场。 // // 三条默认策略与它们的依据: // // 1. 敏感信息 = **阻断**档。API key / bearer token / 密码 / 私钥这类东西 // 一旦进了请求或回复就是既成事实的泄露,标记档(放行 + 记一笔)救不回来, // 因此默认按阻断处理。 // 2. 长度上限 = 只拦病态载荷,不误伤正常对话(两个上限的取值依据见下方常量)。 // 3. 关键词表 = **只在配置提供时**挂。项目不内置任何词表:凭空猜的词只会 // 误伤正常对话,且没有部署方能为它的命中负责(见 config.GuardrailConfig)。 const ( // guardrailMaxInputChars 是请求侧的字符上限。这个检查跑在**组装完成的 // 请求**上(系统提示词 + 记忆块 + 整段对话 + 工具结果),不是单条用户 // 消息,因此上限必须按「整个上下文」来定:默认压缩预算 32K token // 折算成字符约 65K(中文)~131K(拉丁),取 262144(256 Ki)字符约为 // 它的 2~4 倍 —— 正常对话(哪怕很长)永远碰不到,只有把几百 KB 的 // 整块文本直接灌进来这种病态输入才会被拦下。 guardrailMaxInputChars = 262144 // guardrailMaxOutputChars 是响应侧的字符上限。一条回复正常在几十到几百字, // 长代码/长文也就几千字,65536(64 Ki)字符比正常回复高 2~3 个数量级, // 因此不会截断任何正常输出,只拦失控的生成(例如无限复读)。这一点在主 // 流式路径上尤其重要:那里输出侧检查是异步旁路(只会审计,不挡已产出的 // 回复),但在静音/非流式路径(backthink、子代理)上否判决会替换答案, // 所以上限必须留足余量。 guardrailMaxOutputChars = 65536 ) // buildGuardrailPipeline 组装循环与各 agent 共用的护栏管线(design/04 §5)。 // 它是组合根里唯一一处决定「护栏里到底有什么」的地方,因此测试可以直接 // 断言这个函数的产物,而不必启动整个运行时。 // // 选项顺序即检查顺序(guardrail.New 按序应用),且先跑开销低、语义硬的 // 敏感信息检查:命中即返回该判决,长度与关键词检查不会再跑。 func (a *app) buildGuardrailPipeline() *guardrail.Pipeline { opts := []guardrail.Option{ // ① 敏感信息外泄必须拦:阻断档。 guardrail.WithSensitiveInfo(guardrail.SensitiveBlock), // ② 长度上限:0 会关掉那一侧,因此两侧都给上保守上限。 guardrail.WithLengthLimits(guardrailMaxInputChars, guardrailMaxOutputChars), } // ③ 关键词表仅在配置提供时挂:没有配置就不挂(KeywordGuard 空表本也 // 全放行,这里显式分支是为了让「没配置」在管线上就是没有这个检查)。 if keywords := a.guardrailKeywords(); len(keywords) > 0 { opts = append(opts, guardrail.WithKeywords(keywords...)) } return guardrail.New(opts...) } // guardrailKeywords 返回部署方声明的内容策略词表(GUARDRAIL_KEYWORDS); // 未配置时返回 nil,管线里就不含关键词检查。 func (a *app) guardrailKeywords() []string { if a.cfg == nil { return nil } return a.cfg.Guardrail.Keywords }