☰
隔离内网AI Agent实战:LangGraph+vLLM离线部署与并发优化
2026/10/5 5:19:51 网站建设 项目流程

1. 项目概述:为什么要搞隔离内网下的 AI Agent

先把话说透:隔离内网里做 AI Agent,跟你在自己电脑上跑一个 LangChain Demo 完全是两码事。公网上开发 Agent,模型调云端 API,依赖包 pip 一下装完,出问题还能 Google 报错。但换到隔离内网——生产系统内网、政企专网、金融核心业务网这种环境,一切都不一样了:没有外网域名解析,pip 源连不上,模型权重下载不了,甚至你内网机器装什么操作系统、开不开英伟达驱动、能不能执行 Docker 命令,都要先过一遍机房坐席的审批流程。

我这套实战方案,目标场景是“纯物理隔离、无外网出口、机器有 NVIDIA GPU、业务系统在内网运行”的标准企业环境。业务诉求很直白:让 Agent 能自动分析数据库巡检结果、自动生成日报、根据工单上下文调用内部 API 完成操作,全程不出内网。最终落地技术栈是 FastAPI + LangChain + LangGraph,模型侧用开源模型本地部署(以 Qwen 系为例),并用 vLLM 做推理加速。这套东西我已经在生产内网实际跑过,走了不少弯路,这篇文章把规划和细节完整拆开,给你一份可以直接参照的落地手册。

内容会覆盖四个核心层面:离线环境的技术选型、依赖包和模型文件的搬运、LangGraph 编排业务 Agent 的完整流程、以及最重要的一环——在隔离网这种“无外网、无云端、全本地”条件限制下,Agent 服务如何扛住并发访问。适合正在做政企项目、内网知识库 Agent、内网自动化运维助手的同学参考。如果你刚接触 Agent,也能从中搞懂一套完整工程的架构思路和避坑清单。

2. 隔离内网 AI Agent 的架构设计与技术选型

2.1 离线部署 Agent 的整体架构分层

隔离网环境做 Agent,第一个要颠覆的认知是:你不能假设任何东西可以在线获取,从模型权重到 Python wheel 包,到前端静态文件,全部要提前准备成“离线安装包”。所以架构上我分成四层来设计:

  • 模型推理层:GPU 机器上跑 vLLM,加载开源模型(如 Qwen2.5-14B-Instruct),暴露统一 OpenAI 兼容 API。这一层是 Agent 的“大脑”。
  • Agent 编排层:LangGraph 作为核心编排框架,定义节点、状态流转和工具调用;LangChain 的 Tools、Prompt 模板、OutputParser 做辅助。
  • 应用服务层:FastAPI 提供 HTTP 接口,负责接收业务系统的请求、做鉴权、把任务提交给 Agent 编排层,并返回结果或任务 ID。
  • 任务缓冲层:异步场景用 Celery(broker 用内网的 Redis 或 RabbitMQ)处理耗时较长的 Agent 任务,避免 HTTP 长连接把接入服务拖垮。

这套分层背后的逻辑很明确:把 Agent 的“状态编排”和“业务接入”剥离开。LangGraph 专注管好流程(比如先查询数据库、再调用工单系统、最后汇总生成结论),FastAPI 专注管好上游系统接入。事实上如果你的 Agent 只是一问一答,不涉及多步工具调用,用 FastAPI 单层也就够了。但一旦要做“让 AI 真的下地干活”的事情,状态机编排是躲不开的。

2.2 离线条件下模型部署方案选型

模型推理层在隔离内网里可选方案不少,我简单列一下我对比过的几个,以及最终为什么选 vLLM:

方案优势劣势适配场景
vLLM吞吐高、支持连续批处理、OpenAI 兼容接口对 GPU 驱动/CUDA 版本敏感生产环境高并发推理
Ollama部署极简,一条命令起服务并发吞吐中等,接口自定义度低原型验证、小团队内部试用
llama.cppCPU/GPU 都能跑,极致精简吞吐一般、API 不全无 GPU 的离线环境
Triton Inference Server企业级多模型管理配置复杂,学习成本太高大型团队、多模型统一入口

