ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

隔离内网AI Agent实战:从0搭建离线部署最小闭环

2026/10/8 16:28:24 拓冰建站 浏览量
隔离内网AI Agent实战:从0搭建离线部署最小闭环 1. 写在前面为什么要在隔离内网里跑 Agent年初我接手了一个数据合规要求极高的项目客户网络环境是物理隔离的内网业务系统之间走区隔好的专网终端侧基本不暴露公网出口。在这种环境里大家最自然的第一反应是“先别谈智能能跑起来再说”。但你真把一个 LLM API 接到业务系统里安全评审基本不会给你过。因为数据出不了网模型也不能在公网上调。于是“隔离内网下的 AI Agent 工程实战”就成了一个绕不开的话题。我这边最初的目标其实挺朴素让业务人员用自然语言去查内网数据库、调用内部接口、生成分析摘要。说白了就是把一个 Agent 部署在内网里它没有外网依赖所有模型推理、知识检索、工具执行全在本地完成。这里最核心的三个词是本地模型、离线依赖、受控工具。如果这三个你都能搞定Agent 才能算是在内网扎下根否则就是写了个只能跑 demo 的玩具。这篇文章不打算讲太空的架构图也不会上来就让你堆一堆组件。我会从“最小可运行闭环”出发把模型层、Agent 循环、记忆工具、内网部署坑位按实战顺序拆开。适合谁看如果你正准备在内网环境里落地 Agent或者已经被离线依赖折腾得想摔键盘那这篇能帮你少走大半个月弯路。2. 整体架构设计与组件选型2.1 先盘点内网 Agent 需要哪些东西很多人一开始就把 Agent 想成“一个牛逼的模型”其实模型只是其中一个环节。真正要在隔离内网里跑业务你至少需要四层东西。第一层是模型推理层。模型文件必须提前拷进内网推理服务要么是纯 CPU 跑量化小模型要么是有 GPU 时跑中等规模模型。这一层的核心指标是推理速度和显存占用而不是参数量越大越好。第二层是 Agent 框架层。这里包含你的 Agent 主循环、提示词管理、工具调用协议、任务规划逻辑。你可以自研一个几百行的循环也可以引一个离线版框架但前提是它的依赖必须能全部从内网源装好。第三层是记忆与知识层。内网环境里最值钱的往往就是那一堆本地文档和数据字典所以 Embedding 模型、向量库、检索组件是刚需。要注意Embedding 模型同样需要离线部署不能偷偷去公网拉接口。第四层是工具执行与安全层。Agent 要能调用内部 API、SQL、文件读取但不能是个什么都敢跑的野马。得有白名单校验、超时控制、操作审计。很多生产事故不是模型不行而是工具权限没管住。这四层每一层都有大量细节但选型上我建议克制。能用静态编译的二进制就别用重依赖框架能用一个轻量 Python 包就别引整个 Java 微服务。内网环境最难的不是功能实现是你根本没法轻易重新拉依赖。2.2 模型层离线推理引擎怎么选模型层我试过几套方案第一套是直接用 Ollama。它确实方便模型管理命令做得不错但在完全隔离的内网里你需要把安装包和模型文件一起拷进去版本也要严格对上。另一套是 vLLM吞吐高但依赖 CUDA 环境复杂离线安装一堆编译依赖很容易心态爆炸。最后我选了 llama.cpp准确说是基于 llama.cpp 的 server 模式。它的优势很现实编译产物是纯二进制基本不依赖系统 Python 环境glibc 版本对了就能跑。GGUF 模型文件可以提前准备好内网拷贝即用不需要额外格式转换。自带 OpenAI 兼容的 HTTP APIAgent 端只需要改 base_url调用体验和云上 API 几乎一样。支持 CPU 和 GPU 跑量化模型即使是低配机器也能用小模型先跑通流程。另外如果你团队里有 Rust 背景可以关注一下 candle 和 mistral.rs 这类 Rust 生态的推理引擎。candle 是 HuggingFace 出的模型加载逻辑和推理过程都由 Rust 实现部署形态非常干净对嵌入式服务和边缘侧比较友好。但说实话它目前的生态成熟度还比不上 llama.cpp社区交流和踩坑案例少所以我只在原型阶段用过生产还是保守选择 llama.cpp。选完引擎之后就是模型文件的选择。内网环境普遍 GPU 资源有限我这边用的是 Qwen2.5-7B-Instruct 的量化版几个 GB 的模型文件一张 24G 显卡还能同时跑 Embedding 模型。如果是纯 CPU 环境就会用小一点的 Qwen2.5-3B或者 internlm 的小尺寸模型。这个东西没有绝对答案但我的经验是先跑通最小的再逐步升级。2.3 Agent 框架层不迷信 LangChain搭一个轻量内核最开始有同事建议直接用 LangChain图表和工具多社区案例也多。但真在隔离内网里一装问题就出来了。LangChain 的依赖树太深光一个离线安装就能折腾好几轮而且这个包更新速度极快内网里的版本基本跟不上你搜到的文档和实际版本经常对不上。对我来说Agent 核心循环本身并不复杂无非是“思考 - 行动 - 观察 - 再思考”的循环。我花了一个下午用 Python asyncio 写了一个不到 500 行的内核反倒没有版本焦虑逻辑完全可控。具体来说我的内核支持 ReAct 风格和 Plan-and-Execute 风格。ReAct 适合单步工具调用比如查数据、调接口Plan-and-Execute 适合拆解任务比如“对比三个部门的报表并生成摘要”。自研内核的好处是你可以在工具调度、上下文截断、错误恢复这些地方完全按业务需要去写逻辑不被框架的抽象接口绑架。当然你完全可以用 LangChain 或者 LlamaIndex我并不是否定它们只是建议你在隔离内网里先做个“主流程自研、周边组件轻量化”的取舍。毕竟内网项目的最优解不是“功能最多”而是“依赖最少能快速排障”。3. 落地实操从零搭一个内网 Agent3.1 离线环境的依赖包搬运方案这一步看着简单实际坑最多。在隔离内网里你不能随时pip install requests所以最保险的做法是在一台能联网的跳板机上先把所需依赖完整拉下来再往内网搬。重点是要用pip download而不是pip install并且要把所有传递依赖都考虑进去。我当时用的基本姿势是pip download -d /offline_packages \ -r requirements.txt \ --platform manylinux2014_x86_64 \ --python-version 312 \ --only-binary:all:这样会把 requirements 里所有包和其依赖的 wheel 都下载到本地目录。到了内网之后再执行pip install --no-index --find-links/offline_packages -r requirements.txt这里有两个容易踩的坑。第一个--only-binary:all:意味着只下载 wheel 包如果某个依赖没有发布对应的 wheel就会失败。这时候你可以换个 tag 重新下或者找一个替代包。第二个不同 Python 版本兼容的 wheel 不同一定要确认内网机器的 Python 小版本和下载环境一致。另外如果是 CPU 和 GPU 混用的机器像torch这种包还得区分cpu和cu121版本否则一跑就报 CUDA 错误。还有一类隐藏依赖是系统层面的比如libgomp、libaio、ffmpeg动态库。Python 层面没问题了启动服务时却可能因为缺.so文件直接崩。我的建议是在内网机器上先跑一个最小的 import 测试脚本把所有核心包都 import 一遍提前暴露缺库问题。3.2 内网 LLM 服务封装与调用模型推理服务的接口形式强烈建议统一成 OpenAI 兼容格式。这样 Agent 层不必关心你后面是把 llama.cpp 换成了 vLLM还是换了别家推理引擎只要改一下 base_url 就行。llama.cpp 的 server 模式启动命令类似./llama-server -m /models/qwen2.5-7b-instruct-q4_k_m.gguf \ --host 0.0.0.0 --port 8080 \ -c 8192 --n-gpu-layers 999启动之后可以看/health接口确认服务状态。Python 端直接使用openai库from openai import OpenAI client OpenAI( base_urlhttp://192.168.10.10:8080/v1, api_keylocal-not-needed, ) resp client.chat.completions.create( modellocal-qwen, messages[ {role: system, content: system_prompt}, {role: user, content: user_query}, ], )注意这里并不需要一个真实的 API Key随便填一个占位符就行但代码里最好写到配置项里方便适配将来接入统一网关。内网环境模型服务通常是供多个 Agent 共同调用我建议在 llama.cpp 前面再加一层简单的连接池或请求队列避免并发一上来就击穿推理服务。3.3 Embedding 与私有知识库让 Agent 记住内网文档光有语言模型还不够Agent 要真正回答业务问题就得检索内网里的文档和数据字典。我这边安排了一个独立的 Embedding 服务模型用的 BGE 系列bge-large-zh部署方式还是绑定到 llama.cpp 同类的推理引擎上但走的是 Embedding 接口。由于是纯离线部署模型文件从外部拷贝进去后不需要任何联网校验。向量库我选了轻量的 Qdrant 容器版。虽然也有 Chroma 这类更轻的方案但 Qdrant 的多租户和过滤条件支持更好后续给不同部门做数据隔离更方便。在离线内网里容器镜像直接 load 进去docker load -i qdrant-image.tar docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant索引之前得先把文档切块。切块我一般用的是语义切块而不是固定大小简单场景下固定 500 字重叠 100 字也凑合但遇到表格和代码块就容易断裂。RAG 的质量很大程度上取决于切块质量这块不要偷懒。向量检索和 Agent 的衔接是 RAG 的关键。我的做法是Agent 收到用户问题后先重写成一个更适合作检索的关键词组合再从向量库里拉 top-K 个相关片段塞进上下文里。注意一定要把文档来源或标题带上这样模型答完之后能给别人看引用来源避免一问三不知还能一本正经地编。3.4 工具层的核心内网工具的注册与调用Agent 的内网工具无外乎三类查数据库、调内部 HTTP API、读文件或文档。最关键的设计是工具注册表每一个工具都是一个函数加一份 JSON Schema 描述。模型看到的是描述真正执行的是局域网内的受控服务。我举一个例子某个工具是“查询销售订单”注册时的定义类似这样agent_tool( namequery_sales_order, description根据客户名称和日期区间查询销售订单列表, parameters{ type: object, properties: { customer_name: {type: string}, start_date: {type: string}, end_date: {type: string}, }, required: [start_date, end_date], }, ) def query_sales_order(customer_name: str, start_date: str, end_date: str): # 内部执行 SQL 或调用数据服务 return sales_data注册表的核心价值在于模型只需要学会怎么填参数不需要知道 SQL 怎么写。你可以把写 SQL 的权限全部收拢到代码里避免模型生成一些乱七八糟的动态 SQL 打到生产库上。这一点在隔离内网里尤为重要因为数据安全是红线不能为了“智能”就放开数据库权限。同时所有工具执行前后都要记审计日志。哪些请求调了哪个工具、传了什么参数、拿到什么结果、耗了多少时间全量记录。别嫌日志占地方真出了责任问题这些是你唯一能自证的东西。4. 关键环节的实现细节4.1 Agent 主循环的代码骨架这块我直接贴一段经过裁剪的核心代码逻辑上足够你复刻一个最小内核。它做的事很简单接收任务、循环让模型决策、调用工具、再把观察结果喂回去。import asyncio import json from typing import Any class LocalAgent: def __init__(self, llm_client, tools, max_iterations5): self.llm_client llm_client self.tools tools self.max_iterations max_iterations self.tool_map {t.name: t for t in tools} async def run(self, user_query: str) - dict: messages [{role: user, content: user_query}] for step in range(self.max_iterations): response await self.llm_client.chat(messages) action self._parse_action(response[content]) if action[type] final: return {answer: action[answer], steps: step 1} if action[type] tool: tool_result await self._execute_tool(action[tool_call]) messages.append({ role: user, content: f工具调用结果\n{tool_result[:2000]} }) return {answer: 达到最大迭代次数未完成任务, steps: self.max_iterations} async def _execute_tool(self, tool_call: dict) - Any: tool_name tool_call[name] tool_args tool_call[arguments] if tool_name not in self.tool_map: return 错误未找到该工具 return await self.tool_map[tool_name].execute(**tool_args) def _parse_action(self, text: str) - dict: # 自定义解析逻辑兼容 JSON 输出或 markdown 代码块 try: return json.loads(text) except Exception: return {type: tool, tool_call: self._manual_parse(text)}核心要点是_parse_action。这个函数决定了大模型的输出能不能稳定变成工具调用。我踩过很多次模型输出不按 JSON 格式来的坑所以我的_parse_action里做了很强壮的兼容先从 markdown 代码块里提 JSON提不出来就从文本里正则抽取工具名(参数)再不行就模糊匹配。你可以在运维后台里监控一下看看失败的是哪一种再来调提示词。这个循环看着简单真正难的是你怎么把复杂的业务约束写进去。比如某个工具调用之后返回的字段太多导致上下文爆炸又比如上一次调用的结果明显不对模型还在那里硬编。这些都需要在循环内部加防御逻辑不能指望大模型每次都对。4.2 模型上下文管理与截断策略隔离内网里你很难买到超大上下文的服务更多时候是 8K 或者 16K 的模型。而 Agent 一轮轮对话下来历史会越滚越长尤其工具返回的内容经常是几百行 JSON。如果不做管理很快就把上下文塞满。我这里的策略很简单系统提示词和工具描述是固定的算作固定开销尽量优化精简废话不多说。历史消息保留最近 N 轮早期的对话可以压缩成摘要这个摘要可以由 Agent 自己生成也可以是一个独立的小模型做。工具调用的原始返回不做全文保留只保留截断后的关键字段和状态码。如果用户输入特别长先做一次“问题压缩”把冗余信息去掉再喂给模型。每一条我都在生产里遇到过问题。特别是截断工具返回如果不告诉模型“结果已经被截断”模型会一本正经地基于不完整数据得出结论。所以我通常会在截断位置加上一句标识[结果过长仅展示前 500 字]并明确告知模型不能臆测未展示部分的内容。4.3 内网环境下的并发与性能优化内网 Agent 和公网服务一个很大的差异是你没法靠弹性扩容来扛住突发流量。你手上的 GPU 就那一两块服务一旦沦为串行体验会非常差。我在这边做了几层优化第一层是请求级别的并发控制。同一个 Agent 实例的多个任务不要无脑调度模型服务。模型推理是一个高 CPU/GPU 消耗操作我把任务压进一个队列并发数限制在 1~2保证每次推理都拿到完整计算资源。第二层是结果缓存。很多业务查询在短时间内是重复的比如不同的人问同一个“本周订单量”如果模型生成的 SQL 相同我可以直接把上一次的结果返回连模型推理都省了。但注意这里要做时间窗口失效比如 5 分钟有效避免脏数据。第三层是 Embedding 的预加载。很多文档内容不会频繁变化提前把切块向量算好存入向量库用户在提问时不需要实时计算全部文档的向量只做 query 的向量化响应速度会快很多。那么性能指标怎么定我一般会把 P95 响应时间控制在 8 秒以内。如果超过 8 秒用户就明显觉得“卡”。所以我会把模型服务、向量检索、工具调用的耗时都拆开监控看是卡在推理上还是卡在内部接口上然后再针对性优化。5. 实操中踩过的坑与排查建议5.1 依赖问题最难受的压测阶段遇到的缺库压测阶段遇到一个奇怪的问题Agent 在跑一个文档解析脚本时突然崩溃日志报ModuleNotFoundError: No module named unstructured。我明明在 requirements 里写了这个依赖搬运包时也下载了但 module 就是找不到。后来对比了一下发现原来内网机器上存在两个 Python 环境pip 默认装到了系统 Python而启动服务的虚拟环境里根本没装上。这个问题的排查思路其实很简单确认当前服务运行的 Python 路径和pip install的目标环境是同一个。在踩过几次坑之后我现在所有的部署脚本开头都强制写清楚 Python 解释器路径并通过python -m pip install而不是直接pip install这样能避免很多环境错位的问题。5.2 模型加载慢与显存不足小模型还好7B 模型在 GPU 上加载一般就几十秒。但如果你用的是老一点的显卡显存只有 11G那事情就多了。我一开始直接把模型加载到 GPU跑到第 200 个请求时显存突然溢出。后面排查发现是新版本 llama.cpp 加载权重时把部分层放到了共享内存里崩溃和响应变慢交替出现。我的解决方式很简单把层数控制住--n-gpu-layers设成一个保守值剩下的层跑 CPU。这样确实牺牲了一点首 token 速度但换来的是长时间运行的稳定性。另外如果业务并不需要非常强的推理能力可以换 4bit 或 5bit 量化版模型几乎不影响可用性但显存占用能降一个档。5.3 Agent 陷入死循环内网 Agent 看起来最麻烦的问题是“模型自己绕不出来”。比如让它分析某个报表它一直调用“获取报表列表”拿到结果之后又开始调“获取报表详情”反复调用同一个工具形成死循环。我加了两道保险。第一道是最大迭代次数也就是 agent 循环最多跑 N 次超过就主动收尾。这个必须有否则长时间占着 GPU其他人全卡死。第二道是“重复动作检测”。如果 Agent 连续三次调用同一个工具且参数完全一样就不再执行而是返回一个提醒“检测到重复动作请改变策略或直接回答当前问题”。这两道保险配合使用基本上把死循环问题解决到了 95% 以上。你会发现在这个过程中Agent 偶尔会“偷懒”比如直接根据已有信息回答而不是继续调工具这其实是可接受的我们可以通过提示词引导它但不要过度强求每次都做完所有工具。5.4 工具调用 JSON 解析失败还有一个高频问题就是模型输出不规范的 JSON。模型有时候会返回{ name: query_sales_order, arguments: {\customer_name\: \张三\} }arguments 里面套了一层字符串解析的时候没有json.loads就会报错。或者它返回的是 markdown 代码块的 JSON直接当文本处理也会报错。我这里专门写了一个load_action_from_text函数做了三层解析先看是不是合法 JSON再看是不是 markdown 代码块最后用正则抽函数名和参数。经过这种处理解析成功率可以到 99.7%剩下的 0.3% 就直接报错误让用户重新描述。为了更直观我把这些常见问题整理成一个速查表。问题场景典型表现排查方向应急措施依赖缺失启动时报 ModuleNotFoundError查看 Python 路径是否匹配python -m pip install --no-index --find-links...显存溢出运行一段时间后服务崩溃观察nvidia-smi显存曲线降低--n-gpu-layers或换小模型Agent 死循环日志显示重复调用同一工具检查重复动作检测逻辑增加最大迭代次数限制JSON 解析失败工具调用时频繁抛出异常查看原始模型输出更新解析函数兼容多层格式向量检索结果差回答的内容和问题无关检查文档切块粒度缩短切块长度增加重叠区内网接口超时工具调用迟迟无返回检查防火墙和白名单工具执行加超时控制超时降级5.5 内网接口超时与重试的艺术工具调用无论是查数据库还是调内部 HTTP API都会遇到网络或服务超时。内网环境下很多老系统响应非常慢动不动就几十秒甚至不响应。我一开始在工具函数里只写了简单 try-except结果 Agent 一直等不到结果直接进入超时路径最后回答“数据获取失败”。后来我给每个工具的执行都包了一层asyncio.wait_for超时时间根据工具类型分别设置。数据库类工具最长给 15 秒内部 HTTP 工具给 5 秒文档解析类工具给 30 秒。超时之后的降级策略也很重要。如果查询超时我会先尝试换一个参数范围再查实在不行才告诉用户“暂时无法获取数据请稍后重试”。Agent 不能老是直接摆烂要让它学会“换条路走走”。6. 最后说几句隔离内网里的 AI Agent 项目真正卡人的往往不是模型聪明不聪明而是工程上能不能在“没有外网”的约束下把全套系统转起来。我见过太多项目在选型阶段就陷入“想要最强模型、最多工具”的幻想结果在内网里折腾三个月连一个最小闭环都跑不出来。所以我的实际体会是第一版能跑通比什么都重要一个 7B 模型加三个工具加一个向量库已经能解决很多实际业务问题后续再慢慢加模型、加记忆、加管