Prompt 模板:用变量组装发给 AI 的消息

0 阅读5分钟

系列「企业级 AI Agent 实现拆解」E14 篇。上一篇 E13 讲了 Indexer——把知识块存进向量数据库。这篇回到 Agent 的核心流程:怎么把用户问题、对话历史、检索到的知识块,拼成一段完整的 prompt 发给 AI。答案是 ChatTemplate

读完这篇你会知道

  • ChatTemplate 接口是什么:就一个 Format() 方法
  • 三种模板语法:FString({var})、GoTemplate({{.var}})、Jinja2({{var}}
  • MessagesPlaceholder:怎么把整段对话历史插进 prompt
  • 如何在 Chain 和 Graph 里用 ChatTemplate
  • 一个完整示例:带角色设定、对话历史、用户问题的 prompt 组装

一、为什么要有 Prompt 模板

直接拼字符串不行吗?

// 这样写很快出问题:
prompt := "你是" + role + "。用户问:" + question

问题是:不同的对话需要插入不同的历史记录,有些 prompt 很长,变量位置多,拼接代码杂乱,还容易忘掉哪个变量没传。

ChatTemplate 做的是把 prompt 结构和变量填充分开:你写一次模板,运行时填入变量,得到结构化的消息列表。


二、ChatTemplate 接口

源码在 eino/components/prompt/interface.go

type ChatTemplate interface {
    Format(ctx context.Context, vs map[string]any, opts ...Option) ([]*schema.Message, error)
}

就一个 Format()

  • 传入:变量 map,比如 {"role": "程序员鼓励师", "question": "代码报错了怎么办"}
  • 返回:[]*schema.Message,结构化的消息列表,可以直接发给 ChatModel

三、三种模板语法

源码在 eino/schema/message.go,支持三种格式:

type FormatType uint8

const (
    FString    FormatType = 0  // Python 风格:{variable}
    GoTemplate FormatType = 1  // Go 模板:{{.variable}}
    Jinja2     FormatType = 2  // Jinja2 风格:{{variable}}
)

实现上三种格式分别用不同的库:

格式语法底层实现
FString{name}pyfmt 库
GoTemplate{{.name}}Go 标准库 text/template
Jinja2{{name}}eino 内置 Jinja2 实现

三种格式,相同的输入,输出完全一样。 选哪种看团队习惯。国内项目用 FString 最多,和 Python 一致;复杂模板(循环、条件判断)选 Jinja2;Go 纯血项目选 GoTemplate。


四、基础用法:构建模板

FromMessages() 创建模板:

// 源码:eino/components/prompt/chat_template.go
func FromMessages(formatType schema.FormatType, templates ...schema.MessagesTemplate) *DefaultChatTemplate

举例:

template := prompt.FromMessages(schema.FString,
    schema.SystemMessage("你是一个{role},用{style}的语气回答。"),
    schema.UserMessage("{question}"),
)

messages, _ := template.Format(ctx, map[string]any{
    "role":     "技术顾问",
    "style":    "专业简洁",
    "question": "Go 的 goroutine 和线程有什么区别?",
})

// messages[0]: system "你是一个技术顾问,用专业简洁的语气回答。"
// messages[1]: user   "Go 的 goroutine 和线程有什么区别?"

注意:模板里有的变量必须在 vs 里传,少一个就报运行时错误。 这是 eino 官方文档里特别说明的"陷阱",没有编译期检查。


五、MessagesPlaceholder:插入整段对话历史

单条消息里的 {var} 替换只适合文字内容。但对话历史是一个消息列表,不是一个字符串。MessagesPlaceholder 就是干这个的:

// 源码:eino/schema/message.go
func MessagesPlaceholder(key string, optional bool) MessagesTemplate

用法:

template := prompt.FromMessages(schema.FString,
    schema.SystemMessage("你是一个{role}。"),
    schema.MessagesPlaceholder("chat_history", true),  // optional=true:没传就跳过
    schema.UserMessage("{question}"),
)

messages, _ := template.Format(ctx, map[string]any{
    "role": "AI 助手",
    "chat_history": []*schema.Message{
        schema.UserMessage("你好"),
        schema.AssistantMessage("你好!有什么可以帮你的?", nil),
    },
    "question": "帮我写一个冒泡排序",
})

// 输出顺序:
// system:    你是一个AI助手。
// user:      你好
// assistant: 你好!有什么可以帮你的?
// user:      帮我写一个冒泡排序

optional 参数:

  • truechat_history 没传,直接跳过,不报错
  • falsechat_history 没传,返回 error

多轮对话用 optional=true,第一轮没有历史时不会报错。


六、在 Chain 和 Graph 里用

Chain 用法(最简洁)

// 源码参考:eino-examples/compose/chain/main.go
chain := compose.NewChain[map[string]any, *schema.Message]()
chain.
    AppendChatTemplate(prompt.FromMessages(
        schema.FString,
        schema.SystemMessage("You are a {role}."),
        schema.UserMessage("{input}"),
    )).
    AppendChatModel(chatModel)

output, _ := chain.Invoke(ctx, map[string]any{
    "role":  "cat",
    "input": "你的叫声是怎样的?",
})
// output.Content: "喵喵喵~"

Graph 用法(更灵活)

// 源码参考:eino-examples/compose/graph/simple/graph.go
g := compose.NewGraph[map[string]any, *schema.Message]()

pt := prompt.FromMessages(
    schema.FString,
    schema.UserMessage("what's the weather in {location}?"),
)

g.AddChatTemplateNode("prompt", pt)
g.AddChatModelNode("model", chatModel)
g.AddEdge(compose.START, "prompt")
g.AddEdge("prompt", "model")
g.AddEdge("model", compose.END)

r, _ := g.Compile(ctx)
ret, _ := r.Invoke(ctx, map[string]any{"location": "beijing"})

七、完整示例:带历史的多轮对话

来自 eino-examples/quickstart/chat/template.go

func createTemplate() prompt.ChatTemplate {
    return prompt.FromMessages(schema.FString,
        // 系统提示:定义 AI 的角色
        schema.SystemMessage("你是一个{role}。你需要用{style}的语气回答问题。"),

        // 对话历史:可选,首轮没有时自动跳过
        schema.MessagesPlaceholder("chat_history", true),

        // 用户当前问题
        schema.UserMessage("问题: {question}"),
    )
}

// 使用:
messages, _ := template.Format(ctx, map[string]any{
    "role":     "程序员鼓励师",
    "style":    "积极、温暖且专业",
    "question": "我的代码一直报错,感觉好沮丧,该怎么办?",
    "chat_history": []*schema.Message{
        schema.UserMessage("你好"),
        schema.AssistantMessage("嘿!我是你的程序员鼓励师!", nil),
    },
})

// 发给 ChatModel 的消息序列:
// [system] 你是一个程序员鼓励师。你需要用积极、温暖且专业的语气回答问题。
// [user]   你好
// [assist] 嘿!我是你的程序员鼓励师!
// [user]   问题: 我的代码一直报错,感觉好沮丧,该怎么办?

八、Jinja2 适合复杂 prompt

当 prompt 里需要条件判断或循环时,选 Jinja2:

// 来自 eino-examples/adk/multiagent 的真实示例
var plannerPrompt = prompt.FromMessages(schema.Jinja2,
    schema.SystemMessage(`You are an expert planner specializing in Excel data processing.`),
    schema.UserMessage(`
User Query: {{ user_query }}
Current Time: {{ current_time }}
File Preview:
{{ file_preview }}
`),
)

msgs, _ := plannerPrompt.Format(ctx, map[string]any{
    "user_query":   "计算每个产品的平均销售额",
    "current_time": time.Now().Format(time.RFC3339),
    "file_preview": "列A: 产品名, 列B: 销售额...",
})

九、支持的消息角色

// eino/schema/message.go
const (
    System    RoleType = "system"    // 系统设定
    User      RoleType = "user"      // 用户输入
    Assistant RoleType = "assistant" // AI 回复
    Tool      RoleType = "tool"      // 工具调用结果
)

对应的便捷构造函数:

schema.SystemMessage("...")
schema.UserMessage("...")
schema.AssistantMessage("...", toolCalls)

小结

变量 map {"role": "X", "question": "Y", "chat_history": [...]}
    ↓ ChatTemplate.Format()
    ├── SystemMessage 替换变量 → system 消息
    ├── MessagesPlaceholder  → 展开为 N 条历史消息
    └── UserMessage 替换变量 → user 消息
[]*schema.Message  →  直接发给 ChatModel

选哪种模板语法?

场景推荐
简单变量替换,团队熟悉 PythonFString {var}
纯 Go 项目,用标准库GoTemplate {{.var}}
需要条件判断 / 循环的复杂 promptJinja2 {{var}}

ChatTemplate 是 AI Agent 的"嘴"——你描述得越清楚,AI 答得越准。值得认真设计。