LLM 应用生产就绪清单

0阅读11分钟

上线不是 demo 放大

我把一个内部客服助手从 demo 推到生产的第一周,日均 Token 费用从预算的 40 美元飙到 380 美元,P95 延迟从 demo 环境的 1.2 秒退化到 6.8 秒,用户投诉里有 12% 是模型输出了不存在的产品参数。

Demo 环境的问题在于:输入是精心挑选的,并发是假的,成本是按几次调用算的。生产环境会把这些假设全部打破。真实用户的输入比你想象的要脏、要长、要怪;失败路径比你 demo 时考虑的多三倍以上;成本和延迟会随着请求量线性甚至超线性增长。

上线前需要把模型输出纳入工程治理——版本化 Prompt、约束输出格式、建立评测管道、记录完整 trace、控制成本预算、处理安全风险、准备降级回滚方案。这篇文章整理了我从多次上线(以及翻车)中总结出的七个检查维度,附带可执行的清单和代码示例。

理论框架:生产就绪的七个维度

学术界对 GenAI 系统的生产就绪问题已经有了系统性的梳理。2024 年发表在 arXiv 上的 grey literature survey《A State-of-the-practice Release-readiness Checklist for Generative AI》从六个生命周期阶段(预训练与微调、Prompt 工程、部署前评估、部署策略、部署后监控、持续维护)整理了 LLM 应用从实验到生产的关键检查项1。Arthur AI 的工程博客则从实操角度将检查清单浓缩为三大支柱:监控与可观测性、评测体系、安全与护栏2

我把这些来源和我的实际经验交叉对比,归纳出七个维度。这七个维度不是独立检查项,它们之间有依赖关系:

流程图画布 · 115%
Mermaid 流程图加载中...

没有 Prompt 版本化,评测管道就无法区分「改了什么导致结果变差」。没有可观测性,成本和延迟就是黑箱。没有安全护栏,降级策略可能把用户导向更危险的兜底路径。

七个维度的详细检查

一、Prompt 版本化与输出契约

Prompt 是 LLM 应用的「代码」,但它往往散落在业务逻辑里、写在字符串常量中、甚至硬编码在前端。这种状态在 production 中是灾难性的——你无法回滚一个你不知道什么时候被改过的 Prompt。

维度未版本化已版本化
回滚能力无法回滚,只能手动改代码重新部署通过配置中心秒级回滚到上一版本
A/B 测试需要两套代码分支同一代码,不同 Prompt 版本路由
评测关联不知道评测对应哪个 Prompt评测结果与 Prompt 版本绑定
审计追溯出了问题不知道谁改了什么完整的变更历史和 diff

Prompt 版本化不只是把字符串提到配置文件。更关键的是建立 Prompt 与输出格式的契约——模型返回什么结构、哪些字段是必须的、格式错误时怎么处理。

# ❌ 坏做法:Prompt 和解析逻辑散落在业务代码中,无版本控制
def answer_customer_question(question: str, context: list[str]):
    prompt = f"""你是客服助手。根据以下信息回答用户问题:
{'\n'.join(context)}
用户问题:{question}
请用友好的语气回答。"""
    response = llm.chat(prompt)
    # 直接返回模型原始输出,没有任何格式约束
    return response.text

这段代码的问题:Prompt 是字符串拼接,没有版本追踪;输出没有格式约束,模型可能返回任何内容;没有处理模型拒答或格式错误的情况。

# ✅ 好做法:Prompt 外部化、版本化,输出有 JSON Schema 约束
# prompts/customer_service_v2.3.yaml
# version: "2.3"
# template: |
#   你是{brand_name}的客服助手。
#   根据以下参考文档回答用户问题。如果参考文档中没有相关信息,
#   明确告知用户「暂无相关信息」,不要编造。
#   输出格式必须为 JSON:
#   {{"answer": string, "confidence": float, "sources": string[]}}
 
