mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 17:27:31 +00:00
fix(agent): 流式并行工具参数串味 + meta 传对象被静默丢弃
两个独立的真实生产故障,都表现为"agent 答应了却没做到"、用户感觉
"它忘了刚说过的话"。按天统计 QQ 发送失败率:10-21 是 1.3%,10-23 涨到 34%。
## 故障一:流式并行 tool_call 参数串味
`internal/agent/api/provider.go` 累积 SSE 分片时用**切片下标**:
for i, tc := range ck.ToolCalls {
acc := accs[i] // ✗ 并行分片时 i 恒为 0
}
并行多工具时上游把每个 tool_call 的 arguments 分成多个 chunk 下发,
每个 chunk 的 ToolCalls 切片长度通常是 1 ⇒ 12 个并行调用的参数分片
**全部拼进 accs[0]**,其余 11 个拿到空串。
这个 bug 从 2026-08-25(618f51a)就存在。8-26 的 7a51c9b 号称
"按 JSON index 分桶,修复空参数调用",实际只加了 StreamIndex 字段和
诊断日志,**没改累积逻辑** —— 看起来修了,其实没修。
修法:`idx := tc.StreamIndex`(上游 index 才是归属依据)。
生产指纹:单次响应 12 个 tool_call 全部报 "has empty arguments",
6 小时 216 次,全部来自 deepseek-v4.1-flash 的并行调用。
## 故障二:meta 传对象被静默丢弃
"一 call 多发"(f850d75, 10-22)之后,模型对 meta 的传参形态不稳定:
有时传字符串 `"{\"user_id\":123}"`,有时直接传对象 `{"user_id":123}`。
`stringOrArray` 只认 string / []string / []interface{},遇对象返回 nil;
而 `parseOutputItems` 对空值的处理是**静默填空数组**:
if len(metas) == 0 { metas = make([]string, len(payloads)) } // 全填 ""
⇒ 插件收到 meta="" ⇒ 报 "meta 中需要 group_id 或 user_id 字段"。
10-23 一天 132 次。
修法:非字符串值**序列化保留**(marshalToJSONString),数组元素同理
(coerceToString)。内核不知道 meta 的语义,但它知道"模型给出了一个值",
不能当"没提供"处理。
## 判据(6 条,均经变异验证)
- SSE:并行分片各归各位;同一 tool_call 的多片按 index 拼接完整
- meta:对象被保留为 JSON 字符串;字符串原样透传;数组元素一一对应;
确实缺失时仍为空
变异验证:退回 `accs[i]` ⇒ 两条 SSE 判据红;退回不序列化 ⇒ meta 判据红
("meta 被静默丢弃(插件将报「需要 group_id 或 user_id」)")。
This commit is contained in:
@ -609,12 +609,24 @@ func parseOpenAICompatibleSSEBody(raw []byte) (*CompletionResponse, bool) {
|
||||
found = true
|
||||
contentBuf.WriteString(ck.Content)
|
||||
reasoningBuf.WriteString(ck.ReasoningContent)
|
||||
for i, tc := range ck.ToolCalls {
|
||||
acc := accs[i]
|
||||
for _, tc := range ck.ToolCalls {
|
||||
// ★ 必须用**上游的 index**(StreamIndex),不能用切片下标。
|
||||
//
|
||||
// 流式协议里每个分片带 index 表明它属于第几个 tool_call。
|
||||
// 并行多工具时上游会**分成多个 chunk**下发:每个 chunk 的
|
||||
// ToolCalls 切片长度可能都是 1(下标恒为 0),若用切片下标累积,
|
||||
// 12 个并行调用的参数分片会**全部拼进 accs[0]**,其余 11 个
|
||||
// 拿到空串 ⇒ 被 argsLookDropped 判为"丢参数"。
|
||||
//
|
||||
// 线上实证(2026-10-25):2 小时内 74 次 "has empty arguments",
|
||||
// 每次恰好 12 条(= 同一响应的 12 个并行 tool_call 全部报空),
|
||||
// 模型侧表现为"工具拿不到参数",用户侧表现为"agent 忘了刚说要做什么"。
|
||||
idx := tc.StreamIndex
|
||||
acc := accs[idx]
|
||||
if acc == nil {
|
||||
acc = &sseAcc{}
|
||||
accs[i] = acc
|
||||
toolOrder = append(toolOrder, i)
|
||||
accs[idx] = acc
|
||||
toolOrder = append(toolOrder, idx)
|
||||
}
|
||||
if tc.ID != "" {
|
||||
acc.id = tc.ID
|
||||
|
||||
Reference in New Issue
Block a user