在实际的 LLM 应用开发里,真正决定项目成败的往往不是模型跑分差几分,而是“这个模型在具体业务里能不能稳定、便宜、合规地跑起来”。围绕 LLM 的讨论里常出现“Google doesn’t need the LLM crown”这类判断,它提醒我们一件事:一个公司或一个团队的价值,不是靠一个最强模型证明的,而是靠“模型 + 推理基建 + 框架 + 产品场景”的整体组合。这篇文章不从新闻评论的角度出发,而是把它当成一个工程选题:当我们不对着排行榜选型时,LLM 应用应该怎么设计、怎么部署、怎么验证、怎么上线。读者在读完以后,应该能完成一个带上下文的 LLM 问答服务,并掌握本地与云端的取舍逻辑,以及 ComfyUI 这类图像生成工具与 LLM 之间如何正确规划硬件资源。
1. 从“LLM 王冠”说起:应用工程真正要争的不是排行榜
1.1 排行榜衡量的是模型能力,不是系统能力
LLM 是 Large Language Model 的缩写,中文通常叫大语言模型。从形态上看,它是一组经过大规模预训练的神经网络参数,核心任务是根据已经出现的 token 预测下一个 token 的概率。生成一段文本时,模型反复执行“预测下一个 token -> 拿到结果 -> 重新预测”,最终拼出完整回复。这里的“概率预测”决定了 LLM 的输出天生具有不确定性,同样的输入,两次生成可能完全不一样。
公开排行榜通常衡量的是模型在某个测试集上的能力,比如代码生成、数学推理、安全问答。这类评测对研究有价值,但它衡量的是模型的静态能力,不是系统能力。一个可以交付的 LLM 系统,除了模型本身,还包括请求过滤、提示词构造、上下文管理、输出校验、权限控制、缓存、日志和监控。这些部分都不出现在排行榜的分数里。换句话说,即使模型只排在中等位置,只要系统工程做得完整,用户体验和业务落地效果可能远超一个只堆了最强模型的粗糙原型。
1.2 Google 的例子解释了一个工程判断:生态比单点更强
以 Google 在 LLM 领域的布局为例,它能拿出手的不只是某个模型,而是一套完整链路:面向开发者的 API 接口、云端推理基础设施(例如 TPU 和 AI 加速器这类专用硬件)、多年积累的分布式系统经验,以及搜索、办公套件、云平台等大量产品入口。这些组件组合起来,即便某一款模型没有在公开榜单位列第一,开发者和企业照样能在该生态里获得可用能力。
对个人开发者和中小团队来说,这个工程判断是一样的:你不需要成为“最强模型”的使用者,你只需要成为“最适合自己场景的模型”的使用者。判断依据是延迟、成本、数据边界、许可证和可维护性,而不是单纯追求排行榜上的名次。
1.3 三个比跑分更重要的指标
在 LLM 选型阶段,建议先把下面三个指标整理出来,再去看具体模型:
| 指标 | 含义 | 对业务的影响 |
|---|---|---|
| 首 token 延迟 | 从发出请求到收到第一个 token 的时间 | 影响用户等待体验 |
| 单次请求成本 | 按输入输出 token 计费,或按自建资源摊销 | 决定功能能否长期运行 |
| 数据边界 | 输入内容是否离开自己服务器,是否被第三方记录 | 决定能否用于敏感业务 |
例如,一个内部知识库问答系统对数据边界要求很高,可能更适合本地部署小参数量模型;一个面向公开用户的营销文案生成器,更看重生成质量和并发能力,则可以优先考虑云端 API。这些选择都应该基于上述三个指标来做,而不是“因为这个模型跑分更高”。
2. 核心概念先理清:模型、框架和推理服务之间存在三层边界
2.1 模型层:你面对的可以是权重文件,也可以是 API
模型层是能力本体。它可能以权重文件形式存在,也可能隐藏在云 API 后面。对应用工程师来说,模型层真正需要关心的是几个固定参数:上下文窗口大小、单次请求最大 token 数、支持的语言和代码类型、许可证要求,以及部署方式。
举个例子,一个 7B 参数的量化模型在消费级显卡上就能运行,适合本地实验;一个百亿甚至千亿参数模型则需要多卡并行或云端高性能实例。两者在调用代码上的差异不大,差异主要体现在资源规划、推理吞吐和运维复杂度。
2.2 框架层:把提示词、工具调用和记忆组织起来
框架层负责把原始模型能力变成可复用的应用逻辑。常见的 LLM 框架有 LangChain、LlamaIndex、Haystack,它们主要解决四类问题:
- 构造提示模板,统一管理 system、user、assistant 三类消息。
- 管理工具调用,例如搜索、数据库查询、HTTP 请求。
- 实现 RAG 流程,把文档切块、向量化、检索后拼进 prompt。
- 维护多轮对话历史,控制上下文不要无限增长。
但框架不是越多越好。如果核心流程只是“把用户输入转发给模型并把结果返回”,不引入任何框架反而更简单,因为框架版本升级、模型接口变更和依赖冲突都会带来额外成本。
下面是一段不依赖框架的提示模板示例,它展示了“框架层要解决的问题”的最小形态:
def build_messages(system_prompt: str, history: list[dict], user_input: str) -> list[dict]: messages = [{"role": "system", "content": system_prompt}] messages.extend(history) messages.append({"role": "user", "content": user_input}) return messages system_prompt = "你是一个只回答开发问题的技术助手。如果问题与开发无关,请礼貌拒绝回答。" history = [ {"role": "user", "content": "什么是 RAG?"}, {"role": "assistant", "content": "RAG 是检索增强生成,先从外部知识库中找到相关内容,再把这些内容交给模型生成回答。"}, ] messages = build_messages(system_prompt, history, "它有什么缺点?") print(messages)这段代码没有引入任何框架,但它已经把消息结构、角色语义和上下文拼接的基本逻辑写清楚了。实际项目里,如果需求变复杂,再引入框架也不迟。
2.3 推理服务层:模型如何被加载、调度和对外提供 HTTP 接口
推理服务层解决的是“模型以什么形式运行”的问题。本地自建时,常见方案包括 Ollama、vLLM、Text Generation Inference(TGI)。云端使用时,直接调用供应商提供的 HTTP 接口即可。
对应用层开发者来说,推理服务层通常被统一的接口抽象掉。例如,很多本地推理工具提供 OpenAI 兼容接口,意味着你只需要修改base_url,就能把客户端代码从云端切到本地。自建时需要额外关注并发、显存占用、请求排队和故障恢复。
| 推理服务 | 适用场景 | 特点 |
|---|---|---|
| Ollama | 本地快速实验、单机部署 | 安装简单,支持 GPU 和 CPU 混合运行 |
| vLLM | 高并发线上推理 | 吞吐高,支持 PagedAttention,占用显存更高效 |
| TGI | 企业级服务化 | 面向生产环境,支持连续批处理 |
| 云端 API | 无需自建算力 | 有免费额度,按 token 计费,运维成本低 |
把模型、框架、推理服务三层分开思考,选型时就不会把所有问题都归到“哪个模型更强”上面。
3. 架构选型:本地部署和云端 API 的取舍,以及 ComfyUI 与 LLM 是否必须同机
3.1 本地部署适合什么场景
本地部署指把模型权重下载到自己的服务器或电脑上,通过本地推理服务对外提供接口。它的核心优势是数据不出内网、按需使用算力、长期成本可控。
但在实际项目中,本地部署并不轻松。第一,要准备足够的显存,参数越大,显存要求越高;第二,模型的生成质量通常不如同规模商业 API,需要做评估和微调;第三,推理服务的并发能力、GPU 利用率、模型版本升级都成为日常运维工作。
推荐优先考虑本地部署的场景:
- 数据敏感,不允许把文本发送到第三方服务器。
- 需要离线运行,网络环境受限。
- 调用量很大,长期按 token 付费不划算。
- 对模型行为有定制需求,需要微调或换不同权重。
3.2 云端 API 适合什么场景
云端 API 是把模型托管在供应商侧,应用通过 HTTP 接口调用。它的优势是接入快、生成质量高、按调用量付费,不需要自己维护 GPU 服务器。
它的主要风险有两个:一是数据离开自己的网络边界,合规敏感场景不宜使用;二是单位调用量一旦增大,成本会线性上升,可能超过自建资源。
推荐优先考虑云端 API 的场景:
- 快速验证产品可行性,不想一开始就投入大量硬件。
- 需要当前最优质的模型能力。
- 并发波动大,云端自动扩容更省心。
- 团队缺少 GPU 运维经验。
3.3 ComfyUI 与 LLM 是否必须放在同一台电脑上
这是 LLM 应用搭建过程中经常被问到的问题。先看清楚 ComfyUI 是什么:它是一个基于节点的工作流工具,主要用于扩散模型图像生成,例如 Stable Diffusion 系列模型。它的运行负载主要是图像生成过程中的大规模矩阵计算,对显存要求很高;而 LLM 运行负载虽然也是矩阵计算,但更偏向文本 token 的序列推理,两者属于不同类型的工作负载。
结论是:ComfyUI 与 LLM 不需要必须在同一台电脑上。它们之间没有同一主机的强依赖关系。常见的组合方式有三种:
| 组合方式 | 说明 | 适用场景 |
|---|---|---|
| ComfyUI 和 LLM 放同一台机器 | 共用 GPU 资源,节省硬件成本 | 个人电脑、实验环境,负载不高时 |
| 分开两台机器 | 各自独占 GPU,避免显存争抢 | 图像和文本并发需求都很高的生产环境 |
| 图像本机、LLM 用云端 API | 本机跑 ComfyUI,文本推理走 API | 避免为 LLM 额外购置硬件 |
如果你确实想在同一台机器上同时跑 ComfyUI 和本地 LLM,推荐先检查显卡显存。以一张 24GB 显存的显卡为例,同时加载一个 8GB 的扩散模型和一个 7B 量化 LLM 很紧张,容易触发显存溢出。更稳妥的做法是让 ComfyUI 独享 GPU,LLM 调用轻量级 API,或者分别部署到不同实例。
3.4 混合架构的常见形态
混合架构指的是根据请求类型和模型能力,自动选择本地模型或远端 API。例如,简单的文本分类用本地小模型处理,复杂代码生成任务跑到云端大模型。这种架构能兼顾成本和效果,但实现时要注意请求路由和结果回退。
def route_request(user_input: str) -> str: # 简单关键词命中,直接走本地轻量模型 if len(user_input) < 10 and "版本" in user_input: return call_local_model(user_input) # 其他请求走云端大模型 return call_cloud_model(user_input)在系统设计层面,混合架构需要维护两套接口、两套密钥体系和不一致的错误处理逻辑。建议在需求明确之前,先用一条主链路跑通,再考虑智能路由。
4. 最小可运行案例:用 FastAPI 搭一个带上下文的问答服务
4.1 案例目标与约束
这一节的目的是跑通一个最小闭环:客户端传入用户问题和历史对话,服务端负责拼装 system 消息、维护上下文、调用 OpenAI 兼容接口,并返回模型回答。由于各家云服务大多提供 OpenAI 兼容端点,下面示例不绑定具体供应商,你只需要设置模型名称和BASE_URL,就能对接不同服务。
示例代码不包括数据库、权限、限流,也不会处理生产环境的高并发问题。它适合作为学习环境的最小骨架,生产化建议放在后文。
4.2 项目结构和依赖
目录结构建议如下:
llm-qa-demo/ ├── .env ├── requirements.txt ├── main.py └── test_client.py依赖文件requirements.txt内容如下:
fastapi>=0.110.0 uvicorn[standard]>=0.29.0 openai>=1.30.0 python-dotenv>=1.0.0安装依赖:
pip install -r requirements.txt这里使用 OpenAI Python SDK 作为客户端,因为很多本地推理服务和云端 API 都提供 OpenAI 兼容接口,写一遍代码就能适配多个后端。
4.3 配置环境变量
在项目根目录创建.env文件:
LLM_API_KEY=your_api_key_here LLM_BASE_URL=https://your-llm-service.example.com/v1 LLM_MODEL=default-model-name SYSTEM_PROMPT=你是一个严谨的技术助手,回答要简洁、准确,不确定时明确说明。需要注意,BASE_URL在不同服务上差异较大。以本地 Ollama 为例,通常可以设置为http://localhost:11434/v1;如果是云端兼容接口,则按服务商文档填写。原始材料没有给出统一地址,实际开发时务必先看对应文档。
4.4 编写 FastAPI 服务
main.py示例代码如下:
import os from typing import Optional from dotenv import load_dotenv from fastapi import FastAPI, HTTPException from openai import OpenAI from pydantic import BaseModel, Field load_dotenv() app = FastAPI(title="LLM QA Demo") client = OpenAI( api_key=os.getenv("LLM_API_KEY", "not-required"), base_url=os.getenv("LLM_BASE_URL", "http://localhost:11434/v1"), ) MODEL_NAME = os.getenv("LLM_MODEL", "default-model-name") SYSTEM_PROMPT = os.getenv("SYSTEM_PROMPT", "你是一个严谨的技术助手。") class ChatMessage(BaseModel): role: str = Field(..., description="消息角色:system、user 或 assistant") content: str = Field(..., description="消息内容") class ChatRequest(BaseModel): prompt: str = Field(..., min_length=1, description="用户输入") history: list[ChatMessage] = Field(default_factory=list, description="历史消息") temperature: Optional[float] = Field(default=0.7, ge=0.0, le=2.0) class ChatResponse(BaseModel): answer: str model: str @app.post("/chat", response_model=ChatResponse) def chat(request: ChatRequest): messages = [{"role": "system", "content": SYSTEM_PROMPT}] messages.extend( [{"role": item.role, "content": item.content} for item in request.history] ) messages.append({"role": "user", "content": request.prompt}) try: completion = client.chat.completions.create( model=MODEL_NAME, messages=messages, temperature=request.temperature, ) answer = completion.choices[0].message.content return ChatResponse(answer=answer, model=MODEL_NAME) except Exception as exc: raise HTTPException(status_code=502, detail=f"模型调用失败: {exc}")这段代码做了几件关键的事:
- 通过
load_dotenv()读取.env,避免在代码里硬编码密钥。 - 使用 OpenAI SDK 创建客户端,
base_url可以从本地环境切换到云端环境。 - 请求体用 Pydantic 模型校验,保证
prompt不为空,temperature在合法范围内。 history列表允许客户端传入多轮历史,服务端只负责拼装,不负责存储。
4.5 编写客户端测试脚本
test_client.py用来验证服务是否正常:
import requests payload = { "prompt": "请用一句话解释什么是 LLM 框架", "history": [ {"role": "user", "content": "你好"}, {"role": "assistant", "content": "你好,有什么可以帮你?"}, ], "temperature": 0.5, } response = requests.post("http://127.0.0.1:8000/chat", json=payload) print(response.status_code) print(response.json())启动服务:
uvicorn main:app --host 0.0.0.0 --port 8000正常返回示例:
{ "answer": "LLM 框架是用于组织提示词、历史对话和外部工具调用的开发库。", "model": "default-model-name" }如果看到这个结构,说明最小链路已经跑通。
5. 参数、上下文和工程化配置的细节
5.1 核心生成参数速查
模型接口里最常见的几个参数直接决定结果质量。下表给出了参数含义和调整方向:
| 参数 | 作用 | 常见值 | 调整影响 |
|---|---|---|---|
| temperature | 控制随机性 | 0 到 1,代码类任务用 0.2,创意类用 0.8 | 越大越随机,越小越确定 |
| top_p | 核采样,保留累计概率范围内的 token | 0.9 | 与 temperature 二选一交互调整 |
| max_tokens | 限制单次回复最大 token 数 | 按业务自定义 | 过小会截断回答,过大会增加费用 |
| presence_penalty | 让模型更愿意使用新词 | 0 到 1 | 越大越不重复,但也可能更跳跃 |
| frequency_penalty | 惩罚高频重复词 | 0 到 1 | 越大越避免重复,但可能降低流畅度 |
一个常见误区是同时调整 temperature 和 top_p。推荐的做法是先固定一个,再通过实验调整另一个。代码生成场景更推荐temperature=0.2, top_p=0.9;开放对话场景可以尝试temperature=0.8, top_p=0.9。
5.2 上下文窗口和消息历史管理
模型上下文窗口是固定的。比如 8K、32K、128K 指的都是模型能同时处理的输入输出 token 总量。超过窗口上限,请求会直接失败,常见报错是类似于context length exceeded。
管理历史消息有如下几种策略:
| 策略 | 做法 | 适用场景 |
|---|---|---|
| 截断 | 只保留最近 N 条消息 | 代码简单,适合实验 |
| 滑动窗口 | 按 token 数量丢弃最早的对话 | 中等复杂服务 |
| 摘要压缩 | 把旧对话总结成一段摘要 | 长会话场景 |
| RAG 检索 | 只把与当前问题相关的片段拼入 prompt | 知识库问答 |
main.py示例里直接把 history 原样传给模型,这种方式在历史很短时没问题,但生产环境必须控制 token 数量。一种简单写法:
def trim_history(history, max_messages=6): return history[-max_messages:]5.3 成本控制与 token 统计
调用云端 API 时,费用按输入 token 和输出 token 分别计费。同一个模型,长文档分析任务费用高,因为输入内容多;代码补全任务费用相对低,因为输出通常较短。
推荐在服务端记录每次请求的prompt_tokens和completion_tokens。OpenAI 兼容接口返回的 completion 对象里一般包含usage字段:
usage = completion.usage print(f"输入 token: {usage.prompt_tokens}, 输出 token: {usage.completion_tokens}")把 token 使用量写入日志或监控指标,可以帮助后续优化提示词长度和缓存策略。
6. 运行验证与评估:功能跑通只是第一步,还要检查响应质量和资源占用
6.1 接口功能验证
启动服务后,先用最小请求验证基本功能。
curl -X POST http://127.0.0.1:8000/chat \ -H "Content-Type: application/json" \ -d '{ "prompt": "Python 里如何读取环境变量?", "history": [], "temperature": 0.3 }'预期结果应返回一个包含answer和model的 JSON。接着验证带历史的多轮对话,确认系统 prompt 和上一轮消息能被正确拼装。
6.2 响应质量评估
功能跑通后,需要评估输出质量。推荐的评估维度包括:
- 准确性:答案是否与问题相关,是否包含明显错误。
- 一致性:同一问题多次请求,结果是否在可接受范围内。
- 格式规范性:回答中代码块、列表、标点是否正确。
- 上下文理解:多轮对话中是否记住了前文信息。
可以准备一组固定测试用例,写入 JSON 文件后批量执行:
{ "questions": [ "什么是 RAG?", "使用 Python 有哪些常见坑?", "如何降低 LLM 推理成本?" ], "history": [] }评估时不要只看一两条回复。LLM 输出具有随机性,至少每个用例执行 3 次,观察稳定性。如果结果差异太大,就要降低 temperature 或调整提示词。
6.3 资源占用检查
如果用的是本地推理服务,必须关注 GPU 和内存占用。常用命令:
nvidia-smi free -h重点检查显存使用率、GPU 利用率和内存 swap 情况。如果显存接近上限,说明模型规模或并发请求超出机器容量。如果 GPU 利用率长期低于 5%,说明请求量小,本地部署过于浪费资源,可以考虑切到云端 API。
7. 常见问题排查:从报错日志倒推原因
7.1 错误现象与处理方案速查
| 现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 返回 401 或 403 | API 密钥错误或权限不足 | 检查.env中密钥是否有效 | 重新生成密钥,确认环境变量已加载 |
| 提示 context length exceeded | 输入加输出超过模型上下文窗口 | 查看请求 token 数 | 启用历史截断,或换更大上下文模型 |
| model not found | 模型名称与后端不匹配 | 列出后端可用模型 | 修改LLM_MODEL为正确名称 |
| 接口超时 | 模型推理慢或请求排队 | 查看推理服务日志 | 降低并发,或换更强 GPU |
| GPU out of memory | 显存不足 | 运行nvidia-smi查看显存 | 使用量化模型,或减少并发 |
| 返回中文乱码或空字符 | 编码或模型输出内容被截断 | 检查响应内容 | 设置max_tokens更大值,确认请求编码为 UTF-8 |
7.2 典型排查路径:接口报错从状态码开始
当/chat接口返回错误时,按以下顺序排查:
- 查看错误状态码。4xx 通常是参数或密钥问题,5xx 通常是服务端或模型问题。
- 确认
LLM_BASE_URL是否正确。很多本地服务和云端接口路径都以/v1结尾,写错会导致 404。 - 调用同一模型的原生命令行或官方示例,排除应用代码问题。
- 查看推理服务日志。Ollama 和 vLLM 都提供日志,能够显示模型加载和请求处理过程。
- 减小请求复杂度。去掉 history,只传一条 prompt,观察是否能成功。
curl http://localhost:11434/v1/models上面命令可以列出本地推理服务支持的模型名称,用于确认配置里的模型名是否真实存在。
7.3 环境差异导致的隐藏问题
学习环境很容易跑通,切到生产环境却报错,常见原因有三个:
.env文件未上传到服务器,导致密钥和BASE_URL缺失。- 生产环境的 Python 版本与本地不一致,造成依赖兼容问题。
- 防火墙限制,导致生产服务器无法访问云端 API 域名。
推荐在项目根目录准备一个config.example.env模板,部署时复制为.env再填写真实值。同时使用 requirements 或 lock 文件锁定依赖版本。
8. 生产化建议与下一步扩展方向
8.1 从实验代码到生产服务的最小检查清单
上面的 FastAPI 示例只是骨架,生产环境至少还需要补全以下项:
- 配置外置化:密钥、模型名称、超时时间都从环境变量读取,不写入代码库。
- 日志与追踪:记录每次请求的输入摘要、输出 token 数、耗时和状态码。
- 错误处理:对模型超时、限流、无效参数做分类,返回可读的错误信息。
- 限流与配额:防止单个用户刷爆 token 预算。
- 内容安全:增加输入输出过滤,至少过滤明显违规内容和敏感关键词。
- 版本管理:固定模型版本或记录模型快照,避免模型升级后结果行为变化。
- 回滚方案:当新模型表现异常时,能快速切回旧模型或旧版本代码。
8.2 面向业务场景的三条扩展路线
第一条路线是 RAG 知识库问答。在现有服务基础上加入文档切分、向量化和检索模块,让模型基于本地文档回答问题。这时引入 LangChain 或 LlamaIndex 会比较有价值,因为检索链路本身需要编排。
第二条路线是工具调用与智能体。让模型能够根据用户意图调用外部接口,例如查询订单、查天气、执行搜索。实现时需要注意工具参数校验和调用结果反馈,避免模型编造参数。
第三条路线是评估与微调。收集一批真实业务样本,建立自动化评估集,对比不同模型和提示词的效果。当开源模型的评估结果不足以支撑业务时,再考虑微调或换用更大模型。
8.3 对新手最重要的一个练习建议
不要一开始就同时部署多个模型,也不要从复杂框架入手。建议先用一个最容易跑通的小模型或云 API,完成一个不超过三个接口的最小应用,记录延迟、成本和输出质量。当你对模型的调用方式、参数行为和错误链路都熟悉以后,再去研究框架、RAG 和部署优化。这样积累起来的技术判断,比单纯追求“最强模型”可靠得多。
回到开头那句“Google doesn’t need the LLM crown”。这句话放在工程实践里,真正有价值的部分是它提醒我们:模型能力不是终点,围绕模型构建的工程系统才是。把基础链路跑通、把参数吃透、把成本和数据边界算清楚,项目才能真正立得住。