让 Agent 说中文:Prompt 模板怎么管理(第47篇-E33) 系列「企业级 AI Agent 实现拆解」E33 篇Part 9 起步篇第三章。E31 的 Agent 能跑了E32但 system prompt 是硬编码在main.go里的字符串。这篇讲怎么用 Eino 的ChatTemplate管理 prompt从硬编码走向可维护。读完这篇你会知道prompt.FromMessagesschema.FStringEino 的模板 API 怎么用MessagesPlaceholder怎么把历史对话注入模板FString vs GoTemplate vs Jinja2三种插值语法的差别把模板从代码里分离存文件 vs 存数据库在 Chain 里使用模板AppendChatTemplate硬编码的问题E31 里的 Agent 是这样的sr,err:agent.Stream(ctx,[]*schema.Message{{Role:schema.System,Content:你是一个助手帮用户回答问题。},{Role:schema.User,Content:北京今天天气怎么样},})两个问题不能动态填值。如果 system prompt 里要填租户名、用户名、今天日期——只能字符串拼接既丑又容易出 bug。改 prompt 要改代码。Prompt 调优是高频操作每次改都要重新编译部署。Eino 的ChatTemplate解决这两个问题。基本用法import(github.com/cloudwego/eino/components/promptgithub.com/cloudwego/eino/schema)tpl:prompt.FromMessages(schema.FString,schema.SystemMessage(你是一个{role}使用{style}的语气回答问题。),schema.UserMessage(问题{question}),)FromMessages接收格式类型这里是FString和若干MessagesTemplate。每个schema.SystemMessage、schema.UserMessage都实现了MessagesTemplate接口。格式化时传变量messages,err:tpl.Format(ctx,map[string]any{role:程序员助理,style:简洁专业,question:Go 的接口怎么用,})// messages [// {Role: system, Content: 你是一个程序员助理使用简洁专业的语气回答问题。},// {Role: user, Content: 问题Go 的接口怎么用},// ]Format返回[]*schema.Message直接传给chatModel.Generate或agent.Stream。FString 语法FString是 Pythonstr.format()的 Go 实现底层用pyfmt库{variable} ← 直接替换 {name!r} ← repr 格式花括号里只能是变量名不能写表达式。这是 prompt 模板的合理约束——模板里塞逻辑是反模式。如果变量名不存在Format返回 error这是设计意图fail fast别让 Agent 拿着残缺的 prompt 去调 LLM。GoTemplate 语法当你需要条件判断时换用schema.GoTemplatetpl: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 之后、新消息之前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.Messagequestion:退款流程是什么,})// messages [system, user(你好), assistant(你好...), user(退款流程...)]MessagesPlaceholder的第二个参数控制历史消息是否可选truehistory键不存在时跳过第一轮对话没有历史falsehistory键不存在时Format返回 error强制要求传入在 Chain 里用模板模板可以直接作为 Chain 的一个节点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 要重新部署。方案一存文件适合小项目prompts/ ├── assistant.txt ← system prompt 模板 └── customer_service.txtcontent,_:os.ReadFile(prompts/assistant.txt)tpl:prompt.FromMessages(schema.FString,schema.SystemMessage(string(content)),schema.MessagesPlaceholder(history,true),schema.UserMessage({input}),)优点改 prompt 只改文件不改代码文件可以纳入 git 版本管理。方案二存数据库适合多租户 / 多 AgentDeepFlux 的做法prompt 模板存在agent_configs表里带版本号。// 从数据库加载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三要素格式类型FString简单变量替换或GoTemplate需要条件判断消息模板schema.SystemMessage、schema.UserMessage承载带占位符的文本历史占位schema.MessagesPlaceholder把[]*schema.Message插入消息序列从硬编码 → 模板变量 → 存文件/数据库三步完成 prompt 的可维护化。模板负责格式化变量从业务代码填入两者职责分离。下一篇是 E34看你好世界背后发生了什么——从chatModel.Generate()一路追到 HTTP 请求看看调用链怎么走。代码来源eino-examples quickstart/chat