LLM 应用工程化完全指南:从一次"内部工具被运营用一周烧掉几千块"看懂为什么不是调 API 就完了

2024 年我给一个内部工具加了 LLM 能力让运营同学用自然语言查数据库做报表第一版我没多想直接拿 GPT-4 接了上去把用户的问题塞进 prompt 拿到 SQL 执行返回结果我心里很笃定 LLM 应用嘛不就是 prompt 一塞模型一调结果一返三步走可等真把这套东西放到运营同学手里一串麻烦冒了出来第一种最先把我打懵同样的问题问两次返回的 SQL 不一样有时候带 LIMIT 有时候不带运营拿着不同的结果跑来问我哪个对第二种最难缠模型偶尔会编出一个根本不存在的字段名 SQL 执行直接报错运营看着错误一脸懵不知道是自己问错了还是系统坏了第三种最离谱有一次模型生成了一个全表 UPDATE 把生产库的状态字段刷了一遍幸亏运营发现得早回滚了第四种最莫名其妙这套系统跑了一周账单出来我看了一眼差点没缓过来一天烧了几百块 token 费用绝大多数请求其实是同样的几个常见问题被反复问反复跑大模型我盯着这一连串问题想了很久才彻底想明白第一版错在一个根本的认知上我以为做 LLM 应用就是 prompt 一塞模型一调结果一返三步走可这个认知是错的真正的 LLM 应用层要解决的从来不是怎么调模型而是怎么让一个本质上不确定不可控的概率性组件在一个要求确定可观测可回滚的工程系统里稳定运行本文从头梳理为什么三步走会出事 prompt 工程不是写好话而是给约束温度和采样为什么不能用默认值结构化输出比自由文本重要得多函数调用工具不是噱头而是控制边界缓存与回放可以省掉大半成本以及一些把 LLM 应用做扎实要避开的工程坑

2024 年我给一个内部工具加了 LLM 能力让运营同学用自然语言查数据库做报表第一版我没多想直接拿 GPT-4 接了上去把用户的问题塞进 prompt 拿到 SQL 执行返回结果我心里很笃定 LLM 应用嘛不就是 prompt 一塞模型一调结果一返三步走可等真把这套东西放到运营同学手里一串麻烦冒了出来第一种最先把我打懵同样的问题问两次返回的 SQL 不一样有时候带 LIMIT 有时候不带运营拿着不同的结果跑来问我哪个对第二种最难缠模型偶尔会编出一个根本不存在的字段名 SQL 执行直接报错运营看着错误一脸懵不知道是自己问错了还是系统坏了第三种最离谱有一次模型生成了一个全表 UPDATE 把生产库的状态字段刷了一遍幸亏运营发现得早回滚了第四种最莫名其妙这套系统跑了一周账单出来我看了一眼差点没缓过来一天烧了几百块 token 费用绝大多数请求其实是同样的几个常见问题被反复问反复跑大模型我盯着这一连串问题想了很久才彻底想明白第一版错在一个根本的认知上我以为做 LLM 应用就是 prompt 一塞模型一调结果一返三步走可这个认知是错的真正的 LLM 应用层要解决的从来不是怎么调模型而是怎么让一个本质上不确定不可控的概率性组件在一个要求确定可观测可回滚的工程系统里稳定运行本文从头梳理为什么三步走会出事 prompt 工程不是写好话而是给约束温度和采样为什么不能用默认值结构化输出比自由文本重要得多函数调用工具不是噱头而是控制边界缓存与回放可以省掉大半成本以及一些把 LLM 应用做扎实要避开的工程坑

问题背景

