Eino-区分消息结构体AgenticMessage和Message

模拟样例:同一个业务场景

场景:用户提问:帮我查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 服务内存里面,是内存数据建模方式的不同。