大模型应用从 Demo 到生产,中间缺了什么

0阅读9分钟

从一次「Demo 翻车」说起

去年我给一个电商客户做客服机器人。演示当天,我用三条精心挑选的问题跑通了全流程——意图识别、知识检索、回答生成,一条链路下来 3 秒出结果,客户很满意。

两周后接入了真实流量。第一天就出了问题:有用户发了一整屏的退货政策截图,有用户用方言拼音输入,有用户连续发了 12 条消息催单。模型要么返回超长截断,要么把前一条的回答重复了一遍,要么直接超时。客户截图发到群里,配文「这就是你们说的人工智能?」。

这件事让我意识到,Demo 和生产之间不是「加个接口」的距离,而是一整套工程能力的缺失。这篇文章就是我对这个 gap 的系统梳理。

为什么 Demo 能跑通,生产跑不通

先拆一下 Demo 和生产在本质上有什么不同。

Demo 跑的是「理想路径」:用户输入干净、意图明确、上下文简短、模型正常响应。生产面对的是「全集」:输入不完整、格式混乱、敏感信息混杂、模型偶发偏离、接口超时、成本波动。更要命的是,Demo 阶段没有人会去关心「如果模型答错了,谁来兜底」这个问题。

Chip Huyen 在她那篇被广泛引用的文章里提过一个判断:「LLM 应用的难点不在模型本身,而在模型周围的工程。」1 我深以为然。模型是确定的商品——你调用 API 就能拿到结果。但把结果变成一个可靠的、可维护的、成本可控的产品功能,中间需要填的东西远比想象中多。

Martin Fowler 团队在总结 LLM 应用工程实践时也指出:「测试能令人信服地证明 bug 的存在,但永远无法证明 bug 的不存在。」2 这句话放在大模型场景下尤其刺眼——传统软件的测试可以覆盖确定性逻辑,但大模型的输出空间是开放的、概率性的。你不能靠 20 条单元测试就说系统没问题。

三个真实案例,三种典型翻车

案例一:知识检索的「看似相关」陷阱

某内容平台用 RAG 做内部文档问答。Demo 阶段用了 50 篇文档测试,检索准确率 92%。上线后接入了 3000+ 篇文档,问题变成了「跨文档推理」——用户问的是一个流程,涉及三篇不同文档的章节。向量检索按单篇相似度打分,返回的片段各自相关但拼在一起互相矛盾。

模型没有幻觉,它忠实地根据检索到的内容生成了回答——只是检索到的内容本身就是冲突的。

这个问题的根因不在模型,而在检索策略。他们后来改成了两阶段检索:先按文档类型过滤,再在限定范围内做语义搜索,最后加了一个「冲突检测」步骤,如果返回的片段之间有矛盾,就让模型显式说明。

案例二:Eval 缺失导致的「静默退化」

一个代码审查工具,最初用 GPT-4 写 Prompt,效果不错。三个月后团队把底层模型换成了某国产模型以降低成本。替换后跑了一遍手动测试,感觉「差不多」。但上线两周后,开发团队反馈「审查意见变水了」——具体指标是:指出实际 bug 的比例从 35% 降到 18%,但格式和语气看起来没变。

因为没有 Eval 系统,这个退化发生了两周才被人注意到。如果有一组黄金样本和自动回归,模型切换后的质量下降应该在发布前就被发现。

案例三:没有降级策略的「雪崩」

某金融应用的智能摘要功能,直接调用 Claude API。某天 Anthropic 的 API 出现间歇性延迟(从正常的 2 秒飙到 30 秒),前端用户以为卡了,反复点击重试。短时间内积压了大量请求,触发了 API 的速率限制。等到 API 恢复,又有一批重试请求涌入,再次触发限流。

整个过程持续了 40 分钟。期间没有降级——哪怕返回一个「摘要暂时不可用,请稍后查看原文」的模板,也比让用户干等 30 秒好得多。

这三个案例对应三种不同的工程缺失:检索策略粗糙、Eval 体系空白、降级策略缺失。它们都不是模型能力问题,而是生产化工程没跟上。

生产化全景:从 Demo 到上线的完整链路

把一个大模型功能从 Demo 推到生产,需要补齐的能力可以用一张图来理解:

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

这条链路里有几个关键节点,我逐个说明。

输入治理

Demo 阶段不会去想「用户输入了什么」,生产必须记录。输入来源、长度、语言、是否包含敏感字段、用户意图是否明确——这些信息出了问题都要回溯。

一个实际的做法是在入口层加一个中间件,记录每次请求的原始输入、清洗后的输入、以及被过滤掉的敏感信息(做脱敏处理)。这不是为了优化模型,而是为了在出问题时能复现。

上下文管理

大模型应用的上下文通常由多个部分组成:系统规则、用户资料、检索到的材料、对话历史、当前查询。Demo 阶段可能把所有内容拼成一个字符串塞给模型,生产需要分层管理。