很多团队第一次做 LLM 应用都会经历这种"上线半个月就翻车"的尴尬。表面上看是模型不够聪明,实际上是工程层根本没建好。LLM 是个概率性组件,它的输出天然不稳定;但你的系统是确定性的,需要可观测、可重放、可回滚。这两件事的撕扯,才是 LLM 应用的真正难点。下面这几种现象,几乎每个团队都踩过:

  • 不稳定:同样的输入,不同时间会得到不同的输出,排查问题时根本复现不出来。
  • 幻觉:模型会自信地编造一个不存在的字段、API、参数名,下游代码直接炸。
  • 越权:模型有了工具(SQL、API、Shell),稍不注意就能做出破坏性操作。
  • 烧钱:没有缓存、没有路由、没有 fallback,所有请求都走最贵的模型,账单失控。

一、为什么"prompt 一塞模型一调"会出事

很多人第一次接 LLM 都是这个流程:用户输入 → 拼一个 prompt → 调模型 API → 把返回内容塞回前端。三步搞定,看起来很优雅。但这个流程缺了几样东西。

第一,它假设模型的输出是结构化的、可被下游消费的。但默认情况下,模型返回的是一段自由文本,里面可能夹着 markdown、可能带前后缀寒暄、可能格式漂移。下游想把这段文本喂给程序处理,需要先解析,而解析自由文本本身就是一件极其不稳定的事。

第二,它假设模型的输出是可重复的。但 LLM 默认带温度采样,同样的输入会有不同的输出。你今天测通的 case,明天可能就不通。

第三,它假设模型不需要被监控。出问题时你只看到"用户说不对",但你没有日志记录这次请求用了哪个 prompt、哪个模型、哪个版本、温度多少、用了多少 token、花了多少钱。出了线上事故,你连复现都做不到。

第四,它假设成本不重要。等账单出来才知道,绝大多数请求其实是少数高频问题被反复跑,完全可以缓存。

下面是这种"三步走"思维写出来的典型代码,看起来没毛病,但已经埋了一堆雷:

import openai

def answer(user_question: str) -> str:
    prompt = f"你是数据助手,请根据下面的问题生成 SQL:\n{user_question}"
    resp = openai.ChatCompletion.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}],
    )
    sql = resp.choices[0].message.content
    return execute(sql)

这段代码几乎集齐了所有问题:没有 system prompt 约束、没有结构化输出、没有温度控制、没有日志、没有缓存、没有错误兜底、模型直接执行 SQL 没经过任何审查。真要上线,出事是必然的,不是偶然的。

认知翻转:做 LLM 应用,真正花精力的地方不是调模型,而是搭一个能让概率性组件在确定性系统里稳定运行的"外壳"。这个外壳至少要包括:输入约束(prompt 工程 + 模板)、输出约束(结构化 + JSON Schema)、过程控制(温度 + top_p + 工具)、可观测(日志 + 追踪 + 评测集)、成本控制(缓存 + 路由 + fallback)、安全护栏(rate limit + 越权检查)。模型调用本身只是这套体系里的一小步,你越早接受这一点,越早能做出靠谱的 LLM 应用。

二、Prompt 工程不是"写好话",而是"给约束"

很多人理解的 prompt 工程是"想办法把话说得更清楚"。这是初学者的视角。资深一点的视角是:prompt 工程是在用自然语言给模型加约束,告诉它哪些事不能做、哪些格式必须遵守、哪些假设要先确认。

一个生产级的 prompt 通常分成几段:角色定义、能力边界、输入格式、输出格式、错误处理、示例。每一段都不是为了让模型"更聪明",而是为了让模型的输出"更可预测"。

SYSTEM PROMPT 结构(生产级)

[ROLE]
你是一个只读的数据查询助手,只能生成 SELECT 语句,不能生成
INSERT/UPDATE/DELETE/DROP/TRUNCATE/ALTER 等任何修改语句。

[CAPABILITY]
你可以查询的表只有以下白名单:
- orders(id, user_id, amount, created_at, status)
- users(id, name, email, created_at)
任何不在白名单内的表名都禁止使用,如果用户的问题无法用白名单
内的表回答,直接返回 {"error": "table_not_in_whitelist"}。

[INPUT]
用户问题用自然语言表达,可能含歧义。如果歧义影响 SQL 生成,
返回 {"clarify": "...具体问题..."},不要猜测。

