模拟样例:同一个业务场景
场景:用户提问:帮我查2025年贵州茅台的净利润
模型决定调用工具get_financial_report(stock="600519",year="2025")
工具返回:2025年贵州茅台净利润685亿
一、type Message struct
比如我再Eino注册了6个工具:
get_stock_price:查询股票实时价格,参数 stock 股票代码
calculate_irr:内部收益率计算,参数 cash_flows 现金流数组
search_announcement:检索上市公司公告,参数 stock 股票代码,keyword 关键词
get_weather:查询城市天气,参数 city 城市名
search_food:查询当地美食推荐,参数 city 城市名
组装成Message结构体如下:
[]*schema.Message {
{Role: schema.RoleUser, Content: "帮我查 2025 年贵州茅台的净利润"},
}
Eino 拼接出来完整 Prompt,大概如下:
<|im_start|>system
你是金融助手,可以调用下面的工具。
可用工具列表:
get_financial_report: 查询上市公司财报,参数 stock 股票代码,year 年份
get_stock_price: 查询股票实时价格,参数 stock 股票代码
calculate_irr: 内部收益率计算,参数 cash_flows 现金流数组
search_announcement: 检索上市公司公告,参数 stock 股票代码,keyword 关键词
get_weather: 查询城市天气,参数 city 城市名
search_food: 查询当地美食推荐,参数 city 城市名
当需要外部信息时输出工具调用JSON,不要多余文字。
<|im_end|>
<|im_start|>user
帮我查 2025 年贵州茅台的净利润
<|im_end|>
<|im_start|>assistant
Go/Eino根本不会过滤无关工具,不管是财报、天气、美食,只要注册了,全部写进 prompt。 Eino 只是简单循环遍历你注册的全部工具列表,做字符串拼接,没有语义理解能力,分不清工具是否相干。
交给大模型之后,大模型一次性读到 6 个工具描述、用户问题。 模型自己阅读理解:用户问茅台净利润,和天气、美食无关,主动忽略这两个无关工具,选择get_financial_report,输出 JSON。
[{"tool_call_id":"call_001","name":"get_financial_report","arguments":"{\"stock\":\"600519\",\"year\":\"2025\"}"}]
工具数量多的时候,无关工具会增加 token 开销,还可能引发模型误调用,这时就要增加向量检索做工具召回,提前过滤无关工具。
Eino解析后得到的 ToolCall 实例:
toolCall := &schema.ToolCall{
ID: "call_001",
Type: "function",
Function: schema.FunctionCall{
Name: "get_financial_report",
Arguments: `{"stock":"600519","year":"2025"}`,
},
// Index: nil,单次工具调用不需要
// Extra: nil
}
Eino 把 arguments 里面的 stock="600519",year="2025" 传给你的 Go 函数,大概如下:
func fnGetFinancialReport(ctx context.Context, stock, year string) (string, error) {
// 模拟业务:可以查数据库、PDF知识库、第三方金融API
return "2025年贵州茅台净利润685亿", nil
}
然后执行完工具,生成 tool 角色消息:
&schema.Message{
Role: schema.RoleTool,
ToolCallID: "call_001", // 和上面ToolCall.ID对应上!
ToolName: "get_financial_report",
Content: "2025年贵州茅台净利润685亿",
}
现在[]*schema.Message里面一共有 3 条消息:
messages := []*schema.Message{
// 第1条:用户提问
{
Role: schema.RoleUser,
Content: "帮我查 2025 年贵州茅台的净利润",
// ToolCalls nil,用户消息没有工具调用
},
// 第2条:assistant消息,携带ToolCalls(第一轮LLM返回生成)
{
Role: schema.RoleAssistant,
Content: "", // 工具调用模式下content为空,重点在ToolCalls
ToolCalls: []*schema.ToolCall{
{
ID: "call_001",
Type: "function",
Function: schema.FunctionCall{
Name: "get_financial_report",
Arguments: `{"stock":"600519","year":"2025"}`,
},
Index: nil,
Extra: nil,
},
},
},
// 第3条:tool角色消息,Go代码手动构建,工具执行结果
{
Role: schema.RoleTool,
ToolCallID: "call_001", // 必须和上面ToolCall.ID一一对应,用于绑定
ToolName: "get_financial_report",
Content: "2025年贵州茅台净利润685亿",
},
}
Eino 再次渲染完整 prompt
<|im_start|>system
你是金融助手,可以调用下面的工具。
可用工具列表:
get_financial_report: 查询上市公司财报,参数 stock 股票代码,year 年份
get_stock_price: 查询股票实时价格,参数 stock 股票代码
calculate_irr: 内部收益率计算,参数 cash_flows 现金流数组
search_announcement: 检索上市公司公告,参数 stock 股票代码,keyword 关键词
get_weather: 查询城市天气,参数 city 城市名
search_food: 查询当地美食推荐,参数 city 城市名
当需要外部信息时输出工具调用JSON,不要多余文字。
<|im_end|>
<|im_start|>user
帮我查 2025 年贵州茅台的净利润
<|im_end|>
<|im_start|>assistant
[{"id":"call_001","type":"function","function":{"name":"get_financial_report","arguments":"{\"stock\":\"600519\",\"year\":\"2025\"}"}}]
<|im_end|>
<|im_start|>tool
2025年贵州茅台净利润685亿
<|im_end|>
<|im_start|>assistant
把这个完整 prompt 发给大模型(第二次 LLM 请求),大模型读到全部上下文:用户问题 + 刚刚的工具调用、工具返回的财报数据。 模型判断:已经拿到答案,不需要再调用任何工具,直接生成自然语言文本:
2025年贵州茅台净利润为685亿元。
Eino 接收返回文本,解析,生成新的 assistant 消息
&schema.Message{
Role: schema.RoleAssistant,
Content: "2025年贵州茅台净利润为685亿元。",
ToolCalls: nil, // 没有工具调用
}
追加进消息切片,现在消息数组变成 4 条。
具结果消息追加完成后,Eino 会把全部对话历史重新渲染为 prompt,第二次请求大模型。模型结合工具返回的数据生成最终回答。Eino 检测到返回结果不存在 ToolCall,就终止 Agent 循环,把答案返回给用户;如果还有工具调用,则继续循环执行工具。
二、type AgenticMessage struct
组装成AgenticMessage 结构体如下:
messages := []*AgenticMessage{
// System消息:系统提示词 + 全部6个工具描述
{
Role: AgenticRoleSystem,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantText,
AssistantGenText: &AssistantGenText{
Text: `你是金融助手,可以调用下面的工具。
可用工具列表:
get_financial_report: 查询上市公司财报,参数 stock 股票代码,year 年份
get_stock_price: 查询股票实时价格,参数 stock 股票代码
calculate_irr: 内部收益率计算,参数 cash_flows 现金流数组
search_announcement: 检索上市公司公告,参数 stock 股票代码,keyword 关键词
get_weather: 查询城市天气,参数 city 城市名
search_food: 查询当地美食推荐,参数 city 城市名
当需要外部信息时输出工具调用,不要多余文字。`,
},
},
},
},
// 用户提问消息
{
Role: AgenticRoleUser,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeUserText,
UserInputText: &UserInputText{
Text: "帮我查 2025 年贵州茅台的净利润",
},
},
},
},
}
Eino Go 本地处理,渲染 prompt
<|im_start|>system
你是金融助手,可以调用下面的工具。
可用工具列表:
get_financial_report: 查询上市公司财报,参数 stock 股票代码,year 年份
get_stock_price: 查询股票实时价格,参数 stock 股票代码
calculate_irr: 内部收益率计算,参数 cash_flows 现金流数组
search_announcement: 检索上市公司公告,参数 stock 股票代码,keyword 关键词
get_weather: 查询城市天气,参数 city 城市名
search_food: 查询当地美食推荐,参数 city 城市名
当需要外部信息时输出工具调用,不要多余文字。
<|im_end|>
<|im_start|>user
帮我查 2025 年贵州茅台的净利润
<|im_end|>
<|im_start|>assistant
网络请求,把这个 prompt 发给大模型,大模型回复:
[{"id":"call_001","type":"function","function":{"name":"get_financial_report","arguments":"{\"stock\":\"600519\",\"year\":\"2025\"}"}}]
Eino 接收返回的 JSON,组装新的 AgenticMessage(assistant 消息,FunctionToolCall)
{
Role: AgenticRoleAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeFuncToolCall,
FunctionToolCall: &FunctionToolCall{
ID: "call_001",
Type: "function",
Function: FunctionCall{
Name: "get_financial_report",
Arguments: `{"stock":"600519","year":"2025"}`,
},
},
},
},
}
Eino 解析 FunctionToolCall,执行对应 Go 工具函数,取出 Name = "get_financial_report",内存里的 toolMap 字符串精确匹配,找到注册的fnGetFinancialReport,解析 arguments 参数,调用函数:
fnGetFinancialReport(ctx, "600519", "2025")
// 返回结果:"2025年贵州茅台净利润685亿"
Go 代码新建 Tool 角色 AgenticMessage(FunctionToolResult),追加进 messages
{
Role: AgenticRoleTool,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeFuncToolRes,
FunctionToolResult: &FunctionToolResult{
ToolCallID: "call_001", // 和上面tool call的ID对应
Content: "2025年贵州茅台净利润685亿",
},
},
},
}
现在 messages 一共 4 条:system、user、assistant (toolcall)、tool (结果)。
第二轮,Eino 再次渲染全部 messages 为 prompt,第二次请求 LLM
<|im_start|>system
你是金融助手,可以调用下面的工具。
可用工具列表:
get_financial_report: 查询上市公司财报,参数 stock 股票代码,year 年份
get_stock_price: 查询股票实时价格,参数 stock 股票代码
calculate_irr: 内部收益率计算,参数 cash_flows 现金流数组
search_announcement: 检索上市公司公告,参数 stock 股票代码,keyword 关键词
get_weather: 查询城市天气,参数 city 城市名
search_food: 查询当地美食推荐,参数 city 城市名
当需要外部信息时输出工具调用,不要多余文字。
<|im_end|>
<|im_start|>user
帮我查 2025 年贵州茅台的净利润
<|im_end|>
<|im_start|>assistant
[{"id":"call_001","type":"function","function":{"name":"get_financial_report","arguments":"{\"stock\":\"600519\",\"year\":\"2025\"}"}}]
<|im_end|>
<|im_start|>tool
2025年贵州茅台净利润685亿
<|im_end|>
<|im_start|>assistant
大模型读取全部上下文,已经拿到财报数据,不需要继续调用工具。模型输出自然语言文本:
2025年贵州茅台净利润为685亿元。
Eino 接收文本,组装 Assistant 消息(AssistantGenText),追加到 messages
{
Role: AgenticRoleAssistant,
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeAssistantText,
AssistantGenText: &AssistantGenText{
Text: "2025年贵州茅台净利润为685亿元。",
},
},
},
}
三、Message和AgenticMessage 消息结构体区别
Message,一条消息只有:角色 + 文本 Content + 可选 ToolCalls 数组。如果模型一条消息里面,同时输出一段思考文字 + 并行 2 个工具调用,这套结构很难表达;流式分片拼接也麻烦;图片 / 文件 / 音频完全不支持。
而AgenticMessage + ContentBlock
type AgenticMessage struct {
Role AgenticRoleType
ContentBlocks []*ContentBlock // 核心:内容是「内容块切片」
ResponseMeta *AgenticResponseMeta
Extra map[string]any
}
type ContentBlock struct {
Type ContentBlockType
// 一堆不同类型的内容指针:
UserInputText *UserInputText
AssistantGenText *AssistantGenText
FunctionToolCall *FunctionToolCall
FunctionToolResult *FunctionToolResult
UserInputImage *UserInputImage
Reasoning *Reasoning
// ...MCP、音视频、文件等一大堆
}
Role(消息角色,粗粒度):区分整条消息是谁发的:user / assistant / tool / system
ContentBlock.Type(内容块类型,细粒度):在同一条消息内部,区分这一小块内容是什么来源、什么类型(用户文本、模型生成文本、工具调用请求、工具返回结果、图片、思考链等)
比如:
{
Role: AgenticRoleUser, // 整条消息角色:用户
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeUserText, // 内容块标记:用户输入文本
UserInputText: &UserInputText{Text:"帮我查2025茅台净利润"},
},
},
}
Role = user:整条消息来自用户,ContentBlock.Type = user_input_text:这块是用户输入的文本
{
Role: AgenticRoleAssistant, // 整条消息角色:模型助手
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeFuncToolCall, // 内容块:模型发起工具调用
FunctionToolCall: &FunctionToolCall{...},
},
},
}
Role = assistant:整条消息来自大模型,ContentBlock:是模型发起的工具调用请求,不是普通文本回答
{
Role: AgenticRoleTool, // 整条消息角色:工具
ContentBlocks: []*ContentBlock{
{
Type: ContentBlockTypeFuncToolRes, // 内容块:工具返回结果
FunctionToolResult: &FunctionToolResult{...},
},
},
}
Role = tool:整条消息来自工具执行后的返回,ContentBlock 标记这是工具结果。
两则对外给到 LLM 的 prompt 文本是完全一样的!差别只存在 Go 服务内存里面,是内存数据建模方式的不同。