Files
HomeAgent/internal/agent/core/tokenbudget.go
JianFeeeee 6d38a164eb chore(module): Go module 路径随托管地迁移 gitcode → github
## 背景

仓库早已迁移到 GitHub(`github.com/JianFeeeee/HomeAgent`,CI/CD 都在那边跑),
gitcode 保留为**国内只读镜像**(`ci.yml` 的 sync-gitcode-main 快进推送)。
但 go.mod 的 module 路径一直是 `gitcode.com/JianFeeeee/...` —— 既与权威源
不符,`meta.SourceURL`(AGPL §13 的源码提供入口)也指向镜像而非实际运行的代码。

## 关键区分(这是本次改动安全的前提)

两个 module 是**不同**的,影响面差一个数量级:

| module | 谁依赖 | 改它的后果 |
|---|---|---|
| `.../HomeAgent`(主仓) | **只有主仓自己** | 内部重构,零外部影响 |
| `.../homeagent-sdk`(SDK 仓) | 主仓 49 文件 + 所有外部插件 | 需重编插件 |

★ 实测确认:**插件不 import 主仓 module**(`strings qq/plugin.bin | grep HomeAgent` = 0),
只依赖 SDK 仓。而插件与内核的边界是 **RPC 方法名字符串**
(`plugin.init` / `tool.invoke`…,见 proc/protocol.go),**与 Go module 名无关**。
⇒ module 改名**不改变任何 wire 协议**,现网已装的插件照常运行。

SDK 的 GitHub 仓名是 `homeagentsdk`(无连字符),module 路径必须与之精确匹配,
否则 `go get` 会因路径不符失败,故取 `github.com/JianFeeeee/homeagentsdk`。

## 改动

- `go.mod` / `go.sum`:两个 module 路径 + require/replace
- 277 个 Go 文件 + Makefile + 打包脚本的 import 与 ldflags 路径
- SDK 仓 `meta.CoreModule`、`tools/hmapdev` 的 module 声明与工程模板
- 对外文档/链接的仓库地址(README、PLUGIN_DEV、OVERVIEW 等)

## 明确**保留** gitcode 的地方(镜像机制,不能动)

- `GITCODE_TOKEN`、`GC_URL="…@gitcode.com"`(CI 同步 job)
- `gitcode.com/api/v5/...`(release 附件上传)
- git remote `gitcode`(镜像拉取/推送)
- `docs/zh/ci-cd-runbook.md`(整篇讲镜像运维)

## 验证

- `go build ./...` / `go vet ./...` 绿
- `go test ./... -count=1` → 48 包 ok / 0 FAIL
- hmapdev 工具链编译通过(`third_party/homeagent-sdk/tools/hmapdev`)
- 全仓残留检查:module 形态的 `gitcode.com/JianFeeeee` = 0
- 镜像机制抽验:GC_URL / @gitcode / api/v5 / remote 均完好

## 顺带修正

`hmapdev/templates.go` 的注释此前写「gitcode 的模块不在 proxy.golang.org 上」——
module 已迁 GitHub,该论断不再成立。改为如实说明**为何仍默认 replace 到本机 SDK**:
离线可构建、避免版本漂移、内网可用。理由与托管地无关,故 replace 保留。
2026-10-08 10:40:17 +08:00

