代码路径:
C:\Users\Administrator\go\pkg\mod\github.com\cloudwego\eino@v0.9.19\compose\graph.go
type graph struct {...}
graph{
nodes: map[string]*graphNode{
// 以下是一个个组件
"Init": { /* 初始化节点 */ },
"ToolSearch": { /* 新增Lambda节点:工具向量检索 */ },
"ChatModel": { /* AgenticModelNode */ },
"CancelCheck": { /* 中断检查 */ },
"ToolNode": { /* 依然挂载全部6个工具实现,不变 */ },
"AfterToolCalls":{},
"AfterToolCallsCancelCheck": {},
},
// 控制边发生变化:Init 不再直接进ChatModel,先走ToolSearch
controlEdges: map[string][]string{
compose.START: {"Init"},
"Init": {"ToolSearch"}, // 改动点:先去向量检索节点
"ToolSearch": {"ChatModel"}, // 检索完成,把召回的工具放入state,再进大模型
"CancelCheck": {"ToolNode"},
"ToolNode": {"AfterToolCalls"},
"AfterToolCalls": {"AfterToolCallsCancelCheck"},
"AfterToolCallsCancelCheck": {"ChatModel"},
},
branches: map[string][]*GraphBranch{
"ChatModel": { /* 判断模型输出是否带ToolCall,逻辑不变 */ },
},
startNodes: []string{"Init"},
endNodes: []string{compose.END},
stateGenerator: func(ctx context.Context) any {
return &agenticState{
Messages: []*schema.AgenticMessage{},
remainingIterations: 5,
recalledTools: []*schema.ToolInfo{}, // state新增字段:存放向量检索召回出来的工具描述
}
},
compiled: true,
}
执行步骤如下:
Init:拿到用户消息 帮我查2025年贵州茅台的净利润,写入 state.Messages
ToolSearch:拿用户 query 向量检索,召回get_financial_report,存入 state.recalledTools
ChatModel:读取 state.recalledTools,仅把这 1 个工具描述拼进 system prompt,调用 LLM
LLM 输出 tool_call:get_financial_report(stock="600519",year="2025")
ChatModel 的分支检测:识别到存在 tool_call → 路由到 CancelCheck
CancelCheck:判断是否需要中断,无需中断,路由到 ToolNode
ToolNode:6 个工具实现里找到get_financial_report,执行,返回2025年贵州茅台净利润685亿
AfterToolCalls:把工具返回消息追加到 state.Messages
AfterToolCallsCancelCheck:检查是否中断,不需要,路由回到 ChatModel(循环)
ChatModel 再次调用大模型:把历史消息(用户 + 工具返回结果)丢给 LLM
LLM 这次直接输出纯文本答案,没有 tool_call
ChatModel 分支判断:无 tool_call → 路由到 END,流程结束
二、Eino 内置预制编排图
代码路径:
C:\Users\Administrator\go\pkg\mod\github.com\cloudwego\eino@v0.9.19\adk\react.go
func newAgenticReact(ctx context.Context, config *agenticReactConfig) (agenticReactGraph, error) {...}
// 1. 新建空图
g := compose.NewGraph[*agenticReactInput, *schema.AgenticMessage](...)
// 2. 注册各个节点(编排节点)
_ = g.AddLambdaNode(initNode_, ...)
_ = g.AddAgenticModelNode(chatModel_, wrappedModel, ...)
_ = g.AddLambdaNode(cancelCheckNode_, ...) // 框架内部编排加上CancelCheck
_ = g.AddAgenticToolsNode(toolNode_, toolsNode, ...)
_ = g.AddLambdaNode(afterToolCallsNode_, ...)
// 3. 增加边、分支(编排流转路线)
_ = g.AddEdge(compose.START, initNode_)
_ = g.AddEdge(initNode_, chatModel_)
// ... AddBranch 分支逻辑:ChatModel跑完判断是否有tool_call,选择下一跳
// 4. 返回组装好的图
return g, nil
三、节点注册过程
代码路径:
//github.com\cloudwego\eino@v0.9.19\adk\react.go
type agenticReactConfig = typedReactConfig[*schema.AgenticMessage]
这里把泛型参数 M 固定为 *schema.AgenticMessage。只要用 agenticReactConfig,整套 Agent 流转的消息类型就锁定是 *schema.AgenticMessage,不会是旧版 *schema.Message。
wrappedModel = buildModelWrappers(config.model, config.modelWrapperConf)
将原始模型实例增强包装,具备重试、超时、回调 、埋点、请求参数拦截、修改、错误处理等能力,产出包装后的 wrappedModel,接口保持不变,直接送入 ChatModel 节点。
_ = g.AddAgenticModelNode(chatModel_, wrappedModel, compose.WithStatePreHandler(
func(ctx context.Context, input []*schema.AgenticMessage, st *agenticState) ([]*schema.AgenticMessage, error) {
if st.getRemainingIterations() <= 0 {
return nil, ErrExceedMaxIterations
}
st.decrementRemainingIterations()
return input, nil
}), compose.WithNodeName(chatModel_))
func (g *graph) AddAgenticModelNode(key string, node model.AgenticModel, opts ...GraphAddNodeOpt) error {
gNode, options := toAgenticModelNode(node, opts...)
return g.addNode(key, gNode, options)
}
代码路径:
C:\Users\Administrator\go\pkg\mod\github.com\cloudwego\eino@v0.9.19\compose\component_to_graph_node.go
func toAgenticModelNode(node model.AgenticModel, opts ...GraphAddNodeOpt) (*graphNode, *graphAddNodeOpts) {
return toComponentNode(
node,
components.ComponentOfAgenticModel,
node.Generate,
node.Stream,
nil, nil,
opts...,
)
}
C:\Users\Administrator\go\pkg\mod\github.com\cloudwego\eino@v0.9.19\compose\component_to_graph_node.go
func toComponentNode[I, O, TOption any](
node any,
componentType component,
invoke Invoke[I, O, TOption],
stream Stream[I, O, TOption],
collect Collect[I, O, TOption],
transform Transform[I, O, TOption],
opts ...GraphAddNodeOpt,
) (*graphNode, *graphAddNodeOpts) {
meta := parseExecutorInfoFromComponent(componentType, node)
info, options := getNodeInfo(opts...)
run := runnableLambda(invoke, stream, collect, transform,
!meta.isComponentCallbackEnabled,
)
gn := toNode(info, run, nil, meta, node, opts...)
return gn, options
}
把传入的 node.Generate(invoke)、node.Stream(stream)存入 runnablePacker,返回 *composableRunnable,这个对象实现 compose 的 Runnable 接口,具备标准Invoke/Stream,Pregel 引擎能识别。
然后把上面得到的 run(composableRunnable)放到 graphNode 内部,这个graphNode后续存入 graph.nodes[“ChatModel”](也就是chatModel_节点 key)
四、Pregel 引擎执行阶段,流转到 ChatModel 节点
type graphNode struct {
cr *composableRunnable // 就是 composableRunnable!!
g AnyGraph
nodeInfo *nodeInfo
executorMeta *executorMeta
instance any // 这里存原始的 node,也就是 wrappedModel
opts []GraphAddNodeOpt
}
func (e *pregelExecutor) runNode(ctx context.Context, nodeKey string, state *state) error {
// 1. 根据节点名字key,从 graph.nodes map 获取 graphNode
gNode, ok := e.graph.nodes[nodeKey]
if !ok {
return fmt.Errorf("node %s not found", nodeKey)
}
// 直接取 gNode.cr,就是 composableRunnable
runnable := gNode.cr
input := state.ReadMessages()
if streamMode {
// 引擎调用 composableRunnable.Stream
streamReader, err := runnable.Stream(ctx, input)
} else {
output, err := runnable.Invoke(ctx, input)
}
// ...
}
gNode.cr 就是 toComponentNode 里面 run := runnableLambda(…) 返回的那个对象。