分层的好处是:系统规则可以独立版本控制,用户资料可以缓存,检索材料可以按相关性动态裁剪,对话历史可以按 Token 预算截断。任何一层出问题,其他层不受影响。

Prompt 版本管理

这是最容易被忽视的一环。很多团队的 Prompt 散落在代码各处,改一次要搜遍整个仓库。生产系统需要把 Prompt 当作一等公民来管理:独立存储、版本控制、A/B 测试、灰度发布。

Vellum 的团队在他们的「四大支柱」框架中强调:应该让非技术人员也能在不改代码的情况下调整 Prompt,直到它通过评测集。3 这个思路我认同——Prompt 的迭代频率远高于代码,把它锁在代码里会拖慢整个流程。

可观测性

LLM 应用的可观测性和传统服务有交集但也有本质不同。传统服务监控的是延迟、错误率、吞吐量;LLM 应用还需要监控「语义层面的健康度」——回答是否相关、是否有幻觉、是否偏离了预期风格。

Galileo AI 的分析把这两层区分得很清楚:监控(monitoring)追踪的是系统是否在运行,可观测性(observability)解释的是为什么出了问题。4 对于 RAG 或 Agent 这类多步骤架构,只有监控是不够的——你需要能追踪每一步的输入、输出和决策依据。

降级策略

当模型不可用、响应太慢或成本超预算时,系统不能干等。降级策略可以是:返回预设模板、切换到更小的模型、让人工接管、或者暂时关闭该功能并告知用户。

关键是降级策略要在架构设计阶段就规划好,而不是等第一次事故后才想起来加。

Demo 与生产的对比:五个维度

把 Demo 和生产的差异系统化,可以从五个维度来看:

维度Demo 阶段生产阶段差距本质
输入干净、受控、意图明确混乱、超长、含敏感信息、意图模糊输入治理缺失
上下文单文档或短对话多文档、长历史、动态检索上下文分层管理缺失
模型调用固定版本、单次调用多版本、多模型、需要重试和降级调用链工程缺失
输出人工目测「看起来对」需要自动评测、需要置信度和兜底评测体系缺失
运维无所谓,出问题重跑需要日志、追踪、告警、成本监控可观测性缺失

再看工程实践的具体差异:

工程环节Demo 做法生产做法
Prompt 管理硬编码在代码中独立存储 + 版本控制 + 灰度发布
测试手动试几条黄金样本集 + 自动回归 + 对抗测试
错误处理打印错误日志结构化错误分类 + 自动降级 + 人工兜底
成本不计较Token 预算 + 模型路由 + 缓存策略
数据安全明文传输敏感字段脱敏 + 权限隔离 + 审计日志

代码对比:Demo 思维 vs 生产思维

1. 输入处理

Demo 思维:直接拿用户输入就调模型。

# Demo:直接使用
user_input = request.json["message"]
response = llm.chat(user_input)

生产思维:验证、清洗、记录、脱敏。

# 生产:输入治理
user_input = request.json["message"]
 
# 长度限制
if len(user_input) > MAX_INPUT_LENGTH:
    user_input = user_input[:MAX_INPUT_LENGTH] + "[已截断]"
 
# 敏感信息脱敏
user_input = mask_sensitive_fields(user_input)
 
# 记录原始输入(脱敏后)用于回溯
logger.info("llm_input", extra={
    "user_id": current_user.id,
    "input_length": len(user_input),
    "input_preview": user_input[:200],
})
 
response = llm.chat(user_input)

2. 模型调用

Demo 思维:调一次,等结果。

# Demo:单次调用
result = llm.generate(prompt, model="gpt-4")
return result

生产思维:重试、降级、超时控制、成本路由。

# 生产:带降级和路由的调用链
async def generate_with_fallback(prompt, complexity="high"):
    # 根据复杂度路由到不同模型
    model = "gpt-4" if complexity == "high" else "gpt-4-mini"
 
    for attempt in range(MAX_RETRIES):
        try:
            result = await asyncio.wait_for(
                llm.generate(prompt, model=model),
                timeout=REQUEST_TIMEOUT,
            )
            # 输出校验
            if validate_output(result):
                return result
        except (TimeoutError, RateLimitError) as e:
            logger.warning(f"attempt {attempt} failed: {e}")
            if attempt == MAX_RETRIES - 1:
                # 降级:返回模板或人工队列
                return await fallback_response(prompt)
 
    return await fallback_response(prompt)

3. 上下文组装

Demo 思维:全部拼在一起。

# Demo:简单拼接
context = system_prompt + "\n" + user_data + "\n" + retrieved_docs + "\n" + query
response = llm.chat(context)

生产思维:分层管理、Token 预算控制、来源标注。

