系列「企业级 AI Agent 实现拆解」E17 篇。上一篇 E16 介绍了 Manus Agent 和研究团队协作的整体概念。这篇专门深挖 deer-go——它是字节跳动开源项目 deer-flow 的 Go 语言移植版,专为"深度研究"场景设计,比 E16 覆盖的内容多出三个关键节点和一套完整的计划数据结构。

读完这篇你会知道

  • deer-go 来自哪里:字节 deer-flow 的 Go 移植
  • 完整拓扑:8 个子图节点,比 E16 多出了什么
  • BackgroundInvestigator:规划之前先偷偷搜一下
  • Plan 数据结构:Planner 输出的是一份结构化 JSON
  • HasEnoughContext:Planner 如何判断"信息够了不用搜"
  • Human Feedback 节点:计划审批,不是任务中断
  • 统一路由机制:agentHandOff 怎么实现全局调度
  • AnyPredecessor:为什么必须开环才能跑循环
  • CheckPoint:断点续跑的状态持久化
  • 和 Manus 的本质区别:通用 vs. 深度研究

一、先说出处:这不是 Eino 原创

deer-go 的 README.md 第一行:

> 本仓库参考 https://github.com/bytedance/deer-flow 完成改写

bytedance/deer-flow 是字节跳动开源的深度研究 AI 框架,Python 实现,有配套的前端页面。deer-go 是 CloudWeGo 团队把它用 Eino 框架完整移植到 Go 的版本。

关系和 E16 里 Manus 的情况一样:

原版 语言 Go 移植版 语言
FoundationAgents/OpenManus Python eino-examples/flow/agent/manus Go + Eino
bytedance/deer-flow Python eino-examples/flow/agent/deer-go Go + Eino

deer-go 甚至支持复用 deer-flow 的前端页面——用 -s 参数启动后,直接接 deer-flow 前端即可。


二、完整拓扑:8 个子图节点

E16 介绍了 5 个角色(Coordinator / Planner / Researcher / Coder / Reporter)。完整版 deer-go 有 8 个节点,多出了 3 个:

BackgroundInvestigator  ← E16 没讲,规划前的快速预调查
Human                   ← E16 没讲,计划审批节点
ResearchTeam            ← 调度路由层(E16 讲了)

builder.go 里把这 8 个节点全部组装成一张大图(源码 builder.go:62):

outMap := map[string]bool{
    consts.Coordinator:            true,
    consts.Planner:                true,
    consts.Reporter:               true,
    consts.ResearchTeam:           true,
    consts.Researcher:             true,
    consts.Coder:                  true,
    consts.BackgroundInvestigator: true,
    consts.Human:                  true,
    compose.END:                   true,
}

每个节点执行完,都通过同一个 agentHandOff 函数决定去哪个节点。完整的调度权在 state.Goto 字段里——这是整个系统的路由总线,所有节点都读这个字段决定"下一站"。


三、统一路由机制:agentHandOff

这是 deer-go 最核心的设计,和 E16 里 ResearchTeam Router 的模式完全一致,但做到了全图统一:

// builder.go:34
func agentHandOff(ctx context.Context, input string) (next string, err error) {
    _ = compose.ProcessState[*model.State](ctx, func(_ context.Context, state *model.State) error {
        next = state.Goto  // 读 state.Goto,决定去哪
        return nil
    })
    return next, nil
}

每个节点在自己的 router 函数里把目标写进 state.Goto,然后 agentHandOff 读出来交给 Eino 的 Graph 调度器。

// 所有节点统一挂同一个分支函数
_ = g.AddBranch(consts.Coordinator,            compose.NewGraphBranch(agentHandOff, outMap))
_ = g.AddBranch(consts.Planner,                compose.NewGraphBranch(agentHandOff, outMap))
_ = g.AddBranch(consts.Reporter,               compose.NewGraphBranch(agentHandOff, outMap))
_ = g.AddBranch(consts.ResearchTeam,           compose.NewGraphBranch(agentHandOff, outMap))
_ = g.AddBranch(consts.Researcher,             compose.NewGraphBranch(agentHandOff, outMap))
_ = g.AddBranch(consts.Coder,                  compose.NewGraphBranch(agentHandOff, outMap))
_ = g.AddBranch(consts.BackgroundInvestigator, compose.NewGraphBranch(agentHandOff, outMap))
_ = g.AddBranch(consts.Human,                  compose.NewGraphBranch(agentHandOff, outMap))

8 个节点,挂的是同一个函数。路由逻辑完全下沉到各节点的 router 函数里,主图只负责"按 state.Goto 跳",不做任何判断。


四、BackgroundInvestigator:规划前先摸底