[OUTPUT]
严格返回单个 JSON 对象,不要带任何 markdown 包裹,不要带前后缀,
不要解释,字段如下:
{
  "sql": "...",          // 生成的 SQL,失败时为空字符串
  "tables": ["..."],     // 用到的表
  "explanation": "...",  // 一句话给运营看的解释
  "error": ""            // 出错时的错误码
}

[FAIL-SAFE]
如果不确定能不能写出正确 SQL,宁可返回 {"error": "uncertain"}
也不要瞎写。生产环境上,错的 SQL 比"我不知道"代价大得多。

这种 prompt 的好处不是它能让模型变聪明,而是它把"不可控"的可能性压到了最低。没有白名单,模型可能 SELECT 你不存在的表;没有"只读"约束,模型可能生成 DELETE;没有 JSON 输出格式,你下游写解析器要被气死;没有 fail-safe,模型会在不确定时硬编一个看起来合理的答案。

另一个常被忽略的点是 prompt 应该版本化。生产中的 prompt 不应该写在代码里,而应该像配置一样独立存储、独立 review、独立回滚。改一段 prompt 和改一段代码逻辑同样危险,你不会随手改一段生产代码不写 commit message,也不应该随手改一段生产 prompt 不留版本号。

认知翻转:prompt 工程不是"语言艺术",是"接口设计"。你写的 prompt 实际上是模型的 API 契约,定义了输入什么、输出什么、错误怎么处理、边界在哪。把它当成一段 OpenAPI 规范来写,而不是一段对话。多数生产事故都不是模型"答错了",而是 prompt 没把"什么算答错了"说清楚,模型于是用它的理解填了空,而它的理解恰好和你的不一样。

三、温度与采样:为什么不能用默认值

OpenAI 的 API 默认温度是 1.0,这是为了让聊天体验更有人情味、更有"创造力"。但你做的是一个生产应用,你要的不是创造力,是可预测性。temperature=1.0 意味着同一个 prompt 跑十次可能给你十种不同的输出,这在工程上是灾难。

几个关键采样参数的实际工程含义:

采样参数 (Sampling) 的工程语义

temperature:控制随机性,范围 0 到 2
  - 0.0:几乎确定性输出,同样输入 → 同样输出(但不绝对)
  - 0.3:轻度多样性,适合代码/SQL/数据提取
  - 0.7:中等多样性,适合一般问答
  - 1.0:默认,适合聊天/创意写作
  - 1.5+:高随机性,适合头脑风暴

top_p:核采样,只从累计概率 top_p 的 token 中采样
  - 0.1:非常保守,只在最可能的几个 token 中选
  - 0.9:常用,过滤掉长尾低概率 token
  - 1.0:不过滤,跟单纯 temperature 等价

frequency_penalty / presence_penalty:抑制重复
  - 生成长文时用得到,代码/SQL 场景一般用不到

max_tokens:输出上限
  - 必须设,别让模型自由发挥
  - 过小会被截断,过大浪费钱

seed (部分模型支持):
  - 加上之后同样 temperature 同样输入会有更高的可复现性
  - 但 OpenAI 文档明说"best-effort"不是绝对的

一个常见的反模式是:开发时用 temperature=0,觉得"挺稳的"就上线;线上跑一段时间发现某些 case 答得不好,就把 temperature 调高,期望"模型多想几种可能性"。这是错的。temperature 解决不了"模型能力不够"的问题,只会让"能答对的 case"变得不稳定。能力问题应该靠换更强的模型、补 few-shot 示例、补检索增强来解决,不应该靠调高 temperature。

对于结构化输出场景(SQL、JSON、代码生成),温度应该接近 0,top_p 应该 0.1-0.3,这样模型在"语法上必须正确"的约束下,只在语义上做最有把握的选择。对于摘要、改写、扩写这种自然语言任务,温度可以在 0.3-0.7 之间。对于聊天和创意,温度可以到 0.7-1.0。永远不要用默认值,永远在 prompt 旁边一起 review 采样参数。

