首页 / 资讯中心 / 文章详情

第3讲:Prompt 实验——如何科学地测试 Prompt 改动

第3讲:Prompt 实验——如何科学地测试 Prompt 改动 ★ FEATURED ARTICLE
一、Prompt 实验的特殊性传统软件实验 Prompt 实验 ──────────────────────────────────────────────────── 修改代码 → 部署 → 验证 修改 Prompt → 调用 LLM → 评估输出 结果确定可复现 结果概率性波动 回归测试容易自动化 正确答案需要人工判断 性能指标客观延迟/QPS 质量指标主观相关性/完整性 版本回滚简单 Prompt 版本管理复杂核心难题同一个 Prompt 跑两次得到两个不同的回答如何区分是 Prompt 改好了还是运气好二、Prompt 实验的变量类型变量维度可调参数示例指令​system prompt 措辞、语气、约束条件你是一个专业的医生 vs 你是一个科普作者结构​段落顺序、格式要求、分隔符先给示例再给指令 vs 先给指令再给示例示例​few-shot 数量、示例选择、示例顺序3 个示例 vs 5 个示例参数​temperature、top_p、max_tokens0.1 vs 0.8格式​输出格式约束JSON vs Markdown vs 纯文本角色​角色设定、人格特征专家 vs 助手 vs 朋友三、整体架构┌──────────────────────────────────────────────────────────────┐ │ Prompt 实验系统 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │模板管理 │ │版本管理 │ │参数配置 │ │实验编排 │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ └──────────────────────────┬───────────────────────────────────┘ │ ┌──────────────────────────▼───────────────────────────────────┐ │ 实验执行引擎 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │分流器 │ │渲染引擎 │ │LLM 调用 │ │结果收集 │ │ │ │(第2讲) │ │(模板) │ │(第2讲) │ │ │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ └──────────────────────────┬───────────────────────────────────┘ │ ┌──────────────────────────▼───────────────────────────────────┐ │ 评估系统 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │自动评估 │ │LLM Judge│ │人工评估 │ │统计分析 │ │ │ │(BLEU/ROUGE)│ │(打分) │ │(众包) │ │(第1讲) │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ └──────────────────────────────────────────────────────────────┘四、Prompt 模板引擎// prompt/template.go package prompt import ( bytes encoding/json fmt strings text/template time ) // ─── 模板定义 ─── // Template Prompt 模板 type Template struct { ID string json:id Name string json:name Version string json:version System string json:system // system prompt 模板 User string json:user // user message 模板 Variables []VariableDef json:variables // 变量定义 Constraints map[string]string json:constraints // 约束条件 CreatedAt time.Time json:created_at } // VariableDef 变量定义 type VariableDef struct { Name string json:name Type string json:type // string, int, list, object Required bool json:required Default string json:default Description string json:description } // RenderInput 渲染输入 type RenderInput struct { TemplateID string json:template_id Variables map[string]interface{} json:variables Params LLMParams json:params } // LLMParams LLM 调用参数 type LLMParams struct { Temperature float64 json:temperature TopP float64 json:top_p MaxTokens int json:max_tokens FrequencyPenalty float64 json:frequency_penalty PresencePenalty float64 json:presence_penalty } // ─── 模板引擎 ─── // TemplateEngine 模板引擎 type TemplateEngine struct { templates map[string]*Template } // NewTemplateEngine 创建模板引擎 func NewTemplateEngine() *TemplateEngine { return TemplateEngine{ templates: make(map[string]*Template), } } // RegisterTemplate 注册模板 func (te *TemplateEngine) RegisterTemplate(t *Template) error { // 验证模板语法 if _, err : template.New(system).Parse(t.System); err ! nil { return fmt.Errorf(system template syntax error: %w, err) } if _, err : template.New(user).Parse(t.User); err ! nil { return fmt.Errorf(user template syntax error: %w, err) } te.templates[t.ID] t return nil } // Render 渲染 Prompt func (te *TemplateEngine) Render(input RenderInput) (*RenderedPrompt, error) { tmpl, ok : te.templates[input.TemplateID] if !ok { return nil, fmt.Errorf(template not found: %s, input.TemplateID) } // 1. 验证必需变量 for _, v : range tmpl.Variables { if v.Required { if _, exists : input.Variables[v.Name]; !exists { return nil, fmt.Errorf(missing required variable: %s, v.Name) } } } // 2. 填充默认值 vars : make(map[string]interface{}) for k, v : range input.Variables { vars[k] v } for _, v : range tmpl.Variables { if _, exists : vars[v.Name]; !exists { vars[v.Name] v.Default } } // 3. 渲染 system prompt systemBuf : new(bytes.Buffer) sysTmpl, _ : template.New(system).Parse(tmpl.System) if err : sysTmpl.Execute(systemBuf, vars); err ! nil { return nil, fmt.Errorf(render system prompt: %w, err) } // 4. 渲染 user prompt userBuf : new(bytes.Buffer) userTmpl, _ : template.New(user).Parse(tmpl.User) if err : userTmpl.Execute(userBuf, vars); err ! nil { return nil, fmt.Errorf(render user prompt: %w, err) } // 5. 合并参数模板默认参数可以被覆盖 params : mergeParams(tmpl.Constraints, input.Params) return RenderedPrompt{ TemplateID: input.TemplateID, Version: tmpl.Version, SystemPrompt: systemBuf.String(), UserPrompt: userBuf.String(), Params: params, RenderedAt: time.Now(), }, nil } // RenderedPrompt 渲染后的 Prompt type RenderedPrompt struct { TemplateID string json:template_id Version string json:version SystemPrompt string json:system_prompt UserPrompt string json:user_prompt Params LLMParams json:params RenderedAt time.Time json:rendered_at } func mergeParams(constraints map[string]string, params LLMParams) LLMParams { if v, ok : constraints[temperature]; ok { fmt.Sscanf(v, %f, params.Temperature) } if v, ok : constraints[max_tokens]; ok { fmt.Sscanf(v, %d, params.MaxTokens) } return params } // ─── 模板示例 ─── // 示例 1客服 Prompt var customerServiceTemplate Template{ ID: cs_v1, Name: 客服助手, Version: 1.0.0, System: 你是一个专业的客服助手。请遵循以下原则 1. 始终保持礼貌和专业 2. 回答要简洁明了不超过 {{.max_length}} 字 3. 如果无法解决问题提供 {{.escalation_option}} 4. 使用{{.language}}回复 当前上下文 - 用户等级{{.user_level}} - 历史对话轮数{{.history_count}}, User: 用户问题{{.question}}, Variables: []VariableDef{ {Name: question, Type: string, Required: true, Description: 用户提问}, {Name: max_length, Type: int, Required: false, Default: 200}, {Name: language, Type: string, Required: false, Default: 中文}, {Name: user_level, Type: string, Required: false, Default: 普通}, {Name: history_count, Type: int, Required: false, Default: 0}, {Name: escalation_option, Type: string, Required: false, Default: 转接人工}, }, } // 示例 2代码审查 Prompt var codeReviewTemplate Template{ ID: cr_v1, Name: 代码审查, Version: 1.0.0, System: 你是一个资深的 {{.language}} 开发者。请审查以下代码 审查重点 1. 代码正确性是否有 bug 2. 性能问题是否有优化空间 3. 安全漏洞SQL 注入、XSS 等 4. 代码风格是否符合 {{.style_guide}} 输出格式 - 严重问题 [严重] 描述 - 一般问题 [建议] 描述 - 优化建议 [优化] 描述, User: {{.language}}\n{{.code}}\n, Variables: []VariableDef{ {Name: code, Type: string, Required: true}, {Name: language, Type: string, Required: true}, {Name: style_guide, Type: string, Required: false, Default: Google Style Guide}, }, }五、Prompt 版本管理// prompt/version.go package prompt import ( crypto/sha256 encoding/hex fmt sort sync time ) // ─── 版本管理 ─── // VersionManager Prompt 版本管理器 type VersionManager struct { versions map[string][]*PromptVersion // templateID - versions mu sync.RWMutex } // PromptVersion Prompt 版本 type PromptVersion struct { Version string json:version // semver: 1.0.0, 1.1.0 Template *Template json:template Diff string json:diff // 与前版本的差异描述 Author string json:author CreatedAt time.Time json:created_at Checksum string json:checksum // SHA256 of template content } // NewVersionManager 创建版本管理器 func NewVersionManager() *VersionManager { return VersionManager{ versions: make(map[string][]*PromptVersion), } } // CreateVersion 创建新版本 func (vm *VersionManager) CreateVersion(tmpl *Template, author string, diff string) (*PromptVersion, error) { vm.mu.Lock() defer vm.mu.Unlock() // 1. 计算 checksum checksum : computeChecksum(tmpl) // 2. 自动生成版本号 version : vm.nextVersion(tmpl.ID, checksum) pv : PromptVersion{ Version: version, Template: tmpl, Diff: diff, Author: author, CreatedAt: time.Now(), Checksum: checksum, } vm.versions[tmpl.ID] append(vm.versions[tmpl.ID], pv) return pv, nil } // GetVersions 获取所有版本 func (vm *VersionManager) GetVersions(templateID string) []*PromptVersion { vm.mu.RLock() defer vm.mu.RUnlock() return vm.versions[templateID] } // Rollback 回滚到指定版本 func (vm *VersionManager) Rollback(templateID string, version string) (*Template, error) { vm.mu.RLock() defer vm.mu.RUnlock() versions, ok : vm.versions[templateID] if !ok { return nil, fmt.Errorf(template not found: %s, templateID) } for _, pv : range versions { if pv.Version version { return pv.Template, nil } } return nil, fmt.Errorf(version not found: %s/%s, templateID, version) } // CompareVersions 比较两个版本 func (vm *VersionManager) CompareVersions(templateID string, v1, v2 string) (*VersionDiff, error) { t1, err : vm.Rollback(templateID, v1) if err ! nil { return nil, err } t2, err : vm.Rollback(templateID, v2) if err ! nil { return nil, err } return VersionDiff{ TemplateID: templateID, VersionFrom: v1, VersionTo: v2, SystemDiff: diffText(t1.System, t2.System), UserDiff: diffText(t1.User, t2.User), }, nil } func (vm *VersionManager) nextVersion(templateID string, checksum string) string { versions : vm.versions[templateID] // 如果是第一个版本 if len(versions) 0 { return 1.0.0 } // 检查是否内容相同防止重复提交 last : versions[len(versions)-1] if last.Checksum checksum { return last.Version } // 递增 minor 版本号 var major, minor, patch int fmt.Sscanf(last.Version, %d.%d.%d, major, minor, patch) return fmt.Sprintf(%d.%d.%d, major, minor1, patch) } func computeChecksum(tmpl *Template) string { content : tmpl.System ||| tmpl.User hash : sha256.Sum256([]byte(content)) return hex.EncodeToString(hash[:8]) } // VersionDiff 版本差异 type VersionDiff struct { TemplateID string json:template_id VersionFrom string json:version_from VersionTo string json:version_to SystemDiff string json:system_diff UserDiff string json:user_diff } // diffText 简单文本差异生产环境建议使用 difflib func diffText(a, b string) string { if a b { return 无变化 } return fmt.Sprintf(--- 旧版本\n 新版本\n-%s\n%s, truncate(a, 100), truncate(b, 100)) } func truncate(s string, n int) string { runes : []rune(s) if len(runes) n { return s } return string(runes[:n]) ... }六、实验执行引擎// prompt/experiment.go package prompt import ( context fmt sync time ) // ─── Prompt 实验 ─── // PromptExperiment Prompt 实验 type PromptExperiment struct { ID string Name string TemplateID string Variants []PromptVariant Evaluator Evaluator Collector *ResultCollector Splitter interface{ Assign(userID string) string } Status string // running, paused, completed mu sync.Mutex } // PromptVariant Prompt 变体 type PromptVariant struct { ID string Name string Template *Template // 覆盖的模板内容 Params LLMParams // 覆盖的参数 } // ─── 评估器 ─── // Evaluator 评估器接口 type Evaluator interface { Evaluate(ctx context.Context, result *LLMResult) (*Evaluation, error) } // Evaluation 评估结果 type Evaluation struct { Score float64 json:score // 0-1 Passed bool json:passed Details map[string]float64 json:details // 各项子分数 Feedback string json:feedback } // LLMResult LLM 调用结果 type LLMResult struct { Prompt *RenderedPrompt json:prompt Response string json:response Latency time.Duration json:latency TokenUsed int json:token_used Error error json:error,omitempty } // ─── 自动评估器 ─── // AutoEvaluator 自动评估器 type AutoEvaluator struct { criteria []Criterion } type Criterion struct { Name string Weight float64 Check func(response string, expected string) float64 } // NewAutoEvaluator 创建自动评估器 func NewAutoEvaluator() *AutoEvaluator { return AutoEvaluator{ criteria: []Criterion{ {Name: 关键词覆盖率, Weight: 0.3, Check: keywordCoverage}, {Name: 长度合规, Weight: 0.2, Check: lengthCompliance}, {Name: 格式正确性, Weight: 0.3, Check: formatCorrectness}, {Name: 禁止词检测, Weight: 0.2, Check: bannedWordCheck}, }, } } func (ae *AutoEvaluator) Evaluate(ctx context.Context, result *LLMResult) (*Evaluation, error) { totalScore : 0.0 details : make(map[string]float64) for _, c : range ae.criteria { score : c.Check(result.Response, ) details[c.Name] score totalScore score * c.Weight } return Evaluation{ Score: totalScore, Passed: totalScore 0.7, Details: details, Feedback: fmt.Sprintf(综合得分: %.2f, totalScore), }, nil } func keywordCoverage(response string, _ string) float64 { keywords : []string{你好, 请问, 帮助} matched : 0 for _, kw : range keywords { if strings.Contains(response, kw) { matched } } return float64(matched) / float64(len(keywords)) } func lengthCompliance(response string, _ string) float64 { runeCount : len([]rune(response)) if runeCount 50 runeCount 500 { return 1.0 } if runeCount 50 { return float64(runeCount) / 50.0 } return 500.0 / float64(runeCount) } func formatCorrectness(response string, _ string) float64 { // 检查是否包含标点符号 hasPunct : strings.ContainsAny(response, 。,.!?) if hasPunct { return 1.0 } return 0.5 } func bannedWordCheck(response string, _ string) float64 { bannedWords : []string{不知道, 无法回答, error} for _, bw : range bannedWords { if strings.Contains(response, bw) { return 0.0 } } return 1.0 } // ─── LLM-as-Judge 评估器 ─── // LLMJudgeEvaluator 使用 LLM 作为评判者 type LLMJudgeEvaluator struct { judgePrompt string llmClient interface{ Chat(ctx context.Context, req *ChatRequest) (*ChatResponse, error) } } func (je *LLMJudgeEvaluator) Evaluate(ctx context.Context, result *LLMResult) (*Evaluation, error) { judgeReq : ChatRequest{ Messages: []Message{ {Role: system, Content: je.judgePrompt}, {Role: user, Content: fmt.Sprintf( 请评估以下 AI 回答的质量 用户问题%s AI 回答%s 请从以下维度打分0-10 1. 相关性回答是否切题 2. 准确性信息是否正确 3. 完整性是否全面覆盖问题 4. 清晰度表达是否清楚 输出 JSON 格式{relevance: N, accuracy: N, completeness: N, clarity: N} , result.Prompt.UserPrompt, result.Response)}, }, } resp, err : je.llmClient.Chat(ctx, judgeReq) if err ! nil { return nil, err } // 解析 JSON 评分 var scores map[string]float64 json.Unmarshal([]byte(resp.Content), scores) total : 0.0 for _, v : range scores { total v / 10.0 } avg : total / float64(len(scores)) return Evaluation{ Score: avg, Passed: avg 0.7, Details: scores, }, nil } // ─── 结果收集器 ─── // ResultCollector 结果收集器 type ResultCollector struct { results map[string][]*ExperimentResult mu sync.Mutex } type ExperimentResult struct { UserID string json:user_id VariantID string json:variant_id Input interface{} json:input Output string json:output Evaluation *Evaluation json:evaluation Latency int64 json:latency_ms TokenUsed int json:token_used Timestamp time.Time json:timestamp } func (rc *ResultCollector) Record(result *ExperimentResult) { rc.mu.Lock() defer rc.mu.Unlock() rc.results[result.VariantID] append(rc.results[result.VariantID], result) } // AggregateResults 汇总结果 func (rc *ResultCollector) AggregateResults() map[string]*AggregatedResult { rc.mu.Lock() defer rc.mu.Unlock() aggregated : make(map[string]*AggregatedResult) for variantID, results : range rc.results { agg : AggregatedResult{VariantID: variantID} var totalScore, totalLatency float64 for _, r : range results { agg.Count totalScore r.Evaluation.Score totalLatency float64(r.Latency) } if agg.Count 0 { agg.AvgScore totalScore / float64(agg.Count) agg.AvgLatency totalLatency / float64(agg.Count) } aggregated[variantID] agg } return aggregated } type AggregatedResult struct { VariantID string json:variant_id Count int json:count AvgScore float64 json:avg_score AvgLatency float64 json:avg_latency_ms }七、完整实验流程示例// cmd/prompt_experiment.go package main import ( context fmt math/rand time ) func main() { // 1. 注册模板 engine : prompt.NewTemplateEngine() engine.RegisterTemplate(customerServiceTemplate) // 2. 创建版本 vm : prompt.NewVersionManager() vm.CreateVersion(customerServiceTemplate, alice, 初始版本) // 3. 创建实验变体 // 变体 A原始 Prompt variantA : prompt.PromptVariant{ ID: A, Name: 控制组 - 简洁风格, Template: prompt.Template{ System: 你是一个专业的客服助手。请保持回答简洁不超过 100 字。, User: 用户问题{{.question}}, }, Params: prompt.LLMParams{Temperature: 0.3}, } // 变体 B详细风格 variantB : prompt.PromptVariant{ ID: B, Name: 实验组 - 详细风格, Template: prompt.Template{ System: 你是一个专业的客服助手。请提供详细的解答包括 1. 问题原因分析 2. 解决方案步骤 3. 预防措施建议 不少于 200 字。, User: 用户问题{{.question}}, }, Params: prompt.LLMParams{Temperature: 0.7}, } // 4. 创建实验 exp : prompt.PromptExperiment{ ID: exp_cs_001, Name: 客服 Prompt 风格实验, TemplateID: cs_v1, Variants: []prompt.PromptVariant{variantA, variantB}, Evaluator: prompt.NewAutoEvaluator(), Collector: prompt.ResultCollector{results: make(map[string][]*prompt.ExperimentResult)}, Status: running, } // 5. 模拟实验运行 testQuestions : []string{ 我的订单怎么还没到, 我想退货怎么办, 你们有优惠券吗, 账号被盗了怎么办, 如何修改收货地址, } fmt.Println( Prompt 风格实验 \n) for i : 0; i 100; i { userID : fmt.Sprintf(user_%04d, i) question : testQuestions[rand.Intn(len(testQuestions))] // 分流 variantID : A if rand.Float64() 0.5 { variantID B } // 渲染 Prompt var tmpl *prompt.Template if variantID A { tmpl variantA.Template } else { tmpl variantB.Template } rendered, _ : engine.Render(prompt.RenderInput{ TemplateID: cs_v1, Variables: map[string]interface{}{question: question}, Params: prompt.LLMParams{Temperature: 0.5}, }) // 模拟 LLM 调用实际应调用真实 API mockResponse : fmt.Sprintf(这是对「%s」的回答变体 %s, question, variantID) time.Sleep(time.Millisecond * time.Duration(rand.Intn(200))) // 评估 eval, _ : exp.Evaluator.Evaluate(context.Background(), prompt.LLMResult{ Prompt: rendered, Response: mockResponse, }) // 记录结果 exp.Collector.Record(prompt.ExperimentResult{ UserID: userID, VariantID: variantID, Input: question, Output: mockResponse, Evaluation: eval, Latency: int64(rand.Intn(500)), Timestamp: time.Now(), }) } // 6. 输出结果 results : exp.Collector.AggregateResults() fmt.Printf(%-10s %-10s %-10s %-15s\n, 变体, 样本数, 平均分, 平均延迟(ms)) for _, r : range results { fmt.Printf(%-10s %-10d %-10.3f %-15.0f\n, r.VariantID, r.Count, r.AvgScore, r.AvgLatency) } }八、本讲坑位#坑位现象解决方案1Prompt 泄露​用户通过特殊输入套出 system prompt严格过滤 输出约束 审计日志2模板注入​用户变量中包含模板语法导致渲染异常转义变量内容 沙箱渲染3评估噪声​LLM Judge 自身也有随机性多次评估取平均 使用固定 seed4过度拟合​Prompt 在测试集上高分但在线上翻车构建多样化测试集 线上 AB 验证5版本混乱​多人同时修改 Prompt 导致冲突Git 式版本管理 锁定机制6成本爆炸​每次实验都调用 LLM 评估费用飙升先用自动评估筛选再用 LLM Judge 精评九、本讲小结┌─────────────────────────────────────────────────────────────┐ │ 第3讲 核心收获 │ │ │ │ ✅ 理解了 Prompt 实验与传统实验的本质区别 │ │ ✅ 掌握了 Prompt 模板引擎的实现Go template 渲染 │ │ ✅ 实现了 Prompt 版本管理semver checksum 回滚 │ │ ✅ 实现了自动评估器和 LLM-as-Judge 评估器 │ │ ✅ 实现了实验结果采集和汇总 │ │ ✅ 完成了完整的 Prompt 实验流程 │ │ ✅ 知道了 6 个常见坑位及其应对方案 │ │ │ │ 核心理念Prompt 优化不是玄学是可测量、可重复的实验 │ └─────────────────────────────────────────────────────────────┘下一讲预告《RAG 实验——检索策略与切片效果的量化评估》从切片粒度到检索算法从 Top-K 到重排序用实验数据告诉你哪种 RAG 配置最适合你的场景。
阅读完成 · 觉得有帮助?
咨询建站