173 lines
6.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package core
import (
"github.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 / TruncateByTokens 转发到编解码层统一出口。
//
// 本包内调用点很多(context.go / process.go / resident.go / tooldefs.go…),
// 而实现只有一份(api 包,C 化后可选走 C)。保留这两个同名转发,
// 是为了不把调用点全部改写成 api.EstimateTokens —— 那场改动对行为零收益,
// 却把「本包依赖 api」这件事铺得到处都是。
// EstimateTokens 粗略估算 token 数(转发到 api.EstimateTokens)。
func EstimateTokens(text string) int { return api.EstimateTokens(text) }
// TruncateByTokens 截断字符串至不超过 maxTokens 估计值(转发到 api.TruncateByTokens)。
func TruncateByTokens(s string, maxTokens int) string { return api.TruncateByTokens(s, maxTokens) }
// ContextTuning 是上下文预算的**可调参数**(零值 = 历史默认)。
//
// 为何要有它:这些阈值原本全部硬编码在 ComputeTokenBudget 里,
// 配置面板上一个都没有 —— 用户无法为不同窗口(如 200k)调优,
// 也无法做「同窗口下不同策略」的对照实验。
//
// ★ 两个常量叠加会合成一个**没人看得见的分段函数**:
//
// 窗口 ≤ 750000 → 工作面 = 窗口 × 0.8 (80%)
// 窗口 > 750000 → 工作面 = 600000 (利用率退化成 600000/窗口)
//
// 拐点位置由 0.8 与 600000 **共同**决定,改任一个都会挪动它。
// 现在拐点由 MaxTargetTokens 显式表达,而 MaxTargetTokens < 0 表示不封顶
// (利用率对所有窗口都真的等于 UtilizationPercent)。
//
// 零值语义(升级安全):全部字段为 0 时,结果与硬编码时代**逐值相同**。
// 详见 context_tuning_test.go 的 TestContextTuningZeroValueEqualsLegacy。
type ContextTuning struct {
// UtilizationPercent 是目标窗口利用率(百分比,1-100)。
// 0 ⇒ 用历史默认 80。
UtilizationPercent int
// MaxTargetTokens 是**工作区间**的绝对上限(不是模型窗口)。
//
// 0 ⇒ 用历史默认 600000
// < 0 ⇒ 不封顶(工作面纯由 UtilizationPercent 跟随窗口)
// > 0 ⇒ 该值作为上限
//
// 为何不是模型窗口本身:标称窗口 ≠ 有效窗口。接近满窗口时注意力涣散,
// 成本与延迟也随 prompt 线性上升。该源实测 990,034 token 仍能返回,
// 但 600K 才是它的最优工作区间。
MaxTargetTokens int
// MemoryRatioPercent 是记忆上下文占「可用预算」的百分比(1-99)。
//
// 0 ⇒ 用历史默认:available / 3(整数除法,逐值不变)
// 1-99 ⇒ available × pct / 100
//
// ⚠️ 0 特意不换算成 33%:实测 available/3 与 available×33/100 **不等价**
//(1000 时是 333 vs 330),换算会让默认值不再等价。
MemoryRatioPercent int
// ProtectedCount 是裁剪时**无条件保留**的最近事件条数。
//
// 它决定「近处记忆」与「向量检索」的权:条数越大越不容易丢近处信息,
// 越小越依赖检索准确度。0 ⇒ 用历史默认 10。
ProtectedCount int
}
// 历史默认值(硬编码时代的常量,改这里等于改所有未配置实例的行为)。
const (
defaultUtilizationPercent = 80
defaultMaxTargetTokens = 600000
defaultProtectedCount = 10
// legacyMemoryDivisor 是记忆预算的历史除数(available/3)。
legacyMemoryDivisor = 3
)
// ComputeTokenBudget 计算各部分的 token 预算(零值 tuning = 历史行为)。
//
// 保留这个签名是为了不改既有调用点;需要调参的调用方用 ComputeTokenBudgetTuned。
func ComputeTokenBudget(provider api.Provider, systemPromptBase string) TokenBudget {
return ComputeTokenBudgetTuned(provider, systemPromptBase, ContextTuning{})
}
// computeTokenBudget 用本 agent 的配置调参计算上下文预算。
//
// 存在理由:ComputeTokenBudget 是包级函数、拿不到 agent 的配置;
// 若调用点各自传参很容易漏(本仓刚发现 core.agent.max_context_size
// 被注册进面板却从未被读取 —— 一个不报错的死配置)。
// 收成方法后,调用点只需 a.computeTokenBudget()。
func (a *Agent) computeTokenBudget() TokenBudget {
return ComputeTokenBudgetTuned(a.provider, a.systemPrompt, a.ctxTuning)
}
// ComputeTokenBudgetTuned 按配置计算各部分的 token 预算。
//
// 固定部分(system prompt + tools + rules)优先保障,剩余按 MemoryRatioPercent
// 切给 memory context 与 context events。
func ComputeTokenBudgetTuned(provider api.Provider, systemPromptBase string, t ContextTuning) TokenBudget {
// provider 可能为 nil(构造期、或 provider 尚未就绪/已被换掉)。
// 之前直接调 provider.MaxContextTokens() 会 SIGSEGV —— 实测由contextTopK
// 在测试里撞出来,但它同样会在「配置早于 provider 就绪」的启动序列上发生。
var maxCtx int
if provider != nil {
maxCtx = provider.MaxContextTokens()
}
if maxCtx <= 0 {
maxCtx = 32768
}
utilPct := t.UtilizationPercent
if utilPct <= 0 {
utilPct = defaultUtilizationPercent
}
if utilPct > 100 {
utilPct = 100
}
// 整数运算:实测 maxCtx×80/100 与 int(float64(maxCtx)×0.8)
// 在 42 万个窗口值上逐值相同,且不受浮点表示误差影响。
targetUsage := maxCtx * utilPct / 100
// 上限:0=历史默认(600000),负=不封顶。
cap := t.MaxTargetTokens
if cap == 0 {
cap = defaultMaxTargetTokens
}
if cap > 0 && targetUsage > cap {
targetUsage = cap
}
reserved := maxCtx - targetUsage
if reserved < 0 {
reserved = 0
}
fixedTokens := EstimateTokens(systemPromptBase)
available := targetUsage - fixedTokens
if available < 0 {
available = 0
}
memTokens := available / legacyMemoryDivisor
if t.MemoryRatioPercent > 0 {
pct := t.MemoryRatioPercent
if pct > 99 {
pct = 99
}
memTokens = available * pct / 100
}
// 余数归 context:保证 mem + ctx == available(不因取整丢 token)。
ctxTokens := available - memTokens
return TokenBudget{
MaxContext: maxCtx,
TargetUsage: targetUsage,
FixedTokens: fixedTokens,
MemoryTokens: memTokens,
ContextTokens: ctxTokens,
Reserved: reserved,
}
}
// TruncateByTokens 已移至 api 包(编解码层统一出口,见 codec.go)。
// 上方已有同名转发,此处不再重复定义。