认知翻转:温度参数不是用来"调节智能水平"的旋钮,而是用来"调节随机性"的旋钮。模型在 temperature=0 时,选择的是它认为概率最高的下一个 token——这并不意味着它"想得最深",只意味着它"最自信"。如果它最自信的答案就是错的,降低温度只会让错误变得稳定,不会让结果变正确。所以选温度的思路应该是:先想清楚你这个场景"宁可错得稳定"还是"宁可对得不稳定",再决定数值。生产系统几乎永远选前者。

四、结构化输出:别让下游去解析自由文本

LLM 的默认输出是自由文本。但你的下游程序需要的是结构化数据。把这两者衔接起来,有三条路:正则解析、prompt 强约束、API 原生的结构化输出。前两条都是早期方案,问题极多;现在的最佳实践是第三条。

OpenAI 的 response_format=json_object 已经能保证返回合法的 JSON,但 JSON 内部结构还是靠 prompt 描述。更强的是 response_format=json_schema(或叫 structured output),你直接给一个 JSON Schema,API 层会强制模型输出符合 schema 的内容。Anthropic 的 tool_use、Google 的 controlled generation 都是同类的方案。

from openai import OpenAI
client = OpenAI()

schema = {
    "type": "object",
    "properties": {
        "sql": {"type": "string"},
        "tables": {"type": "array", "items": {"type": "string"}},
        "explanation": {"type": "string"},
        "error": {"type": "string"},
    },
    "required": ["sql", "tables", "explanation", "error"],
    "additionalProperties": False,
}

resp = client.chat.completions.create(
    model="gpt-4o-2024-08-06",
    messages=[
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": user_question},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "sql_answer",
            "strict": True,
            "schema": schema,
        },
    },
    temperature=0,
    max_tokens=800,
)

import json
data = json.loads(resp.choices[0].message.content)
# 直接拿到字典,不再需要任何自由文本解析

用 schema 强约束之后,你下游再也不用写"先去掉 markdown 包裹,再 try except 解析 json,再补默认值"这种又长又脆的代码了。模型层就保证了输出符合契约,这是工程上巨大的简化。代价是模型要付出一点点"按 schema 输出"的智能开销,但收益远远大于代价。

如果你用的模型不支持原生结构化输出,退而求其次的方案是"严格 prompt + 严格解析 + 重试"。prompt 里明确写"只输出 JSON 不要任何其他字符",解析失败时用错误信息再追问一次让模型修正,二次失败直接降级。这个方案能用但远不如原生 schema 干净。

认知翻转:能用 schema 就一定要用 schema。让"模型必须输出合法结构"这件事在 API 层就被解决,而不是在你的应用层用一堆 try except 兜着。所有写过"自由文本解析"代码的人都知道,那种代码是会随着模型升级而坏掉的——上个月跑得好好的解析逻辑,模型换个版本可能就因为多了一个空格、一个 markdown 反引号而完全崩溃。把契约推到 API 层,你的应用层就稳了。

五、函数调用与工具:控制边界,而不是放飞自我

现代 LLM 应用里很流行让模型"调用工具"——比如让模型自己决定去查数据库、调 API、跑代码。这种模式叫 function calling 或 tool use,确实让 LLM 应用变得强大很多。但很多人忽略了一点:工具不是用来扩展模型能力的,工具是用来约束模型行为的。

没有工具时,模型想做什么动作只能"用文字描述",这段文字到了你的代码里你要去解析、判断、执行,中间有大量自由发挥空间。有了工具之后,模型只能"调用你定义好的工具",参数也只能符合你定义的 schema——它能做什么、能用什么参数、能传什么类型,全部由你定义,模型只是在你画好的格子里跳。这个约束比 prompt 强约束强得多。

真实场景下,工具的设计哲学是"小而专"。不要做"通用执行 SQL"这种工具,要做"查询 orders 表的工具""查询 users 表的工具",每个工具的参数都是结构化的、有 enum、有 range,模型只能在格子里选。这样模型即使"想越权"也越不了。

