系列「企业级 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 第一行:
shell
> 本仓库参考 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):
go
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 的模式完全一致,但做到了全图统一:
go
// 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 调度器。
go
// 所有节点统一挂同一个分支函数
_ = 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。
go
// 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
})
}
执行完毕,bIRouter 把 state.Goto 设为 consts.Planner,背景调查结果就传给了下一个节点。
为什么要这一步?
Planner 拿到背景信息之后,生成的研究计划更有针对性。比如"研究最新量子计算进展",背景调查可能发现最近有一篇重要论文,Planner 就可以在计划里专门设一个步骤去深挖它。
这个节点是可选的------state.EnableBackgroundInvestigation 控制是否开启。Coordinator 里:
go
// 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 结构体后驱动整个研究流程。
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 解析:
go
// 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:需要研究,走正常流程
go
// 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):
go
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):
go
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 实现了 DeerCheckPoint(model/state.go:73):
go
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(含已完成步骤的结果),恢复后继续跑剩余步骤。
十、完整流程:带所有节点的一次深度研究
ini
用户提问
↓
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 介绍的版本,多出三件事:
- BackgroundInvestigator 在规划前预搜索,给 Planner 提供背景
- 结构化 Plan JSON 把研究计划变成可程序驱动的数据,而不是模糊的文字
- Human 计划审批 在执行前让人确认研究方向,代价最小的人工干预点
三者合在一起,让 deer-go 能在"深度研究"这个场景里做到:方向对、计划实、执行可追踪。
代码来源:cloudwego/eino-examples · 原版:bytedance/deer-flow