feat(config,runtime): 上下文预算按模型窗口推导 + usage/缓存可观测 + sidecar 存全文(T1)
- 窗口声明: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)。
This commit is contained in:
1 parent
5b2b385524
commit
72b95df22b
39 files changed
+2496
-377
No files matched your search
+77
-10
@@ -1,16 +1,83 @@
|
||||
package main
|
||||
|
||||
import "git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
// contextBudget 返回循环的上下文溢出压缩预算(CONTEXT_MAX_TOKENS,
|
||||
// design/04 §1.3)。**非正 = 关闭压缩**,行为与本条接线前逐字节一致;
|
||||
// 未配置时由 config 层给出 DefaultContextMaxTokens(即默认启用)。
|
||||
"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。
|
||||
//
|
||||
// 取值只从配置来,组合根不做二次猜测:阈值语义(含「0 关掉」)由配置层
|
||||
// 一处定义,组合根只负责把它交给 loop.Options.MaxContextTokens。
|
||||
func (a *app) contextBudget() int {
|
||||
if a.cfg == nil {
|
||||
return config.DefaultContextMaxTokens
|
||||
// 为什么推导放在这里而不是配置层:窗口与 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
|
||||
}
|
||||
return a.cfg.Context.MaxTokens
|
||||
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}
|
||||
}
|
||||
Reference in new issue
Block a user