# 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 归一，等价回归通过；灰度期已过、graph.go 退役、compose 为唯一引擎（**例外**：报告 intent 仍走独立 `handleReport` 流水线，刻意未 compose 化，理由见 Phase C 改动清单）
- [~] **Phase D · 状态化执行**：✅ 任务生命周期 FSM / ✅ HITL 人工审批中断（审批节点暂停→waiting→批准回 running / 拒绝→rejected，NATS 决定回传 + 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` 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` 这层。
- `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+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 一起上报（已有中文名/作用，扩字段即可）。
- 新增 `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〕✅ 已完成（并存灰度，等价回归通过）

> 全图编译器：`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.go；compose 编译失败自动降级回 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（章节并行 + 汇聚天然契合）。~~
  **实际未迁，刻意保留为独立路径（诚实修正）**：`orchestrator.go` 按 `intent==IntentReport`
  分流到 `handleReport`，用自研 `writeSections`（WaitGroup + 信号量并发 + 部分失败可见标记）做
  规划→分章并行→渲染，**不经 compose.Graph**。原因：① 报告是固定形状的专用流水线（规划-map-渲染），
  并行能力已由 `writeSections` 拿到，compose 化不带来用户可见收益；② 硬迁移会拿一个有单测背书、
  已 live 验证的稳定功能去冒回归风险。故报告是 compose 之外的第二条编排路径，通用 DSL 图仍全走 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 顺带桥接。

---

## 落地顺序与依赖

```
A（ChatModel 组件）── 地基，解锁全部
   └─> B（Tool 适配 + ADK ReAct）── 质变：模型自主调工具
          └─> C（compose 编排 + 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 重点）。