这是 E16 没有覆盖的节点。它在 Planner 生成正式研究计划之前,先快速搜一把,把结果写进 state.BackgroundInvestigationResults

// investigator.go:34
func search(ctx context.Context, name string, opts ...any) (output string, err error) {
    // 找 MCP 工具集里第一个名字以 "search" 结尾的工具
    for _, cli := range infra.MCPServer {
        ts, _ := mcp.GetTools(ctx, &mcp.Config{Cli: cli})
        for _, t := range ts {
            info, _ := t.Info(ctx)
            if strings.HasSuffix(info.Name, "search") {
                searchTool, _ = t.(tool.InvokableTool)
                break
            }
        }
    }

    // 用用户的最后一条消息作为搜索词,结果写入 state
    compose.ProcessState[*model.State](ctx, func(_ context.Context, state *model.State) error {
        args := map[string]any{"query": state.Messages[len(state.Messages)-1].Content}
        result, _ := searchTool.InvokableRun(ctx, string(argsBytes))
        state.BackgroundInvestigationResults = result  // 写入共享 State
        return nil
    })
}

执行完毕,bIRouterstate.Goto 设为 consts.Planner,背景调查结果就传给了下一个节点。

为什么要这一步?

Planner 拿到背景信息之后,生成的研究计划更有针对性。比如"研究最新量子计算进展",背景调查可能发现最近有一篇重要论文,Planner 就可以在计划里专门设一个步骤去深挖它。

这个节点是可选的——state.EnableBackgroundInvestigation 控制是否开启。Coordinator 里:

// coordinator.go:66
if state.EnableBackgroundInvestigation {
    state.Goto = consts.BackgroundInvestigator
} else {
    state.Goto = consts.Planner
}

五、Plan 数据结构:Planner 输出一份 JSON

这是 deer-go 区别于普通 Agent 最重要的设计。Planner 不是输出一段文字给下一个 AI 看,而是输出一份结构化 JSON,解析成 Go 结构体后驱动整个研究流程。

// model/planner.go
type Plan struct {
    Locale           string `json:"locale"`             // 用户语言
    HasEnoughContext bool   `json:"has_enough_context"` // 信息够了吗?
    Thought          string `json:"thought"`            // Planner 的思考过程
    Title            string `json:"title"`              // 研究课题标题
    Steps            []Step `json:"steps"`              // 研究步骤清单
}

type Step struct {
    NeedWebSearch bool     `json:"need_web_search"` // 这步需要联网吗
    Title         string   `json:"title"`
    Description   string   `json:"description"`
    StepType      StepType `json:"step_type"`          // "research" or "processing"
    ExecutionRes  *string  `json:"execution_res,omitempty"` // 执行结果(nil = 未完成)
}

routerPlanner 收到 Planner 输出后,直接 json.Unmarshal 解析:

// planner.go:78
err = json.Unmarshal([]byte(input.Content), state.CurrentPlan)
if err != nil {
    // JSON 解析失败 → 直接去 Reporter(勉强输出)
    if state.PlanIterations > 0 {
        state.Goto = consts.Reporter
    }
    return nil
}

这个设计的好处

  • ResearchTeam Router 可以直接读 step.StepType 决定叫谁,不用再让 AI 判断
  • step.ExecutionRes == nil 即"未完成",一目了然
  • Planner 每次迭代都覆盖 state.CurrentPlan,研究进展完整保存在 State 里

六、HasEnoughContext:Planner 的自我判断

Planner 的 prompt 里要求它输出 has_enough_context 字段,这是一个布尔值:

  • true:任务本身信息已经足够,不需要搜索,直接去 Reporter 输出
  • false:需要研究,走正常流程
// planner.go:89
if state.CurrentPlan.HasEnoughContext {
    state.Goto = consts.Reporter  // 信息够了 → 跳过研究直接汇报
    return nil
}
state.Goto = consts.Human  // 需要研究 → 先让人确认计划

举个例子:“2 + 2 等于多少?” 这种问题,Planner 会把 HasEnoughContext 设为 true,直接跳到 Reporter,不会浪费资源跑搜索。


七、Human Feedback:计划审批,不是任务中断

deer-go 的 Human 节点和 Manus 的 Human-in-the-Loop 完全不同,目的不一样:

Manus Human 节点 deer-go Human 节点
时机 AI 完成每一轮思考后 Planner 生成计划后
目的 让用户确认 AI 接下来的行动 让用户审批研究计划
用户操作 输入新指令或按 y 继续 接受计划 / 要求修改
修改后去哪 重新思考 回 Planner 重新规划

代码逻辑(human_feedback.go:28):

