从去年年底开始,我所在的项目组接到一个“硬骨头”:在完全隔离的内网环境里,搭建一套能跑起来的 AI Agent 系统。所谓隔离内网,就是物理上跟公网断开,没有外部 API、没有在线模型服务、甚至连 pip 都连不上 PyPI 的那种环境。我们原本设想的“调一个云端大模型、接几个插件、跑个 Agent 循环”的方案,在进场第一天就全线失效。这篇文章就是我在这大半年里,把隔离内网下的 AI Agent 从“跑不通”干到“能干活”的全过程复盘,重点讲清楚架构选型、离线部署、Agent 编排、业务对接和调试排障这五块内容。如果你也在政企内网、生产专网这类封闭环境里做 AI 应用,这篇文章应该能帮你少走不少弯路。
1. 隔离内网下的真实约束:先看清 Agent 上路的三座大山
在动手之前,我先把团队在隔离内网里遇到的“不可抗力”列了个清单。这不是劝退,而是为了搞清楚我们到底在跟什么对抗。很多人第一次听到“隔离内网”这个概念,第一反应是“没网而已,麻烦一点”。真实情况远不止如此。
1.1 依赖获取的全面切断
我们内部的开发机原本都是连外网的,装依赖直接 pip install,拉模型直接 huggingface-cli download,一切都很顺。进了隔离区之后,最直接的问题就是:没有任何一个包管理源是可用的。pip 默认指向的 PyPI 连不上,apt 源连不上,npm 源连不上,Docker Hub 连不上。
这意味着什么呢?意味着你连一个最基础的requests库都装不上,除非你手上提前备好了对应版本的 wheel 包。我们当时第一周几乎全在干一件事:把 Python、Node、Rust 工具链以及所有第三方依赖的离线包全部手动收集一遍,这活儿说起来简单,做起来极其痛苦,尤其是传递依赖的版本锁定关系,稍微错一个版本,后面整个环境就是一团乱麻。
1.2 模型获取与服务化的“断供”
大模型时代的应用,命脉就是模型本身。隔离内网意味着 HuggingFace、ModelScope 这些模型托管平台全部不可达。你要用什么模型,必须在外网提前下载好,然后“搬运”进内网。
我们项目选型时比较务实,用的是基于 Llama 架构的 7B 和 14B 量级的中文模型。但这里有个特别容易被忽略的问题:光把模型权重文件拷进来是远远不够的。模型跑起来还需要分词器、配置文件、生成参数模板,以及一套能把它加载起来做推理的服务框架。我们最早的方案是直接用 HuggingFace Transformers 配合 PyTorch 加载权重做推理,但后来发现这个方案在隔离内网里“太重了”,每次拉起推理服务的耗时、显存占用、并发处理能力都不尽如人意。
1.3 Agent 工具的“失联”
Agent 之所以叫 Agent,核心在于可以调用工具。大多数 Agent 场景里的工具都依赖外部服务:搜索用 web 搜索 API、新闻要请求第三方接口、地图要调地图平台 SDK。这些东西在隔离内网里全部不可用。
我们当时接到的业务需求是做一个能辅助做数据分析和自动生成报表的 Agent。这导致它的工具链必须是“向内看”的——连接的是内网数据库、内网知识库、内网工单系统。这反而逼着我们思考了一个更本质的问题:Agent 工具的核心价值是“连接外部世界”,但在隔离内网里,外部世界的定义变成了企业内部的业务系统。你得重新设计一套工具协议,让 Agent 跟内网的数据和服务打交道。
这三座大山看清之后,我们的破局思路就清晰了。一句话概括:把 AI Agent 当成一个完全自洽的离线软件系统来建设,而不是一个依赖公共云服务的玩具。所有组件、依赖、模型、服务,全部走“提前准备、离线灌入”的路径。
2. 离线模型供给与推理服务化:让 Agent 有一颗能用的“大脑”
Agent 的核心是一个能理解意图、规划步骤、生成回复的语言模型。在隔离内网里,这个“大脑”必须完全本地化。这一节我把模型选型、格式转换、推理服务化这几步拆开讲,每一步都有我们实测过的具体参数和踩坑记录。
2.1 模型选型:先看显存,再谈效果
模型选型的第一原则不是“效果最好”,而是“硬件扛得住”。我们内网用来跑模型的机器是两台英伟达 4090 的服务器,单卡 24GB 显存,不支持多卡并行(也没那个条件),所以 7B 和 14B 是我们能触及的上限范围。
拿 7B 模型来说,如果是 FP16 精度加载,光权重就要占 14GB 左右显存,加上 KV Cache 和激活值,基本就是贴着 24GB 的边跑,并发稍微一上来就 OOM。后来我们把模型量化到了 INT4,用 GPTQ 方案,权重体积压缩到 4GB 左右,显存富余出来了,推理速度也快了不少。效果上不能说完全没有损失,但 7B 模型在 INT4 下跑日常任务,生成质量下降在可接受范围内。
14B 模型我们也试过,INT4 量化后权重大约 8GB,单卡 4090 能跑,但并发能力和响应速度明显比 7B 吃力。我们最后的结论是:在隔离内网这种“算力自有、无法弹性扩容”的环境里,优先选更小、更快、更稳的模型,把效果优化放在 prompt 设计和工具编排上,而不是盲目追大模型。
2.2 离线环境下的模型转换与拷贝
模型选型定了之后,最麻烦的一步来了:怎么把模型搞进内网。
网上很多教程的做法是直接拿 HuggingFace 格式的权重文件,配合 Transformers 库加载。这在能连外网时当然没问题,但隔离内网里我们要提前考虑两件事:一是文件格式的兼容性,二是文件体积的搬运成本。
我们最终采用的是 GGUF 格式。原因很简单:GGUF 是 llama.cpp 社区的标准格式,支持 CPU/GPU 混合推理,而且配合 Ollama 这类工具可以极简部署。把 HuggingFace 格式转成 GGUF 的步骤,这里分享一个我们验证过的稳定流程:
整条链路是:外网下载原始权重 → 用llama.cpp的转换脚本生成 GGUF → 用llama-quantize做 INT4 量化 → 把.gguf文件通过移动硬盘拷贝进内网。
光是做这一个转换,我们就折腾了大概三天。最大的坑在于转换脚本的版本必须和推理框架版本匹配,否则会出现“模型加载成功但生成乱码”的情况,而且这种问题在日志里极难排查,因为报错信息非常泛——往往就是一句“illegal instruction”或者直接进程崩溃。
2.3 推理服务化的三种落地姿势
模型文件进去了,下一步是搭建推理服务。我们对比了三种方案,各有取舍:
| 方案 | 部署复杂度 | 并发能力 | 功能特性 | 适用场景 |
|---|---|---|---|---|
| Ollama | 极低,几乎开箱即用 | 中等 | 内置 API、模型管理 | 快速验证、小规模使用 |
| vLLM | 较高,需要 Python 环境 | 高,支持 Continuous Batching | OpenAI 兼容接口,适配 LangChain 等框架 | 生产环境、高并发 |
| llama.cpp server | 中等,需自行编译 | 中等 | 轻量,资源占用小 | 算力有限、嵌入式场景 |
我们最终选了 vLLM,因为 Agent 场景对推理服务的稳定性、并发响应速度要求很高,vLLM 的 OpenAI 兼容接口对我们后续接编排框架非常友好。但这里要提醒一句:vLLM 的离线部署依赖那个“全套依赖打包”的能力,它依赖的vllm包、torch、transformers、xformers这些,全部要在外网准备好对应版本的 wheel 文件,一个都不能漏。我们在这一步吃了大亏,后面第三章会细说。
2.4 统一推理网关:让 Agent 用一套接口调用所有模型
模型服务化之后,我们还做了一层“推理网关”。原因是内网里可能同时存在不同业务线的多个模型服务,有的跑在 GPU 上,有的是纯 CPU 推理的小模型(比如做 embedding 的)。如果每个 Agent 都直连各个模型服务,接口不统一、负载不均衡、故障不隔离,后期运维就是灾难。
我们的做法是用一个轻量级的 FastAPI 应用做统一网关,对外暴露一套标准的 OpenAI 风格接口:/v1/chat/completions和/v1/embeddings。网关内部再根据请求中的model字段把请求路由到对应的后端推理服务上。这样无论底层是 vLLM、Ollama,还是自己用 Transformers 写的服务,Agent 那一侧永远只认这一套接口。
这一步做完,Agent 的“大脑”算是正式上线了。但光有大脑还不够,Agent 要运行起来还需要一个完整的离线依赖环境,这也是我们踩坑最多、最值得单独拿出来讲的一块。
3. 依赖与组件的离线分发:搭建内网自有的“弹药库”
Agent 是个复杂的软件系统,跑起来需要 Python、Node、各种 C 库、推理框架、编排框架等等一大堆东西。在隔离内网里,所有这些东西都要靠人肉搬运。这一节我把我们的离线依赖管理方案完整讲一遍,包括工具链准备和最终验证方法。
3.1 工具链准备:外网“进货”的正确姿势
我们当时是这么操作的:找了一台有外网权限的机器,把这台机器当成“采购站”,所有要进内网的东西都在上面准备好。采购分三个层次:系统级工具、Python 依赖、模型权重。
系统级工具比较容易,提前下载好 deb/rpm 安装包,拷进去之后用离线方式安装就行。Python 依赖最麻烦,因为第三方库之间存在大量依赖关系,单纯把requests的 wheel 拷进去还不够,它的依赖urllib3、certifi、charset-normalizer一个都不能少。
我们的做法是:锁定一个 requirements.txt,然后在外网机器上用 pip download 一次性把所有依赖的 wheel 包全部拉下来。注意这里一定要加--no-deps以外的参数组合,最关键的是用pip download -r requirements.txt -d ./packages/,它会自动分析依赖树,把每个包的所有依赖都下载出来,然后我们在内网用pip install --no-index --find-links=./packages/ -r requirements.txt安装。
这个过程有几个必须注意的点:
- 必须用和生产环境完全一致的 Python 版本去下载,比如内网是 Python 3.10,外网采购站也要用 Python 3.10,否则可能遇到“这个 wheel 不支持当前平台”的问题。
- 优先下载
manylinux标签的 wheel 包,这类包自带 C 扩展,不需要在内网现场编译。如果一不小心拿了个 sdist 源码包进去,内网没有编译工具链就直接歇菜。 - 把整份 requirements.txt 锁定到精确版本号(
==),不要用兼容范围(>=),否则内网离线安装时版本解析容易出幺蛾子。
3.2 私有 PyPI 的搭建和使用
光把 wheel 包拷进去手动安装,在小规模验证的时候没问题,但一旦 Agent 系统要扩容、要多台机器部署,这种手工方式就得被替代。我们后来在内网搭了一个私有 PyPI 源,用 devpi 实现,把采购站下载的所有 wheel 包全部上传进去,内网机器统一配置pip.conf指向这个内网源。
这一步带来的收益非常直接:后续安装依赖就跟外网体验基本一致,只是源换成了内网地址。而且 devpi 支持缓存功能,谁在这个内网源上装过一次包,后面下载就走缓存,速度飞快。
搭建私有 PyPI 的核心配置我这里贴一下,方便你照着操作:
# /etc/pip.conf [global] index-url = http://pypi.internal.example.com/simple/ trusted-host = pypi.internal.example.com timeout = 60trusted-host这几行必须加上,因为我们内网用的是自签名 HTTPS 证书(或者干脆就是 HTTP 明文),pip 默认会校验证书,不加这个就会直接拒绝连接。这个问题在部署文档里经常被忽略,但我们实际排障时发现,十次连接失败有八次是这个原因。
3.3 Rust 工具链的离线安装与 cargo vendor
前面说的都是 Python 生态,但我们的 Agent 编排层有一部分核心逻辑是用 Rust 写的,这也是我们团队的一次尝试,后面第五章会专门聊。Rust 工具链在隔离内网里的安装方式比较特殊:Rust 不像 Python 有 wheel 包,它必须通过rustup安装工具链,而且安装过程中需要从静态服务器下载组件。
我们的做法是:在外网用rustup-init下载完整的工具链压缩包(包括rustc、cargo、标准库、rust-analyzer),然后拷贝到内网,执行离线安装脚本。这里有个小技巧:安装时设置RUSTUP_DIST_SERVER和RUSTUP_UPDATE_ROOT环境变量,指向内网的文件服务,这样后续如果需要更新组件也能走离线模式。
Rust 项目的依赖处理走的是cargo vendor。cargo vendor会把项目依赖的所有 crate 源码下载到本地vendor/目录,然后构建时通过.cargo/config.toml指定:
# .cargo/config.toml [source.crates-io] replace-with = "vendored-sources" [source.vendored-sources] directory = "vendor"这样cargo build就完全不需要网络了。要注意的是,crate 的版本锁文件Cargo.lock必须提交到代码仓库里,否则每次构建时 cargo 会尝试联网更新版本索引,这在离线环境直接就报错了。
3.4 容器镜像的离线化:docker save 与内网 Registry
我们最终的生产部署形态是容器化,这又带来一个问题:基础镜像怎么进内网。
方案其实很简单,在外网把需要的镜像拉下来,然后docker save -o打成 tar 包,拷贝进内网之后docker load -i导入,再docker tag并push到内网自建的 Registry(我们用 Harbor)。这一步看似简单,但坑也不少:
- 很多基础镜像的镜像层非常大(比如 pytorch 镜像动辄 10GB+),而且存在多个平台变体,保存之前要先确认架构是
amd64还是arm64,否则导入后跑不起来。 docker save打出来的 tar 包不保留镜像的 tag 层级关系,导入后必须重新 tag。- 如果内网有多台节点需要跑同一个镜像,一定不要每台机器手动 load,而是统一推到 Harbor 上,节点从 Harbor 拉取。否则后续镜像更新一次,你就得一台一台地拷 tar 包,人会疯掉。
3.5 依赖清单与完整性校验
最后说说依赖清单。我们在采购站做完所有准备后,拉了一份“物料清单”,格式大概是这样的:
- 系统工具:NVIDIA 驱动、CUDA 工具包、nvidia-container-toolkit
- Python 依赖:requirements.txt 对应的所有 wheel 包(共 147 个)
- Rust 工具链:rustc 1.75.0、cargo 1.75.0、rustup 组件
- 模型文件:llama-7b-chat-int4.gguf(约 4GB)、embedding 模型(约 0.5GB)
- 容器镜像:python:3.10-slim、pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime
这份清单就是后面所有环境搭建的依据。每一件物料进来之后,我们还会用 checksum 做一次完整性校验,防止拷贝过程中文件损坏。文件损坏这个问题在隔离内网里特别容易被忽视——U 盘拷贝、移动硬盘传输都可能有静默的数据错误,模型文件一旦损坏,跑起来可能只是生成结果变“飘”,完全不在代码层面报错,排查难度极高。
4. Agent 编排逻辑的工程化:从 Demo 到可用的关键一跃
环境问题解决之后,终于可以聊 Agent 本身了。很多团队在“能调通模型 API”之后就宣告 Agent 成功了,但真正到了工程化阶段,你才会发现:模型只是轮船的发动机,编排逻辑才是整条船的舵和帆。
4.1 ReAct 循环:就是从“想一步做一步”到“想完再做”
目前绝大多数 Agent 框架的底层逻辑都是 ReAct 模式(Reason + Act)。它的工作流是这样的:
- 用户输入一个问题。
- Agent 把问题交给大模型,让它推理:我当前有哪些工具可用,下一步应该调用哪一个。
- 模型输出一个“动作”(比如调用某个工具、传入某些参数)。
- Agent 框架执行这个动作,拿到结果。
- 把结果和之前的对话历史一起再次交给模型,让它继续推理。
- 循环直到模型认为不需要再调用工具,直接生成最终答案。
听起来不复杂,但工程化落地时有几个关键点处理不好,整个系统就跑不稳。
4.2 工具注册、调用与结果解析:最容易出错的三个环节
工具注册阶段,我们用的是 JSON Schema 协议。每个工具都有一个名字、一段自然语言描述、一份 JSON Schema 格式的入参定义。模型根据这个 Schema 生成调用请求。这里有个特别重要的细节:工具的描述必须写得极其具体。同样的一个“查询数据库”工具,如果你只写“查询数据库”,模型根本不知道怎么用;但如果你写“查询 MySQL 数据库,参数包括 sql(必填,SQL 查询语句)、limit(可选,返回行数上限,默认 10)”,模型就能很准确地生成调用参数。
工具调用结果解析也是一个大坑。我们发现,大模型生成的工具调用参数经常不合法,最常见的是漏掉必填参数、参数类型不匹配、返回结果太长导致上下文爆炸。我们当时的应对办法是异常重试机制:如果模型生成的参数解析失败,把错误信息反馈给模型,让它重新生成参数,最多重试三次。三次还不行,就放弃这次工具调用,直接把“工具调用失败”作为观察结果返回给模型,让它走下一步。
4.3 上下文窗口管理:养鱼还是养金鱼,就看这一步
Agent 的上下文窗口是有限的,哪怕模型支持 128K 上下文,你也不能无限制地往里塞内容。因为窗口越长,推理越慢、越贵,而且模型在超长上下文下的注意力集中度会显著下降。
我们的策略是分层管理上下文:
- 固定系统提示词:包括 Agent 的人设、可用工具列表、安全规则。这部分始终保留在上下文最前面。
- 历史消息压缩:当对话轮数超过阈值后,不再把历史消息原文放入上下文,而是用一个大模型对历史消息做摘要,只保留摘要。
- 工具结果截断:工具返回的结果非常长(比如 SQL 查询返回几万行数据)时,只截取前 2000 字符,并注明“完整结果已截断,当前显示前 2000 字符”。
这套上下文管理策略,我们实测下来能让 Agent 在长时间运行中保持稳定。没有这套策略之前,Agent 大概聊到第十几轮就开始“精分”,有时候会忘记自己已经调用过某个工具,重复执行同样的动作,上下文管理的价值在这个场景下体现得淋漓尽致。
4.4 Python 编排栈与 Rust 编排栈的取舍
在编排层实现语言上,我们做了一次非常大胆的尝试:主体编排用 Python(主要是 LangGraph),但有一个核心的调度模块用 Rust 重写。
为什么这么折腾?因为团队里那位 Rust 味儿很浓的同事坚持认为:Python 的 GIL 限制和异步调度的不确定性在某类高并发工具调用的场景下会成为瓶颈。当时我们实测了一个极端场景:50 个并发对话,每个对话每分钟要调用 3 次工具,Python 那套在调度层确实出现了大量的 async 阻塞和 CPU 空转。
Rust 版调度模块做的事情其实很纯粹:维护一个任务队列,每个任务包含“对话 ID、当前状态、需要调用的工具、参数、回调地址”。它用tokio异步运行时高效地调度这些任务,再把调用请求通过 HTTP 转发给 Python 那边的工具执行服务。跑完一轮之后返回结果,状态更新后再进入下一轮。这个模块在并发场景下的 CPU 占用比 Python 版低了将近 40%,延迟也稳定了不少。
当然,Rust 不是银弹。它的开发效率比 Python 低,调试起来也更麻烦。如果不是遇到真实的性能瓶颈,我不建议大家在 Agent 初版就引入 Rust。这个决策的关键是拿数据说话,而不是为了“用 Rust”而用 Rust。
5. 让 Agent 真正干活的工具链:内网业务系统的对接实践
Agent 编排跑通了,接下来就是“给 Agent 配武器”。在隔离内网里,Agent 的工具不是搜索引擎,而是企业内网的各种系统。我们当时基于业务需求做了三套工具,每一套展开讲都能写一篇长文,这里挑最核心的分享。
5.1 内网知识库检索工具:让 Agent 学会“查资料”
第一个工具是知识库检索。我们把企业内部的各种制度文档、项目文档、FAQ 全部导入了内网向量数据库(用的是 Milvus),然后封装了一个search_knowledge_base工具给 Agent 调用。
这个工具的逻辑是:接收一个 query 参数,调用 embedding 模型生成向量,在 Milvus 里做近似检索,返回 Top-K 条相关文档。这里面有一个细节特别影响最终效果:分块粒度。
如果分块太小(比如 200 字),检索回来的文档片段可能语义不完整;如果分块太大(比如 2000 字),向量化之后的表示会非常粗糙,检索精度下降。我们实验下来,最佳分块大小在 500-800 字之间,重叠 100 字左右。
5.2 内网数据库查询工具:让 Agent 学会“查数”
第二个工具是数据库查询。我们的业务场景需要 Agent 能从内网数据库中提取数据并生成分析结论。最初的想法很简单——给 Agent 一个query_database工具,它直接生成 SQL 然后执行。
这个想法很快就被我们否掉了。原因很简单:大模型生成的 SQL 极容易出安全问题,比如执行了全表删除。后来我们的方案是双重的:
第一重,约束查询范围。Agent 不能直接连数据库,必须通过一个查询网关,网关里配置好了白名单数据表清单、查询超时时间、最大返回行数。不在这张白名单里的表,直接拒绝。
第二重,要求 Agent 先生成“查询意图”而非 SQL。我们让 Agent 描述自己想查什么(比如“我想知道 6 月份各个项目组的工单量及平均处理时长”),系统把这个意图翻译成结构化的查询请求,再由后台的模板引擎拼接出最终 SQL。这样做的代价是灵活性下降,但安全性大幅提升。
5.3 消息通知与自动化推送:Agent 的“手脚”
第三个工具是消息通知。Agent 需要能主动向指定群组或个人发送消息通知。这个工具实现起来本身不复杂,就是调用内网消息平台 API,但这里有三个实际部署中的细节问题:
- 第一,消息频率限流。如果 Agent 在循环里“发疯”,短时间内给同一群人发几百条消息,那场面会非常尴尬。我们的做法是给消息发送工具加了硬性频控:单 Agent 每分钟最多发 10 条,并且所有外发消息都必须经过审核队列,审核通过才真正发送。
- 第二,消息内容格式。Agent 生成的内容默认是纯文本,但业务系统里消息常常需要表格、链接,甚至按钮等交互卡片。我们是让 Agent 在消息内容里带上结构化标记,后台再渲染成对应平台的富文本格式。
- 第三,所有工具调用行为都必须有审计日志。这也是隔离内网项目的老规矩——每一条 Agent 执行过的指令、每个触发过的动作,都要留痕、可追溯。这个不是形式主义,是出事之后唯一能自证清白的手段。
5.4 一个真实的业务案例:自动生成日报并推送
为了让你更直观地理解这套工具链是怎么协同工作的,我拿一个我们实际跑的日报告警 Agent 来举例。每天早上 9 点,定时任务触发一个 Agent 任务,用户消息是“请生成昨日的工单处理日报,并发送给项目群”。
Agent 的思考过程大致是这样的:
- 调用
query_database,从工单表中查询昨天所有已处理工单的状态分布、平均时长、超时工单明细。 - 拿到查询结果后,再调用
search_knowledge_base,检索一下近期是否有针对超时工单的处置规则或公告,避免日报内容与最新政策脱节。 - 模型综合两个工具的返回结果,生成日报全文,内容包括整体数据、趋势对比、异常点标注。
- 调用
send_message,把日报以图文格式推送到指定的企业微信群组。 - 最后生成一条简短的确认消息,表示任务已完成。
整个流程看起来顺理成章,但为了让它从“偶尔成功”变成“稳定成功”,我们在工具描述、参数设计、失败重试上迭代了不下二十轮。尤其要注意的是,Agent 生成的日报内容必须经过一轮格式校验,任何包含可疑数字(比如负工单量、超时率超过 100%)的内容都会被拦下重新生成,避免把模型幻觉直接推向生产环境。
6. 隔离内网特有的调试与观测:看不见的网络里怎么找问题
隔离内网环境里,最痛苦的事情不是写代码,而是出了问题根本没有外网资料可以查。Stack Overflow 打不开,GitHub Issues 连不上,官方文档只能凭记忆。这种“断网式开发”逼着我们建立了自己的调试与观测体系。
6.1 全链路日志:从用户输入到模型生成的每一步
我们的第一条铁律是:全链路日志,一个环节都不能漏。日志记录点包括:用户原始输入、Agent 每一轮的推理结果、工具调用的请求与响应、token 消耗、延迟指标、最终生成结果。每一次 Agent 执行,都会生成一个唯一的trace_id,贯穿整个链路。
隔离内网里调试 Agent 最常见的一个场景是:用户反馈“Agent 回答得不对”。想在代码里复现这个错误,如果日志不全,基本就是大海捞针。有了 trace_id 和全链路日志,我们可以直接打开该次执行的日志,精确看到模型在哪个环节做了什么样的推理、调了什么工具、拿到了什么样的返回、最终为什么给出了这样的回答。这个排查成本降了一个量级。
6.2 模型输出的结构化纠错:比想象中更重要的兜底机制
在调试 Agent 的过程中,我们花时间最多的其实是模型输出的“不听话”。大模型生成 JSON 不合法、生成工具参数遗漏、推理链中断,这些问题在离线小模型上尤其常见。
我们的兜底方案是设计了一个“输出修复器”:当模型输出不是合法的 JSON 时,先把原文本交给一个轻量级的修复模型尝试修复;修复失败再去解析文本中的 JSON 片段;还不行就放弃这次工具调用,要求模型重新生成。这个多级修复机制,让我们的工具调用成功率从 82% 提升到了 95% 以上。剩下的 5% 我们选择接受,因为再花成本去优化,边际收益已经非常低了。
6.3 延迟瓶颈的定位与静态化优化
另一个高频问题是性能调优。在隔离内网里,所有的服务都是自建的,任何一层的性能问题都会被无限放大。我们遇到的最典型场景是:Agent 处理一个问题需要 30 秒以上,用户体验很差。
定位过程是这样的:
- 先看日志里的分段时间戳,发现大部分时间花在了模型推理上。
- 再看模型服务监控,发现单次推理确实慢,但主要慢在 prompt 太长——工具返回的知识库片段和 SQL 结果占了大半上下文。
- 于是我们做了一个“静态化”优化:把经常使用的知识库检索结果做缓存,把 SQL 查询结果缩到最精简的形式再送回给模型,最终单轮响应时间从 30 秒降到了 12 秒左右。
还有一个让我们印象深刻的瓶颈是 embedding 服务。Agent 每次调用知识库工具都要走一次 embedding 模型,如果 embedding 服务响应慢,整个工具链的节奏就会被拖垮。我们后来专门对 embedding 做了批量处理和 GPU 常驻优化,这个不起眼的环节反而是提升整体体验的重要杠杆。
7. 踩过的坑和沉淀的经验清单
最后,把这半年踩过的坑整理成一份“经验清单”,不一定全面,但每一条都是真金白银换来的。
7.1 最容易“翻车”的五个细节
离线依赖版本不锁定。最初我们有一个依赖没锁版本号,内网安装时 pip 自动选了一个兼容版本,结果那个版本的某个行为变化导致 Agent 推理结果断断续续,排查了一整天才定位到是依赖版本问题。永远使用精确版本号锁定。
模型格式与推理框架版本不匹配。GGUF 文件格式本身在演进,新版 llama.cpp 可能已经改了量化格式的解析逻辑,模型文件也要跟着版本走。转换和部署必须记录好对应的版本号。
自签名证书引发的 HTTP 连接失败。pip、conda、docker 这些工具默认都会校验 HTTPS 证书,内网自建服务如果没有配好证书,会带来大量“看似是网络不通、实际是证书校验失败”的问题,配置信任关系要趁早。
向量化检索的“分块焦虑”。分块大小、重叠长度、embedding 模型的选择,显著影响知识库检索的效果。建议先用测试集做好标注评估再上线,不要凭感觉调参数。
Agent 的“自激循环”。没有工具调用频控和步数限制的 Agent,在遇到循环推理场景时可能会无限调用工具,白烧算力。必须在框架层加最大步数限制(我们设的是 15 步)和工具调用频控。
7.2 给同样在隔离内网里做 Agent 的人几条建议
架构上,优先选择“模型服务与编排层解耦”的架构,模型服务单独一个团队维护,编排层只依赖统一推理网关,这样两边可以独立演进,排障时也更容易划分责任边界。
交付物上,一定要把“离线安装包、物料清单、安装手册”三者作为一个整体交付。很多内网项目失败不是因为技术不行,而是因为文档缺失,交接时根本不知道某个依赖是从哪来的、为什么这么配。
运维上,Agent 不是写完了就结束了,它是一个需要持续观察、持续调优的系统。日志监控、告警规则、定期复盘,一个都不能少。我们后来专门给 Agent 加了“异常会话自动转存”功能,任何一次失败的对话都会被自动保存,方便后续复盘和分析。
7.3 最后说点心里话
做隔离内网下的 AI Agent,最大的感受是:“慢”反而是优势。外网生态里,你总想着今天发布的新模型、新框架,明天就要用上;但在隔离内网里,你没有这个选项,只能把手头的模型和工具打磨到极致。这种限制,反而逼着我们深入理解了 Agent 的每一个环节——从模型推理、依赖管理到工具链设计、运维观测。技术上的收获,比在外网“什么都拿来试试”要大得多。
如果你所在的环境也是隔离内网,希望这篇实战记录能给你提供一个可参考的路线图。先把模型推理服务跑稳,再把依赖分发搞顺,然后一步步地丰富 Agent 的工具链,最后让 Agent 在业务场景里创造价值。这中间没有捷径,但每条路都有人帮你蹚过了,你会走得快很多。