|
|
bcd75d25fd
|
fix(provider): has empty arguments 误报 —— 零参数工具被当成参数丢失
## 现象
部署后生产日志出现 14 次:
provider.go:414 [provider:llmsproxy] tool_call seq_list (...) has empty arguments
## 根因
原始响应里参数**完好**(从日志扒出来):
"tool_calls":[{"function":{"arguments":"{}","name":"clawhubadapter_list"},...}]
诊断条件是 `len(tc.Arguments)==0 && tc.RawArguments==""`,而
`parseToolArguments("{}")` 走 string 分支 → `json.Unmarshal("{}", &m)`
成功且 `m != nil`(**非 nil 的空 map**)⇒ 返回空 map ⇒ 命中告警。
被点名的全是**零参数工具**(`seq_list` / `*_list` / `seq_help`,
它们的 `properties` 本来就是 `{}`)。
## 为什么必须修
不是"日志吵"。这条诊断的本职是抓「上游/适配器**真的**把参数丢了」,
真发生时会被这 14 次噪音淹没 —— **诊断日志失去信噪比就等于没有**。
## 修法
新增 `argsLookDropped(rawArgs)`,判 `RawArguments` **原文**而非解析后的 map:
- 空串 / 纯空白 ⇒ 上游没给 arguments 键 ⇒ 真丢
- 能解析成 JSON(哪怕是 `{}`)⇒ 上游确实回了参数 ⇒ 不报
- 解析失败(如半截 JSON)⇒ 参数本身是坏的 ⇒ 等同丢失
## 判据(3 条)
- `TestEmptyArgumentsDiagnosticIgnoresExplicitEmptyObject` 4 个子用例:
`{}` / ` { } ` / 完全缺失 / 只有空白
- `TestArgsLookDroppedIgnoresNonEmpty` 非空参数一律不报
- `TestArgsLookDroppedEndToEnd` 用**日志里出现过的真实 body** 走
`normalizeOpenAIToolCalls` 到判定的完整接缝 —— 单测过了但接缝不对
只有端到端抓得到
写判据时我先用错了类型:拿 `apiToolCall`(**非流式**路径的结构)喂
`normalizeOpenAIToolCalls`,vet 直接报错才纠正为 `openAIToolCall`。
两套结构并存,很容易接错缝。
## 顺带记录:另一个告警不是内核缺陷
`重复申请 stage 锁`(5 次)经排查是**插件侧**问题,内核自愈机制工作正常:
- `proc_main.go.tmpl:1569` SDK 模板在每个 stage handler 入口**自动**调
`stage.lock`;`lock.go:51` 锁**不可重入** ⇒ 同一次 `before_toolcall`
被触发两次且首次未释放就命中
- 已排除 qq 业务代码:`beforeToolcall`(plugin.go:1303-1350)只有
`ctx.Lock()`(SDK **数据**锁,与 proc stage 锁是两把锁)与纯本地调用,
无任何再次触发 stage 的路径
- 成因在插件进程侧运行时(编译进 9月14日的 `plugin.bin`,**不随 homed 部署**)
- 内核 `stage.go:102-106` 的强制释放是**有意设计**("锁仲裁回内核"自愈,
实验 9),避免后续插件死锁;`stages.go:258` 把错误收进 `ctx.Errors`
不中断流程 ⇒ 那轮 212 秒正常跑完
两条结论都写进文档,避免以后有人当内核缺陷去修。
门禁:`-race` 通过,`go test ./internal/... ./cmd/...` 全绿。
|
2026-09-27 21:18:32 +08:00 |
|
|
|
20d357328b
|
docs: 两份 toolcall 文档对齐实现与部署实况
## 契约文档:状态头从「尚未实现」改为「已实现并部署」
生产已注册 7 个 `seq_*` 工具,但文档仍写着"设计定稿,尚未实现" ——
实现者(和读者)会以为 seq 还不存在。
新增 §9.3「实现落点」:设计稿 §8 写的是**六个** `seq_*` 工具,实现时
多了一个 `seq_when_call`(跨序列条件调用独立成工具,否则模型要手写
"先 seq_list 再挑目标再 seq_call",多一次往返且容易挑错),并记下三项
设计之外的修正(`seq_create` O(n²)、`Store.List()` 误认任意 `.json`、
存储用 AST 而非原始文本)。
同时点明:设计条款**仍是契约**,实现与本文冲突时以本文为准并修实现。
## 修一处预先存在的失效引用
§7 末尾 `见 §4.5` —— §4 只到 4.4,该小节不存在。改为按标题名引用
(`§4「结果契约」的 ErrToolNotFound 哨兵`):将来增删小节时不会再次失效。
自查脚本第一版把 8 个**存在**的章节误报成失效引用 —— 标题格式是
`## 1. 背景`(编号后跟 `.`),而我的正则要求编号后是空格。判据自己错了,
改成 `(\d+(?:\.\d+)*)\.?\s` 后才得到真实结果。
## 并行计划文档:部署小节 + 白名单专节
- 原「⚠ 部署前置条件(未完成)」改为「✅ 部署(已完成)」,补实际验证数据
- 记下"实例自述没有编排工具"不是说谎:生产二进制构建于 06:36、seq 引入
于 `71c894c`(更晚)⇒ `strings | grep -c internal/plugins/seq` 为 0。
这类"实例自述与代码状态不一致"应先查二进制构建时间,别急着怀疑提示词
- 新增「设备命令白名单改为可配置」:起因、替换语义、daemon 路径的疏漏
- 明确写下**已知局限**:白名单只匹配命令名、不看参数,
`find -delete` / `sed -i` / `sort -o` 仍放行 ⇒ **不要把它叫"只读白名单"**,
那会让人以为写操作被挡住了
- 记 106 此前无 `online` 日志的成因(旧 waiter 落在未 bind 时收 ping 会断连的
缺陷窗口),以及 `ssh` 吃掉 `read` 输入导致"喂了 yes 却说已取消"
删掉了初稿里一段"反引号内 `+=` 写进 heredoc 导致赋值落到子 shell"的说法 ——
脚本与 git 历史里都没有这种写法,属凭记忆误记,不能留。
文中数字均与现场核对:seq 工具 7、二进制 86784400 / 12691402、白名单 22 条。
|
2026-09-27 20:00:33 +08:00 |
|
|
|
07feecba11
|
docs(plan): 补记全面压测结果与部署前置条件
两版隔离实例实测(规模 3 = 288 条输入),核心差异是**处理数**而非耗时:
旧版 openai.lua 缺 stream_index 透传 ⇒ 多个分片并到槽 0、参数混拼 ⇒
工具一个都没真跑,却因为「少干活」而耗时更短。
!slowbatch8 旧 0.220s / 0 个 → 新 0.409s / 8 个
并发 vs 强制串行(新版内部) N=8 加速 3.88×
调度器轰炸 288 输入 两版均 100% 通过
连续稳定性 20 轮 两版均无错误、内核无 panic
规模 1(32 条)与规模 3(288 条)结果完全一致 ⇒ 可复现。
同时记录部署前置条件:生产是 -tags=onnxruntime 构建(strip 后 75MB vs
普通构建 28MB),package-linux.sh:139 会显式拒绝非 onnxruntime 构建。
本次改动未触及任何 ONNX 路径,故压测结论对生产成立,但必须走
deploy/packaging/build.sh 才能部署。
|
2026-09-27 19:00:53 +08:00 |
|
|
|
56fe10d3a4
|
docs(plan): 记录更新前后全面压测结果与部署前置条件
两版隔离实例实测(规模 3 = 288 条输入),核心差异是**处理数**而非耗时:
旧版 openai.lua 缺 stream_index 透传 ⇒ 多个分片并到槽 0、参数混拼 ⇒
工具一个都没真跑,却因为「少干活」而耗时更短。
!slowbatch8 旧 0.220s / 0 个 → 新 0.409s / 8 个
并发 vs 强制串行(新版内部) N=8 加速 3.88×
调度器轰炸 288 输入 两版均 100% 通过
连续稳定性 20 轮 两版均无错误、内核无 panic
规模 1(32 条)与规模 3(288 条)结果完全一致 ⇒ 可复现。
同时记录部署前置条件:生产是 -tags=onnxruntime 构建(strip 后 75MB vs
普通构建 28MB),package-linux.sh:139 会显式拒绝非 onnxruntime 构建。
本次改动未触及任何 ONNX 路径,故压测结论对生产成立,但必须走
deploy/packaging/build.sh 才能部署。
|
2026-09-27 19:00:19 +08:00 |
|
|
|
4fd18a2e83
|
docs(plan): 遗留项收敛——D4 与端到端已完成,提权无需决策
|
2026-09-27 13:18:28 +08:00 |
|
|
|
532500e3c9
|
docs(plan): 内核主线与插件线全部完成,记录两个设计决策与三处遗留
|
2026-09-27 13:06:10 +08:00 |
|
|
|
ae3cdaa1a0
|
docs: 同步两份文档的实现进度(内核主线 0~2.5 全部完成)
|
2026-09-27 11:56:01 +08:00 |
|
|
|
446645d0a0
|
docs(plan): 阶段 2 标记完成,记录并发规则与 2c 判据闭合
|
2026-09-27 11:27:53 +08:00 |
|
|
|
3d12e82f65
|
refactor(toolcall): StageContext 拆 per-tool,为并发执行消除共享槽(阶段 2c)
问题:f.StageCtx 是**单槽**,批内每个工具都覆写它(ToolCalls=[单元素]、
ToolResults 覆写、Results[0] 回读)。串行下看不出问题,但并发下
N 个 goroutine 同写一个 ctx = 数据竞争,且 after_toolcall 插件可能读到
**别的工具**的结果。
改动(task.go):
· TaskFrame 增 toolCtxs []sdk.StageContext(每工具一份)
· buildToolContexts 在 stepLLM 设 PendingTools 时建池;
Extra **逐份浅拷贝**——共享同一 map 即竞争(stage handler 会写它)
· toolCtxFor(i) 取第 i 份,越界/未建时回落 f.StageCtx(测试替身安全)
· stepToolBegin(before_toolcall + 参数回填)、stepToolExec(写结果)、
stepToolAfter(读结果)三处全部切到 per-tool ctx
判据(toolbatch_test.go 追加两条):
· 每个工具的 before_toolcall ctx 只带自己的 ToolCalls[0].Name,
且 Extra[output_channel] 逐份带过去(stage.go:18 依赖它)
· after_toolcall 读到的 Result 必须属于当前工具,不能是批内另一个的
★ 诚实记录:这两条判据在**串行**下**测不出与单槽的差别**——串行时
每工具跑完才进下一个,不存在交错。变体验证(toolCtxFor 退回单槽)后
判据仍全绿。故 2c 记为「实现已就位、判据未闭合」,真正判据必须与 2d
(并发执行)一起写,并以 -race 确认无竞争。已在执行计划中标注。
过程中两次自伤:
· 我的 harness 没设 Extra[output_channel](那是 prepareInputTask 才写的,
task.go:380),判据一度报「产品缺陷」——核实后是我造的场景,已对齐生产;
· 阶段 1 的 TestStageCtxSuccessIsHonestEndToEnd 读 f.StageCtx.ToolResults,
拆分后失效——判据跟随新结构改为按批索引取 toolCtxs[i],
断言的仍是内核产出的 Success 值本身。
顺带记录(非本次引入):TestResidualKeep/Drop 偶发失败,根因是
offload_test.go 的 SpawnResident 起了子调度器 goroutine,而测试
enqueue 后无同步就读同一队列。干净基线 3/3 全绿属运气。已在计划中
记为待修,避免后续误判为并行化引入的回归。
|
2026-09-27 11:17:06 +08:00 |
|
|
|
4ba72977d2
|
test(toolcall): 补批内路径的三条缺失判据(阶段 0.5)
同一批多个 tool_call 的循环(StepToolBegin→Exec→After)此前只被
scheduler_critical_test.go:125 一条用例覆盖「按序执行」,缺的三条正是
阶段 2(并行执行层)要改的地方:
· tool_call_id 配对完整性 —— 阶段 2 改消息落法(一个 assistant 带全部
tool_calls + N 条 tool)时,配对断裂上游会直接报错
· ContentOnce 批内语义 —— 同一段 assistant 文本在批内重复 N 次,撑爆上下文
· denied 后继续批内 —— 改成 abort 会丢掉本可执行的后续调用
判据 toolbatch_test.go(4 条),全部确定性断言:阶段 0 已消除 map
迭代随机性,同批工具的落序与配对可稳定断言。
变异验证:令 stepToolBegin 跳过批内最后一个工具后,4 条判据同时 FAIL
(既有那条也 FAIL),报错直指 ToolsUsed=[tool_alpha]、
tool_call_id "c2" 被声明 0 次。
更正一处此前的不准确表述:我曾说「无任何测试直接驱动批内路径」——
不准确。scheduler_critical_test.go:125 已驱动「同批两工具按序执行」;
漏查是因为只 grep 了 PendingTools/ToolIdx 字段名,没查断言内容。
真正缺的是上表三条。
过程中三次自伤(均由「判据先写」暴露):臆造不存在的 helper;
stageHost 置 nil 后又使用;给 newTaskFrame 传 nil 导致 stepPrepare 于
task.go:519 nil 解引用 panic(改用仓内既有 a.stageCtxFromInput)。
顺带记录:生产两处 newTaskFrame 调用都传真实 ctx,但 stepPrepare 对
f.StageCtx 无 nil 兜底——本次不修(无生产触发路径),记为潜在缺口。
回归:internal/agent/... 与 internal/plugins/... 全绿。
|
2026-09-27 08:59:30 +08:00 |
|
|
|
7a566d50b7
|
fix(toolcall): 工具「不存在」类型化 + 修父 io 兜底吞错误 + 修并行 tool_call 落序随机
主线:工具调用并行化改造(阶段 0 与 0.2)。
① flush 顺序随机(process.go)
flushToolCall 由 `for idx := range accs` 驱动,Go map 迭代顺序随机化
⇒ 同一批并行 tool_call 进入 resp.ToolCalls 的顺序每次运行都可能不同。
对 output_send__ 这类用户可见通道,分段消息到达顺序不可复现。
改为收集 index 后 sort.Ints 再 flush(两个调用点统一走 flushAll)。
判据 stream_flush_order_test.go(8 工具 × 200 轮),已变异验证可检测。
② 工具「不存在」类型化(io/channel.go、core/stages.go、core/toolcall.go)
工具是动态注册的,「不存在」是运行期常态而非异常。原先内核用
strings.Contains(err, "not found in any plugin") 判别——约定而非契约,
插件文案含该子串即被误判。改用哨兵 ErrToolNotFound + errors.Is
(沿用仓内 ErrInputChannelUnknown 的先例)。
⚠️ 顺带修一个静默 bug:IOManager 向父兜底时吞掉父的执行失败,
误报为「工具不存在」。后果是设备离线这类本该 retry 的失败被判为
「工具没了」⇒ 整组被跳过,与「插件真没加载」无法区分。改为只传递
「确实不存在」,其余如实上抛。
「不存在」的文案改为可执行指引(get_plugin_tools / output_list_channels),
而非含糊的「执行失败」——后者会让模型反复重试同一个不存在的名字。
判据:toolcall_error_test.go(类型化 vs 诱饵子串、%w 穿透、执行期文案)、
channel_error_test.go(父失败不吞、真的不存在仍可判别)。
两者均经变异验证。回归:internal/agent/... 与 internal/plugins/... 全绿(14 包)。
设计文档:docs/zh/toolcall-contract-and-sequence-design.md
执行计划:docs/zh/toolcall-parallel-execution-plan.md
|
2026-09-27 08:55:25 +08:00 |
|