调用大模型 API 时,你是直接拼接字符串,还是用模板管理?如果只是简单调用,字符串拼接够用;但当 Prompt 越来越多、越来越复杂,散落在代码各处的字符串就成了维护噩梦。变量替换、多语言提示、版本化管理,这三个问题不解决,代码迟早变成「意大利面条」。这篇就聊用 Go 实现 Prompt 模板管理的实践。


为什么需要 Prompt 模板?

假设你有一个翻译场景,最简单的写法:

prompt := fmt.Sprintf("请把以下文本翻译成%s:\n%s", targetLang, text)

看起来没问题。但当项目里有几十个类似的 Prompt,问题就来了:

  1. 变量散落targetLangtext 这些变量从哪来?类型安全吗?
  2. 修改困难:想改 Prompt 措辞,得在代码里搜索字符串,改完还得改测试
  3. 多语言难搞:要支持中英文 Prompt,怎么办?if-else 一把梭?
  4. 版本混乱:今天改了 Prompt,明天效果变差,想回滚却发现没保存旧版本

解决这些问题的核心思路是:把 Prompt 当成代码的一部分,用模板引擎管理


变量替换:从字符串拼接到模板引擎

1.1 最简单的模板实现

Go 标准库的 text/template 就够用。先定义模板,再传入变量:

const translatePrompt = `请把以下文本翻译成{{.TargetLang}}:
{{.Text}}
翻译要求:保持原文语气`

type TranslateParams struct {
    TargetLang string
    Text       string
}

// 渲染:params 传入结构体,返回填充后的字符串
prompt, _ := RenderPrompt(translatePrompt, params)

这样做的优点:

  • 类型安全TranslateParams 结构体定义了所有变量,编译期就能发现拼写错误
  • 可读性好:模板和代码分离,修改 Prompt 不用动逻辑代码
  • 复用性强:同一个模板可以多次渲染,参数不同即可

1.2 支持默认值和必填校验

实际场景中,有些变量有默认值,有些必须填写。可以封装一个增强版:

type PromptTemplate struct {
    template  *template.Template
    required  []string          // 必填字段
    defaults  map[string]any    // 默认值
}

// 创建模板:Question 必填,Role 和 Style 有默认值
tmpl, _ := NewPromptTemplate(
    "你是一个{{.Role}},请用{{.Style}}风格回答:\n{{.Question}}",
    []string{"Question"},
    map[string]any{"Role": "AI助手", "Style": "专业"},
)
prompt, _ := tmpl.Render(map[string]any{"Question": "什么是Go语言?"})

Render 方法内部会:检查必填字段 → 合并默认值 → 渲染模板。调用时只需传核心参数,减少重复代码。

1.3 嵌套模板和复用

复杂的 Prompt 往往有重复片段,比如「输出格式说明」「注意事项」。可以用嵌套模板复用:

const formatInstruction = `输出格式要求:使用 JSON 格式,字段包括:{{.Fields}}`

const mainPrompt = `{{template "format" .}}
任务:{{.Task}}
{{template "format" .}}`

// 渲染时先注册 helper 模板,再解析主模板

通过 {{template "name" .}} 引入公共片段,一处修改,处处生效。


多语言提示:一套模板,多种语言

2.1 多语言模板的结构设计

假设你的产品要支持中英文,Prompt 也得跟着变。一种方案是按语言存放模板:

prompts/
├── zh/
│   ├── translate.yaml
│   └── summarize.yaml
└── en/
    ├── translate.yaml
    └── summarize.yaml

每个 YAML 文件定义一个 Prompt:

# prompts/zh/translate.yaml
name: translate
version: "1.0"
template: |
  请把以下文本翻译成{{.TargetLang}}:
  {{.Text}}
required:
  - TargetLang
  - Text
defaults:
  TargetLang: 英语
# prompts/en/translate.yaml
name: translate
version: "1.0"
template: |
  Please translate the following text into {{.TargetLang}}:
  {{.Text}}
