1. 项目概述:这不是“学AI”或“学全栈”,而是把AI真正焊进业务流水线的实战手册
“AI全栈开发最佳实践”这八个字,最近在技术社区里被刷得发烫,但多数人点进去看到的,要么是教你怎么调用一个大模型API写个Hello World,要么是堆砌一堆高大上的架构图,底下连一行可运行的代码都没有。我干这行十一年,带过二十多个从0到1的AI产品项目,亲手踩过所有能踩的坑——模型训出来跑不进生产环境、前端调用延迟高到用户以为页面卡死、后端服务半夜OOM崩掉、运维同学凌晨三点打电话问我“你那个AI接口到底吃多少内存”,还有更扎心的:业务方说“你们搞了半年AI,怎么客户根本没感知?”
这本不是讲“AI+全栈”的概念拼盘,而是讲怎么把AI能力像水电一样嵌进真实业务系统里,让算法工程师写的模型、前端工程师写的页面、后端工程师写的接口、运维工程师管的服务器,全部咬合在一个稳定、可测、可扩、可追责的工程链条上。核心关键词就三个:AI(不是玩具模型,是能扛住日均50万次调用的推理服务)、全栈(从前端交互逻辑、API网关策略、模型服务编排、向量数据库选型,到GPU资源调度和灰度发布机制)、最佳实践(不是理论最优,是我在电商推荐、金融风控、智能客服三类高并发、强合规、低容错场景里,用真金白银试出来的最小可行路径)。适合谁?如果你正在做AI应用落地,不是在实验室调参,而是在给销售团队上线一个实时话术建议工具;不是在写技术博客,而是在给CTO写一份《AI服务SLA保障方案》;不是在学LangChain文档,而是在解决“用户上传的PDF解析失败率突然升到37%”——那这篇就是为你写的。它不教你从零训练一个大模型,但会告诉你,当线上模型响应P95延迟从800ms跳到2.3秒时,你该先查Prometheus里的哪三个指标,再看哪三行日志,最后动哪一行配置。
2. 内容整体设计与思路拆解:放弃“端到端大一统”,拥抱“分层解耦+契约驱动”
很多团队一上来就想搞“一个平台打天下”:前端Vue+后端Spring Boot+模型服务FastAPI+向量库Milvus+工作流Orchestration,全堆在一个Git仓库里。结果呢?前端改个按钮样式要等模型服务CI跑完,算法同学想换个小版本LoRA要重启整个API网关,运维发现GPU显存泄漏,排查三天发现是前端传了个超长prompt没截断。我们后来在三个项目里彻底推翻这种模式,转向四层解耦架构,每层只对上层暴露清晰契约,对下层隐藏实现细节。
第一层是能力抽象层(Capability Abstraction Layer)。这里不放任何具体技术栈,只定义业务能力契约。比如“智能摘要”能力,契约就三条:输入是{text: string, max_length: number},输出是{summary: string, confidence: number},SLA是P95<1.2秒。算法团队可以用Llama-3-8B微调,也可以用Qwen2-7B量化版,只要满足契约,前端和后端完全无感。我们用OpenAPI 3.1规范写死这个契约,生成TypeScript客户端和Java SDK,连mock server都自动生成。好处是什么?去年Qwen2发布,算法组两天就切过去,前端连build都没重跑。
第二层是服务编排层(Service Orchestration Layer)。这里才是真正的“全栈”战场。我们不用Kubeflow或Airflow这种重型框架,而是用轻量级的Litellm Proxy作为统一入口。为什么选它?不是因为它多新潮,而是它解决了三个致命问题:一是协议转换,上游HTTP/JSON调用,下游可能是OpenAI兼容接口、Ollama本地模型、甚至私有化部署的vLLM服务,Litellm自动做字段映射;二是熔断降级,当某个模型服务超时,它能按预设策略自动切到备用模型(比如主用Qwen2-7B,备选Phi-3-mini),这个切换对前端完全透明;三是审计埋点,所有请求/响应/耗时/token数自动记录到ClickHouse,不用每个服务自己写日志。我们实测过,单节点Litellm Proxy在4核16G机器上,QPS能稳在1200以上,比手写Go网关少维护3个服务。
第三层是模型服务层(Model Serving Layer)。这里坚决反对“一个模型一个服务”。我们按模型类型分组:文本生成类(Qwen、Llama)用vLLM,因为它的PagedAttention能榨干A10显存;多模态类(Qwen-VL、InternVL)用Triton Inference Server,它对CUDA kernel优化更狠;Embedding类(bge-m3、text2vec)直接用Sentence-Transformers + ONNX Runtime,CPU就能跑出2000 QPS。关键点在于资源隔离:每个模型组独占一个K8s namespace,GPU显存配额硬限制,避免一个模型OOM拖垮全家。我们还加了“冷启动预热”机制——每天凌晨用脚本调用各模型一次,把权重预加载进显存,否则早高峰第一个请求要等8秒。
第四层是数据协同层(Data Synergy Layer)。AI全栈最常被忽视的其实是数据流。比如客服对话场景,前端传来的用户消息,要同时喂给意图识别模型、情感分析模型、知识库检索器。如果每个模型自己去查MySQL,DB瞬间被打穿。我们的方案是:所有原始数据(用户输入、上下文、设备信息)先发到Kafka Topic,然后用Flink Job做实时ETL——把文本清洗、敏感词过滤、会话ID关联做完,再分发到不同模型的专用Topic。模型服务只订阅自己的Topic,数据格式、schema变更、上下游解耦全由Flink保证。这套链路在日均2亿条消息的金融场景里跑了14个月,数据丢失率为0。
这个设计的核心逻辑就一条:用契约代替耦合,用事件代替调用,用隔离代替共享。它不追求技术炫技,但让每个角色都能在自己熟悉的领域里高效工作——算法专注模型效果,前端专注交互体验,后端专注API稳定性,运维专注资源水位,大家不再互相甩锅。
3. 核心细节解析与实操要点:从模型加载到前端渲染,每个环节的“魔鬼参数”
光有架构不够,真正决定成败的是那些藏在文档角落、只有踩过坑才懂的参数。我把最关键的六个环节拆开,说透每个参数背后的血泪教训。
3.1 模型加载:别迷信“自动量化”,vLLM的--quantization必须手动选
很多人用vLLM部署Qwen2-7B,直接加--quantization awq,结果发现显存是省了,但首token延迟从350ms飙到1.8秒。原因?AWQ量化对Qwen2的MLP层权重压缩过度,导致KV Cache计算精度崩塌。我们实测了四种量化方式:
| 量化方式 | 显存占用(A10) | P95延迟 | 首token延迟 | 推理质量(BLEU) |
|---|---|---|---|---|
--load-format pt(原生) | 14.2GB | 820ms | 350ms | 92.1 |
--quantization awq | 6.8GB | 1820ms | 1750ms | 84.3 |
--quantization gptq | 7.1GB | 950ms | 420ms | 89.7 |
--quantization fp8(vLLM 0.5+) | 8.3GB | 780ms | 360ms | 91.8 |
结论很明确:Qwen2系列一律用fp8,Llama3用gptq,Phi-3用awq。而且fp8必须配合--kv-cache-dtype fp8,否则显存不降反升。这些参数没写在官网首页,但在vLLM GitHub的issue#4217里,作者亲口承认“fp8对Qwen2的适配是0.5版本最大改进”。
3.2 Litellm Proxy路由:litellm_settings.yaml里藏着服务稳定的命门
Litellm Proxy的路由配置不是简单写个model list。我们在线上环境强制要求三个字段:
model_list: - model_name: qwen2-7b-chat litellm_params: model: "openai/qwen2-7b-chat" api_base: "http://vllm-qwen2:8000/v1" # 关键!防止上游恶意传超长prompt拖垮GPU max_tokens: 2048 # 关键!熔断阈值,连续5次超时就切备用 num_retries: 0 # 交给litellm全局重试 timeout: 30 # 关键!强制启用streaming,避免前端等待整段响应 stream: true最致命的是num_retries: 0。很多人设成3,结果上游服务超时,Litellm自己重试3次,每次30秒,用户等90秒才看到错误。我们改成0,让重试逻辑下沉到前端——前端收到504就自动切到备用模型,用户无感。timeout: 30也必须设,否则vLLM进程卡死,Litellm会无限等待。
3.3 向量数据库选型:Milvus不是万能的,Zilliz Cloud的consistency_level必须调
做RAG时,很多人一上来就上Milvus,结果在高并发更新场景下,搜索结果和插入数据对不上。根源在一致性模型。Milvus默认consistency_level = bounded,意思是“最多延迟5秒”,但客服场景要求“刚插入的知识,下一秒就要能搜到”。我们最终切到Zilliz Cloud,把consistency_level设为strong,代价是写入吞吐降30%,但搜索准确率从82%提到99.2%。参数设置位置在SDK里:
from pymilvus import Collection collection = Collection("faq_kb") # 必须!每次search前显式设置 collection.search( data=[embedding], anns_field="vector", param={"metric_type": "COSINE", "params": {"nprobe": 10}}, limit=3, consistency_level="Strong" # 就是这一行 )3.4 前端Stream处理:别用response.text(),用response.body.getReader()
前端调用AI接口,最常见错误是等整个响应回来再渲染,用户体验极差。正确姿势是用ReadableStream:
const response = await fetch("/api/chat", { method: "POST", body: JSON.stringify({ message: input }) }); const reader = response.body?.getReader(); let buffer = ""; while (true) { const { done, value } = await reader?.read() || { done: true, value: undefined }; if (done) break; // 关键!vLLM返回的是SSE格式,每行以data:开头 const chunk = new TextDecoder().decode(value); buffer += chunk; // 按行解析,避免粘包 const lines = buffer.split('\n'); buffer = lines.pop() || ""; // 最后一行可能不完整,留到下次 for (const line of lines) { if (line.startsWith('data:')) { const data = line.slice(5).trim(); if (data === '[DONE]') continue; try { const parsed = JSON.parse(data); // 这里更新UI,逐字渲染 appendToChat(parsed.choices[0].delta.content || ""); } catch (e) { console.error("Parse SSE error:", e); } } } }这段代码看着复杂,但解决了三个问题:一是避免response.text()阻塞主线程,二是正确处理SSE的data:前缀和[DONE]标记,三是用buffer防粘包。我们实测,开启streaming后,用户感知延迟从平均2.1秒降到0.3秒。
3.5 日志追踪:OpenTelemetry的span.kind必须设为server
AI服务日志混乱的根源,是没区分“谁在调用”和“谁在被调用”。我们强制所有服务(Litellm、vLLM、Flink)都用OpenTelemetry,且span.kind必须设为server。为什么?因为Jaeger里默认把所有span当client处理,导致调用链里看不到vLLM内部的prefill和decode阶段耗时。正确配置:
# vLLM服务中 from opentelemetry import trace from opentelemetry.exporter.jaeger.thrift import JaegerExporter from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor provider = TracerProvider() processor = BatchSpanProcessor(JaegerExporter()) provider.add_span_processor(processor) trace.set_tracer_provider(provider) # 关键!创建span时指定kind tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("vllm.generate", kind=trace.SpanKind.SERVER) as span: # 这里跑实际推理 outputs = llm.generate(prompt, sampling_params)这样在Jaeger里才能看到完整的调用树:frontend -> litellm -> vllm.generate -> vllm.prefill -> vllm.decode,每个环节耗时一目了然。
3.6 灰度发布:用Istio的VirtualService做流量染色,别碰K8s Service
AI模型上线最怕“一刀切”。我们用Istio做灰度,核心是给请求头加x-model-version: qwen2-7b-v2,然后在VirtualService里匹配:
apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: ai-gateway spec: hosts: - "ai.example.com" http: - match: - headers: x-model-version: exact: "qwen2-7b-v2" route: - destination: host: vllm-qwen2-v2 subset: stable weight: 100 - route: - destination: host: vllm-qwen2-v1 subset: stable weight: 100注意两点:一是match必须放在route前面,否则永远走默认路由;二是weight总和必须是100,Istio不支持小数。我们曾因写成weight: 10导致90%流量被丢弃,监控告警响了半小时才发现。
4. 实操过程与核心环节实现:从零搭建一个可上线的AI客服助手
现在把所有细节串起来,带你实操一个真实可用的AI客服助手。目标:用户在网页输入问题,3秒内返回结构化答案(含知识库引用+置信度),支持流式输出,日均承载5万次请求。环境:阿里云ACK集群(3台C7ne,每台A10*1),Zilliz Cloud免费版,前端Vue3。
4.1 环境准备:K8s集群的GPU驱动和vLLM镜像构建
第一步不是写代码,是确保GPU能用。很多团队卡在这一步:kubectl get nodes显示GPU节点,但nvidia-smi在pod里执行报错。原因是NVIDIA Container Toolkit没装。必须在每台worker节点执行:
# 安装nvidia-container-toolkit curl -sL https://nvidia.github.io/nvidia-container-runtime/stable/rpm/nvidia-container-runtime.repo | \ sudo tee /etc/yum.repos.d/nvidia-container-runtime.repo sudo yum install -y nvidia-container-runtime # 配置containerd sudo tee /etc/containerd/config.toml <<EOF version = 2 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia] privileged_without_host_devices = false runtime_type = "io.containerd.runc.v2" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia.options] BinaryName = "/usr/bin/nvidia-container-runtime" EOF sudo systemctl restart containerd然后构建vLLM镜像。别用官方镜像,它没预装我们用的量化库。Dockerfile核心段:
FROM vllm/vllm-cu121:0.5.1 # 安装fp8依赖 RUN pip install --upgrade pip && \ pip install intel-extension-for-pytorch==2.3.0+cpu -f https://download.pytorch.org/whl/torch_stable.html && \ pip install transformers==4.41.2 # 复制我们优化的启动脚本 COPY start_vllm.sh /start_vllm.sh RUN chmod +x /start_vllm.sh CMD ["/start_vllm.sh"]start_vllm.sh内容:
#!/bin/bash # 关键参数!防止OOM export CUDA_VISIBLE_DEVICES=0 export VLLM_ATTENTION_BACKEND=FLASHINFER # 启动命令,fp8量化+动态批处理 python -m vllm.entrypoints.api_server \ --model Qwen/Qwen2-7B-Instruct \ --tensor-parallel-size 1 \ --dtype half \ --quantization fp8 \ --kv-cache-dtype fp8 \ --max-num-seqs 256 \ --max-model-len 8192 \ --port 8000 \ --host 0.0.0.0--max-num-seqs 256是重点,vLLM默认是256,但A10显存只能撑住128,我们实测128是平衡点——再高,P95延迟就上2秒。
4.2 Litellm Proxy部署:YAML文件里的生存指南
litellm-deployment.yaml:
apiVersion: apps/v1 kind: Deployment metadata: name: litellm-proxy spec: replicas: 2 selector: matchLabels: app: litellm-proxy template: metadata: labels: app: litellm-proxy spec: containers: - name: litellm image: ghcr.io/berriai/litellm:latest ports: - containerPort: 4000 env: - name: LITELLM_LOG_LEVEL value: "DEBUG" - name: LITELLM_CONFIG_PATH value: "/app/config.yaml" volumeMounts: - name: config-volume mountPath: /app/config.yaml subPath: litellm_config.yaml volumes: - name: config-volume configMap: name: litellm-config --- apiVersion: v1 kind: ConfigMap metadata: name: litellm-config data: litellm_config.yaml: | model_list: - model_name: qwen2-7b-chat litellm_params: model: "openai/qwen2-7b-chat" api_base: "http://vllm-qwen2:8000/v1" max_tokens: 2048 timeout: 30 stream: true litellm_settings: # 关键!防止恶意请求 drop_params: true # 关键!日志必须进ES success_callback: ["langfuse"] failure_callback: ["langfuse"]注意drop_params: true,它会自动过滤掉model、temperature等非法参数,避免上游传{"model":"gpt-4"}导致Litellm去调用不存在的服务。
4.3 Zilliz知识库初始化:用Flink做实时同步的Python脚本
知识库数据来自MySQL的FAQ表。我们不用Logstash,用Flink Python API写实时同步Job:
from pyflink.datastream import StreamExecutionEnvironment from pyflink.table import StreamTableEnvironment, EnvironmentSettings from pyflink.table.descriptors import Schema, OldCsv, FileSystem, Kafka env = StreamExecutionEnvironment.get_execution_environment() t_env = StreamTableEnvironment.create(env, environment_settings=EnvironmentSettings.in_streaming_mode()) # 从MySQL读取 t_env.connect(FileSystem().path('/data/faq.csv')) \ .with_format(OldCsv().field_delimiter(',').field('id', 'BIGINT').field('question', 'STRING').field('answer', 'STRING')) \ .with_schema(Schema().field('id', 'BIGINT').field('question', 'STRING').field('answer', 'STRING')) \ .create_temporary_table('mysql_faq') # 调用Embedding API(用requests同步调用,Flink里允许) def embed_text(text): resp = requests.post("http://litellm-proxy:4000/embeddings", json={ "model": "bge-m3", "input": [text] }) return resp.json()['data'][0]['embedding'] # 注册UDF t_env.register_function("embed_text", embed_text) # 写入Zilliz t_env.execute_sql(""" INSERT INTO zilliz_faq SELECT id, question, answer, embed_text(question) as vector FROM mysql_faq """) # Zilliz表DDL(在Zilliz Cloud控制台执行) """ CREATE TABLE faq_kb ( id INT64, question VARCHAR(2000), answer VARCHAR(5000), vector FLOAT_VECTOR(1024) ) PARTITION BY RANGE (id) ( PARTITION p0 VALUES LESS THAN (100000), PARTITION p1 VALUES LESS THAN (200000) ); """关键点:FLOAT_VECTOR(1024)必须和bge-m3的输出维度一致,否则插入失败;分区按id范围分,避免单一分区过大。
4.4 前端Vue3集成:Composition API里的流式渲染实战
ChatView.vue核心逻辑:
<script setup> import { ref, onMounted, onUnmounted } from 'vue' const messages = ref([]) const input = ref('') const isStreaming = ref(false) const controller = ref(null) const sendMessage = async () => { if (!input.value.trim()) return messages.value.push({ role: 'user', content: input.value }) isStreaming.value = true input.value = '' // 创建AbortController,支持取消 controller.value = new AbortController() try { const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: messages.value[messages.value.length - 1].content }), signal: controller.value.signal }) if (!response.ok) throw new Error(`HTTP ${response.status}`) const reader = response.body.getReader() let buffer = "" let currentMessage = { role: 'assistant', content: '' } while (true) { const { done, value } = await reader.read() if (done) break const chunk = new TextDecoder().decode(value) buffer += chunk const lines = buffer.split('\n') buffer = lines.pop() || "" for (const line of lines) { if (line.startsWith('data:')) { const data = line.slice(5).trim() if (data === '[DONE]') continue try { const parsed = JSON.parse(data) const delta = parsed.choices?.[0]?.delta?.content || '' currentMessage.content += delta // 强制更新UI,避免Vue批量更新延迟 messages.value = [...messages.value] } catch (e) { console.warn('SSE parse error:', e) } } } } // 添加最终消息 if (currentMessage.content) { messages.value.push(currentMessage) } } catch (err) { if (err.name === 'AbortError') { console.log('Stream cancelled') } else { messages.value.push({ role: 'assistant', content: `❌ 请求失败: ${err.message}` }) } } finally { isStreaming.value = false controller.value = null } } // 取消功能 const cancelStream = () => { if (controller.value) { controller.value.abort() } } onUnmounted(() => { if (controller.value) { controller.value.abort() } }) </script>这里messages.value = [...messages.value]是关键,Vue3的响应式系统对数组push不敏感,必须强制触发更新。
4.5 监控告警:Prometheus的四个黄金指标配置
没有监控的AI服务就是定时炸弹。我们在Prometheus里只盯四个指标:
vLLM GPU显存使用率(防止OOM):
100 - (gpu_memory_free_bytes{container="vllm"} / gpu_memory_total_bytes{container="vllm"}) * 100 > 95告警:连续2分钟>95%,立刻扩容或切流。
Litellm请求成功率(服务健康):
sum(rate(litellm_request_failed_total{model_name=~"qwen.*"}[5m])) by (model_name) / sum(rate(litellm_request_total{model_name=~"qwen.*"}[5m])) by (model_name) > 0.05告警:失败率>5%,检查vLLM是否存活。
Zilliz搜索P95延迟(RAG质量):
histogram_quantile(0.95, sum(rate(zilliz_search_latency_seconds_bucket[5m])) by (le, collection)) > 1.5告警:>1.5秒,检查向量索引是否重建。
前端Stream中断率(用户体验):
sum(rate(frontend_stream_aborted_total[5m])) / sum(rate(frontend_stream_started_total[5m])) > 0.1告警:>10%,说明网络或Litellm不稳定。
所有告警都接入企业微信,值班同学手机响,5分钟内必须响应。
5. 常见问题与排查技巧实录:那些让你凌晨三点爬起来的“幽灵Bug”
再完美的设计也挡不住现实世界的毒打。我把三年来最常遇到的七个问题,按发生频率排序,附上真实排查路径和根治方案。
5.1 问题:vLLM服务突然卡死,nvidia-smi显示GPU 100%但htop里vLLM进程CPU<1%
现象:用户请求全部超时,curl http://vllm:8000/health返回503,但nvidia-smi显示GPU显存占满,htop里vLLM进程CPU占用0.3%。
排查路径:
kubectl logs vllm-pod -c vllm | tail -50→ 发现大量CUDA out of memory但没报错kubectl exec -it vllm-pod -- nvidia-smi -q -d MEMORY | grep "Used"→ 显存确实100%kubectl exec -it vllm-pod -- ls /dev/shm→ 发现/dev/shm目录下有12GB的vllm_cache_*文件
根因:vLLM的PagedAttention缓存默认存在/dev/shm(内存映射),但K8s pod的/dev/shm默认只有64MB,缓存写满后vLLM无法分配新页,卡死。
根治方案:在Deployment里加volumeMounts:
volumeMounts: - name: dshm mountPath: /dev/shm volumes: - name: dshm emptyDir: medium: Memory sizeLimit: 16Gi并启动命令加--block-size 16(减小单页大小)。
5.2 问题:Litellm Proxy返回503 Service Unavailable,但vLLM健康检查正常
现象:Litellm日志里全是Connection refused,但curl http://vllm:8000/health返回200。
排查路径:
kubectl exec -it litellm-pod -- curl -v http://vllm:8000/health→Connection refusedkubectl exec -it litellm-pod -- nslookup vllm→ 解析到ClusterIP,但ping vllm不通kubectl get endpoints vllm→ 发现endpoint为空
根因:vLLM的Service没配selector,或者vLLM Pod的label和Service的selector不匹配。
根治方案:检查vLLM Deployment的spec.template.metadata.labels,必须和Service的spec.selector完全一致。我们曾因Deployment里写app: vllm-qwen2,Service里写app: vllm,导致DNS解析失败。
5.3 问题:Zilliz搜索结果为空,但count_entities显示数据已插入
现象:Flink Job日志显示“insert 1000 rows”,但zilliz_client.query返回空列表。
排查路径:
zilliz_client.get_collection_stats(collection_name="faq_kb")→row_count正确zilliz_client.load_collection(collection_name="faq_kb")→ 执行后仍为空zilliz_client.get_index_info(collection_name="faq_kb")→ 发现index_name为空
根因:Zilliz必须建索引才能搜索,Flink插入后没触发索引构建。
根治方案:在Flink Job最后加一步:
# Flink Job结束后,调用Zilliz API建索引 import requests requests.post("https://YOUR-ENDPOINT.zillizcloud.com/v1/collections/faq_kb/indexes", json={"index_name": "vector_idx", "field_name": "vector", "index_type": "AUTOINDEX"})5.4 问题:前端Stream渲染卡顿,字符逐字出现但中间有1秒停顿
现象:用户输入“你好”,前端先显示“你”,停1秒,再显示“好”,再停1秒,最后显示“,有什么可以帮您?”
排查路径:
- 浏览器Network面板看SSE响应 → 发现每行
data:之间间隔1秒 kubectl logs litellm-pod | grep "stream"→ 发现Litellm日志里streaming字段为false- 检查Litellm配置 →
litellm_config.yaml里漏写了stream: true
根因:Litellm默认不开启streaming,必须显式配置。
根治方案:所有model_list项强制加stream: true,并在CI流程里加YAML校验脚本。
5.5 问题:模型输出乱码,中文变成``或<0x80><0x94>
现象:vLLM返回的JSON里choices[0].message.content包含大量``。
排查路径:
curl http://vllm:8000/v1/chat/completions -H "Content-Type: application/json" -d '{"model":"qwen2","messages":[{"role":"user","content":"你好"}]}'→ 本地curl正常- Litellm日志里看到
response: {"choices":[{"delta":{"content":""}}]}→ 问题在Litellm转发层
根因:Litellm的text-generation-inference后端对UTF-8编码处理有bug,vLLM返回的bytes没正确decode。
根治方案:升级Litellm到1.42.0+,或临时在Litellm配置里加:
litellm_settings: drop_params: true # 强制UTF-8解码 default_headers: Accept: "application/json" Content-Type: "application/json; charset=utf-8"5.6 问题:RAG召回的知识片段和用户问题完全不相关
现象:用户问“退款流程”,返回的知识是“发票开具时间”。
排查路径:
zilliz_client.search单独测试 → 结果正确- 查看Flink同步的日志 → 发现
embed_text("退款流程")返回的向量和embed_text("发票开具时间")余弦相似度0.92 - 检查Embedding模型 → 用的是
bge-m3,但没加query:前缀
根因:bge-m3要求查询文本加query:前缀,文档文本加passage:前缀,否则向量空间不一致。
根治方案:修改Flink UDF:
def embed_text(text, is_query=True): prefix = "query:" if is_query else "passage:" resp = requests.post("http://litellm-proxy:4000/embeddings", json={ "model": "bge-m3", "input": [prefix + text] }) return resp.json()['data'][0]['embedding']搜索时调用embed_text(user_input, is_query=True),插入时调用embed_text(doc_text, is_query=False)。
5.7 问题:Litellm日志爆炸,单Pod每小时写10GB日志
现象:kubectl logs litellm-pod命令卡死,df -h显示/var/log占满。
排查路径:
- `kubectl exec -it litellm-pod -- ls