Eino-启动 Agent 智能体任务具体过程

一、NewAsyncIteratorPair 创建一对 iterator(对外读) / generator(内部发事件)

其中generator结构体对象由协程执行,接收大模型返回的 SSE 流式分片,转成 Eino 的 frame,包装成TypedAgentEvent事件,调用generator.Send(event),推到内部 channel。

generator.Send(&TypedAgentEvent[M]{Err: fmt.Errorf("ChatModelAgent getRunFunc error: %w", err)})

主协程执行iterator结构体对象。拿到iterator对象,for event, ok := iter.Next()循环读取 channel 里的数据,拿到事件,提取文本打印控制台

其中:TypedAgentEvent[M] 是 Eino 中 Agent 执行链路统一的事件载体,泛型参数 M 承载对话消息结构体(schema.Messageschema.AgenticMessage),Agent 运行期间产生的各类信息,包括大模型输出的消息、工具调用的动作(工具开始执行 / 工具执行完成结果)、执行链路追踪信息以及运行异常错误,都会被封装为该结构体实例,通过 generator 发送到事件通道,上层业务借助 iterator 读取事件,依靠结构体里的 Output、Action、Err 等字段区分事件类型,统一处理模型文本、工具行为和报错,从而解耦 Agent 后台执行逻辑与上层的事件消费、展示、日志逻辑。

eventB := TypedAgentEvent[*schema.AgenticMessage]{
    AgentName: "chat_agent",
    RunPath: []RunStep{...},
    Output: nil, // 这一步不是大模型输出,Output为空
    Action: &AgentAction{
        Type: AgentActionTypeToolFinished,
        ToolResult: res, // "2025年贵州茅台净利润685亿"
    },
    Err: nil,
}

二、处理 ctx:设置取消、panic 保护上下文,把 generator 存入 ctx

ctx 的是在单次 Agent 任务链路里传递统一的生命周期信号与任务上下文,它本身不会终止代码,而是让后续 run 的 ReAct 循环能够主动监听取消信号,一旦收到取消指令(如:用户按下Ctrl+C → ctx 触发 cancel),就停止模型调用、工具执行等操作,配合 generator 关闭事件流,实现任务的统一可控终止,同时把 generator 等任务专属数据向下传递,避免多层函数签名臃肿,保证并发多个 Agent 任务之间上下文相互隔离、互不干扰。

ctx = withTypedChatModelAgentExecCtx(ctx, &typedChatModelAgentExecCtx[M]{
	generator:                generator,
	cancelCtx:                cancelCtx,
	failoverLastSuccessModel: a.model,
})

ctx, run, bc, err := a.getRunFunc(ctx)

三、getRunFunc():拿到底层 Agent 循环函数 run,拿到 bridgeConfig(里面存 system 指令、全部工具信息)

getRunFunc是获取 Agent 核心执行函数 run、对话配置 bc 并返回携带任务信息的新 ctx;

比如说,我注册了一些工具,【详见:Eino-区分消息结构体AgenticMessage和Message 工具】

getRunFunc 取出包含全部工具元信息的 bc 以及 run 主循环函数,run 拿到 bc.toolInfos ,将工具信息传入 compose 编排配置;底层模型组件会把工具描述拼装进 prompt / 工具定义参数发送给大模型;当大模型返回 get_financial_report 的 ToolCall 时,run 内部的 ReAct 逻辑识别到工具调用指令,依据工具名称在预先注册的工具注册表中找到对应的 Go 业务函数并执行,获取财报查询结果后,将结果包装为 tool 角色消息追加到消息数组,随后 run 继续循环,把更新后的完整消息数组再次提交给大模型,反复执行该流程,直到大模型输出纯文本内容、不再产生工具调用为止。当然,除了模型输出纯文本结束,也可能因为ctx 取消、超时、最大轮次限制提前终止 ReAct 循环。

底层模型组件会把工具描述拼装进 prompt / 工具定义参数发送给大模型,怎么理解这句话呢?大模型底层确实是一个 token 一个 token 自回归预测,但是 OpenAI / Ark 这类服务做了封装,模型服务层(API 网关)额外做了结构化输出解析。

{
  "messages": [
    {"role":"system", "content":"你是助手..."},
    {"role":"user", "content":"查茅台净利润"}
  ],
  "tools": [  // 独立字段!不是写在content文本里面
    {
      "type":"function",
      "function":{
        "name":"get_financial_report",
        "description":"查询财报",
        "parameters": {...}
      }
    }
  ],
  "stream":true
}

客户端通过 API 协议将 messages 和 tools 参数发给大模型服务商服务端;服务商网关会把 tools 工具定义翻译成文本,合并到提示词上下文,转为 token 送入底层大模型。大模型底层仍然是逐 token 预测输出文本(工具调用本质是输出 JSON 字符串);服务商服务端再解析模型输出,把工具调用信息封装成 API 协议里独立的 tool_calls 字段返回给调用方。

四、组装 compose 编排选项:把所有工具信息注入模型参数

