前几天刚把一个AI Agent项目从公网测试环境迁到客户的隔离内网里,整个过程比写Agent本身痛苦得多。这项目本身不难——用户问几句,Agent调内部接口拿数据,再组织答案。难的是网络受限、依赖受限、模型算力受限之后,原来那些顺手的东西全都不顺手了。先给结论:隔离内网里做AI Agent,模型算法只占三成,剩下七成都是工程问题,而这七成恰恰是普通教程讲得最少的部分。这篇文章把我这次实战的完整链路梳理一遍,包括选型、部署、上下文管理、工具接入和排坑经验,希望能让同样要做内网私有化Agent的人少走几个弯路。
这次做的项目是给一家单位做内部运维助手,Agent需要部署在独立网段的服务器上,能访问内部工单系统、知识库和监控平台,但外网完全不通。用户的问题集中在“某台机器现在什么状态”“这个故障以前怎么处理”“帮我开一个工单”这类日常运维诉求。听起来像是一个增强版问答机器人,但真正落地时要拆成模型层、Agent调度层、工具层、管理平台四部分,每一层都要在内网环境下重新适配。
1. 这类项目真正要解决的是什么
1.1 隔离内网不是“没有网”,而是“受控的网”
很多人听到“隔离内网”第一反应是“这台机器上不了网而已”,实际远没那么简单。我在现场遇到的隔离环境有三个特征:没有公网出口,但有完整的内部DNS、LDAP和软件源镜像;服务器上能跑容器和GPU推理,但所有镜像、依赖包都要经过审批拷贝进来;网络安全策略很严格,Agent不能随便访问所有内部系统,每个工具调用都要留痕。
这意味着AI Agent的经典玩法全部失效。不能直接调云厂商的大模型API,不能在线拉取LangChain依赖,不能靠联网搜索增强回答,甚至连“下载一个Python包”这种最普通的操作都要提前准备离线wheel。但换个角度想,这种限制也帮我们规避了很多隐患:数据不出域、模型完全私有化、整条调用链路可审计,这在政企和工业场景里反而是核心卖点。
所以做这类项目,第一个要建立的认知是:内网Agent不是“阉割版”,而是“重装版”。公网上的Agent可以随时借助外部资源补能力,内网Agent必须把模型、知识、工具三个能力都长在自己身上。整个设计思路要从“能用就行”变成“可交付、可运维、可解释”。
1.2 公网上的Agent方案为什么不能直接搬
如果只把公网Agent项目打包传到内网,大概率跑不起来,因为公网方案有几个默认前提,在内网里全不成立。
第一,模型推理依赖外部服务。公网Agent默认调用OpenAI兼容接口,但内网没有这些地址,即便你把环境变量指向内部地址,模型文件也不存在。必须换成本地推理服务,也就是在自己机器上跑开源大模型,再对外暴露一个兼容接口。
第二,工具调用的生态被砍掉一大半。公网方案里常见的联网搜索、网页解析、公共API组件,在内网里全变成无用项。你只能把内部已有的系统接口包装成工具,比如工单系统的查询接口、监控平台的告警接口、知识库的检索接口,这些往往没有统一的鉴权方式,需要自己写适配层。
第三,软件供应链完全变样。Python包、Node模块、Docker镜像、模型权重,全部要离线获取。哪怕项目代码写得再干净,只要有一个依赖没收到,整个服务就起不来。
第四,运维和审计要求更高。公网项目可以容忍“偶尔出错然后人工介入”,内网项目往往要求每次调用都有日志、每个工具动作都能回溯,甚至Agent给出的错误答案要能定位原因。这些不是模型能力问题,而是工程架构问题。
用一个类比来说:公网做Agent像在超市里做饭,缺什么随时下楼买;内网做Agent像在荒岛做饭,必须在出发前把火种、锅、食材全部装进背包。那这篇实战文章要讲的,就是怎么把这个“背包”完整打包、安全拆包、再在每个环节上不出差错。
2. 方案选型:从模型、框架到交互层
2.1 模型层:本地LLM与推理服务的选型
模型层是内网Agent的地基,这一步选不好,后面全是折腾。选型时我盘了三件事:算力有几张卡、显存有多大、业务对中文能力的要求有多高。
我这次使用Qwen2.5 32B Instruct模型,离线下载后用Int4量化部署,主要考虑是中文理解能力强、工具调用格式稳定、量化后单卡能跑。如果显存只有16G,建议考虑14B或7B级别;如果有A100/H系列大显存,可以直接上更大模型或减少量化精度。核心原则是“够用就好”,不要一味追求大模型,内网部署还要预留并发推理的显存。
推理框架选了vLLM。它在隔离内网场景里有一个很大的优势:对外提供OpenAI兼容接口,这样上层Agent代码可以沿用标准的chat completions协议,以后换模型也不需要动Agent主程序。vLLM的PagedAttention机制对长上下文和高并发也友好,我们后来压测时同一块GPU上并发拉满,显存管理明显稳。
嵌入模型也很关键,因为Agent的长期记忆和知识库检索都要靠向量化。我们部署了bge-m3这个开源嵌入模型,本地起一个embedding服务,用来把知识库文档切块后转成向量。至此模型层两件套就齐了:一个生成模型负责推理和工具调用,一个嵌入模型负责检索和记忆。
注意:模型量化后能力会有轻微下降,但不要因此选择超出显存的模型然后强行跑,那样会出现OOM或KV Cache不足,反而更不稳定。选型前先把显存账算清楚,32B Int4大约占用16G左右,还需要至少8G跑KV Cache和并发,照这个标准配机器基本不会翻车。
2.2 Agent主体:单Agent+工具调用的ReAct路线
Agent主体架构方面,我这次刻意做了“减法”。目前主流的Agent架构有ReAct、Plan-and-Execute、多智能体协作等,但隔离内网场景最怕的是不稳定。多智能体意味着多次模型调用、多套提示词、更长的链路,任何一个节点出问题都不好排查。所以我最终选了单Agent + 工具调用的ReAct路线。
ReAct的思路很直观:让模型在每轮回复中输出思考过程、要调用的工具名、工具参数,然后由程序执行工具并把结果返回给模型,模型继续判断是否还要调用工具。举个例子,用户问“帮我查一下这台机器的负载并写个排查建议”,Agent先调用告警系统工具拿到指标数据,再调用知识库检索相似案例,最后汇总成答案。
工程上最重要的是约束输出格式。模型不能自由发挥输出自然语言,必须按我们定义的JSON结构返回Thought和Action。我的做法是在系统提示词里明确给出动作格式,并要求Agent在工具调用后用Observation继续推理,直到生成Final Answer。同时,在解析层做了容错:模型偶尔会输出多余文字或格式错乱的JSON,程序需要能提取其中的action字段,而不是直接报错。
我还设置了一个硬性限制:单轮对话最多允许5次工具调用,超过就强制Agent给出当前能得出的结论。这个限制主要防止Agent在一个问题上反复调用工具、绕圈子,浪费模型推理时间,也让问题更容易定位。
2.3 周边设施:Rust写高并发入口,Django做管理平台
技术栈方面,我采用了“Rust + Django”的混合方案。这个组合不是赶时髦,而是从内网环境的特点倒推出来的。
Agent的入口和调度层用Rust编写。原因是隔离内网的服务器配置通常是固定的,我们希望入口服务体积小、部署简单、并发能力强。Rust可以编译成一个独立的二进制文件,不需要在目标机器上配置一堆运行时环境,拷过去就能跑,非常契合离线交付。我们用Tokio做异步运行时,负责接收HTTP请求、转发给本地模型服务、执行工具调用、维护会话状态,实测并发性能很稳,内存占用比Python实现低了不少。
管理平台用Django。Agent上线后运维同学需要配置工具开关、调整提示词、查看日志和审计记录,这些管理性功能正好是Django的强项:ORM操作简单、Django Admin可以直接生成后台、权限体系现成。Rust负责“跑”,Django负责“管”,两边通过内部HTTP接口通信,互不干扰。
提示:不要在Rust里硬写管理界面,也不要在Django里做高并发网关。各做各的强项,整体工程复杂度反而更低。如果团队对Rust不熟,也可以用Go替换,核心思路是“调度入口要轻、管理后台要快”。
3. 落地实施:一步步搭建隔离内网Agent
3.1 基础环境准备与离线依赖导入
内网项目进场的第一步往往不是写代码,而是确认目标机器上有什么。这次目标服务器是GPU服务器,装的是Ubuntu系统。我先对照资产清单确认了显卡驱动和CUDA版本,然后装了Docker和NVIDIA Container Toolkit,后续模型服务和向量库都跑在容器里,环境隔离性好,也方便迁移备份。
离线依赖导入是最容易埋坑的地方。Python依赖不能现场pip install,必须提前在能联网的机器上把用到的包全部下载为wheel文件,再一起带进内网。我的做法是准备一个requirements.txt,在公网机器上执行:
pip download -r requirements.txt -d ./offline_wheels/ -i https://pypi.org/simple然后把整个offline_wheels目录拷贝进内网,在内网执行:
pip install --no-index --find-links=./offline_wheels/ -r requirements.txtDocker镜像也是同样的套路,在外网机器上先把镜像导成tar包,再进入内网用docker load导入。这个方法看起来原始,但非常可靠。项目管理上我会在开始第一天就做一次“离线启动清单”,把系统包、Python包、模型权重、Docker镜像全部列成表,每拷进来一项就勾一项,防止最后缺东少西。
提示:离线导入时最容易忽略的是传递依赖。pip download不加参数时只下载顶层依赖,必须用pip download默认行为把所有依赖一并下载。如果从conda环境打包,建议用conda-pack直接导出完整环境,比逐个补包省心。
3.2 部署模型推理服务与向量库全流程
基础环境准备妥当后,开始部署模型服务。模型权重放进内网指定目录后,我用vLLM启动OpenAI兼容服务:
python -m vllm.entrypoints.openai.api_server \ --model /data/models/qwen2.5-32b-instruct-int4 \ --served-model-name qwen2.5-32b \ --host 0.0.0.0 --port 8000 \ --max-model-len 32768 \ --gpu-memory-utilization 0.85这里几个参数需要解释。served-model-name用于Agent端访问时指定的模型名,我们约定为qwen2.5-32b;max-model-len是模型最大上下文长度,设为32768意味着最多能接收约3万token,足够长;gpu-memory-utilization表示给KV Cache预留的显存比例,设成0.85代表最多用85%显存,剩下15%留给模型加载和临时计算。
启动后用curl快速验证一次接口是否正常,确认返回的格式是标准OpenAI风格:
curl http://127.0.0.1:8000/v1/models接着部署嵌入模型和向量库。向量库选了Milvus的轻量模式,嵌入模型bge-m3以独立服务方式提供embedding能力。知识库里的文档会先切块,然后用bge-m3向量化后写入Milvus,后续Agent需要长期记忆时,先从Milvus检索相似度最高的片段,作为上下文的一部分喂给大模型。
部署完成后,我把整体调用链路画在一个共享文档里:用户请求 -> Agent网关(Rust) -> 模型服务(vLLM) -> 工具调用(内部API) -> 返回。这个链路图在整个项目期间反复使用,谁排查问题都要先看它。
3.3 实现Agent上下文管理与Token预算
内网环境没有真实的Token计费,但Token管理仍然直接决定Agent能不能稳定工作。Token是大模型对文本长度的一种基本计量单位,中文大约1个字对应1.5到2个Token,模型上下文窗口就是它能同时处理的最大Token数量。如果超过窗口,模型会直接报错或者丢掉最前面的信息。
我给Agent设计了Token预算分配方案。假设最大上下文长度是32768,那么大致分配是:系统提示词占3%,工具定义占15%,历史对话占60%,本轮用户问题及输出占剩下22%。实际编码时,我会用Token估算函数把每段文本长度预计算一遍,而不是完全依赖模型返回的token数。比如在Rust项目里写一个简单的估算函数:
fn estimate_tokens(text: &str) -> usize { (text.chars().count() as f64 * 1.7) as usize }这个估算函数对中文场景足够用,虽然不如真实tokenizer精确,但能完成第一层过滤。如果需要更精确,可以在离线环境跑一个Tokenizer服务,但会增加部署复杂度,目前项目里估算+裁剪策略运行得很稳。
上下文裁剪我采用“滑动窗口+摘要”方式:把所有历史对话按轮次切分,最新M轮完整保留,再往前的内容如果超预算,就调用一次模型把旧对话压缩成一段摘要放在最前面。这样既保留关键信息,又不至于无限增长。另外,工具返回结果如果过大,也要做截断,比如工单详情最多保留2000字符,其余部分用“结果过长已省略”代替,避免工具返回内容挤爆上下文。
3.4 工具接入与权限收敛
Agent的价值在于能干活,而干活靠的是工具调用。在内网环境里,工具不是公网那些花哨API,而是单位内部的工单系统、监控平台、知识库。我最初的做法是把这些系统的HTTP接口包成OpenAI function calling schema,让模型按schema选择工具并填参数。
一个工具schema长这样:
{ "type": "function", "function": { "name": "query_work_order", "description": "根据工单号查询工单当前状态、处理人、处理进度", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "工单编号" } }, "required": ["order_id"] } } }模型返回的action会包含工具名和参数,Rust网关解析后校验参数格式,再转发到内部系统。这里有几个必须在工程上强控的地方。第一,工具白名单:不是所有内部接口都能被Agent调用,我在管理平台里维护了一份可调用工具清单,Agent只能看到并调用清单里的工具。第二,调用超时:每个工具请求设置了最长10秒超时,超过则返回错误,避免Agent卡住。第三,审计日志:每一次工具调用的入参、出参、耗时、调用者都记录到Django管理后台,方便事后排查。
提醒:模型填参数时偶尔会犯迷糊,比如把工单号的空格带上,或者把日期格式写错。工具适配层内最好做一次参数清洗和格式校验,不要直接透传给内部系统。我们踩过这个坑,后来在工具执行器里加了一层param_clean函数,问题基本消失。
4. 常见故障与排查经验
4.1 我踩过的几个典型坑
整个项目从联调到稳定运行,大概花了两周,期间问题不断,但有几个坑特别有代表性。
第一个坑是模型服务OOM。上线第一天高峰期,vLLM服务直接崩了。排查发现因为并发请求太多,显存里的KV Cache被占满,模型来不及释放旧缓存。后来我把gpu-memory-utilization从0.9降到0.85,又在Agent网关层加了并发限流,同时让vLLM启用自动前缀缓存功能,问题才缓解。结论是:模型服务不能裸奔,前面必须有一层限流和队列。
第二个坑是工具返回的JSON格式偶尔不合法。模型在生成Action时会把参数写成带注释的JSON、或者末尾多一个逗号,解析直接失败。后来我在Rust网关里加了一个“JSON提取+修复”的逻辑,先从回复中截取从{到}的子串,再用宽松模式解析;如果仍然失败,就把当前错误返回给模型,让它重新生成一次工具调用。这个容错机制效果很好。
第三个坑是内网DNS和镜像导致启动失败。有台机器没有配置内部DNS,vLLM启动时尝试访问外部地址做初始化,直接卡住。解决方法是设置模型路径为纯本地,并关闭不需要的联网检查。另外,Python依赖版本冲突也出现过,后来规定所有依赖必须锁版本号,生成requirements.lock,避免“能装但跑不起来”。
第四个坑是Agent在多个工具之间死循环。用户问一个复杂问题,Agent调用工具A拿到结果后又调用工具B,B结果不如预期,又回头调用A,来回倒腾。我们通过最大迭代次数限制解决,同时把工具描述写得更明确,让模型判断更准确。
4.2 排查工具与速查表
在内网环境排查问题,最依赖的还是日志和监控。Rust网关会把每个请求的完整trace打印出来,包含模型输入、模型输出、工具调用记录和耗时;Django管理平台则单独保留审计日志,按时间倒序排列。排查问题时先看网关trace,确认是模型环节还是工具环节,再逐层深入。
我把常见问题整理成一张速查表,方便团队里其他同学遇到类似情况时直接对照。
| 现象 | 排查思路 | 解决办法 |
|---|---|---|
| 模型服务OOM或崩溃 | 看vLLM日志和nvidia-smi显存占用 | 降低gpu-memory-utilization、限制并发、启用前缀缓存 |
| Agent回复“不知道”但知识库有信息 | 看网关trace中检索结果是否为空 | 调整检索top_k、检查嵌入模型是否生效 |
| 工具调用返回“参数错误” | 看Django审计日志中的入参 | 在工具适配层做参数清洗和格式校验 |
| 请求响应很慢 | 看模型推理耗时和工具耗时指标 | 启用流式输出、减小max-model-len、优化工具接口 |
| 依赖包启动时提示找不到 | 查看pip列表和日志报错 | 用离线wheel方式安装,检查版本锁定 |
| Agent陷入工具调用死循环 | 看trace中action重复出现 | 设置最大迭代次数、优化工具描述 |
排查工具方面,我推荐先看“最靠近故障源头”的那一环。如果模型返回慢,看GPU利用率;如果工具一直报错,直接构造一条假请求测试内部接口;如果只是单条对话错误,用同一问题重复调用,看是否复现。内网环境没有公有云那些全家桶监控,但只要有trace和日志,问题同样能定位得很好。
4.3 扩展方向与工程建议
项目上线稳定后,还可以再做三件事来提升Agent的实用性。
第一,建立Agent评测集。把用户经常问的问题整理成几十条测试用例,每次改动提示词或模型后自动跑一遍,看正确率和工具调用成功率变化。没有评测集,后面升级模型完全靠感觉,风险太大。
第二,把工具接入方式标准化。现在每个内部系统都要写单独的适配层,后续工具多了会很冗余。建议抽象成“连接器”概念,每个连接器定义好鉴权方式、请求格式、错误码映射,管理后台动态注册,新系统接入成本会大幅降低。
第三,为Agent增加主动学习的能力。内网场景下知识库不可能覆盖所有新问题,可以把“用户问了但Agent没答好”的案例收集下来,定期整理成新文档写入知识库,形成正循环。这一步不需要复杂训练,只需要简单的后台人工审核机制。
关于架构上要不要升级,我的建议是:如果当前单Agent方案已经满足需求,就不要为了技术升级去改多智能体。多智能体在隔离内网里意味着更长的调用链、更复杂的资源分配和更难的排障,实际收益未必明显。先把单Agent的稳定性做到极致,真有需求时再按场景拆分。
最后说点实在的
如果让我重新做一次这个项目,我会在进场前就把“离线依赖清单”做到最细,包括每一个Python包、每一个Docker镜像、每一份模型权重,并在第一天就用脚本验证整条链路能跑通。模型选型可以多花时间比较,但工程上的沉默成本才是最容易被忽略的。Agent能不能在隔离内网里稳定跑起来,从来不只取决于模型聪明不聪明,而是整个系统在受限环境里能不能被完整地搭起来、被可控地运维、被清晰地审计。这一点,比任何算法技巧都重要。