Eino-图编排

代码路径:

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(…) 返回的那个对象。