ctx, run, bc, err := a.getRunFunc(ctx)
if bc != nil {
	if len(bc.toolInfos) > 0 {
		co = append(co, compose.WithChatModelOption(model.WithTools(bc.toolInfos)))
	}
	if bc.toolSearchTool != nil {
		co = append(co, compose.WithChatModelOption(model.WithToolSearchTool(bc.toolSearchTool)))
	}
	if bc.toolUpdated {
		co = append(co, compose.WithToolsNodeOption(compose.WithToolList(bc.toolsNodeConf.Tools...)))
	}
}
// 模拟 execContext 实例
var bc *execContext
ec := &execContext{
    instruction: `你是金融助手,可以调用下面的工具。
可用工具列表:
get_financial_report: 查询上市公司财报,参数 stock 股票代码,year 年份
get_stock_price: 查询股票实时价格,参数 stock 股票代码
calculate_irr: 内部收益率计算,参数 cash_flows 现金流数组
search_announcement: 检索上市公司公告,参数 stock 股票代码,keyword 关键词
get_weather: 查询城市天气,参数 city 城市名
search_food: 查询当地美食推荐,参数 city 城市名
当需要外部信息时输出工具调用,不要多余文字。`,

    // 工具执行节点配置:存放所有工具的真实Go实现函数
    toolsNodeConf: compose.ToolsNodeConfig{
        Tools: []tool.BaseTool{
            get_financial_report_tool,
            get_stock_price_tool,
            calculate_irr_tool,
            search_announcement_tool,
            get_weather_tool,
            search_food_tool,
        },
    },

    // returnDirectly:某些工具执行完直接返回结果,不再次丢给大模型(这里我们不用,全部false)
    returnDirectly: map[string]bool{
        "get_financial_report": false,
        "get_stock_price": false,
        "calculate_irr": false,
        "search_announcement": false,
        "get_weather": false,
        "search_food": false,
    },

    // toolInfos:6个工具的【元信息/描述+参数schema】,给LLM看的ToolInfo
    toolInfos: []*schema.ToolInfo{
        toolInfo_get_financial_report,
        toolInfo_get_stock_price,
        toolInfo_calculate_irr,
        toolInfo_search_announcement,
        toolInfo_get_weather,
        toolInfo_search_food,
    },

    // unwrappedTools:解包后的底层工具实例集合
    unwrappedTools: []tool.BaseTool{
        get_financial_report_tool,
        get_stock_price_tool,
        calculate_irr_tool,
        search_announcement_tool,
        get_weather_tool,
        search_food_tool,
    },

    // 开启原生工具检索:不为nil,代表模型节点支持工具向量召回
    toolSearchTool: toolInfo_tool_search,

    // rebuildGraph:工具发生改动,需要重新构建 Compose 执行图(Agent图)
    rebuildGraph: true,

    // toolUpdated:工具列表变更,需要给ToolsNode传入WithToolList选项同步函数
    toolUpdated: true,
}
var co []compose.Option 
 &compose.Option{
	options: []any{
		// ① ChatModel节点配置:全部6个工具元信息toolInfos
		&chatmodel.ChatModelOption{
			Tools: bc.toolInfos,
		},
		// ② ChatModel节点配置:绑定工具检索工具
		&chatmodel.ChatModelOption{
			ToolSearchTool: bc.toolSearchTool,
		},
		// ③ ToolsNode工具执行节点配置:挂载6个工具的真实Go函数
		&toolnode.ToolsNodeOption{
			Tools: bc.toolsNodeConf.Tools,
		},
	},
	handler: nil,
	paths: nil,
	maxRunSteps: 20,
	checkPointID: nil,
	writeToCheckPointID: nil,
	forceNewRun: false,
	stateModifier: nil,
	subGraphCheckpointPublisher: nil,
}

如果不开启工具检索器toolSearch(向量检索),原始 toolInfos 共 6 个工具,ChatModel 节点内部遍历所有[]*schema.ToolInfo,自动生成工具描述,拼接在你写的 instruction 后面,送给大模型完整 system prompt:

你是金融助手,可以调用下面的工具。
当需要外部信息时输出工具调用,不要多余文字。

# 可用工具列表
1. get_financial_report
描述:查询上市公司财报
参数:
- stock: string,股票代码
- year: string,年份

2. get_stock_price
描述:查询股票实时价格
参数:
- stock: string,股票代码

3. calculate_irr
描述:内部收益率计算
参数:
- cash_flows: array,现金流数组

4. search_announcement
描述:检索上市公司公告
参数:
- stock: string,股票代码
- keyword: string,关键词

5. get_weather
描述:查询城市天气
参数:
- city: string,城市名

6. search_food
描述:查询当地美食推荐
参数:
- city: string,城市名

如果开启开启 toolSearchTool(向量检索),框架先拿到完整的 6 条 toolInfos 作为检索候选库,使用用户问题“帮我查 2025 贵州茅台净利润”调用 toolSearchTool 执行向量相似度检索,仅召回匹配的 get_financial_report 这一条 ToolInfo;随后将召回后的这条 ToolInfo 序列化并拼接进 system 提示词再传给大模型,此时送入 LLM 的 system prompt 里只包含检索命中的这 1 个工具描述,原始 bc.toolInfos 里完整的 6 条工具仅作为检索候选库,不会全部塞进 prompt。

你是金融助手,可以调用下面的工具。
当需要外部信息时输出工具调用,不要多余文字。

# 可用工具列表
1. get_financial_report
描述:查询上市公司财报
参数:
- stock: string,股票代码
- year: string,年份

用户提问:帮我查2025年贵州茅台的净利润

大模型看到的只有这一个工具,直接输出 toolcall,不会看到天气、美食等无关工具。

为什么需要编排呢?因为Component 都是独立零件:ChatModel、ToolsNode、Retriever。单个零件自己只能干自己的活。

ChatModel:只会读消息、输出文本 / ToolCall

ToolsNode:只会收到 ToolCall,执行工具函数,返回结果 单独放着,两者互不认识。编排 = 给零件连线,定义数据怎么走

所以,需要把多个独立 Component(模型节点、工具节点、检索节点、回调节点)拼出一条执行流水线(有向图),这个 “拼接 + 连线 + 配置节点” 的动作,就叫编排。