企业级语言模型自托管优秀实践

360影视 日韩动漫 2025-06-20 17:23 1

摘要:大型语言模型(LLMs)随处可见,从日常应用到高级工具都可以看到他们的身影。虽说使用起来很容易,但如果要运行自己的模型就是另外一回事了。比如对模型进行微调并处理了一些隐私敏感数据,复杂性就会增加。在这篇文章中,我们将分享在构建我们自己的 LLM 推理系统时所学

大型语言模型(LLMs)随处可见,从日常应用到高级工具都可以看到他们的身影。虽说使用起来很容易,但如果要运行自己的模型就是另外一回事了。比如对模型进行微调并处理了一些隐私敏感数据,复杂性就会增加。在这篇文章中,我们将分享在构建我们自己的 LLM 推理系统时所学到的知识。我们将涵盖存储和部署模型、设计服务架构以及解决路由、流式传输和管理微服务等现实问题。这个过程涉及挑战,但最终,我们建立了一个可靠的系统,并获得了值得分享的经验教训。

大型语言模型 (LLMs) 正在为各类应用提供强大支持,覆盖范围从聊天机器人、流程代理到智能自动化工具。尽管检索增强生成(RAG)、工具调用以及多代理协作机制非常重要,但它们都依赖于一个核心引擎:基础 LLM。

许多项目依赖于外部提供商(如 OpenAI、Gemini 或 Anthropic),对于大多数应用场景而言,这已能满足需求。然而,对于某些特定应用,这种方式可能很快会遇到问题。例如:若提供商服务中断该怎么办?若需要完全掌控延迟、定价或系统可用性又该如何应对?最关键的是,若企业重视隐私,无法将用户数据发送给第三方,该如何处理?
此时,自托管的重要性便凸显出来。使用预训练或微调模型进行自托管,不仅能获得完全控制权、提升安全性,还能针对特定业务需求定制模型功能。构建这样的自托管系统并不需要庞大的团队或资源投入。实际上,我们仅依靠有限的预算、一个小型团队和少量服务器节点便成功构建了系统。这种资源限制影响了我们的架构设计思路,迫使我们聚焦于实用性与效率。

在接下来的章节中,我们将详细阐述项目遇到的挑战、所实施的解决方案,以及在此过程中汲取的经验教训。

上文我们提到的自托管系统,下面我们会针对构成系统核心架构的关键组件,给大家做逐一介绍。

格式与编码跨服务采用统一的数据格式至关重要,包括一致的请求 / 响应结构、参数生成规则、对话历史格式,以及从前端到后端再到模型运行器的通用序列化方案。流式处理与路由系统需要高效处理多模型、多请求类型及主机优先级,因此路由策略必须精准。我们将解析用户请求的流转路径 —— 从初始入口到目标工作节点,以及如何返回流式响应。模型存储与部署模型如何存储?如何优化以适应生产环境?推理与验证我们将探讨关键测试流程,确保模型的稳定性和可靠性。可观测性如何判断系统运行状态?我们将介绍核心监控指标、故障排查方法,以及保障系统健壮性的探针机制。数据模式与编码选择高效的数据传输模式是核心任务。统一的跨服务格式能简化集成、减少错误并提升扩展性。我们的目标是让系统无缝兼容自托管模型与第三方服务,同时向用户隐藏底层差异。

当前 LLM 领域的数据交换缺乏统一标准。虽然 OpenAI 的模式被多数服务商采用,但 Claude、Gemini 等平台仍存在关键性差异。部分服务商通过兼容层(如 Anthropic 的 OpenAI 适配 SDK、Gemini 的兼容接口)提供近似支持,但往往存在功能限制。OpenRouter 等项目则尝试统一这些差异,将其封装为标准化接口。

获得经过充分验证的稳定 API。兼容现有成熟的 SDK 和工具链。导致供应商锁定,难以支持多服务商接入。缺乏扩展性,无法灵活适应业务需求或数据科学团队的特殊要求。面临不可控的 API 变更或废弃风险。传统架构约束限制了精细化控制能力。

为此,我们决定建立专属的内部数据模型 —— 这是一个完全根据业务需求设计的架构体系,可灵活映射到各类外部格式。

双向兼容性:能轻松转换为外部服务商所需格式,也能反向转换。功能完整性:全面支持业务需求和数据科学团队的特殊功能。可扩展性:模式设计需预留未来升级空间。消息(包括提示语、对话历史等)。生成参数(如温度值、top_p、最大 token 数等)。

同时,我们发现部分参数(如服务层级、元数据字段、推理模式等)属于各服务商特有的内部配置项。这些非通用元素被归类为可选扩展项。只有当某项功能被广泛采用或对互操作性至关重要时,我们才会考虑将其纳入核心模式。

