让 Agent 说中文:Prompt 模板怎么管理(第47篇-E33)

系列「企业级 AI Agent 实现拆解」E33 篇,Part 9 起步篇第三章。E31 的 Agent 能跑了,E32,但 system prompt 是硬编码在 main.go 里的字符串。这篇讲怎么用 Eino 的 ChatTemplate 管理 prompt,从硬编码走向可维护。

读完这篇你会知道

  • prompt.FromMessages + schema.FString:Eino 的模板 API 怎么用
  • MessagesPlaceholder:怎么把历史对话注入模板
  • FString vs GoTemplate vs Jinja2:三种插值语法的差别
  • 把模板从代码里分离:存文件 vs 存数据库
  • 在 Chain 里使用模板:AppendChatTemplate

硬编码的问题

E31 里的 Agent 是这样的:

go 复制代码
sr, err := agent.Stream(ctx, []*schema.Message{
    {Role: schema.System, Content: "你是一个助手,帮用户回答问题。"},
    {Role: schema.User, Content: "北京今天天气怎么样?"},
})

两个问题:

  1. 不能动态填值。 如果 system prompt 里要填租户名、用户名、今天日期------只能字符串拼接,既丑又容易出 bug。
  2. 改 prompt 要改代码。 Prompt 调优是高频操作,每次改都要重新编译部署。

Eino 的 ChatTemplate 解决这两个问题。


基本用法

go 复制代码
import (
    "github.com/cloudwego/eino/components/prompt"
    "github.com/cloudwego/eino/schema"
)

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

FromMessages 接收格式类型(这里是 FString)和若干 MessagesTemplate。每个 schema.SystemMessageschema.UserMessage 都实现了 MessagesTemplate 接口。

格式化时传变量:

go 复制代码
messages, err := tpl.Format(ctx, map[string]any{
    "role":     "程序员助理",
    "style":    "简洁专业",
    "question": "Go 的接口怎么用?",
})
// messages = [
//   {Role: system, Content: "你是一个程序员助理,使用简洁专业的语气回答问题。"},
//   {Role: user,   Content: "问题:Go 的接口怎么用?"},
// ]

Format 返回 []*schema.Message,直接传给 chatModel.Generateagent.Stream


FString 语法

FString 是 Python str.format() 的 Go 实现(底层用 pyfmt 库):

python 复制代码
{variable}          ← 直接替换
{name!r}            ← repr 格式

花括号里只能是变量名,不能写表达式。这是 prompt 模板的合理约束------模板里塞逻辑是反模式。

如果变量名不存在,Format 返回 error,这是设计意图:fail fast,别让 Agent 拿着残缺的 prompt 去调 LLM。


GoTemplate 语法

当你需要条件判断时,换用 schema.GoTemplate

go 复制代码
tpl := prompt.FromMessages(schema.GoTemplate,
    schema.SystemMessage(`你是{{.role}}。
{{- if .company}}你来自公司:{{.company}}。{{end}}
今天是{{.date}}。`),
)

messages, _ := tpl.Format(ctx, map[string]any{
    "role":    "助手",
    "company": "DeepFlux",
    "date":    "2026-07-07",
})

GoTemplate 用 Go 标准库的 text/template,支持 {{if}}{{range}}{{with}}。变量访问用 .variable 而不是 {variable}

FString 的适用场景: 简单变量替换,大多数 prompt 够用。 GoTemplate 的适用场景: 需要条件分支(比如有无用户画像时生成不同 prompt)。


MessagesPlaceholder:注入历史对话

多轮对话时,历史消息需要插入模板中间(在 system 之后、新消息之前):

go 复制代码
tpl := prompt.FromMessages(schema.FString,
    schema.SystemMessage("你是{role}。"),
    schema.MessagesPlaceholder("history", true),  // 第二个参数:true = 历史为空时不报错
    schema.UserMessage("{question}"),
)

history := []*schema.Message{
    {Role: schema.User,      Content: "你好"},
    {Role: schema.Assistant, Content: "你好!有什么可以帮你?"},
}

messages, _ := tpl.Format(ctx, map[string]any{
    "role":     "客服助手",
    "history":  history,        // 类型必须是 []*schema.Message
    "question": "退款流程是什么?",
})
// messages = [system, user(你好), assistant(你好!...), user(退款流程...)]