# 生产:分层上下文管理
def build_context(query, user, retrieved_docs):
    layers = {
        "system": load_system_prompt(version=CURRENT_PROMPT_VERSION),
        "user_profile": format_user_context(user),
        "retrieved": [],
        "query": query,
    }
 
    # Token 预算分配
    budget = MAX_CONTEXT_TOKENS
    budget -= count_tokens(layers["system"])
    budget -= count_tokens(layers["user_profile"])
    budget -= count_tokens(layers["query"])
 
    # 按相关性排序,在预算内填充
    for doc in sorted(retrieved_docs, key=lambda d: d.score, reverse=True):
        doc_tokens = count_tokens(doc.content)
        if doc_tokens <= budget:
            layers["retrieved"].append(doc)
            budget -= doc_tokens
 
    return render_context(layers)

4. 评测

Demo 思维:自己看几条,觉得不错就行。

# Demo:手动测试
test_cases = ["问题1", "问题2", "问题3"]
for case in test_cases:
    print(llm.chat(case))
    input("看起来对吗?按回车继续...")

生产思维:黄金样本集 + 自动评测 + 回归检测。

# 生产:自动化 Eval 框架
def run_eval_suite(eval_dataset, prompt_version):
    results = []
    for sample in eval_dataset:
        response = llm.chat(
            sample.input,
            prompt_version=prompt_version,
        )
        results.append({
            "input": sample.input,
            "expected": sample.expected,
            "actual": response,
            "relevance_score": evaluate_relevance(response, sample.expected),
            "faithfulness_score": evaluate_faithfulness(response, sample.context),
            "latency_ms": response.latency,
        })
 
    # 与上一次基线对比
    baseline = load_baseline(eval_dataset.name)
    regression = detect_regression(results, baseline)
 
    if regression.severity > THRESHOLD:
        raise RegressionError(
            f"质量回归: {regression.summary}",
            details=regression.details,
        )
 
    return results

上线前检查清单

把上面的讨论落地成一份可执行的清单。每次大模型功能上线前,我会逐项过一遍:

输入与数据

  • 输入长度有上限,超长输入有截断或拒绝策略
  • 敏感信息(手机号、身份证、银行卡)在发给模型前做了脱敏
  • 输入来源、用户 ID、时间戳有日志记录
  • 对异常输入(空值、纯符号、极端长度)有防御处理

上下文与 Prompt

  • Prompt 独立管理,有版本号,不硬编码在业务逻辑中
  • 上下文分层(系统、用户、检索、查询),各层可独立更新
  • Token 预算有上限,超长上下文有裁剪策略
  • 检索结果有来源标注,模型输出可追溯到具体材料

模型调用与降级

  • 模型调用有超时控制,不会无限等待
  • 有重试策略,但重试次数有限(避免雪崩)
  • 有降级方案:模型不可用时返回模板、切小模型或人工兜底
  • 有模型路由:简单问题不用大模型,控制成本

评测与回归

  • 有一组黄金样本(至少 50 条),覆盖典型场景和边界情况
  • 每次 Prompt 或模型变更都跑自动回归
  • 有明确的评测指标:相关性、忠实度、拒答率、延迟
  • 评测结果与上一次基线对比,回归超过阈值则阻断发布

可观测性与运维

  • 每次请求记录:模型版本、Prompt 版本、Token 用量、延迟、状态
  • 有告警规则:错误率、延迟、成本超过阈值时通知到人
  • 能通过请求 ID 回溯完整链路:输入 → 上下文 → Prompt → 模型响应 → 输出
  • 有成本监控:按功能、按用户维度能看到 Token 消耗趋势

安全与合规

  • 模型输出有基本的内容安全检查(避免泄露系统 Prompt 或敏感信息)
  • 涉及用户数据的功能有权限控制,不同用户看到不同范围的结果
  • 对外发布的内容有人工审核环节(至少在早期)
  • 有数据保留和清理策略,符合隐私合规要求

适用边界:不必一次做完

上面列了很多东西,但不意味着每个项目都要一次性全部补齐。我的经验是按风险等级来分配工程投入。

轻量上线(写作辅助、内部工具、低频场景):保留最小闭环——请求日志、Prompt 版本、50 条黄金样本、人工反馈入口。出了问题能查到,能回退,就够了。

中等投入(客服、推荐、内容生成):加上自动 Eval、降级策略、成本监控和基本的可观测性。这些功能面向外部用户或影响业务流程,需要更严格的保障。

重度投入(自动审批、金融决策、医疗辅助):完整的评测平台、自动回归、细粒度监控、审计日志、人工兜底。这些场景模型出错直接影响用户权益,工程投入不能省。

生产化的目标不是让模型永远正确——这在当前技术条件下做不到。目标是让系统在模型不稳定时仍然可观察、可解释、可恢复。用户能理解为什么出了错,团队能快速定位问题,系统能自动降级而不是雪崩。

这才是 Demo 和生产之间真正缺的东西。

参考资料

Footnotes

  1. Chip Huyen. Building LLM Applications for Production. 2023-04.

  2. Martin Fowler. Engineering Practices for LLM Application Development. 2024-02.

  3. Vellum. The Four Pillars of Building a Production-Grade AI Application. 2024-01.

  4. Galileo AI. LLM Monitoring vs Observability: Understanding the Key Differences. 2026-04.

评论 0

0 / 1000