fix(guardrail): 输入上限与附件注入预算统一换算(多附件不再被拦)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 残留缺口:护栏量整段装配请求(字符),注入预算是单份 ⇒ 1M 窗口下 2×349525 就顶到旧上限 699050、5 份必被拦。根因是两套换算(上限按 1char=1token,注入按 loop 的 4char=1token)。

- 修法:上限 = max(262144, 推导预算 tokens × loop.CharsPerToken) ⇒ 1M 窗口 2,796,200 字符(5 份满额注入 1,747,625 有余量);不变量升级为「N 份」(maxChatDocumentsPerTurn=5,带出处注释);WARN 文案升级为多附件口径(max_documents/document_inject_chars/document_inject_total_chars/guardrail_max_input_chars/hint)。

- 同一换算常量复用:在 loop/compactor.go 的既有无导出常量旁加导出别名 loop.CharsPerToken = charsPerToken(2 行、零行为改动、不重命名)—— 这是本切片唯一触及「不要碰」清单的改动,已由主会话复核接受(替代方案是复制一个字面量 4,会重新造出两套换算)。

- 同步陈旧注释(config/guardrail.go、wire_runtime.go 里的旧公式)与 .env.example(新公式 + 多附件口径 + 1M 算例 2796200/1747625),并有「模板 ↔ 代码现算值」的一致性用例。

- 主会话独立验收:隔离 worktree(G2b 的 7 文件,排除 B1/ST1 在途)build/vet 绿、全树 59 包 0 FAIL、护栏用例 17 PASS、双平台交叉编译绿。

- 残留(未修,需裁决):预算关闭或落在 (0,81920) token 的部署,派生态上限 = 262144 而 5×64K = 327680 ⇒ 满载附件回合仍可能被拦(启动会记 WARN,不静默);真正的下限应取 max(262144, maxDocs×每份注入)。
This commit is contained in:
AskaEth committed 2026-10-04 19:41:44 +08:00
1 parent 2a43035f5d
commit 137b3e9eaf
7 files changed
+366 -82

No files matched your search

+13 -6
View File
@@ -226,16 +226,23 @@ SUBAGENT_TIMEOUT_SEC=600
# 单份附件注入模型可见上下文的字符上限(sidecar 仍存全文,只受 8 MiB 安全上限约束)。
# 留空 = 按推导出的预算算:推导预算的 1/8(按一个回合最多 5 个附件留余量),下限 64K 字符。
# 它决定「每回合往上下文里放多少」,也因此决定清单行给的续读游标从哪里开始。
# ⚠️ 它必须**不超过**下面 GUARDRAIL_MAX_INPUT_CHARS 生效的输入上限,否则大附件会在装配完之后
# ⚠️ 它必须**不超过**下面 GUARDRAIL_MAX_INPUT_CHARS 生效的输入上限 —— 严格说是
# 「单份注入 × 单回合附件数(最多 5)」不超过上限,否则满载附件的回合会在装配完之后
# 被输入护栏整回合拦死(input_length_exceeded)。改了本键就顺手核对那一项。
# DOCUMENT_MAX_INJECT_CHARS=
# 请求侧字符上限(输入护栏跑在**组装完成的请求**上:系统提示词 + 记忆块 + 整段对话 + 工具结果)。
# 三种取值:
# · 未设置 / 0(默认)= 由推导出的上下文预算派生:max(262144, 推导预算 token 数)
# (1 字符 ≈ 1 token 的保守口径);1M 窗口 ⇒ 预算 699050 token ⇒ 上限 699050 字符,
# 而单份附件注入预算是 349525 字符 ⇒ 关系不变量「注入预算 ≤ 输入上限」成立,
# 「分享大文件」才走得通。小窗口部署下派生的 token 数低于 262144,行为与旧硬编码一致。
# · > 0 = 显式覆盖(原样使用;调小到注入预算之下会让大附件被拦,届时启动日志里有 WARN)
# · 未设置 / 0(默认)= 由推导出的上下文预算派生:
# 上限 = max(262144, 推导预算 token 数 × 4)
# 那个 4 与附件注入预算用的是**同一个换算常量**(loop.CharsPerToken = 4 字符 ≈ 1 token)。
# 上限量的是整段装配请求,而注入预算是单份的,两边必须是同一套换算,否则**多附件**回合
# 会被拦死(G2b 修的正是这个:旧口径按 1 字符 ≈ 1 token 兑,2 份满额注入就顶到上限):
# 1M 窗口 ⇒ 预算 699050 token ⇒ 上限 2796200 字符;
# 单份附件注入预算 349525 字符 ⇒ 5 份(单回合附件上限)满额注入合计 1747625 字符,
# 关系不变量「单回合全部附件的注入预算合计 ≤ 输入上限」成立,「分享大文件」才走得通。
# 预算关闭(CONTEXT_MAX_TOKENS=0)或预算很小时,上限落回 262144。
# · > 0 = 显式覆盖(原样使用;压到「单份注入 × 单回合附件数」之下会让多附件回合被拦,
# 届时启动日志里有 WARN)
# · < 0 = 非法:记一条 WARN 并回落派生值(绝不静默生效)
# 它只动**输入**侧;输出上限(65536 字符)防的是模型输出失控,与上下文预算无关,不开放配置。
# 敏感信息(API key / bearer token / 密码 / 私钥)为阻断档,同样是代码内定的安全默认值。