实测下来,内网生产环境还是 vLLM 最稳。原因很简单:它自带 PagedAttention,连续批处理做得好,同一个模型在多并发请求下吞吐远胜 Ollama 和 llama.cpp 的默认调度。另外一个关键点是 vLLM 直接暴露 OpenAI 兼容接口,LangChain 的 ChatOpenAI 可以直接配置 base_url 指向内网 vLLM 服务,零代码改动就把 Agent 底层模型接上了。

模型选型上,隔离内网环境一般没有条件跑几百 B 的大模型。我的经验是把参数量控制在 14B~32B 之间,比如 Qwen2.5-14B-Instruct。如果业务场景是“只做意图识别+工具调用”,14B 足够;如果要处理长篇知识库检索后的摘要生成,建议 32B 起步,但对应显存压力要提前算清楚。后面章节会专门展开显存和并发之间的关系。

2.3 为什么用 LangGraph 而不是纯 LangChain

很多初学者会把 Agent 写成“一个大 Prompt 让模型自己决定下一步”,LangChain 原生 Agent 也是这个套路,但这在隔离内网业务场景里会出大问题:模型一旦乱调工具,业务系统就可能出现脏数据,没人敢担责。因此我坚持用 LangGraph 把流程写死——该调数据库查询的时候就调数据库查询,该调工单系统的时候就调工单系统,模型只负责“填写参数、判断分支”,不负责“决定调用哪个系统”。

LangGraph 的核心价值是四个字:可控编排。它把流程拆成节点和边,节点之间的状态用一个共享 dict 传递,每个节点都是一个普通 Python 函数。比如“数据库巡检 Agent”就可以拆成:意图识别节点 → 巡检参数提取节点 → 数据库查询节点 → 结果分析节点 → 报告生成节点。每个节点只做纯函数式操作,上一节点输出作为下一节点输入,异常可以在任意节点拦截。这在生产代码评审中非常加分,因为安全团队看得懂、审得过。

如果说 LangChain 是“工具箱”,LangGraph 就是“流水线设计图”。内网项目往往要过等保测评、安全审计,你在评审会上拿一张节点状态图讲流程,比拿一段“灵活自决策”的 Agent Prompt 有说服力得多。

3. 隔离内网的“搬家”工程:依赖、模型与运行环境

3.1 Python 依赖包的离线制备

这一节是隔离网项目最容易翻车的环节,没有之一。你的开发者用一台能上外网的机器写完代码,然后要把全部依赖搬进内网,但凡少一个 wheel 包,内网机器上就会当场报 ModuleNotFoundError。解决办法是做一个完整的离线源。

我这里给一套经过实战检验的操作流程,先在联网机器上(与内网目标机相同 Python 版本,比如都基于 Python 3.10 或 3.11):

pip download -r requirements.txt -d ./offline_packages \ --platform manylinux2014_x86_64 \ --implementation cp \ --python-version 311 \ --only-binary=:all:

关键词是--only-binary=:all:,它强制只下载编译好的二进制包。因为在内网目标机上,绝大多数机器没有编译工具链,如果拉下来一堆 sdist 源码包,内网 pip 安装时会现场编译然后失败。当然,LangChain 系部分组件可能没有对应的 wheel,这种情况下我会提前在联网机器上把源码包下载,并在内网安装时准备好 GCC 工具链。

进入内网后执行离线安装:

pip install --no-index --find-links=./offline_packages -r requirements.txt

这套流程的坑点在于:pip 只会下载一个包的直接依赖,不会递归解析完整的交叉依赖树。因此我的建议是先在联网机器上用pip freeze > requirements.txt生成锁定版本清单,再基于它执行 download。如果你手头没有现成工程,也可以分层制作:先把 torch、vllm 这类大件装好,再把 LangChain、LangGraph、FastAPI 等业务依赖装完,分两批进入内网。

3.2 模型权重的离线搬运与加载

内网模型有两个来源:一是从公网 HuggingFace 下载后拷贝进内网,二是用企业内部已有的模型库。HuggingFace 下载模型推荐用hf命令行工具,比git lfs clone稳定得多,支持断点续传:

hf download Qwen/Qwen2.5-14B-Instruct \ --local-dir ./qwen25-14b-instruct \ --exclude "*.safetensors.index.json" # 按需求选择

下载完成后,把整个模型目录 tar 打包传进内网。这里特别提醒一个容易被忽略的点:模型文件夹里如果有cache目录或*.lock文件,传到内网后要先清干净再加载,否则偶尔会出现莫名其妙的 “Token indices sequence length is longer than the specified maximum sequence length” 这类问题。

模型进来之后,用 vLLM 起服务。我的启动脚本一般这样:

python -m vllm.entrypoints.openai.api_server \ --model /data/models/qwen25-14b-instruct \ --served-model-name qwen-agent \ --host 0.0.0.0 \ --port 8000 \ --gpu-memory-utilization 0.9 \ --max-model-len 32768 \ --enable-auto-tool-choice \ --tool-call-parser hermes

--gpu-memory-utilization很关键。如果设太满(比如 0.99),多个并发请求同时进来时容易 OOM;设太低又浪费显存。我实测 14B 模型在 4 张 24GB 显存卡上,设 0.85~0.9 足够支撑日常并发。另外,如果是纯内网无证书环境,记得在启动前统一设置export HF_HUB_OFFLINE=1,防止 vLLM 初始化时尝试联网检查本地模型缓存。

3.3 内网机器系统环境与基础设施准备

隔离内网经常不是你想用什么就用什么。有些老机房机器还是 CentOS 7,自带 Python 3.6,这会让 vLLM 直接无法安装——因为它要求 Python 版本通常不低于 3.9。这里我建议优先在内网准备一台“开发交付机”:系统统一用 Rocky Linux 9 或 Ubuntu 22.04 LTS,Python 用 3.11/3.12,NVIDIA 驱动版本不低于 535,CUDA 用 12.1 以上。如果机房审批困难,至少要做好 Docker 镜像交付的准备,把 Python 环境和 vLLM 全部打进镜像里,内网机器只需要有 NVIDIA Container Toolkit 就能跑。

网络层面,隔离网也有“内部网络”。FastAPI 服务、vLLM 服务、Redis 之间是通的,所以域名是不需要的,直接用内网 IP 加端口互相调用。但这种环境下时钟同步问题很容易被忽略——如果内网机器没有配置 NTP 服务器,各节点系统时间相差超过几分钟,JWT 鉴权和 Redis 会话过期策略会随机报错。建议从上往下先把 NTP 服务打通,再部署任何应用,这是我在几个项目里被坑过的最基础也最难受的问题。

4. LangGraph 编排 Agent 的核心实现与业务接入

4.1 Agent 业务场景定义:以内网“数据库巡检报告助手”为例

为了让流程更具体,我拿一个真实场景来讲:内网核心库每周要做巡检,DBA 需要汇总数据库实例的状态信息、慢查询、表空间使用率,然后生成一份 Word 报告发给领导。传统做法是 DBA 手工登录每台数据库执行 SQL,再复制粘贴结果。我用 Agent 把它变成了一条自动流水线。

场景拆解后,Agent 需要具备的能力清单:

  • 调用数据库查询工具(连内网数据库执行只读 SQL)
  • 调用慢查询分析工具(读取指定时间窗内的慢查询记录)
  • 对查询结果做简单总结,提取“异常项”(比如表空间超 80%)
  • 生成 Markdown 格式巡检报告,并调用接口转成 Word 文件

整个流程是确定性的,所以不是“对话式 Agent”,更像“任务式 Agent”。用 LangGraph 表达这个确定性流程,是最合适的选择。

4.2 LangGraph 节点定义与状态流转设计

LangGraph 的核心概念是 StateGraph。每个节点是一个异步函数或同步函数,输入是全局状态 dict,输出是更新后的部分状态。我定义一个简单的状态类型:

from typing import TypedDict, Optional class AgentState(TypedDict): task: str # 原始任务描述 db_instances: list # 需要巡检的数据库实例列表 sql_results: dict # 各实例查询结果 slow_query_results: dict # 慢查询分析结果 anomalies: list # 识别出的异常项 report_md: str # 生成的 Markdown 报告 status: str # 当前状态 error: Optional[str] # 错误信息

状态机的节点大致这样组织:

from langgraph.graph import StateGraph, END def extract_params(state: AgentState) -> AgentState: """从任务描述中提取数据库实例列表和巡检时间窗""" # 这里用 LLM 做抽取,Prompt 限定死输入 schema ... def query_database(state: AgentState) -> AgentState: """对每个实例执行只读 SQL""" ... def analyze_slow_query(state: AgentState) -> AgentState: """检查慢查询统计""" ... def identify_anomalies(state: AgentState) -> AgentState: """比对各实例指标,找出异常项""" ... def generate_report(state: AgentState) -> AgentState: """汇总成 Markdown 报告""" ...

构图时的关键点是:把模型参与的部分严格收敛到两处——参数抽取和异常总结。数据库查询、模式匹配、状态判断都交给代码,不要让模型做全流程决策。这样做的稳妥性在多次内网试运行中完全得到了验证:不管用户怎么表述任务,Agent 都会按固定流程走,绝不出现模型自己“灵机一动”调用一个外部系统的情况。

LangGraph 的连线逻辑如下示意:

graph = StateGraph(AgentState) graph.add_node("extract_params", extract_params) graph.add_node("query_database", query_database) graph.add_node("analyze_slow_query", analyze_slow_query) graph.add_node("identify_anomalies", identify_anomalies) graph.add_node("generate_report", generate_report) graph.set_entry_point("extract_params") graph.add_edge("extract_params", "query_database") graph.add_edge("query_database", "analyze_slow_query") graph.add_edge("analyze_slow_query", "identify_anomalies") graph.add_edge("identify_anomalies", "generate_report") graph.add_edge("generate_report", END) app = graph.compile()

4.3 FastAPI 服务层与 LangGraph 的桥接

FastAPI 的一个常规用法是收到 HTTP 请求后,直接同步调用 graph 的.invoke()方法,然后把整个结果返回。但内网场景里上游系统常常需要“提交任务后立刻拿到 task_id,再轮询结果”,因为 Agent 流程即使再快,如果包含多步数据库查询,整体耗时也可能 20 秒以上。HTTP 长连接等待不仅让上游系统难做超时控制,还容易把 FastAPI 的 worker 全部占满。

所以我把接口拆成两个:

  • POST /api/agent/run:提交任务,返回task_id
  • GET /api/agent/task/{task_id}:查询任务状态和最终结果

任务异步执行的方案用 Celery,FastAPI 只负责校验参数、封装请求体、把任务对象扔进 Redis 队列,然后立即返回。Celery worker 在另一个进程里通过 LangGraph 的ainvoke()执行复杂任务。这样 FastAPI 进程永远保持“轻快”,Uvicorn 本身的事件循环不会被 Agent 的长耗时任务卡死。

这里有一个很容易犯的错:FastAPI 的接口函数如果定义成def而不是async def,FastAPI 会把它丢到线程池里执行,每个请求占一个线程。如果线程池默认大小(40)被 Agent 任务占满,系统的所有接口包括健康检查都会超时。我建议所有涉及 Agent 提交的接口用async def,把耗时任务全部交给 Celery,FastAPI 主进程保持异步非阻塞。

4.4 自定义 Tools:内网工具接入的正确姿势

LangGraph 节点内部需要调用外部系统时,我推荐把调用封装成 LangChain@tool,而不是在节点里裸写请求。好处是统一做入参校验、统一做日志审计、统一做超时与重试。

以数据库查询工具为例:

from langchain_core.tools import tool import pymysql @tool def query_db(instance: str, sql: str) -> str: """在指定数据库实例上执行只读SQL,返回JSON格式结果""" # 强制校验 SQL 只读前缀,防止误操作写库 sql_upper = sql.strip().upper() if not sql_upper.startswith("SELECT"): return "错误:仅允许 SELECT 查询" conn = pymysql.connect( host=instance_host_map[instance], port=3306, user=readonly_user, password=readonly_pwd, connect_timeout=10 ) try: with conn.cursor() as cursor: cursor.execute(sql) columns = [col[0] for col in cursor.description] rows = cursor.fetchall() return json.dumps([dict(zip(columns, row)) for row in rows], ensure_ascii=False, default=str) finally: conn.close()

这是我认为整套 Agent 工程里最重要的编程习惯:工具函数必须自己兜底异常、校验输入、限制权限。因为在内网环境里,Agent 一旦获得数据库连接能力,安全边界就被推进到了代码层。我把这个 Rule 写进了项目规范:所有工具调用函数禁止接收自由文本 SQL,必须由节点函数根据结构化参数拼 SQL,杜绝模型直接操控 SQL 文本。这是被安全评审逼出来的经验,但后续在生产上救了不止一次。

4.5 Web 端“智能体”接入:让前端也能对话

内网项目用户其实并不关心 Agent 的原理,他们只关心能不能像 ChatGPT 一样在网页里输入文字拿结果。所以我额外提供了一套前端接入方案:基于 Streamlit 或 Flask 写一个轻量对话页,后端统一打到 FastAPI 接口。如果你希望对话体验更接近实时打字机效果,FastAPI 再加一个 WebSocket 接口,Celery 任务执行过程中不断向客户端推送节点状态,这样用户能看到“正在查询数据库”“正在生成报告”,体验远好于傻等几秒空白页。

由于我很早把 FastAPI 和 LangGraph 拆成了独立服务,前端接入完全复用现有接口,任何业务系统只要有 HTTP 调用能力就能对接 Agent,不需要额外改模型相关代码。这套“服务接口前置、Agent 引擎后置”的做法,在面对多部门接入需求时特别省事。

5. 并发性能优化:隔离网下 Agent 到底怎么扛并发

5.1 并发瓶颈分析:模型推理、编排器与应用服务

每次有人问我“AI Agent 怎么扛并发”,我的第一反应不是甩结论,而是先把瓶颈分层。隔离网下 Agent 链路长,每一层都可能成为瓶颈:

  • 模型推理层:vLLM 的吞吐能力、显存大小决定最大并发上限
  • Agent 编排层:LangGraph 状态存储(默认是内存)在多线程并发下的线程安全性
  • 应用服务层:FastAPI 进程数、Celery worker 数、Redis 连接池上限
  • 上游业务系统:内网数据库/API 自身能承受的调用频率

我在内网做过一次比较完整的压测,条件如下:单机 4 张 NVIDIA A800(80GB)跑 Qwen2.5-14B-Instruct,vLLM 设置最大并发 128,Agent 编排是 LangGraph + Celery,FastAPI 单机 4 个 worker。压测脚本用 Locust 模拟 50 个并发用户,每个用户提交“生成某个实例的巡检报告”任务。

压测得到的现象:前 20 个并发请求,系统 TPS 线性增长,平均响应时间保持在 8 秒内;并发超过 30 后,响应时间快速恶化,系统吞吐反而下降,Redis 连接数飙升,同时数据库连接被打满。这说明瓶颈已经不在 vLLM 推理,而在底层业务系统的连接池容量。这个结果对工程调优有很强的指导意义:Agent 扛并发不可能只靠把模型服务参数调大,必须做全链路容量规划。

5.2 vLLM 侧并发参数的实战调优

vLLM 侧可以做的调优项不少,我按优先级排序:

第一优先级是max-model-len的控制。很多人图的省事设置成 32768 甚至更长,但内网业务的实际请求通常不会超过 4096 token。超长的上下文会显著增加每请求的 KV cache 显存占用,直接压低并发上限。我建议先统计业务实际请求长度,再设定合理值,比如训练阶段先用 8192,后续业务扩展再拉长。

第二优先级是--gpu-memory-utilization。设 0.9 意味着预留少量显存给 CUDA context 和其他开销,如果模型权重接近显存上限,调 0.95 可能成功,但并发时会 OOM。我在四卡并行时,把 0.85 到 0.9 之间做了一个矩阵测试,最终定格 0.88,既稳定又不会让系统显得“跑不满”;如果你使用tensor-parallel-size为 2 或 4,显存利用率和吞吐的关系更要仔细测。