下面是一个典型的工具定义,它把模型可能做的事限制得极细:

[mermaid]
flowchart TD
A[用户问题] --> B[LLM 决定调用工具]
B --> C{tool 选择}
C -->|query_orders| D[查 orders 工具]
C -->|query_users| E[查 users 工具]
C -->|need_clarify| F[向用户追问]
D --> G[白名单字段 + 参数校验]
E --> G
G --> H[执行 SQL]
H --> I[结果返回给 LLM]
I --> J[LLM 生成自然语言回答]
J --> K[返回给前端]
F --> L[等用户补充]

tools = [
    {
        "type": "function",
        "function": {
            "name": "query_orders",
            "description": "查询订单表 orders,只能 SELECT,不能修改",
            "parameters": {
                "type": "object",
                "properties": {
                    "fields": {
                        "type": "array",
                        "items": {
                            "type": "string",
                            "enum": ["id", "user_id", "amount",
                                     "created_at", "status"],
                        },
                    },
                    "filter_status": {
                        "type": "string",
                        "enum": ["paid", "pending", "cancelled", "any"],
                    },
                    "date_from": {"type": "string", "format": "date"},
                    "date_to": {"type": "string", "format": "date"},
                    "limit": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 1000,
                    },
                },
                "required": ["fields", "limit"],
                "additionalProperties": False,
            },
        },
    },
]

注意几个细节:fields 用 enum 限制字段名,模型编不出"orders.user_phone"这种不存在的字段;limit 强制 1-1000,模型生成不出 SELECT * 不带 LIMIT 的危险查询;filter_status 也是 enum,模型不会写错状态值。这些约束加起来,你的"工具"实际上是一个被驯化得很好的、只在指定格子里跳的舞者,而不是一只乱抓东西的猴子。

另一边,工具执行层一定要有第二道审查。不要相信模型给的参数就一定安全,所有从模型来的参数,都要在执行前再 validate 一次。Schema 校验已经在 API 层做了一遍,但你的业务层还要再做一遍:

def execute_query_orders(args: dict, user_ctx: dict) -> dict:
    # 第二道防线:不信任来自模型的参数,再 validate 一次
    fields = args["fields"]
    limit = args["limit"]

    # 业务规则:普通用户最多查 100 条,管理员才能 1000
    if user_ctx["role"] != "admin" and limit > 100:
        limit = 100

    # 业务规则:日期范围不能反着来
    date_from = args.get("date_from")
    date_to = args.get("date_to")
    if date_from and date_to and date_from > date_to:
        return {"error": "invalid_date_range"}

    # 业务规则:非管理员看不到 user_id 这种敏感字段
    if user_ctx["role"] != "admin":
        fields = [f for f in fields if f != "user_id"]

    # 拼 SQL 时用参数化,绝对不能字符串拼接
    sql = f"SELECT {','.join(fields)} FROM orders WHERE 1=1"
    params = []
    if date_from:
        sql += " AND created_at >= %s"
        params.append(date_from)
    if date_to:
        sql += " AND created_at <= %s"
        params.append(date_to)
    sql += " LIMIT %s"
    params.append(limit)

    return db.fetch(sql, params)

比如 date_from 不能晚于 date_to,limit 在某些权限下不能超过 100,某些状态只有管理员能查。模型给的参数永远是"不可信输入",这一点和"用户输入"是一个级别的。

认知翻转:很多人理解的工具调用是"给模型更多能力"——这是反的。工具调用是"把模型能做的事限制到一个清单里",清单之外的事它做不出来。这种限制反而是真正生产级 LLM 应用的核心安全边界。一个 LLM 应用的安全水平,基本等于你工具定义的精细程度。粗粒度的"执行 SQL"工具是定时炸弹,细粒度的"按字段白名单查指定表"工具才是工程。

六、缓存、路由与 Fallback:成本与可用性的工程

