跳到主内容

{title} ​

结论先行:LLM 应用不能再靠"改个 Prompt 直接发" ​

给电商客服 Agent 做发布,我的核心结论是:必须把 Prompt、模型版本、MCP 工具 Schema、RAG 配置当成同一个"发布物",用 Git 统一版本化,再走"离线黄金评测 → 影子流量 → 小比例金丝雀 → 自动回滚"的流水线。我们靠这套机制把一次差点全量爆发的"误判退款"事故拦在了 1% 流量里,故障恢复从原来的四十分钟手工改配置,压到了三十秒内自动切回。

传统后端发布看的是错误率和延迟,而 LLM 应用"不报错也可能是错的"——它会一本正经地给出错误的退款承诺、调用一个参数已经变了的工具、或者检索到过期的促销政策。这篇随笔记录我们团队在真实电商客服场景里搭这套发布工程的取舍与踩坑。

为什么 LLM 发布比传统服务危险得多 ​

客服 Agent 看起来就是个对话框,背后却耦合了四类随时会变、且彼此强依赖的对象。

LLM 应用发布物的四层版本化对象
1
交互层
System Prompt 模板Few-shot 示例安全与拒答话术
2
模型层
模型名称与版本号temperature / top_p超时与重试策略
3
工具层
MCP 工具 Schema参数校验规则工具权限白名单
4
知识层
RAG 索引版本检索 top_k 与阈值重排模型与混合权重

我见过最典型的一次事故:运营只改了一句退款话术的 Prompt,觉得"几个字而已,不用走流程",结果新话术里"为您申请退款"的措辞,让模型在本该走"退货退款"流程时直接调用了"仅退款"工具,一天内多退了十几万。问题不在那句话本身,而在于Prompt 的措辞改变了工具调用倾向,而工具 Schema 的参数校验又没拦住。这四类对象从来不是独立的:

  • Prompt 变更会改变模型选择哪个工具、填什么参数;
  • 模型版本升级可能让同一套 Prompt 的输出格式漂移,JSON 突然带了多余字段;
  • MCP 工具 Schema 一改(比如 order_id 改成必填),老 Prompt 里的示例就会集体失效;
  • RAG 索引或检索阈值一动,模型引用的政策条文可能整批换掉。

任何一处"悄悄改",都可能在没有任何 500 报错的情况下污染线上回答。这正是 LLM 发布工程和传统灰度最大的区别:我们要灰度的不是二进制,而是一组非确定性的、互相耦合的"认知配置"。

方案对比:三种发布姿态的取舍 ​

搭流水线之前,我们认真比较过三条路。

第一种是"直接全量 + 人工盯群"。 成本最低,大多数早期 Agent 团队都这么干。好处是快,坏处是坏答案直接面对所有消费者,且客服这种场景涉及真金白银,一次失误就是资损和投诉。我们在月单量上去之后彻底放弃了它。

第二种是"蓝绿两套 + 人工对比"。 比全量稳,但 LLM 的输出没法简单 diff,人工看几十条样本既慢又容易漏,蓝绿切换本身也是一次性全量,拦不住渐进式的语义退化。

第三种就是我们最终选择的"版本化 + 影子流量 + 金丝雀 + 自动回滚"。 前期投入最大,要维护评测集、要搭分流和监控,但它把"判断这次变更好不好"从拍脑袋变成了有数据闸门的工程动作。对一个直接影响退款、赔付、用户情绪的客服 Agent,这笔投入是必须的,而不是可选的优化。

第一步:把四类变更全部纳入 Git 版本化 ​

我们用一个独立的 agent-release 仓库管理所有非代码配置,目录结构刻意让人一眼看清耦合关系:

text
agent-release/
├── prompts/
│   └── customer_service/
│       ├── system_prompt.txt          # 主话术,改动需附变更说明
│       └── fewshot_examples.jsonl     #  few-shot 示例
├── models/
│   └── model_config.yaml              # 模型版本与采样参数
├── tools/
│   └── mcp_schemas/                   # MCP 工具 Schema
│       ├── refund_tool.json
│       └── order_query_tool.json
└── rag/
    └── retrieval_config.yaml          # 索引版本、top_k、阈值、重排权重

