mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-04 00:03:59 +00:00
feat(parallel): 并发安全改为声明式,并审计标注 37 个工具
把"能不能并发"从内核硬编码名单改成**工具自己的声明项**,形态照 SDK 的
NoMemory 走。
## ★ 起因:提示词在跟内核不一致
阶段 2.5 写进提示词的「内核默认并行执行」当时是**假的**:toolParallelSafe
只查 stageHost 与 io 两个来源,而全仓 ParallelSafe:true 的生产代码数量
是 **0**。于是除碰巧只发一个工具外,每一批都整批串行回退,而提示词正教
模型把多个查询放同一轮。**内核行为与提示词不一致 = 对模型说谎。**
并发面:0 → 37 个工具(18 插件 ParallelSafe + 19 插件 Serial + 9 内置只读)。
## 声明形态(照 SDK,不自创)
### 插件:结构体字段
s.RegisterTool("config_get", sdk.ToolDef{
Name: ..., Description: ...,
Parameters: map[string]interface{}{...},
// 已核实只读:…
ParallelSafe: true, ← 插在 Parameters 之后、handler 之前
}, p.handleGet(s))
位置与 SDK 的 NoMemory/ContextPolicy/RecallPolicy 一致:Name 在首位,
声明项在末尾,不打散 gofmt 对齐。
### 新增 SDK 声明项:ToolDef.Serial
ParallelSafe 的**反向**标记,判据优先级高于 ParallelSafe。
为什么需要:ParallelSafe 零值 false 已表达"安全",插件无法区分"我没想过"
与"我确认过必须串行"。没有这个区分,工具作者只能靠命名约定传递意图。
内核已消费它(io.ToolDef 同步加字段对齐),并有判据守"Serial 胜出"。
### 内置工具:toolDef 的 toolParallel 选项
内置工具以裸 schema map 下发,没有 ToolDef 结构,所以用变参选项:
toolDef(名字, 描述, 属性) // 默认串行
toolDef(名字, 描述, 属性, "toolParallel") // 已核实只读,可并发
读工具表的老调用点一行不用动,声明就写在工具定义那一行。
## ★ 走过的弯路(都留了判据)
1. **硬编码白名单**:先在 toolParallelSafe 里查一张
builtinParallelSafeTools map。那把声明从"工具自己"搬回了内核 ——
工具改名/新增不会自动跟着变,得靠一条 grep 源码的判据才能发现漂移,
而判据一改就忘。已删,改为从定义读。
2. **判据前提错(同一个坑踩了两次)**:拿裸 &Agent{} 的 buildToolDefs 输出
当"实际可见工具",但这 9 个内置工具全在条件分支里(a.knowledge != nil /
a.social != nil / a.parentID != ""…),裸 Agent 一个都不产出 ⇒ 全部误报
"声明形同虚设"。第一次叫它"幽灵条目",没认出是同一个坑。
3. **注释模仿真实签名污染判据**:toolParallel 的用法注释写着
`toolDef("knowledge_search", ...)`,判据按文本匹配先撞上注释。
4. **buildToolDefs 的 nil 不一致**:开头判了 a.io != nil,末尾却无条件
a.io.ListChannels()。任何无 IO 的 Agent 调它都 panic —— 而 panic 报在
io 包里,根因在 tooldefs.go。已补。
5. **插入脚本用正则找"最后一个顶层字段"**:被嵌套 map 里的同形文本骗到,
823 处错误重排把文件改坏。改用括号深度 + 记录进入深度 3 的行号
(空 properties 会让深度在同一行进出平衡,只判 depth==2 不够)。
工具在 SDK 仓 tools/annotate_parallel/,复用时用绝对路径。
## 提示词措辞同步修正
「默认并行执行」→「尽量并发执行,但这是**逐工具判断**的」,并教模型
**把查询类放同一轮、写操作单独发一轮**(写和查混在一批,整批都串行)。
## 判据
- TestSerialOverridesParallelSafe Serial 优先于 ParallelSafe
- TestToolParallelDeclarationsAudit 并发面不许再归零
- TestNoToolDeclaresBothParallelAndSerial 两者同标即谎话
- TestBuiltinParallelDeclaredWhereDefined 声明写在定义处、且内核真读到
- TestStoreListIgnoresForeignJSON 压测抓到的 List() 缺陷
This commit is contained in:
@ -293,6 +293,24 @@ func (a *Agent) buildToolCatalog() string {
|
||||
// map[string]interface{} 字面量(约 20 行/条);本助手把它压成一次调用,
|
||||
// 只消除重复、不改变 schema 形状——properties 原样保留(空表仍序列化为 {}),
|
||||
// required 为空则整个键省略。
|
||||
// toolDefOption 是内置工具定义处的声明标记。
|
||||
//
|
||||
// ★ 形态与 SDK 的 NoMemory **完全同构**:声明写在**工具自己的定义里**,
|
||||
// 内核从定义读,没有任何硬编码名单表。
|
||||
//
|
||||
// toolDef("knowledge_search", "...", props, "toolParallel") // 默认串行
|
||||
// toolDef("knowledge_list", "...", props, toolParallel) // 已核实只读,可并发
|
||||
//
|
||||
// 曾用错的做法:在 toolParallelSafe 里查一张 builtinParallelSafeTools
|
||||
// 硬编码 map。那把声明从工具挪回了内核 —— 工具改名/新增不会自动跟着变,
|
||||
// 要靠一条 grep 源码的判据才能发现漂移,而判据一改就忘。
|
||||
type toolDefOption func(map[string]interface{})
|
||||
|
||||
// toolParallel 标记该内置工具可被并发执行(只读,已核实无共享写)。
|
||||
func toolParallel(fn map[string]interface{}) {
|
||||
fn["parallel_safe"] = true
|
||||
}
|
||||
|
||||
func toolDef(name, description string, properties map[string]interface{}, required ...string) map[string]interface{} {
|
||||
params := map[string]interface{}{
|
||||
"type": "object",
|
||||
@ -301,14 +319,37 @@ func toolDef(name, description string, properties map[string]interface{}, requir
|
||||
if len(required) > 0 {
|
||||
params["required"] = required
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"type": "function",
|
||||
"function": map[string]interface{}{
|
||||
"name": name,
|
||||
"description": description,
|
||||
"parameters": params,
|
||||
},
|
||||
fn := map[string]interface{}{
|
||||
"name": name,
|
||||
"description": description,
|
||||
"parameters": params,
|
||||
}
|
||||
// 并发声明走**变参 options**:不额外改签名,读工具表的老调用点一行不用动。
|
||||
for _, o := range parseToolDefOptions(required) {
|
||||
if o != nil {
|
||||
o(fn)
|
||||
}
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"type": "function",
|
||||
"function": fn,
|
||||
}
|
||||
}
|
||||
|
||||
// parseToolDefOptions 从 required 变参里分离出"声明项"。
|
||||
//
|
||||
// 为什么不单独加一个 options 变参:required 是 ...string,再加一个
|
||||
// ...toolDefOption 会让 33 个调用点里绝大多数(不需要声明的)也跟着改。
|
||||
// 混在一个变参里,声明就写在工具定义**那一行**,读代码时一眼可见。
|
||||
func parseToolDefOptions(required []string) []toolDefOption {
|
||||
var out []toolDefOption
|
||||
for _, r := range required {
|
||||
switch r {
|
||||
case "toolParallel":
|
||||
out = append(out, toolParallel)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (a *Agent) buildToolDefs() []interface{} {
|
||||
@ -387,8 +428,8 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
"query": map[string]interface{}{"type": "string", "description": "查询关键词"},
|
||||
"top_k": map[string]interface{}{"type": "integer", "description": "返回数量", "default": 5},
|
||||
"category": map[string]interface{}{"type": "string", "description": "可选:限定在某个分类内(前缀匹配子树,如 tech 会搜 tech/go、tech/rust)。留空则搜全库"},
|
||||
}, "query"))
|
||||
tools = append(tools, toolDef("knowledge_list", "列出知识库中所有知识分类。", map[string]interface{}{}))
|
||||
}, "query", "toolParallel"))
|
||||
tools = append(tools, toolDef("knowledge_list", "列出知识库中所有知识分类。", map[string]interface{}{}, "toolParallel"))
|
||||
}
|
||||
|
||||
if a.knowledge != nil {
|
||||
@ -429,7 +470,7 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
tools = append(tools, toolDef("doc_query", "查询文档记忆。输入查询内容,返回相关文档摘要。", map[string]interface{}{
|
||||
"query": map[string]interface{}{"type": "string", "description": "查询内容"},
|
||||
"top_k": map[string]interface{}{"type": "integer", "description": "返回数量", "default": 3},
|
||||
}, "query"))
|
||||
}, "query", "toolParallel"))
|
||||
tools = append(tools, toolDef("doc_commit", "提交一条文档记忆。将重要信息显式写入文档记忆层。", map[string]interface{}{
|
||||
"content": map[string]interface{}{"type": "string", "description": "文档内容"},
|
||||
"summary": map[string]interface{}{"type": "string", "description": "摘要(可选)"},
|
||||
@ -449,7 +490,7 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
if a.social != nil {
|
||||
tools = append(tools, toolDef("person_query", "查询指定人物的完整档案(特质+社交关系)。用于了解一个人的性格、喜好、背景和社交圈。", map[string]interface{}{
|
||||
"name": map[string]interface{}{"type": "string", "description": "人物名称"},
|
||||
}, "name"))
|
||||
}, "name", "toolParallel"))
|
||||
tools = append(tools, toolDef("person_set_trait", "记录/更新一个人的特质(性格、喜好、习惯等)。例如:person_set_trait(name=\"张三\", trait=\"喜欢\", value=\"红色\")。如果该特质已存在则覆盖。", map[string]interface{}{
|
||||
"name": map[string]interface{}{"type": "string", "description": "人物名称"},
|
||||
"trait": map[string]interface{}{"type": "string", "description": "特质名称,如:喜欢、性格、职业、年龄"},
|
||||
@ -463,7 +504,7 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
tools = append(tools, toolDef("person_network", "查询某人的社交网络(多度关系)。显示该人物周围的相关人物及其关系和特质。", map[string]interface{}{
|
||||
"name": map[string]interface{}{"type": "string", "description": "人物名称"},
|
||||
"depth": map[string]interface{}{"type": "integer", "description": "关系深度(默认2)", "default": 2},
|
||||
}, "name"))
|
||||
}, "name", "toolParallel"))
|
||||
}
|
||||
|
||||
if a.pluginReg != nil && a.pluginDir != "" {
|
||||
@ -473,7 +514,7 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
// 按插件动态拉取工具定义(避免全量注入提示词污染)
|
||||
tools = append(tools, toolDef("get_plugin_tools", "获取指定插件的完整工具定义(名称/参数/用途)。参数 plugin_name 传插件名(见系统提示的【可用工具能力】列表)。省略时返回全部插件的工具摘要。", map[string]interface{}{
|
||||
"plugin_name": map[string]interface{}{"type": "string", "description": "插件名,如 qq / remotedevice / weather", "default": ""},
|
||||
}))
|
||||
}, "toolParallel"))
|
||||
|
||||
tools = append(tools, toolDef("spawn_child", "启动一个异步子 Agent 执行独立任务。子 Agent 后台运行,不阻塞当前对话。完成后系统会自动通知你,届时请调用 child_result 工具查看输出。\n使用时机:多个互不依赖的子任务(如同时查三个网站、分别处理多个文件)可以在**同一轮**里一次 spawn 多个子 Agent——同轮调用默认并行,子 Agent 会各自后台启动(是否真正并发取决于工具的并发安全声明)。长耗时任务(批量处理、多轮搜索)也应交给子 Agent,避免阻塞对话。注意:一次 spawn 只是一个启动动作;要立刻拿到结果仍需另一次 `child_result` 调用。", map[string]interface{}{
|
||||
"task": map[string]interface{}{
|
||||
@ -493,7 +534,7 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
}, "task_id"))
|
||||
|
||||
if a.providerManager != nil {
|
||||
tools = append(tools, toolDef("llm_list_sources", "列出所有可用的 LLM 源(如 deepseek、openai、ollama),每个源有对应的 Lua 适配器和配置。如需切换 LLM 源,请使用 llm_set_source。", map[string]interface{}{}))
|
||||
tools = append(tools, toolDef("llm_list_sources", "列出所有可用的 LLM 源(如 deepseek、openai、ollama),每个源有对应的 Lua 适配器和配置。如需切换 LLM 源,请使用 llm_set_source。", map[string]interface{}{}, "toolParallel"))
|
||||
tools = append(tools, toolDef("llm_set_source", "切换当前 LLM 源到指定名称。变更立即生效,后续对话将使用新的 LLM 源。源名称可通过 llm_list_sources 查看。", map[string]interface{}{
|
||||
"name": map[string]interface{}{
|
||||
"type": "string",
|
||||
@ -502,7 +543,15 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
}, "name"))
|
||||
}
|
||||
|
||||
channels := a.io.ListChannels()
|
||||
// ⚠️ 这里必须判 nil:本函数开头对 a.io 做了 nil 保护(io 工具那段),
|
||||
// 末尾却没有,前后不一致。任何没有 IO 的 Agent(单测、ToolAPI 校验)
|
||||
// 调 buildToolDefs 都会 panic —— Go 允许对 nil 指针调方法,
|
||||
// panic 发生在 ListChannels 内部解引用字段时,症状出现在 io 包里,
|
||||
// 根因却在这里。
|
||||
var channels []agentIO.ChannelInfo
|
||||
if a.io != nil {
|
||||
channels = a.io.ListChannels()
|
||||
}
|
||||
for _, ch := range channels {
|
||||
if ch.Type != agentIO.DeviceOutput && ch.Type != agentIO.DeviceIO {
|
||||
continue
|
||||
@ -536,7 +585,7 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
tools = append(tools, toolDef("output_send__"+ch.Name+"_help", "查看 "+ch.Name+" 输出通道的 meta 格式说明和 type 枚举", map[string]interface{}{}))
|
||||
}
|
||||
|
||||
tools = append(tools, toolDef("output_list_channels", "列出所有可用输出通道及其能力(如 text/file/image/audio)和对应的输出门工具名称。", map[string]interface{}{}))
|
||||
tools = append(tools, toolDef("output_list_channels", "列出所有可用输出通道及其能力(如 text/file/image/audio)和对应的输出门工具名称。", map[string]interface{}{}, "toolParallel"))
|
||||
|
||||
// 父侧:驻留子控制面(单工具多动作,见设计 §7)。
|
||||
if a.parentID == "" {
|
||||
@ -586,7 +635,7 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
"type": "string",
|
||||
"description": "view=detail 时必填:inputch 名",
|
||||
},
|
||||
}))
|
||||
}, "toolParallel"))
|
||||
|
||||
if a.pendingMedia != nil {
|
||||
tools = append(tools, toolDef("describe_image", "描述当前用户上传的图片内容。使用配置的多模态模型或默认 LLM 进行识别。调用此工具后你将获得图片的详细文字描述。", map[string]interface{}{
|
||||
|
||||
Reference in New Issue
Block a user