很多人用 LangChain 或直接调 API 跑通了一个 Demo,然后就以为可以上线了。生产环境和 Demo 之间的距离,远比想象的大。

这篇文章记录我在把几个 LLM 应用推向生产过程中遇到的核心问题和解法,不涉及模型选型和 prompt 入门,直接讲工程层面的坑。

上下文管理:最容易被忽视的问题

LLM 的 context window 是有限的,而且 token 越多,推理越慢、费用越高。大多数 Demo 不处理这个问题,但生产环境必须处理。

几种常见策略:

滑动窗口

保留最近 N 轮对话,丢弃早期历史。实现最简单,但会导致模型”失忆”,适合对上下文依赖不强的场景。

def trim_messages(messages: list, max_tokens: int = 4000) -> list:
    # 始终保留 system message
    system = [m for m in messages if m["role"] == "system"]
    rest = [m for m in messages if m["role"] != "system"]

    # 从最新的开始往前保留
    kept = []
    total = count_tokens(system)
    for msg in reversed(rest):
        t = count_tokens([msg])
        if total + t > max_tokens:
            break
        kept.insert(0, msg)
        total += t

    return system + kept

摘要压缩

当对话历史超过阈值时,用模型本身把早期历史压缩成摘要,再继续对话。保留关键信息,但引入额外的 API 调用成本。

RAG 替代长上下文

把知识库内容做向量化,查询时按相关性动态注入,而不是把整个文档塞进 context。这是目前最主流的做法。


错误处理:不要相信 API 永远可用

LLM API 会超时、会限速、会返回空结果。生产代码必须处理这些情况。

必须处理的错误类型:

错误原因处理方式
RateLimitError请求频率过高指数退避重试
Timeout生成时间过长设置超时,降级返回
InvalidRequestErrorcontext 超长截断后重试
空响应 / 截断响应模型输出不完整检测并重新生成
import time
from openai import RateLimitError, APITimeoutError

def call_llm_with_retry(messages, max_retries=3):
    for attempt in range(max_retries):
        try:
            response = client.chat.completions.create(
                model="gpt-4o",
                messages=messages,
                timeout=30,
            )
            return response.choices[0].message.content
        except RateLimitError:
            wait = 2 ** attempt  # 1s, 2s, 4s
            time.sleep(wait)
        except APITimeoutError:
            if attempt == max_retries - 1:
                return "服务暂时不可用,请稍后重试。"
    return None

成本控制:token 用量要可见

不知道每次请求花了多少 token,就无法做成本优化。上线前必须把 token 用量记录下来。

最简单的做法:把每次调用的用量写日志

def log_usage(response, endpoint: str):
    usage = response.usage
    logger.info(
        "llm_usage",
        extra={
            "endpoint": endpoint,
            "prompt_tokens": usage.prompt_tokens,
            "completion_tokens": usage.completion_tokens,
            "total_tokens": usage.total_tokens,
            "model": response.model,
        }
    )

有了日志,就可以按接口、按用户、按时段统计,找出高消耗路径优化。

常见优化点:

  • system prompt 尽量短,去掉冗余描述
  • 对简单问题用小模型(如 gpt-4o-mini),复杂问题才用大模型
  • 对重复性高的请求做语义缓存(相似问题复用上一次的回答)

可观测性:出问题要能定位

LLM 应用的 bug 通常不是代码错误,而是”模型回答不符合预期”。这类问题很难复现,必须把完整的输入输出都记录下来。

最低可观测性要求:

  1. 记录完整请求:messages 列表、model 参数、temperature
  2. 记录完整响应:原始输出、finish_reason、token 用量
  3. 关联 trace_id:一次用户请求可能触发多次 LLM 调用,要能串联
import uuid

def traced_llm_call(messages, **kwargs):
    trace_id = str(uuid.uuid4())

    logger.info("llm_request", extra={
        "trace_id": trace_id,
        "messages": messages,
        **kwargs
    })

    response = client.chat.completions.create(messages=messages, **kwargs)

    logger.info("llm_response", extra={
        "trace_id": trace_id,
        "content": response.choices[0].message.content,
        "finish_reason": response.choices[0].finish_reason,
        "usage": response.usage.model_dump(),
    })

    return response

总结

从 Demo 到生产,核心差距在于:

  • 上下文管理:不能无限堆 token,要有截断或压缩策略
  • 错误处理:API 不稳定是常态,重试和降级必须有
  • 成本可见:不记录用量就无法优化
  • 可观测性:出问题要能回溯,日志要记完整

这些都不是炫技,是工程上最基础的要求。大模型本身很强,但应用层如果不做好这些,很容易在生产环境翻车。