模型配置长这样,版本号必须是不可变的精确版本,而不是 latest:

yaml
# models/model_config.yaml
model:
  # 永远锁定到具体版本,禁止用 latest / 浮动别名
  name: "your-chat-model"
  version: "2025-11-01"
  temperature: 0.2        # 客服场景偏低,减少自由发挥
  top_p: 0.9
  timeout_ms: 8000
  max_retries: 1
release:
  # 本次发布物的整体版本,四类配置共用,便于原子回滚
  bundle_version: "cs-agent-2025.11.18-r3"

每个 PR 强制做两件事:一是 Prompt diff 必须由业务负责人和工程各 review 一人,CODEOWNERS 卡住;二是 CI 自动跑 MCP Schema 的兼容性检查——新增必填字段、删除字段、改枚举值都直接判为破坏性变更,要么同步改 Prompt 示例,要么版本号升大版本。这个坑我排查了整整一下午:一次工具把 refund_type 的枚举从 ONLY_REFUND 悄悄改成了 REFUND_ONLY,Prompt 里全是旧值,模型照旧输出,工具侧静默走了默认值,表面看调用"成功",实际退错了款。

第二步:黄金评测集是流水线的"地基" ​

我们沉淀了约 300 条黄金评测集,全部来自真实客服对话的脱敏标注,按业务风险加权:退款/仅退款、价保、运费险、物流异常、发票、促销解释。每条样本不只标"标准答案",更重要的是标三类结构化信号:

json
{
  "id": "refund-017",
  "scene": "仅退款_已签收_非质量问题",
  "user": "东西收到了但不想要了,能直接退钱不退货吗?",
  "expect": {
    "intent": "consult_refund_policy",
    "must_call_tool": "refund_policy_query",
    "must_not_call_tool": ["refund_execute"],
    "answer_must_contain": ["退货"],
    "answer_must_not_contain": ["直接为您退款"]
  },
  "risk_weight": 5
}

评测不追求"语义相似度"这种虚指标,而是看可裁决的硬指标:意图准确率、工具调用成功率、参数合法率、退款合规率(高权重)、关键话术命中率。CI 设的闸门是:整体不低于基线,且任何高风险样本不允许新增失败。这里的取舍是:宁可让一个"更通顺但风险未知"的版本卡在评测阶段,也不让它碰线上。

第三步:影子流量先跑,金丝雀再放量 ​

离线集再好也覆盖不了真实分布,所以候选版本先接影子流量:线上请求复制一份发给候选版本,返回值只记录、不回给用户、不真正执行退款这类有副作用的工具(工具调用做 dry-run 沙箱化)。我们重点看两个分歧信号——答案分歧率和工具调用分歧率,跑满二十四小时覆盖大促和夜间的分布差异。

影子没问题,才进入真正的金丝雀。

客服 Agent 金丝雀发布流水线
  1. 1
    提交与评审
    四类变更走 Git PR,Prompt diff 与 Schema 兼容性检查
    强制 CODEOWNERS 与离线黄金集跑分
  2. 2
    离线黄金评测
    300 条标注对话回归,关键指标不得低于基线
    意图准确率、工具调用成功率、退款合规率
  3. 3
    影子流量
    复制线上请求到候选版本,只记录不生效
    对比答案分歧率与工具调用差异,持续 24 小时
  4. 4
    1% 金丝雀
    按店铺 ID 稳定分流,实时看板监控业务指标
    人工抽检 + 自动告警,观察 2 小时
  5. 5
    渐进放量
    5% → 20% → 50% → 100%,每档设闸门
    任一闸门触发阈值即暂停
  6. 6
    自动回滚
    指标越线立即切回上一个不可变版本
    配置与索引版本一并原子回退

分流我们坚持用店铺 ID 做稳定哈希,而不是随机按请求分流。原因很现实:同一个买家的连续追问必须命中同一个版本,否则上一轮候选版说"可以退",下一轮基线版说"不能退",用户立刻感知到精神分裂。放量按 1% → 5% → 20% → 50% → 100% 推进,每一档都有独立闸门。