LLM 调用是有成本的,而且不便宜。一个生产应用如果不做任何缓存和路由,基本上账单会让你呼吸困难。这部分聊聊把成本和可用性都做扎实的几个工程手段。

第一是请求级缓存。生产中大量请求是高度重复的——同样的几个问题被不同的用户反复问。一个简单的语义缓存可以省掉大半成本。实现思路是把用户问题向量化,跟历史问题做相似度匹配,命中阈值就直接返回历史答案。这套机制对 FAQ 类、报表类应用尤其有效。

第二是模型路由。不是所有请求都要走最贵的模型。简单分类、规整改写、文案微调这些任务,小模型完全够用;只有复杂推理、多步规划才需要顶级模型。一个常见的架构是:先用小模型分类,再根据分类结果路由到合适的模型。比如下面这种分级:

模型路由策略 (按任务复杂度分级)

L1 - 简单意图分类 / 文本规整 / 改写
     模型: gpt-4o-mini 或 claude-haiku
     成本: ~$0.15 / 1M input tokens
     适用: 路由决策、关键词抽取、简单格式转换

L2 - 一般问答 / 结构化抽取 / 短文档摘要
     模型: gpt-4o 或 claude-sonnet
     成本: ~$3 / 1M input tokens
     适用: 常规业务问答、字段抽取、报表生成

L3 - 复杂推理 / 多步规划 / 长文档分析
     模型: gpt-4-turbo / o1 / claude-opus
     成本: ~$15-60 / 1M input tokens
     适用: 难题、长链推理、关键决策辅助

兜底: 任一上游不可用时降级到下一级,并打告警

第三是 fallback 与降级。任何上游 API 都会挂,OpenAI 也会挂、Anthropic 也会挂。生产应用必须假设"我依赖的模型今天会不可用 5 分钟",然后基于这个假设设计降级路径。最简单的降级是"切到另一家供应商",更彻底的降级是"返回一个缓存答案 + 标注'参考'"或者"返回一个静态的 FAQ 链接"。永远不要让你的应用因为"模型挂了"而对用户呈现一片白屏或者一个红色 500。

第四是预算护栏。给每个用户、每个项目、每天都设一个 token 预算,超额就拒绝或降级。LLM 应用的成本是按次计费的,一个被恶意构造的长 prompt 可能一次烧掉你几十块钱,没有预算护栏的话,一个脚本一晚上就能把你打到欠费。一个最小可用的预算护栏长这样:

import time, redis

r = redis.Redis()

DAILY_USER_TOKEN_LIMIT = 100_000     # 单用户每天 10 万 token
DAILY_USER_USD_LIMIT = 2.0           # 单用户每天 2 美元
HOURLY_QPS_LIMIT = 30                # 单用户每小时 30 次请求

def check_budget(user_id: str, est_tokens: int, est_usd: float) -> None:
    today = time.strftime("%Y%m%d")
    hour = time.strftime("%Y%m%d%H")

    qps_key = f"llm:qps:{user_id}:{hour}"
    tok_key = f"llm:tok:{user_id}:{today}"
    usd_key = f"llm:usd:{user_id}:{today}"

    qps = int(r.get(qps_key) or 0)
    tok = int(r.get(tok_key) or 0)
    usd = float(r.get(usd_key) or 0)

    if qps >= HOURLY_QPS_LIMIT:
        raise BudgetError("rate_limit_hourly_qps")
    if tok + est_tokens > DAILY_USER_TOKEN_LIMIT:
        raise BudgetError("daily_token_budget_exceeded")
    if usd + est_usd > DAILY_USER_USD_LIMIT:
        raise BudgetError("daily_usd_budget_exceeded")