from prompt_registry import get_prompt
 
CUSTOMER_RESPONSE_SCHEMA = {
    "type": "object",
    "properties": {
        "answer": {"type": "string"},
        "confidence": {"type": "number", "minimum": 0, "maximum": 1},
        "sources": {"type": "array", "items": {"type": "string"}},
    },
    "required": ["answer", "confidence", "sources"],
}
 
def answer_customer_question(question: str, context: list[str]) -> dict:
    prompt = get_prompt("customer_service", version="2.3")
    response = llm.chat(
        prompt.format(brand_name="XX品牌", context=context, question=question),
        response_format={"type": "json_schema", "schema": CUSTOMER_RESPONSE_SCHEMA},
    )
    parsed = parse_and_validate(response.text, CUSTOMER_RESPONSE_SCHEMA)
    if parsed["confidence"] < 0.5:
        return fallback_to_human(question, context)
    return parsed

好做法的核心变化:Prompt 从代码中分离,通过版本注册表管理;输出有明确的 JSON Schema 约束;低置信度自动降级到人工处理。

二、评测管道

评测是区分「感觉还行」和「真的能用」的分水岭。Arthur AI 的建议很直接:让评测结果是二值的(binary),不要让 LLM 在 1-10 分之间打分——模型在范围打分上极不一致2

评测类型执行时机数据来源目的
离线评测(Supervised Evals)发布前固定 golden dataset验证 Prompt/模型/RAG 变更没有引入回归
在线评测(Continuous Evals)生产实时真实用户流量检测模型漂移、质量退化
红队测试(Red Teaming)发布前/定期对抗性样本发现安全漏洞、偏见和边界情况
A/B 评测灰度期间分流真实流量对比新旧版本的核心指标差异

评测样例集不是一次性产物,它需要随着线上 bad case 不断扩充。每次用户在反馈入口点了「踩」,这条对话就应该进入评测候选池。

# ❌ 坏做法:评测靠人肉看几个例子,用模糊标准判断
def manual_eval():
    examples = ["问题1", "问题2", "问题3"]
    for q in examples:
        answer = llm.chat(q)
        print(f"Q: {q}\nA: {answer}\n感觉怎么样?(y/n)")
        # 依赖人工主观判断,不可复现,不可扩展
# ✅ 好做法:结构化评测管道,二值判定,自动回归
from deepeval import assert_test
from deepeval.metrics import AnswerRelevancyMetric, FaithfulnessMetric
 
# golden dataset 每条包含:问题、期望答案要点、必须引用的文档 ID
golden_dataset = load_dataset("evals/customer_service_v47.jsonl")
 
def run_regression_eval(prompt_version: str):
    relevancy_metric = AnswerRelevancyMetric(threshold=0.7)
    faithfulness_metric = FaithfulnessMetric(threshold=0.8)
    failures = []
 
    for case in golden_dataset:
        response = run_with_prompt_version(case.question, prompt_version)
        # 二值判定:通过或不通过,不打分数
        try:
            assert_test(response, [relevancy_metric, faithfulness_metric])
        except AssertionError:
            failures.append({
                "question": case.question,
                "expected_sources": case.expected_sources,
                "actual_sources": response.sources,
            })
 
    # 任何失败都阻断发布
    if failures:
        raise EvalGateError(
            f"评测未通过:{len(failures)}/{len(golden_dataset)} 条失败",
            failures=failures,
        )

三、可观测性:trace、日志和模型参数

传统应用的可观测性关注请求延迟、错误率和吞吐量。LLM 应用需要额外记录:完整 Prompt(包括 system prompt 和 user prompt)、模型参数(temperature、top_p、max_tokens)、Token 消耗(input/output 分别计数)、检索命中的文档片段、工具调用的输入输出。

Arthur AI 的原则是「You can't fix what you can't see」2。没有 trace 数据,你甚至不知道成本花在了哪里。