我们的输入模式主要由四大组件构成:

模型标识:作为路由键,指引系统将请求分发到正确的工作节点。生成参数:核心模型配置(温度值、top_p、max_tokens 等)。消息内容:包含对话历史和提示信息。工具定义:模型可调用的工具说明。

基于以上设计,我们构建了类似 Pydantic 的结构化模式(为简化说明,部分实现细节在此省略)。

复制

class ChatCompletionRequest(BaseModel): model: str # Routing key to select the appropriate model or service messages: list[Message] # Prompt and dialogue history generation_parameters: GenerationParameters # Core generation settings tools: list[Tool] # Optional tool defenitionsclass GenerationParameters(BaseModel): temperature: float top_p: float max_tokens: int beam_search: BeamSearchParams # Optional, non-core fields specific to certain providers provider_extensions: dict[str, Any] = {} ... # Other parameters1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.

我们刻意将生成参数(如温度值、top-p 等模型配置)设计为独立的嵌套字段,而非直接置于根层级。这种架构设计实现了关键区分:

静态参数:固定配置项(模型设置、生成参数等)。动态组件:可变内容(消息内容、工具定义等)。

这种分离设计契合实际需求:多数团队会将这些静态参数存储在外部配置系统中,结构分层既符合工程实践,也便于系统维护。

在GenerationParameters类中,我们特别添加了provider_extensions字段。该字段用于处理各 LLM 服务商特有的差异化参数,其数据验证和解释逻辑交由专门的模型推理模块处理 —— 该模块知晓如何与具体服务商对接。这种设计带来两大优势:

避免因跨服务重复验证导致的冗余耦合。保持核心模式的简洁性与通用性。

为保障向后兼容性,新增功能均通过显式可选字段实现:

这些字段实质上是功能开关,用户必须主动启用才能获得特定行为。核心模式保持稳定,新功能通过渐进式扩展实现。

典型应用:仅当请求中明确启用时,系统才会在输出中包含推理追踪数据。

所有模式定义均封装在共享 Python 库中,确保各服务模块在处理请求和响应时遵循统一规范。

为数据科学团队提供合成数据生成能力,支持原型设计和实验。处理通用任务时,某些现成的专有模型表现更优。适用于隐私要求低、延迟不敏感或无需严格基础设施控制的非核心业务。

与外部服务商的交互流程如下:

专用 LLM-Gateway 服务接收符合内部标准的用户请求。将请求转换为目标服务商特定格式(含 provider_extensions 等扩展字段)外部服务商处理请求并返回响应。LLM-Gateway 将响应转换回标准化内部格式。

LLM 响应采用逐令牌生成方式,并通过数据块(chunks)聚合实现高效传输。为确保终端用户在不同平台(浏览器 / 移动应用 / 终端)上获得流畅体验,必须采用低延迟的实时流式传输机制。

当前主流实现方案有两种:

WebSockets:支持全双工通信,允许客户端与服务器持续双向交互。SSE(服务器发送事件):基于 HTTP 的单向流式传输协议,广泛用于实时更新

我们最终选择 SSE 方案,主要基于以下考量:

技术适配性:SSE 是 LLM 推理场景的主流选择,尤其兼容 OpenAI 等标准 API。实现优势:

a.基于标准 HTTP 协议,无需特殊协商机制。

b.主流浏览器原生支持,无需额外依赖库。

c.天然匹配 LLM 单向输出特性。

d.完美兼容 HTTP 代理等基础设施。

SSE 特别适合文本类即时流式响应场景。但对于需要双向交互的复杂用例(如实时语音转录),WebSockets 仍是更优选择 —— 这也是 OpenAI 实时 API 在服务间通信采用该方案的原因。

鉴于我们系统主要处理文本交互,为保持技术简洁性与兼容性,最终采用与流式模型最匹配的 SSE 方案。

确定传输层后,需要规范流式数据的结构设计。高效的流式传输不仅要传递原始文本,还需包含结构化元数据和上下文信息,以支持客户端 UI 和自动化工具等下游消费场景。流式响应必须包含以下核心要素:

头部元数据包含请求 ID 等基础标识信息。内容块主体

a.核心输出内容(模型生成的 token 或字符串)。

b.采用序列化分块传输(如 n=2、n=4 等并行序列)。

c.每个序列独立生成,通过增量块流式传输。