def record_usage(user_id: str, tokens: int, usd: float) -> None:
    today = time.strftime("%Y%m%d")
    hour = time.strftime("%Y%m%d%H")
    pipe = r.pipeline()
    pipe.incr(f"llm:qps:{user_id}:{hour}"); pipe.expire(f"llm:qps:{user_id}:{hour}", 3700)
    pipe.incrby(f"llm:tok:{user_id}:{today}", tokens); pipe.expire(f"llm:tok:{user_id}:{today}", 90000)
    pipe.incrbyfloat(f"llm:usd:{user_id}:{today}", usd); pipe.expire(f"llm:usd:{user_id}:{today}", 90000)
    pipe.execute()

第五是日志与回放。每次调用都要记录完整的 input prompt、参数、output、用了多少 token、花了多少钱、用了多少时间。出问题时你才能复现;做评测时你才能从日志里抽样本;做优化时你才知道钱花在了哪。LLM 应用的可观测性比传统应用更重要,因为模型是个黑盒,你只能靠日志反推它的行为。一个最小的日志结构应该包含这些字段:

{
  "request_id": "req_2026052401abc",
  "user_id": "u_8821",
  "project": "ops_data_assistant",
  "timestamp": "2026-05-24T10:23:11.421Z",
  "prompt_version": "ops_sql_v3.2",
  "model": "gpt-4o-2024-08-06",
  "temperature": 0,
  "max_tokens": 800,
  "input_tokens": 612,
  "output_tokens": 138,
  "input_cost_usd": 0.00153,
  "output_cost_usd": 0.00138,
  "latency_ms": 1284,
  "tool_calls": ["query_orders"],
  "cache_hit": false,
  "route_decision": "L2_gpt4o",
  "fallback_used": false,
  "error": null,
  "user_question_hash": "a3f7...",
  "response_schema_valid": true
}

认知翻转:LLM 应用的运维门槛比很多人想象的高。它不是一个"调 API 就完"的活,它是一个"调 API + 缓存 + 路由 + 降级 + 限流 + 预算 + 日志 + 评测"的工程系统。每一项都不复杂,但少了任何一项,生产环境都会以一种你没预料到的方式打脸。一个新手做的 LLM 应用上线一周就要烧掉几千块钱并不奇怪,而成熟的团队同样一个应用每月可能只花几百,差别全在这些工程手段上。这些功夫看起来跟"AI"没关系,可恰恰是这些功夫决定了你做出来的是一个能稳定服务的产品,还是一个能跑 demo 的玩具。

关键概念速查

概念 含义 常见误区 正确做法
Prompt 工程 给模型加输入/输出/边界约束 当成"写好话" 当成接口契约设计,明确角色/能力/格式/错误处理
Temperature 采样随机性 用默认 1.0 上生产 结构化场景设 0,自然语言场景 0.3-0.7
结构化输出 API 层强约束输出格式 用正则解析自由文本 用 json_schema / tool_use 让 API 层兜住
Function Calling 用工具定义约束模型行为 设计成通用工具放飞自由 小而专的工具 + enum/range 参数
语义缓存 用向量相似度命中历史答案 不缓存或只精确匹配 向量化 + 阈值匹配,FAQ 命中率可达 50%+
模型路由 按任务复杂度选模型 所有请求都走最强模型 分级路由,简单任务用 mini 模型
Fallback 上游不可用时的降级方案 没有,直接 500 切换供应商 / 降级缓存 / 静态兜底
预算护栏 限制单位时间/用户的 token 用量 无限制裸跑 每用户/每项目/每天上限,超额拒绝
评测集 固定一组样本评估模型/prompt 改动 靠手感判断好坏 建评测集 + 自动跑分,每次改动回归
Prompt 版本化 prompt 像代码一样版本管理 写死在代码里乱改 独立存储 + review + 回滚