金丝雀阶段的监控必须是业务指标 + 系统指标双层:系统层看延迟、报错、工具超时;业务层看退款执行率、转人工率、用户负向情绪率、政策引用命中率。LLM 场景里系统指标全绿但业务指标恶化才是常态,只盯 P99 一定会漏。

第四步:自动回滚要"原子",否则等于没回 ​

回滚最容易被想简单。我们的教训是:回滚的单位必须是整个 bundle,而不是只回滚 Prompt。因为新版可能同时动了 Prompt、Schema 和 RAG 索引,只退 Prompt,新 Schema 和旧 Prompt 反而更不兼容。

yaml
# 金丝雀自动回滚规则(示意)
rollback:
  trigger:
    - metric: refund_compliance_rate   # 退款合规率
      window: 5m
      threshold_below: 0.995           # 高风险,阈值极严
    - metric: tool_call_error_rate
      window: 5m
      threshold_above: 0.02
    - metric: negative_sentiment_rate
      window: 10m
      threshold_above_baseline: 0.03
  action:
    # 一次性原子切回上一个 bundle 版本(Prompt+模型+Schema+RAG)
    target_bundle: "previous_stable"
    traffic_switch_seconds: 30
    freeze_new_release: true           # 回滚后冻结发布,等人排查

触发后网关在约三十秒内把流量整体切回 previous_stable,RAG 索引版本随配置一起切回(这要求索引只增不删、旧版本可寻址),并自动冻结后续发布,强制人工复盘。下面这组数字是我们上线这套流水线前后最直观的变化:

上线流水线前后的关键指标对比
故障平均恢复时间从约 40 分钟降至约 30 秒,归一化为相对改善百分比
99
坏变更到达全量用户比例金丝雀阶段拦截,最多影响 1% 流量
1
黄金集回归覆盖场景覆盖售后、退款、物流、价保等高频场景
300

避坑总结:用真金白银换来的几条注意事项 ​

  • 禁止使用 latest 这类浮动模型版本。 现象:某天没发任何代码,回答质量突然下滑。根因是模型供应商静默更新了 latest 指向的版本。修正:所有模型锁定到带日期的不可变版本,升级本身就是一次显式发布,必须走黄金集。
  • 工具 Schema 的枚举值和必填字段改动要当成破坏性变更。 现象:工具调用"成功率"正常,业务结果却错(走了默认值)。修正:CI 做新旧 Schema 兼容 diff,枚举改名、字段必填化一律升大版本,并同步更新 Prompt 里的全部 few-shot。
  • 分流键一定要稳定,别用随机请求分流。 现象:用户多轮对话里前后答复自相矛盾。修正:按店铺 ID(或会话 ID)做一致性哈希,保证同一会话始终命中同一版本。
  • 有副作用的工具在影子流量里必须 dry-run。 现象:影子请求真的执行了退款/发券,造成资损。修正:影子环境对接沙箱工具或加 dry_run=true,只读类工具可真实调用,写操作一律模拟。
  • 回滚要原子回退整个发布物,RAG 索引只增不删。 现象:只回滚了 Prompt,反而和新 Schema、新索引更不兼容。修正:以 bundle 为版本单位,索引保留历史版本可随时寻址,回滚时四类配置一起切。
  • 黄金评测集要持续"喂养",否则会过拟合。 现象:CI 分数越来越高,线上新问题照样出错。修正:每次线上 bad case 复盘后,脱敏回流进评测集,并定期调整风险权重。

LLM 应用正在从"调 Prompt 的手艺活"变成"需要工程纪律的生产系统"。对一个手握退款权限的电商客服 Agent 而言,发布工程不是束缚迭代的枷锁,而是让我们敢频繁上线的底气——毕竟,能在三十秒内安全撤回,才真正敢每天发版。这套从版本化到自动回滚的方法论,也是我在 AI 时代重新理解"工程素养"的一个切片,本文首发于开发者日记,希望能帮到同样在把 Agent 推向生产环境的你。 ===CONTENT===

免责声明

本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。