1. 企业级 LLM 落地,为什么“能跑通 Demo”和“能扛住生产”之间隔着一整条鸿沟
如果你在过去一年里参与过任何企业级 LLM 项目,大概率见过这样的场景:Demo 阶段用几十条数据跑得风生水起,老板看完拍板立项,结果一上生产环境就原形毕露——响应延迟从 2 秒飙到 20 秒,并发一上来就超时,模型输出时好时坏,成本账单月底一看直接翻了三倍。这不是某个团队的问题,而是整个行业在从“玩模型”转向“用模型”时必然要交的学费。
我自己经手过几个从零到一的企业级 LLM 项目,涉及智能客服、文档问答、代码辅助、数据可视化问答等不同场景,踩过的坑足够写一本小册子。这个系列写到第九篇,我打算把之前零散聊过的东西收拢一下,专门讲清楚一件事:企业级 LLM 和普通 LLM 应用到底差在哪里,以及一个真正能上生产的系统应该怎么搭。
这篇文章适合三类人看:一是正在做 LLM 技术选型的架构师,你需要知道哪些环节是真正的成本大头和风险点;二是刚接触 LLM 但有一定后端基础的开发者,你想知道从调用 API 到构建完整系统中间缺了哪些东西;三是技术负责人,你需要一套可落地的评估框架来判断团队方案是否靠谱。我会尽量少讲空泛的概念,多讲具体的参数、配置、代码结构和排查思路,让你看完能直接对照自己的项目做检查。
2. 企业级 LLM 系统的整体架构设计与选型逻辑
2.1 从“调 API”到“建系统”的思维转变
很多人对 LLM 应用的认知还停留在“写个 prompt 调接口”的阶段。这种认知在个人项目里没问题,但放到企业环境里会立刻撞墙。原因很简单:企业级系统要同时满足稳定性、可观测性、成本可控、数据安全、可扩展这五个约束,而单纯的 API 调用一个都不沾。
我习惯把企业级 LLM 系统拆成五层来看,从下往上分别是:基础设施层、模型服务层、编排调度层、应用逻辑层、交互展示层。每一层都有自己独立的选型考量和故障模式,混在一起谈就容易乱。
基础设施层解决的是算力和网络问题。这里有个常见的误区:很多人一上来就纠结用哪家云、买什么卡,其实应该先算清楚你的峰值并发和平均 token 吞吐。举个例子,假设你的客服场景日均 5 万次对话,每次平均输入 800 token、输出 300 token,峰值集中在工作日的 9 点到 11 点,大约占总量的 30%。那么峰值时段需要处理的 token 量约为 5万 × 30% × 1100 ≈ 1650 万 token,按两小时算,每秒需要约 2300 token 的吞吐。这个数字直接决定了你是用 API 还是自部署、需要几张卡、要不要做请求队列。
模型服务层是选型分歧最大的地方。我的经验是:不要试图用一个模型解决所有问题。企业场景里通常至少需要三类模型——一个通用对话模型处理开放式问答,一个嵌入模型做检索和聚类,一个小的分类或抽取模型做意图识别和结构化输出。这三类模型的选型标准完全不同,通用模型看综合能力和成本,嵌入模型看检索召回率,小模型看推理速度和准确率的平衡。
编排调度层是很多团队容易忽略但实际最影响体验的部分。它要处理的事情包括:请求路由(不同意图走不同模型)、上下文管理(多轮对话怎么裁剪和压缩)、缓存策略(哪些结果可以复用)、降级方案(主模型超时了怎么办)、限流熔断(防止一个坏请求拖垮整个服务)。这一层做得好不好,直接决定了系统的 P95 延迟和可用性。
2.2 模型选型的三个硬指标与两个软指标
选模型这件事,网上讨论最多的是榜单排名,但榜单和你的实际业务往往关系不大。我总结下来,真正需要盯的是三个硬指标和两个软指标。
三个硬指标是:首 token 延迟、输出吞吐、单位成本。首 token 延迟决定了用户感知的“反应速度”,一般要求控制在 1 秒以内,超过 2 秒用户就会觉得卡。输出吞吐决定了长回答的生成速度,低于 20 token/s 用户会明显感到“一个字一个字往外蹦”。单位成本要按“每百万 token 的综合成本”来算,注意要把输入和输出分开计价,因为输出通常贵 2 到 4 倍。
两个软指标是:指令遵循能力和格式稳定性。指令遵循能力指的是模型能不能严格按照你要求的格式输出,比如“只返回 JSON,不要任何解释”。格式稳定性指的是同样的 prompt 多次调用,输出结构是否一致。这两个指标在 Demo 阶段看不出问题,但到了生产环境,一次格式错误就可能导致下游解析失败,进而触发告警。
我一般会用一个简单的测试集来评估候选模型:准备 50 条真实业务请求,覆盖简单问答、多轮追问、结构化抽取、边界情况四类,然后对每个模型跑三遍,记录延迟、成本、格式正确率、人工评分。这个测试集不需要很大,但一定要来自真实场景,否则测出来的结果没有参考价值。
2.3 为什么“企业级”三个字意味着额外的工程约束
企业级和个人项目最大的区别在于,个人项目可以接受“偶尔失败”,企业级系统必须做到“失败可控”。这意味着你需要考虑很多在 Demo 阶段完全不会想到的问题。
比如数据隔离。多租户场景下,A 客户的对话历史绝对不能出现在 B 客户的检索结果里。这要求你在向量库、缓存、日志三个层面都做租户维度的隔离,而不是简单地在 prompt 里加一句“只回答当前用户的问题”。
再比如审计合规。企业系统需要记录每一次模型调用的输入输出、耗时、成本、调用方,并且这些日志要能按时间、用户、场景检索。这不是为了“监控”,而是为了在出问题时能快速定位,以及在需要时能向业务方解释“为什么模型给出了这个答案”。
还有版本管理。模型会更新,prompt 会迭代,检索策略会调整。如果没有一套版本管理机制,你很快就会发现“上周还好好的,这周就不对了”,而且根本不知道是哪个环节变了。我的做法是给 prompt、检索配置、模型版本都打上版本号,每次变更记录变更原因和影响范围,出问题时可以快速回滚。
3. 核心模块拆解:从请求进入到结果返回的完整链路
3.1 请求预处理:意图识别与上下文组装
一个企业级 LLM 请求进入系统后,第一件事不是直接调模型,而是做预处理。预处理的核心任务是搞清楚“用户到底想要什么”,然后据此决定后续走哪条链路。
意图识别我通常用一个小模型或者规则引擎来做,而不是用大模型。原因很简单:意图分类的类别是有限的、固定的,用大模型既慢又贵。一个几百兆的小模型在 CPU 上就能跑到 10 毫秒以内,准确率足够支撑路由决策。规则引擎则用于处理那些高频、明确的模式,比如“帮我查一下订单”这种可以直接匹配到订单查询链路。
上下文组装是预处理里最容易被低估的环节。多轮对话场景下,你不能把全部历史都塞进 prompt,那样 token 成本会爆炸,而且模型注意力会被稀释。我的做法是维护一个滑动窗口加摘要的混合策略:最近 3 轮对话保留原文,更早的对话用一个小模型压缩成摘要,摘要长度控制在 200 token 以内。这样既保留了关键信息,又控制了上下文长度。
这里有个实操细节:摘要的生成时机。我试过两种方案,一种是在每轮对话结束后异步生成摘要,另一种是在需要时实时生成。异步方案延迟低但可能摘要不及时,实时方案准确但增加延迟。最终我选择了折中方案:每 5 轮对话触发一次异步摘要,同时保留最近 5 轮的原文,这样即使摘要稍有滞后,也不影响当前对话的连贯性。
3.2 检索增强:向量检索与关键词检索的混合策略
RAG 是企业级 LLM 最常用的模式,但很多人对 RAG 的理解还停留在“把文档切块、存向量库、检索 top-k”这个层面。实际生产中,纯向量检索的问题很明显:它对精确匹配不敏感,比如用户问“XX-1234 型号的参数”,向量检索可能返回一堆语义相似但型号不对的文档。
我的做法是混合检索:向量检索负责语义召回,关键词检索负责精确匹配,然后用一个重排序模型对两路结果做融合。具体参数上,向量检索取 top-20,关键词检索取 top-20,合并去重后大约 30 条,再用重排序模型取 top-5 送入生成模型。这个配置在多个项目里验证下来,召回率和准确率的平衡比较好。
重排序模型的选择也有讲究。早期我用的是基于 BERT 的交叉编码器,效果好但速度慢,30 条文档要跑 200 毫秒。后来换成轻量级的重排序模型,速度降到 50 毫秒以内,效果只下降不到 2 个百分点。对于延迟敏感的场景,这个取舍是值得的。
还有一个容易被忽略的点是文档切块策略。固定长度切块简单但效果一般,因为可能把一段完整的逻辑切碎。我通常采用“语义切块加重叠”的策略:先按段落切,如果段落超过 500 token 再按句子切,相邻块之间保留 50 token 的重叠。这样既保证了块的语义完整性,又避免了边界信息丢失。
3.3 生成控制:参数调优与输出约束
生成环节的参数调优,很多人只关注 temperature,其实还有几个参数对企业级应用同样重要。
temperature 控制随机性,企业场景下我一般设在 0.1 到 0.3 之间。太低了输出死板,太高了输出不可控。对于需要严格格式化的任务,比如 JSON 输出,我会直接设成 0。
top_p 和 temperature 配合使用,通常设 0.9 左右。如果 temperature 已经很低,top_p 的影响就不大了。
max_tokens 要设一个合理的上限,防止模型“刹不住车”生成超长内容。我的经验是设为预期输出的 1.5 倍,比如预期输出 300 token,就设 450。
frequency_penalty 和 presence_penalty 用于控制重复。企业场景下我一般设 0.1 到 0.3,太高了会导致输出不连贯。
输出约束方面,除了在 prompt 里明确要求格式,我还会在服务端做一层校验。如果模型返回的不是合法 JSON,就触发重试或者降级到规则引擎。这个校验层看起来简单,但能挡住 90% 的格式问题。
3.4 后处理与缓存:让系统跑得更快更省
后处理包括几个动作:敏感信息过滤、格式规范化、引用标注、置信度评估。敏感信息过滤用正则加小模型双保险,格式规范化把模型输出转成下游需要的结构,引用标注把检索到的文档 ID 附在回答里方便溯源,置信度评估用一个简单的启发式规则判断回答是否可靠。
缓存是企业级系统降本增效的关键。我通常做三级缓存:第一级是精确匹配缓存,key 是用户 query 的哈希,命中直接返回;第二级是语义缓存,用嵌入向量做相似度匹配,相似度超过 0.95 视为同一问题;第三级是子结果缓存,比如检索结果缓存、摘要缓存。三级缓存配合下来,实际模型调用量能降低 40% 到 60%。
这里有个坑要注意:缓存失效策略。模型更新了、知识库更新了,缓存必须跟着失效。我的做法是给缓存打上版本标签,模型或知识库版本变更时,旧版本缓存自动过期。
4. 实操落地:一个可复现的企业级 LLM 服务搭建过程
4.1 环境准备与依赖选型
假设我们要搭建一个文档问答服务,支持多租户、混合检索、流式输出。技术栈我选择 Python 作为主语言,FastAPI 做 Web 框架,Milvus 做向量库,Redis 做缓存,PostgreSQL 做元数据和日志存储。
为什么选 FastAPI 而不是 Django 或 Flask?FastAPI 的原生异步支持对 LLM 场景很重要,因为模型调用是 IO 密集型操作,异步能显著提升并发能力。而且 FastAPI 的 Pydantic 模型对请求校验和文档生成很友好,省去了很多手写校验的代码。
向量库选 Milvus 是因为它在多租户隔离和水平扩展方面比较成熟,支持按分区隔离数据。如果数据量不大,Qdrant 或 pgvector 也是不错的选择,部署更简单。
依赖管理我用 Poetry,锁版本很重要,LLM 生态的库更新频繁,不锁版本很容易出现“昨天能跑今天报错”的情况。
poetry add fastapi uvicorn milvus-client redis sqlalchemy pydantic poetry add openai tiktoken sentence-transformers4.2 核心配置参数与计算过程
配置参数不是拍脑袋定的,每个数字背后都应该有计算依据。我以并发配置为例说明。
假设我们的服务部署在 4 核 8G 的机器上,模型调用走外部 API,平均延迟 2 秒。那么单进程能支撑的并发数约为 1 / 2 = 0.5 QPS。用 Uvicorn 启动 4 个 worker,理论并发能力是 2 QPS。但实际要考虑 API 的限流和网络抖动,所以我会把目标并发设为 1.5 QPS,留 25% 的余量。
超时设置上,我一般设三层:连接超时 5 秒,读取超时 30 秒,总超时 35 秒。读取超时设 30 秒是因为长回答生成可能需要 20 秒以上,留 10 秒缓冲。
重试策略上,我只对 5xx 错误和超时做重试,重试次数 2 次,退避策略用指数退避,初始 1 秒,倍数 2。不对 4xx 错误重试,因为那通常是请求本身的问题,重试没用。
缓存 TTL 设置上,精确匹配缓存设 1 小时,语义缓存设 30 分钟,检索结果缓存设 15 分钟。TTL 太短起不到降本作用,太长又会导致信息过时。
4.3 关键代码结构与实现要点
服务的主体结构我分成四个模块:API 层、编排层、检索层、模型层。API 层负责请求校验和响应格式化,编排层负责流程控制,检索层负责混合检索和重排序,模型层负责调用和重试。
编排层的核心是一个状态机,根据意图识别的结果决定走哪条链路。比如意图是“文档问答”就走检索加生成,意图是“闲聊”就直接走生成,意图是“数据查询”就走结构化查询加格式化。
检索层的实现要点是并行执行向量检索和关键词检索,然后合并结果。Python 里用 asyncio.gather 可以很方便地实现并行。
async def hybrid_retrieve(query, tenant_id, top_k=5): vector_task = asyncio.create_task(vector_search(query, tenant_id, top_k=20)) keyword_task = asyncio.create_task(keyword_search(query, tenant_id, top_k=20)) vector_results, keyword_results = await asyncio.gather(vector_task, keyword_task) merged = merge_and_deduplicate(vector_results, keyword_results) reranked = rerank(query, merged, top_k=top_k) return reranked模型层的实现要点是封装重试和降级逻辑。主模型超时或报错时,自动切换到备用模型,备用模型也失败时返回兜底话术。
async def generate_with_fallback(prompt, tenant_id): try: return await call_primary_model(prompt, timeout=30) except (TimeoutError, ModelError): try: return await call_backup_model(prompt, timeout=20) except Exception: return FALLBACK_RESPONSE4.4 部署与监控配置
部署我用 Docker Compose 做单机编排,生产环境用 K8s。Docker Compose 的好处是本地开发和测试环境一致,减少“在我机器上能跑”的问题。
监控分三个层面:基础设施监控(CPU、内存、网络)、应用监控(QPS、延迟、错误率)、业务监控(token 消耗、缓存命中率、用户满意度)。基础设施用 Prometheus 加 Grafana,应用监控用 OpenTelemetry,业务监控自己埋点上报。
告警规则我设了四条:P95 延迟超过 5 秒告警,错误率超过 1% 告警,token 消耗超过日预算 80% 告警,缓存命中率低于 30% 告警。这四条覆盖了性能、稳定性、成本、效率四个维度。
日志方面,每次模型调用记录:请求 ID、租户 ID、意图、模型名称、输入 token 数、输出 token 数、延迟、是否命中缓存、是否重试。这些字段足够做后续的分析和优化。
5. 常见问题与排查技巧实录
5.1 模型输出不稳定怎么排查
模型输出不稳定是最常见的问题,表现是同样的输入有时返回正确结果,有时返回错误结果。排查思路是分层定位。
先看是不是 temperature 设太高了。如果 temperature 大于 0.5,先降到 0.2 试试。如果降了还不行,看 prompt 是不是有歧义。我遇到过很多次,prompt 里写了“尽量简洁”,但模型理解成“可以省略关键信息”。这种要改成明确的约束,比如“回答不超过 100 字,必须包含型号和价格”。
如果 prompt 没问题,看检索结果是不是不稳定。混合检索里,向量检索的结果可能因为索引更新而波动。检查一下检索 top-k 的结果是否一致,如果不一致,说明索引有问题。
最后看模型本身。有些模型在特定输入下就是会抽风,这时候要么换模型,要么在 prompt 里加 few-shot 示例。我一般会准备 3 到 5 个示例,覆盖容易出错的场景。
5.2 延迟突然飙升的排查路径
延迟飙升通常有四个原因:模型服务端问题、网络问题、检索变慢、请求量突增。
排查顺序是从外到内。先看模型 API 的响应时间,如果模型端就慢了,那是供应商的问题,只能等或者切备用模型。如果模型端正常,看网络延迟,用 ping 和 traceroute 检查。如果网络正常,看检索耗时,检索变慢通常是索引膨胀或者查询复杂度增加。如果检索也正常,看是不是请求量突增导致排队。
我遇到过一次延迟飙升,排查了半天发现是 Redis 缓存满了,导致缓存命中率骤降,所有请求都打到模型端。后来加了缓存淘汰策略和容量告警,问题就解决了。
5.3 成本失控的常见原因与优化手段
成本失控的原因通常有三个:token 消耗超预期、缓存命中率低、重试次数过多。
token 消耗超预期往往是上下文太长。检查一下是不是把全部历史都塞进去了,或者检索返回的文档太多。优化手段是压缩上下文、减少检索 top-k、用更小的模型做摘要。
缓存命中率低通常是缓存 key 设计不合理。如果 key 包含了时间戳或者随机 ID,那永远命中不了。缓存 key 应该只包含影响输出的因素,比如 query、租户 ID、模型版本。
重试次数过多通常是超时设置太短。如果模型平均延迟 3 秒,你设 2 秒超时,那大部分请求都会超时重试。超时应该设为 P99 延迟的 1.5 倍。
5.4 多租户数据隔离的实操要点
多租户隔离要在四个层面做:向量库分区、缓存 key 前缀、日志租户字段、prompt 租户上下文。
向量库我用 Milvus 的 partition 功能,每个租户一个 partition。查询时指定 partition,这样物理上就隔离了。缓存 key 加租户前缀,比如tenant:{tenant_id}:query:{query_hash}。日志每条都带 tenant_id 字段,方便按租户检索。prompt 里加一句“当前用户属于租户 X”,虽然模型不一定用得上,但能减少跨租户信息泄露的风险。
还有一个细节:嵌入模型的调用也要带租户信息。如果多个租户共用嵌入模型,理论上存在通过嵌入向量反推原文的风险。虽然实际很难,但合规上要求隔离,所以我会给每个租户单独部署嵌入模型,或者至少用不同的模型版本。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决手段 |
|---|---|---|---|
| 输出格式错误 | prompt 约束不明确 | 检查 prompt 是否有明确格式要求 | 加 few-shot 示例,服务端加校验 |
| 延迟突然升高 | 缓存失效或请求突增 | 看缓存命中率和 QPS 曲线 | 扩容,修复缓存 |
| 成本超预算 | 上下文过长或重试过多 | 看 token 消耗分布和重试率 | 压缩上下文,调整超时 |
| 检索结果不相关 | 切块策略或重排序问题 | 人工检查 top-k 结果 | 调整切块,换重排序模型 |
| 多租户数据串扰 | 隔离层面有遗漏 | 检查向量库、缓存、日志 | 补全隔离逻辑 |
| 模型输出重复 | penalty 参数设置不当 | 检查 frequency_penalty | 调到 0.1 到 0.3 |
| 流式输出中断 | 网络或超时问题 | 看客户端和服务端日志 | 调整超时,加心跳 |
6. 一些踩坑之后的个人体会
做企业级 LLM 这几年,我最大的体会是:模型能力只是起点,工程能力才是终点。一个 70 分的模型配 90 分的工程,效果远好于 90 分的模型配 70 分的工程。因为工程决定了系统的稳定性、成本和可维护性,而这些才是企业真正在意的。
另一个体会是,不要追求一步到位。我见过很多团队一开始就想搭一个“全能”的 LLM 平台,结果半年过去了还在做基础设施。我的建议是先跑通一个最小闭环,比如只做文档问答,把检索、生成、缓存、监控这条链路跑顺,然后再逐步扩展场景。每扩展一个场景,复用已有的基础设施,边际成本会越来越低。
最后分享一个小技巧:给每个 prompt 加一个版本号和变更日志。我吃过亏,有一次优化 prompt 后效果变差了,但忘了改了什么,只能从头对比。后来我强制要求每次改 prompt 都记录变更原因和预期效果,出问题时可以快速回滚到上一个版本。这个习惯看起来麻烦,但关键时刻能救命。
这个系列后面我还会继续写,下一篇打算聊聊企业级 Agent 平台的搭建,包括工具调用、多步推理、失败恢复这些话题。如果你正在做类似的事情,欢迎交流踩坑经验。