很多人用 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 | 生成时间过长 | 设置超时,降级返回 |
InvalidRequestError | context 超长 | 截断后重试 |
| 空响应 / 截断响应 | 模型输出不完整 | 检测并重新生成 |
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 通常不是代码错误,而是”模型回答不符合预期”。这类问题很难复现,必须把完整的输入输出都记录下来。
最低可观测性要求:
- 记录完整请求:messages 列表、model 参数、temperature
- 记录完整响应:原始输出、finish_reason、token 用量
- 关联 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 不稳定是常态,重试和降级必须有
- 成本可见:不记录用量就无法优化
- 可观测性:出问题要能回溯,日志要记完整
这些都不是炫技,是工程上最基础的要求。大模型本身很强,但应用层如果不做好这些,很容易在生产环境翻车。