Files
Cyrene_v2/internal/config/guardrail.go
T
AskaEth 51c1672355
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
fix(guardrail): 输入护栏上限按推导预算派生(修「大附件被拦死」)+ 第 5 条 flake
- 真 bug:文档注入预算(1M 窗口 ⇒ 349525 字符)> 输入护栏硬编码上限(262144)⇒ 带 744KB 附件的 POST /chat 直接 input_length_exceeded/guardrail_input,整个回合作废(主会话活服务实测)。

- 修法:guardrailMaxInputChars → guardrailMinInputChars(256Ki 下限) + 纯函数 deriveGuardrailInputLimit(log, override, budgetTokens, docInjectChars):派生值 = max(下限, 推导预算 tokens)(1M ⇒ 699050);新增 env GUARDRAIL_MAX_INPUT_CHARS(未设/0=派生、>0=覆盖、<0=WARN 回落);预算只在 wire_context 推导一次并经 buildGuardrailPipeline(ctxBudget) 传入(避免「窗口未声明」WARN 记两遍);输出上限 65536 刻意不参与派生(有显式回归钉住)。

- 关系不变量:文档注入预算 ≤ 输入上限,人为破坏 ⇒ 一条 WARN(不静默)。

- 第 5 条 flake(exec/TestTerminal_Timeout_Killed):仅改测试、terminal.go 保持 pristine;根因不是「杀进程失效」而是旧断言的 ~200ms 精度把宿主负载下的进程创建耗时算了进去(重压下 cmd.Start 会直接返回 deadline exceeded,红行却报 process not killed)⇒ 改为「控制组证明同命令不设短超时正常退出」的确定性证明 + 把墙钟断言降级为 10s 语义判别界(子进程自然存活期拉到 30s)。

- 主会话独立验收:隔离 worktree(G2 的 10 个文件,排除 M1 在途的 wire_channels.go)build/vet 绿、全树 59 包 0 FAIL、双平台交叉编译绿、Guardrail* 全 PASS、exec -count=20 绿;变异独立复现(把派生值退回硬编码 ⇒ 4 条红,含 budget=699050 → 262144)。

- 残留缺口(已排 G2b):护栏量的是整段装配请求、注入预算是单份 ⇒ ≥2 份各撞注入预算的大附件仍可能被拦。
2026-10-04 19:15:12 +08:00

47 lines
2.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
// 本文件承载内容护栏里**由部署方声明**的那一部分(design/04 §5)。
//
// 敏感信息阻断档是代码内定的安全默认值(见组合根的 buildGuardrailPipeline),
// 不对外开放配置:它不该被一次误配置关掉。这里放的是两项**与部署环境相关**的
// 取值 —— 内容策略词表,以及请求侧字符上限的显式覆盖。
//
// 请求侧字符上限为什么必须能配:它的默认值由**推导出的上下文预算**派生
// (预算被抬高到 1M 窗口能吃的量级后,硬编码的 256 Ki 字符反而成了瓶颈,
// 会把「分享大文件」整条主线打成 guardrail_input 结束,见
// cmd/cyrene/wire_guardrail.go 的说明)。派生值已经能自适应窗口,本键只是
// 给需要收紧或放宽的部署留一个显式落点。
// GuardrailMaxInputCharsEnv 是请求侧字符上限的配置键名。
//
// 它必须与 GuardrailConfig.MaxInputChars 的 env 标签**逐字一致**:WARN 文案要能
// 直接点出「改哪个键」,因此键名只能有一处出处(一致性由 guardrail_test.go 钉住)。
const GuardrailMaxInputCharsEnv = "GUARDRAIL_MAX_INPUT_CHARS"
// GuardrailConfig 承载内容护栏里由部署方声明的取值(design/04 §5)。
type GuardrailConfig struct {
// Keywords 是内容策略关键词表(GUARDRAIL_KEYWORDS,逗号分隔)。
//
// **为空时不挂关键词护栏**:项目绝不硬编一张词表 —— 凭空猜出来的词
// 只会误伤正常对话,且没有任何部署方能对它的命中负责。只有显式声明过
// 的词才会被拦(大小写不敏感的子串匹配)。
Keywords []string `env:"GUARDRAIL_KEYWORDS"`
// MaxInputChars 是**请求侧**字符上限的显式覆盖(GUARDRAIL_MAX_INPUT_CHARS)。
// 三态:
//
// - 未设置 / 0(默认)= 按推导出的上下文预算派生:
// 输入上限 = max(256 Ki, 推导预算 token 数)(1 字符 ≈ 1 token 的保守口径);
// - > 0 = 显式覆盖(原样使用);
// - < 0 = 非法:记一条 WARN 并回落派生值,绝不静默生效(AGENTS §10)。
//
// 派生态下「文档注入预算 ≤ 输入上限」这条关系不变量恒成立(1M 窗口 ⇒
// 注入 349525 字符 ≤ 上限 699050 字符);显式覆盖把它设得比注入预算小时,
// 组合根记一条 WARN 明说「大附件会被护栏拦下」,而不是等到用户上传大文件
// 时才以一个没有上下文的 input_length_exceeded 表现出来。
//
// 它只动**输入**侧:输出上限(65536)防的是模型输出失控,与上下文预算无关,
// 因此不随本键变化、也不开放配置。
MaxInputChars int `env:"GUARDRAIL_MAX_INPUT_CHARS"`
}