1. 隔离内网下的 AI Agent 工程实战:从零搭建到稳定运行
很多做企业级交付的朋友都遇到过这种场景:客户现场是一套完全物理隔离的内网,没有外网出口,没有公网镜像源,甚至连 pip install 都得靠离线包。偏偏这时候甲方提了个需求——"你们这个系统能不能加个 AI Agent,帮我们自动处理工单、查知识库、跑数据分析"。这就是我最近半年反复在踩的坑,也是这篇文章想聊透的事。
隔离内网下的 AI Agent 工程实战,核心要解决的不是"Agent 能不能跑起来",而是"在一个没有外网、没有现成工具链、算力受限、运维窗口极窄的环境里,怎么把 Agent 从 demo 做成能扛住业务的生产系统"。它涉及模型本地化部署、MCP 协议在内网的适配、Skills 能力的离线封装、工具调用的权限收敛、以及一整套不依赖公网的工程化流程。适合谁看?适合正在做 ToB/ToG 交付的后端、算法、运维工程师,也适合想搞清楚"Agent 落地到底难在哪"的技术负责人。如果你只在外网环境玩过 LangChain 或者扣子这类平台,那内网这一套会让你重新认识什么叫"工程"。
我下面讲的所有内容,都基于一个真实项目的抽象:某单位内网,约 200 台服务器,Agent 需要对接 3 个业务系统、1 个知识库、1 套报表工具,日均调用量峰值在 3000 次左右,全程无外网。所有方案我都实际跑过,踩过的坑会明确标出来。
2. 内网 Agent 的整体架构设计与选型逻辑
2.1 为什么不能照搬外网那套架构
外网做 Agent,大家习惯的套路是:调 OpenAI 或者 Claude 的 API,用 LangChain 编排,工具通过 MCP 或者 Function Calling 挂上去,向量库用云服务,日志丢到云端可观测平台。这套东西在内网里几乎全部失效——API 调不通、云服务连不上、依赖装不上。
所以内网 Agent 的第一原则是:所有组件必须能离线自持。模型要本地跑,向量库要本地部署,工具调用要本地闭环,连日志都得自己搭一套。这不是"能不能简化"的问题,而是"少了任何一环整个链路就断"的问题。
我当时的架构决策是这样的:
| 层级 | 外网常见方案 | 内网替代方案 | 选型理由 |
|---|---|---|---|
| 模型层 | GPT-4/Claude API | Qwen2.5-14B-Instruct 本地部署 | 中文能力强,14B 在单张 A100 上能跑,量化后 4090 也能扛 |
| 编排层 | LangChain/LangGraph | LangGraph + 自研状态机 | LangGraph 支持离线安装,状态机便于审计 |
| 工具协议 | MCP over HTTP | MCP over stdio | 内网无稳定 HTTP 服务发现,stdio 更可靠 |
| 向量库 | Pinecone/云 Milvus | Milvus 单机版 | 离线部署简单,支持本地磁盘 |
| 可观测 | LangSmith | 自研日志 + Prometheus | 无外网,只能自建 |
这张表看着简单,但每一行的选择背后都是被坑出来的。比如模型层,一开始想用 7B 省资源,结果工具调用的准确率惨不忍睹,经常把参数填错,最后还是上了 14B。再比如 MCP,一开始想走 HTTP,结果内网的服务注册发现机制不健全,Agent 经常找不到工具服务,改成 stdio 之后稳定多了。
2.2 MCP 协议在内网里的定位
MCP(Model Context Protocol)这两年被聊得很多,但很多人对它的理解停留在"让模型调用工具"这个层面。在内网环境里,MCP 的真正价值是把工具能力和模型解耦。
什么意思?假设你的 Agent 需要查工单、查知识库、跑报表三个能力。如果不用 MCP,你得在 Agent 代码里硬编码这三个工具的调用逻辑,模型换了、工具改了,代码就得重写。用了 MCP 之后,每个工具是一个独立的 MCP Server,Agent 只负责"发现工具、调用工具",工具内部怎么实现、用什么语言写,Agent 完全不关心。
在内网里这一点尤其重要,因为内网的业务系统往往是异构的——有的用 Java,有的用 Python,有的甚至是老掉牙的 C# 系统。MCP 让每个团队可以独立维护自己的工具服务,Agent 侧只需要一份统一的协议描述。
提示:内网部署 MCP Server 时,优先选 stdio 模式而不是 SSE/HTTP 模式。stdio 不依赖网络端口,进程生命周期由 Agent 管理,出问题好排查。HTTP 模式在内网里经常因为防火墙策略、端口占用、服务发现失败而翻车。
2.3 Skills 的离线封装思路
Skills 这个概念最近很火,Claude 的 Agent Skills、Codex 的 Skills 都在推。本质上 Skills 就是"预封装的能力包"——把一段提示词、一组工具、一套流程打包成一个可复用的单元。
内网里做 Skills 封装,我的经验是按业务场景切,不要按技术能力切。比如"工单处理"是一个 Skill,"知识库问答"是一个 Skill,"报表生成"是一个 Skill。每个 Skill 内部包含:触发条件、所需工具、提示词模板、输出格式约束、失败兜底逻辑。
这样做的好处是,业务方提需求的时候可以直接说"我要一个 XX Skill",而不是"我要一个能查数据库又能调 API 的 Agent"。交付边界清晰,测试也好做。
3. 核心组件在内网环境下的落地细节
3.1 模型本地化部署:显存、量化与推理框架
模型部署是内网 Agent 的第一道坎。我实测下来,Qwen2.5-14B-Instruct 用 vLLM 部署,单张 A100 80G 可以跑 FP16,吞吐大概在 1500 tokens/s 左右;如果用 4090 24G,必须上 AWQ 4bit 量化,吞吐掉到 400 tokens/s 左右,但够用。
部署命令大概是这样:
python -m vllm.entrypoints.openai.api_server \ --model /data/models/Qwen2.5-14B-Instruct-AWQ \ --quantization awq \ --dtype float16 \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --port 8000 \ --served-model-name qwen-agent几个关键参数的解释:
--max-model-len 8192:Agent 场景下上下文经常很长(系统提示词 + 工具描述 + 历史对话),8192 是底线,能上 16384 更好,但显存会吃紧。--gpu-memory-utilization 0.9:留 10% 给 KV Cache 之外的显存开销,设太高容易 OOM。--quantization awq:4bit 量化,精度损失在 Agent 场景下可以接受,但工具调用的参数格式偶尔会出错,需要在提示词里加强约束。
注意:内网部署模型时,一定要提前把模型权重、tokenizer、配置文件全部拷贝到本地。vLLM 启动时会去 HuggingFace 拉配置,内网拉不到会直接报错。用
--model指向本地目录,并且确保目录里有完整的config.json、tokenizer.json、tokenizer_config.json。
3.2 MCP Server 的离线开发与注册
MCP Server 的开发本身不复杂,官方有 Python 和 TypeScript 的 SDK。内网里的难点在于依赖安装和服务注册。
依赖安装这块,我的做法是:在一台有外网的机器上,用pip download把所有依赖下成 wheel 包,然后拷贝到内网,用pip install --no-index --find-links=./wheels安装。MCP SDK 的依赖不多,主要是mcp、pydantic、httpx这几个。
一个最简单的 MCP Server 长这样:
from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent app = Server("ticket-server") @app.list_tools() async def list_tools(): return [ Tool( name="query_ticket", description="根据工单号查询工单详情", inputSchema={ "type": "object", "properties": { "ticket_id": {"type": "string", "description": "工单编号"} }, "required": ["ticket_id"] } ) ] @app.call_tool() async def call_tool(name: str, arguments: dict): if name == "query_ticket": ticket_id = arguments["ticket_id"] # 这里调用内网业务系统的接口 result = query_internal_api(ticket_id) return [TextContent(type="text", text=result)] async def main(): async with stdio_server() as (read, write): await app.run(read, write, app.create_initialization_options()) if __name__ == "__main__": import asyncio asyncio.run(main())服务注册这块,内网没有 Consul 或者 Nacos 的话,最简单的办法是用配置文件管理。Agent 启动时读取一个mcp_servers.json,里面列出所有 MCP Server 的启动命令和参数:
{ "mcpServers": { "ticket": { "command": "python", "args": ["/opt/mcp/ticket_server.py"] }, "knowledge": { "command": "python", "args": ["/opt/mcp/knowledge_server.py"] } } }Agent 侧用 MCP Client 逐个拉起这些进程,通过 stdio 通信。这种方式的缺点是进程管理要自己做,好处是简单、可控、不依赖任何外部服务。
3.3 向量库与知识库的离线构建
知识库问答是 Agent 最常见的场景。内网里做 RAG,向量库我选的是 Milvus 单机版,用 Docker 部署,数据落本地磁盘。
docker run -d --name milvus-standalone \ -p 19530:19530 \ -p 9091:9091 \ -v /data/milvus:/var/lib/milvus \ milvusdb/milvus:v2.4.0 standaloneEmbedding 模型用的是 BGE-M3,本地部署,通过 FastAPI 包一层 HTTP 接口给 Agent 调用。这里有个坑:BGE-M3 的模型文件也不小,内网拷贝的时候记得把sentence_transformers的缓存目录一起拷过去,否则第一次加载会去外网拉。
知识库的构建流程是:文档解析(PDF/Word/Excel)→ 分块 → 向量化 → 入库。分块策略我试过几种,最后定的是按语义分块 + 512 token 上限 + 128 token 重叠。纯按固定长度切会把一句话切断,检索出来的片段读起来很别扭。
提示:内网知识库更新是个麻烦事。我的做法是做一个"知识库同步"的定时任务,每天凌晨扫描指定目录,有新文档就增量入库,同时记录文档指纹避免重复。这个任务本身不依赖外网,纯本地跑。
4. 实操过程:从零到跑通一个内网 Agent
4.1 环境准备清单
在动手之前,先把这些东西准备好,缺一个都会卡住:
- 模型权重文件(Qwen2.5-14B-Instruct-AWQ,约 9GB)
- vLLM 及其依赖的离线 wheel 包
- Milvus Docker 镜像(提前
docker save成 tar 包) - BGE-M3 模型文件及 sentence_transformers 缓存
- MCP SDK 及依赖 wheel 包
- Python 3.10+ 运行时(内网机器自带或离线安装)
- 各业务系统的接口文档和访问凭证
这些东西加起来大概 30GB 左右,用一个移动硬盘拷进去就行。我建议做一个"内网部署包",把所有东西按目录组织好,附一份 README,这样下次交付直接复用。
4.2 Agent 主程序的编排逻辑
Agent 主程序用 LangGraph 写,核心是一个状态机。状态定义如下:
from typing import TypedDict, Annotated from langgraph.graph import StateGraph, END import operator class AgentState(TypedDict): messages: Annotated[list, operator.add] next_action: str tool_calls: list final_answer: str节点包括:understand(理解用户意图)、plan(规划下一步)、call_tool(调用工具)、reflect(反思结果)、respond(生成最终回答)。边的关系是:understand → plan → 判断是否需要工具 → 需要则 call_tool → reflect → 回到 plan;不需要则直接 respond → END。
这个状态机的好处是每一步都可审计。内网环境里,甲方往往要求"Agent 做的每个决策都要有日志",状态机天然满足这个需求。
4.3 工具调用的权限收敛
内网 Agent 最容易被忽视的是权限问题。Agent 能调用的工具,本质上就是它能访问的系统资源。如果不做收敛,一个提示词注入就可能让 Agent 去查它不该查的数据。
我的做法是三层收敛:
- 工具白名单:Agent 只能调用注册在
mcp_servers.json里的工具,其他一律拒绝。 - 参数校验:每个 MCP Server 内部对参数做严格校验,比如工单号必须符合特定格式,SQL 查询必须带租户 ID 条件。
- 调用审计:每次工具调用都记录到日志,包括调用者、参数、返回结果摘要、耗时。
注意:千万不要让 Agent 直接执行 SQL 或者 shell 命令。我见过有项目为了"灵活"给 Agent 开了数据库直连,结果模型幻觉生成了一个
DROP TABLE,虽然最后被权限拦住了,但惊出一身冷汗。工具要封装成"业务语义"的接口,而不是"技术语义"的接口。
4.4 并发与稳定性处理
热词里有个"ai agent 怎么扛并发",这确实是内网 Agent 的痛点。我的实测数据是:单张 A100 跑 14B 模型,并发 8 的时候响应时间还在 2s 以内,并发 16 就开始排队,并发 32 直接超时。
扛并发的思路有几个:
- 请求队列 + 限流:用 Redis 或者内存队列做缓冲,超过阈值的请求排队等待,而不是直接打爆模型。
- 模型实例横向扩展:如果有多张卡,起多个 vLLM 实例,前面挂一个负载均衡。
- 结果缓存:对于高频重复的查询(比如"XX 工单状态"),缓存结果,减少模型调用。
- 异步化:Agent 的思考过程异步执行,前端先返回"处理中",完成后推送结果。
我最后用的是"队列 + 缓存 + 双实例"的组合,峰值 3000 次/天的调用量下,P99 响应时间控制在 5s 以内。
5. 常见问题与排查技巧实录
5.1 模型输出格式错误
这是最高频的问题。Agent 需要模型输出结构化的工具调用参数,但模型经常输出多余的说明文字,或者 JSON 格式不对。
排查思路:先看提示词里有没有明确要求"只输出 JSON,不要任何其他内容",再看有没有给 few-shot 示例。如果还不行,就在解析层做容错——用正则提取 JSON 部分,解析失败就重试一次。
我的经验是:提示词约束 + 输出解析容错 + 重试机制,三件套缺一不可。重试的时候把上一次的错误信息也塞进提示词,模型往往能自我纠正。
5.2 MCP Server 启动失败
内网里 MCP Server 启动失败,90% 是依赖问题。排查步骤:
- 手动执行启动命令,看报错信息。
- 如果是
ModuleNotFoundError,检查 wheel 包是否装全。 - 如果是权限问题,检查 Python 解释器路径和文件权限。
- 如果是端口占用(HTTP 模式),换端口或者改 stdio。
我建议在 Agent 启动时加一个"健康检查"环节,逐个拉起 MCP Server 并调用一个ping工具,确认所有工具可用后再开始服务。
5.3 知识库检索不准
RAG 检索不准,通常是三个原因:分块不合理、Embedding 模型不匹配、检索策略单一。
我的优化顺序是:先调分块(语义分块 + 合适的大小),再换 Embedding 模型(BGE-M3 比 BGE-large 在中文上强不少),最后加混合检索(向量检索 + BM25 关键词检索,结果融合)。
混合检索的代码大概是这样:
def hybrid_search(query, top_k=5): vector_results = milvus_search(query, top_k=top_k*2) bm25_results = bm25_search(query, top_k=top_k*2) # RRF 融合 scores = {} for rank, doc in enumerate(vector_results): scores[doc.id] = scores.get(doc.id, 0) + 1/(60 + rank) for rank, doc in enumerate(bm25_results): scores[doc.id] = scores.get(doc.id, 0) + 1/(60 + rank) return sorted(scores.items(), key=lambda x: -x[1])[:top_k]5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 模型加载 OOM | 显存不足 | nvidia-smi看显存 | 降量化精度或换小模型 |
| 工具调用超时 | 业务系统响应慢 | 看 MCP Server 日志 | 加超时 + 重试 + 降级 |
| 回答答非所问 | 提示词不清晰 | 打印完整 prompt | 优化系统提示词 |
| 检索结果重复 | 分块重叠过大 | 检查分块参数 | 减小 overlap |
| Agent 死循环 | 状态机无终止条件 | 看状态转移日志 | 加最大步数限制 |
| 并发下响应慢 | 模型排队 | 看 vLLM 队列长度 | 加实例或限流 |
6. 内网 Agent 工程化的几点个人体会
做了几个内网 Agent 项目之后,我最大的体会是:内网 Agent 的难点从来不是 AI,而是工程。模型能力再强,如果依赖装不上、服务起不来、日志查不到,整个项目就是空中楼阁。
第二个体会是收敛比灵活重要。外网做 Agent 追求"什么都能干",内网做 Agent 追求"该干的干好,不该干的碰都别碰"。工具白名单、参数校验、调用审计,这三样东西看着笨,但能救命。
第三个体会是离线部署包要标准化。我现在的做法是维护一个"内网 Agent 部署模板",包含所有依赖、配置、脚本、文档,新项目直接复制改配置。这样交付周期从两周压缩到三天。
最后分享一个小技巧:内网环境里,Agent 的日志一定要打到本地文件,并且按天切割。我见过有项目日志只打 stdout,结果服务一重启日志全没了,出问题根本没法排查。日志格式建议用 JSON,方便后续用脚本分析。
这个方向后续还能扩展的地方很多,比如把 Agent 的执行过程做成可视化面板、把 Skills 做成可热插拔的插件、把 MCP Server 做成容器化部署。但这些都是锦上添花,把基础链路跑稳,才是内网 Agent 工程实战的第一要务。