Files
sundynix-agentix/EINO_ADOPTION.md
T
Blizzard 1c7e8e1ea9 docs: 修 DEPTH_ROADMAP/EINO 两处过期标记
- 删 T4.E 熔断器接回 failover 重复的过期 [ ] 项(222 行  为准);
  T2.1 熔断器联动注明已整合。
- EINO Phase D FSM 回写主题名笔误 sundynix.tasks.status → sundynix.status.task
  (文档 141 行自己记过这次迁移)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 13:29:01 +08:00

203 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Eino 全面采纳迁移方案
> 目标:从"只借 Eino 的类型"演进为 **Eino-native 的 Agent 编排核心**,充分发挥框架的组件 / ADK / 编排 / 可观测能力。
> 原则:**分阶段、不推倒重写、每步保持现有测试绿、可随时回退**。自研 `graph.go` 解释器演进为 `compose.Graph`,而非盲删。
---
## 进度
- [x] **Phase A · 地基**`llm.Pool` 换 Eino ChatModel 组件(commit d84b1ec,验收通过)
- [x] **Phase B · 质变**MCP 工具→`InvokableTool` + ReAct agent(模型自主调工具,验收 7/7 命中)
- [x] **Phase C · 编排归一**:✅ 全图 `DSL→compose.Graph` 编译器(全节点 + branch + DAG 并行调度)+ callbacks→ExecEvent 归一,等价回归通过(EINO_COMPOSE 灰度开关,默认关;过渡期后 graph.go 退役)
- [~] **Phase D · 状态化执行**:✅ 任务生命周期 FSM / ✅ HITL 人工审批中断(审批节点暂停→waiting→批准回 running / 拒绝→rejectedNATS 决定回传 + AckWait 续租,全栈含 Studio 审批节点 + 运行抽屉批准条)/ ⬜ 多智能体(按场景)
**组件化补完(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`)拿到。
---
## 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` RAGMilvus/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 Kitagent_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` 这层。
- `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→观察→答复)。
>
> **两个工程发现**
> 1. **`StreamToolCallChecker` 必须扫整段流**:默认只看首个流片段,deepseek 常先吐文本再给 tool call → 漏判致不调工具。已用 `streamHasToolCall` 扫全流修复(命中率 ~0 → 7/7)。
> 2. **eval 看不到工具结果 → 误判**LLM 评委只见 query+answeragent 用召回记忆作答会被判"虚构"。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 一起上报(已有中文名/作用,扩字段即可)。
- 新增 `agent` 节点类型
- `dsl/compile.go` 识别 `kind=="agent"` 且开启"自主工具"开关 → 走 ReAct`flow/agent/react``adk`)。
- agent 绑定:ChatModelPhase A+ 一组 InvokableTool(按节点配置或 owner 可用工具集)。
- `graph.go``agent` 节点分支委托给 ReAct runner,复用现有 ExecEvent 回流。
**验收**
- 画一个「agent + 若干工具」的图,给一句需要检索/记忆的提问,观测**模型自己决定调了哪个 MCP 工具**ExecEvent 里出现 tool 调用轨迹)。
- 旧的静态工具流不受影响。
**风险**:中。ReAct 多轮会放大 token 成本与时延 → 设最大步数 + 复用熔断器;工具 schema 不准会导致乱调 → 先小工具集灰度。
---
## Phase C · 编排归一:迁到 compose.Graph 〔P2〕✅ 已完成(并存灰度,等价回归通过)
> 全图编译器:`compose_compiler.go` 把整张 DSL 图编译为 `compose.Graph`——
> - 每个节点 = 一个 Lambda,节点体复用现有逻辑(`execDSLNode`input/memory/retriever/tool/agent/aggregate/render/map/output),黑板进 compose 本地状态(`WithGenLocalState` + `ProcessState`)。
> - branch = `AddBranch` + 状态感知条件(复用 `branchNode` 选路);边载荷用空 `flowSignal`(注册 no-op 合并支持 fan-in),真实数据全走黑板。
> - `WithNodeTriggerMode(AllPredecessor)` DAG 模式:无依赖节点并行调度(效率)。
> - `Handle → executeGraph` 按 `EINO_COMPOSE` 开关选 compose / graph.gocompose 编译失败自动降级回 graph.go(安全网)。
> - **等价回归**:线性图、分支图经解释器与 compose 两路径产出逐字一致(单测);live 多节点分支图 compose 路径 2800 字答复 eval 1.00、FSM done、0 幽灵。
>
> 待过渡期 soak 后把默认翻到 compose、退役 graph.go。**性能后续**:编译图按 DSL-hash 缓存(当前每任务编译一次)。
> 落地(并存+等价回归策略):对话主流程已可跑在 `compose.Graph` 上——
> - `compose_graph.go``runConversation` 按 `EINO_COMPOSE` 开关分流;`runComposeConversation` 建图 `START→ChatModel→END`、`Compile`→`Stream`token 回流;模型未就绪/编译失败降级回 `runAgent`。**默认关,graph.go 仍是默认且权威。**
> - `compose_callbacks.go``composeTracer` 用 `utils/callbacks` 把 ChatModel/Tool 的 start/end/error 桥到 ExecEvent(可观测归一)。
> - 测试:compose 图编译+运行、compose 对话流式回流、开关关→走 runAgent,三个单测;live 实测 compose 路径出 54 字答复 + eval 1.00,默认路径 eval 1.00。
> - **顺带修真 bug**`SubjectTaskStatus` 原为 `sundynix.tasks.status`,落在任务流捕获通配 `sundynix.tasks.>` 内 → 状态事件被当成"幽灵任务"自我放大(实测污染 2300+ 条)。已挪到 `sundynix.status.task` + dispatcher 加空任务护栏。
>
> 剩余:branch / map / render / retriever / prompt 等节点逐步迁 compose(同并存+等价回归),对齐后 graph.go 退役。
**目标**:自研解释器退役,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** ✅ 已完成:`submitted → running → done / failed / timeout` 显式状态机。
- dispatcher 经新主题 `sundynix.status.task` 回写(`TaskStatusEvent`):进入执行→running、收尾→done/failed、整体超时上限 `taskExecTimeout=3min`→timeout;网关 `SubscribeTaskStatus` 落 PG`Task.Status/Detail`)。
- UI 轮询:`GET /api/v1/tasks/:id` 返回 `{status, detail}`
- 验收:submitted→running→done 实测流转、PG 持久化、3min 超时兜底——根治"卡运行中看不出来"。
- 桌面端轮询接线仍可补(当前后端 + 端点已就绪)。
- **中断/恢复(HITL)**:审批型工业流程(生成中途人工确认)。需 checkpoint 持久化(PG/Redis)。等有具体审批用例再做。
- **多智能体协同**`flow/agent/multiagent`):出现真实多角色编排需求时再上,现在无用例。
---
## 暂时不做
- **Ollama 官方组件**:与"开发期不拉本地 Ollama"策略冲突(见记忆 llm-provider-strategy)。
- **拿 callbacks 当唯一理由迁移**:已有 ExecEvent + Prometheus + eval 护栏,callbacks 仅在 Phase C 顺带桥接。
---
## 落地顺序与依赖
```
AChatModel 组件)── 地基,解锁全部
└─> BTool 适配 + ADK ReAct)── 质变:模型自主调工具
└─> Ccompose 编排 + callbacks + retriever/prompt)── 编排归一
└─> D(状态化执行:任务生命周期 FSM / HITL 中断恢复 / 多智能体)── 按场景
```
逐项实现,每 Phase 一个(或多个)提交,跑 `make test-go` + 桌面端冒烟后再进下一阶段。
---
## 总验收/回归基线(每阶段都要过)
1. `make test-go`shared / gateway / dispatcher / mcp-go 全绿。
2. 桌面端冒烟:编排运行(含分支/并行)、报告生成+导出、记忆 consolidate、知识库检索。
3. 管理端「服务状态」:四服务 + 五基建 + 工具注册正常。
4. 行为等价:迁移前后同输入输出一致(Phase C 重点)。