required:
  - TargetLang
  - Text
defaults:
  TargetLang: Chinese

2.2 加载和渲染多语言模板

封装一个 Prompt 管理器,按语言加载模板:

type PromptManager struct {
    templates map[string]map[string]*PromptTemplate // lang -> name -> template
    baseDir   string
}

// 加载:读取 YAML → 解析配置 → 创建 PromptTemplate
func (pm *PromptManager) Load(lang, name string) error { ... }

// 渲染:根据语言和模板名找到模板,调用 Render
func (pm *PromptManager) Render(lang, name string, params map[string]any) (string, error) { ... }

使用示例:

pm := NewPromptManager("./prompts")
pm.Load("zh", "translate")
pm.Load("en", "translate")

// 中文 Prompt
promptZh, _ := pm.Render("zh", "translate", map[string]any{
    "TargetLang": "日语",
    "Text":       "你好,世界",
})

// 英文 Prompt
promptEn, _ := pm.Render("en", "translate", map[string]any{
    "TargetLang": "Japanese",
    "Text":       "Hello, World",
})

2.3 运行时语言切换

实际项目中,语言往往从请求上下文获取(如用户偏好、HTTP Header)。可以封装一个中间层:

type PromptService struct {
    manager *PromptManager
}

func (ps *PromptService) Translate(ctx context.Context, targetLang, text string) (string, error) {
    lang := GetLangFromContext(ctx) // 从上下文获取语言偏好,默认 "zh"
    return ps.manager.Render(lang, "translate", map[string]any{
        "TargetLang": targetLang,
        "Text":       text,
    })
}

这样业务代码不用关心语言切换,模板层自动处理。


版本化管理:追踪变更,支持回滚

3.1 为什么需要版本化?

Prompt 是 AI 应用的「代码」,修改 Prompt 就像修改代码一样,需要版本控制:

  1. 效果对比:改了 Prompt 后,想对比新旧效果
  2. 问题排查:某天输出变差了,想查是哪个 Prompt 改出的问题
  3. A/B 测试:同时跑多个版本,看哪个效果好
  4. 合规审计:某些场景需要记录 Prompt 变更历史

3.2 在文件名或配置中嵌入版本

最简单的方式是在文件名中带版本号:

prompts/
├── zh/
│   ├── translate_v1.yaml
│   ├── translate_v2.yaml
│   └── translate_v3.yaml

或者在配置中维护版本历史:

# prompts/zh/translate.yaml
current: "3"
versions:
  "1":
    template: "翻译成{{.TargetLang}}:{{.Text}}"
    created_at: "2024-01-01"
  "2":
    template: "请把以下文本翻译成{{.TargetLang}}:\n{{.Text}}"
    created_at: "2024-02-01"
  "3":
    template: "请把以下文本翻译成{{.TargetLang}}:\n{{.Text}}\n翻译要求:保持原文语气"
    created_at: "2024-03-01"

3.3 版本化 Prompt 管理器

扩展 PromptManager 支持版本:

type VersionedPromptManager struct {
    templates map[string]map[string]map[string]*PromptTemplate // lang -> name -> version -> template
}

// 渲染:version 为空或 "latest" 时使用最新版本
func (vpm *VersionedPromptManager) Render(lang, name, version string, params map[string]any) (string, error) {
    if version == "" || version == "latest" {
        version = vpm.getLatestVersion(lang, name)
    }
    return vpm.templates[lang][name][version].Render(params)
}

使用示例:

// 使用最新版本
prompt, _ := vpm.Render("zh", "translate", "latest", params)

// 使用指定版本(用于 A/B 测试或回滚)
prompt, _ := vpm.Render("zh", "translate", "2", params)

写在最后

Go 在 AI 应用中的作用,往往被低估。大家习惯用 Python 调模型、做原型,但 Go 并不是一无是处。text/template 不是最强大的模板引擎,但足够用,对于 Go 开发者来说,它的价值不在于花哨,而在于得心应手