func routerHuman(ctx context.Context, input string, opts ...any) (output string, err error) {
    compose.ProcessState[*model.State](ctx, func(_ context.Context, state *model.State) error {
        state.Goto = consts.ResearchTeam  // 默认:接受计划,开始研究

        if !state.AutoAcceptedPlan {  // 没有开自动接受
            switch state.InterruptFeedback {
            case consts.AcceptPlan:
                return nil  // 用户说"接受" → 去研究
            case consts.EditPlan:
                state.Goto = consts.Planner  // 用户说"修改" → 回 Planner
                return nil
            default:
                return compose.InterruptAndRerun  // 还没反馈 → 暂停等人
            }
        }
        return nil
    })
    return output, err
}

compose.InterruptAndRerun 是 Eino 的断点机制——节点返回这个错误时,图暂停在当前位置,等待外部通过 WithStateModifier 注入新的用户反馈,然后从这个节点重新执行(不是从头来)。


八、AnyPredecessor:开环才能跑循环

builder.go 编译时用了一个关键参数(builder.go:107):

r, err := g.Compile(ctx,
    compose.WithGraphName("EinoDeer"),
    compose.WithNodeTriggerMode(compose.AnyPredecessor),  // 关键!
    compose.WithCheckPointStore(model.NewDeerCheckPoint(ctx)),
)

Eino Graph 默认是 DAG(有向无环图)——节点只有在所有前驱节点都完成后才触发。这对于"每个步骤都可能需要回到前面"的场景不够用。

AnyPredecessor 把触发模式改为:只要任意一个前驱节点完成,我就执行。这样 Researcher 完成后可以回到 ResearchTeam,ResearchTeam 可以再叫 Researcher,形成循环而不死锁。

没有这个参数,整个多轮研究流程跑不起来。


九、CheckPoint:研究中途可以断点续跑

deer-go 实现了 DeerCheckPointmodel/state.go:73):

type DeerCheckPoint struct {
    buf map[string][]byte
}

func (dc *DeerCheckPoint) Get(ctx context.Context, checkPointID string) ([]byte, bool, error) {
    data, ok := dc.buf[checkPointID]
    return data, ok, nil
}

func (dc *DeerCheckPoint) Set(ctx context.Context, checkPointID string, checkPoint []byte) error {
    dc.buf[checkPointID] = checkPoint
    return nil
}

当前实现用内存 map,工程上换成 Redis 或数据库即可。

CheckPoint 的意义:深度研究任务可能跑很长时间(10+ 分钟),中途如果出现网络问题或服务重启,可以从最后一个 CheckPoint 恢复,而不是从头来。State 里保存了完整的 CurrentPlan(含已完成步骤的结果),恢复后继续跑剩余步骤。


十、完整流程:带所有节点的一次深度研究

用户提问
 ↓
Coordinator(确认语言,决定是否做背景调查)
 ↓(可选)
BackgroundInvestigator(快速预搜索,结果写入 State)
 ↓
Planner(生成结构化 Plan,含步骤清单)
 ├──→ HasEnoughContext=true → Reporter(跳过研究)
 └──→ HasEnoughContext=false ↓
Human(展示计划给用户)
 ├──→ AcceptPlan  → ResearchTeam(开始执行)
 └──→ EditPlan    → Planner(重新规划)
 ↓
ResearchTeam Router(循环检查 Plan.Steps)
 ├──→ step.StepType=research   → Researcher(ReAct×40)
 ├──→ step.StepType=processing → Coder
 └──→ 全部完成 → Reporter
 ↓
Reporter(汇总所有 step.ExecutionRes,生成报告)
 ↓
END

十一、和 Manus 的本质区别

Manus(Go 复刻版) deer-go
来源 FoundationAgents/OpenManus 移植 bytedance/deer-flow 移植
定位 通用全能 Agent 深度研究专用
结构 单 Agent + 工具循环 8 节点多 Agent 团队
输入 任意任务 需要调研的问题
计划 无显式计划,AI 逐步决策 显式 Plan JSON,驱动研究流程
人工介入 每轮完成后确认行动 计划生成后审批一次
背景调查 BackgroundInvestigator
适用场景 浏览器操作、代码执行、通用任务 竞品分析、行业调研、知识整理

选择原则:任务是"帮我做 X",用 Manus;任务是"帮我研究 X 并出报告",用 deer-go。


小结

deer-go 相比 E16 介绍的版本,多出三件事:

  1. BackgroundInvestigator 在规划前预搜索,给 Planner 提供背景
  2. 结构化 Plan JSON 把研究计划变成可程序驱动的数据,而不是模糊的文字
  3. Human 计划审批 在执行前让人确认研究方向,代价最小的人工干预点

三者合在一起,让 deer-go 能在"深度研究"这个场景里做到:方向对、计划实、执行可追踪。


代码来源:cloudwego/eino-examples · 原版:bytedance/deer-flow

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