
前几天刚把一个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. 落地实施一步步搭建隔离内网Agent3.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-32bmax-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和镜像导致启动失败。有台机器没有配置内部DNSvLLM启动时尝试访问外部地址做初始化直接卡住。解决方法是设置模型路径为纯本地并关闭不需要的联网检查。另外Python依赖版本冲突也出现过后来规定所有依赖必须锁版本号生成requirements.lock避免“能装但跑不起来”。第四个坑是Agent在多个工具之间死循环。用户问一个复杂问题Agent调用工具A拿到结果后又调用工具BB结果不如预期又回头调用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能不能在隔离内网里稳定跑起来从来不只取决于模型聪明不聪明而是整个系统在受限环境里能不能被完整地搭起来、被可控地运维、被清晰地审计。这一点比任何算法技巧都重要。