Files
HomeAgent/internal/agent/api/codec.go
JianFeeeee 7351c6ca2e feat(c-core): 内核编解码层 C 化第一刀 —— L1 纯函数层落地并打通构建链
第一刀只做三个纯函数(窗口推断 / token 估算 / token 截断),
价值不在功能(Go 版没问题),而在打通「Go → cgo → C」全链路并
建立可复现的对照范式,后面每扩一个函数都复用它。

## 为什么是这三个

按「无状态 → 有状态」分层,L1 协议编解码最安全:纯 string in → struct out,
不碰网络、不碰 Lua、不碰 goroutine。三个函数更是同一组纯算术,最小可验证切片。

## 关键设计:包内符号链接,不链接静态库、不 include 包外源

三种做法都实测过,只有一种同时满足「可构建 + 可交叉编译 + 缓存可跟踪」:

1. ❌ 链接 `csrc/build/libha_codec.a`(原方案)
   - .a 是构建产物、不入库(.gitignore 的 build/ 命中 csrc/build/),
     而发布脚本原先并不产出它 ⇒「不入库 + 不生成」两头空,
     实测报 `cannot find .../libha_codec.a`
   - 交叉编译 linux/arm64(homed 真实发布目标)时,宿主 x86-64 的 .a
     被链进目标产物,实测报 `file in wrong format`

2. ❌ `#include "../../../csrc/src/ha_codec.c"`(包外相对包含)
   ★ Go 构建缓存**不跟踪包外被 #include 的 C 文件**。实测:包外源把返回值
   7→8,`go test` 依然通过(缓存命中、静默沿用旧代码);同样改动落在包内
   文件时立即判红。对「逐步推进 C 化」这是致命的——改 C 源码不生效且无报错。
   (包内 shim `#include` 包外源同样漏跟踪,已实测排除。)

3. ✅ 包内符号链接 `internal/agent/api/ha_codec.{c,h}` → `csrc/`
   文件在包目录内 ⇒ 缓存按内容正确跟踪;只有一份权威源 ⇒ 无副本漂移,
   也不需要「同步 C 源」的 make 目标。

## 不需要额外 build tag

ha_codec 是零依赖纯 C99 源码内联编译,不需要外部库或工具链前提;
而 homed 本就强制 cgo(sqlite3 + gojieba),故 C 路径自然生效。
只用 `cgo` / `!cgo` 一组约束(对比 onnxruntime:那个需运行期 .so,故必须显式 tag)。

## 顺带修掉的既有缺陷(非 C 化引入,但一直缺覆盖)

- Makefile 的 arm64 目标缺 CC/CXX:cgo 回退到宿主 g++,报
  `gcc_arm64.S: no such instruction: 'stp x29,x30,[sp,'`
  (deploy/packaging/build.sh:49 一直是对的,Makefile 漏了)
- Makefile 的 arm64 目标缺 .syso 隔离:cmd/{homed,waiter}/*.syso 是 Windows
  COFF 资源对象,Go 会把同目录 .syso 无条件链进任何目标,交叉到非 Windows
  平台报 `file format not recognized`(build.sh 有 hide_syso_for_target)

## 验证(每条可复现)

- `make build-linux-arm64` → ELF 64-bit LSB executable, ARM aarch64(82MB)
- `bash deploy/packaging/build.sh linux/arm64 homed` → ELF aarch64(79MB)
- 移走 csrc/build/ 后 homed(cgo)与 waiter(CGO=0)均能构建
- 变异 C 源(131072→777)后**同一缓存**下 go test 立即 FAIL(改前:仍报 ok)
- `make check-codec-paths` 两条路径 OK;`make csrc-test` C 契约测试 100%
- 黄金对照:手写用例 + 2000 次随机对拍,C 与纯 Go 逐值相等
- 全量 `go test -count=1 ./...` → 57 包:38 ok + 19 无测试 + 0 FAIL

新增 `make check-codec-paths` 防回归:只测一条路径时,另一条的破坏不会被发现。

## 文档

- `docs/zh/c-core/llm-orchestration-c.md` 同步为「已落地」,并更正因
  「Windows 原生已放弃」而过时的 §2.3(回退路径的理由需重述)
- plan.md 的 P0-1 标记为已修复,附实测证据
2026-09-25 14:45:08 +08:00

44 lines
1.9 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 api
// codec.go —— 编解码层的**统一出口**(无论 CGO 开关如何,调用方只认这里)。
//
// 分层:
// codec_pure.go —— 纯 Go 实现,永远参与编译(回退 + 黄金对照基准)
// codec_cgo.go —— CGO_ENABLED=1:真正调 C 库
// codec_nocgo.go —— CGO_ENABLED=0:把 C 符号转发到纯 Go
// codec.go —— 本文件:对外的稳定 API,含兜底与日志
//
// 这样调用方(provider.go / core)不需要写任何 build tag 分支。
import (
"log"
"strings"
)
// ModelContextWindow 返回模型的最大上下文窗口(token 数)。
//
// 推断不出时(如 model="AUTO")记一行日志并回退 defaultInferredContextWindow:
// 窗口被低估必须可见,部署方用 per-source
// core.llm.sources.<name>.context_window 显式声明真实值即可覆盖。
//
// 标称窗口 ≠ 有效窗口:接近满时注意力涣散,调用方应取 70-80% 为目标利用率。
func ModelContextWindow(model string) int {
if w := modelContextWindowC(model); w != contextWindowUnknown {
return w
}
log.Printf("[provider] 模型 %q 无法推断上下文窗口,回退 %d;"+
"若真实窗口更大,请设置 core.llm.sources.<name>.context_window",
strings.ToLower(model), defaultInferredContextWindow)
return defaultInferredContextWindow
}
// EstimateTokens 粗略估算 token 数。
//
// 注意:这是**高频热路径**(上下文裁剪对每个事件都调)。走 C 的跨语言开销
// 对短文本未必划算 —— 是否该留在 C 侧由 codec_bench_test.go 的实测数据决定,
// 不要凭直觉断言(见 docs/zh/c-core/llm-orchestration-c.md §七 未决问题 3)。
func EstimateTokens(text string) int { return estimateTokensC(text) }
// TruncateByTokens 截断字符串至不超过 maxTokens 估计值。
func TruncateByTokens(s string, maxTokens int) string { return truncateByTokensC(s, maxTokens) }