From c53d1da2a6f376b3c88681853e9e46e22532866c Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sun, 18 Oct 2026 18:36:26 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=86=85=E6=A0=B8=E6=80=A7=E8=83=BD?= =?UTF-8?q?=E5=AE=9E=E6=B5=8B=E5=9F=BA=E7=BA=BF=20+=20payload=20=E9=80=8F?= =?UTF-8?q?=E4=BC=A0/=E4=BF=9D=E7=95=99=E9=94=AE=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## docs/zh/kernel-performance.md(新增) 全部是实测数据,不是估算,每条标出测量方法与复现命令。 冷启动 15.4s(5 层 17 模块 + 22 插件),耗时分布: 第 0→1 层 3.1s 20% 内存/ONNX/文档存储初始化 第 1→4 层 2.4s 16% LLM/知识库/agent 装配 第 4→完成 9.9s 64% 22 个插件子进程建链 ★ 与早期记录(1538–1709ms)的差异要说明:那是**纯内核**装配时间, 不含插件。两者口径不同,混用会得出"启动变慢 10 倍"的错误结论。 其余指标:RSS 26MB / 插件进程 12 个(13 个按需加载)/ 三层工具超时 (30s ctx → max(声明,参数) → 10m 硬上限)/ 上下文预算实测值 / `offload_busy_after` 1m。 含三个实测踩过的性能陷阱:长工具执行期间 stop 无法生效、 同阶段重复注册静默覆盖、before_output 双触发路径。 ## kernel-modularization-design.md 新增 2.7 节 注入 payload 的透传机制与保留键硬约束。 保留键必须覆盖两类:注入路径自己写入的(no_memory/context_policy/…) 与内核读取的语义键(interrupt/stop/command/…)。插件写保留键会被 跳过并记日志 —— 允许覆盖等于允许伪造"这条不记记忆"或"这条立即打断", 那是权限提升。 同时记录 output_send__* 的 stage 语义:before_output/after_output 有两条触发路径,output.go 那条才是"消息真发出去"的时刻,而它历史上 连 Extra 都没初始化(实测 extra_keys=0)。 ## 设计过程中的一次返工 曾加 InjectOptions.Meta,等于把同一件事做两遍且只服务注入路径。 用户指出后改为复用 InputEvent.Payload 的扩展口,所有注入路径共用。 --- docs/zh/kernel-modularization-design.md | 72 +++++++++ docs/zh/kernel-performance.md | 201 ++++++++++++++++++++++++ 2 files changed, 273 insertions(+) create mode 100644 docs/zh/kernel-performance.md diff --git a/docs/zh/kernel-modularization-design.md b/docs/zh/kernel-modularization-design.md index 4be74aa7..9c4a166b 100644 --- a/docs/zh/kernel-modularization-design.md +++ b/docs/zh/kernel-modularization-design.md @@ -921,6 +921,78 @@ func (d *Distiller) distillLoop() { - `TestDeclarationsTraceToRealCallSites`:声明与**签名**不漂移 - §2.8.2(A) 的 onnx→textmem 顺序:声明与**运行期读取**不漂移 +## 2.7 注入 payload 的透传与保留键(2026-10-18) + +### 背景 + +通道插件在**注入**时要能让"这一轮的身份"跟着走到后续阶段。但: + +``` +on_input 的 stageCtx 是局部变量(stageCtxFromInput),runStage 完就丢 +before_output 的 stageCtx 是内核新建的(emitResponse / output.go) +⇒ 两者不共享 Extra +``` + +插件于是只能读全局状态猜"这轮是谁引起的"——并发多个会话时会**串号** +(A 的指示打到 B 身上)。 + +### 机制 + +`InjectOptions.Payload` 是插件自带的不透明字段,内核不解释: + +```text +注入点 opts.Payload + → 与真 payload 合并(保留键跳过) + → emitResponse / output_send 把**非保留键**原样写进 stageCtx.Extra +``` + +`InputEvent.Payload` 本就是通道送入内核的请求体,业务字段本该在那里; +缺的只是一个**扩展口**(`IOInjector` 的方法签名是 +`(source, channel, text, opts)`,插件无法直接给 payload 加字段)。 + +> 设计过程中曾误加 `InjectOptions.Meta` —— 那等于把同一件事做两遍, +> 且只服务注入路径。改名为 `Payload` 后 HTTP/CLI/ACP 共用同一套机制。 + +### 保留键(硬约束) + +`reservedPayloadKeys` 必须覆盖两类键: + +1. **注入路径自己写入的**:`no_memory` / `context_policy` / + `recall_policy` / `scene_policy` / `cleaner_name` / `priority` / + `type` / `content` +2. **内核读取的语义键**:`interrupt` / `interrupt_source` / + `interrupt_channel` / `stop` / `command` / `scene` / `source` / + `channel` / `media_blocks` / `media_type` + +插件写了保留键会被**跳过并记日志**,不是拒绝整次注入。 + +**为什么必须拦**:允许覆盖等于允许插件伪造「这条不记记忆」或 +「这条要立即打断」——那是权限提升,不是便利。 + +**为什么是跳过而不是报错**:插件可能只想带一个业务字段,因顺手多写一个 +保留键就让整条注入失败,代价太大。 + +⚠️ **新增内核语义键时必须同步这个集合**,否则新键就是新的提权面 +(判据 `TestReservedKeysCoverInjectionOwnKeys` 覆盖这一点)。 + +### `output_send__*` 的 stage 语义 + +`before_output` / `after_output` 都有**两条触发路径**: + +| 来源 | Extra | 含义 | +|---|---|---| +| `emitResponse` | `input_source` + payload 透传字段 | 一轮的回复(turn 边界) | +| `output.go` | `output_channel` / `output_meta` / `output_type` | 单次发送 | + +`output.go` 的路径**才是"消息真的发出去"的时刻**,但历史上它构造 stageCtx 时 +连 `Extra` 都没初始化(nil),插件在那个时点读不到任何上下文 +(实测 `extra_keys=0`)。 + +现在补齐三个键,并补上缺失的 `after_output`(`Execute` 成功后触发; +失败时不触发 —— 消息没送达,"发出后收尾"的语义不成立)。 + +插件用 `_, has := ctx.Extra["output_meta"]` 区分两条路径。 + ## 3. 兼容性约束(★ 硬约束) 1. **存量插件的 `plugin.json` 不变必须照常工作** diff --git a/docs/zh/kernel-performance.md b/docs/zh/kernel-performance.md new file mode 100644 index 00000000..81d734a5 --- /dev/null +++ b/docs/zh/kernel-performance.md @@ -0,0 +1,201 @@ +# 内核性能指标(实测基线) + +本文记录**实测**数据,不是估算。每条都标出测量方法与采集时间, +便于复现与追回归。 + +> 采集环境:`main` @ `ecffb8a`,Linux amd64,Go 1.27.1, +> 生产实例 `/home/newqqagent`(21.4GB 数据目录), +> 22 个外部插件 + 5 层 / 17 模块内核。 +> 采集时间:2026-10-18 18:12(冷启动)。 + +--- + +## 1. 冷启动 + +`systemctl restart homeagent` → `内核装配完成`。 + +| 阶段 | 时刻 | 累计 | 说明 | +| --- | --- | --- | --- | +| 启动 | 18:12:15 | 0s | 依赖图加载(`depgraph.bin`,digest `2067c913`) | +| 第 0 层完成 | 18:12:15 | ~0.1s | 2 个:`config`, `kernel-api` | +| 第 1 层完成 | 18:12:18 | **3.1s** | 9 个:`doc-store`, `knowledge-migration`, `lua`, `memory`, `migration`, `multimodal`, `onnx-parser`, `supervisor`, `tracker` | +| 第 2 层完成 | 18:12:18 | 3.2s | 3 个:`knowledge-store`, `llm`, `text-memory` | +| 第 3 层完成 | 18:12:18 | 3.3s | 1 个:`stage-host` | +| 第 4 层完成 | 18:12:20 | **5.5s** | 2 个:`agent`, `plugin-host` | +| 装配完成 | 18:12:30 | **15.4s** | 含 22 个外部插件建链 | + +峰值并发 4(原为 17 步串行)。 + +### 耗时分布 + +```text +第 0→1 层 3.1s ████████████████ 20% 内存/ONNX/文档存储初始化 +第 1→4 层 2.4s ████████████ 16% LLM/知识库/agent 装配 +第 4→完成 9.9s ████████████████ 64% 22 个插件子进程建链 +```text + +**结论**:启动时间主要由**插件建链**决定(64%),不是内核模块本身。 +每多一个 proc 插件约 +0.45s。 + +> ⚠️ 与早期记录(1538–1709ms)的差异:那是**纯内核**(5 层 17 模块) +> 的装配时间,不含插件。两者口径不同,不要混用。 + +### 复现 + +```bash +systemctl restart homeagent +journalctl -u homeagent --since "1 min ago" \ + | grep -E "启动顺序来自|第 [0-9] 层完成|内核装配完成" +```text + +--- + +## 2. 资源占用 + +稳态(启动完成、空闲): + +| 指标 | 值 | +| --- | --- | +| 内核 RSS | **26 MB** | +| 内核 VSZ | 2104 MB(含 Go 保留地址空间,非常驻) | +| 插件子进程数 | 12(其余按需加载) | +| 二进制体积 | 84 MB(未 strip,含 debug_info) | + +> 12 而非 22:13 个插件声明了 `auto_start: false`(按需加载), +> 只有被 `plugin_enable` 拉起时才起进程。 + +--- + +## 3. 超时与容量 + +这些是**硬约束**,决定"一个任务最多能占用多久"。 + +### 工具超时(三层) + +| 层 | 值 | 位置 | +| --- | --- | --- | +| `cmd_run` 自身 ctx | 默认 **30s**,可由参数调 | `internal/plugins/cmd/plugin.go` | +| 内核工具等待 | `max(声明值, 参数值)` | `internal/agent/core/toolcall.go:37` | +| 硬上限 | **10m** | `toolcall.go:28` `maxToolTimeout` | + +计算规则(`resolveToolTimeout`): + +```text +base = 60s(默认) + ↓ 若 ToolDef 声明了 Timeout +base = 声明值 + ↓ 若调用参数带 timeout 且 > base +base = 参数值 + ↓ 若 base > 10m +base = 10m +```text + +**实测验证**: + +```text +cmd_run command="sleep 200"(不传 timeout) +→ 工具 cmd_run 失败: 命令执行超时(30s) +```text + +⚠️ **参数可以合法绕过默认值**:模型传 `timeout="2m"` 时, +`cmd_run` 的 30s 保护被提高到 2 分钟。这是设计允许的(长命令确有需要), +但也意味着**单个工具最长可占用 10 分钟**,期间调度器只能等。 + +### 上下文预算 + +运行时日志(`task.go:599`): + +```text +tool call loop start, max_ctx=1048576 target=600000 fixed=3004 mem=198998 ctx=397 +```text + +| 项 | 值 | 含义 | +| --- | --- | --- | +| `max_ctx` | 1048576 | 模型上下文上限(1M) | +| `target` | 600000 | 目标使用量(60 万,留 40% 余量) | +| `fixed` | 3004 | 固定开销(system prompt 等) | +| `mem` | 198998 | 记忆注入占用 | +| `ctx` | 397 | 当前对话占用 | + +### 记住-遗忘阈值 + +| 键 | 值 | 说明 | +|---|---|---| +| `core.agent.offload_busy_after` | **1m** | 忙超过此时长则转投 offload 子 agent | + +--- + +## 4. 安全层开销 + +| 项 | 值 | +| --- | --- | +| 密钥来源 | `embedded`(构建期注入,指纹 `0db8e9c5`) | +| 变量条数 | 8 | +| 加密算法 | AES-GCM(`enc:v1:` 前缀) | +| 出向/入向扫描 | 每次工具调用与工具返回各一次(`security.Patterns`) | +| 日志脱敏 | `scrubForLog`(幂等,nil-store 安全) | + +扫描是**纯内存正则匹配**,未观测到成为瓶颈(工具调用耗时由工具本身主导)。 + +--- + +## 5. 插件建链 + +| 指标 | 值 | +| --- | --- | +| 建链数(本次启动) | 11 | +| `loaded` 事件 | 28 | +| 单插件建链均摊 | ~0.45s(9.9s / 22) | + +协议版本:`sdk=1.4.0 core=1.4.0`。 + +--- + +## 6. 已知的性能陷阱 + +以下都是**实测踩过**的,不是理论风险。 + +### 6.1 长工具执行期间 `stop` 无法生效 + +`stop` 请求只在 `pre_action` 被消费: + +```text +[agent] stop requested by webui (queued=1 will be short-circuited at pre-action) +```text + +若 agent 正卡在 `sleep 150` 这类长命令里,**没有 pre_action 可以插入** ⇒ +停止按钮要等到当前工具跑完才生效。 + +现场(2026-10-18):`task#3` 跑了 10 分钟,期间 `task#4`~`task#8` +全部堵在队列,两次 stop 请求都无效,最后只能重启。 + +### 6.2 同阶段重复注册 = 静默覆盖 + +插件侧 `stageHandlers` 是 `map[阶段名]handler`,同阶段注册两次后者覆盖前者。 +除了功能丢失,还意味着**权限门可能被覆盖掉**。hmapdev 在构建期告警。 + +### 6.3 `before_output` 有两条触发路径 + +一轮结束 vs 单次 `output_send__`。误用会让"正在输入"漏触发, +或让状态归位过于频繁(干活期间反复跳回空闲)。 + +详见 SDK README「阶段钩子」一节。 + +--- + +## 7. 复现与回归 + +```bash +# 冷启动 +systemctl restart homeagent && sleep 20 +journalctl -u homeagent --since "1 min ago" | grep "内核装配完成" + +# 资源 +ps -o rss= -p $(pgrep -f "homed" | head -1) | awk '{printf "RSS: %.0f MB\n", $1/1024}' + +# 插件进程 +pgrep -c -f "plugin.bin" + +# 工具超时(应 30s 失败) +# 经 agent 调:cmd_run command="sleep 200" +```text