用量与细粒度元数据包含生成 token 数、时间戳等基础指标,以及可选的诊断信息(如对数概率、推理跟踪),用于计费、调试和模型评估非功能性设计要求在满足基础功能外,流式架构还需保证:结构化:清晰划分内容类型与事件边界。可扩展:支持灵活添加元数据字段,保持向前兼容。健壮性:容错处理格式异常、延迟或数据缺失多序列处理机制在并排比较、多样性采样等场景中,单个生成请求可能包含多个并行序列。参考 OpenAI API 规范:每个数据块的choices数组可包含多个序列更新。即使当前实现中单块通常只含单个增量,设计仍需保留多序列扩展能力。官方 Python SDK 等工具已原生支持此结构。

为保持生态兼容性,我们采用相同结构设计。下图示例展示了包含 3 个序列、分 6 个数据块传输的完整流程:

片段 1—— 生成开始。这个片段标志着整个生成过程的开始。它不包含实际内容,但包含共享的元数据,如生成 ID、时间戳和角色(例如 "助手")。片段 2—— 序列开始(绿色和紫色)。两个序列同时开始流动,每个序列都带有唯一标识符以区分其他序列。片段 3—— 序列开始(蓝色)和序列增量。第三个序列(蓝色)开始,同时前两个序列(绿色和紫色)通过增量事件流式传输新增内容。片段 4—— 中途更新和完成(紫色)。绿色和蓝色序列继续传输增量内容,而紫色序列完成,包含结构化的完成原因(如停止、达到长度等)。片段 5—— 剩余序列完成。绿色和蓝色序列都已完成。每个序列的生命周期现在都有明确的开始和结束标记。片段 6—— 生成完成。这个片段结束整个生成过程,可能包含全局使用统计数据、最终标记计数、延迟信息或其他诊断信息。

为了使数据流更健壮且易于解析,我们选择显式标记整体生成和每个序列的开始与结束事件,而不是依赖隐式机制(如空值检查、EOF 或特殊标记)。这种结构化方法简化了下游解析,特别是在多个结果并行流式传输时,还提升了调试能力和运行时的故障隔离能力。

此外,我们引入了专门的错误片段,包含结构化的故障信息。像格式错误或授权问题这类错误可以通过标准 HTTP 响应代码直接返回。但如果错误发生在生成过程中,我们有两个选择:突然终止 HTTP 流,或发送格式正确的 SSE 错误事件。我们选择后者,因为突然关闭连接会让客户端难以区分是网络问题还是模型 / 服务故障。通过专用错误片段,我们能更可靠地检测和传递流中的问题。

系统的核心是单一入口点 ——LLM-Gateway。它负责处理以下基础功能:身份验证、使用跟踪与配额控制、请求格式化,以及根据指定模型进行路由分发。虽然网关看似承担了多项职责,但每个功能都经过精心设计,保持简单和模块化。

对于外部服务商,网关会将请求适配到它们的 API 规范,并将响应转换回统一格式。对于自托管模型,请求则直接路由到内部系统,使用我们自有的标准化模式。这种设计通过统一的接口,实现了对外部服务和内部模型的无缝支持。

如之前提到的,服务器发送事件 (SSE) 非常适合向终端用户推送流式响应,但不适合内部后端通信。当请求到达系统时,需要将其路由到合适的工作节点进行模型推理,并将结果以流式方式返回。虽然有些系统采用链式 HTTP 代理和基于标头的路由方案,但根据我们的实践经验,随着业务逻辑复杂度提升,这种方案会变得难以维护和扩展。

我们的内部基础设施需要支持以下核心功能:

为满足这些需求,我们采用消息代理来解耦任务路由和结果传递。这种架构设计在不同负载和路由条件下展现出更好的灵活性和健壮性。虽然 RabbitMQ 是我们的实现选择(考虑到其成熟度和与现有工具的兼容性),但根据具体的延迟要求、吞吐量需求和运维偏好,其他消息代理也是可行的替代方案。

下面让我们具体看看这个系统是如何实现的:

我们为每个模型使用专用队列,以便根据模型兼容性和节点能力来路由请求。流程如下:

客户端发送请求LLM-Gateway 服务(表示为用户)发起 HTTP 请求以触发文本生成任务。调度器服务启动一个新的请求处理程序来管理此请求。通过调度器服务进行任务路由调度器处理请求,根据请求的模型选择适当的队列(在图中标记为绿色)并将消息附加到其中。工作节点接收任务合适的推理工作节点(为简化只显示一个,但实际上有很多)订阅队列并接收任务开始处理。该工作节点在本地运行所选模型。流式传输响应工作进程将响应逐个块地流式传输到响应队列,请求处理的调度器副本已订阅该队列。接收响应块调度器监听回复队列,并在响应块到达时接收它们。SSE 流式传输将这些块转换为 SSE 格式,并流式传输到客户端。