# ❌ 坏做法:只记录最终输出,出问题无法排查
def handle_request(user_input: str):
    result = llm.chat(user_input)
    logger.info(f"User: {user_input}, Response: {result.text[:100]}")
    return result
# ✅ 好做法:完整 trace 记录,包括 Prompt、参数、检索结果、Token 消耗
from opentelemetry import trace
import json
 
tracer = trace.get_tracer("llm-service")
 
def handle_request(user_input: str, session_id: str):
    with tracer.start_as_current_span("llm_request") as span:
        # 记录检索步骤
        retrieved_docs = retriever.search(user_input, top_k=5)
        span.set_attribute("retrieval.doc_count", len(retrieved_docs))
        span.set_attribute("retrieval.doc_ids", json.dumps([d.id for d in retrieved_docs]))
 
        # 构建完整 Prompt 并记录
        prompt = build_prompt(user_input, retrieved_docs)
        span.set_attribute("prompt.version", "2.3")
        span.set_attribute("prompt.token_count", count_tokens(prompt))
 
        # 调用模型,记录参数和消耗
        response = llm.chat(
            prompt,
            model="gpt-4o",
            temperature=0.2,
            max_tokens=1024,
        )
        span.set_attribute("llm.model", "gpt-4o")
        span.set_attribute("llm.temperature", 0.2)
        span.set_attribute("llm.input_tokens", response.usage.prompt_tokens)
        span.set_attribute("llm.output_tokens", response.usage.completion_tokens)
        span.set_attribute("llm.cost_usd", calculate_cost(response.usage))
        span.set_attribute("session.id", session_id)
 
        return response

有了这些 trace 数据,你可以回答「某个用户的 bad case 是因为检索没召回相关文档,还是模型忽略了指令」这类问题。没有 trace,你只能猜测。

四、成本预算

LLM 应用的成本结构和传统应用完全不同。传统应用的边际成本趋近于零(多一个请求只是多一点 CPU),但 LLM 应用的边际成本是实打实的 Token 费用。Morph 的成本优化指南给出了具体数据:模型路由可以节省 40-70%,上下文压缩可以减少 50-70% Token,缓存命中可以节省 90%——这五项策略叠加可以将总成本降低 70-85%3

优化策略成本节省实施复杂度适用场景
模型路由40-70%60-80% 请求是常规任务时
上下文压缩50-70% Token多轮对话,上下文越来越长
Prompt 缓存90%(缓存命中)系统提示词稳定、重复前缀多
Prompt 精简30-50% output Token系统提示词过长、few-shot 示例过多
批处理50%非实时任务:评测、标注、回填

关键是:不要等上线后才发现成本失控。上线前就要根据预估的请求量、平均 Token 数和模型定价算出月度预算,并设置硬性告警阈值。

# ❌ 坏做法:所有请求都用最贵的模型,没有成本意识
def handle_any_request(user_input: str):
    # 不管问题多简单,一律用 GPT-4o
    return llm.chat(user_input, model="gpt-4o")
# ✅ 好做法:模型路由 + 缓存 + 成本预算控制
from cost_tracker import CostTracker, BudgetExceededError
 
tracker = CostTracker(daily_budget_usd=100.0)
 
# 简单分类器判断任务复杂度,路由到不同模型
def route_to_model(user_input: str) -> str:
    # 实际项目中可以用轻量分类模型或规则引擎
    if is_simple_query(user_input):
        return "gpt-4o-mini"       # $0.15 / 1M input tokens
    elif is_standard_query(user_input):
        return "gpt-4o"            # $2.50 / 1M input tokens
    else:
        return "gpt-4o"            # 复杂任务才用最贵模型
 