避坑清单

  1. 不要把 prompt 硬编码在业务代码里,要像配置一样独立存储、独立版本、独立 review,改 prompt 等同于改生产代码。
  2. 不要在生产场景用默认 temperature=1.0,结构化输出必须接近 0,任何场景都不要不显式设置采样参数。
  3. 不要让模型直接生成可执行内容(SQL/Shell/代码)而不做白名单和审查,模型给的内容永远当成"不可信用户输入"对待。
  4. 不要用正则解析模型自由文本输出,能用 json_schema 就用 json_schema,能用 tool_use 就用 tool_use。
  5. 不要做"通用工具",要做"小而专的工具",参数全部 enum/range 约束,把模型的发挥空间压到最低。
  6. 不要不做缓存,语义缓存对 FAQ 类应用能省 50% 以上成本,不做就是烧钱。
  7. 不要所有请求都走最贵模型,做模型路由,简单任务交给 mini 模型,贵模型只处理真正复杂的请求。
  8. 不要假设上游模型 100% 可用,必须设计 fallback 路径,至少要能切换供应商或降级到静态答案。
  9. 不要不设预算护栏,按用户/项目/天/小时多维度限额,超额拒绝或降级,防止恶意 prompt 把你打到欠费。
  10. 不要不建评测集,每次改 prompt、换模型、调参数都要在固定评测集上回归,凭手感"感觉变好了"是大坑。

总结

很多团队对 LLM 应用最大的误判,是把它当成"调用一个聪明 API"的事。你打开 SDK 文档,确实看起来就是 chat.completions.create 三个参数搞定。但真正把一个 LLM 应用做到能上生产、能稳定、能控成本,你写的代码里跟"模型调用"直接相关的部分顶多占两成,剩下八成都是 prompt 管理、参数管理、结构化输出、工具设计、缓存路由、预算护栏、日志评测——这些事跟"AI"两个字关系不大,跟"工程"两个字关系极大。

这套工程量被严重低估的根源是 demo 的迷惑性。你拿 GPT-4 写个 demo 跑通三五个 case 觉得"这事真简单",但生产场景里,你面对的是一万个用户用一万种你没设想过的方式去问问题、一百种你没预料过的输入会让模型懵掉、十种你没准备过的失败模式会让上游挂掉。demo 演示的是"模型能做到什么",生产服务的是"模型在最坏情况下不会怎样"。两者完全不是一回事。

打个比方,做 LLM 应用就像开一家用了"全自动后厨机器人"的连锁餐厅。机器人本身很厉害,菜谱也很丰富,演示给老板看的时候做出来的菜色香味俱全。但等你真把它装到一百家门店,你才发现机器人偶尔会切到自己的手指、偶尔会用错调料、偶尔会做到一半罢工、偶尔会在客流高峰时延迟十几秒、电费一个月比传统厨师还贵。开餐厅的真正本事不在"调教机器人"上,而在"机器人罢工时谁来兜底""出错的菜怎么不流到顾客手里""怎么让它每天稳定出菜""怎么让成本能算清楚"。这些事,跟你买的是哪个品牌的机器人没多大关系。

所以做 LLM 应用,本地跑通三个 case 永远暴露不了真正的问题。它暴露不了 prompt 在不同输入下的退化,暴露不了 temperature 默认值带来的不稳定,暴露不了模型偶尔会编造字段,暴露不了上游半夜抽风你的服务就跟着挂,暴露不了同样的请求被重复跑导致账单失控,更暴露不了运营拿到错答案不知道是模型错了还是自己问错了。真正的检验在生产环境,在一个月之后的账单上,在一次 OpenAI 区域性故障的凌晨三点,在一个被恶意构造长 prompt 攻击的午后。把上面六节里的功夫提前做扎实,等那些时刻到来时,你会感谢自己当初没图省事。如果你正打算做一个 LLM 应用,请从一开始就把它当一个工程系统设计,而不是一个"调 API 的脚本"——这是新手到熟手最关键的一步,也是最容易被忽略的一步。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

JVM 内存调优完全指南:从一次"OOM 就加 -Xmx 越调越卡"看懂为什么堆大小不是越大越好

2026-5-24 13:33:37

技术教程

Redis 分布式锁完全指南:从一次"同一笔订单被扣三次款"看懂 SET NX EX 为什么不是分布式锁

2026-5-24 13:45:17

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索