LLM应用工程实践:从模型选型到FastAPI问答服务
2026/9/16 0:07:33 网站建设 项目流程

在实际的 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,它们主要解决四类问题:

  1. 构造提示模板,统一管理 system、user、assistant 三类消息。
  2. 管理工具调用,例如搜索、数据库查询、HTTP 请求。
  3. 实现 RAG 流程,把文档切块、向量化、检索后拼进 prompt。
  4. 维护多轮对话历史,控制上下文不要无限增长。

但框架不是越多越好。如果核心流程只是“把用户输入转发给模型并把结果返回”,不引入任何框架反而更简单,因为框架版本升级、模型接口变更和依赖冲突都会带来额外成本。

下面是一段不依赖框架的提示模板示例,它展示了“框架层要解决的问题”的最小形态:

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}")

这段代码做了几件关键的事:

  1. 通过load_dotenv()读取.env,避免在代码里硬编码密钥。
  2. 使用 OpenAI SDK 创建客户端,base_url可以从本地环境切换到云端环境。
  3. 请求体用 Pydantic 模型校验,保证prompt不为空,temperature在合法范围内。
  4. 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核采样,保留累计概率范围内的 token0.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_tokenscompletion_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 }'

预期结果应返回一个包含answermodel的 JSON。接着验证带历史的多轮对话,确认系统 prompt 和上一轮消息能被正确拼装。

6.2 响应质量评估

功能跑通后,需要评估输出质量。推荐的评估维度包括:

  1. 准确性:答案是否与问题相关,是否包含明显错误。
  2. 一致性:同一问题多次请求,结果是否在可接受范围内。
  3. 格式规范性:回答中代码块、列表、标点是否正确。
  4. 上下文理解:多轮对话中是否记住了前文信息。

可以准备一组固定测试用例,写入 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 或 403API 密钥错误或权限不足检查.env中密钥是否有效重新生成密钥,确认环境变量已加载
提示 context length exceeded输入加输出超过模型上下文窗口查看请求 token 数启用历史截断,或换更大上下文模型
model not found模型名称与后端不匹配列出后端可用模型修改LLM_MODEL为正确名称
接口超时模型推理慢或请求排队查看推理服务日志降低并发,或换更强 GPU
GPU out of memory显存不足运行nvidia-smi查看显存使用量化模型,或减少并发
返回中文乱码或空字符编码或模型输出内容被截断检查响应内容设置max_tokens更大值,确认请求编码为 UTF-8

7.2 典型排查路径:接口报错从状态码开始

/chat接口返回错误时,按以下顺序排查:

  1. 查看错误状态码。4xx 通常是参数或密钥问题,5xx 通常是服务端或模型问题。
  2. 确认LLM_BASE_URL是否正确。很多本地服务和云端接口路径都以/v1结尾,写错会导致 404。
  3. 调用同一模型的原生命令行或官方示例,排除应用代码问题。
  4. 查看推理服务日志。Ollama 和 vLLM 都提供日志,能够显示模型加载和请求处理过程。
  5. 减小请求复杂度。去掉 history,只传一条 prompt,观察是否能成功。
curl http://localhost:11434/v1/models

上面命令可以列出本地推理服务支持的模型名称,用于确认配置里的模型名是否真实存在。

7.3 环境差异导致的隐藏问题

学习环境很容易跑通,切到生产环境却报错,常见原因有三个:

  1. .env文件未上传到服务器,导致密钥和BASE_URL缺失。
  2. 生产环境的 Python 版本与本地不一致,造成依赖兼容问题。
  3. 防火墙限制,导致生产服务器无法访问云端 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”。这句话放在工程实践里,真正有价值的部分是它提醒我们:模型能力不是终点,围绕模型构建的工程系统才是。把基础链路跑通、把参数吃透、把成本和数据边界算清楚,项目才能真正立得住。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询