代码 Deer-Go:字节Deer-Flow的Go移植,深度研究Agent全拆解

2026-07-03 06:30:35 +0800 CST views 6

Deer-Go:字节 Deer-Flow 的 Go 移植,深度研究 Agent 全拆解

系列「企业级 AI Agent 实现拆解」E17 篇

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

读完这篇你会知道:

  • deer-go 来自哪里:字节 deer-flow 的 Go 移植
  • 完整拓扑:8 个子图节点
  • 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 的版本。

原版语言Go 移植版语言
FoundationAgents/OpenManusPythoneino-examples/flow/agent/manusGo + Eino
bytedance/deer-flowPythoneino-examples/flow/agent/deer-goGo + 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 个节点全部组装成一张大图:

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 调度器。

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 控制是否开启。


五、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
原文来自微信公众号。

推荐文章

在 Nginx 中保存并记录 POST 数据
2024-11-19 06:54:06 +0800 CST
html夫妻约定
2024-11-19 01:24:21 +0800 CST
JavaScript数组 splice
2024-11-18 20:46:19 +0800 CST
HTML和CSS创建的弹性菜单
2024-11-19 10:09:04 +0800 CST
Nginx 跨域处理配置
2024-11-18 16:51:51 +0800 CST
前端代码规范 - 图片相关
2024-11-19 08:34:48 +0800 CST
程序员茄子在线接单