mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-04 00:03:59 +00:00
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
This commit is contained in:
@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
|
||||
@ -101,9 +102,11 @@ func (a *Agent) executeToolCallInner(tc agentAPI.ToolCall, channel string, turnS
|
||||
if a.stageHost != nil {
|
||||
if result, err := a.stageHost.ExecuteTool(tc.Name, tc.Arguments); err == nil {
|
||||
return fmt.Sprintf("%v", result)
|
||||
} else if !strings.Contains(err.Error(), "not found in any plugin") {
|
||||
} else if !agentIO.IsToolNotFound(err) {
|
||||
// 非「不存在」= 真的执行失败,如实上报(可被 on_error/retry 处置)。
|
||||
return fmt.Sprintf("工具 %s 执行失败: %v", tc.Name, err)
|
||||
}
|
||||
// 是「不存在」:继续往下走 io / 设备路径,两处都没有才报缺工具。
|
||||
}
|
||||
|
||||
// 设备类工具的**授权闸**(最小授权的缺口在这里)。
|
||||
@ -128,11 +131,25 @@ func (a *Agent) executeToolCallInner(tc agentAPI.ToolCall, channel string, turnS
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if agentIO.IsToolNotFound(err) {
|
||||
// 工具是动态注册的,"不存在"是常态而非异常(插件未加载/已卸载/崩溃)。
|
||||
// 文案必须让模型知道该做什么,而不是含糊的"执行失败"——
|
||||
// 后者会让模型反复重试同一个不存在的名字。
|
||||
return fmt.Sprintf("工具 %s 不存在或未注册:它可能属于未加载/已崩溃的插件。"+
|
||||
"先调 get_plugin_tools(\"\") 看当前可用工具,或 output_list_channels 看通道;"+
|
||||
"确认名称无误后再调用", tc.Name)
|
||||
}
|
||||
return fmt.Sprintf("工具 %s 执行失败: %v", tc.Name, err)
|
||||
}
|
||||
return fmt.Sprintf("%v", result)
|
||||
}
|
||||
|
||||
// toolNotFound / isToolNotFound 是 agentIO 哨兵在 core 侧的薄封装,
|
||||
// 便于 core 内部与测试直接使用(core 依赖 io,不反向)。
|
||||
func toolNotFound(name string) error { return agentIO.ToolNotFound(name) }
|
||||
|
||||
func isToolNotFound(err error) bool { return agentIO.IsToolNotFound(err) }
|
||||
|
||||
func (a *Agent) executeMemoryTool(tc agentAPI.ToolCall, turnScenes []string) string {
|
||||
g := a.graphMem()
|
||||
if g == nil {
|
||||
|
||||
Reference in New Issue
Block a user