- 窗口声明:models.json 组级 contextWindow + env LLM_CONTEXT_WINDOW(per-group materialize 播种,0 不写盘、负数报错)。 - 预算三态:CONTEXT_MAX_TOKENS 未设置 ⇒ 按窗口推导;>0 显式覆盖;0|负 关闭压缩(不变)。推导 budget = min(窗口×比例, 窗口−输出保留),比例默认 2/3(CONTEXT_BUDGET_RATIO,1M ⇒ 699050 tokens ≈ 683Ki);输出保留 = max(32768, 该组 maxTokens) 且封顶窗口/2(防配置矛盾时退化成 1 token);窗口未声明 ⇒ 回落 131072 + 恰好一条 WARN。接线在组合根 wire_context.go(唯一决策点)。 - usage/缓存可观测(此前**完全不可观测**):Response 增 CacheHit/CacheMiss/Reasoning tokens + CacheHitRate;解析 OpenAI 兼容(prompt_cache_hit_tokens/prompt_cache_miss_tokens/cached_tokens/reasoning_tokens)与 anthropic(cache_read_input_tokens/cache_creation_input_tokens)两种形状;记录点在适配器 record(换入口不丢成本日志),日志无凭据;单次输入 ≥预算 90% 记 WARN(不阻断)。 - 根治清单「续读」假承诺:抽取期不再按注入预算截断(sidecar 存全文,只受 MaxSidecarBytes=8 MiB 安全上限,ExtractorVersion v1→v2 旧 sidecar 失效重抽);注入预算改在装配期生效 ⇒ ref.Chars/Truncated 是注入侧事实、游标真是继续读的起点;document-read 的 limit 上限改为独立具名常量。 - 顺手修越界:config.go 505 行(超 500)拆出 llm.go。 - 主会话独立验收:隔离 worktree(HEAD + 本切片 39 文件,排除在途 gateway)build/vet 绿、全树 59 包 0 FAIL、linux+darwin 交叉编译绿;变异独立复现(比例改 0.25 ⇒ 推导用例红 262144 vs 699050)。
84 lines
3.9 KiB
Go
84 lines
3.9 KiB
Go
package main
|
||
|
||
import (
|
||
"log/slog"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||
)
|
||
|
||
// 本文件是**上下文预算的唯一决策点**(组合根):把「配置 + 生效模型组」摊平成
|
||
// config.ContextBudgetInput,喂给纯函数 config.DeriveContextBudget,再把结论交给
|
||
// loop.Options.MaxContextTokens 与 loop.Options.DocumentMaxInjectChars。
|
||
//
|
||
// 为什么推导放在这里而不是配置层:窗口与 maxTokens 都在 models.json 的**生效组**
|
||
// 上(面板可编辑、热重载),而 CONTEXT_MAX_TOKENS 在 .env。两个来源的会合点只有
|
||
// 组合根,推导函数本身因此保持纯粹、可表驱动测试。
|
||
//
|
||
// 语义(design/04 §1.3 + 附录 A.3):
|
||
// - CONTEXT_MAX_TOKENS 未设置 ⇒ 按窗口推导 budget = min(窗口 × 比例, 窗口 − 输出保留);
|
||
// - > 0 ⇒ 显式覆盖;0 / 负数 ⇒ 关闭压缩(与接线前一致);
|
||
// - 窗口未声明 ⇒ 回落 config.DefaultModelContextWindow 并记**恰好一条** WARN;
|
||
// - 比例非法(CONTEXT_BUDGET_RATIO)⇒ 记一条带原始取值的 WARN 并回落 2/3。
|
||
|
||
// ctxBudget 是组合根推导出的两项预算:循环的溢出压缩预算与单份附件注入预算。
|
||
type ctxBudget struct {
|
||
// Budget 是压缩预算的完整结论(含推导来源,供日志与断言)。
|
||
Budget config.ContextBudget
|
||
// DocumentInjectChars 是单份附件注入上下文的字符上限(T1 §7.2)。
|
||
DocumentInjectChars int
|
||
}
|
||
|
||
// contextBudget 计算本次运行生效的上下文预算。envPath 是启动时合并的 .env 路径
|
||
// (比例键的读取与其余配置同一套优先级;空串表示不看 .env)。
|
||
//
|
||
// 模型组取**生效组**(models.json 下标 0,或 .env 合成的组):压缩预算与注入预算
|
||
// 都是循环级的单一值,而生效组才是新请求默认用的那个。
|
||
func (a *app) contextBudget(envPath string) ctxBudget {
|
||
in := config.ContextBudgetInput{}
|
||
docOverride := 0
|
||
if a.cfg != nil {
|
||
in.Override = a.cfg.Context.MaxTokens
|
||
in.OverrideConfigured = a.cfg.Context.MaxTokensSet
|
||
docOverride = a.cfg.Context.DocumentMaxInjectChars
|
||
}
|
||
if a.llm != nil {
|
||
g := a.llm.Group()
|
||
in.Window = g.ContextWindow
|
||
in.GroupMaxTokens = g.MaxTokens
|
||
}
|
||
return deriveContextBudget(sourcedWiringLog(a.log), envPath, in, docOverride)
|
||
}
|
||
|
||
// deriveContextBudget 是 contextBudget 的可测内核:取值来源收成显式参数,
|
||
// 日志与「恰好一条 WARN」也在这里(测试用捕获型 handler 断言条数)。
|
||
func deriveContextBudget(log *slog.Logger, envPath string, in config.ContextBudgetInput, docOverride int) ctxBudget {
|
||
ratio, err := config.LoadContextBudgetRatio(envPath)
|
||
if err != nil {
|
||
// 非法比例:记一条带原始取值的 WARN 并回落默认(AGENTS §10:不许静默失效)。
|
||
log.Warn("context budget ratio invalid, falling back",
|
||
"env", config.ContextBudgetRatioEnv, "error", err,
|
||
"fallback_ratio", config.DefaultContextBudgetRatio)
|
||
ratio = config.DefaultContextBudgetRatio
|
||
}
|
||
in.Ratio = ratio
|
||
budget := config.DeriveContextBudget(in)
|
||
// 单份附件注入预算:显式覆盖优先,否则随推导出的预算走。
|
||
docChars := docOverride
|
||
if docChars <= 0 {
|
||
docChars = loop.DocumentInjectCharsFor(budget.Tokens)
|
||
}
|
||
if budget.Mode == config.ContextBudgetDerived && !budget.WindowDeclared {
|
||
// 窗口未声明:**恰好一条** WARN(每次启动只推导一次)。
|
||
log.Warn("model context window not declared, falling back",
|
||
"env", "LLM_CONTEXT_WINDOW", "json_key", "contextWindow",
|
||
"window", budget.Window, "hint", "声明窗口才能吃满模型的真实窗口")
|
||
}
|
||
log.Info("context budget",
|
||
"mode", string(budget.Mode), "tokens", budget.Tokens,
|
||
"window", budget.Window, "window_declared", budget.WindowDeclared,
|
||
"ratio", budget.Ratio, "output_reserve", budget.OutputReserve,
|
||
"document_inject_chars", docChars)
|
||
return ctxBudget{Budget: budget, DocumentInjectChars: docChars}
|
||
}
|