mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
## 背景 仓库早已迁移到 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 保留。
167 lines
5.5 KiB
Go
167 lines
5.5 KiB
Go
package core
|
||
|
||
import (
|
||
"testing"
|
||
|
||
agentAPI "github.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||
)
|
||
|
||
// 会话级用量累计:跨调用求和,且命中率口径必须正确。
|
||
//
|
||
// ── 为什么需要 ──
|
||
//
|
||
// 单次用量一直在(StageCtx、LLM chain 事件),但没有任何地方把多次调用加起来,
|
||
// 于是「这个会话花了多少、缓存省了多少」答不出来。这是「无法统计缓存命中」
|
||
// 的另一半:数据流出来了,却没有落点。
|
||
|
||
// TestLedgerSumsAcrossCalls 跨调用求和。
|
||
func TestLedgerSumsAcrossCalls(t *testing.T) {
|
||
var l usageLedger
|
||
l.record(agentAPI.TokenUsage{
|
||
Prompt: 1000, Completion: 50, Total: 1050,
|
||
CacheRead: 768, CacheReported: true,
|
||
})
|
||
l.record(agentAPI.TokenUsage{
|
||
Prompt: 2000, Completion: 80, Total: 2080,
|
||
CacheRead: 1024, CacheMiss: 256, CacheReported: true,
|
||
ReasoningTokens: 300,
|
||
})
|
||
|
||
got := l.snapshot()
|
||
if got.Calls != 2 {
|
||
t.Errorf("Calls = %d,期望 2", got.Calls)
|
||
}
|
||
if got.Prompt != 3000 {
|
||
t.Errorf("Prompt = %d,期望 3000", got.Prompt)
|
||
}
|
||
if got.Completion != 130 {
|
||
t.Errorf("Completion = %d,期望 130", got.Completion)
|
||
}
|
||
if got.Total != 3130 {
|
||
t.Errorf("Total = %d,期望 3130", got.Total)
|
||
}
|
||
if got.CacheRead != 1792 {
|
||
t.Errorf("CacheRead = %d,期望 1792(768+1024)", got.CacheRead)
|
||
}
|
||
if got.Reasoning != 300 {
|
||
t.Errorf("Reasoning = %d,期望 300", got.Reasoning)
|
||
}
|
||
if got.CacheReportedCalls != 2 {
|
||
t.Errorf("CacheReportedCalls = %d,期望 2", got.CacheReportedCalls)
|
||
}
|
||
}
|
||
|
||
// TestLedgerHitRateExcludesUnreportedCalls 是这组判据里最重要的一条:
|
||
// **命中率的分母只能算「上游报了缓存」的调用**。
|
||
//
|
||
// 为何:没报缓存的提供商/版本不是「命中 0」而是「不知道」。
|
||
// 把它们算进分母会把命中率稀释成一个无意义的低值,
|
||
// 让人去优化一个本来就没开的功能 —— 拿假数据做决定。
|
||
func TestLedgerHitRateExcludesUnreportedCalls(t *testing.T) {
|
||
var l usageLedger
|
||
// ① 报了缓存:768 命中 / 256 未命中
|
||
l.record(agentAPI.TokenUsage{
|
||
Prompt: 1024, CacheRead: 768, CacheMiss: 256, CacheReported: true,
|
||
})
|
||
// ② 没报缓存:只有总量,绝不能拿它稀释命中率
|
||
l.record(agentAPI.TokenUsage{Prompt: 100000, Completion: 500})
|
||
|
||
got := l.snapshot()
|
||
rate, ok := got.CacheHitRate()
|
||
if !ok {
|
||
t.Fatal("CacheHitRate ok=false —— 有一次调用报过缓存,应可计算")
|
||
}
|
||
// 正确:768/(768+256) = 0.75
|
||
if rate < 0.7499 || rate > 0.7501 {
|
||
t.Errorf("CacheHitRate = %.4f,期望 0.75(768/1024)—— "+
|
||
"若把未报缓存的 100000 token 算进分母会得到 0.0076 这种无意义值", rate)
|
||
}
|
||
}
|
||
|
||
// TestLedgerHitRateUnavailableWhenNothingReported 守住「不知道」与「0%」的分野。
|
||
//
|
||
// 混为一谈的后果是撒谎:把「无数据」显示成 0% 命中率。
|
||
func TestLedgerHitRateUnavailableWhenNothingReported(t *testing.T) {
|
||
var l usageLedger
|
||
l.record(agentAPI.TokenUsage{Prompt: 5000, Completion: 100}) // 上游没提缓存
|
||
got := l.snapshot()
|
||
if _, ok := got.CacheHitRate(); ok {
|
||
t.Error("没有任何调用报过缓存,CacheHitRate 应返回 ok=false(表示「不知道」)," +
|
||
"而不是一个 0 值让人读成「命中率 0%」")
|
||
}
|
||
}
|
||
|
||
// TestLedgerHitRateReportedButZero 覆盖「上游报了缓存、这次全未命中」。
|
||
//
|
||
// 这时**必须**可计算并给出 0 —— 这与「没报缓存」是两件事。
|
||
func TestLedgerHitRateReportedButZero(t *testing.T) {
|
||
var l usageLedger
|
||
l.record(agentAPI.TokenUsage{
|
||
Prompt: 1000, CacheRead: 0, CacheMiss: 1000, CacheReported: true,
|
||
})
|
||
got := l.snapshot()
|
||
rate, ok := got.CacheHitRate()
|
||
if !ok {
|
||
t.Fatal("上游报了缓存(命中为 0),应可计算 —— 这是「这次全未命中」," +
|
||
"不是「不知道」")
|
||
}
|
||
if rate != 0 {
|
||
t.Errorf("CacheHitRate = %.4f,期望 0", rate)
|
||
}
|
||
}
|
||
|
||
// TestLedgerFillsMissingTotal 上游没给 total 时按分量补,
|
||
// 避免累计出现「total 小于 prompt+completion」的自相矛盾记录。
|
||
func TestLedgerFillsMissingTotal(t *testing.T) {
|
||
var l usageLedger
|
||
l.record(agentAPI.TokenUsage{Prompt: 100, Completion: 20}) // Total 缺失
|
||
got := l.snapshot()
|
||
if got.Total != 120 {
|
||
t.Errorf("Total = %d,期望 120(prompt 100 + completion 20,"+
|
||
"上游未给 total 时应补出)", got.Total)
|
||
}
|
||
}
|
||
|
||
// TestLedgerConcurrentRecordIsSafe 并发记账不丢数。
|
||
//
|
||
// 为何要测:LLM 调用来自多个 goroutine(主 agent、驻留子 agent、
|
||
// 工具内部发起的子调用)。计数丢失是**静默**的 —— 少算一点没人看得出来,
|
||
// 所以这里用 -race 与总量双重验证。
|
||
func TestLedgerConcurrentRecordIsSafe(t *testing.T) {
|
||
var l usageLedger
|
||
const goroutines, each = 8, 250
|
||
|
||
done := make(chan struct{})
|
||
for g := 0; g < goroutines; g++ {
|
||
go func() {
|
||
for i := 0; i < each; i++ {
|
||
l.record(agentAPI.TokenUsage{Prompt: 1, Completion: 1, Total: 2})
|
||
}
|
||
done <- struct{}{}
|
||
}()
|
||
}
|
||
for g := 0; g < goroutines; g++ {
|
||
<-done
|
||
}
|
||
|
||
got := l.snapshot()
|
||
want := int64(goroutines * each)
|
||
if got.Calls != want {
|
||
t.Errorf("Calls = %d,期望 %d —— 并发记账丢了计数", got.Calls, want)
|
||
}
|
||
if got.Prompt != want {
|
||
t.Errorf("Prompt = %d,期望 %d", got.Prompt, want)
|
||
}
|
||
}
|
||
|
||
// TestLedgerReset 清零(供「重置统计」入口使用)。
|
||
func TestLedgerReset(t *testing.T) {
|
||
var l usageLedger
|
||
l.record(agentAPI.TokenUsage{Prompt: 500, Completion: 10, Total: 510})
|
||
l.reset()
|
||
got := l.snapshot()
|
||
if got.Calls != 0 || got.Prompt != 0 || got.Total != 0 {
|
||
t.Errorf("reset 后仍有残留: %+v", got)
|
||
}
|
||
}
|