agent 节点带 autonomous:true 时走 ReAct(flow/agent/react),模型在
推理-工具循环里自行决定调哪些 MCP 工具,而非只跑画死的工具节点。
- 新增 eino/react_agent.go:mcpTool 把 MCP 工具(NATS)适配成
components/tool.InvokableTool;agentTools 暴露 wiki_search +
recall_user_memory(context 参数 user_id 服务端注入,不暴露给模型);
runReactAgent 流式回流答复,工具调用经适配器落 ExecEvent 轨迹。
- LLM 接口 + *llm.Pool 增 ToolCallingModel()(openai 组件实现
model.ToolCallingChatModel);fakeLLM 同步桩(返回 nil → 降级普通对话)。
- graph.go:agent 节点按 autonomous 开关分流 ReAct / 普通;自主 agent
不预注入画像(让其经 recall_user_memory 工具按需自取)。
关键修复:默认 StreamToolCallChecker 只看首个流片段,deepseek 常先吐
文本再给 tool call 致漏判 → 改 streamHasToolCall 扫整段流(命中率 ~0→7/7)。
验收:自主工具调用 7/7 命中,args={} 证明注入生效,完整闭环跑通;
make test-go 全绿。已知 eval 看不到工具结果会误判 agent(Phase C 修)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
Eino 全面采纳迁移方案
目标:从"只借 Eino 的类型"演进为 Eino-native 的 Agent 编排核心,充分发挥框架的组件 / ADK / 编排 / 可观测能力。 原则:分阶段、不推倒重写、每步保持现有测试绿、可随时回退。自研
graph.go解释器演进为compose.Graph,而非盲删。
进度
- Phase A · 地基:
llm.Pool换 Eino ChatModel 组件(commit d84b1ec,验收通过) - Phase B · 质变:MCP 工具→
InvokableTool+ ReAct agent(模型自主调工具,验收 7/7 命中) - Phase C · 编排归一:
Flow→compose.Graph+ callbacks 桥接 - Phase D · 状态化执行:任务生命周期 FSM / HITL 中断恢复 / 多智能体
0. 现状审计(2026-06-22)
实际跑在 sundynix-dispatcher/internal/ 下:
| 维度 | 现状 | 用没用 Eino |
|---|---|---|
| 消息/流类型 | schema.Message、StreamReader、Pipe 全链路在用 |
✅ 真用(承重) |
| ChatModel | llm/pool.go 自研 OpenAI 兼容客户端(手写 SSE 流解析) |
❌ 直连,绕过框架 |
eino/model.go 的 poolModel |
实现了 model.BaseChatModel,但 newPoolModel 从未被调用 |
⚠️ 空接缝 |
| 工具调用 | NATS request-reply(ToolCall/ToolResult),图里画死 |
❌ 自研,无函数调用 |
| 编排 | eino/graph.go 自研解释器(拓扑/分支真假边/map 并行),有测试 |
❌ 非 compose |
| 检索 | mcp-go RAG(Milvus/Bleve/Neo4j RRF),经 wiki_search 工具 |
❌ 自研 |
| 提示词 | report.go / memory_extract.go 手拼字符串 |
❌ |
| 可观测 | 自研 ExecEvent 轨迹流 + Prometheus + eval 护栏 |
❌(不缺) |
| 人机交互 | 无 | ❌ |
结论:卡在最底层——没有真正的 ChatModel 组件,ADK / 编排都用不起来。采纳必须自底向上。
Eino v0.9.9 可用包(已核对本地 module)
github.com/cloudwego/eino/schema # ✅ 已用
github.com/cloudwego/eino/components/model # ChatModel 接口
github.com/cloudwego/eino/components/tool # Tool 接口
github.com/cloudwego/eino/components/retriever# Retriever 接口
github.com/cloudwego/eino/components/prompt # ChatTemplate
github.com/cloudwego/eino/compose # Graph / Workflow / branch / tool_node / lambda
github.com/cloudwego/eino/flow/agent/react # ReAct agent
github.com/cloudwego/eino/flow/agent/multiagent
github.com/cloudwego/eino/adk # Agent Development Kit(agent_tool / react / callback)
github.com/cloudwego/eino/callbacks # 回调/可观测
github.com/cloudwego/eino-ext/... # ⚠️ 官方组件实现(openai 等),尚未拉取,Phase A 新增
终态架构
| 层 | 现在 | 终态 |
|---|---|---|
| 模型 | 自研 llm.Pool |
eino-ext openai 组件 + 我们的热更新/熔断包一层 |
| 工具 | NATS CallTool,画死 |
每个 MCP 工具包成 components/tool.InvokableTool,BindTools 给模型动态选 |
| 检索 | wiki_search 工具 |
mcp-go RAG 包成 components/retriever.Retriever |
| 提示词 | 手拼 | components/prompt.ChatTemplate |
| 编排 | 自研 graph.go |
compose.Graph(流式 reduce/branch + 类型化边) |
| 智能体 | 无 | flow/agent/react 或 adk 的 ReAct agent;后续多智能体 |
| 可观测 | ExecEvent + Prom | callbacks 桥接到现有 ExecEvent,统一 |
| 人机交互 | 无 | 中断/恢复(审批场景,需 checkpoint store) |
Phase A · 地基:接真 ChatModel 组件 〔P0〕✅ 已完成(commit d84b1ec)
落地:
llm.Pool内部已换成eino-ext/openai的 ChatModel(ChatStream→Stream、Chat→Generate),对外签名不变;go get eino-ext/components/model/openai v0.1.13;真实链路验证通过(提交任务流式出答复 + eval 1.00)。poolModel接缝保留给 B/C。
目标:模型调用走 Eino 官方 ChatModel 组件,接上空着的 poolModel 接缝;行为对现有用例不变。
依赖:go get github.com/cloudwego/eino-ext/components/model/openai(确认与 eino v0.9.9 兼容版本)。
改动:
sundynix-dispatcher/internal/llm/pool.go- 内部把手写 HTTP/SSE 换成 eino openai 组件作为 backend;
SetConfig时用激活配置New一个 ChatModel 实例(热更新 = 重建实例,加读写锁)。 - 保留对外签名:
Chat/ChatStream/StreamText/Ready/ModelName,让graph.go/report.go/memory_extract.go零改动。 - 熔断器(
harness.CircuitBreaker)仍包在Pool这层。
- 内部把手写 HTTP/SSE 换成 eino openai 组件作为 backend;
sundynix-dispatcher/internal/eino/model.go- 让
poolModel真正包住组件后的 Pool(或直接暴露底层model.BaseChatModel),供 Phase B/C 用。
- 让
验收:
make test-go(含eino/graph_test.go、integration_test.go、memory_extract_test.go)全绿。- 桌面端跑一次编排 / 一次报告 / 触发一次记忆 consolidate,输出与现状一致。
- 「服务状态」面板 dispatcher 仍显示模型名 + 运行时长。
风险/回退:低。出问题回退到手写客户端(保留旧代码一版,绿后再删)。
Phase B · 质变:函数调用 + ADK 单智能体 〔P1〕✅ 已完成
落地:新增
eino/react_agent.go——mcpTool把 MCP 工具(NATS)适配成tool.InvokableTool;agent节点带autonomous:true时走flow/agent/react(首批工具:wiki_search、recall_user_memory,context 参数 user_id 服务端注入、不暴露给模型)。验收:自主工具调用 7/7 命中,args={} 证明注入生效,完整闭环(模型→tool→MCP→观察→答复)。两个工程发现:
StreamToolCallChecker必须扫整段流:默认只看首个流片段,deepseek 常先吐文本再给 tool call → 漏判致不调工具。已用streamHasToolCall扫全流修复(命中率 ~0 → 7/7)。- eval 看不到工具结果 → 误判:LLM 评委只见 query+answer,agent 用召回记忆作答会被判"虚构"。Phase C 用 callbacks 把工具上下文喂给 eval 修正。
目标:模型能自主选择并调用 MCP 工具(ReAct),而非只跑画死的工具节点。这是"工作流执行器 → Agent 平台"的关键一跳。
改动:
- 新增
sundynix-dispatcher/internal/eino/tools.go- 把每个 MCP 工具包成
components/tool.InvokableTool适配器:InvokableRun内部走subscriber.CallTool(ToolSubjectGo/Py, ToolCall)。 - 工具 schema(参数)从哪来:① 先手写少量核心工具的 JSON Schema;② 后续让 MCP
list_tools连参数 schema 一起上报(已有中文名/作用,扩字段即可)。
- 把每个 MCP 工具包成
- 新增
agent节点类型dsl/compile.go识别kind=="agent"且开启"自主工具"开关 → 走 ReAct(flow/agent/react或adk)。- agent 绑定:ChatModel(Phase A)+ 一组 InvokableTool(按节点配置或 owner 可用工具集)。
graph.go:agent节点分支委托给 ReAct runner,复用现有 ExecEvent 回流。
验收:
- 画一个「agent + 若干工具」的图,给一句需要检索/记忆的提问,观测模型自己决定调了哪个 MCP 工具(ExecEvent 里出现 tool 调用轨迹)。
- 旧的静态工具流不受影响。
风险:中。ReAct 多轮会放大 token 成本与时延 → 设最大步数 + 复用熔断器;工具 schema 不准会导致乱调 → 先小工具集灰度。
Phase C · 编排归一:迁到 compose.Graph 〔P2〕
目标:自研解释器退役,DSL 图编译为 compose.Graph,吃到流式 reduce/branch、类型化边、自动并发、callbacks。
改动:
sundynix-dispatcher/internal/dsl/compile.go(兑现文件头那句 TODO:"演进为 compose.NewGraph 的完整多节点编译")- 写
DSL Flow → compose.Graph编译器:节点映射为 ChatModel / ToolNode / Retriever /Lambda;连线 → 边;branch 真假 →compose分支;map → 并发分支。
- 写
callbacks:实现 handler 把 Eino 回调桥接到现有ExecEvent(节点 start/end/error + 耗时),统一可观测,不重复造。report.go:报告多步流水线改用 compose(章节并行 + 汇聚天然契合)。- 检索节点 →
components/retriever(包 mcp-go RAG);提示词 →components/prompt.ChatTemplate。 - 对齐策略:compose 版与 graph.go 版并存,用
graph_test.go/integration_test.go做等价回归;全绿且灰度通过后,graph.go才退役。
验收:分支路由、map 并行、报告流水线在 compose 版下与旧版输出等价;ExecEvent 轨迹不丢。
风险:高(动编排核心)。靠"并存 + 等价测试 + 灰度"控制,绝不一刀切。
Phase D · 状态化执行 〔later,按场景〕
主题:把"执行"从一次性 DAG 升级为可持久化、可恢复的状态机。三件事同一条线,一起做。
- 任务生命周期 FSM 🆕:现在
Task.Status只写死submitted、全仓从不流转(store/models.go+pgsql.go:96),是个摆设——这正是"卡运行中看不出来"的根因。- 设计:
submitted → running → done / failed / timeout显式状态机。 - dispatcher 开跑/跑完/出错 经 NATS 回写状态(新增
sundynix.tasks.status或复用 exec 流),网关落 PG 并推给 UI。 - 收益:管理端「服务状态」/ 桌面端能看到任务真实进度,超时自动翻红,无需人工猜。
- 与下面同源:Eino compose 的 graph state + 节点级状态正好承载它。
- 设计:
- 中断/恢复(HITL):审批型工业流程(生成中途人工确认)。需 checkpoint 持久化(PG/Redis)。等有具体审批用例再做。
- 多智能体协同(
flow/agent/multiagent):出现真实多角色编排需求时再上,现在无用例。
暂时不做
- Ollama 官方组件:与"开发期不拉本地 Ollama"策略冲突(见记忆 llm-provider-strategy)。
- 拿 callbacks 当唯一理由迁移:已有 ExecEvent + Prometheus + eval 护栏,callbacks 仅在 Phase C 顺带桥接。
落地顺序与依赖
A(ChatModel 组件)── 地基,解锁全部
└─> B(Tool 适配 + ADK ReAct)── 质变:模型自主调工具
└─> C(compose 编排 + callbacks + retriever/prompt)── 编排归一
└─> D(状态化执行:任务生命周期 FSM / HITL 中断恢复 / 多智能体)── 按场景
逐项实现,每 Phase 一个(或多个)提交,跑 make test-go + 桌面端冒烟后再进下一阶段。
总验收/回归基线(每阶段都要过)
make test-go:shared / gateway / dispatcher / mcp-go 全绿。- 桌面端冒烟:编排运行(含分支/并行)、报告生成+导出、记忆 consolidate、知识库检索。
- 管理端「服务状态」:四服务 + 五基建 + 工具注册正常。
- 行为等价:迁移前后同输入输出一致(Phase C 重点)。