mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +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:
@ -42,33 +42,33 @@ func init() {
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
name string
|
||||
mu sync.Mutex
|
||||
reports []llmReport
|
||||
sessionID string
|
||||
name string
|
||||
mu sync.Mutex
|
||||
reports []llmReport
|
||||
sessionID string
|
||||
selfToolNames map[string]bool
|
||||
checkMu sync.Mutex
|
||||
checkMu sync.Mutex
|
||||
|
||||
stopCh chan struct{}
|
||||
perfData PerfData
|
||||
stopCh chan struct{}
|
||||
perfData PerfData
|
||||
|
||||
autoInterval time.Duration
|
||||
llmTimeout time.Duration
|
||||
llmMaxTurns int
|
||||
llmMaxTokens int
|
||||
perfHistory int
|
||||
autoInterval time.Duration
|
||||
llmTimeout time.Duration
|
||||
llmMaxTurns int
|
||||
llmMaxTokens int
|
||||
perfHistory int
|
||||
}
|
||||
|
||||
type PerfData struct {
|
||||
LastCheck time.Time `json:"last_check"`
|
||||
Checks []PerfCheckPoint `json:"checks"`
|
||||
LastCheck time.Time `json:"last_check"`
|
||||
Checks []PerfCheckPoint `json:"checks"`
|
||||
}
|
||||
type PerfCheckPoint struct {
|
||||
Time time.Time `json:"time"`
|
||||
Passed int `json:"passed"`
|
||||
Failed int `json:"failed"`
|
||||
Total int `json:"total"`
|
||||
ElapsedMs int64 `json:"elapsed_ms"`
|
||||
Time time.Time `json:"time"`
|
||||
Passed int `json:"passed"`
|
||||
Failed int `json:"failed"`
|
||||
Total int `json:"total"`
|
||||
ElapsedMs int64 `json:"elapsed_ms"`
|
||||
}
|
||||
|
||||
func New(name string) *Plugin {
|
||||
@ -170,6 +170,8 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
"plugin": map[string]interface{}{"type": "string", "description": "可选:指定只检查该插件的健康状态(列出插件工具并逐一测试),不填则检查全部插件"},
|
||||
},
|
||||
},
|
||||
// 共享 p.mu 写锁,且 healthcheck_report 会 append p.reports
|
||||
Serial: true,
|
||||
}, func(args map[string]interface{}) (interface{}, error) {
|
||||
plugin, _ := args["plugin"].(string)
|
||||
return p.runFullCheck(s, plugin)
|
||||
@ -183,6 +185,8 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{},
|
||||
},
|
||||
// 共享 p.mu 写锁,且 healthcheck_report 会 append p.reports
|
||||
Serial: true,
|
||||
}, func(args map[string]interface{}) (interface{}, error) {
|
||||
return p.checkPlugins(s)
|
||||
})
|
||||
@ -195,6 +199,8 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{},
|
||||
},
|
||||
// 共享 p.mu 写锁,且 healthcheck_report 会 append p.reports
|
||||
Serial: true,
|
||||
}, func(args map[string]interface{}) (interface{}, error) {
|
||||
return p.listAllTools(s)
|
||||
})
|
||||
@ -207,6 +213,8 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{},
|
||||
},
|
||||
// 共享 p.mu 写锁,且 healthcheck_report 会 append p.reports
|
||||
Serial: true,
|
||||
}, func(args map[string]interface{}) (interface{}, error) {
|
||||
return p.checkMemory(s)
|
||||
})
|
||||
@ -224,6 +232,8 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
},
|
||||
"required": []string{"tool_name", "status"},
|
||||
},
|
||||
// 共享 p.mu 写锁,且 healthcheck_report 会 append p.reports
|
||||
Serial: true,
|
||||
}, func(args map[string]interface{}) (interface{}, error) {
|
||||
toolName, _ := args["tool_name"].(string)
|
||||
status, _ := args["status"].(string)
|
||||
@ -245,6 +255,8 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{},
|
||||
},
|
||||
// 共享 p.mu 写锁,且 healthcheck_report 会 append p.reports
|
||||
Serial: true,
|
||||
}, func(args map[string]interface{}) (interface{}, error) {
|
||||
return s.Status().GetKernelStatus(), nil
|
||||
})
|
||||
@ -258,6 +270,8 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{},
|
||||
},
|
||||
// 共享 p.mu 写锁,且 healthcheck_report 会 append p.reports
|
||||
Serial: true,
|
||||
}, func(args map[string]interface{}) (interface{}, error) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
@ -268,8 +282,8 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
failed += c.Failed
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"status": "ok",
|
||||
"last_check": p.perfData.LastCheck,
|
||||
"status": "ok",
|
||||
"last_check": p.perfData.LastCheck,
|
||||
"total_checks": len(p.perfData.Checks),
|
||||
"total_passed": passed,
|
||||
"total_failed": failed,
|
||||
@ -833,16 +847,16 @@ func (p *Plugin) collectToolDefsForLLM(s *sdk.PluginSDK, pluginFilter string) []
|
||||
func isSafeReadonlyTool(name string) bool {
|
||||
// 明确只读的查询/列表类工具
|
||||
readonlyExact := map[string]bool{
|
||||
"memory_recall": true,
|
||||
"memory_introspect": true,
|
||||
"doc_query": true,
|
||||
"knowledge_search": true,
|
||||
"knowledge_list": true,
|
||||
"person_query": true,
|
||||
"person_network": true,
|
||||
"llm_list_sources": true,
|
||||
"memory_recall": true,
|
||||
"memory_introspect": true,
|
||||
"doc_query": true,
|
||||
"knowledge_search": true,
|
||||
"knowledge_list": true,
|
||||
"person_query": true,
|
||||
"person_network": true,
|
||||
"llm_list_sources": true,
|
||||
"output_list_channels": true,
|
||||
"terminal_list": true,
|
||||
"terminal_list": true,
|
||||
}
|
||||
if readonlyExact[name] {
|
||||
return true
|
||||
|
||||
Reference in New Issue
Block a user