第三优先级是--max-num-seqs。vLLM 内部最大同时处理的序列数,默认值通常可以满足需求,但如果你发现响应延迟增高但 GPU 利用率不高,可以适当降低这个值减少排队,或在显存有余量时调高以提升吞吐。这块的参数名在不同版本间略有差异,模型服务起来后用页面或 API 检查真实显存分配最靠谱。

实测下来,在 vLLM 正确调优后,模型推理层很少成为瓶颈,真正的瓶颈会转移到下面要说的编排和服务层。

5.3 FastAPI + Celery 的并发架构与参数设置

FastAPI 底层跑 Uvicorn,而 Uvicorn 启动时默认单 worker。单 worker 在异步模型下可以支撑成百上千的 HTTP 长连接,因为它不靠线程并发,而是事件循环。但隔离内网工程上我仍建议用 Gunicorn 管理多 worker,因为 Agent 接口虽然异步,但中间有些同步库(如 pymysql)会短暂阻塞事件循环,多个 worker 可以提供冗余:

gunicorn -w 4 -k uvicorn.workers.UvicornWorker \ --bind 0.0.0.0:8080 \ --timeout 120 \ main:app

-w 4在 4 核机器上比较稳妥,再高就没有收益了。--timeout 120一定要设置,否则 Agent 后端处理超过 30 秒时,Gunicorn 默认 30 秒会 kill worker,导致任务丢失。这个参数是我踩过一次大坑后才养成的习惯——内网业务系统提交长任务时,网关和 Gunicorn 的超时参数是必须联动调整的。

Celery worker 的并发模型用--concurrency=8(如果你的机器核心数足够),每个 worker 进程内用 prefork 模式跑任务。注意 Celery worker 和 FastAPI worker 不建议共用同一台机器的全部资源,否则长任务会把 CPU 抢光,影响接入服务的响应。

5.4 LangGraph 并发调用的线程安全与状态隔离

LangGraph 的StateGraph在编译成app后,默认是线程安全的吗?答案是有条件的:每次invoke()都会创建一个独立的状态快照,所以不同请求之间互不干扰,这是 LangGraph 设计上的优势。但如果你在节点里使用了全局变量或共享连接对象,并发调用就会踩坑。比如我在早期版本里把数据库连接写成一个模块级全局变量,结果 20 个并发请求进来后,连接池排队,部分 SQL 查询直接超时。

正确做法是把所有资源连接(数据库、Redis、外部 API 客户端)都放在节点函数内部创建,或者使用线程专属的 connection factory。如果你是用ainvoke()异步调用,节点里就不能用同步的 pymysql 直连,否则会阻塞事件循环。这种场景建议改用aiomysql或在节点函数里使用run_in_executor。

LangGraph 还有thread_id机制,用来在多轮对话中维护会话状态。隔离内网场景如果 Agent 不是多轮对话型,thread_id可以不传;但如果要做“用户多轮追问”,一定要在 FastAPI 层统一生成thread_id并透传给 LangGraph 的后台线程,否则每个请求都是一个新会话,上下文就断掉了。这块在 LangGraph 的config = {"configurable": {"thread_id": "xxx"}}里维护,实测在高并发短任务场景无冲突。

5.5 并发压测方案与实测数据参考

我给出一份可以参考的压测方案,不涉及具体业务数据,只说方法和量级。工具我推荐 Locust,因为它是纯 Python,能直接在隔离网内用,也能把压测脚本写进自动化测试平台。

压测核心关注三个指标:

  • 任务提交接口的 TPS:反映 FastAPI 层的吞吐能力
  • 任务从提交到完成的 P95 延迟:反映 Agent 全链路耗时
  • 任务失败率:反映系统在高压下的稳定性