def handle_request_with_budget(user_input: str) -> str:
    model = route_to_model(user_input)
    # 预算检查:如果今日已超预算,拒绝新请求并告警
    if tracker.exceeded():
        tracker.alert("Daily budget exceeded")
        return "系统繁忙,请稍后重试"
    response = llm.chat(user_input, model=model)
    tracker.record(cost=calculate_cost(response.usage), model=model)
    return response.text

五、延迟 SLA

延迟对用户体验的影响是非线性的。1 秒以内的响应感觉是「即时的」,3 秒以上用户开始分心,10 秒以上大多数用户会离开或反复刷新。LLM 的流式输出(streaming)可以缓解感知延迟,但首 Token 时间(TTFT)仍然需要控制在合理范围。

场景TTFT 目标总延迟目标流式输出
实时对话< 800ms首段 < 2s必须
搜索增强回答< 1.5s完整 < 5s推荐
文档摘要< 3s完整 < 30s可选
批量处理无要求24h 内完成不需要

延迟优化的核心杠杆:缩短输入 Token(上下文压缩、Prompt 精简)、缓存(避免重复计算)、模型选择(小模型延迟天然更低)、以及流式输出(让用户尽早看到部分结果)。

六、安全护栏

LLM 应用的安全风险与传统应用有本质区别。传统应用的安全边界是确定的(SQL 注入、XSS 有成熟的防御方案),但 LLM 的输入是自然语言,攻击面更模糊。arXiv 的综述论文特别指出了 Prompt Injection、PII 泄露和输出毒性三个核心风险1

Arthur AI 建议将护栏分为两层:事前拦截(Pre-LLM)和事后拦截(Post-LLM)2

护栏层执行时机检查内容处置方式
事前拦截(Pre-LLM)用户输入到达模型前PII 检测与脱敏、Prompt Injection 检测、敏感数据拦截拦截请求、返回提示
事后拦截(Post-LLM)模型输出返回用户前幻觉检测、毒性内容过滤、输出格式验证、工具调用安全校验拦截输出、触发自纠正循环
运行时监控持续异常 Token 消耗、异常请求模式、批量注入尝试告警、限流、熔断
# ❌ 坏做法:用户输入直接拼入 Prompt,无任何安全检查
def dangerous_handler(user_input: str):
    prompt = f"根据以下信息回答问题:{user_input}"
    return llm.chat(prompt)
# ✅ 好做法:双层护栏 + 自纠正循环
from guardrails import PIIDetector, PromptInjectionDetector, ToxicityFilter
 
pii_detector = PIIDetector()
injection_detector = PromptInjectionDetector()
toxicity_filter = ToxicityFilter()
 
def safe_handler(user_input: str) -> str:
    # 事前拦截:PII 脱敏
    sanitized_input, pii_found = pii_detector.redact(user_input)
    if pii_found:
        logger.warning(f"PII detected and redacted: {pii_found}")
 
    # 事前拦截:Prompt Injection 检测
    if injection_detector.detect(sanitized_input):
        return "检测到异常输入,请重新提问。"
 
    # 调用模型
    response = llm.chat(build_prompt(sanitized_input))
 
    # 事后拦截:毒性过滤
    if toxicity_filter.is_toxic(response.text):
        return generate_safe_response()
 
    # 事后拦截:输出格式验证
    validated = validate_output(response.text, expected_schema)
    if not validated.ok:
        # 自纠正:将格式错误反馈给模型重新生成(最多 1 次)
        response = llm.chat(corrective_prompt(response.text, expected_schema))
 
    return response.text

七、降级与回滚

LLM 应用比其他系统更需要降级策略,因为它依赖的外部组件更多:模型 API 可能超时或返回错误、向量数据库可能检索失败、工具调用可能异常。没有降级策略的 LLM 应用,任何一个依赖出问题都会导致用户体验完全中断。

灰度发布是降低回滚成本的关键。不要一次性把所有流量切到新版本,而是按比例逐步放量:

