From 7388f2741d01f8d4c2af20476d356dfb4e3061af Mon Sep 17 00:00:00 2001 From: Blizzard Date: Tue, 23 Jun 2026 14:29:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(mcp-go,dispatcher):=20=E8=87=AA=E4=B8=BB?= =?UTF-8?q?=20agent=20=E5=B7=A5=E5=85=B7=E9=9B=86=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E5=8C=96=20=E2=80=94=E2=80=94=20list=5Ftools=20=E8=87=AA?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0=20+=20=E5=8A=A8=E6=80=81=E5=8F=91=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 杜绝硬编码:自主 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) --- EINO_ADOPTION.md | 1 + .../internal/eino/react_agent.go | 85 ++++++++++----- sundynix-mcp-go/internal/mcp/gateway.go | 100 +++++++++++++----- 3 files changed, 133 insertions(+), 53 deletions(-) diff --git a/EINO_ADOPTION.md b/EINO_ADOPTION.md index 950fa73..1ac2753 100644 --- a/EINO_ADOPTION.md +++ b/EINO_ADOPTION.md @@ -13,6 +13,7 @@ - [~] **Phase D · 状态化执行**:✅ 任务生命周期 FSM(已完成)/ ⬜ HITL 中断恢复 / ⬜ 多智能体(按场景) **组件化补完(A)**:检索 → `ragRetriever`(`components/retriever.Retriever`,`eino_components.go`);提示词 → `buildMessages` 改用 `prompt.FromMessages`+`MessagesPlaceholder`;工具 → `mcpTool`(`InvokableTool`,模型自主调用)。至此终态架构 8 层中 模型/工具/检索/提示词/编排/智能体(单)/可观测 均已 Eino 组件化;剩 人机交互(中断恢复,按场景)。 +**自主 agent 工具集动态化**:agent 工具集不再硬编码——mcp-go 注册表(单一事实源)每个工具声明 `agent/params/inject`,`list_tools` 上报,dispatcher `agentTools()` 动态发现并建 `InvokableTool`、运行时注入 `user_id/session_id/kb`(不暴露给模型)。**加工具只改 mcp-go 注册表,dispatcher 零改动。** 当前暴露 4 个(wiki_search/recall_user_memory/remember_user_fact/history_get);实测模型自主调用新暴露的 remember_user_fact 成功(参数自生成、user_id 服务端注入)。 **性能注记**:compose 每任务编译实测 ~13µs(基准 BenchmarkComposeCompile),相对 LLM 秒级可忽略 → 编译图缓存判定为 premature optimization,暂不做;真正的并行效率已由 Phase C 的 DAG 调度(`AllPredecessor`)拿到。 --- diff --git a/sundynix-dispatcher/internal/eino/react_agent.go b/sundynix-dispatcher/internal/eino/react_agent.go index 7e8f922..d7efc38 100644 --- a/sundynix-dispatcher/internal/eino/react_agent.go +++ b/sundynix-dispatcher/internal/eino/react_agent.go @@ -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 建 InvokableTool;inject 参数(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 工具。 diff --git a/sundynix-mcp-go/internal/mcp/gateway.go b/sundynix-mcp-go/internal/mcp/gateway.go index 730db3b..4e758a6 100644 --- a/sundynix-mcp-go/internal/mcp/gateway.go +++ b/sundynix-mcp-go/internal/mcp/gateway.go @@ -32,11 +32,25 @@ type Gateway struct { tools map[string]toolDef // 工具注册表:唯一事实源,dispatch 与 list_tools 共用,杜绝漂移 } -// toolDef 是一个注册工具的元信息(中文名 / 作用)+ 处理函数。 +// paramSpec 是一个工具参数的声明(供自主 agent 据此生成调用入参)。 +type paramSpec struct { + Name string `json:"name"` + Type string `json:"type"` // string / number / integer / boolean / object / array + Desc string `json:"desc"` + Required bool `json:"required"` +} + +// toolDef 是一个注册工具的元信息 + 处理函数。新增 agent 暴露元信息,让自主 agent 能动态发现工具: +// agent=是否给模型自主调用;agentName=模型可见名(空=注册键);params=模型可填参数; +// inject=服务端运行时注入、不暴露给模型的参数名(如 user_id / session_id / kb / task_id)。 type toolDef struct { - cn string // 中文名 - desc string // 作用简述 - handler func(context.Context, *contract.ToolCall) *contract.ToolResult + cn string + desc string + agent bool + agentName string + params []paramSpec + inject []string + handler func(context.Context, *contract.ToolCall) *contract.ToolResult } func NewGateway(b *sharedbus.Bus, s *search.Hybrid, m *memory.Store, h *history.Store, r *rag.Engine) *Gateway { @@ -60,29 +74,53 @@ func (g *Gateway) Serve(ctx context.Context) error { // buildRegistry 注册 mcp-go 全部工具:名称 → (中文名, 作用, 处理函数)。 // 这是工具的唯一事实源——dispatch 据此路由、list_tools 据此上报,二者永不漂移。 +// 想让某工具能被自主 agent 调用:把 agent 设 true,写清 params(模型可填)与 inject(服务端注入)。 +// 加新工具只改这一处——dispatcher 经 list_tools 动态发现,无需改调度代码。 func (g *Gateway) buildRegistry() map[string]toolDef { return map[string]toolDef{ - "wiki_search": {"知识检索", "向量检索知识库(Milvus),返回最相关片段", g.wikiSearch}, - "kb_ingest": {"知识入库", "文本切块 → 向量化 → 写入 Milvus / Bleve", g.kbIngest}, - "kb_search": {"检索台查询", "结构化返回命中内容与相似度分数", g.kbSearch}, - "kb_graph": {"知识图谱", "取某库的实体关系三元组(Neo4j)", g.kbGraph}, - "report_render": {"报告渲染", "把结构化报告渲染为 Word(.docx)", g.reportRender}, - "report_store": {"报告存源", "暂存报告源数据,供导出时按需渲染", g.reportStore}, - "report_export": {"报告导出", "按需把已存报告导出为 Word / Markdown", g.reportExport}, - "external_api": {"外部接口", "受控调用第三方 HTTP API(带 SSRF 校验)", g.externalAPI}, - "memory_get": {"记忆召回", "取用户长期画像(已按打分排序)", g.memoryGet}, - "memory_upsert": {"记忆写入", "新增 / 更新一条用户偏好(带重要度)", g.memoryUpsert}, - "memory_delete": {"记忆删除", "软删一条偏好(对账判定过时 / 矛盾时)", g.memoryDelete}, - "memory_list": {"记忆列表", "列出用户全部偏好(供管理面板查看)", g.memoryList}, - "history_get": {"历史召回", "取会话最近多轮对话", g.historyGet}, - "history_append": {"历史追加", "往会话写入一条消息", g.historyAppend}, - "health": {"健康检查", "上报 Milvus / Neo4j / embedding 就绪情况", - func(_ context.Context, _ *contract.ToolCall) *contract.ToolResult { + // —— 暴露给自主 agent 的工具(带参数 schema / 注入声明)—— + "wiki_search": { + cn: "知识检索", desc: "检索知识库,返回与查询最相关的资料片段。需要外部知识/事实依据时调用。", + agent: true, + params: []paramSpec{{Name: "q", Type: "string", Desc: "检索查询语句", Required: true}}, + inject: []string{"kb"}, handler: g.wikiSearch, + }, + "memory_get": { + cn: "记忆召回", desc: "召回当前用户的长期画像与偏好(称呼/职业/回答偏好等)。需要个性化、了解“我是谁”时调用。", + agent: true, agentName: "recall_user_memory", inject: []string{"user_id"}, handler: g.memoryGet, + }, + "memory_upsert": { + cn: "记忆写入", desc: "把关于用户的一条事实/偏好长期记住(如称呼、职业、回答偏好)。", + agent: true, agentName: "remember_user_fact", + params: []paramSpec{ + {Name: "key", Type: "string", Desc: "记忆条目的键,如 称呼/职业/回答偏好", Required: true}, + {Name: "value", Type: "string", Desc: "记忆条目的值", Required: true}, + }, + inject: []string{"user_id"}, handler: g.memoryUpsert, + }, + "history_get": { + cn: "历史召回", desc: "取当前会话最近多轮对话,用于理解上下文。", + agent: true, inject: []string{"session_id"}, handler: g.historyGet, + }, + + // —— 仅内部/流水线/管理用,不暴露给自主 agent —— + "kb_ingest": {cn: "知识入库", desc: "文本切块 → 向量化 → 写入 Milvus / Bleve", handler: g.kbIngest}, + "kb_search": {cn: "检索台查询", desc: "结构化返回命中内容与相似度分数", handler: g.kbSearch}, + "kb_graph": {cn: "知识图谱", desc: "取某库的实体关系三元组(Neo4j)", handler: g.kbGraph}, + "report_render": {cn: "报告渲染", desc: "把结构化报告渲染为 Word(.docx)", handler: g.reportRender}, + "report_store": {cn: "报告存源", desc: "暂存报告源数据,供导出时按需渲染", handler: g.reportStore}, + "report_export": {cn: "报告导出", desc: "按需把已存报告导出为 Word / Markdown", handler: g.reportExport}, + "external_api": {cn: "外部接口", desc: "受控调用第三方 HTTP API(带 SSRF 校验)", handler: g.externalAPI}, + "memory_delete": {cn: "记忆删除", desc: "软删一条偏好(对账判定过时 / 矛盾时)", handler: g.memoryDelete}, + "memory_list": {cn: "记忆列表", desc: "列出用户全部偏好(供管理面板查看)", handler: g.memoryList}, + "history_append": {cn: "历史追加", desc: "往会话写入一条消息", handler: g.historyAppend}, + "health": {cn: "健康检查", desc: "上报 Milvus / Neo4j / embedding 就绪情况", + handler: func(_ context.Context, _ *contract.ToolCall) *contract.ToolResult { data, _ := json.Marshal(g.rag.Status()) return &contract.ToolResult{OK: true, Content: string(data)} }}, - "echo": {"回显", "原样返回入参(调试用)", - func(_ context.Context, call *contract.ToolCall) *contract.ToolResult { + "echo": {cn: "回显", desc: "原样返回入参(调试用)", + handler: func(_ context.Context, call *contract.ToolCall) *contract.ToolResult { return &contract.ToolResult{OK: true, Content: fmt.Sprint(call.Args["text"])} }}, } @@ -102,16 +140,24 @@ func (g *Gateway) dispatch(ctx context.Context, call *contract.ToolCall) *contra return td.handler(ctx, call) } -// listTools 自省:上报本服务注册的工具清单(名称 + 中文名 + 作用),供管理端展示。 +// listTools 自省:上报本服务注册的工具清单(名称 + 中文名 + 作用 + agent 暴露元信息), +// 供管理端展示 & dispatcher 动态构建自主 agent 工具集(加工具只改注册表,无需改调度代码)。 func (g *Gateway) listTools() *contract.ToolResult { type info struct { - Name string `json:"name"` - CN string `json:"cn"` - Desc string `json:"desc"` + Name string `json:"name"` + CN string `json:"cn"` + Desc string `json:"desc"` + Agent bool `json:"agent_exposed"` // 是否给自主 agent + AgentName string `json:"agent_name,omitempty"`// 模型可见名(空=name) + Params []paramSpec `json:"params,omitempty"` // 模型可填参数 + Inject []string `json:"inject,omitempty"` // 服务端注入参数(不暴露给模型) } out := make([]info, 0, len(g.tools)) for name, td := range g.tools { - out = append(out, info{Name: name, CN: td.cn, Desc: td.desc}) + out = append(out, info{ + Name: name, CN: td.cn, Desc: td.desc, + Agent: td.agent, AgentName: td.agentName, Params: td.params, Inject: td.inject, + }) } sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) // map 无序 → 稳定输出 data, _ := json.Marshal(map[string]any{"service": "mcp-go", "tools": out})