我实测过的一个典型数据(上述 4×A800 + 14B 模型配置,32 并发用户,每用户每 5 秒提交一个巡检任务):任务提交接口 TPS 约 45,任务完成 P95 延迟 18.6 秒,失败率 0.4%。如果把模型换成 7B 模型,同样的配置下 P95 延迟可以降到 8 秒以内。压测时我特别观察了 vLLM 的 GPU 利用率曲线:32 并发下 GPU 利用率稳定在 87% 到 94% 之间,说明推理层没有被榨干但也没有明显空闲,整体资源利用是健康的。

如果业务期望更高的并发,需要做两件事:一是模型服务部署多副本,在 vLLM 前端加一层内网负载均衡;二是业务上做“限流”:同一个人 10 秒内只能提交一个 Agent 任务。这两条加一起,基本能扛住几百人的部门级使用场景。

6. 隔离网实战中高频问题与排查实录

6.1 模型服务启动失败与显存管理的坑

vLLM 在内网启动失败是最常见的故障。网上报错信息五花八门,但归纳下来就几类:

一类是 CUDA error: out of memory。很多人以为是显存不够,其实经常是--gpu-memory-utilization设置过高,留给 CUDA context 的空间不足。处理方式是把参数从 0.95 降到 0.88 再试,如果模型权重本身就占掉 90% 显存,那只能换更大显存卡或降低模型参数量。

另一类是RuntimeError: NCCL error,多卡并行时出现。原因多半是内网机器没有配置好 GPU 间通信依赖,或者/dev/shm空间太小。vLLM 多卡模式需要跨进程共享显存和通信,/dev/shm默认只有 64MB 会直接报错。解决方式是在容器里加--shm-size=16g,用物理机部署就要在启动脚本里把共享内存参数调大。这个坑排查起来很隐蔽,因为系统日志不会直接告诉你“共享内存不够”,只会报 NCCL 相关错误。

还有一类是模型文件加载到一半卡住,多半是内网磁盘 IO 速度太慢。模型文件动辄几十 GB,如果不做预加载预热,第一个请求会等很久。解决方式是在启动后主动发一个空请求触发模型加载,配合健康检查脚本,等模型真正就绪后再对外提供服务。

6.2 LangChain/LangGraph 离线运行中的依赖与网络问题

LangChain 生态里有一些组件默认会尝试访问外网,即便你的业务不依赖外网,也会在初始化时触发网络请求导致超时。比较典型的几个:

  • langchain_community里某些 document loader 初始化时检查user_agent,发送匿名统计
  • 部分Embedding模型默认从公网下载模型配置
  • HuggingFace 相关的AutoTokenizer加载本地模型时仍会尝试检查远端版本

针对这类问题,我统一做了离线约束:在内网机器的环境变量里设置HF_HUB_OFFLINE=1、TRANSFORMERS_OFFLINE=1、LANGCHAIN_TRACING_V2=false。特别地,LANGCHAIN_TRACING_V2如果不显式关掉,它会在后台尝试连接 LangSmith,虽然失败不影响主流程,但会引入无谓的延迟和烦人的报错日志。

关于 LangChain 版本,我强烈建议在requirements.txt里锁死大版本,LangChain 生态的小版本更新频次极高,内网环境一旦装上某个不符合预期的版本,想在无网环境升级会很麻烦。我自己的项目锁的是langchain==0.2.x、langgraph==0.2.x、langchain-openai==0.1.x,这个组合在 Qwen 系模型上配合良好,没有出现接口签名不兼容的问题。

6.3 中文文本处理与大模型输出质量问题

内网 Agent 在中文场景下容易出现两类问题。第一类是模型输出中的格式不稳定,比如要求输出 JSON 却夹杂 Markdown 代码块,或者字符编码混乱。对策是在 LangGraph 节点的 output parser 上做兜底:先尝试严格 JSON 解析,失败后用正则提取花括号内内容再解析,再失败就返回“生成失败”状态而不是把错误文本传给下游。第二类是中文乱码,排查方向一般不在模型,而在数据库连接字符集设置。pymysql 连接时如果忘记指定charset="utf8mb4",中文字段读回来就是乱码。这类问题排查起来很基础,但内网环境里因为各节点系统 locale 设置不统一,还真出现过几次。

