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
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
@@ -18,16 +19,24 @@ import (
|
||||
// 2. **清单恒在**:每份文档至少要留下「名、MIME、大小、结论」这一行,
|
||||
// 绝不出现「传上去了但模型一无所知」的空上下文(§4.2-3);
|
||||
// 3. **失败必须可见**:抽取失败解析为诚实的文本标记 + status=unreadable,
|
||||
// 回合照常继续(§4.4)。
|
||||
// 回合照常继续(§4.4);
|
||||
// 4. **注入预算在这里生效**(切片 T1 §7.2):正文按本回合的注入预算
|
||||
// (l.documentInjectChars)截取,ref.Chars = 实际注入的字符数、
|
||||
// ref.Truncated = 注入是否被截断。sidecar 存的是抽取全文,因此清单行给出的
|
||||
// 续读游标从此**真的是**「继续读的起点」,与 document-read 的 offset 对齐。
|
||||
//
|
||||
// 它作用于**装配后的模型可见视图**而不是消息窗口:窗口(checkpoint 快照、
|
||||
// 记忆提取输入)里只留文档引用(§2.4),正文每次装配时按引用读 sidecar 得到。
|
||||
const (
|
||||
// documentInjectedCharsCap 是单份文档注入上下文的字符上限(design/24 §3.4
|
||||
// 的 64K 字符)。它与抽取器的 documents.MaxExtractChars **同值同义**:这里
|
||||
// 不重复实现截断(截断由抽取器完成并写进 sidecar,供后续回合复用),只在
|
||||
// 压缩估算里按同一口径计入文档部件的代价。两个包各自独立,避免反向依赖。
|
||||
documentInjectedCharsCap = 64 * 1024
|
||||
// defaultDocumentInjectChars 是单份文档注入上下文的**字符**上限的缺省值
|
||||
// (64K 字符)。它的实际取值由组合根按推导出的上下文预算给出
|
||||
// (Options.DocumentMaxInjectChars / DocumentInjectCharsFor):64K 是
|
||||
// 「小窗口下不退化」的下限,不再兼任抽取期的截断预算 —— 抽取期只受
|
||||
// documents.MaxSidecarBytes 这个安全上限约束(切片 T1 §7.2)。
|
||||
//
|
||||
// 它同时是压缩估算里文档部件代价的封顶依据(compactor.go 的
|
||||
// documentPartTokens):注入多少,就按多少计入 token。
|
||||
defaultDocumentInjectChars = 64 * 1024
|
||||
|
||||
// documentMinTokens 是清单行(名/MIME/大小/结论)的粗略 token 下限:无论
|
||||
// 文档多小、甚至完全读不到,清单行都进了上下文,因此它不是免费的。
|
||||
@@ -40,8 +49,35 @@ const (
|
||||
// bytesPerKiB / bytesPerMiB 是清单行渲染大小用的单位(仅展示用)。
|
||||
bytesPerKiB = 1024
|
||||
bytesPerMiB = 1024 * bytesPerKiB
|
||||
|
||||
// documentInjectBudgetShare 是单份附件注入正文可占推导预算的份数(1/份数):
|
||||
// 一个回合最多 5 个附件(design/24 §3.4),取 8 意味着「每份最多占预算的
|
||||
// 1/8」—— 5 份合计不超过 5/8,其余留给系统提示词、历史、工具 schema 与本轮
|
||||
// 输出。字符与 token 的换算沿用循环的离线估算口径(charsPerToken)。
|
||||
documentInjectBudgetShare = 8
|
||||
)
|
||||
|
||||
// DocumentInjectCharsFor 由推导出的上下文预算(token)算出**单份附件**注入
|
||||
// 模型可见上下文的字符预算(design/24 §9.6 连带项;切片 T1 §7.2 第 2 条)。
|
||||
//
|
||||
// 这是「注入预算随上下文预算推导」这条要求的落点:1M 窗口 × 2/3 ⇒ 699050 token
|
||||
// 的预算 ⇒ 单份附件可注入 349525 字符,而不是旧的 64K 卡死。取值下限是历史默认
|
||||
// 64K(小窗口下附件行为不退化),并且绝不超过预算本身(预算被显式设得很小时,
|
||||
// 也不该让一份附件就吃掉整个窗口)。
|
||||
func DocumentInjectCharsFor(budgetTokens int) int {
|
||||
if budgetTokens <= 0 {
|
||||
return defaultDocumentInjectChars
|
||||
}
|
||||
chars := budgetTokens * charsPerToken / documentInjectBudgetShare
|
||||
if chars < defaultDocumentInjectChars {
|
||||
chars = defaultDocumentInjectChars
|
||||
}
|
||||
if chars > budgetTokens {
|
||||
chars = budgetTokens
|
||||
}
|
||||
return chars
|
||||
}
|
||||
|
||||
// 清单行与结论文案(design/24 §4.3、§4.4)。清单行恒在,结论文案随结论变化:
|
||||
// 已抽取(可含截断与续读游标)、仅清单、不可读。
|
||||
const (
|
||||
@@ -124,7 +160,7 @@ func (l *Loop) resolveDocumentMessage(ctx context.Context, m model.Message) (mod
|
||||
content = append(content, part)
|
||||
continue
|
||||
}
|
||||
ref, body := documentResolution(part, refs, bodies, failed)
|
||||
ref, body := documentResolution(part, refs, bodies, failed, l.documentInjectChars)
|
||||
content = append(content, model.Content{Type: model.ContentTypeText, Text: documentBlock(part, ref, body)})
|
||||
out = append(out, ref)
|
||||
}
|
||||
@@ -152,7 +188,12 @@ func (l *Loop) documentResolutions(ctx context.Context, m model.Message) (map[st
|
||||
// 状态口径:解析器给出的三态照用;缺失时 —— 解析器整个缺席(功能未接线)算
|
||||
// listed(「只给了清单」是事实),而解析器**报了错**(整条消息解析不了)算
|
||||
// unreadable(作者「读不到正文」比「只给了清单」更准确,也让失败对客户端可见)。
|
||||
func documentResolution(part model.Content, refs map[string]history.DocumentRef, bodies map[string]string, failed bool) (history.DocumentRef, string) {
|
||||
//
|
||||
// 注入预算(injectChars)在这一步生效:已抽取的正文按 rune 截到 injectChars,
|
||||
// ref.Chars 记**实际注入**的字符数、ref.Truncated 记**注入**是否被截断。抽取侧
|
||||
// 自己那套 chars/truncated(sidecar 正文长度、是否撞上安全上限)不是客户端关心
|
||||
// 的东西,读侧另有出口(document-read 的 totalChars / extractTruncated)。
|
||||
func documentResolution(part model.Content, refs map[string]history.DocumentRef, bodies map[string]string, failed bool, injectChars int) (history.DocumentRef, string) {
|
||||
ref := history.DocumentRef{Name: part.DocumentName, MIME: part.DocumentMIME, Size: part.DocumentSize}
|
||||
if got, ok := refs[part.DocumentName]; ok {
|
||||
ref.Status, ref.Chars, ref.Truncated = got.Status, got.Chars, got.Truncated
|
||||
@@ -168,7 +209,13 @@ func documentResolution(part model.Content, refs map[string]history.DocumentRef,
|
||||
if ref.Status == history.DocumentStatusUnreadable && body == "" {
|
||||
body = documentUnreadableBody
|
||||
}
|
||||
return ref, body
|
||||
if ref.Status != history.DocumentStatusExtracted {
|
||||
return ref, body
|
||||
}
|
||||
injected, truncated := truncateRunesAt(body, injectChars)
|
||||
ref.Chars = utf8.RuneCountInString(injected)
|
||||
ref.Truncated = truncated
|
||||
return ref, injected
|
||||
}
|
||||
|
||||
// documentStatusKnown 报告 status 是不是三个已知取值之一。unknown 一律按
|
||||
@@ -204,7 +251,8 @@ func documentBlock(part model.Content, ref history.DocumentRef, body string) str
|
||||
|
||||
// documentStatusNote 渲染清单行里的结论部分。截断时给出**续读游标**(已注入
|
||||
// 的字符数即抽取正文里的偏移量),使模型与后续的 document-read 工具知道从哪
|
||||
// 继续(design/24 §4.3:截断绝不静默)。
|
||||
// 继续(design/24 §4.3:截断绝不静默)。因为 sidecar 存的是抽取全文
|
||||
// (切片 T1 §7.2),这个游标**真的**是「继续读的起点」。
|
||||
func documentStatusNote(ref history.DocumentRef) string {
|
||||
switch ref.Status {
|
||||
case history.DocumentStatusExtracted:
|
||||
|
||||
Reference in new issue
Block a user