MessagesPlaceholder 的第二个参数控制历史消息是否可选:

  • truehistory 键不存在时,跳过(第一轮对话没有历史)
  • falsehistory 键不存在时,Format 返回 error(强制要求传入)

在 Chain 里用模板

模板可以直接作为 Chain 的一个节点:

go 复制代码
chain := compose.NewChain[map[string]any, *schema.Message]()
chain.
    AppendChatTemplate(prompt.FromMessages(schema.FString,
        schema.SystemMessage("你是{role}。"),
        schema.UserMessage("{input}"),
    )).
    AppendChatModel(chatModel)

runner, _ := chain.Compile(ctx)

result, _ := runner.Invoke(ctx, map[string]any{
    "role":  "翻译官",
    "input": "把'hello world'翻译成中文",
})

Chain 的输入类型是 map[string]anyAppendChatTemplate 节点把它格式化成 []*schema.Message,传给下一个 AppendChatModel 节点。

类型流:map[string]any[ChatTemplate][]*schema.Message[ChatModel]*schema.Message


把模板从代码里分离

硬编码模板的最大问题是:改 prompt 要重新部署

方案一:存文件(适合小项目)

perl 复制代码
prompts/
├── assistant.txt      ← system prompt 模板
└── customer_service.txt
go 复制代码
content, _ := os.ReadFile("prompts/assistant.txt")
tpl := prompt.FromMessages(schema.FString,
    schema.SystemMessage(string(content)),
    schema.MessagesPlaceholder("history", true),
    schema.UserMessage("{input}"),
)

优点:改 prompt 只改文件,不改代码;文件可以纳入 git 版本管理。

方案二:存数据库(适合多租户 / 多 Agent)

DeepFlux 的做法:prompt 模板存在 agent_configs 表里,带版本号。

go 复制代码
// 从数据库加载
cfg, _ := agentConfigRepo.Get(ctx, tenantID, configName)
tpl := prompt.FromMessages(schema.FString,
    schema.SystemMessage(cfg.SystemPrompt),     // 数据库里的模板字符串
    schema.MessagesPlaceholder("history", true),
    schema.UserMessage("{input}"),
)

这样不同租户、不同 Agent 配置用不同模板,运营人员可以在管理后台直接改 prompt,不需要工程师介入。


模板的边界

两件不要做的事:

不要在模板里塞太多逻辑。 {if user.is_premium}... {end} 这类逻辑放到模板里是错的------它属于业务层,应该在 Go 代码里处理后再填入模板。模板只负责格式化,不负责决策。

不要在模板里硬编码工具描述。 工具的描述由 utils.InferTool 从结构体 tag 自动生成,不应该手动写进 system prompt,否则工具接口一改就要同步改 prompt,是维护噩梦。


小结

prompt.FromMessages 三要素:

  1. 格式类型FString(简单变量替换)或 GoTemplate(需要条件判断)
  2. 消息模板schema.SystemMessageschema.UserMessage 承载带占位符的文本
  3. 历史占位schema.MessagesPlaceholder[]*schema.Message 插入消息序列

从硬编码 → 模板变量 → 存文件/数据库,三步完成 prompt 的可维护化。模板负责格式化,变量从业务代码填入,两者职责分离。

下一篇是 E34,看"你好世界"背后发生了什么------从 chatModel.Generate() 一路追到 HTTP 请求,看看调用链怎么走。


代码来源:eino-examples quickstart/chat

相关推荐
FantasticPerson1 小时前
03 · Claude Code 如何工作
ai编程
西贝爱学习1 小时前
【Reasonix桌面端】介绍与安装
windows·ai编程
不好听6131 小时前
让 Agent 开口说话:用 Chalk 实现可视化终端
agent
不好听6131 小时前
AI Agent 的四肢:Node.js 内置模块实战指南
agent
学渣超1 小时前
微服务日志智能诊断系统(七) 日志 RAG 引擎——蒸馏、索引与精准检索
java·后端·agent
掉鱼的猫1 小时前
代码审查 Agent Harness 实战:AI 自动 Code Review
java·llm·agent
学渣超1 小时前
微服务日志智能诊断系统(六) Fast Path vs Slow Path——双路径日志诊断策略
java·后端·agent
apprentice1 小时前
跳出余弦相似度陷阱:RAG检索本质是筛选
agent·ai编程
GrowthGPT1 小时前
平台自报的 ROAS 在骗你
agent