把 AI Agent 推进隔离内网的时候,我最直观的感受是:网上那些 Agent 演示项目,到了内网几乎没有一个能直接跑起来。这不是代码写得不行,而是它们默认的世界里什么都有——模型权重从 HuggingFace 拉、Python 依赖从 PyPI 装、搜索工具调在线 API、向量库连公网 SaaS 服务。一个断网环境,把这些“想当然”全部打碎了。
这篇就把我自己在隔离内网里从零搭建 AI Agent 工程的全过程写下来,包括模型层怎么选、编排层怎么改、并发怎么压、以及最容易被忽略的评测和迭代问题。适合正在做私有化交付、国企/金融/能源类内网项目,或者单纯想搞清楚 Agent 工程化到底比 Demo 多哪些事的人参考。我不会吹某个框架多强,只讲实际推过去、稳下来的方案。
1. 隔离内网部署 Agent,先分清“断网”断到什么程度
很多人一听到隔离内网,就直接理解成“没有互联网”。实际上手之后你会发现,同样叫内网,限制条件完全不同。部署方案要跟着网络边界走,第一步不是选模型,而是搞清楚你到底在什么环境里干活。
1.1 两种常见隔离形态,决定了你的工程策略
我这次遇到的是典型的逻辑隔离区:业务服务器可以访问内网基础设施,比如内部 DNS、内部数据库、NTP 时间服务器,但出不了公网。也就是说,不能从内网机器直接访问 HuggingFace、PyPI、Docker Hub 这些外部站点,但可以和同一内网里的其他服务互通。
比它更严格的是物理隔离区,通常是那种连网线都物理断开的机房,代码和模型只能靠刻盘或经过审核的专用通道导进去。物理隔离环境我经历过一次,那会儿最折腾的不是 AI 部分,而是“怎么把几十 GB 的模型权重和几百个依赖包安全地送进去”,权限申请和介质审核的周期比技术落地还长。
这两种环境下的工程做法有几处关键差异:
- 逻辑隔离区可以把构建机放在公网侧,先同步依赖、打镜像,再推到内网仓库,后续迭代相对顺滑。
- 物理隔离区最好一次性把依赖树、模型文件、安装脚本全部准备好,现场尽量只做校验和启动,不要指望临时下载任何东西。
- 如果内网里已经有制品仓库(比如 Nexus、JFrog 或自建的 PyPI 服务),处境会好很多,所有外部依赖转成内网制品仓库的地址就行。
所以我的建议是:动手之前先拿一张纸,画出目标环境的网络拓扑,标出哪些服务在内网可见、哪些完全访问不到。这一步看起来很基础,但能帮你后面少拆三次环境。
1.2 盘点整套 Agent 系统的依赖清单
AI Agent 不是一个单体程序,而是一组组件的组合。把这些组件按“默认依赖公网”和“内网替代方案”列成一张表,项目边界一下子就清晰了。
| 组件角色 | 常见公网依赖 | 内网可替代方案 |
|---|---|---|
| 大模型推理 | HuggingFace 模型权重 | 提前下载权重,内网部署推理服务 |
| Embedding 模型 | 在线向量化 API | 私有化 BGE 或 m3e 类模型 |
| Agent 编排 | LangGraph 依赖包 | 锁定版本 + 私有 PyPI 仓库 |
| 工具调用 | 在线搜索/天气/地图 API | 改造为内部 RPA、API 网关或内部接口 |
| 向量数据库 | 云托管 SaaS | 自建 Milvus 或轻量用 SQLite-VSS |
| 前端可视化 | 在线地图、公网静态资源 | 本地静态资源包或内网地图服务 |
这个表会直接影响架构设计。比如工具调用这块,如果业务里必须用“联网搜索”能力,而在内网没有搜索引擎服务,那就要么自建一套搜索服务,要么把 Agent 的能力边界主动收窄,禁止模型发起外部调用。这不是技术问题,而是需求边界问题,一定要提前和业务方对齐,不然 Demo 做出来,现场一跑就露馅。
2. 模型层落地:先把推理底座钉死
模型是所有 Agent 能力的地基。在内网环境下,模型选型不只是“哪个效果更好”,而是“哪个模型能离线跑、能在有限算力上稳定服务”。我这次遇到的约束是:2 张 A100 80G,还要腾出显存给 Embedding 和 rerank 模型使用。这个条件不算差,但也没到可以随意挥霍的地步。
2.1 开源模型选型:别只看榜单,要看部署友好度
市面上的开源模型很多,Qwen 系列、DeepSeek、GLM 系列都不错。但在内网场景,我优先考虑的是三条硬指标:权重是否完整公开、推理框架是否原生支持、社区踩坑是否足够多。
为什么社区踩坑数量重要?因为隔离内网出问题的时候,你是没法上网搜答案的,只能靠本地文档和已有经验排查。一个冷门模型的坑,可能三天都填不完;而主流模型遇到的问题,大概率别人已经遇到过,至少离线文档还能翻到些线索。
我最终选了 Qwen 系列的 72B 量化版作为主模型,原因是它在中文指令理解、工具调用格式上和 LangGraph 这类编排框架配合比较成熟,量化后单卡也能跑,吞吐够用。如果只是做垂直领域的简单问答,7B 或 14B 模型加 RAG 反而性价比更高,因为小模型在低延迟场景的优势是 72B 比不了的。
2.2 推理框架选型:vLLM 和 Ollama 的取舍
模型权重只是原始数据,真正在线上服务的是推理框架。我对比了三个常用方案:
- vLLM:吞吐量高,支持连续批处理,显存管理激进。适合高并发、长稳运行的生产环境,我用它跑主模型。
- Ollama:部署简单,开箱即用,适合单机场景和快速验证。但它的并发控制、自定义前后处理能力不如 vLLM 灵活。
- TGI:HuggingFace 出的方案,各方面比较均衡,但有些新模型的算子支持会慢半拍。
我的实际组合是“vLLM 跑生成模型 + 单独部署一个轻量 Embedding 服务”。生成和向量化分开跑,避免两者抢显存互相干扰。
还需要注意推理服务的接入方式。vLLM 默认提供 OpenAI 兼容的 HTTP API,这意味着 Agent 编排层调用模型时,代码可以复用标准的base_url+ API Key 方式。我让 LangGraph 里的模型地址直接指向内网推理服务的 IP,端口 8000,完全不用改模型调用代码。OpenAI 兼容协议在今天已经是事实标准,这点在内网环境帮了大忙。
2.3 显存与量化估算:先把账算明白
内网环境最怕的事是模型拉进来了,结果显存不够跑不起来。显存估算必须提前做,而且要留足余量。以 7B 模型为例,FP16 精度下权重约占 14GB;如果使用 AWQ 或 GPTQ 4bit 量化,权重降到约 4GB,但推理时 KV Cache 和激活值还会占显存,所以实际上 7B 量化模型建议至少有 12GB 可用显存。
我这次用 72B 模型,千万不能用 FP16 直接上。单个 A100 80G 要跑 72B 全精度很勉强,所以我改成 AWQ 4bit 量化,权重约 42GB,剩下约 30GB 放 KV Cache,通过 vLLM 的--max-model-len参数控制最大输入长度,把单请求的上下文限制在 16K 左右,实测并发 10 的时候显存依然稳得住。
这里有个经验:模型的上下文长度不是越大越好。上下文越长,KV Cache 占用越大,并发能力越低。你得多测几组数据,找到“业务够用”和“并发能扛”之间的平衡点。
2.4 Embedding 模型与知识库:Agent 的长期记忆
Agent 不是只靠大模型死记硬背,还要能检索内部知识。所以我单独部署了一个 BGE-large-zh 作为 Embedding 模型,把内部文档切片后向量化,存进自建的向量库。
内网环境下检索链路有几个容易踩的坑:
- 文档切片长度要跟 Embedding 模型的 max sequence length 对齐,BGE 一般支持 512 token,超出部分会被截断,检索效果直接打折。
- 向量库不能用公网的托管服务,我选择了 Milvus,它在内网部署不算复杂,但如果你项目规模不大,用 PostgreSQL + pgvector 也完全够。
- 检索后最好接一个 rerank 模型,不然 Agent 拿到一堆相关性不高的上下文,回答质量会很飘。rerank 模型不大,几百 MB 级别,对显存压力很小,效果提升却非常明显。
3. Agent 编排层:LangGraph 方案的内网改造
模型底座稳了,接下来就是让模型“长出手脚”——这就是 Agent 编排层的事。我最终用了 LangGraph,但不用指望它原封不动搬进内网就能跑。编排层的改造工作,比想象中琐碎得多。
3.1 为什么还是选了 LangGraph,而不是什么都自己写
热搜词里有“基于 Rust 语言 AI Agent”“Spring AI Agent”这些方向,各有各的道理。Rust 适合追求极致性能和资源占用的场景,Spring AI 适合 Java 技术栈的统一团队。但我这次场景里,团队后端以 Python 为主,需要快速实现复杂的状态流转和工具调用,LangGraph 的生态和资料最充足。
LangGraph 的核心价值是把 Agent 的工作流建模成一张状态图,节点是“调用模型”“调用工具”“判断是否结束”,边是流转条件。相比裸写无限循环让模型自己决定下一步,它能做到可控和可观测。这个特性在隔离内网尤其重要:线上跑偏的时候,你得能快速定位模型是在哪一步乱掉的。
不过我不建议什么场景都上 LangGraph。如果你的 Agent 只是“一问一答”,或者流程固定只有三四步,手写状态机反而更轻。LangGraph 真正发挥价值是在多工具、多分支、需要人工介入审批节点的复杂流程里。
3.2 离线依赖三件套:锁版本、私有仓库、wheel 目录
这是内网部署最容易卡壳的环节。LangGraph 的依赖树非常庞大,它的不同版本对应不同版本的 LangChain Core,接口行为有差异。如果在内网机器上盲装最新版,大概率会碰到某个子依赖版本不兼容,而且报错信息很长,排查起来很痛苦。
我的做法分三步:
- 在能联网的构建机上,用
pip freeze生成完整的 requirements.txt,并锁定每一个传递依赖的精确版本。 - 用
pip download -r requirements.txt -d ./wheels把所有包和依赖下载为 wheel 文件。 - 把 wheels 目录拷贝进内网,执行
pip install --no-index --find-links=./wheels -r requirements.txt离线安装。
这里容易忽略一个细节:LangGraph 的部分功能会按需导入额外依赖,比如某些 memory 后端。你得先通读代码里所有 import,把隐藏依赖也补进 requirements,不然跑起来才发现缺包。
3.3 从“全自动图”回归“受控状态机”
LangGraph 上手时,大家很容易把图画得特别花哨,恨不得让模型自己决定一切。但内网生产环境教会我,Agent 越自由,越难收场。
我的建议是:默认把流程走成受控状态机。比如“客服助手”这个场景,第一步固定做意图识别,第二步根据意图走固定分支,第三步如果需要查知识库就走 RAG 子图,第四步才让模型自由发挥生成回答。模型只在“生成”这一步有自由度,前面的路由全部用规则控制。
这样做的好处一是稳定,二是容易排查。模型乱说话的概率大幅降低,因为它的每一步都有人盯着;出问题时看节点日志就知道走到哪断了,不用靠猜。
代码层面核心就是定义一个 StateGraph,往里加节点和边:
from langgraph.graph import StateGraph, END class AgentState(TypedDict): query: str intent: str docs: list[str] answer: str graph = StateGraph(AgentState) graph.add_node("intent", route_intent) graph.add_node("retrieve", retrieve_docs) graph.add_node("generate", generate_answer) graph.set_entry_point("intent") graph.add_conditional_edges("intent", lambda s: "retrieve" if s["intent"] == "kb" else "generate") graph.add_edge("retrieve", "generate") graph.add_edge("generate", END)这套结构的好处是边界非常清晰。你可以在任意一个节点前后插入日志、埋点、人工审核点。
3.4 工具调用改造:把在线工具全部换成内部接口
Agent 之所以叫 Agent,是因为它能调用工具。而内网环境里,绝大多数公网工具 API 都不可用,这一步不做改造,整体方案就是空中楼阁。
我把工具分成了三类:
- 内部系统 API:比如查订单、查库存,这类直接封装成 Python 函数,用内网服务地址调用。
- 知识库检索:封装成向量库检索函数,返回相关文档片段。
- 外部不可用能力:比如实时股价、公网天气,这类直接砍掉,或者在功能设计上明确告诉用户“此能力不可用”。
工具改造还有一个细节:给模型看的工具描述必须写清楚“什么情况下用这个工具”“参数是什么含义”“返回结果长什么样”。内网系统返回的数据结构往往很乱,如果工具描述不清,模型就会瞎猜参数,浪费时间还不稳定。
另外,工具的鉴权方式要注意。内网系统的安全策略各不相同,有的用内部 token,有的用客户端证书,还有的用 IP 白名单。我在工具层做了一个统一的鉴权封装,对外只暴露call_tool(name, params)接口,内部根据工具名分发到不同鉴权逻辑,避免 Agent 代码里散落一堆密钥。
3.5 用 FastAPI 封装一个长期运行的 Agent 服务
Agent 编排层本身只是一个库,要对外提供能力,我选择用 FastAPI 做服务封装。它和 LangGraph 配合很自然,async 接口也符合异步推理调用的需求。
我的服务结构大致是这样:
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class AskRequest(BaseModel): query: str session_id: str @app.post("/agent/ask") async def ask(req: AskRequest): return await run_agent(req.query, req.session_id)选择 FastAPI 而不是 Django 是因为这层服务只需要提供 API,不需要模板、ORM、Admin 那一套。如果你需要用 Django 开发 Agent 后台管理系统,也不是不行,只是没必要把 API 服务和业务管理系统耦合在一个进程里。
关于“Agent 中台”这个概念,在这个场景下其实就是把模型服务、向量库、工具层、编排层拆成独立模块,通过统一 API 暴露给上层业务。内网部署更要重视模块间依赖方向,尽量让编排层只依赖抽象接口,不直接感知某个工具的内部实现,这样后续替换模型、换向量库,都不需要大改图定义。
4. 并发与稳定性:Agent 中台的扛压之路
很多人在 Demo 阶段从没想过 Agent 服务也要扛并发,直到联调时被业务方问“这系统能支持多少人同时用”,才意识到问题的严重性。Agent 系统的并发瓶颈和传统 Web 服务不一样,需要单独拆开说。
4.1 瓶颈通常不在模型,而在外围资源
大家下意识觉得,并发大了模型推理会先扛不住。其实在并发还没高到把 GPU 打满之前,真正的瓶颈往往是外围资源。
我实际踩过的坑:
- 向量数据库连接池被打满。Milvus 默认连接数有限,并发查询一多,客户端直接报连接超时。
- 工具层调用的内部 API 有 QPS 限制。Agent 一个流程里可能多次调用同一接口,稍不注意就把对方限流触发。
- 同步阻塞代码把 FastAPI 事件循环卡死。如果某个节点里用了同步 requests 库,并发一上来,整个服务响应都会变慢,跟死锁似的。
- 单次 Agent 流程耗时长,HTTP 客户端超时设太短。前端请求过来,3 秒就断了,但后端 Agent 还在跑,造成资源白耗。
所以我在设计阶段就把这些外围资源当成一等公民对待。
4.2 异步入口 + 同步 Worker:先接住请求,再排队干活
FastAPI 的 async 能力只是第一步,它解决的是“并发接入”问题,但不代表整个 Agent 流程都必须写成异步。Agent 编排通常涉及很多 IO 操作,如果全程 async,代码里到处是 await,且 LangGraph 的部分实现不一定完全兼容异步回调,调试起来非常痛苦。
我采用的架构是“异步 API 入口 + 同步 Worker 执行”。具体做法:
- FastAPI 收到请求后,立刻把任务写入 Redis 队列或者直接发到进程内任务队列,并向上层返回“任务已受理”。
- 后台固定 N 个 Worker 进程消费队列,同步执行 Agent 流程。
- 客户端通过轮询或者 WebSocket 获取最终结果。
这个模式牺牲了一点点接口实时性,但换来了极高的稳定性。即使某个 Worker 因为异常卡死,队列里的其他任务也能被其他 Worker 继续消费。
压测给我的数据是:在 2 个 Worker 的情况下,单 Agent 流程平均耗时 8 秒,系统能稳定支撑约 8 个并发任务;如果把 Worker 加到 4 个,并发能力翻倍,但 GPU 显存的 KV Cache 也会等比例上涨。并发和成本是要放在一起算的。
4.3 用压力测试把数字测出来,而不是靠感觉
“能扛多少并发”这个问题,必须用数字回答。我的压测脚本很简单,用 Python 的 asyncio 包发并发请求,记录成功率和 P95 延迟:
import asyncio import httpx async def send_one(client, query): try: r = await client.post("http://agent-service:8000/agent/ask", json={"query": query, "session_id": "test"}) return r.status_code except Exception as e: return str(e) async def main(): async with httpx.AsyncClient(timeout=60) as client: tasks = [send_one(client, f"测试问题 {i}") for i in range(20)] results = await asyncio.gather(*tasks) ok = sum(1 for r in results if r == 200) print(f"成功 {ok}/20") asyncio.run(main())压测结果出来后,我做的第一件事不是加机器,而是看超时和失败分布。如果失败集中在“连接被拒绝”,说明服务入口抗不住;如果错误集中在“上游超时”,说明模型推理或工具层是瓶颈;如果错误集中在“向量库连接超时”,那就去调连接池。定位到具体环节再优化,比盲目扩容有效得多。
4.4 超时、重试和幂等:生产环境的“老三样”
Agent 流程因为链条长,很容易在某个环节超时。我设了一套分层的超时策略:
- 模型调用超时设为 60 秒,因为长上下文的生成确实慢,但超过 60 秒大概率是卡住了。
- 工具调用超时设为 10 秒,如果是内部 API 慢,直接标记工具失败并让 Agent 换个路径。
- 整个 Agent 流程设置总超时 120 秒,超时后立刻返回兜底话术,比如“处理超时,请稍后重试”。
重试要格外小心。模型调用失败后重试没有副作用,但工具调用不一定。比如“创建工单”这种有副作用的操作,重试会导致重复提交。我的做法是把工具分为幂等和非幂等两类,非幂等工具失败后不自动重试,而是把错误信息交给模型判断是否二次确认。
幂等设计也很重要。我给每个请求生成一个request_id,把它透传到所有工具调用里。内部系统支持用request_id做去重的话,整个链路就可以放心重试。
最后提一句热搜里总出现的“AI Agent 怎么扛并发”。结论是:没有银弹。你先得把 Agent 流程拆成可排队、可重试、可观测的单元,然后用压测数据去校准资源配比。框架层面选 Rust 可能带来性能优势,但工程改造的核心永远是流程和解耦。
5. 隔离网内的效果评测与持续迭代
模型部署上去了,Agent 也能跑了,但这只是开始。真正让我头疼的是:内网环境里,怎么证明 Agent 效果好,怎么在不让线上业务冒风险的情况下持续迭代。这一点往往被大家忽略,但交付验收时它是致命问题。
5.1 评测集先于代码落地
我第一次做 Agent 项目时犯过一个错:先把功能全写好了,再想怎么测试。结果很尴尬,因为业务方问“效果怎么样”,我只能回答“我觉得还行”。“我觉得”在验收场景里没有说服力。
正确做法是,在写第一行代码之前,先找业务方拿一批真实问题,整理成评测集。评测集规模不需要很大,但必须有代表性,涵盖正常提问、模糊提问、超纲提问、恶意输入这几类。
隔离内网环境里,标注数据不能外包,只能内部搞定。我这次是让业务方评了 200 条问题,每条标注“期望回应类型”和“关键信息点”,然后把它固定成自动化评测基准。之后每次改代码、换模型,都拿这批数据跑一遍,对比通过率,谁好谁坏一目了然。
5.2 全链路日志:没有 Trace,Agent 就是黑盒
Agent 是多步决策系统,如果只记录最终回答,那么一旦效果变差,你完全不知道是意图识别错了、检索没召回到、还是模型生成崩了。因此全链路日志必须从第一天就做好。
日志的设计遵循“一个 session 一条链路”的原则:
2025-06-01 10:00:01.234 [agent] session_id=abc123 node=intent input="怎么查昨天的订单" output="kb" 2025-06-01 10:00:01.567 [retriever] session_id=abc123 top1_doc="订单查询操作手册.pdf" score=0.87 2025-06-01 10:00:03.890 [llm] session_id=abc123 prompt_tokens=1200 completion_tokens=350 2025-06-01 10:00:04.200 [tool] session_id=abc123 tool_name="order_query" status="success" latency_ms=310有了这样的日志,复现问题时就能按session_id拉出整个决策链路,看模型在哪个节点做了错误判断,再对症下药。Agent 系统没有 Trace 就等于裸奔,这句话在我这里是血泪教训。
5.3 模型更新的灰度策略:别一把梭
内网环境虽然节奏比公网慢,但模型还是要迭代的。直接一键切新模型是不可取的,我习惯做“影子模式”:新模型和旧模型同时跑,但新模型的输出只进日志,不返回给用户。等影子模式的评测通过率明显优于线上模型,再灰度切一部分流量,没问题再全量。
隔离内网还有个特殊情况:模型文件转移成本很高。几十 GB 的权重文件想更新一次,光在审批和传输上就要耗不少时间。所以我建议,模型迭代不要太频繁,而是把优化重心放在提示词、工具编排、检索质量这些轻量改动上。这些改动只需要改配置,不需要动模型文件,迭代成本低得多。
5.4 常见失败模式对照表
最后整理一份我遇到的失败模式和应对手段,给后来者一个排查起点:
| 症状 | 可能原因 | 处理方式 |
|---|---|---|
| 回答和知识库完全无关 | Embedding 模型加载失败,走了纯 LLM 裸答 | 检查 Embedding 服务和向量库连通性 |
| 同一问题每次答案漂移 | 温度参数过高或上下文混入其他会话 | 降低 temperature,检查 session 隔离 |
| 工具调用格式反复出错 | 模型对工具描述理解不准确 | 精简工具描述,减少参数数量 |
| Agent 流程运行一半卡住 | 某个工具接口无响应 | 给工具调用加超时和熔断 |
| 并发一高就大量失败 | Worker 数或连接池配置不足 | 先看日志是卡在模型还是向量库 |
| 内网模型答复带“幻觉” | RAG 召回不足,模型硬编 | 优化切片质量和 rerank 策略 |
写在最后
隔离内网做 AI Agent,和网上那些五花八门的 Demo 完全是两码事。它逼着你把每一步都钉死:模型怎么进去、依赖怎么带过去、工具怎么接、并发怎么扛、效果怎么证明。这些事看着琐碎,但任何一个环节掉了链子,项目都推不动。
我个人最大的体会是:内网环境天然反对“花哨架构”,它像一个过滤器,把那些依赖公网便利性的设计全部筛掉,留下的都是真正必要的复杂度。所以如果你正被隔离网折腾得焦头烂额,别急着上更复杂的框架,先把网络边界、依赖清单、评测基线这三件事做扎实,项目成功率直接翻倍。
最后分享一个小技巧:搭建隔离环境的依赖包时,不要只下载当前平台的 wheel,最好在构建机上把 Linux、Windows 不同 Python 版本的 wheel 都拉下来存进制品仓库。现场机器环境往往和你预想的不一样,多存一个版本,就能少跑一趟介质审批流程。