一、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.Message或schema.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(模型节点、工具节点、检索节点、回调节点)拼出一条执行流水线(有向图),这个 “拼接 + 连线 + 配置节点” 的动作,就叫编排。