第十一篇:Eino核心解析 - Message与Prompt上下文编排
承接第十篇对Eino底层ChatModel组件的解析(核心入参为[]*schema.Message),本篇将从实战开发者视角,结合完整源码与可运行案例,系统讲解Eino的消息协议设计、Prompt模板引擎、多轮上下文管理、流式消息处理等核心能力。无论你是刚接触Eino的新手,还是需要深度定制大模型应用的开发者,都能通过本文掌握Eino上下文编排的底层逻辑与最佳实践。
一、为什么需要schema包?—— 统一大模型消息协议
在第十篇中我们知道,Eino的ChatModel组件只认[]*schema.Message作为输入,但为什么要单独设计schema包?
市面上不同大模型厂商(OpenAI、通义千问、DeepSeek、Ollama)的消息格式差异极大:
- • 角色定义不统一(有的叫
assistant,有的叫bot); - • 多模态消息(图片/音频)的存储结构各不相同;
- • 工具调用的参数格式千差万别;
- • 流式返回的分片规则不一致。
Eino的schema包本质是全局统一的消息协议层,核心价值在于:
- 1. 屏蔽厂商差异:统一4种核心角色、标准化多模态/工具调用格式;
- 2. 降低开发成本:提供开箱即用的消息构造、模板渲染、流式合并工具;
- 3. 保证扩展性:支持业务自定义扩展字段,兼容未来新的消息类型;
- 4. 提升稳定性:内置类型校验、字段规范,避免格式错误导致的调用失败。
源码入口:
github.com/cloudwego/eino/schema
二、核心基础:Message结构体全解析
Message是Eino中所有对话内容的载体,先从最基础的结构开始拆解,让你一看就懂。
2.1 先搞懂:4种核心消息角色
Eino定义了4种标准化角色,覆盖所有对话场景,且与主流大模型兼容:
type RoleType string
const (
Assistant RoleType = "assistant" // AI模型的输出消息
User RoleType = "user" // 用户输入的消息
System RoleType = "system" // 系统提示词(定义模型人设/规则)
Tool RoleType = "tool" // 工具调用的返回结果
)为了简化开发,Eino提供了4个快捷构造函数(推荐优先使用,避免手动赋值出错):
// 示例:快速创建各类消息
sysMsg := schema.SystemMessage("你是一个专业的Go开发助手,回答简洁易懂") // 系统消息
userMsg := schema.UserMessage("Eino的Message结构体有哪些核心字段?") // 用户消息
assistMsg := schema.AssistantMessage("核心字段包括Role、Content等", nil) // 助手消息
toolMsg := schema.ToolMessage("查询结果:100", "call-123", schema.WithToolName("get_user_info")) // 工具消息2.2 Message完整结构:字段详解
以下是Message的核心字段拆解,标注了“必选/可选”和“使用场景”,告别源码看不懂的烦恼:
| 字段名 | 类型 | 是否必选 | 核心作用 |
|---|---|---|---|
| Role | RoleType | 是 | 消息角色(system/user/assistant/tool) |
| Content | string | 纯文本是 | 纯文本消息内容(普通对话优先用这个) |
| UserInputMultiContent | []MessageInputPart | 可选 | 用户侧多模态输入(图片/音频/文件,替代旧版MultiContent) |
| AssistantGenMultiContent | []MessageOutputPart | 可选 | 模型侧多模态输出(图片/思考链,替代旧版MultiContent) |
| ToolCalls | []ToolCall | 可选 | Assistant消息专属:模型发起的工具调用指令 |
| ToolCallID | string | Tool消息是 | Tool消息专属:绑定对应的工具调用ID(必须和ToolCalls中的ID一致) |
| ResponseMeta | *ResponseMeta | 可选 | 模型返回元数据(Token消耗、结束原因、对数概率) |
| Extra | map[string]any | 可选 | 业务扩展字段(存储自定义数据,如消息ID、创建时间) |
关键使用规范
- 1. 纯文本对话:只填
Role + Content,别碰多模态字段; - 2. 多模态输入(比如用户发图片):只写
UserInputMultiContent,不要同时赋值Content; - 3. 工具调用场景:Tool消息必须填
ToolCallID,否则无法关联对应的工具调用; - 4. 扩展字段:
Extra只存业务数据,核心逻辑字段(如角色、内容)不要放这里。
2.3 多模态消息:图片/音频怎么存?
如果需要处理图片、音频等多模态内容,Eino分“用户输入”和“模型输出”两种结构,逻辑清晰:
1)用户侧多模态输入(MessageInputPart)
支持文本、图片(URL/Base64)、音频、视频、文件,以图片为例:
// 示例:创建带图片的用户消息
imgPart := schema.MessageInputPart{
Type: schema.ChatMessagePartTypeImage,
Image: &schema.MessageInputImage{
URL: "https://example.com/ai-code.png", // 图片URL
Detail: schema.ImageURLDetailHigh, // 高清解析
Extra: map[string]any{"image_id": "img-001"},
},
}
// 组装成用户消息
userMultiMsg := &schema.Message{
Role: schema.User,
UserInputMultiContent: []schema.MessageInputPart{imgPart},
}2)模型侧多模态输出(MessageOutputPart)
相比输入,额外支持“思考链”(适配DeepSeek、通义千问推理版等模型):
// 示例:模型返回思考链+文本
reasoningPart := schema.MessageOutputPart{
Type: schema.ChatMessagePartTypeReasoning,
Reasoning: &schema.MessageOutputReasoning{
Content: "用户问图片中的代码问题,先分析语法错误,再给出修复方案",
Sign: "xxx", // 思考链签名(多轮对话需回传)
},
}
textPart := schema.MessageOutputPart{
Type: schema.ChatMessagePartTypeText,
Text: "图片中的代码缺少error处理,修复后如下:...",
}
// 组装成助手消息
assistMultiMsg := &schema.Message{
Role: schema.Assistant,
AssistantGenMultiContent: []schema.MessageOutputPart{reasoningPart, textPart},
}2.4 元数据:Token消耗/结束原因怎么看?
模型返回的非内容信息(比如用了多少Token、为什么停止输出)都存在ResponseMeta里:
// 示例:获取Token消耗
if resp.ResponseMeta != nil && resp.ResponseMeta.Usage != nil {
usage := resp.ResponseMeta.Usage
fmt.Printf("提示词Token:%d,生成内容Token:%d,总计:%d\n",
usage.PromptTokens, usage.CompletionTokens, usage.TotalTokens)
}三、Prompt模板引擎:动态拼接多轮上下文
第十篇中ChatModel需要固定的[]*Message入参,但实际业务中,我们经常需要:
- • 动态替换模板变量(比如“{user_name}”“{query}”);
- • 注入多轮历史对话;
- • 支持条件/循环等复杂模板逻辑。
Eino的MessagesTemplate接口就是为解决这些问题而生,提供3种易用的模板引擎。
3.1 3种模板引擎:选对工具效率翻倍
Eino支持3种渲染方式,覆盖从简单到复杂的所有场景:
| 引擎类型 | 语法示例 | 适用场景 | 特点 |
|---|---|---|---|
| FString | 你好{name},{age}岁 | 简单变量替换(推荐日常使用) | 类似Python格式化,易上手 |
| GoTemplate | {{if .isVIP}}VIP用户{{end}} | 复杂逻辑(分支/循环) | Go原生语法,功能强 |
| Jinja2 | {% if isVIP %}VIP{% endif %} | 兼容Python生态模板 | 禁用危险标签,安全可控 |
3.2 核心痛点解决:多轮历史对话注入
手动拼接历史对话容易出错(比如格式不对、角色混乱),Eino提供MessagesPlaceholder专门解决这个问题:
// 创建历史对话占位符:key=变量名,optional=是否可选(缺失是否报错)
historyPlaceholder := schema.MessagesPlaceholder("chat_history", true)核心优势
- 1. 类型安全:只接受
[]*schema.Message类型,杜绝字符串拼接的格式错误; - 2. 容错可控:可选占位符缺失时返回空,非可选占位符缺失直接报错,提前暴露问题;
- 3. 逻辑清晰:历史对话与模板分离,代码易维护。
3.3 模板使用示例:一步到位
// 1. 构建模板(FString引擎)
tpl := prompt.FromMessages(schema.FString,
schema.SystemMessage("你是{domain}助手,回答风格:{style}"), // 系统消息带变量
schema.MessagesPlaceholder("chat_history", true), // 历史对话占位
schema.UserMessage("用户问题:{query}"), // 用户问题带变量
)
// 2. 准备变量(含历史对话)
variables := map[string]any{
"domain": "Eino Go大模型框架",
"style": "简洁专业",
"chat_history": []*schema.Message{ // 历史对话
schema.UserMessage("schema.Message有几种核心角色?"),
schema.AssistantMessage("核心角色有4种:system、user、assistant、tool"),
},
"query": "MessagesPlaceholder的核心作用是什么?",
}
// 3. 渲染模板 → 标准化消息切片
fullMessages, err := tpl.Format(ctx, variables)
if err != nil {
// 错误处理
}四、流式消息合并:解决分片拼接难题
第十篇提到ChatModel支持流式返回(Stream方法),每次只能拿到“碎片化”的消息(比如先返回“你”,再返回“好”),手动拼接容易丢失信息(比如工具调用、Token元数据)。
Eino内置了完整的流式合并工具,一行代码搞定:
4.1 核心合并函数(开箱即用)
// 1. 获取流式阅读器
stream, err := model.Stream(ctx, fullMessages)
if err != nil { /* 错误处理 */ }
defer stream.Close()
// 2. 合并所有分片 → 完整Message
fullMsg, err := schema.ConcatMessageStream(stream)
if err != nil { /* 错误处理 */ }
// 3. 拿到完整结果
fmt.Println(fullMsg.Content) // 完整文本
fmt.Println(fullMsg.ToolCalls) // 完整工具调用
fmt.Println(fullMsg.ResponseMeta.Usage) // 完整Token消耗4.2 合并规则
Eino会自动处理:
✅ 拼接文本内容、思考链;
✅ 合并工具调用分片(按Index排序);
✅ 汇总Token消耗、保留最后一个结束原因;
✅ 校验所有分片的角色一致性(避免拼接错误)。
五、完整实战Demo:覆盖所有核心能力
以下是可直接运行的代码,包含“模板渲染+多轮上下文+模型调用+流式合并”全流程。
5.1 前置准备
- 1. 安装依赖:
go get github.com/cloudwego/eino
go get github.com/cloudwego/eino-ext/components/model/openai
go mod tidy- 2. 获取DeepSeek API Key(免费申请:https://platform.deepseek.com/),配置环境变量:
# Linux/Mac
export DEEPSEEK_API_KEY="你的API Key"
# Windows
set DEEPSEEK_API_KEY="你的API Key"5.2 完整代码
package main
import (
"context"
"fmt"
"os"
eino_openai "github.com/cloudwego/eino-ext/components/model/openai"
"github.com/cloudwego/eino/components/prompt"
"github.com/cloudwego/eino/schema"
)
// ==================== Eino 框架:Prompt 模板 ====================
// 本 demo 演示 Eino 的 Prompt 模板:变量替换 + 历史消息占位 + 渲染为标准消息。
// 对比 learn10(手动构造 messages),这里用模板引擎实现参数化 Prompt。
const (
baseURL = "https://api.deepseek.com"
modelName = "deepseek-v4-flash" // DeepSeek 模型,若不存在可改 deepseek-chat
)
// ptr 返回值的指针,用于 *float32 等需要指针的配置字段。
func ptr[T any](v T) *T { return &v }
// newChatModel 创建 DeepSeek ChatModel 实例。
func newChatModel(ctx context.Context) (*eino_openai.ChatModel, error) {
apiKey := os.Getenv("DEEPSEEK_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("DEEPSEEK_API_KEY 环境变量未设置")
}
return eino_openai.NewChatModel(ctx, &eino_openai.ChatModelConfig{
BaseURL: baseURL,
APIKey: apiKey,
Model: modelName,
Temperature: ptr(float32(0.4)), // 随机性:0-1,越小越稳定
MaxTokens: ptr(1024), // 最大生成 Token 数
})
}
// buildTemplate 构建带变量和历史占位的 Prompt 模板。
// schema.FString 使用 Python 风格的 {var} 占位符;MessagesPlaceholder 注入多轮历史。
func buildTemplate() *prompt.DefaultChatTemplate {
return prompt.FromMessages(schema.FString,
schema.SystemMessage("你是{domain}技术助手,回答风格:{style}"),
schema.MessagesPlaceholder("chat_history", true), // true=可选,无历史时不报错,如果填 false 则必填 chat_history 变量
schema.UserMessage("用户问题:{query}"),
)
}
// buildVariables 准备模板变量:领域、风格、多轮历史、当前问题。
func buildVariables() map[string]any {
history := []*schema.Message{
schema.UserMessage("Eino 的 schema 包有什么作用?"),
schema.AssistantMessage("schema 包是 Eino 的统一消息协议层,屏蔽不同大模型的格式差异,简化开发", nil),
}
return map[string]any{
"domain": "Eino Go 大模型框架",
"style": "简洁专业,用新手能懂的语言",
"chat_history": history, // 如果 MessagesPlaceholder 中填 false,这里必填 chat_history 变量
"query": "如何正确使用 MessagesPlaceholder?",
}
}
// printMessages 打印渲染后的消息列表(调试用)。
func printMessages(messages []*schema.Message) {
fmt.Println("====== 渲染后的上下文 ======")
for i, msg := range messages {
fmt.Printf("[%d] 角色:%s → 内容:%s\n", i, msg.Role, msg.Content)
}
}
// printUsage 打印 Token 消耗(成本核算必备)。
func printUsage(resp *schema.Message) {
if resp.ResponseMeta == nil || resp.ResponseMeta.Usage == nil {
return
}
u := resp.ResponseMeta.Usage
fmt.Println("====== Token 消耗 ======")
fmt.Printf("提示词 Token:%d\n生成 Token:%d\n总计 Token:%d\n",
u.PromptTokens, u.CompletionTokens, u.TotalTokens)
}
func main() {
ctx := context.Background()
// 1. 初始化模型
model, err := newChatModel(ctx)
if err != nil {
fmt.Printf("模型初始化失败:%v\n", err)
return
}
// 2. 构建模板并渲染(变量替换 + 历史注入)
tpl := buildTemplate()
messages, err := tpl.Format(ctx, buildVariables())
if err != nil {
fmt.Printf("模板渲染失败:%v\n", err)
return
}
printMessages(messages)
// 3. 调用模型(同步模式)
fmt.Println("\n====== 模型回答 ======")
resp, err := model.Generate(ctx, messages)
if err != nil {
fmt.Printf("模型调用失败:%v\n", err)
return
}
fmt.Println(resp.Content)
printUsage(resp)
}
5.3 运行结果示例
====== 渲染后的上下文 ======
[0] 角色:system → 内容:你是Eino Go 大模型框架技术助手,回答风格:简洁专业,用新手能懂的语言
[1] 角色:user → 内容:Eino 的 schema 包有什么作用?
[2] 角色:assistant → 内容:schema 包是 Eino 的统一消息协议层,屏蔽不同大模型的格式差异,简化开发
[3] 角色:user → 内容:用户问题:如何正确使用 MessagesPlaceholder?
====== 模型回答 ======
MessagesPlaceholder 的作用是在提示模板中预留一个位置,运行时动态插入一组**消息列表**(例如历史对话),用于支持多轮对话上下文。
正确使用步骤如下:
1. **定义模板**:在模板中声明占位符,例如 `{{.History}}`,它代表一个消息数组。
2. **构造输入**:在生成请求时,通过 `map[string]any` 传入变量,其中 `History` 的值必须是 `[]*schema.Message` 类型(来自 `schema` 包)。
3. **渲染模板**:模板引擎会将该消息列表原样插入,而不是转成单个字符串,因此每条消息都会保留 role/content 等结构化信息。
**示例(伪代码)**:
```go
import "github.com/cloudwego/eino/schema"
tmpl := "... {{.History}} ... {{.Question}}" // 模板
input := map[string]any{
"History": []*schema.Message{
{Role: schema.User, Content: "你好"},
{Role: schema.Assistant, Content: "有什么可以帮你?"},
},
"Question": "你是谁?",
}
// 渲染后即可作为完整的对话输入传给模型
```
常见错误:
- 将历史对话直接作为字符串拼接,而不是传入 `[]*schema.Message`。
- 占位符名称与模板中变量名不一致(如写 `{{.history}}` 但传入 `History`)。
确保引用 `schema` 包中的 `Message` 类型,并检查你的 Eino 版本中模板语法是否基于 Go `text/template`。更详细的 API 请参考 Eino 官方文档中 “Prompt Template” 或 “ChatTemplate” 部分。
====== Token 消耗 ======
提示词 Token:150
生成 Token:892
总计 Token:1042
六、最佳实践 & 避坑清单
6.1 最佳实践
- 1. 消息创建:优先使用快捷构造函数(SystemMessage/UserMessage),减少手动赋值错误;
- 2. 模板选择:日常业务用FString,复杂逻辑用GoTemplate,尽量不用Jinja2;
- 3. 历史对话:强制用MessagesPlaceholder注入,禁止字符串拼接;
- 4. 流式处理:必须用ConcatMessageStream合并分片,不要手动拼接Content;
- 5. 多模态:严格区分UserInputMultiContent(用户输入)和AssistantGenMultiContent(模型输出)。
6.2 常见坑点
- 1. 给Tool消息漏填ToolCallID → 工具调用无法关联;
- 2. 多模态消息同时赋值Content和UserInputMultiContent → 模型可能忽略部分内容;
- 3. MessagesPlaceholder传入字符串/切片 → 直接报类型错误;
- 4. 流式分片手动拼接Content → 丢失工具调用/Token元数据;
- 5. Jinja2模板尝试加载本地文件 → 被Eino安全机制拦截,报错。
七、总结
- • 第十篇的ChatModel是“执行器”,本篇的schema包是“数据载体”:ChatModel只认
[]*schema.Message,而schema包负责标准化、模板化生成这个载体; - • Prompt模板引擎解决“动态上下文组装”问题,让多轮对话开发更高效;
- • 流式合并工具解决ChatModel.Stream的碎片化数据问题,保证结果完整;
- • schema包是Eino的“数据总线”,连接Prompt、ChatModel、工具调用、Agent等所有核心组件。
MiaoAll