1. 为什么要在隔离内网里折腾 AI Agent
先把场景说清楚。所谓隔离内网,就是那种物理上跟公网断开、或者只允许极少数白名单流量出入的办公网、生产网、涉密研发网。这类环境里,你没法直接pip install openai,没法调云端大模型 API,甚至连 GitHub 都拉不下来。但偏偏这类环境里的需求一点都不少:代码仓库检索、日志分析、工单自动分类、内部知识库问答、批量文档处理,这些活儿用 AI Agent 来干,效率提升是肉眼可见的。
我前后在三个不同规模的内网环境里落地过 AI Agent,从几十人的小团队到上千人的研发中心都趟过。最大的感受是:内网 AI Agent 的难点从来不是模型本身,而是"工程化"这三个字。公网上你随便找个框架跑个 demo 就能发朋友圈,内网里你要考虑的是模型怎么进去、依赖怎么装、Agent 怎么跟内部系统对接、并发上来之后怎么不崩、出了问题怎么排查。这一整套东西,才是真正卡住绝大多数人的地方。
这篇内容适合三类人看:一是被派了"在内网搞个 AI 助手"任务但不知道从哪下手的工程师;二是已经跑通了 demo 但一上量就各种报错的开发者;三是想系统了解 AI Agent 工程化落地全流程的技术负责人。我会把 MCP、Skills、并发扛压、内网部署这些热搜词背后的东西,用我自己踩过的坑串起来讲一遍。不吹概念,只讲能直接抄作业的东西。
2. 内网 AI Agent 的整体架构设计思路
2.1 先想清楚:内网 Agent 和公网 Agent 的本质差异
很多人第一次做内网 Agent,习惯性地把公网那套架构照搬进来,结果处处碰壁。根子上是因为两者的约束条件完全不同。
公网 Agent 的典型架构是:你的程序 → 调用云端大模型 API → 模型侧完成推理 → 返回结果。你的机器基本就是个"传话筒",算力、模型、工具生态全在云上。而内网 Agent 必须把这一整条链路全部搬到本地:模型要本地部署,推理要本地算力,工具调用要本地实现,连依赖包都得提前离线准备好。
这个差异直接决定了三件事。第一,模型选型不能贪大。公网上你可以随便调千亿参数模型,内网里你得看手头有什么卡。一张 24G 显存的消费级卡,跑个 7B 到 14B 的量化模型是比较现实的;如果只有 CPU,那基本只能上 3B 以下的小模型或者用蒸馏版本。第二,工具生态要自己搭。公网 Agent 可以直接调各种现成插件,内网里你得自己写工具函数、自己定义协议。第三,网络通信要重新设计。公网 Agent 走 HTTP 调 API 天经地义,内网里如果 Agent 和模型不在同一台机器上,你得考虑内网服务发现、端口规划、防火墙策略。
我一般建议的架构分层是这样的:最底层是模型推理层,用 vLLM 或者 Ollama 这类推理框架把模型跑起来,对外暴露一个兼容 OpenAI 格式的接口;中间是Agent 编排层,负责 prompt 管理、工具调用、多轮对话状态维护;最上面是业务接入层,对接内网的具体系统,比如 GitLab、Jira、内部 Wiki。这三层之间用内网 HTTP 或者 gRPC 通信,全部走内网地址,不碰公网。
2.2 模型怎么进内网:离线搬运的几种实操路径
这是内网落地的第一道坎,也是最多人卡住的地方。模型文件动辄几个 G 到几十个 G,内网又没法直接下载,怎么办?
我实测下来最稳的方案是"外网下载 + 介质摆渡"。具体操作是:在一台能上外网的机器上,用huggingface-cli download或者modelscope把模型权重、tokenizer、配置文件全部拉下来,打包成一个压缩包。然后通过合规的介质(移动硬盘、内部文件摆渡系统)拷进内网。这里有个细节很多人会忽略:模型目录结构必须完整,包括config.json、tokenizer.json、tokenizer_config.json、special_tokens_map.json以及所有的.safetensors分片文件,少一个都加载不起来。
如果你用的是 GGUF 格式的量化模型(比如通过 Ollama 部署),那就更简单,单个文件拷进去就行。但要注意量化等级的选择:Q4_K_M 是性价比最高的档位,Q5 以上质量更好但显存占用明显上升,Q3 以下质量下降比较厉害,除非显存实在紧张否则不建议。
提示:搬运前务必核对模型的 SHA256 校验值,内网环境里文件损坏是排查起来最痛苦的问题之一,一个字节的差异可能导致模型加载时报出完全看不懂的错误。
还有一种情况是内网有私有镜像仓库。这种情况下可以把推理框架(比如 vLLM)打成 Docker 镜像,在外网构建好之后推到私有仓库,内网直接拉取。模型文件则通过挂载卷的方式提供。这种方式适合规模化的团队,一次搭建好之后,后续部署新模型就是改改配置的事。
2.3 MCP 协议在内网场景下的价值与取舍
MCP(Model Context Protocol)这两年被提得很多,它的核心价值是给模型和外部工具之间定了一套标准化的通信协议。公网生态里 MCP 已经有不少现成的 server 可以用,但在内网里,MCP 的意义要重新评估。
我的判断是:内网里 MCP 值得用,但不要为了用而用。它的真正价值在于解耦。假设你的 Agent 需要访问内网的 GitLab、Jira、数据库三个系统,如果每个都硬编码在 Agent 代码里,那后续任何一个系统接口变了,你都得改 Agent 主体逻辑。而用 MCP 的话,每个系统对应一个 MCP server,Agent 只跟 MCP 协议打交道,系统接口变了只改对应的 server 就行。
内网里搭 MCP server 有个坑要注意:MCP 默认的通信方式有 stdio 和 SSE 两种。stdio 方式适合 Agent 和 server 在同一台机器上,简单直接;SSE 方式适合跨机器,但内网里 SSE 的长连接容易被防火墙或者负载均衡掐断。我一般建议内网优先用 stdio,如果必须跨机器,就用 HTTP 短连接轮询的方式自己封装一层,别硬上 SSE。
另外,MCP server 本身也是要写代码的,内网里没有现成的 npm 包或者 pip 包可以随便装,所以依赖要提前在外网准备好,打成离线包带进去。这一点在规划阶段就要考虑到,别等到写代码的时候才发现装不上依赖。
3. Skills 机制:让 Agent 真正"会干活"的关键
3.1 Skills 到底是什么,为什么它比单纯的工具调用更强
Skills 这个概念最近很火,但很多人理解得比较浅,以为就是"给 Agent 加几个函数"。实际上 Skills 的本质是把领域知识和操作流程封装成可复用的能力单元。
举个具体的例子。你要让 Agent 帮你分析内网的一台服务器为什么 CPU 飙高。如果只是工具调用,你给 Agent 一个"执行 shell 命令"的工具,它可能会瞎跑一堆命令,效率很低。但如果你封装一个"服务器性能诊断"的 Skill,里面预置了诊断流程:先看top找高占用进程,再看该进程的线程状态,再查对应的日志,最后给出结论。这个 Skill 里既有工具调用,也有流程编排,还有领域经验。Agent 拿到这个 Skill,就相当于一个新手拿到了老师傅的诊断手册。
在内网环境里,Skills 的价值被进一步放大。因为内网的业务系统往往有很强的特殊性,公网模型根本不知道你们内部的工单系统长什么样、代码规范是什么、审批流程怎么走。这些知识只能通过 Skills 注入进去。
3.2 内网 Skills 的设计原则:原子化、可组合、可测试
我设计内网 Skills 的时候,遵循三个原则。
原子化是指每个 Skill 只干一件事,粒度要小。比如"查询工单状态"是一个 Skill,"修改工单优先级"是另一个 Skill,不要把一堆操作塞进一个 Skill 里。这样做的好处是复用性强,组合灵活。
可组合是指 Skill 之间可以互相调用。比如"生成周报"这个 Skill,内部可以调用"查询本周工单"、"统计代码提交"、"汇总会议记录"三个子 Skill。这种组合关系用配置文件描述,不要硬编码在代码里,方便后续调整。
可测试是指每个 Skill 都要能单独测试。内网环境调试困难,如果 Skill 只能整体跑起来才能验证,那排查问题会非常痛苦。我的做法是给每个 Skill 写一个简单的测试入口,输入固定的参数,看输出是否符合预期。
下面是一个 Skill 定义的简化示例,用 YAML 描述:
name: query_ticket_status description: 根据工单ID查询内网工单系统的当前状态 parameters: - name: ticket_id type: string required: true description: 工单编号,格式为 TK-数字 implementation: type: http method: GET url: http://internal-jira/api/ticket/{ticket_id} headers: Authorization: Bearer ${INTERNAL_TOKEN} response_mapping: status: $.fields.status.name assignee: $.fields.assignee.displayName updated: $.fields.updated这种声明式的写法有个好处:非开发人员也能看懂,改起来不用动代码。内网里经常出现的情况是,业务方想调整某个 Skill 的行为,但开发排期很紧,如果 Skill 是配置化的,业务方自己就能改。
3.3 Skills 的加载与调度:内网里的性能考量
Skills 多了之后,怎么让 Agent 快速找到该用哪个 Skill,是个工程问题。公网上很多框架用的是"把所有 Skill 的描述塞进 prompt,让模型自己选",这在 Skill 数量少的时候没问题,但内网里如果 Skill 有几十上百个,prompt 会变得极长,推理速度直线下降。
我的做法是两级检索。第一级用关键词或者向量检索,从所有 Skill 里粗筛出 Top 10 候选;第二级把这 10 个 Skill 的详细描述塞进 prompt,让模型做最终选择。这样既保证了准确率,又控制了 prompt 长度。
向量检索在内网里需要本地部署一个 embedding 模型,用个小模型就行,比如 bge-small 这类,几百兆的大小,CPU 也能跑。把所有 Skill 的描述预先向量化存起来,查询的时候算一下相似度。这套东西搭一次,后续加 Skill 就是往索引里加一条记录的事。
注意:Skill 的描述文本质量直接决定检索准确率。描述要写清楚"这个 Skill 干什么、什么时候用、输入输出是什么",别写得太抽象。我见过有人把 Skill 描述写成"处理数据",这种描述检索出来全是噪声。
4. 并发扛压:内网 Agent 最容易翻车的地方
4.1 内网 Agent 的并发瓶颈到底在哪
公网 Agent 谈并发,瓶颈通常在 API 限流或者网络延迟。内网 Agent 的瓶颈完全不一样,我总结下来主要是三个:模型推理的显存瓶颈、Agent 编排层的状态管理、以及工具调用的阻塞。
模型推理这块,一张卡同时能处理多少请求,取决于模型大小、量化等级、上下文长度和推理框架的调度策略。以 7B 模型 Q4 量化、4K 上下文为例,一张 24G 卡用 vLLM 部署,并发 8 到 16 路是比较舒服的区间,再往上延迟会明显上升。这个数字不是拍脑袋来的,是实测出来的:并发 8 的时候首 token 延迟大概 300ms,并发 16 的时候涨到 800ms 左右,并发 32 的时候直接飙到 2 秒以上,用户体验就很差了。
Agent 编排层的状态管理是很多人忽略的坑。Agent 处理一个请求往往要经过多轮"思考-调用工具-再思考",这个过程中会话状态要一直保持。如果编排层是无状态的,每个请求都重新加载上下文,那并发一上来内存直接爆。我的做法是用 Redis 或者内存数据库存会话状态,设置合理的过期时间,请求之间共享状态存储。
工具调用的阻塞是最隐蔽的。假设你的 Agent 调了一个内网接口,这个接口响应要 5 秒,那这 5 秒里 Agent 的线程就被占住了。并发一高,线程池瞬间打满。解决办法是把所有工具调用改成异步的,用asyncio或者线程池管理,别让一个慢接口拖垮整个 Agent。
4.2 实测有效的并发优化手段
先说模型层。如果显存允许,开启连续批处理(continuous batching)是提升吞吐最有效的手段。vLLM 默认就支持,Ollama 新版本也支持了。它的原理是把多个请求的动态 batch 拼在一起推理,GPU 利用率能从 30% 提到 70% 以上。开启方式很简单,vLLM 启动时加--enable-chunked-prefill参数就行。
再说编排层。请求队列 + 限流是必须的。我一般用信号量控制同时进行的 Agent 会话数,超过阈值的请求进队列等待,而不是直接拒绝。队列长度也要设上限,防止内存无限增长。具体参数怎么定?我的经验公式是:队列长度 = 并发数 × 3,这样既能吸收突发流量,又不会积压太多导致超时。
工具调用层,超时和重试必须配好。内网接口不稳定是常态,一个接口超时 30 秒不返回,如果不设超时,Agent 就卡死在那了。我的配置是:普通查询接口超时 5 秒,重试 2 次;写操作接口超时 10 秒,不自动重试(避免重复写入)。重试要加退避,别一失败就立刻重试,那样会把下游打垮。
下面是一个用 Python asyncio 做并发控制的简化示例:
import asyncio from asyncio import Semaphore class AgentPool: def __init__(self, max_concurrent=8, queue_size=24): self.semaphore = Semaphore(max_concurrent) self.queue = asyncio.Queue(maxsize=queue_size) async def handle_request(self, request): if self.queue.full(): return {"error": "系统繁忙,请稍后重试"} await self.queue.put(request) async with self.semaphore: await self.queue.get() return await self._process(request) async def _process(self, request): # 实际的 Agent 处理逻辑 ...这段代码的核心是信号量控制并发数,队列控制积压量。实测下来,8 并发 + 24 队列的配置,在 7B 模型上能稳定支撑 50 人左右的小团队日常使用。
4.3 压测怎么做:内网环境下的土办法
内网里没有公网那些现成的压测服务,得自己想办法。我一般用locust或者自己写个脚本,模拟多个用户同时发请求。关键是压测数据要真实,别用"你好"这种简单请求,要用实际业务里最复杂的那些 query,因为复杂 query 的 token 数多,推理时间长,才是真正的压力来源。
压测的时候重点看三个指标:P95 延迟、错误率、GPU 利用率。P95 延迟反映大多数用户的体验,错误率反映系统稳定性,GPU 利用率反映资源是否浪费。我的经验是,P95 延迟控制在 3 秒以内、错误率低于 1%、GPU 利用率在 60% 到 80% 之间,是比较健康的状态。
压测过程中如果发现 GPU 利用率很低但延迟很高,那瓶颈多半在编排层或者工具调用层,要去查是不是有同步阻塞。如果 GPU 利用率打满但延迟还能接受,那说明模型层是瓶颈,考虑加卡或者换更小的模型。
5. 内网部署的完整实操流程
5.1 环境准备:从裸机到可运行
假设你拿到了一台内网服务器,配置是 32 核 CPU、128G 内存、一张 24G 显存的卡,操作系统是 Ubuntu 22.04。从零开始搭一个能跑的 AI Agent 环境,步骤是这样的。
第一步,装基础依赖。内网没法apt update,所以要提前在外网把需要的 deb 包下载好,用dpkg -i离线安装。核心依赖包括:Python 3.10 以上、CUDA 驱动和 toolkit、Docker(如果用容器化部署的话)。CUDA 版本要和推理框架匹配,vLLM 0.4 以上版本一般要求 CUDA 12.1 以上。
第二步,准备 Python 环境。用 conda 或者 venv 建一个独立环境,然后把推理框架和 Agent 框架的离线 wheel 包拷进来装。这里有个技巧:在外网用pip download把所有依赖下载到一个目录,注意要指定平台和 Python 版本,比如pip download vllm --platform manylinux2014_x86_64 --python-version 310 --only-binary=:all:,这样下下来的包才能在内网直接用。
第三步,部署模型。把模型文件放到指定目录,用 vLLM 启动服务:
python -m vllm.entrypoints.openai.api_server \ --model /data/models/qwen2-7b-instruct \ --served-model-name qwen2-7b \ --tensor-parallel-size 1 \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --port 8000参数说明:tensor-parallel-size是张量并行数,单卡就填 1;max-model-len是最大上下文长度,根据显存调整,24G 卡跑 7B 模型 8K 上下文比较稳;gpu-memory-utilization是显存占用比例,0.9 表示用 90% 的显存,留一点给系统。
第四步,验证模型服务。用 curl 测一下:
curl http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model": "qwen2-7b", "messages": [{"role": "user", "content": "你好"}]}'能正常返回就说明模型层通了。
5.2 Agent 编排层的搭建与配置
模型服务通了之后,接下来搭 Agent 编排层。我一般用 FastAPI 做服务框架,因为它轻量、异步支持好、内网部署简单。
核心代码结构是这样的:一个agent.py负责 Agent 的主逻辑,一个skills/目录放所有 Skill 的实现,一个config.yaml放配置。Agent 主逻辑要做的事包括:接收请求、加载会话历史、检索相关 Skill、构造 prompt、调用模型、解析模型输出、执行工具调用、返回结果。
这里有个关键设计:prompt 模板要可配置。内网里不同业务场景需要的 prompt 差别很大,硬编码在代码里改起来麻烦。我一般把 prompt 模板放在配置文件里,用 Jinja2 渲染,改 prompt 不用重启服务。
工具调用的解析是另一个重点。模型输出的工具调用格式可能不规范,要做容错。我的做法是用正则先提取,提取失败就尝试 JSON 解析,再失败就让模型重新生成。这个重试逻辑要设上限,最多重试 2 次,避免死循环。
5.3 与内网业务系统的对接
Agent 要真正干活,必须能访问内网的各种系统。对接方式取决于系统的开放程度。
如果系统有 REST API,那最简单,直接写个 HTTP 客户端调用就行。注意内网 API 的认证方式,常见的有 token、cookie、mTLS 几种。token 方式最普遍,把 token 放在配置文件里,注意权限控制,别用管理员账号。
如果系统只有数据库访问权限,那就直连数据库查询。这里要特别注意只读权限,Agent 绝对不能有写权限,否则模型一个幻觉就可能把数据改了。我一般给 Agent 配一个只读账号,只能 SELECT,不能 INSERT/UPDATE/DELETE。
如果系统啥接口都没有,只能通过命令行操作,那就用 subprocess 调用。这种方式最脆弱,因为命令行输出格式可能变,解析容易出错。我的建议是尽量推动业务方提供 API,实在不行再用命令行,并且要做好输出解析的容错。
提示:所有对接外部系统的 Skill,都要加详细的日志。内网排查问题全靠日志,日志里要记录请求参数、响应内容、耗时、错误信息。日志级别用 INFO 就行,别用 DEBUG,否则日志量太大。
6. 常见问题与排查技巧实录
6.1 模型加载失败类问题
问题一:模型加载时报 "out of memory"。这是最常见的。原因通常是显存不够,或者gpu-memory-utilization设得太高。解决办法:先降低max-model-len,从 8192 降到 4096 试试;还不行就换更小的量化等级,从 Q5 换到 Q4;再不行就换更小的模型。
问题二:模型加载时报 "config.json not found"。这是模型文件不完整。检查模型目录下是否有config.json、tokenizer.json等文件,对比外网下载时的文件列表,缺啥补啥。
问题三:模型能加载但推理输出乱码。这通常是 tokenizer 不匹配,或者模型文件损坏。先核对 SHA256,再检查 tokenizer 配置。
6.2 并发相关问题的排查
问题一:并发一上来就报 "connection refused"。这是模型服务的连接数打满了。vLLM 默认的连接数有限,启动时加--max-num-seqs参数调大,比如设成 64。
问题二:并发时延迟忽高忽低。这是批处理调度的问题。检查是否开启了连续批处理,如果开了还这样,可能是请求的 token 长度差异太大,长请求把短请求堵住了。解决办法是给请求按长度分队列,短请求优先处理。
问题三:Agent 处理到一半卡住不动。这多半是工具调用阻塞了。检查工具调用的超时设置,看是不是某个接口没返回。用py-spy这类工具 dump 一下线程栈,能快速定位卡在哪。
6.3 内网特有的坑
坑一:DNS 解析慢。内网 DNS 服务器如果配置不当,每次请求解析域名都要几百毫秒。解决办法是在/etc/hosts里把常用的内网域名写死,绕过 DNS。
坑二:防火墙静默丢包。内网防火墙有时候不返回 RST,直接丢包,导致连接一直挂着直到超时。这种问题最难查,因为从应用层看就是"卡住"。排查方法是抓包,看请求发出去了但没响应,那就是被防火墙拦了。
坑三:时间不同步。内网机器如果时间不同步,会导致 token 过期、日志时间错乱等问题。部署前先确认 NTP 服务正常,所有机器时间一致。
下面整理一个常见问题速查表:
| 现象 | 可能原因 | 排查方法 | 解决手段 |
|---|---|---|---|
| 模型加载 OOM | 显存不足 | 看 nvidia-smi | 降上下文/降量化/换小模型 |
| 推理输出乱码 | tokenizer 不匹配 | 核对模型文件 | 重新拷贝完整模型 |
| 并发连接拒绝 | 连接数打满 | 看服务日志 | 调大 max-num-seqs |
| 延迟忽高忽低 | 批处理调度问题 | 看请求长度分布 | 按长度分队列 |
| Agent 卡住 | 工具调用阻塞 | py-spy dump 线程栈 | 加超时/改异步 |
| DNS 解析慢 | DNS 配置问题 | 测解析耗时 | 写 hosts |
| 连接挂起 | 防火墙丢包 | 抓包分析 | 调整防火墙策略 |
6.4 几个我踩过的坑和独家技巧
技巧一:模型预热。服务启动后,先发几个请求把模型"跑热",让 CUDA kernel 编译好、显存分配好,这样正式请求进来时延迟会低很多。我一般写个预热脚本,启动后自动跑 10 个请求。
技巧二:日志分级。内网排查问题全靠日志,但日志太多又影响性能。我的做法是:正常请求只记 INFO 级别,记录请求 ID、耗时、结果状态;异常请求记 ERROR 级别,记录完整上下文。这样既能排查问题,又不会日志爆炸。
技巧三:灰度上线。内网 Agent 上线别一次性全量,先找几个愿意尝鲜的同事试用,收集反馈,迭代几轮再推广。我见过太多一上线就被吐槽然后项目黄了的案例。
技巧四:留后门。这里说的后门不是安全后门,是"降级开关"。Agent 如果出问题,要能一键切回原来的工作方式。比如 Agent 挂了,用户还能通过原来的工单系统提需求。这个降级开关在项目初期就要设计好,别等出事了才想。
7. 关于内网 Agent 工程化的一些个人体会
做内网 AI Agent 这几年,我最大的体会是:技术选型要保守,工程实现要扎实,别追新。公网上那些花里胡哨的框架和技巧,内网里大部分用不上,或者用起来成本极高。反而是那些最基础的东西——稳定的模型服务、清晰的架构分层、完善的日志监控、合理的并发控制——才是决定项目成败的关键。
另一个体会是,内网 Agent 的迭代速度天然比公网慢。公网上你今天发现个新框架,明天就能试;内网里你发现个新东西,光是把依赖搬进去就要好几天。所以内网项目的规划要做得更长远,选型的时候多考虑"这个东西半年后还维护吗"、"社区活跃吗"、"离线部署方便吗"。
最后说个具体的:如果你现在正准备在内网搞 AI Agent,我的建议是先跑通最小闭环。别一上来就想着做多牛的功能,先把"模型能跑起来、Agent 能调通一个工具、用户能问一个问题得到回答"这条链路走通。这条链路走通了,后面加功能、优化性能都是在这个基础上迭代。链路没走通,想再多都是空中楼阁。
至于后续扩展,我个人的方向是把 Skills 做得更细、更贴合业务,同时把并发能力再往上提一提。内网里用户量虽然不大,但大家对响应速度的要求一点不比公网低。这块还有不少优化空间,等有新进展再跟大家分享。