第3讲:Prompt 工程与管理——像管理代码一样管理提示词

一、为什么需要管理 Prompt

初级做法: ┌──────────────────────────────────────────────┐ │ func askAI(question string) string { │ │ prompt := "请回答以下问题:" + question │ │ // 直接拼字符串调用 LLM │ │ } │ └──────────────────────────────────────────────┘ 痛点: 1. Prompt 散落在代码各处,改一处要搜遍全项目 2. 无法版本管理,不知道哪个 Prompt 效果好 3. 没有模板化,同样的逻辑写 N 遍 4. 无法 A/B 测试,凭感觉改 Prompt 5. 线上出问题,不知道当前用的是哪个版本的 Prompt

目标:像管理代码一样管理 Prompt——模板化、版本化、可测试、可追溯。


二、架构设计

┌──────────────────────────────────────────────┐ │ Prompt 管理系统 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 模板引擎 │ │ 版本管理 │ │ A/B 测试 │ │ │ │ │ │ │ │ │ │ │ │ {{var}} │ │ v1.0.0 │ │ 实验分组 │ │ │ │ 条件渲染 │ │ v1.1.0 │ │ 流量分配 │ │ │ │ 循环 │ │ 回滚 │ │ 效果统计 │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ └──────────────┴──────────────┘ │ │ │ │ │ ┌────────▼────────┐ │ │ │ 存储后端 │ │ │ │ (文件/DB/etcd) │ │ │ └─────────────────┘ │ └────────────────────────────────────────────────┘

三、模板引擎实现

// prompt/engine.go package prompt import ( "bytes" "encoding/json" "fmt" "regexp" "strings" "text/template" ) // Template Prompt 模板 type Template struct { ID string `json:"id"` // 模板唯一 ID Name string `json:"name"` // 模板名称 Version string `json:"version"` // 版本号 Content string `json:"content"` // 模板内容(Go template 语法) Variables []VariableDef `json:"variables"` // 变量定义 Description string `json:"description"` // 描述 Tags map[string]string `json:"tags"` // 标签 CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // VariableDef 变量定义 type VariableDef struct { Name string `json:"name"` Type string `json:"type"` // string/number/boolean/array/object Required bool `json:"required"` Default string `json:"default,omitempty"` Description string `json:"description,omitempty"` } // RenderInput 渲染输入 type RenderInput struct { Variables map[string]interface{} `json:"variables"` UserID string `json:"user_id,omitempty"` SessionID string `json:"session_id,omitempty"` } // RenderOutput 渲染输出 type RenderOutput struct { Content string `json:"content"` UsedVars map[string]interface{} `json:"used_vars"` Warnings []string `json:"warnings,omitempty"` } // TemplateEngine 模板引擎 type TemplateEngine struct { funcMap template.FuncMap } // NewTemplateEngine 创建模板引擎 func NewTemplateEngine() *TemplateEngine { return &TemplateEngine{ funcMap: template.FuncMap{ "upper": strings.ToUpper, "lower": strings.ToLower, "title": strings.Title, "trim": strings.TrimSpace, "json": toJSON, "join": strings.Join, "now": now, "date": formatDate, "contains": strings.Contains, "hasPrefix": strings.HasPrefix, "hasSuffix": strings.HasSuffix, "replace": strings.ReplaceAll, }, } } // Render 渲染模板 func (e *TemplateEngine) Render(tpl *Template, input *RenderInput) (*RenderOutput, error) { output := &RenderOutput{ UsedVars: make(map[string]interface{}), } // 1. 校验必填变量 for _, v := range tpl.Variables { if v.Required { val, ok := input.Variables[v.Name] if !ok || val == nil { return nil, fmt.Errorf("missing required variable: %s", v.Name) } } } // 2. 合并默认值 mergedVars := make(map[string]interface{}) for _, v := range tpl.Variables { if val, ok := input.Variables[v.Name]; ok { mergedVars[v.Name] = val output.UsedVars[v.Name] = val } else if v.Default != "" { mergedVars[v.Name] = v.Default output.UsedVars[v.Name] = v.Default output.Warnings = append(output.Warnings, fmt.Sprintf("variable '%s' using default value", v.Name)) } } // 3. 添加内置变量 mergedVars["_user_id"] = input.UserID mergedVars["_session_id"] = input.SessionID mergedVars["_now"] = now() // 4. 渲染 tmpl, err := template.New(tpl.ID).Funcs(e.funcMap).Parse(tpl.Content) if err != nil { return nil, fmt.Errorf("parse template: %w", err) } var buf bytes.Buffer if err := tmpl.Execute(&buf, mergedVars); err != nil { return nil, fmt.Errorf("execute template: %w", err) } output.Content = buf.String() return output, nil } // Validate 校验模板语法 func (e *TemplateEngine) Validate(content string) error { _, err := template.New("validate").Funcs(e.funcMap).Parse(content) return err } // helper functions func toJSON(v interface{}) string { b, _ := json.Marshal(v) return string(b) } func now() string { return "2026-10-27 12:00:00" } func formatDate(format string, t string) string { // 简化实现 return t } // ─── 模板示例 ─── const customerServiceTemplate = ` 你是一个{{.role}}客服,负责处理用户的{{.domain}}相关问题。 ## 用户信息 - 用户等级:{{.level}} - 历史订单数:{{.order_count}} ## 对话上下文 {{if .history}} {{range $i, $msg := .history}} {{$msg.role}}: {{$msg.content}} {{end}} {{else}} 暂无历史对话。 {{end}} ## 当前用户问题 {{.question}} ## 回答要求 1. 语气{{.tone}},使用{{.language}} 2. 字数控制在{{.max_words}}字以内 3. {{if .need_order_info}}需要查询订单信息后再回答{{else}}直接回答即可{{end}} 4. 如果无法解决,引导用户联系人工客服 ## 请给出回答: `

四、版本管理

// prompt/version.go package prompt import ( "fmt" "sort" "sync" "time" ) // VersionRecord 版本记录 type VersionRecord struct { Version string `json:"version"` Content string `json:"content"` Author string `json:"author"` ChangeLog string `json:"change_log"` CreatedAt time.Time `json:"created_at"` } // VersionManager 版本管理器 type VersionManager struct { templates map[string][]*VersionRecord // templateID -> versions active map[string]string // templateID -> active version mu sync.RWMutex } // NewVersionManager 创建版本管理器 func NewVersionManager() *VersionManager { return &VersionManager{ templates: make(map[string][]*VersionRecord), active: make(map[string]string), } } // SaveVersion 保存新版本 func (vm *VersionManager) SaveVersion(templateID, content, author, changeLog string) (*VersionRecord, error) { vm.mu.Lock() defer vm.mu.Unlock() // 生成版本号 records := vm.templates[templateID] newVersion := fmt.Sprintf("v%d.%d.%d", len(records)/100+1, (len(records)%100)/10+1, len(records)%10+1) record := &VersionRecord{ Version: newVersion, Content: content, Author: author, ChangeLog: changeLog, CreatedAt: time.Now(), } vm.templates[templateID] = append(vm.templates[templateID], record) vm.active[templateID] = newVersion return record, nil } // GetActive 获取当前活跃版本 func (vm *VersionManager) GetActive(templateID string) (*VersionRecord, error) { vm.mu.RLock() defer vm.mu.RUnlock() version, ok := vm.active[templateID] if !ok { return nil, fmt.Errorf("no active version for template: %s", templateID) } return vm.GetVersion(templateID, version) } // GetVersion 获取指定版本 func (vm *VersionManager) GetVersion(templateID, version string) (*VersionRecord, error) { records, ok := vm.templates[templateID] if !ok { return nil, fmt.Errorf("template not found: %s", templateID) } for _, r := range records { if r.Version == version { return r, nil } } return nil, fmt.Errorf("version %s not found for template: %s", version, templateID) } // Rollback 回滚到指定版本 func (vm *VersionManager) Rollback(templateID, version string) error { vm.mu.Lock() defer vm.mu.Unlock() // 验证版本存在 _, err := vm.GetVersion(templateID, version) if err != nil { return err } vm.active[templateID] = version return nil } // ListVersions 列出所有版本 func (vm *VersionManager) ListVersions(templateID string) []*VersionRecord { vm.mu.RLock() defer vm.mu.RUnlock() records := vm.templates[templateID] sorted := make([]*VersionRecord, len(records)) copy(sorted, records) sort.Slice(sorted, func(i, j int) bool { return sorted[i].CreatedAt.After(sorted[j].CreatedAt) }) return sorted } // Diff 比较两个版本差异 func (vm *VersionManager) Diff(templateID, versionA, versionB string) (string, error) { recA, err := vm.GetVersion(templateID, versionA) if err != nil { return "", err } recB, err := vm.GetVersion(templateID, versionB) if err != nil { return "", err } // 简单逐行对比 linesA := splitLines(recA.Content) linesB := splitLines(recB.Content) var diff strings.Builder diff.WriteString(fmt.Sprintf("--- %s (%s)\n", templateID, versionA)) diff.WriteString(fmt.Sprintf("+++ %s (%s)\n", templateID, versionB)) maxLen := len(linesA) if len(linesB) > maxLen { maxLen = len(linesB) } for i := 0; i < maxLen; i++ { if i >= len(linesA) { diff.WriteString(fmt.Sprintf("+ %s\n", linesB[i])) } else if i >= len(linesB) { diff.WriteString(fmt.Sprintf("- %s\n", linesA[i])) } else if linesA[i] != linesB[i] { diff.WriteString(fmt.Sprintf("- %s\n", linesA[i])) diff.WriteString(fmt.Sprintf("+ %s\n", linesB[i])) } } return diff.String(), nil } func splitLines(s string) []string { return strings.Split(s, "\n") }

五、A/B 测试系统

// prompt/abtest.go package prompt import ( "crypto/md5" "encoding/hex" "fmt" "hash/fnv" "sync" "time" ) // ABTest A/B 测试配置 type ABTest struct { ID string `json:"id"` Name string `json:"name"` TemplateID string `json:"template_id"` Groups []TestGroup `json:"groups"` TrafficMode string `json:"traffic_mode"` // hash/user_id/random StartAt time.Time `json:"start_at"` EndAt time.Time `json:"end_at"` Status string `json:"status"` // running/paused/ended Metrics map[string]int64 `json:"metrics"` mu sync.RWMutex } // TestGroup 测试分组 type TestGroup struct { Name string `json:"name"` // control/variant_a/variant_b Version string `json:"version"` // 对应的 Prompt 版本 Weight float64 `json:"weight"` // 流量权重 0.0-1.0 IsControl bool `json:"is_control"` // 是否为对照组 } // ABTestManager A/B 测试管理器 type ABTestManager struct { tests map[string]*ABTest versionMgr *VersionManager results map[string]*TestResults mu sync.RWMutex } // TestResults 测试结果 type TestResults struct { TestID string `json:"test_id"` GroupName string `json:"group_name"` TotalCalls int64 `json:"total_calls"` SuccessRate float64 `json:"success_rate"` AvgLatency time.Duration `json:"avg_latency"` AvgTokens int `json:"avg_tokens"` UserRating float64 `json:"user_rating"` // 用户评分 1-5 Metrics map[string]float64 `json:"metrics"` } // NewABTestManager 创建 A/B 测试管理器 func NewABTestManager(versionMgr *VersionManager) *ABTestManager { return &ABTestManager{ tests: make(map[string]*ABTest), versionMgr: versionMgr, results: make(map[string]*TestResults), } } // CreateTest 创建 A/B 测试 func (m *ABTestManager) CreateTest(name, templateID string, groups []TestGroup) (*ABTest, error) { // 校验权重总和为 1 totalWeight := 0.0 for _, g := range groups { totalWeight += g.Weight } if totalWeight < 0.999 || totalWeight > 1.001 { return nil, fmt.Errorf("total weight must be 1.0, got %f", totalWeight) } test := &ABTest{ ID: generateID(), Name: name, TemplateID: templateID, Groups: groups, TrafficMode: "hash", StartAt: time.Now(), Status: "running", Metrics: make(map[string]int64), } m.mu.Lock() m.tests[test.ID] = test m.mu.Unlock() return test, nil } // AssignGroup 为用户分配测试组 func (m *ABTestManager) AssignGroup(testID, userID string) (*TestGroup, error) { m.mu.RLock() test, ok := m.tests[testID] m.mu.RUnlock() if !ok { return nil, fmt.Errorf("test not found: %s", testID) } if test.Status != "running" { return nil, fmt.Errorf("test is not running: %s", test.Status) } if time.Now().After(test.EndAt) && !test.EndAt.IsZero() { return nil, fmt.Errorf("test has ended") } // 根据分流模式选择分组 var groupIndex int switch test.TrafficMode { case "hash": h := fnv.New32a() h.Write([]byte(testID + ":" + userID)) ratio := float64(h.Sum32()) / float64(1<<32) groupIndex = selectGroup(ratio, test.Groups) case "random": groupIndex = selectGroup(float64(time.Now().UnixNano()%1000)/1000, test.Groups) default: groupIndex = 0 } return &test.Groups[groupIndex], nil } // RecordResult 记录测试结果 func (m *ABTestManager) RecordResult(testID, groupName string, success bool, latency time.Duration, tokens int, rating float64) { key := testID + ":" + groupName m.mu.Lock() defer m.mu.Unlock() result, ok := m.results[key] if !ok { result = &TestResults{ TestID: testID, GroupName: groupName, Metrics: make(map[string]float64), } m.results[key] = result } result.TotalCalls++ if success { result.SuccessRate = (result.SuccessRate*float64(result.TotalCalls-1) + 1) / float64(result.TotalCalls) } else { result.SuccessRate = (result.SuccessRate*float64(result.TotalCalls-1)) / float64(result.TotalCalls) } result.AvgLatency = (result.AvgLatency*time.Duration(result.TotalCalls-1) + latency) / time.Duration(result.TotalCalls) result.AvgTokens = (result.AvgTokens*int(result.TotalCalls-1) + tokens) / int(result.TotalCalls) result.UserRating = (result.UserRating*float64(result.TotalCalls-1) + rating) / float64(result.TotalCalls) } // GetReport 获取测试报告 func (m *ABTestManager) GetReport(testID string) map[string]*TestResults { m.mu.RLock() defer m.mu.RUnlock() report := make(map[string]*TestResults) for key, result := range m.results { if strings.HasPrefix(key, testID+":") { groupName := strings.TrimPrefix(key, testID+":") report[groupName] = result } } return report } // selectGroup 根据比例选择分组 func selectGroup(ratio float64, groups []TestGroup) int { cumulative := 0.0 for i, g := range groups { cumulative += g.Weight if ratio <= cumulative { return i } } return len(groups) - 1 } func generateID() string { h := md5.New() h.Write([]byte(time.Now().String())) return hex.EncodeToString(h.Sum(nil))[:16] }

六、完整使用示例

// prompt/example.go package prompt import ( "fmt" "log" ) func Example() { // 1. 创建模板引擎 engine := NewTemplateEngine() // 2. 创建版本管理器 vm := NewVersionManager() // 3. 定义客服模板 csTemplate := &Template{ ID: "customer_service", Name: "客服对话模板", Version: "v1.0.0", Content: customerServiceTemplate, Description: "通用客服对话 Prompt", Variables: []VariableDef{ {Name: "role", Type: "string", Required: true, Description: "客服角色"}, {Name: "domain", Type: "string", Required: true, Description: "业务领域"}, {Name: "level", Type: "string", Required: true, Description: "用户等级"}, {Name: "order_count", Type: "number", Required: true, Description: "历史订单数"}, {Name: "question", Type: "string", Required: true, Description: "用户问题"}, {Name: "tone", Type: "string", Required: false, Default: "友好专业", Description: "语气"}, {Name: "language", Type: "string", Required: false, Default: "中文", Description: "语言"}, {Name: "max_words", Type: "number", Required: false, Default: "200", Description: "最大字数"}, {Name: "need_order_info", Type: "boolean", Required: false, Default: "false", Description: "是否需要查订单"}, {Name: "history", Type: "array", Required: false, Description: "历史对话"}, }, } // 4. 保存第一个版本 v1, _ := vm.SaveVersion( csTemplate.ID, csTemplate.Content, "alice", "初始版本", ) fmt.Printf("Saved version: %s\n", v1.Version) // 5. 迭代第二个版本(增加 emoji 要求) v2Content := csTemplate.Content + "\n6. 适当使用 emoji 表达情感\n" v2, _ := vm.SaveVersion( csTemplate.ID, v2Content, "bob", "增加 emoji 要求", ) fmt.Printf("Saved version: %s\n", v2.Version) // 6. 渲染模板 input := &RenderInput{ Variables: map[string]interface{}{ "role": "售后", "domain": "电子产品退货", "level": "金牌会员", "order_count": 23, "question": "我买的手机屏幕碎了,能退货吗?", "tone": "温和耐心", "max_words": "150", "history": []map[string]string{ {"role": "user", "content": "你好,我想咨询退货"}, {"role": "assistant", "content": "您好,请问有什么可以帮助您的?"}, }, }, UserID: "U12345", SessionID: "S67890", } output, err := engine.Render(csTemplate, input) if err != nil { log.Fatal(err) } fmt.Println("\n=== 渲染结果 ===") fmt.Println(output.Content) fmt.Println("\n=== 使用变量 ===") for k, v := range output.UsedVars { fmt.Printf(" %s: %v\n", k, v) } if len(output.Warnings) > 0 { fmt.Println("\n=== 警告 ===") for _, w := range output.Warnings { fmt.Printf(" ⚠️ %s\n", w) } } // 7. A/B 测试 abMgr := NewABTestManager(vm) test, _ := abMgr.CreateTest("客服模板 Emoji 测试", csTemplate.ID, []TestGroup{ {Name: "control", Version: "v1.0.0", Weight: 0.5, IsControl: true}, {Name: "variant_a", Version: "v2.0.0", Weight: 0.5, IsControl: false}, }) // 模拟用户请求 users := []string{"U001", "U002", "U003", "U004"} for _, uid := range users { group, _ := abMgr.AssignGroup(test.ID, uid) fmt.Printf("\nUser %s assigned to group: %s (version: %s)", uid, group.Name, group.Version) // 模拟调用结果 abMgr.RecordResult(test.ID, group.Name, true, 350*time.Millisecond, 180, 4.5) } // 查看报告 report := abMgr.GetReport(test.ID) fmt.Println("\n\n=== A/B 测试报告 ===") for group, result := range report { fmt.Printf("\n组: %s", group) fmt.Printf("\n 调用次数: %d", result.TotalCalls) fmt.Printf("\n 成功率: %.1f%%", result.SuccessRate*100) fmt.Printf("\n 平均延迟: %v", result.AvgLatency) fmt.Printf("\n 平均 Token: %d", result.AvgTokens) fmt.Printf("\n 用户评分: %.1f", result.UserRating) } }

七、生产坑位

#

坑位

现象

解决方案

1

模板注入​

用户输入包含{{导致模板解析失败

对用户输入做转义处理

2

版本混乱​

多人同时修改导致版本冲突

加锁 + 变更通知

3

A/B 测试污染​

同用户在不同会话分配到不同组

使用 user_id hash 保证一致性

4

模板膨胀​

单个模板超过 10K Token

拆分子模板 + 组合模式

5

变量遗漏​

必填变量没传导致渲染报错

编译期校验 + 运行时兜底

6

性能问题​

频繁渲染大型模板

预编译 + 缓存 AST


八、本讲小结

┌─────────────────────────────────────────────────────────────┐ │ 第3讲 核心收获 │ │ │ │ ✅ 实现了 Go template 驱动的 Prompt 模板引擎 │ │ ✅ 建立了版本管理体系,支持创建/回滚/对比 │ │ ✅ 搭建了 A/B 测试系统,支持 hash/random 分流 │ │ ✅ 掌握了变量校验、默认值、内置函数等工程化手段 │ │ ✅ 了解了 6 个常见坑位及其解决方案 │ │ │ │ 核心理念:Prompt 也是代码,需要版本管理、测试和度量 │ └─────────────────────────────────────────────────────────────┘

九、配套工具推荐

🧰 开发之余的小工具推荐

在日常调试 Prompt 时,经常需要对比不同版本的差异、格式化 JSON 变量、生成测试数据:

  • 文本差异对比:zz365.top 的文本对比工具可以快速看出两个 Prompt 版本的每一行变化
  • JSON 格式化:将复杂的变量结构展开,确认模板渲染时的数据源是否正确
  • 正则表达式测试:验证模板中的变量提取规则
  • Base64 编解码:处理某些场景下的编码转换

同类可备站点:

  • tool.lu(综合在线工具箱)
  • 360站长工具(zz360.cc,偏 SEO/建站方向)

提醒:所有涉及业务数据和用户信息的操作,务必先在本地脱敏再使用在线工具。


下一讲预告:《RAG 从零实现——让大模型学会查你的数据库》

你会学到:Embedding 选型、向量数据库集成、文档切分策略、检索排序与重排、RAG 全链路实现。

准备好了告诉我"继续"。