feat(mcp-go,dispatcher): 自主 agent 工具集动态化 —— list_tools 自描述 + 动态发现

杜绝硬编码:自主 agent 的工具菜单不再写死在 dispatcher,而是从 mcp-go
注册表(单一事实源)动态发现。加工具只改 mcp-go 一处,dispatcher 零改动。

- mcp-go:toolDef 增 agent/agentName/params/inject 元信息(paramSpec 声明
  模型可填参数;inject 声明服务端注入、不暴露给模型的参数如 user_id);
  list_tools 上报这些。当前标 agent 的 4 个:wiki_search / recall_user_memory
  / remember_user_fact / history_get。
- dispatcher:agentTools() 改为调 list_tools → 取 agent_exposed → 按上报的
  params 建 schema.ToolInfo → 生成 mcpTool;inject 参数(user_id/session_id/
  kb/task_id)运行时绑定。删除硬编码的 2 个工具。

验收:实测自主 agent 调用新暴露的 remember_user_fact(memory_upsert)成功——
参数由模型按 schema 自生成(key/value),user_id 服务端注入(map 带 task_id
佐证);make test-go 全绿;管理端状态面板兼容(忽略多余 JSON 字段)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-23 14:29:20 +08:00
parent 45ad852806
commit 7388f2741d
3 changed files with 133 additions and 53 deletions
@@ -85,42 +85,75 @@ func (m *mcpTool) InvokableRun(ctx context.Context, argsJSON string, _ ...tool.O
return res.Content, nil
}
// agentTools 构建 ReAct 可用的工具集(按当前任务上下文绑定 uid/kb)。
// 工具名是给"模型看"的语义名;mcpName 是实际 NATS 调用名。context 参数(user_id)服务端注入,不暴露给模型。
// toolCatalogEntry 是 MCP list_tools 上报的一条工具元信息(与 mcp-go listTools 输出对齐)。
type toolCatalogEntry struct {
Name string `json:"name"`
CN string `json:"cn"`
Desc string `json:"desc"`
Agent bool `json:"agent_exposed"`
AgentName string `json:"agent_name"`
Params []struct {
Name string `json:"name"`
Type string `json:"type"`
Desc string `json:"desc"`
Required bool `json:"required"`
} `json:"params"`
Inject []string `json:"inject"`
}
// agentTools 动态构建 ReAct 可用的工具集:调 MCP list_tools 自描述目录 → 取 agent_exposed 的工具
// → 用上报的参数 schema 建 InvokableToolinject 参数(user_id/session_id/kb/task_id)服务端运行时
// 绑定、不暴露给模型。新增工具只需在 mcp-go 注册表标 agent,无需改这里(杜绝硬编码)。
func (o *Orchestrator) agentTools(b *board, taskID string, tr *execTracer) []tool.BaseTool {
if o.tools == nil {
return nil
}
kbBind := map[string]any{}
if b.kb != "" {
kbBind["kb"] = b.kb
cctx, cancel := context.WithTimeout(context.Background(), toolCallTimeout)
defer cancel()
res, err := o.tools.CallTool(cctx, contract.ToolSubjectGo("list_tools"), &contract.ToolCall{Tool: "list_tools"})
if err != nil || res == nil || !res.OK {
return nil
}
return []tool.BaseTool{
&mcpTool{
mcpName: "wiki_search",
var cat struct {
Tools []toolCatalogEntry `json:"tools"`
}
if json.Unmarshal([]byte(res.Content), &cat) != nil {
return nil
}
// inject 参数名 → 本任务的运行时值(不进模型菜单)。
injectVal := map[string]any{"user_id": b.uid, "session_id": b.sid, "task_id": taskID, "kb": b.kb}
var out []tool.BaseTool
for _, e := range cat.Tools {
if !e.Agent {
continue
}
params := map[string]*schema.ParameterInfo{}
for _, p := range e.Params {
params[p.Name] = &schema.ParameterInfo{Type: schema.DataType(p.Type), Desc: p.Desc, Required: p.Required}
}
bind := map[string]any{}
for _, inj := range e.Inject {
if v, ok := injectVal[inj]; ok && v != "" {
bind[inj] = v
}
}
name := e.AgentName
if name == "" {
name = e.Name
}
out = append(out, &mcpTool{
mcpName: e.Name,
subject: contract.ToolSubjectGo,
caller: o.tools, taskID: taskID, tr: tr,
bind: kbBind,
bind: bind,
info: &schema.ToolInfo{
Name: "wiki_search",
Desc: "检索知识库,返回与查询最相关的资料片段。需要外部知识/事实依据时调用。",
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"q": {Type: schema.String, Desc: "检索查询语句", Required: true},
}),
Name: name, Desc: e.Desc,
ParamsOneOf: schema.NewParamsOneOfByParams(params),
},
},
&mcpTool{
mcpName: "memory_get",
subject: contract.ToolSubjectGo,
caller: o.tools, taskID: taskID, tr: tr,
bind: map[string]any{"user_id": b.uid},
info: &schema.ToolInfo{
Name: "recall_user_memory",
Desc: "召回当前用户的长期画像与偏好(称呼/职业/回答偏好等)。需要个性化、了解“我是谁”时调用。",
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{}),
},
},
})
}
return out
}
// runReactAgent 执行带"自主工具"的 agent 节点:模型在 ReAct 循环里自行决定调哪些 MCP 工具。