在路由和消息发布处理中,每个请求队列都是专门针对单一模型类型的标准 RabbitMQ 队列。要实现优先级感知调度,可以通过消息优先级机制实现 —— 高优先级值的消息会优先于低优先级消息被传递和处理。对于硬件感知路由(需要将消息优先导向性能最优的节点),可采用消费者优先级机制:只要高优先级消费者处于活动状态,就会优先接收消息;仅当高优先级消费者阻塞或不可用时,低优先级消费者才会接收消息。

发布者确认:确保代理已接收并存储消息。持久化设置:启用持久队列和持久消息,保障重启后数据不丢失。仲裁队列:提供更强健的持久性,并支持 RabbitMQ 4.0 + 的简化消息和消费者优先级功能。

关于任务发布已讨论完毕,接下来探讨流式响应的处理方案。首先需要理解 RabbitMQ 临时队列的工作原理。代理支持 "独占队列" 概念,这类队列具有以下特性:

绑定到单一连接。连接关闭时自动删除。完美契合我们的使用场景。

我们为每个调度器服务副本创建独占队列,确保副本关闭时自动清理。但这带来新的挑战:虽然每个服务副本对应一个 RabbitMQ 队列,但需要同时处理大量请求。

解决方案是将 RabbitMQ 队列作为传输层,负责将响应路由到正确的调度器副本。具体实现:

为每个用户请求分配唯一标识符,并嵌入每个响应块。调度器内部维护内存路由层,包含临时内存队列(每个活动请求对应一个)。根据标识符匹配传入块并转发到对应内存队列。请求完成后自动丢弃内存队列,而 RabbitMQ 队列持续到服务副本生命周期结束。

示意图如下:

调度程序内的中央调度程序将块分派到适当的内存队列,每个队列由专用处理程序管理。然后处理程序使用 SSE 协议将块流式传输给用户。

目前有几个成熟的框架可用于高效的 LLM 推理,例如 vLLM 和 SGLANG。这些系统设计用于并行处理多个序列并实时生成响应标记,通常具备连续批处理和 GPU 内存优化等功能。在我们的架构中,我们使用 vLLM 作为核心推理引擎,并进行了以下自定义修改:

自定义波束搜索实现:以更好地适应我们的生成逻辑并支持结构化约束。支持结构化输出模式:允许模型返回符合特定业务格式的输出通过实践,我们发现,即使是微小的库更新也可能显著影响模型行为 - 无论是在输出质量、确定性还是并发性能方面。因此,我们建立了严格的测试流程:压力测试:发现并发问题、内存泄漏或稳定性退化。确定性测试:确保固定种子和参数集下输出一致。参数网格测试:覆盖广泛的生成参数范围但不过度。

大多数现代系统运行在容器化环境中 - 无论是云环境还是 Kubernetes (K8s)。虽然这种配置对典型后端服务很有效,但在模型权重存储方面会带来挑战。LLM 模型大小可能达到数十甚至数百 GB,直接将模型权重嵌入 Docker 镜像会带来问题:

构建缓慢:即使使用多阶段构建和缓存,传输大型模型文件仍会显著增加 CI 时间。部署缓慢:每次部署都需要拉取大型镜像,可能需要几分钟,导致停机或延迟。资源效率低:Docker 注册表和 Kubernetes 节点都没有针对超大镜像优化,导致存储膨胀和带宽紧张。

为解决这个问题,我们将模型存储与 Docker 镜像生命周期分离。我们的模型存储在外部 S3 兼容对象存储中,在推理服务启动前获取。为提高启动速度并避免重复下载,我们还使用本地持久卷 (PVCs) 在每个节点上缓存模型权重。

这样一个基于流处理、消息队列和实时标记生成的系统,需要强大的可观测性来确保规模化时的可靠性和性能。

除了标准服务级别指标 (CPU、内存、错误率等),我们发现以下监控至关重要:

队列深度、消息积压和消费者数量:监控待处理消息数、当前队列大小和活跃消费者数有助于检测任务分发瓶颈和工作负载不均衡。标记 / 块吞吐量:跟踪每秒生成的标记或响应块数有助于识别延迟或吞吐量下降。分布式追踪:准确定位请求在网关、代理、工作节点等组件中的失败或停滞位置。推理引擎健康检查:由于推理过程可能在极少数情况下崩溃 (如错误输入或极端参数),主动监控活跃性和就绪性至关重要。

虽然构建一个可靠且独立于供应商的 LLM 服务系统初看很复杂,但并不需要从头造轮子。每个组件通过 SSE 进行流传输、通过消息代理进行任务分发、由 vLLM 等运行时处理推理 都有明确目的,并基于现有且得到良好支持的工具。通过正确架构,可以创建满足生产需求、可维护且适应性强的系统,而不会引入不必要的复杂性。

来源:51CTO一点号

相关推荐