mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-23 10:28:06 +00:00
问题:core.llm.model=AUTO,而 ModelContextWindow("auto") 匹配不到任何分支、
掉进 default 32768 —— 该源真实窗口是 1M(实测 990,034 token 的 prompt 通过),
内核却按小 30 倍的窗口算全部预算。
三处改动:
1. ModelContextWindow 补 deepseek-v4/v3 → 1M;推断不出时打日志(静默降级是
这次问题的成因,不能再默默退回一个小值)。
2. sourceFieldDefs 补 context_window 声明:它早已被 readInt 读进 LLMSource
并透传到 provider,但没进这张表 ⇒ WebUI 里看不见也改不了。
3. ComputeTokenBudget 新增 maxTargetTokens=600000:窗口 1M 不等于按 838K
(80%)干活。标称窗口≠有效窗口,600K 是该源最优工作区间,所以把
「窗口上限」(会不会被上游拒)与「工作区间」(预算分配)分开。
配套 core.llm.max_tokens 4096→32768(实测长输出样本达 18,272 token,
16384 仍会截断;上限是 cap 不是目标,短问答零成本)。
测试:tokenbudget_test.go 钉死封顶生效且小窗口不受影响;
context_window_test.go 钉死推断值与显式声明优先级。
104 lines
3.1 KiB
Go
104 lines
3.1 KiB
Go
package core
|
||
|
||
import (
|
||
"unicode/utf8"
|
||
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||
)
|
||
|
||
// TokenBudget 上下文 token 预算分配结果
|
||
type TokenBudget struct {
|
||
MaxContext int // 模型窗口上限
|
||
TargetUsage int // 目标使用量(受 maxTargetTokens 与 utilizationRate 共同约束)
|
||
FixedTokens int // 固定部分(system prompt base + tools + rules)
|
||
MemoryTokens int // memory context 可用预算
|
||
ContextTokens int // 上下文事件可用预算
|
||
Reserved int // 预留(response 空间)
|
||
}
|
||
|
||
// EstimateTokens 粗略估算 token 数
|
||
// 中文 ~1.5 token/字,英文 ~0.3 token/字符
|
||
// 保守估计取 max(1, runeCount * 2),对混合文本足够安全
|
||
func EstimateTokens(text string) int {
|
||
if text == "" {
|
||
return 0
|
||
}
|
||
runeCount := utf8.RuneCountInString(text)
|
||
if runeCount == 0 {
|
||
return 0
|
||
}
|
||
t := runeCount * 2
|
||
if t < 1 {
|
||
return 1
|
||
}
|
||
return t
|
||
}
|
||
|
||
// maxTargetTokens 是**有效工作区间**的上限(不是模型窗口)。
|
||
//
|
||
// 为什么窗口 1M 却不能按 800K 干活:标称窗口 ≠ 有效窗口。接近满窗口时注意力
|
||
// 明显涣散、成本与延迟也随 prompt 线性上升。该源(llmsproxy)实测 990,034 token
|
||
// 仍能返回,但 600K 才是它的最优工作区间 —— 超过这个量级,回答质量与延迟都不划算。
|
||
//
|
||
// 因此把「窗口上限」(判断请求会不会被上游拒)与「工作区间」(分配记忆/历史预算)
|
||
// 分开:前者由 provider.MaxContextTokens() 给,后者封顶在这里。
|
||
const maxTargetTokens = 600000
|
||
|
||
// ComputeTokenBudget 计算各部分的 token 预算
|
||
// utilizationRate 为目标窗口利用率(0.0-1.0),预留 1-utilizationRate 给 response
|
||
// 固定部分优先保障,剩余预算 1:2 分配给 memory context 和 context events
|
||
func ComputeTokenBudget(provider api.Provider, systemPromptBase string) TokenBudget {
|
||
maxCtx := provider.MaxContextTokens()
|
||
if maxCtx <= 0 {
|
||
maxCtx = 32768
|
||
}
|
||
|
||
utilizationRate := 0.8
|
||
targetUsage := int(float64(maxCtx) * utilizationRate)
|
||
// 窗口很大(如 1M)时不要把 80% 当成工作面:按 maxTargetTokens 封顶。
|
||
if targetUsage > maxTargetTokens {
|
||
targetUsage = maxTargetTokens
|
||
}
|
||
reserved := maxCtx - targetUsage
|
||
if reserved < 0 {
|
||
reserved = 0
|
||
}
|
||
|
||
fixedTokens := EstimateTokens(systemPromptBase)
|
||
|
||
available := targetUsage - fixedTokens
|
||
if available < 0 {
|
||
available = 0
|
||
}
|
||
|
||
// memory context 占 1/3,context events 占 2/3
|
||
memTokens := available / 3
|
||
ctxTokens := available - memTokens
|
||
|
||
return TokenBudget{
|
||
MaxContext: maxCtx,
|
||
TargetUsage: targetUsage,
|
||
FixedTokens: fixedTokens,
|
||
MemoryTokens: memTokens,
|
||
ContextTokens: ctxTokens,
|
||
Reserved: reserved,
|
||
}
|
||
}
|
||
|
||
// TruncateByTokens 截断字符串至不超过 maxTokens 估计值
|
||
func TruncateByTokens(s string, maxTokens int) string {
|
||
if maxTokens <= 0 || s == "" {
|
||
return ""
|
||
}
|
||
runes := []rune(s)
|
||
if len(runes)*2 <= maxTokens {
|
||
return s
|
||
}
|
||
// 从开头保留 maxTokens/2 个字符(每个字符约 2 token)
|
||
keep := maxTokens / 2
|
||
if keep >= len(runes) {
|
||
return s
|
||
}
|
||
return string(runes[:keep])
|
||
}
|