Qwen智能体实战:从零搭建可落地的文章阅读工具

Qwen智能体实战:从零搭建可落地的文章阅读工具 1. 项目概述这不是又一个“调API”的教程而是一次真实可用的智能体落地实践你点开这个标题大概率是被“从零开始”“新手也能轻松上手”这几个字吸引来的。但我想先说清楚这确实是一份面向零基础 Python 学习者的实操指南但它不教你怎么复制粘贴几行代码跑通 demo而是带你亲手搭出一个能真正读文章、理解段落逻辑、提取关键信息、还能按你要求格式输出结果的 Qwen 智能体——它不是玩具是你可以明天就塞进工作流里用的工具。核心关键词“Qwen”“智能体”“Python”“API”不是随便堆砌的标签。Qwen 是通义千问系列模型尤其 Qwen2-7B-Instruct 和 Qwen2.5-7B-Instruct 这两个版本在中文长文本理解、指令遵循和结构化输出上表现稳定社区支持成熟本地部署门槛相对可控“智能体”在这里不是玄学概念它指代一个具备明确角色比如“专业文章摘要员”、拥有记忆能记住你刚上传的PDF内容、能调用工具比如解析PDF、调用向量库检索、并能自主规划执行步骤的程序实体而 Python 和 API则是我们把这一切串起来的骨架与神经——不用写 C不用碰 CUDA 编译用 pip 装几个包写几十行逻辑清晰的脚本就能让大模型为你干活。我带过不少刚转行做 AI 工具开发的朋友他们最大的卡点从来不是“模型多大”“参数多少”而是不知道第一步该装什么、第二步该改哪行配置、第三步报错时该看哪条日志。这篇指南就是为解决这些具体问题写的。它覆盖了从环境初始化、模型加载、Prompt 工程设计、文档解析接入、到最终封装成可交互 CLI 或 Web 接口的完整链路。所有命令都经过 Ubuntu 22.04 Python 3.10 NVIDIA A10G16GB显存实测也兼容 macOS M2/M3CPU 推理模式和 Windows WSL2 环境。如果你连pip install都没敲过别慌——我会告诉你python --version输出不对时怎么修 PATH如果你已经会写爬虫那你会惊喜地发现智能体的“工具调用”模块本质上就是你熟悉的 requests BeautifulSoup 的升级版。这不是一篇教你“成为大模型科学家”的文章而是一份“如何让大模型成为你最听话的实习生”的操作手册。接下来的内容每一节都对应一个你马上会遇到的真实场景为什么选 vLLM 而不是 Ollama为什么 PDF 解析不能只靠 PyPDF2Prompt 里加一句“请用中文回答”真的有用吗API 返回 token 超限错误到底是模型限制还是你 prompt 写太啰嗦这些答案不会藏在论文附录里就写在下面每一段实操记录中。2. 整体架构设计与技术选型逻辑为什么这样搭而不是那样搭2.1 智能体不是“模型前端”而是三层协同的工作流很多初学者一上来就想做个带聊天界面的网页结果卡在 CORS、WebSocket 断连、模型加载白屏上最后放弃。其实一个真正能处理文章阅读任务的智能体本质是三个层次的协作底层模型推理引擎—— 负责把你的文字输入变成高质量的文本输出。它不关心你是从网页输的、CLI 输的还是从微信发来的。它的唯一职责是快、准、稳。中层智能体框架Agent Framework—— 负责“动脑子”。它决定用户传来的是一篇 PDF 还是 Markdown要不要先调用解析工具提取的关键词要不要去向量库查相似文献当前回答是否已超 2000 字要不要自动分段这一层是智能体的“操作系统”决定了它有没有“思考能力”。上层交互接口Interface—— 负责“打交道”。可以是命令行python reader.py --file report.pdf --summary可以是 FastAPI 接口POST /api/summarize也可以是 Gradio 界面。它只管收和发不管想。我们这次搭建采用“中层主导、上下解耦”的思路先确保中层智能体逻辑跑通再快速套上 CLI 和 Web 两套接口。这样即使你后期想对接钉钉机器人或飞书多维表格只需替换上层中层完全复用。2.2 为什么首选 vLLM 而非 Ollama、llama.cpp 或 Transformers 原生加载这是新手最容易纠结的问题。网上教程五花八门有人说 Ollama 最简单ollama run qwen2:7b一行搞定有人说 llama.cpp 在 Mac 上丝滑还有人坚持用 HuggingFace 的pipeline。但回到“文章阅读”这个具体任务我们必须看硬指标方案吞吐量tokens/s显存占用Qwen2-7B支持 Streaming支持 PagedAttention中文长文本优化Transformers generate()~1814.2 GB✅需手动实现❌⚠️需手动管理 kv cacheOllama~2213.8 GB✅✅⚠️默认配置未针对中文长文本调优llama.cppGPU offload~2612.5 GB❌仅 CPU 模式支持流式❌✅量化后对中文词元更友好vLLM推荐~4111.6 GB✅✅✅社区有 Qwen 专用适配 patch数据来源我们在同一台 A10G 服务器上用 4K 上下文长度、batch_size4 的标准测试集含 10 篇中文科技报告 PDF 解析后文本实测得出。vLLM 的优势不是“快一点”而是在同等显存下它能把并发请求数翻倍且响应延迟更稳定——这对智能体至关重要。因为智能体在执行“解析PDF→提取摘要→生成要点→格式化输出”这一连串动作时往往需要多次调用模型。如果每次调用都要等 3 秒整个流程就会卡顿。而 vLLM 的 PagedAttention 技术让多个请求共享显存中的 key/value cache极大降低重复计算。更重要的是vLLM 官方已原生支持 Qwen2 系列模型的rope_theta和max_position_embeddings自动识别无需像 Transformers 那样手动修改 config.json。我们实测过直接运行python -m vllm.entrypoints.api_server \ --model Qwen/Qwen2-7B-Instruct \ --tensor-parallel-size 1 \ --dtype bfloat16 \ --max-model-len 8192 \ --port 8000服务就能正常启动且curl http://localhost:8000/v1/completions返回的usage.prompt_tokens和completion_tokens统计精准这对后续做 token 成本核算、防超限保护非常关键。提示不要被“vLLM 需要编译”吓退。它提供预编译 wheel 包pip install vllm在主流 Linux 发行版上 95% 场景可直接成功。若报错ninja not found只需pip install ninja即可无需手动装 CMake 或 GCC。2.3 为什么绕过 Dify/Coze/扣子等平台坚持手写智能体框架Dify、Coze 这类平台确实“上手快”拖拽几个节点就能出效果。但它们对“文章阅读”这类深度定制任务存在三个硬伤PDF 解析黑盒化Dify 默认用 Unstructured.io它对扫描版 PDF哪怕带 OCR 文字层识别率极低且无法指定页码范围比如“只读第 5–8 页”。而我们手写框架可自由切换pymupdf速度快、pdfplumber表格保留好、unstructured语义分块强三套解析器并根据文件类型自动路由。Prompt 控制粒度粗平台里 Prompt 编辑器是富文本框你没法动态插入变量如“用户上次提问时间{{last_time}}”也没法在不同步骤间传递中间状态如“已提取的 3 个核心论点”。而手写框架中我们用 Jinja2 模板引擎{{doc.title}}、{{sections|first}}、{{user_profile.tone}}全部可编程控制。调试成本高在 Dify 里看到输出错误你得反复点“重试”看日志面板里滚动的 JSON根本不知道是 Prompt 写错了还是工具函数返回了空数组。而手写框架加一行print(fStep 2 result: {result})立刻定位问题环节。这不是反对低代码而是强调当你需要精确控制每一个 token 的来龙去脉时手写就是最短路径。我们后面会展示一个包含“文档解析→语义分块→向量检索→多跳推理→格式化输出”的完整智能体主循环核心逻辑代码仅 127 行远比学习平台私有 DSL 更高效。2.4 API 设计原则不是暴露模型而是暴露能力很多教程教你怎么用requests.post(http://localhost:8000/v1/chat/completions)直接调模型。这等于把菜刀递给人却不说切菜还是砍骨头。我们的 API 设计遵循“能力即接口”原则POST /api/parse只做一件事——把 PDF/DOCX/Markdown 转成结构化 JSON含 title、author、sections[]、references[]POST /api/summarize接收解析后的 JSON返回带层级的摘要Executive Summary 3 Key Points Technical DetailsPOST /api/qna接收文档 ID 和自然语言问题返回引用原文段落的答案带 page_num 和 text_snippet每个接口都有明确的输入 Schema用 Pydantic 定义和输出契约。例如/api/summarize的请求体必须包含{ doc_id: report_2024_q2, max_length: 500, include_references: true, tone: executive }而不是让用户自己拼system_prompt和user_message。这种设计让前端、移动端、甚至 Excel VBA 宏都能安全调用无需理解大模型原理。3. 核心细节解析与实操要点从环境初始化到第一个可运行智能体3.1 环境初始化避开 Python 版本与依赖冲突的深坑别跳过这一步。90% 的“安装失败”源于环境混乱。我们采用pyenv poetry组合而非系统 Python 或 conda原因很实在pyenv可在同一台机器共存 Python 3.9/3.10/3.11避免apt install python3.10-dev污染系统poetry锁定依赖版本poetry.lock文件确保你在公司服务器、个人 Mac、同事笔记本上poetry install后得到完全一致的包集合。实操步骤Ubuntu 22.04 示例安装 pyenvcurl https://pyenv.run | bash export PYENV_ROOT$HOME/.pyenv export PATH$PYENV_ROOT/bin:$PATH eval $(pyenv init -)安装 Python 3.10.12Qwen2 官方测试版本pyenv install 3.10.12 pyenv global 3.10.12 python --version # 应输出 3.10.12安装 poetrycurl -sSL https://install.python-poetry.org | python3 - export PATH$HOME/.local/bin:$PATH初始化项目并创建虚拟环境mkdir qwen-reader cd qwen-reader poetry init -n poetry env use 3.10.12注意poetry env use必须在poetry init后立即执行否则 poetry 会默认用系统 Python 创建环境导致后续torch安装失败。我们踩过这个坑——某次在 WSL2 上因未指定版本poetry 自动用了 Python 3.8结果pip install torch下载的是 CPU-only 版本GPU 加速彻底失效。3.2 模型下载与验证本地化不是为了“离线”而是为了“可控”Qwen2-7B-Instruct 的 HuggingFace 模型卡 Qwen/Qwen2-7B-Instruct 约 4.2GB。直接git lfs clone很慢且容易中断。我们用huggingface-hub库的断点续传功能pip install huggingface-hub python -c from huggingface_hub import snapshot_download snapshot_download( repo_idQwen/Qwen2-7B-Instruct, local_dir./models/qwen2-7b-instruct, revisionmain, max_workers3 ) 下载完成后务必验证模型完整性ls -lh models/qwen2-7b-instruct/ # 应看到 pytorch_model-00001-of-00002.bin (2.1GB) 和 ...00002.bin (2.1GB)以及 config.json, tokenizer.model 等更关键的是验证 tokenizer 是否能正确处理中文from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(./models/qwen2-7b-instruct) text 人工智能正在改变世界格局。 tokens tokenizer.encode(text) print(f原文: {text}) print(fToken IDs: {tokens}) print(f解码回: {tokenizer.decode(tokens)}) # 正确输出应为原文完全一致且 tokens 长度为 12非乱码或截断实操心得如果tokenizer.decode()返回乱码如▁人 ▁工 ▁智 ▁能...说明 tokenizer 加载路径错误可能指向了Qwen/Qwen1.5-7B的旧版 tokenizer。务必确认from_pretrained()的路径与模型文件夹完全匹配。3.3 文档解析模块PDF 不是文本而是带结构的“数据源”“文章阅读”的第一道坎永远是解析。网上大量教程用PyPDF2但它对中文 PDF 支持极差——字体嵌入缺失、编码映射错误、表格变乱码。我们采用三引擎策略按文件类型自动选择扫描版 PDF无文字层→pymupdfeasyocrOCR 识别印刷版 PDF有文字层→pdfplumber保留表格、字体、位置信息DOCX/Markdown→python-docx/markdown-it-py核心代码parser.pyimport fitz # pymupdf import pdfplumber from docx import Document import re def parse_document(file_path: str) - dict: if file_path.endswith(.pdf): return _parse_pdf(file_path) elif file_path.endswith(.docx): return _parse_docx(file_path) elif file_path.endswith(.md): return _parse_markdown(file_path) else: raise ValueError(fUnsupported format: {file_path}) def _parse_pdf(file_path: str) - dict: # 先尝试 pdfplumber快且保结构 try: with pdfplumber.open(file_path) as pdf: pages [] for page in pdf.pages: # 提取文本 表格 text page.extract_text() or tables page.extract_tables() pages.append({text: text, tables: tables, page_num: page.page_number}) return {title: _extract_title(pages[0][text]), pages: pages} except Exception as e: # pdfplumber 失败降级为 pymupdf OCR print(fpdfplumber failed: {e}, falling back to pymupdfOCR) return _parse_pdf_with_ocr(file_path) def _extract_title(text: str) - str: # 用正则提取首行大写字母中文标题 lines text.strip().split(\n) for line in lines[:3]: if len(line) 5 and re.search(r[A-Z\u4e00-\u9fff], line): return line.strip() return Untitled Document注意事项pdfplumber对加密 PDF 会直接抛异常需提前用pymupdf解密doc fitz.open(file_path) if doc.is_encrypted: doc.authenticate(password) # 若有密码我们把这个逻辑封装进_parse_pdf开头避免下游报错中断。3.4 Prompt 工程实战不是“写得漂亮”而是“让模型少犯错”很多人以为 Prompt 就是写一段话。但在 Qwen2 上一个生产级 Prompt 必须包含四个强制区块Role Definition角色定义明确告诉模型“你是谁”而非“你要做什么”。例如你是一位资深科技期刊编辑专注人工智能领域。你精通中英文文献擅长从复杂技术报告中提炼核心贡献与局限性。Context Constraints上下文约束限定输入范围防止幻觉。例如以下是你将要分析的文档内容严格限定在此范围内不得编造任何未提及的信息 document {{document_text}} /documentOutput Schema输出规范用 JSON Schema 强约束格式而非“请用三点列出”。例如请严格按照以下 JSON Schema 输出不得添加额外字段或解释 { summary: 不超过300字的总体摘要, key_points: [ {point: 要点1, evidence: 直接引用原文句子带页码}, {point: 要点2, evidence: 直接引用原文句子带页码} ], limitations: 文档中明确指出的技术局限性若无则填null }Failure Guard失败防护预设兜底逻辑。例如如果文档内容为空、或无法识别有效文本请输出{error: DOCUMENT_EMPTY, suggestion: 请检查文件是否损坏或为纯图片格式}我们把这四部分写成 Jinja2 模板prompts/summarize.j2调用时动态渲染from jinja2 import Environment, FileSystemLoader env Environment(loaderFileSystemLoader(prompts)) template env.get_template(summarize.j2) prompt template.render(document_textcleaned_text)实测对比用纯自然语言 Prompt无 Schema 约束Qwen2-7B 在 100 次摘要任务中JSON 格式错误率 37%加入严格 Schema 后错误率降至 1.2%且evidence字段 100% 引用原文无幻觉。4. 实操过程与核心环节实现从 CLI 到 Web一个智能体的完整诞生4.1 构建第一个 CLI 工具qwen-read命令行阅读器目标运行qwen-read --file report.pdf --mode summary直接在终端输出结构化摘要。步骤 1创建入口脚本cli.pyimport argparse import json from parser import parse_document from agent import QwenReaderAgent def main(): parser argparse.ArgumentParser(descriptionQwen 文章阅读智能体 CLI) parser.add_argument(--file, requiredTrue, help输入文件路径 (PDF/DOCX/MD)) parser.add_argument(--mode, choices[parse, summary, qna], defaultsummary) parser.add_argument(--question, help当 modeqna 时指定问题) args parser.parse_args() # 解析文档 doc_data parse_document(args.file) # 初始化智能体连接本地 vLLM API agent QwenReaderAgent(base_urlhttp://localhost:8000/v1) if args.mode parse: print(json.dumps(doc_data, ensure_asciiFalse, indent2)) elif args.mode summary: result agent.summarize(doc_data) print(json.dumps(result, ensure_asciiFalse, indent2)) elif args.mode qna and args.question: result agent.qna(doc_data, args.question) print(json.dumps(result, ensure_asciiFalse, indent2)) if __name__ __main__: main()步骤 2实现智能体核心agent.pyimport requests import json from jinja2 import Environment, FileSystemLoader class QwenReaderAgent: def __init__(self, base_url: str): self.base_url base_url self.session requests.Session() self.session.headers.update({Content-Type: application/json}) self.env Environment(loaderFileSystemLoader(prompts)) def summarize(self, doc_data: dict) - dict: # 渲染 Prompt template self.env.get_template(summarize.j2) prompt template.render(document_textself._flatten_pages(doc_data[pages])) # 调用 vLLM API payload { model: Qwen/Qwen2-7B-Instruct, prompt: prompt, max_tokens: 1024, temperature: 0.3, stop: [|endoftext|, |im_end|] } try: resp self.session.post(f{self.base_url}/completions, jsonpayload, timeout120) resp.raise_for_status() output resp.json()[choices][0][text].strip() # 解析 JSON 输出带容错 return self._safe_json_parse(output) except requests.exceptions.Timeout: return {error: TIMEOUT, message: 模型响应超时请检查 vLLM 服务状态} except json.JSONDecodeError as e: return {error: PARSE_ERROR, raw_output: output, hint: 模型未按 JSON Schema 输出请检查 Prompt} def _flatten_pages(self, pages: list) - str: # 合并所有页面文本插入页码标记 full_text for page in pages: full_text f\n--- Page {page[page_num]} ---\n{page[text]}\n return full_text def _safe_json_parse(self, text: str) - dict: # 尝试从文本中提取 JSON 块应对模型偶尔在 JSON 前加解释 match re.search(r\{.*\}, text, re.DOTALL) if match: try: return json.loads(match.group(0)) except json.JSONDecodeError: pass return {error: INVALID_JSON, raw: text}步骤 3安装为可执行命令在pyproject.toml中添加[project.scripts] qwen-read cli:main然后运行poetry install qwen-read --file ./test/report.pdf --mode summary实操心得第一次运行时vLLM 服务会加载模型约 45 秒CLI 会卡住。这不是错误是正常现象。建议在cli.py中加一行提示“正在加载模型请稍候...”。另外--max_tokens参数必须小于 vLLM 启动时的--max-model-len否则会返回context window limit错误。我们设为 1024留出 7168 tokens 给 prompt足够处理 8K 上下文文档。4.2 封装为 Web APIFastAPI vLLM 的轻量级服务CLI 好用但团队协作需要 Web 接口。我们用 FastAPI因为它自动生成 OpenAPI 文档/docs页面可直接测试内置 Pydantic 验证自动校验输入格式异步支持好async def可挂起等待 vLLM 响应app.py核心代码from fastapi import FastAPI, HTTPException, BackgroundTasks from pydantic import BaseModel from typing import Optional, List, Dict, Any import asyncio import requests app FastAPI(titleQwen Reader API, version1.0) class ParseRequest(BaseModel): file_content: str # Base64 编码的文件内容 file_name: str class SummarizeRequest(BaseModel): doc_id: str max_length: int 500 tone: str professional # executive, technical, casual class QnARequest(BaseModel): doc_id: str question: str app.post(/api/parse) async def parse_document(req: ParseRequest): # 这里可集成 MinIO/S3 存储当前简化为内存处理 # 实际生产环境应保存文件到磁盘返回 doc_id return {doc_id: temp_ req.file_name.split(.)[0], status: parsed} app.post(/api/summarize) async def summarize_document(req: SummarizeRequest): # 模拟从数据库获取 doc_data doc_data { title: 2024 Q2 AI Infrastructure Report, pages: [{text: The rise of vLLM has accelerated..., page_num: 1}] } # 调用本地 vLLM try: async with asyncio.timeout(120): loop asyncio.get_event_loop() # 使用线程池避免阻塞事件循环 with concurrent.futures.ThreadPoolExecutor() as pool: result await loop.run_in_executor( pool, lambda: requests.post( http://localhost:8000/v1/completions, json{ model: Qwen/Qwen2-7B-Instruct, prompt: generate_summary_prompt(doc_data, req.max_length, req.tone), max_tokens: req.max_length } ) ) return result.json() except asyncio.TimeoutError: raise HTTPException(status_code408, detailRequest timeout) # 启动命令uvicorn app:app --reload --host 0.0.0.0 --port 8001注意FastAPI 默认不支持同步 requests 调用会阻塞事件循环。我们用asyncio.to_thread()Python 3.9或concurrent.futures.ThreadPoolExecutor将其放入线程池执行确保高并发下不卡死。4.3 错误处理与 Token 超限防护让智能体“有底线”API 错误不是 bug而是智能体的“免疫系统”。我们重点防护三类高频错误Context Window Exceeded上下文超限当用户上传 100 页 PDF解析后文本超 8192 tokensvLLM 会返回400 error: context window limit。解决方案在parse_document后用 tokenizer 预估 token 数from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(./models/qwen2-7b-instruct) total_tokens sum(len(tokenizer.encode(page[text])) for page in doc_data[pages]) if total_tokens 7000: # 留 1192 tokens 给 prompt # 自动启用语义分块用 sentence-transformers 找关键段落 top_chunks select_top_k_chunks(doc_data[pages], k5) doc_data[pages] top_chunksOutput Token Overflow输出超限vLLM 的max_tokens是硬上限但模型可能在达到前就停止如生成/s。我们设置max_tokens1024并在返回后检查实际长度output_tokens len(tokenizer.encode(output_text)) if output_tokens 950: # 预警阈值 log_warning(fOutput near token limit: {output_tokens}/1024)Model Crash / Connection RefusedvLLM 进程意外退出时CLI 或 Web 会报ConnectionRefusedError。我们在QwenReaderAgent.__init__()中加入健康检查def __init__(self, base_url: str): self.base_url base_url self._check_health() def _check_health(self): try: resp requests.get(f{self.base_url}/health, timeout5) if resp.status_code ! 200: raise RuntimeError(fvLLM health check failed: {resp.status_code}) except Exception as e: raise RuntimeError(fCannot connect to vLLM at {self.base_url}: {e})实操心得在poetry.lock中锁定requests2.31.0避免新版 requests 的连接池行为变更导致健康检查误报。我们曾因升级到 2.32.0/health接口返回 503排查了 3 小时才发现是 requests 的默认 keep-alive 超时与 vLLM 的 uvicorn 配置冲突。5. 常见问题与排查技巧实录那些官方文档不会写的真相5.1 “Qwen embedding 没有识别为 text embedding” —— 这不是 bug是设计选择搜索热词里频繁出现这句话。根源在于Qwen2 系列模型Qwen2-7B/Qwen2.5-7B没有内置的 embedding 层。它的get_input_embeddings()返回的是词嵌入矩阵但forward()方法不支持input_ids单独前向传播以获取句向量。官方推荐方案是用Qwen2-7B-Instruct的最后一层 hidden state 作为 embedding需修改模型代码用text2vec-large-chinese等专用 embedding 模型推荐我们实测text2vec-large-chinese384维在中文语义相似度任务上比 Qwen2 的 last_hidden_state4096维准确率高 12.7%且速度更快。因此在智能体的“向量检索”环节我们直接调用from text2vec import SentenceModel embedder SentenceModel(shibing624/text2vec-large-chinese) vectors embedder.encode([人工智能发展现状, AI industry status]) similarity cosine_similarity([vectors[0]], [vectors[1]])[0][0]提示text2vec-large-chinese模型约 1.2GB首次加载慢建议在QwenReaderAgent.__init__()中预加载而非每次qna时才初始化。5.2 “API error: the model has reached its context window limit.” —— 90% 是 prompt 写太长这个错误常被误认为是模型限制。但 Qwen2-7B 的max_position_embeddings32768vLLM 启动时设--max-model-len 8192理论上可处理 8K tokens 输入。为何还报错我们抓包发现问题出在 Prompt 模板错误写法在 Jinja2 中用{% for %}循环拼接 100 段文本{% for section in sections %} {{ section.title }}: {{ section.content }} {% endfor %}这会产生大量空白行和重复空格token 数暴增。正确写法用|trim和|striptags清洗{% for section in sections %} {{ section.title | trim }}: {{ section.content | striptags | trim }} {% endfor %}更进一步我们用正则压缩空白import re cleaned_text re.sub(r\s, , raw_text).strip() # 将多空格/换行转单空格实测一篇 5000 字报告原始解析文本 token 数 6820经清洗后降至 5210成功规避超限。5.3 “vLLM qwen” 启动失败CUDA out of memory 的真实原因A10G 16GB 显存按理说跑 Qwen2-7B11.6GB绰绰有余但启动时报CUDA out of memory。排查顺序如下检查其他进程占显存nvidia-smi查看是否有残留的python进程kill -9干净确认 vLLM 启动参数--tensor-parallel-size 1单卡必须与实际 GPU 数匹配设为 2 会强制分配双卡显存关闭不必要的日志vLLM 默认--log-level INFO会缓存大量日志到显存改为--log-level WARNING最关键的一步禁用 FlashAttention-2Qwen2 官方推荐用 FlashAttention-2但它在某些驱动版本如