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
@@ -39,16 +39,18 @@ const (
|
||||
// 这两处是成对契约,改一处必须改两处,否则模型照清单调用会拿到「未知工具」。
|
||||
documentReadToolName = "document-read"
|
||||
|
||||
// defaultDocumentReadChars 是 limit 缺省时的读取字符数。依据:装配期一次注入
|
||||
// 最多 64K 字符(documents.MaxExtractChars),本工具是**按需**取用,单次 8000
|
||||
// 字符(≈2–3K token)是「读完整一段」与「别让一次工具往返吃掉整个上下文」之间的
|
||||
// 折中,与 file(5 MiB)/ web-fetch(2 MiB)同属「有上限的取用」。
|
||||
// defaultDocumentReadChars 是 limit 缺省时的读取字符数。本工具是**按需**取用,
|
||||
// 单次 8000 字符(≈2–3K token)是「读完整一段」与「别让一次工具往返吃掉整个
|
||||
// 上下文」之间的折中,与 file(5 MiB)/ web-fetch(2 MiB)同属「有上限的取用」。
|
||||
defaultDocumentReadChars = 8000
|
||||
|
||||
// maxDocumentReadChars 是 limit 的上限 = **抽取期预算**(sidecar 正文的物理上限)。
|
||||
// 它直接取抽取器的常量而不是再写一个字面量:正文在抽取期已被截到
|
||||
// documents.MaxExtractChars,再大的请求都读不到更多,因此上限就是它。
|
||||
maxDocumentReadChars = documents.MaxExtractChars
|
||||
// maxDocumentReadChars 是单次 limit 的**独立上限**(64K 字符)。
|
||||
//
|
||||
// 它刻意不再复用 documents.MaxExtractChars(切片 T1 §7.2 第 3 条):那个常量
|
||||
// 已随「sidecar 存全文」作废,而且「一次工具往返能取多少」与「sidecar 有多长」
|
||||
// 本来就是两件事 —— sidecar 现在可以有 8 MiB,一次读 8 MiB 进上下文显然不合
|
||||
// 适。模型要继续读就按 nextOffset 翻页:上限约束的是**一次往返**,不是总量。
|
||||
maxDocumentReadChars = 64 * 1024
|
||||
|
||||
// documentReadNoReason 是 sidecar 说「不可读」却没给出原因时的兜底文案:不静默,
|
||||
// 也不编造一个具体原因。
|
||||
@@ -87,14 +89,16 @@ func (d *documentReadTool) Name() string { return documentReadToolName }
|
||||
// Description implements sdk.Tool. It documents the coordinate system, because the
|
||||
// model only sees this text plus the attachment list line that pointed it here.
|
||||
func (d *documentReadTool) Description() string {
|
||||
return "Read a slice of an already-extracted document (an attachment) by character offset. " +
|
||||
"Use it with the attachment list line: that line prints a continuation cursor 'offset=N' when the " +
|
||||
"injected text was cut, and 'offset' here is that same 0-based character offset into the extracted " +
|
||||
"text (the CSV/TSV summary line, when present, is part of it). Each result reports totalChars and, " +
|
||||
"when more text is available after this slice, truncated=true plus nextOffset to continue from. " +
|
||||
"Only server-issued document names from the attachment list can be read; nothing is re-extracted, " +
|
||||
"so the extracted text is the same text that was injected. Note that the extraction itself is " +
|
||||
"capped: when extractTruncated is true the extracted text ends there but the original document is longer."
|
||||
return fmt.Sprintf("Read a slice of an already-extracted document (an attachment) by character offset. "+
|
||||
"Use it with the attachment list line: that line prints a continuation cursor 'offset=N' when the "+
|
||||
"injected text was cut, and 'offset' here is that same 0-based character offset into the extracted "+
|
||||
"text (the CSV/TSV summary line, when present, is part of it). Each result reports totalChars and, "+
|
||||
"when more text is available after this slice, truncated=true plus nextOffset to continue from. "+
|
||||
"Only server-issued document names from the attachment list can be read, and nothing is ever "+
|
||||
"re-extracted, so the extracted text is the very text the injection was cut from. Two caps are "+
|
||||
"separate: this call returns at most 'limit' characters (default %d, capped at %d), while the "+
|
||||
"extraction itself is bounded by a safety limit - when extractTruncated is true the extracted text "+
|
||||
"ends there and the original document is longer.", defaultDocumentReadChars, maxDocumentReadChars)
|
||||
}
|
||||
|
||||
// Parameters implements sdk.Tool.
|
||||
@@ -137,10 +141,11 @@ type documentReadResult struct {
|
||||
Truncated bool `json:"truncated"`
|
||||
// NextOffset 是续读游标(= Offset + 本次返回的字符数);没有更多内容时为 0 并省略。
|
||||
NextOffset int `json:"nextOffset,omitempty"`
|
||||
// ExtractTruncated 报告**抽取期**是否已按预算截断(sidecar 的 truncated):为 true 时
|
||||
// 「正文到此为止」不等于「原文件到此为止」—— 预算之外的内容不在 sidecar 里
|
||||
//(design/24 §9.6 第 3 条:64K 预算落在抽取期)。这条事实必须让模型看得见,
|
||||
// 否则它会拿「读到末尾」当成「文件只有这么长」。
|
||||
// ExtractTruncated 报告**抽取期**是否已撞上安全上限(sidecar 的 truncated,8 MiB
|
||||
// 文本):为 true 时「正文到此为止」不等于「原文件到此为止」—— 上限之外的内容
|
||||
// 不在 sidecar 里。这条事实必须让模型看得见,否则它会拿「读到末尾」当成
|
||||
// 「文件只有这么长」。注意它与「本回合装配期有没有被注入预算截断」是两件事:
|
||||
// 后者体现在清单行的续读游标上,本字段不受它影响。
|
||||
ExtractTruncated bool `json:"extractTruncated,omitempty"`
|
||||
// Text 是本次读到的正文(sidecar 第一行元数据之后的部分)。
|
||||
Text string `json:"text"`
|
||||
@@ -232,8 +237,7 @@ func documentReadWindow(m map[string]any) (int, int, error) {
|
||||
|
||||
// documentReadSlice 返回 s 从第 offset 个字符起的至多 limit 个字符,以及其后是否还有
|
||||
// 内容。单位是**字符(rune)**而不是字节 —— 与清单行的续读游标同一口径(loop 给的
|
||||
// 是「已注入的字符数」),因此多字节字符既不会被算错,也不会被切成两半。s 的长度受
|
||||
// 抽取预算封顶(documents.MaxExtractChars),故这里的一次转换是有界的。
|
||||
// 是「已注入的字符数」),因此多字节字符既不会被算错,也不会被切成两半。
|
||||
func documentReadSlice(s string, offset, limit int) (string, bool) {
|
||||
runes := []rune(s)
|
||||
if offset >= len(runes) {
|
||||
|
||||
Reference in new issue
Block a user