调 Prompt 时我还发现,Qwen 系模型在system角色里描述工具调用规范时,有时候会忽略约束;反过来把它放进user消息里,配合 few-shot 示例,输出稳定性会明显提升。Agent 输出的可靠性才是生产系统的命门,这块值得多花时间调 Prompt。

6.4 常见问题速查表

现象可能原因排查步骤与解决
vLLM 启动报 CUDA OOMgpu-memory-utilization 过高或权重过大降到 0.85 重试;检查是否多进程重复加载模型
多卡并行报 NCCL 错误共享内存不足或缺少 GPU 间通信库容器加 --shm-size;检查 nvidia-smi 拓扑
第一个请求很慢模型未预热启动后发一次空请求触发加载,等模型状态 ready
LangChain 初始化卡顿后台尝试连接外网设置 LANGCHAIN_TRACING_V2=false、HF_HUB_OFFLINE=1
数据库查询中文乱码连接字符集未指定pymysql 或连接串中追加 charset=utf8mb4
Celery 任务丢失worker 被 kill 或没有配置 ack_late设置 task_acks_late=True;调大 --timeout
并发高时任务堆积瓶颈在底层系统连接池扩大数据库/API 连接池;增加预校验逻辑拦截无效任务

6.5 独家避坑经验:从评审会到运维交接

隔离网项目有一个公网项目永远不会遇到的环节:安全评审和运维交接。我不想说太多流程上的空话,只提三个让我记忆深刻的实战点。

第一,安全评审时一定不要只讲“Agent 多智能”。要让评审看到你如何控制工具调用边界,模型能碰什么、不能碰什么,节点之间数据如何流转,出错有无兜底。LangGraph 的节点状态图直接打印成设计文档附在评审材料里,通过率会高非常多。

第二,运维交接文档必须写清楚“离线启动顺序”。模型服务、Redis、Celery worker、FastAPI 服务这四者的启动有先后次序:先 Redis,再模型服务,再 Celery worker,最后 FastAPI。如果顺序反了,Celery worker 会因为连不上 Redis 或 vLLM 而疯狂重试,产生大量无用日志。把这个启动顺序写进运维手册,比任何花哨架构图都有用。

第三,给所有对外接口做统一的返回结构和错误码。Agent 链路多,任何一个节点失败,上游业务系统都希望能拿到明确的错误标识,而不是一段大模型生成的“抱歉,我遇到了一些问题”。我们最终的实践是 FastAPI 层统一拦截 LangGraph 异常,转成结构化错误码,用户侧只看到友好提示,详细堆栈走日志系统。这大幅降低了与业务系统联调时来回扯皮的成本。

7. 个人实战后想补充的几点体会

做隔离内网的 AI Agent,算法和模型永远是其中最简单的一部分。真正花掉大量时间的,是如何让模型在受限网络环境里稳定运行、如何构建可控可审计的 Agent 流程、如何在并发压力下保证系统不崩。我个人经历过在评审会前夜应急修并发问题、在隔离网里反复搬运依赖包的狼狈时刻,也最终把一套系统稳定跑上生产。现在的体会很朴素:内网环境更像一种“降维试炼”,它逼你把所有依赖、接口、监控、兜底都做得极其明确,不允许任何暧昧的“在线就好”。

如果你想把这个项目继续往下扩展,我会建议优先做两件事:一是给 Agent 加一套完整的知识库(RAG),把运维文档、工单历史沉淀进去,提高回答的领域专业性;二是评估多 Agent 协同——把巡检、报告、通知拆成不同角色的 Agent,通过 LangGraph 的并行分支和路由机制协作,形成更接近“团队作战”的自动化能力。这两块在隔离内网里都完全可行,而且随着企业数据积累得越来越厚,它们带来的价值会越来越明显。

隔离内网做 Agent,真正约束你的从来不是模型智商,而是工程严谨度。框架选型、依赖管理、并发架构、离线运维,每一层都用确定性的工程手法去解决,AI 落地也就没那么玄了。

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

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

立即咨询