一、Prompt 实验的特殊性
传统软件实验 Prompt 实验
────────────────────────────────────────────────────
修改代码 → 部署 → 验证 修改 Prompt → 调用 LLM → 评估输出
结果确定可复现 结果概率性波动
回归测试容易自动化 "正确"答案需要人工判断
性能指标客观(延迟/QPS) 质量指标主观(相关性/完整性)
版本回滚简单 Prompt 版本管理复杂
核心难题:同一个 Prompt 跑两次得到两个不同的回答,如何区分是 Prompt 改好了还是运气好?
二、Prompt 实验的变量类型
| 变量维度 | 可调参数 | 示例 |
|---|---|---|
| 指令 | system prompt 措辞、语气、约束条件 | "你是一个专业的医生" vs "你是一个科普作者" |
| 结构 | 段落顺序、格式要求、分隔符 | 先给示例再给指令 vs 先给指令再给示例 |
| 示例 | few-shot 数量、示例选择、示例顺序 | 3 个示例 vs 5 个示例 |
| 参数 | temperature、top_p、max_tokens | 0.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", ¶ms.Temperature)
}
if v, ok := constraints["max_tokens"]; ok {
fmt.Sscanf(v, "%d", ¶ms.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, minor+1, 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)
}
}
八、本讲坑位
| # | 坑位 | 现象 | 解决方案 |
|---|---|---|---|
| 1 | Prompt 泄露 | 用户通过特殊输入套出 system prompt | 严格过滤 + 输出约束 + 审计日志 |
| 2 | 模板注入 | 用户变量中包含模板语法导致渲染异常 | 转义变量内容 + 沙箱渲染 |
| 3 | 评估噪声 | LLM Judge 自身也有随机性 | 多次评估取平均 + 使用固定 seed |
| 4 | 过度拟合 | 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 配置最适合你的场景。