隔离内网AI Agent实战:离线部署、依赖管理和安全边界全解析

发布时间:2026/10/8 16:27:01
隔离内网AI Agent实战:离线部署、依赖管理和安全边界全解析 上个月我被派到一个物理隔离的内网机房交付一套 AI Agent。整个环境没有任何公网访问能力不能 pip install、不能 docker pull、连大模型 API 都调不了。要在这台只有有限计算资源的服务器上让 Agent 能自主拆解任务、调用内网工具、完成指定的业务操作。这活儿一听就知道隔离内网下的Agent工程根本不是写个Agent的问题而是一整套围绕依赖、模型、架构、权限、运维的体系问题。这篇文章就是我对这次实战的复盘从路线选型到核心实现再到一堆只有内网环境才会踩到的坑尽量完整地分享给同样要在这个场景下落地的人。1. 隔离内网落地Agent先想清楚要解决哪三类约束很多人拿到隔离内网部署AI Agent这个需求第一反应是直接把公网上的LangChain项目拷进去跑。但实际你会发现隔离内网和公网开发完全是两种生态公网上随手能获得的资源在内网里全都变成了需要提前人工搬运的货物。我习惯把约束拆成三类来看模型出身、依赖血统、工具链边界。这三类约束直接决定了后续所有技术选型。模型出身公网环境你可以在云端调用GPT/Claude这类大模型API或者从ModelScope/HuggingFace直接下载模型权重。隔离内网什么都没有你只有两个选择——要么在内部提前批量导入开源模型权重文件要么自己训练/微调一个领域模型。我这里直接选了开源模型权重导入这条路线因为自训练根本不在工期允许范围内。而模型一旦落到本地推理GPU资源、量化精度、上下文窗口长度就成了硬约束。依赖血统这是最容易被低估的。公网项目里requirements.txt一写pip install就完事。内网里所有的Python包、系统库、静态编译产物、模型转换工具都必须提前在有网的摆渡机上准备好再通过审批流程拷贝进去。而且一个很现实的问题是很多包的依赖链条很深你只下载了顶层包运行起来才发现某个底层c扩展库没有——这时候你在内网里连编译器都可能缺排查起来极其痛苦。工具链边界公网Agent可以调搜索引擎、读取任意URL、用公开API。内网Agent面对的是企业内部系统任何一个工具的调用都可能触达生产数据。所以内网Agent的工具集必须是白名单权限最简化的它不能像公网那样自由但这恰恰也给了我们一个好处——安全边界是可控的、清晰的。想明白这三类约束之后我给这次项目定的方案是本地开源模型推理 离线依赖包 单进程Agent调度器 白名单工具集。下面展开说每部分的取舍和实现。1.1 公网能轻易做到的事内网里为什么全部失效拿一个很小的例子很多Agent框架支持所谓自动安装缺失依赖。在公网这很爽但内网里这个功能就是定时炸弹。因为Agent根据你的指令去调pip然后它发现包不存在它不会告诉你内网不可达它会尝试Alternative:换源重试、卸载重装。如果你还没做权限限制这个搜索过程就可能在内网里翻箱倒柜甚至尝试访问内网其他的包镜像引发安全隐患。内网环境的根本特征是一切运行时行为都必须是预先可知的。Agent能感知到的资源是谁注入的、谁配置的Agent能调用的API是谁提前注册好的。把公网那套鲁棒性靠无限重试的思路带进来一定翻车。1.2 明确Agent运行的信任边界选型才不会走偏在隔离内网里做Agent第一件事不是选框架是画边界。我画的边界是Agent在固定的一台或两台服务器上运行只能通过预定义工具与内网其他服务通信不直接暴露给终端用户操作。信任边界决定了三件事模型推理服务选什么我选了Ollama和vLLM见后面章节Agent运行时的自主度多大工具调用必须白名单化日志审计的粒度多细。不同方案的对比如下方案模型来源依赖管理工具范围安全成本适合场景公网LLM API云服务pip联网安装任意公网API低开发原型内网LLM API离线依赖本地推理离线wheel导入白名单工具中高正式内网交付自训练模型全自主Agent本地内部数据离线定制动态扩展非常高长期演进项目我的目标很清晰三期试点先把闭环跑通稳定优先所以选了第二行。2. 架构与选型单进程调度加工具进程执行稳定优先确定了边界之后下一步是架构设计。这个环节如果只看热搜词ai agent 主流架构你会看到一堆花哨名词但真正落地时你必须根据环境来砍。2.1 主流Agent架构回顾以及隔离内网下的取舍目前主流Agent架构大概可以分成三类ReAct模式让大模型在思考Reasoning和行动Acting之间循环。每一步根据当前观察决定下一步动作。优点是简单直观、容易控制、调试友好缺点是每一步都要调一次模型延迟高上下文越长越容易跑偏。Plan-and-Execute先让模型生成一个完整计划再逐个执行。优点是省调用、上下文利用率高缺点是计划一旦出错后面全崩而且内网任务的计划变更非常频繁往往计划赶不上变化。多Agent协作规划Agent、执行Agent、反思Agent各自分工类似MetaGPT的做法。优点是任务分解更清晰缺点是进程模型复杂、通信成本高、故障点翻倍。在隔离内网这种少即是多的环境里除非你的团队对这种架构非常熟否则不推荐第一期就上。我最终选了ReAct的变体主循环是ReAct但每个步骤内部做两层校验工具白名单校验参数正则校验并且设置了最大迭代次数硬上限。原因很简单这个模式每一步都有执行痕迹内网审计最看重的就是每一步在做什么、为什么这么做。2.2 为什么我不选LangChain/LangGraph这类重型框架坦白讲隔离内网场景下LangChain这类框架反而是负资产。理由很现实依赖深渊LangChain的依赖树极其庞大离线导入时你会发现要带几十上百个传递依赖。内网导入这些包的审批流程会拖慢整个进度。而且这些框架更新很快你在有网机器上download的时候拿到的版本可能已经跟你代码不兼容了。行为黑盒LangChain内部做了很多隐式处理比如自动拼接prompt、自动管理memory、自动选择工具。出了问题你要翻它的源码才能知道它干了什么。在内网环境里定位问题的成本本来就高而你连它原本预置了什么prompt都得小心验证。我最后选择了自己实现一个百来行的最小Agent运行时。代码少出问题、每一行都能审查、依赖只有一个openai客户端库和Python标准库。这不是说LangChain不能用而是在这个场景下可控性优先于开发效率。2.3 热搜里的Rust语言AI Agent到底适合写在哪个位置很多人看到rust会问Agent主控能不能用Rust写我的回答是Agent主控用Rust开发性价比很低因为Agent核心就是调大模型工具编排上下文管理你需要快速迭代Python最合适。但Rust在Agent体系里有一个非常合适的生态位——工具执行侧。我这次就把两个高频工具用Rust实现了一个是内网配置查询工具直接编译成单二进制放内网跑不依赖Python环境另一个是定时任务巡检Agent的调度入口。Rust编译产物天然适配隔离内网的交付模式——拷贝一个二进制文件进去就能跑不需要装任何解释器。这个思路非常值得借鉴如果你的Agent需要暴露HTTP服务或者做高并发轮询Rust写一个小服务Python侧只要能调它的接口就行。3. 离线依赖与内网模型部署先让大脑能呼吸架构定了接下来是准备最枯燥但最关键的环节把Agent的大脑和口粮运进内网。3.1 有网机器上完整的离线依赖准备流程我在这台有网的跳板机上做的操作用到了pip download把包缓存下来同时把系统库也一并打包。流程是这样先在有网机器和另一台同架构Linux机器上创建完全一致的Python版本内网目标机是CentOS 7.xPython 3.10所以我用同样的镜像验证。用pip download把项目运行需要的所有包以及依赖下载到本地目录pip download -r requirements.txt -d ./offline_packages \ --platform manylinux2014_x86_64 \ --python-version 310 \ --only-binary:all: \ --no-deps这里有个关键点直接pip download -r requirements.txt是不够的必须带--platform、--python-version、--only-binary参数强制下载与目标机匹配的二进制wheel。如果不加可能会下载到需要在目标机上现场编译的源码包——而内网机器大概率没有gcc编译链到时候就是一场灾难。把所有wheel打包成一个tar连同requirements.txt一起提交审批导入内网。在内网目标机上创建虚拟环境然后用pip install --no-index --find-links离线安装python3 -m venv /opt/agent/venv source /opt/agent/venv/bin/activate pip install --no-index --find-links/opt/agent/offline_packages -r requirements.txt这一步完成后用一段简单代码验证核心依赖能import比如torch、openai、requests避免后面运行时才发现基础环境有问题。如果你要打docker镜像直接在跳板机上build好镜像再通过docker save导出为tar文件内网里docker load导入这也是常见做法。3.2 内网LLM推理服务的两种搭建方式与显存估算隔离内网里跑Agent必须有本地推理的LLM服务。我试了两种方式Ollama和vLLM分别对应不同场景。Ollama适合单机小模型快速起步。它的优势是部署极简命令行一行就完事模型文件直接放在本地目录离线可用。缺点是并发能力弱、自定义参数自由度低、如果你需要同时服务多个Agent实例可能会吃紧。我在试运行阶段用的是Ollama Qwen2.5-7B-Instruct量化级别Q4_K_M。vLLM适合服务化且并发要求高的正式环境。吞吐量高支持OpenAI兼容接口。Agent框架只需要发HTTP请求完全不用知道后端是vLLM。缺点是显存占用更大配置略复杂。我在后期切换成了vLLM。显存估算有个简单公式一个7B模型FP16精度大概需要14GB显存量化到INT4大概需要5~6GB。再加上KV Cache和推理中间态建议至少预留1.5倍余量。也就是说7B模型8K上下文32GB的显卡才比较稳。如果只有16GB卡建议上4~6B的量化模型。启动命令示例vLLMvllm serve /opt/models/Qwen2.5-7B-Instruct \ --served-model-name qwen \ --max-model-len 8192 \ --gpu-memory-utilization 0.85 \ --port 8000Ollama的启动更简单设置环境变量指定模型目录后直接serve即可。内网里没有下载模型的公网渠道所以模型文件是从跳板机拷进来的放到Ollama的models路径下然后执行create命令注册。3.3 token管理Agent在窗口内的生存法则模型部署好之后最实际的一个问题是大模型的上下文窗口限制。隔离内网里模型参数量有限一般7B、14Bcontext长度通常在4K到32K之间。而Agent每轮思考、工具返回结果、历史对话都会消耗token。token的计算在中文环境里有个经验值大概1个汉字对应1.5~2个token英文则约4个字母1个token。我在代码里写了一个极简的估算器def estimate_tokens(text: str) - int: # 粗略估算中文约1.5字符/token英文约3.5字符/token cjk_count sum(1 for ch in text if \u4e00 ch \u9fff) other_count len(text) - cjk_count return int(cjk_count * 1.5 other_count / 3.5) 4这个估算不是为了精确而是为了决定什么时候触发上下文裁剪。实际干活时我会严格控制每一次工具调用的返回体大小比如让工具只返回摘要、只返回前N行。这比事后裁剪更有效——从源头减少token消耗。4. Agent核心实现从规划器到工具调用的完整链路现在进入核心代码部分。我搭的这个最小Agent运行时核心是一个循环取任务、思考、决定调用哪个工具、执行工具、观察结果、再思考直到得出最终答案或达到最大步数。4.1 Agent运行时数据结构与主循环首先定义必要的结构dataclass class AgentStep: thought: str action: str | None action_input: dict | None observation: str | None class AgentRuntime: def __init__(self, model_endpoint, tools, max_iterations8): self.model_endpoint model_endpoint self.tools {tool.name: tool for tool in tools} self.max_iterations max_iterations self.history []主循环的核心判断部分def run(self, task: str) - str: messages self._build_messages(task) for i in range(self.max_iterations): resp self._call_llm(messages) step self._parse_response(resp) if step.action is None: return step.thought if step.action not in self.tools: step.observation fError: 工具 {step.action} 不存在 else: result self._execute_tool(step.action, step.action_input) step.observation self._truncate(result) self.history.append(step) messages.extend([ {role: assistant, content: resp}, {role: tool, content: step.observation} ]) return f已达到最大迭代次数 {self.max_iterations}有一个必须强调的点工具不存在时必须让Agent看到明确的错误信息而不是静默失败。内网排查问题时如果Agent因为某个工具名拼写错误做了10次无用尝试你会非常痛苦。4.2 工具注册表与统一调用接口每个工具都要注册名称、描述和参数schema。LLM根据这些信息来决定调用哪个工具所以工具描述的字段必须准确。dataclass class ToolSpec: name: str description: str parameters: dict function: callable tools_registry [ ToolSpec( namequery_internal_config, description查询内网应用配置中心的配置项参数: key(字符串)。, parameters{ type: object, properties: { key: {type: string, description: 配置项键名} }, required: [key] }, functionquery_internal_config ), ToolSpec( nameexecute_sql, description执行只读SQL查询参数: sql(字符串)。只允许SELECT语句。, parameters{ type: object, properties: { sql: {type: string, description: SELECT语句} }, required: [sql] }, functionexecute_sql ) ]这里有个细节工具描述最好写清楚失败条件比如数据库连接失败时会返回错误码这样LLM不至于因为一次失败就反复重试同一个错误。4.3 上下文压缩与工具结果截断这个为主循环加上一道保险。工具返回的内容经常很长但Agent真正需要的可能只有前几十行。我在_execute_tool里做了一层统一的后处理def _truncate(self, text: str, max_chars: int 800) - str: if len(text) max_chars: return text head text[:600] tail text[-150:] return head \n...[内容已截断]...\n tail首尾都保留中间截断。因为工具的报错信息通常在末尾而关键的开头部分又能让Agent明白大概内容。同时我在prompt里固定写了一句工具返回结果如果过长只解读首尾。如果信息不足请使用更精确的参数重新查询。此外当估算的token总量超过窗口的70%时我会触发摘要压缩把最早的历史对话转成一个三句话的摘要塞回系统消息里替换原来的历史。这虽然损失了一些细节但至少保住Agent不会彻底失忆。4.4 用最小实现验证闭环整个流程走通后我给Agent一个测试任务查询内网应用配置中心的超时时间参数然后执行一次只读SQL统计今天的告警数量。Agent的表现是先调用query_internal_config拿到超时时间再调用execute_sql执行统计最后汇总输出。这期间模型一共调用了4次都走了工具链路没有一次越权访问或误操作。这说明最小闭环成立后面的精力可以花在安全和运维上。5. 内网安全边界控制Agent比让Agent聪明更重要隔离内网里的Agent有个特殊性它拿着的是内网通行证能触达核心数据。一旦模型被prompt注入或者被不可信输入诱导就可能执行危险操作。所以安全设计不是锦上添花是生存底线。5.1 内网Agent的真实风险清单我按照危害级别列过一份清单prompt注入用户输入或工具返回内容中包含恶意指令诱导Agent执行未授权操作。在公网可能只是误下载文件在内网可能是触碰生产库。工具越权Agent调用了它没权限用的工具或者用合理参数调了不该调的数据。比如查询接口如果允许传任意表名Agent理论上可以读全库。权限放大Agent运行的进程如果是以root或管理员身份跑的那它一旦被攻破整个内网节点就沦陷了。资源滥用Agent循环执行一个查询把内网服务打到过载。这种问题在公网只是成本问题在内网可能是生产故障。5.2 工具白名单与参数校验落地对每个工具我在注册之外又加了一层运行时校验器def safe_execute_sql(sql: str) - str: sql_stripped sql.strip().rstrip(;) if not sql_stripped.lower().startswith(select): return 拒绝执行只允许SELECT语句 forbidden_keywords [insert, update, delete, drop, alter, grant, --] if any(kw in sql_stripped.lower() for kw in forbidden_keywords): return 拒绝执行检测到禁止关键词 return execute_sql_with_ro_user(sql_stripped)这里有三个关键点第一SQL语句只允许以SELECT开头且所有写操作关键词直接拒绝第二数据库连接用的是只读账号即使工具逻辑出了漏洞也无法写入第三所有参数在传给实际函数前必须先经过校验函数不能让Agent把任意字符串直接拼接给系统命令。再来一个系统命令工具的例子def safe_run_cmd(cmd: str) - str: allowed_prefixes (ps , df , free, uptime, cat /opt/agent/logs/) if not any(cmd.startswith(p) for p in allowed_prefixes): return 拒绝执行命令不在白名单 if re.search(r[;|$], cmd): return 拒绝执行包含非法字符 return subprocess.run(cmd.split(), capture_outputTrue, textTrue, timeout10).stdout注意我用了startswith前缀白名单同时用正则把;、、|、、、$这些危险字符全部干掉。这是防注入最基础的一层宁可让Agent多绕几步也不能放危险命令过去。5.3 审计日志与人工确认机制每个Agent步骤都记审计日志日志内容包括用户请求、Agent思考内容、工具名、工具参数、工具结果摘要、时间戳、Agent版本。格式我用JSON Lines方便后续接入内网日志平台{ts: 2025-06-10T14:22:01Z, req_id: job-001, step: 1, thought: 需要先查询配置, tool: query_internal_config, input: {key: timeout}, output_preview: 3000ms, status: ok}对于高危险工具例如批量导入、停机重启、写文件我加入了人工确认机制Agent发出请求后进入pending状态由操作员在Web控制台确认后才实际执行。初期跑路期间人工确认的比例我设得比较高宁可多一步审批也不能让Agent自动触碰生产环境。提示安全设计不是静态的。我建议上线前做一次agent红队演练准备几段恶意prompt和超长工具输出看它是否会被诱导执行非法操作。这个步骤非常重要比写再多防护代码都管用。6. 部署上线与运维监控能跑半年不重启才算交付Agent写完只是开始。隔离内网的运维有个特点你没法像公网那样随时远程登录改代码进一次机房往往要提前审批、申请工单。所以部署和监控必须一次做对并且要能独立自愈。6.1 服务化部署与守护进程我用systemd把Agent做成常驻服务。一个典型的unit文件是这样[Unit] DescriptionAI Agent Runtime Afternetwork.target vllm.service [Service] Useragentuser Groupagentgroup WorkingDirectory/opt/agent ExecStart/opt/agent/venv/bin/python /opt/agent/main.py Restartalways RestartSec10 EnvironmentFile/etc/agent/env.conf [Install] WantedBymulti-user.target注意User指定成普通用户agentuser绝不能用root。WorkingDirectory也固定住避免Agent因为工作目录不对找不到文件。Restartalways保证崩溃后自动拉起。6.2 可观测性三件套落地内网运维必须有一眼看出问题的手段。我的三件套是健康检查、指标监控、离线日志补采。健康检查最简单启动一个HTTP服务暴露/healthz端点返回Agent进程状态和最近一次LLM调用延迟。与此同时写一个定时curl脚本超过阈值就发内部告警内网里有自己的监控平台直接对接HTTP回调。指标监控用Prometheus格式暴露以下指标agent_steps_total总步骤数agent_tool_calls_total各工具调用次数agent_llm_latency_seconds模型调用耗时直方图agent_invalid_tool_calls_total非法工具调用次数这些指标能直接反映Agent是否在空转、工具是否被频繁调用、模型响应是否变慢。日志方面由于内网可能没有集中日志服务我先写到本地目录并写了个定期打包脚本把旧日志压缩转移。这样即使哪天真要排查也能在文件里找到完整链路。6.3 模型与代码的迭代更新机制隔离内网上线之后最难受的事就是模型升级。公网上模型权重一个命令就能拉新内网里你得走一次模型文件导入流程。我的做法是模型文件保留在目录/opt/models/每次升级至少保留两个版本当前版上一个版本。同时在Agent配置里加上模型名参数如果推理服务切换模型Agent重连后自动生效。代码更新也类似用发布包tar交付解压后通过systemctl restart agent-restart执行。版本号写在文件开头每次启动都会记录日志里能追到每个请求是用哪个版本跑的。7. 踩坑实录隔离内网才能遇到的疑难杂症最后这部分我把这次实战里真正让人抓狂的四个坑记下来。这些坑每一个都花了我至少半天时间希望你看了之后能绕开。7.1 DNS解析把推理服务指向了错误的IP第一次部署完成后Agent始终报连接错误。我在内网里排查时发现LLM服务的端口能通但curl另一个服务时总解析到一台已经不用的旧机器IP。原因内网DNS缓存和hosts文件存在冲突记录而我的Agent代码里没配置超时导致每次请求要等很久才失败。解决办法在Agent里不要依赖默认DNS解析直接在配置连接地址时指向具体IP和端口并且所有HTTP客户端都设置connect timeout5秒。另外把内网服务主机名映射写死在/etc/hosts里避免DNS波动。7.2 离线wheel在目标机器上编译失败这是我在准备离线依赖时踩的最大的坑。我download了一个包以为wheel是预编译好的结果安装时它竟然尝试现场编译——目标机上没装gcc直接报错。原因是我拿wasm的包源去下载没有强制--platform参数。后来我重新在跳板机上用之前说的参数拉取强制only-binary并且对每个包都手动确认它是manylinux的wheel而不是sdist。一个简单的检查方法解压wheel看看里面有没有.so文件。有.so且平台标签正确才是真离线可用。7.3 Agent死循环与上下文污染测试过程中Agent在查配置的时候陷入了一个循环每次拿到结果都很长但它没读到关键值于是反复用同样参数查询直到把上下文塞满模型终于忘记初始任务开始编答案。解决用了三重手段第一max_iterations强行限制第二工具返回内容按截断策略压缩第三在prompt里明确提示如果第一次查询结果不完整请换一种查询参数不要重复相同请求。之后这种循环基本不再出现。你可能觉得这是小事但在内网环境里Agent每空转一次都是在消耗一台昂贵推理机的算力不可忽视。7.4 并发调用LLM导致OOM正式切换vLLM后我直接开了8并发调用还没跑几分钟机器就OOM了。原因是我在vLLM启动参数里没设置--max-num-seqs默认值在我的显卡配置下不够安全。同时vLLM虽然支持并发但每个序列的KV Cache会持续占用显存如果一次塞入太多长请求显存瞬间见底。解决方法是限制并发上限并调低--gpu-memory-utilization到0.8给系统留出余量。这也提醒我内网推理机的资源规划不能只按模型大小估还要按并发峰值估。尾声隔离内网下的AI Agent工程真正的难点从来不在Agent本身而在于你必须在资源受限、依赖封闭、安全高压的三重条件下把每一个环节都做成可控的、可追踪的、可回滚的。如果你正在部署同类的系统我的建议很简单架构往小了做、依赖往少了做、权限往紧了做、日志往细了做。只要这四条守住哪怕Agent写得粗一点它也会是个稳定、安全的工程交付物。模型选型、框架选型都可以后期再换但边界和监控一定得一开始就扎稳。