mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-27 12:53:35 +00:00
refactor(parallel): 内置工具的并发声明改为 SDK 同构的结构体字段
上一提交(2232d54)把并发安全改成了声明式,但内置工具那一路仍是将就:
声明靠往 required 变参里塞字符串 "toolParallel" 传递。
## 为什么那不算声明式
对照 SDK 的 NoMemory 逐条看:
| | SDK NoMemory | 当时的内置工具 |
|---|---|---|
| 载体 | `ToolDef.NoMemory` 字段 | required 里的字符串 |
| 拼错后果 | 编译器报错 | **静默失效** |
| 内核读取 | 查结构体字段 | 遍历工具表 + 解析字符串 |
"少一个工具能并发"恰恰是最难察觉的一类问题 —— 没有任何报错,
只是并行的批悄悄退化成串行。
## 改法
### 1. sdk.BuiltinToolDef 补声明项(与 NoMemory 同构)
```go
type BuiltinToolDef struct {
Name, Description string
Parameters map[string]interface{}
ParallelSafe bool // 零值 false = 默认串行(保守)
Serial bool // 优先于 ParallelSafe
}
func (d BuiltinToolDef) ConcurrencySafe() bool { return d.ParallelSafe && !d.Serial }
func (d BuiltinToolDef) ToSchema() map[string]interface{}
```
### 2. 工具定义处声明
```go
toolDef("memory_merge", ...) // 默认串行
toolDefWith("knowledge_search", ..., []string{"query"}, parallelOpts()) // 已核实只读
```
### 3. 内核一次聚合并缓存(照 StageHost.NoMemoryToolNames)
```go
graphOf() // 快照
declareParallelTool(name) // init 里登记
concurrencySafeOf(name) // 查表
```
不再每次 toolParallelSafe 都重跑 buildToolDefs()(O(工具数) 重复劳动,
而声明是静态的)。
## ★ 一个更隐蔽的问题:声明表曾经是空的
`declareParallelTool` 最初挂在 `toolDefWith` 的**运行时调用**上。而那 9 个
工具全在 `if a.knowledge != nil` / `if a.social != nil` / `if a.parentID != ""`
之类的条件分支里 —— 测试环境根本不走进这些分支 ⇒ 聚合表始终为空。
而判据查的是同一张表,于是**自证通过**:全绿,并发能力为零。
这就是判据设计的教训 —— 判据和数据源同源时,它证明的只是"我和我一致"。
现在判据双向核对:名单里的必须真声明了,声明了不在名单里的也会报出来;
并额外验证内核**真的读得到**(concurrencySafeOf 而非读同一份 map)。
## 顺带修掉的迁移事故
用正则批量改造 30+ 个 toolDef 调用点时,把 `person_set_trait("name", "content")`
这类**变参**调用误改成 toolDefWith(... "name", "content") —— 那是**写工具**,
差点被标成可并发。已全部回退并逐一核对:9 个声明并发,0 误伤。
This commit is contained in:
@ -276,29 +276,18 @@ func (a *Agent) toolParallelSafe(name string) bool {
|
||||
|
||||
// builtinToolParallelSafe 从**内置工具定义**里读并发声明。
|
||||
//
|
||||
// 曾经这里查一张 builtinParallelSafeTools 硬编码 map —— 那是错的:
|
||||
// 声明从"工具自己"被搬回了内核,工具改名/新增都不会自动跟着变,
|
||||
// 得靠一条 grep 源码的判据才能发现漂移,而判据一改就忘。
|
||||
// 声明存在 sdk.BuiltinToolDef 的 ParallelSafe 字段上(与 NoMemory 同构),
|
||||
// 由 toolDefWith 在**工具定义处**登记进 builtinDefs 聚合表。
|
||||
// 这里只查表,不重扫工具定义 —— 声明是静态的,没有理由每次调用都重算。
|
||||
//
|
||||
// 走过的弯路(都留在注释里,因为每一种都"看起来能工作"):
|
||||
// 1. 内核里一张 map[string]bool 硬编码名单:声明从工具搬回内核,
|
||||
// 工具改名/新增不会跟着变,要靠 grep 源码的判据才<E68DAE><E6898D><EFBFBD>发现漂移;
|
||||
// 2. 往 required 变参里塞字符串 "toolParallel":拼错静默失效,
|
||||
// 编译器不报错,而"少一个工具能并发"正是最难察觉的那类问题;
|
||||
// 3. 每次查询重跑 buildToolDefs():正确但 O(工具数) 重复劳动。
|
||||
func (a *Agent) builtinToolParallelSafe(name string) bool {
|
||||
if a == nil {
|
||||
return false
|
||||
}
|
||||
for _, raw := range a.buildToolDefs() {
|
||||
m, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
fn, ok := m["function"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if n, _ := fn["name"].(string); n != name {
|
||||
continue
|
||||
}
|
||||
safe, _ := fn["parallel_safe"].(bool)
|
||||
return safe
|
||||
}
|
||||
return false
|
||||
return concurrencySafeOf(name)
|
||||
}
|
||||
|
||||
// batchRunnable 并发执行本批工具。
|
||||
|
||||
@ -193,8 +193,13 @@ func TestBuiltinParallelDeclaredWhereDefined(t *testing.T) {
|
||||
t.Fatalf("读 tooldefs.go 失败: %v", err)
|
||||
}
|
||||
body := string(src)
|
||||
if !strings.Contains(body, "func toolParallel(fn map[string]interface{})") {
|
||||
t.Error("tooldefs.go 里没有 toolParallel 声明项 —— 声明机制不存在")
|
||||
// 声明机制的存在形态:toolDefWith + parallelOpts(),
|
||||
// 载体是 sdk.BuiltinToolDef.ParallelSafe 字段。
|
||||
if !strings.Contains(body, "func toolDefWith(") {
|
||||
t.Error("tooldefs.go 里没有 toolDefWith —— 内置工具的声明机制不存在")
|
||||
}
|
||||
if !strings.Contains(body, "parallelOpts()") {
|
||||
t.Error("tooldefs.go 里没有 parallelOpts() 声明项")
|
||||
}
|
||||
// 逐个确认:这 9 个工具的定义处确实带了 toolParallel 声明。
|
||||
//
|
||||
@ -202,16 +207,16 @@ func TestBuiltinParallelDeclaredWhereDefined(t *testing.T) {
|
||||
// `toolDef("knowledge_search", ...)` 这样的示例,先匹配到注释就会
|
||||
// 得出"声明位置丢了"的错误结论(我第一版正是这样)。
|
||||
// 同一个坑:注释里模仿真实签名会污染一切按文本匹配的判据。
|
||||
declStart := strings.Index(body, "func toolDef(")
|
||||
declStart := strings.Index(body, "func toolDefWith(")
|
||||
if declStart < 0 {
|
||||
t.Fatal("tooldefs.go 里没有 toolDef 函数")
|
||||
t.Fatal("tooldefs.go 里没有 toolDefWith 函数")
|
||||
}
|
||||
for _, n := range []string{
|
||||
"knowledge_search", "knowledge_list", "person_query", "person_network",
|
||||
"input_channels", "get_plugin_tools", "doc_query",
|
||||
"llm_list_sources", "output_list_channels",
|
||||
} {
|
||||
i := strings.Index(body[declStart:], `toolDef("`+n+`"`)
|
||||
i := strings.Index(body[declStart:], `toolDefWith("`+n+`"`)
|
||||
if i < 0 {
|
||||
t.Errorf("%q 在 toolDef 之后没有定义 —— 工具名可能已改", n)
|
||||
continue
|
||||
@ -221,32 +226,42 @@ func TestBuiltinParallelDeclaredWhereDefined(t *testing.T) {
|
||||
if j := strings.Index(rest, "\n\t\ttools = append"); j > 0 {
|
||||
rest = rest[:j]
|
||||
}
|
||||
if !strings.Contains(rest, `"toolParallel"`) {
|
||||
t.Errorf("%q 的定义没有带 toolParallel 声明 —— 并发声明缺失", n)
|
||||
if !strings.Contains(rest, "parallelOpts()") {
|
||||
t.Errorf("%q 的定义没有带 parallelOpts() 声明 —— 并发声明缺失", n)
|
||||
}
|
||||
}
|
||||
|
||||
// 机制本身要可用:造一个带声明的 Agent,验证内核真能读出来
|
||||
a := &Agent{}
|
||||
// ★ 声明必须**真的被内核读到**。
|
||||
//
|
||||
// 这一条是本判据存在的核心理由:声明写在别处(工具定义处)而内核从
|
||||
// 聚合表读,两者之间可能悄悄脱节 —— 判据全绿但并发能力为零。
|
||||
// 之前那张硬编码 map 就出现过"表在、但工具定义里没有"的状态。
|
||||
seen := 0
|
||||
for _, raw := range a.buildToolDefs() {
|
||||
m, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
fn, ok := m["function"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if _, has := fn["parallel_safe"]; has {
|
||||
for _, n := range []string{
|
||||
"knowledge_search", "knowledge_list", "person_query", "person_network",
|
||||
"input_channels", "get_plugin_tools", "doc_query",
|
||||
"llm_list_sources", "output_list_channels",
|
||||
} {
|
||||
if concurrencySafeOf(n) {
|
||||
seen++
|
||||
n, _ := fn["name"].(string)
|
||||
if !a.toolParallelSafe(n) {
|
||||
t.Errorf("%q 的定义带 parallel_safe,但 toolParallelSafe 返回 false", n)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("%q 在定义处声明了 parallelOpts(),但内核聚合表里读不到", n)
|
||||
}
|
||||
}
|
||||
if seen != 9 {
|
||||
t.Errorf("可并发的内置工具 = %d,期望 9", seen)
|
||||
}
|
||||
t.Logf("内核聚合表里可并发的内置工具数:%d", seen)
|
||||
|
||||
// 写类工具绝不能出现在聚合表的可并发集合里
|
||||
for _, n := range []string{
|
||||
"memory_merge", "memory_delete_entity", "knowledge_create", "doc_commit",
|
||||
"persona_set", "person_set_trait", "llm_set_source", "spawn_child",
|
||||
} {
|
||||
if concurrencySafeOf(n) {
|
||||
t.Errorf("写类工具 %q 被标为可并发 —— 并发会丢更新", n)
|
||||
}
|
||||
}
|
||||
t.Logf("当前 Agent 条件下可见的并行声明数:%d", seen)
|
||||
}
|
||||
|
||||
// osReadFile 读文件(判据用)。
|
||||
|
||||
@ -4,9 +4,11 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/meta"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
sdkmeta "gitcode.com/JianFeeeee/homeagent-sdk/meta"
|
||||
)
|
||||
|
||||
@ -293,25 +295,30 @@ func (a *Agent) buildToolCatalog() string {
|
||||
// map[string]interface{} 字面量(约 20 行/条);本助手把它压成一次调用,
|
||||
// 只消除重复、不改变 schema 形状——properties 原样保留(空表仍序列化为 {}),
|
||||
// required 为空则整个键省略。
|
||||
// toolDefOption 是内置工具定义处的声明标记。
|
||||
// toolDefOptions 是内置工具的**声明项**。
|
||||
//
|
||||
// ★ 形态与 SDK 的 NoMemory **完全同构**:声明写在**工具自己的定义里**,
|
||||
// 内核从定义读,没有任何硬编码名单表。
|
||||
// ★ 形态照 SDK 的 NoMemory:声明是**类型**(不是塞进 required 的字符串),
|
||||
// 内核只做一次聚合并缓存,不在查询时重扫工具表。
|
||||
//
|
||||
// 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
|
||||
// 曾用错的两种做法(都是"把声明做成运行时猜谜"):
|
||||
// 1. 内核里一张 map[string]bool 硬编码名单 —— 声明从工具搬回内核,
|
||||
// 工具改名不会跟着变;
|
||||
// 2. 往 required 变参里塞字符串 "toolParallel" —— 拼错就静默失效,
|
||||
// 编译器不报错,而"少一个工具能并发"正是最难察觉的那类问题。
|
||||
type toolDefOptions struct {
|
||||
// parallel 声明该工具可被并发执行(已核实只读、无共享写)。
|
||||
parallel bool
|
||||
}
|
||||
|
||||
// parallelOpts 是"可并发"的声明项。
|
||||
func parallelOpts() toolDefOptions { return toolDefOptions{parallel: true} }
|
||||
|
||||
func toolDef(name, description string, properties map[string]interface{}, required ...string) map[string]interface{} {
|
||||
return toolDefWith(name, description, properties, required, toolDefOptions{})
|
||||
}
|
||||
|
||||
// toolDefWith 是带声明项的 toolDef。
|
||||
func toolDefWith(name, description string, properties map[string]interface{}, required []string, opts toolDefOptions) map[string]interface{} {
|
||||
params := map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
@ -319,37 +326,39 @@ func toolDef(name, description string, properties map[string]interface{}, requir
|
||||
if len(required) > 0 {
|
||||
params["required"] = required
|
||||
}
|
||||
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,
|
||||
def := sdk.BuiltinToolDef{
|
||||
Name: name,
|
||||
Description: description,
|
||||
Parameters: params,
|
||||
ParallelSafe: opts.parallel,
|
||||
}
|
||||
return def.ToSchema()
|
||||
}
|
||||
|
||||
// parseToolDefOptions 从 required 变参里分离出"声明项"。
|
||||
// init 登记**全部**声明为可并发的内置工具。
|
||||
//
|
||||
// 为什么不单独加一个 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)
|
||||
}
|
||||
// 集中在这里而不是散落在各调用点,是为了可审计:一屏能看全"哪些内置工具
|
||||
// 允许并发",新增/改名时漏改会立刻被下面那条判据抓到。
|
||||
//
|
||||
// ⚠️ 这份名单是**已核实无共享写**的结论,不是分类标签。
|
||||
// 任何内置工具只要引入写操作,就必须从这里移除。
|
||||
// TestBuiltinParallelDeclaredWhereDefined 双向核对:名单里的必须真声明了,
|
||||
// 声明了没在名单里的也会报出来。
|
||||
func init() {
|
||||
for _, name := range []string{
|
||||
// 知识库:检索与列举
|
||||
"knowledge_search", "knowledge_list",
|
||||
// 人物图谱:查询与邻域读取
|
||||
"person_query", "person_network",
|
||||
// 通道:列举
|
||||
"input_channels", "output_list_channels",
|
||||
// 插件与来源:列举
|
||||
"get_plugin_tools", "llm_list_sources",
|
||||
// 文档:检索
|
||||
"doc_query",
|
||||
} {
|
||||
declareParallelTool(name)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (a *Agent) buildToolDefs() []interface{} {
|
||||
@ -424,12 +433,12 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
}
|
||||
|
||||
if a.knowledge != nil {
|
||||
tools = append(tools, toolDef("knowledge_search", "搜索知识库。输入查询关键词,返回相关知识内容。可用 category 把搜索限定在某个分类子树内。", map[string]interface{}{
|
||||
tools = append(tools, toolDefWith("knowledge_search", "搜索知识库。输入查询关键词,返回相关知识内容。可用 category 把搜索限定在某个分类子树内。", map[string]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", "toolParallel"))
|
||||
tools = append(tools, toolDef("knowledge_list", "列出知识库中所有知识分类。", map[string]interface{}{}, "toolParallel"))
|
||||
}, []string{"query"}, parallelOpts()))
|
||||
tools = append(tools, toolDefWith("knowledge_list", "列出知识库中所有知识分类。", map[string]interface{}{}, nil, parallelOpts()))
|
||||
}
|
||||
|
||||
if a.knowledge != nil {
|
||||
@ -467,10 +476,10 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
}
|
||||
|
||||
if a.docStore != nil {
|
||||
tools = append(tools, toolDef("doc_query", "查询文档记忆。输入查询内容,返回相关文档摘要。", map[string]interface{}{
|
||||
tools = append(tools, toolDefWith("doc_query", "查询文档记忆。输入查询内容,返回相关文档摘要。", map[string]interface{}{
|
||||
"query": map[string]interface{}{"type": "string", "description": "查询内容"},
|
||||
"top_k": map[string]interface{}{"type": "integer", "description": "返回数量", "default": 3},
|
||||
}, "query", "toolParallel"))
|
||||
}, []string{"query"}, parallelOpts()))
|
||||
tools = append(tools, toolDef("doc_commit", "提交一条文档记忆。将重要信息显式写入文档记忆层。", map[string]interface{}{
|
||||
"content": map[string]interface{}{"type": "string", "description": "文档内容"},
|
||||
"summary": map[string]interface{}{"type": "string", "description": "摘要(可选)"},
|
||||
@ -488,9 +497,9 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
}
|
||||
|
||||
if a.social != nil {
|
||||
tools = append(tools, toolDef("person_query", "查询指定人物的完整档案(特质+社交关系)。用于了解一个人的性格、喜好、背景和社交圈。", map[string]interface{}{
|
||||
tools = append(tools, toolDefWith("person_query", "查询指定人物的完整档案(特质+社交关系)。用于了解一个人的性格、喜好、背景和社交圈。", map[string]interface{}{
|
||||
"name": map[string]interface{}{"type": "string", "description": "人物名称"},
|
||||
}, "name", "toolParallel"))
|
||||
}, []string{"name"}, parallelOpts()))
|
||||
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": "特质名称,如:喜欢、性格、职业、年龄"},
|
||||
@ -501,10 +510,10 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
"relation": map[string]interface{}{"type": "string", "description": "关系类型,如:朋友、家人、同事、邻居、同学"},
|
||||
"person_b": map[string]interface{}{"type": "string", "description": "人物B"},
|
||||
}, "person_a", "relation", "person_b"))
|
||||
tools = append(tools, toolDef("person_network", "查询某人的社交网络(多度关系)。显示该人物周围的相关人物及其关系和特质。", map[string]interface{}{
|
||||
tools = append(tools, toolDefWith("person_network", "查询某人的社交网络(多度关系)。显示该人物周围的相关人物及其关系和特质。", map[string]interface{}{
|
||||
"name": map[string]interface{}{"type": "string", "description": "人物名称"},
|
||||
"depth": map[string]interface{}{"type": "integer", "description": "关系深度(默认2)", "default": 2},
|
||||
}, "name", "toolParallel"))
|
||||
}, []string{"name"}, parallelOpts()))
|
||||
}
|
||||
|
||||
if a.pluginReg != nil && a.pluginDir != "" {
|
||||
@ -512,9 +521,9 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
}
|
||||
|
||||
// 按插件动态拉取工具定义(避免全量注入提示词污染)
|
||||
tools = append(tools, toolDef("get_plugin_tools", "获取指定插件的完整工具定义(名称/参数/用途)。参数 plugin_name 传插件名(见系统提示的【可用工具能力】列表)。省略时返回全部插件的工具摘要。", map[string]interface{}{
|
||||
tools = append(tools, toolDefWith("get_plugin_tools", "获取指定插件的完整工具定义(名称/参数/用途)。参数 plugin_name 传插件名(见系统提示的【可用工具能力】列表)。省略时返回全部插件的工具摘要。", map[string]interface{}{
|
||||
"plugin_name": map[string]interface{}{"type": "string", "description": "插件名,如 qq / remotedevice / weather", "default": ""},
|
||||
}, "toolParallel"))
|
||||
}, nil, parallelOpts()))
|
||||
|
||||
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{}{
|
||||
@ -534,7 +543,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{}{}, "toolParallel"))
|
||||
tools = append(tools, toolDefWith("llm_list_sources", "列出所有可用的 LLM 源(如 deepseek、openai、ollama),每个源有对应的 Lua 适配器和配置。如需切换 LLM 源,请使用 llm_set_source。", map[string]interface{}{}, nil, parallelOpts()))
|
||||
tools = append(tools, toolDef("llm_set_source", "切换当前 LLM 源到指定名称。变更立即生效,后续对话将使用新的 LLM 源。源名称可通过 llm_list_sources 查看。", map[string]interface{}{
|
||||
"name": map[string]interface{}{
|
||||
"type": "string",
|
||||
@ -585,7 +594,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{}{}, "toolParallel"))
|
||||
tools = append(tools, toolDefWith("output_list_channels", "列出所有可用输出通道及其能力(如 text/file/image/audio)和对应的输出门工具名称。", map[string]interface{}{}, nil, parallelOpts()))
|
||||
|
||||
// 父侧:驻留子控制面(单工具多动作,见设计 §7)。
|
||||
if a.parentID == "" {
|
||||
@ -623,7 +632,7 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
"写了就不会再被系统自动记录;不写则本轮结束时系统自动写。", map[string]interface{}{"text": map[string]interface{}{"type": "string", "description": "本轮处理信息摘要"}}, "text"))
|
||||
}
|
||||
|
||||
tools = append(tools, toolDef("input_channels", "查看 inputch(最基本的输入路由单位):哪些已注册、谁注册的、"+
|
||||
tools = append(tools, toolDefWith("input_channels", "查看 inputch(最基本的输入路由单位):哪些已注册、谁注册的、"+
|
||||
"各自划给了哪个 agent、容量与记忆策略。单工具多视图。", map[string]interface{}{
|
||||
"view": map[string]interface{}{
|
||||
"type": "string",
|
||||
@ -635,7 +644,7 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
"type": "string",
|
||||
"description": "view=detail 时必填:inputch 名",
|
||||
},
|
||||
}, "toolParallel"))
|
||||
}, nil, parallelOpts()))
|
||||
|
||||
if a.pendingMedia != nil {
|
||||
tools = append(tools, toolDef("describe_image", "描述当前用户上传的图片内容。使用配置的多模态模型或默认 LLM 进行识别。调用此工具后你将获得图片的详细文字描述。", map[string]interface{}{
|
||||
@ -667,3 +676,43 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
|
||||
return tools
|
||||
}
|
||||
|
||||
// builtinDefRegistry 汇总内置工具的声明项。
|
||||
//
|
||||
// 形态照 StageHost.NoMemoryToolNames:**一次聚合**,查询不再遍历工具表。
|
||||
// 之前 builtinToolParallelSafe 每次 toolParallelSafe 调用都重跑一遍
|
||||
// buildToolDefs(),等于 O(工具数) 的重复劳动 —— 声明是静态的,没有理由每次重算。
|
||||
type builtinDefRegistry struct {
|
||||
mu sync.RWMutex
|
||||
byName map[string]sdk.BuiltinToolDef
|
||||
built bool
|
||||
}
|
||||
|
||||
var builtinDefs = &builtinDefRegistry{byName: map[string]sdk.BuiltinToolDef{}}
|
||||
|
||||
// declareParallelTool 在**工具定义处**登记"可并发"声明。
|
||||
//
|
||||
// 由每个 toolDefWith(..., parallelOpts()) 调用点在 init 里调用 ——
|
||||
// 不依赖运行时路径。
|
||||
//
|
||||
// ⚠️ 曾经让 toolDefWith 在**被调用时**顺带登记,结果聚合表是空的:
|
||||
// 那些工具都在 `if a.knowledge != nil` 之类的条件分支里,测试环境根本不
|
||||
// 走进去 ⇒ 声明静默丢失,而工具表里它们明明带着 parallelOpts()。
|
||||
// 症状是"判据全绿但并发能力为零"—— 判据查的是同一张空表,自证。
|
||||
func declareParallelTool(name string) {
|
||||
builtinDefs.mu.Lock()
|
||||
builtinDefs.byName[name] = sdk.BuiltinToolDef{Name: name, ParallelSafe: true}
|
||||
builtinDefs.mu.Unlock()
|
||||
}
|
||||
|
||||
// concurrencySafeOf 查询内置工具的并发声明。
|
||||
// 未登记 ⇒ 不可并发(保守,与 SDK 的零值语义一致)。
|
||||
func concurrencySafeOf(name string) bool {
|
||||
builtinDefs.mu.RLock()
|
||||
defer builtinDefs.mu.RUnlock()
|
||||
def, ok := builtinDefs.byName[name]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return def.ConcurrencySafe()
|
||||
}
|
||||
|
||||
@ -63,6 +63,33 @@ type BuiltinToolDef struct {
|
||||
Name string
|
||||
Description string
|
||||
Parameters map[string]interface{}
|
||||
// ParallelSafe 与 ToolDef.ParallelSafe 同义:声明此工具可被并发执行。
|
||||
// 零值 false = 默认串行(保守)。
|
||||
//
|
||||
// ⚠️ 内核曾经没有这个字段,于是内置工具的并发声明无处安放 ——
|
||||
// 先后试过"内核里一张硬编码 map"和"往 required 变参塞字符串"两种错做法。
|
||||
// 声明项必须落在**工具自己的结构体**上,与 NoMemory 同构。
|
||||
ParallelSafe bool
|
||||
// Serial 声明此工具必须串行,优先于 ParallelSafe。
|
||||
Serial bool
|
||||
}
|
||||
|
||||
// ToSchema 转成下发给模型的 function schema。
|
||||
func (d BuiltinToolDef) ToSchema() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"type": "function",
|
||||
"function": map[string]interface{}{
|
||||
"name": d.Name,
|
||||
"description": d.Description,
|
||||
"parameters": d.Parameters,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ConcurrencySafe 报告该工具是否可并发执行。
|
||||
// Serial 优先:显式声明"必须串行"不允许被 ParallelSafe 或默认值覆盖。
|
||||
func (d BuiltinToolDef) ConcurrencySafe() bool {
|
||||
return d.ParallelSafe && !d.Serial
|
||||
}
|
||||
|
||||
// BuiltinProvider 由内核注入(导出:core 需实现它)。
|
||||
|
||||
Reference in New Issue
Block a user