阶段流量比例关注指标持续时间回滚条件
金丝雀1-5%错误率、延迟 P95、用户反馈2-4 小时错误率 > 1% 或 P95 > SLA
小流量10-20%成本/请求、评测指标、客诉率1-2 天成本超预算 30% 或评测下降
半量50%全量指标对比2-3 天任何核心指标显著退化
全量100%持续监控长期持续监控,随时可回滚

回滚策略需要覆盖三个层面:Prompt 回滚(秒级,通过配置中心切换版本)、模型回滚(分钟级,切换到上一个稳定模型版本)、全链路回滚(切流量到旧版本服务)。

上线前检查清单

以下是按阶段分组的可执行清单,总计 28 项:

发布前(Pre-Deployment)

  • Prompt 版本化:所有 Prompt 从代码中分离,通过版本注册表管理,支持秒级切换
  • 输出契约:模型输出有 JSON Schema 或结构化约束,解析失败有兜底处理
  • Golden Dataset:至少有 50+ 条覆盖核心场景的评测样例,包含期望答案和引用来源
  • 离线评测通过:回归评测通过率 ≥ 95%,无关键场景失败
  • 红队测试:完成至少一轮 Prompt Injection、越权和毒性测试
  • PII 处理:用户输入中的 PII 在到达模型前被检测和脱敏
  • 失败路径定义:模型拒答、格式错误、检索为空、工具调用失败、超时五种情况都有明确处理

灰度阶段(Canary Release)

  • 成本预算:根据预估请求量计算月度预算,设置 80% 和 100% 两级告警
  • 延迟基线:建立 TTFT 和总延迟的 P50/P95/P99 基线,设置 SLA 告警
  • Trace 完整性:每次模型调用都记录完整 trace(Prompt、参数、Token、检索结果)
  • 在线评测运行:Continuous Evals 在生产流量上持续运行,结果写入监控面板
  • 灰度放量计划:制定 1% → 10% → 50% → 100% 的放量节奏和各级回滚条件

生产运行(Post-Deployment)

  • 告警覆盖:成本超预算、延迟超 SLA、错误率上升、评测指标下降四类告警全部配置
  • 反馈闭环:用户「踩」的反馈自动进入评测候选池,每周更新 Golden Dataset
  • 模型漂移监控:对比当前输出分布与基线分布,检测概念漂移
  • 回滚演练:至少完成一次 Prompt 回滚和一次模型回滚演练,验证回滚时间在分钟级内
  • 定期红队:每月至少一轮红队测试,覆盖新出现的攻击向量
  • 成本复盘:每月复盘实际成本 vs 预算,识别优化空间(缓存命中率、路由比例、上下文长度)

用证据决定上线

上线判断应来自评测结果、灰度数据和监控指标,而不是团队的主观感受。没有证据的上线,只是在把 demo 风险转嫁给用户。

我见过太多团队在 demo 效果不错的时候就急着全量放开,然后在第一周被成本、延迟和 bad case 打得措手不及。上述七个维度和 28 项清单不是为了阻止你上线,而是为了让你在上线后不用花三倍的代价去补救。

最后一点:这份清单不是一次性的。LLM 应用在运行中会持续变化——用户输入分布会变、模型提供商会更新模型、知识库会过期。检查清单需要随着应用的生命周期持续更新,每次重大变更后重新过一遍。


参考资料

Footnotes

  1. Müller, M. et al. "A State-of-the-practice Release-readiness Checklist for Generative AI Software." arXiv:2403.18958 (2024). https://arxiv.org/html/2403.18958v1 2

  2. Arthur AI. "Your Checklist to Launch a Production-Ready AI Agent." Arthur AI Blog (2026). https://www.arthur.ai/blog/checklist-to-launch-a-production-ready-ai-agent 2 3 4

  3. Morph. "LLM Cost Optimization: 5 Levers to Cut API Spend 70-85%." Morph Blog. https://www.morphllm.com/llm-cost-optimization

评论 0

0 / 1000