
干这一行的人应该都有这种经历模型评测跑通了Demo演示惊艳了半个办公室结果一进客户现场网络是隔离的外网连不上在线API一个都不通。去年我们接的企业项目就是典型情况服务器部署在内部安全网络里所有设备不允许自动外联数据更不可能出域。当时的任务很明确——把一个能对话、能查数据库、能调内部接口的AI Agent从“能跑的Demo”变成“在隔离内网里 7x24 小时稳定服务的系统”。这篇就把隔离内网下AI Agent工程落地的完整链路拆开讲从依赖准备、模型私有化接入、Agent编排、并发优化到故障排查全是我实际踩过的坑和能直接复用的方案。适合正在做私有化交付、企业内网知识库、企业级Agent平台的工程师也适合准备给业务方交付“能下地干活”AI产品的同学参考。1. 项目背景与整体技术选型思路1.1 隔离内网下的三座大山模型、依赖、在线服务进了隔离内网才知道平时开发依赖的东西全都没了。第一座山是模型调用在线大模型API想都不要想必须把模型权重搬到内网服务器上自己起服务。这个过程不是说拷个文件就能跑要考虑显存够不够、量化到什么程度、要不要上多卡并行。第二座山是依赖。Python生态这点特别恶心一个LangChain项目装下来二三十个包很正常再加上向量库、HTTP异步框架、各种工具SDK依赖树拉出来能绕桌子一圈。内网机器的 pip 源是空的不提前做好准备光装环境就能耗掉你一整天。我们当时还遇到过一个坑开发机上能跑通的 LangChain 版本组合到了内网装出来版本对不上整个链路直接崩。第三座山是在线服务。内网环境里没有 HuggingFace没有在线Embedding API没有对象存储甚至连一个公共DNS都不一定能用。这意味着不光模型要私有化Embedding模型要私有化连各种中间件、离线模型的下载渠道都要自己准备。这三座山的本质只有一个所有资源都必须提前“离线化”在能访问外网的环境里准备好一切然后一次性搬到内网。后面所有工程方案都是围绕这个原则展开的。1.2 为什么技术栈落在 LangChain LangGraph FastAPI 上项目刚启动的时候团队内部对技术栈有过几轮争论。有人提议用 Django 直接写编排逻辑说 Python Web 圈熟、想怎么改怎么改也有人提 Spring AI理由是 Java 体系在传统企业里更“正统”运维团队管起来顺手。最后还是选了 FastAPI LangChain LangGraph 这套组合。核心原因有四个。第一LangChain 生态里的工具封装已经很成熟内网场景下我们自己包的工具函数可以复用它的 BaseTool 接口省掉很多胶水代码。第二LangGraph 解决的是“可控性”问题它不是让模型自由发挥而是把 Agent 的思考、工具调用、最终生成建模成一张有向图每个节点都能加超时、重试和状态检查。这对交付一个生产级系统至关重要最怕的就是 Agent 一跑起来“失控”。第三FastAPI 原生支持异步接口后续接企业里的統一鉴权、监控、任务队列都很顺畅。第四这套技术栈的 Python API 非常贴近“描述业务逻辑”的思维方式我们服务端工程师调整成本很低。Spring AI 不是不好而是对于这个项目的交付节奏来说Java 生态在 LLM 工具链的丰富度上还是差一截。Django 做管理后台确实舒服但做高并发下的大模型任务编排FastAPI 的异步性能和轻量程度更适合当服务层。技术选型这事没有绝对标准核心看你的团队能不能在一个项目周期内稳定交付。1.3 一套四层架构把“能跑的 Agent”变成“能上线的系统”隔离内网项目最忌讳直接把 Agent 代码塞进 Web 服务里就对外开接口。一开始我们就是这么干的结果并发一上来服务直接卡死。后来重新设计了四层架构每一层只干一件事接入层对外提供 API 和 WebSocket 通道负责鉴权、限流、请求日志不写任何业务代码。Agent 编排层使用 LangGraph 定义 Agent 状态机负责意图识别、工具选择、多轮对话维护。工具执行层封装内网里的数据库查询、HTTP 接口调用、文件读取等能力统一处理超时和错误重试。模型服务层私有化部署的 LLM 和 Embedding 模型通过 OpenAI 兼容接口暴露给上层调用。这套分层最大的价值在于每一层都能独立扩缩容和故障隔离。模型服务挂了工具层还能响应健康检查编排层超时接入层可以立刻返回降级文案不会让调用方一直等。后面几节的内容基本都是这套架构里各层怎么落地的具体实践。2. 离线环境搭建与模型私有化接入的实操细节2.1 依赖离线打包的正确姿势从 Wheelhouse 到内网源离线装 Python 依赖第一反应是直接拿开发机的 site-packages 整个目录拷过去。千万别这么干跨机器、跨Python小版本、跨操作系统都可能出现诡异问题而且你不知道哪些包是编译出来的哪些带二进制扩展。我在这个上面吃过亏拷过去之后 faiss-cpu 直接 import 报错排查了两小时。正规做法是做一个干净的 wheelhouse。在能访问外网的开发机上用 pip download 把所有依赖连依赖的依赖全部拉成 .whl 文件pip download -r requirements.txt -d ./wheelhouse \ --platform manylinux2014_x86_64 \ --python-version 312 \ --implementation cp \ --only-binary:all:注意这里指定了 manylinux 平台和 Python 3.12 的 ABI防止下载到微软平台的包或纯 Python 包的意外版本。如果项目里不可避免有需要源码编译的包比如某些 Postgres 驱动那就在开发机上编译好成 wheel 再带进去。缺一不可否则内网机器没有编译器也没网基本就是等死。到了内网之后不要傻乎乎地把 wheelhouse 里几百个文件手动装先建一个本地 PyPI 源。最简单的方式是用 pip install 加 --find-links 参数直接指向目录pip install --no-index --find-links./wheelhouse -r requirements.txt如果内网有多台机器或者之后还要扩容我建议在内部服务器上启动一个简单的索引服务比如用 devpi 或者 pypiserver把 wheelhouse 传上去。这样其他机器只要把 pip 的 index-url 指到内部 IP 就行。pypiserver 是纯 Python 的一条命令就能跑非常适合隔离内网。还有一个很容易忽略的点锁定版本。requirements.txt 里每个包都必须精确到 patch 版本不要写langchain0.1而是langchain0.2.14。LangChain 这种包版本一升级API 说变就变内网里没有网去查新文档千万别赌“小版本应该没事”。2.2 私有化大模型选型与显存估算模型选型是整个项目里最需要提前确认的事因为一旦把模型文件搬进内网再想换就是几天的返工。我们的原则是能用 7B 级别的模型解决就不要上 14B除非业务场景对复杂推理和指令跟随要求极高。私域知识问答场景7B 量化模型配合好的 RAG 链路效果完全够用。模型系列方面我们测下来推荐 Qwen2.5-7B-Instruct 这类中文能力强的开源底座配合内部语料做了简单微调。做代码生成或结构化抽取的话CodeQwen 系列也不错。团队里如果有人有偏好也可以考虑 DeepSeek-R1-Distill 等蒸馏模型但一定要先确认蒸馏模型在你们特定任务上的表现不能光看 Benchmark。显存估算是重头戏计算公式不复杂模型参数量的两倍如果用 FP16是基础显存FP16 下一个 7B 模型的权重约 14GB。要是用 4bit 量化权重部分只有约 4GB。但这是“模型权重”的显存实际部署还要加上 KV Cache、CUDA 运行开销、中间激活值。保守计算可以直接把权重显存乘以 1.5再留出 30% 的余量。我们最终用的 Qwen2.5-7B-Instruct加载为 4bit 量化格式单并发下显存占用差不多 8GB用一张 L20 训练卡完全能带起来。如果要把并发拉到 10 个以上还得分流到两张卡做 vLLM 的 tensor parallel或者多副本部署。有一个公式可以估算并发容量单请求平均 1500 tokens模型输出速率在 4bit 量化下约 1800 tok/s那一个请求最长运行时间就是 1500 / 1800 ≈ 0.83 秒加上调度开销算 1 秒单卡差不多能撑 5-8 并发。很粗糙但比拍脑袋强。2.3 通过 OpenAI 兼容接口把模型服务“藏”在 Agent 后面模型服务起来之后不要直接让 Agent 代码去连 vLLM 的原生接口而是通过 OpenAI 兼容接口统一暴露。原因很实际vLLM、TGI、Ollama 这些服务各自有各自的调用格式而 LangChain 的 ChatOpenAI 类可以直接对接任何 OpenAI 兼容服务只需改 base_url 和 api_key哪怕只是个占位符。我们在内网服务器上用 vLLM 部署from langchain_openai import ChatOpenAI from langchain.adapters import init_chat_model llm ChatOpenAI( base_urlhttp://10.0.3.8:8000/v1, api_keyinternal-key, modelqwen/Qwen2.5-7B-Instruct, temperature0.2, max_tokens2048, timeout30, )注意 timeout 一定要设置。内网服务其实也会卡模型推理长请求触发超时是常态不设 timeout 的后果就是外部请求全部挂在 FastAPI 的线程池里积压到整个服务不可用。另外这个 base_url 千万不要写死到代码里放到配置中心或者环境变量不然每次模型服务迁移都要改代码重新打镜像。还有个小坑内网机器的 /etc/hosts 或者 DNS 解析如果有问题直接填写 IP 地址最稳妥。我遇到过企业内部 DNS 配置了泛域名解析导致 HTTP 调用走了错误网关的情况排查了非常久。到了隔离环境能用 IP 就不要用域名。3. Agent 编排层设计与并发优化实战3.1 异步接口不是银弹FastAPI 里的阻塞陷阱FastAPI 给人印象是“异步高性能”但很多人忽略了一点如果你在 async 路由函数里调用了同步的、会阻塞的事件循环的代码整个服务的并发就废了。LangChain 的很多链和 Agent 默认是同步执行的模型调用那段尤其慢一次可能花几秒甚至几十秒。把它直接丢进 async 函数里效果就是第二个请求要等第一个请求完。对比两种写法# 错误示范整个 event loop 被阻塞 app.post(/chat) async def chat(request: ChatRequest): result agent_executor.invoke({input: request.message}) return {answer: result[output]}# 正确做法同步调用丢进线程池主循环保持响应 from fastapi.concurrency import run_in_threadpool app.post(/chat) async def chat(request: ChatRequest): result await run_in_threadpool( agent_executor.invoke, {input: request.message} ) return {answer: result[output]}这个改动看起来不起眼实测效果天差地别。我们第一次压测错误写法下 20 个并发请求直接超时一半改完线程池之后同样的压测脚本P95 从 25 秒降到了 8 秒。原因就是事件循环不再被模型调用阻塞FastAPI 可以持续接收新连接并调度线程处理。3.2 并发上限怎么算从吞吐倒推信号量与线程池AI Agent 服务的并发规划和常规 API 完全不一样。普通接口可能 100 并发轻轻松松但 Agent 每个请求都要调用多次模型、执行多个工具一个请求占用模型推理的时间是秒级。所以不能预设“我们要支持多少并发”而要反过来算单实例模型服务能支撑多少并发然后限住上游防止压垮模型服务。我们当时的推算逻辑大概是这样的参数数值说明单请求平均模型调用次数2.5Agent 思考可能再次调用工具单次调用平均 token2000输入上下文 输出模型服务单进程吞吐1800 tok/s4bit 量化实测目标响应时间P95≤ 15 秒业务方要求单请求理论耗时2000×2.5÷1800≈2.78s纯推理时间单实例合理并发5~6加上调度和排队余量基于这个推算我们给 Agent 执行入口加了一个 asyncio.Semaphore(5) 做并发闸门多出的请求在队列里等待而不是无限把线程拖到模型服务里。from asyncio import Semaphore agent_semaphore Semaphore(5) app.post(/chat) async def chat_with_semaphore(request: ChatRequest): async with agent_semaphore: return await run_in_threadpool( agent_executor.invoke, {input: request.message} )压测之前这个改动是被团队里一位同事质疑的他觉得限制并发等于降低系统能力。实际上恰恰相反隔离内网里的模型服务是最宝贵的资源与其让 30 个请求同时冲进模型层然后全部排队互相拖慢不如在上游有节奏地放行保证每个请求都能在承诺时间内完成。把指标从“并发数”换成“P95 延迟”之后这个限制的价值就很清楚了。3.3 LangGraph 状态机给 Agent 加上超时、重试和熔断项目早期用 LangChain 的 AgentExecutor 直连模型连续跑几天之后我发现一个规律大部分线上问题不是模型质量差而是模型在某个状态下“绕不出来”。比如它一直找不到合适的工具或者在一个循环里反复调用同一个接口每次都返回“再试一次”。对于内网生产系统这种失控是不能接受的。后来我们把核心流程迁移到 LangGraph。它的思路是把一次会话建模成状态图接收用户输入 → 判断是否需要工具调用 → 执行工具 → 更新状态 → 再次判断 → 生成最终回答。图的每个节点都可以单独设置重试次数和超时时间还能通过条件边把死循环直接掐断。from langgraph.graph import StateGraph, END def call_model(state): # 模型调用内部带超时 response llm_with_tools.invoke(state[messages]) return {messages: [response]} def execute_tool(state): # 执行工具内部有异常捕获 tool_result run_selected_tool(state[tools], state[arguments]) return {messages: [tool_result]} def should_continue(state): if state.get(loop_count, 0) 4: return fallback return end状态机一旦把流程约束住出问题的概率大幅下降。遇到模型连续两次尝试用一个已经失败的工县我们直接让图进入“兜底回答”节点返回“当前工具不可用请稍后再试”而不是让用户傻等。3.4 “Agent 怎么扛并发”的朴素答案排队比并行更重要外部流行讨论 Agent 并发时都在讲并行、异步、流式但我在内网项目里发现大模型的并发瓶颈是物理算力而不是代码层面你能开多少线程。GPU 就那么大vLLM 的 continuous batching 也只是把算力切分得更细总量没变。与其花大量时间优化并行逻辑不如把请求队列设计好。我们在 FastAPI 之上加了一层简单的用户级等待队列请求到达先进入队列由调度器按优先级把请求交给模型服务。队列长度超过阈值就直接返回 503并带上“服务繁忙”提示。这个设计带来两个直接好处一是模型层永远处于有节奏的负载状态不会因为瞬时突发而 OOM二是运维侧可以通过队列深度直接看到系统压力比看 CPU 利用率直观得多。当然如果业务方要求的是“多人同时聊天不排队”那就要靠加卡了。两条路都很清晰关键别在软件层面强行硬扛老老实实告诉业务方“隔离内网下算力决定并发上限”这是交付阶段最重要的预期管理。4. 工具调用、知识库与对外服务的落地内网方案4.1 把内网 API 包成 Agent 工具鉴权、超时与异常透传Agent 的价值一半在模型一半在它能调用的工具。内网系统数据库、工单接口、监控平台定时报表这些都是企业已有的资产把 Agent 接上去它才算真正干活。工具封装我总结下来有三个不能省的点。第一是鉴权。内网业务系统普遍有自己的认证体系有的是 Header Token有的是 OAuth2。我们在工具层做了一个统一凭证管理器把各种系统的令牌全部放在内存缓存里自动续期Agent 调用时只管传业务参数不用理会底层鉴权细节。注意别把凭证写进 Agent 的上下文否则模型可能把 Token 当普通文本输出到日志里。第二是超时。一个工具调用最长允许 15 秒一旦超过立刻中断把“工具执行超时”作为结果返回给模型让它换一个方案。这是防止 Agent 在慢接口上反复耗时间的唯一手段。我们踩过坑某个 DashBoard 查询接口平时 2 秒就返回月底数据量大时要 40 秒。Agent 调用它时直接卡死 40 秒整个编排链路一起变慢。第三是异常透传。工具内部不要吞掉异常而是把异常消息按固定格式返回给模型例如工具执行失败连接数据库超时(ErrorCode 10023)。请尝试重新调用或建议用户稍后再试。这种格式让模型能根据错误信息选择重试还是换策略而不是糊里糊涂地告诉用户“系统出错了我无法回答”。4.2 RAG 知识库离线化Embedding 模型与向量库选型知识库问答是这类项目的高频需求。内网环境下Embedding 模型也必须本地部署。我们用的是 BGE 系列模型中文效果稳定体积也不算大bge-large-zh-v1.5 只有 1.2GB 左右CPU 跑也能接受GPU 上更是轻松。注意 Embedding 模型和 LLM 必须分开部署不能共用同一个 GPU 卡跑推理否则两个任务会互相抢占显存实测会让 LLM 的 Token 输出速度降低一半。向量库选型要看数据量和团队运维能力。如果知识库文档量在百万条之内FAISS 这种轻量级库完全够用以文件形式落盘离线部署非常省心。如果数据量大到千万级再考虑 Milvus 或者 Qdrant。我们当时的数据量大约 40 万 chunk直接用 FAISS 构建索引检索 P95 只有 20 毫秒完全没有必要上重型中间件。文档切分有个参数细节值得关注我们按 350 个字符切块、重叠 50 字符适配中文表达习惯。切太大了检索粒度粗、上下文稀释切太小了语义不连贯。这个参数要跟着业务文档类型微调制度类文档可以切大一点闲聊型 FAQ 切小一点。检索阶段top_k 默认取 5加上重排之后效果比单纯 top_k 提升明显。BGE 官方的 reranker 模型也可以离线部署多花几毫秒体验提升很大。4.3 给业务方一个能用的“中台”从控制台到 WebSocket 任务通道项目交付不能只丢一段 Python 代码。业务方不懂 LangChain他们需要的是一套能看、能试、能监控的东西。我们在四层架构之上做了一个轻量级控制台主要提供三个功能Agent 效果试玩、调用记录查看、意图命中情况统计。接入层除了 REST API还实现了 WebSocket 通道适合需要流式输出打字机效果的场景。FastAPI 的 WebSocket 集成很简单from fastapi import WebSocket app.websocket(/ws/chat) async def ws_chat(websocket: WebSocket): await websocket.accept() try: while True: user_message await websocket.receive_text() async with agent_semaphore: result await run_in_threadpool( agent_executor.invoke, {input: user_message} ) await websocket.send_text(result[output]) except WebSocketDisconnect: pass这种任务通道的好处是业务方前端不需要关心长连接的轮询后端 Agent 执行时间再长前端都能比较优雅地等待和展示。我们的实测结论是企业内部要做 Agent 中台基本就是“控制台 API 任务通道”三件套不要试图把一个 Agent 工程做成一个大而全的 PaaS。5. 常见问题与排查技巧实录5.1 内网部署高频问题速查表把项目周期里遇到的高频问题整理成了速查表都是能直接对号入座的问题现象可能原因排查思路与解决方式依赖安装报 No matching distribution内网 pip 源指向了无效地址确认 pip index-url 指向内网 pypiserver且版本命令里没有通配符模型响应极慢甚至无响应显存不足导致模型服务频繁换入换出用 nvidia-smi 看显存占用确认是否加载了两份模型副本Agent 反复调用同一个工具LangGraph 缺少循环计数在状态图里加入 loop_count 限制超过次数直接进 fallback 节点HTTP 接口超时工具层未设置超时或鉴权失败反复重试统一工具层超时时间把鉴权失败单独分类处理不要重试无意义请求导入模块时报 DLL/so 错误依赖包在开发机是编译版本用纯 wheel 方式重新打包避免跨机拷贝 site-packages线程池耗尽大量请求排队同步阻塞调用直接占满了默认线程池所有大模型调用和工具调用走 run_in_threadpool并设置自定义线程池大小5.2 没有在线观测平台怎么做链路追踪LangSmith 这类在线观测平台在内网里用不了但你总得知道一个请求在哪个环节慢。我们的做法是零依赖的轻量追踪每个外部请求进来生成一个 request_id一路透传到 Agent 编排层、工具层和模型服务层所有日志记录都必须带上这个ID。日志格式统一成 JSON 行每条日志包含阶段名、输入摘要、输出摘要、耗时、token 数。比如{request_id:a1b2c3,stage:call_model,duration_ms:2340,tokens_in:1280,tokens_out:420} {request_id:a1b2c3,stage:execute_tool,tool:query_asset,duration_ms:120,status:ok}排障的时候只要 grep 某个 request_id整个请求完整路径就出来了。哪一步慢一目了然。后来我们还基于这些结构化日志写了一个不到 200 行的脚本统计每小时的平均耗时、工具成功率、模型调用次数分布当作简易监控面板。这个方案虽然有“手动挡”的感觉但在隔离内网里反而是最可靠、最不依赖外部服务的。别一开始就把坑挖大去上 Prometheus Grafana 全家桶先解决“能不能看清请求路径”的问题数据积累多了再考虑可视化。5.3 迭代部署的经验镜像与灰度最后聊聊迭代部署。模型服务和 Agent 服务是两套独立生命周期。模型权重一代版本几乎不变Agent 代码却可能每周都改。所以模型服务做成独立 Docker 镜像训练好模型之后一次性打进去Agent 服务正常走代码仓库构建镜像。内网服务器没有互联网镜像仓库我们通过在开发机完善的 CI 构建完成后把镜像 save 成 tar 包拷到内网后 docker load 加载。特别注意一下容器打包的归属内网交付环境里Docker 镜像尽量把交互相关的系统依赖也提前装好。很多服务器上的基础系统是精简版本缺 libgl、缺 OpenSSL 的情况很常见。镜像里提前 pip 装好pydantic2.x、openai1.x、protobuf这些关键二进制包能省去现场调试的时间。Agent 服务升级建议按批次灰度。比如先给 20% 的调用方切到新版本观察日志里的工具成功率和P95延迟稳定再全量。业务方如果催得急宁可灰度两轮也不要一把梭。原因很简单内网问题排查链路比外网慢太多一次全量事故的代价远高于多花一天灰度。项目做完我最大的体会是隔离内网里的 Agent 工程和在线开发完全不是一回事。在线环境你可以依赖无穷无尽的外部生态出了问题随手查文档、换包、升级组件内网环境你手里的牌是提前准备的打完就没了。这种约束逼着人把每一层都做得克制而扎实——依赖版本锁死、模型选型提前确认、并发上限写进设计文档、工具层统一兜底。最后再分享一个小经验任何时候都别高估模型在失控场景下的“自我修正”能力。测试期多测几个刁钻问题看看 Agent 会不会陷入死循环上线前把所有节点超时时间和次数限制设置好。内网交付没有后悔药每个坑都得提前堵死。