
接到一个让我印象很深的部署任务把一套基于大模型的故障问答 Agent 搬进隔离内网。说实话接到需求那一刻我还挺乐观的——Agent 代码是现成的模型服务也有老搭档帮忙搭过不就是换个环境部署吗真动手才发现隔离内网下的 AI Agent 工程难点根本不在 Agent 本身的算法而在于你要在一个又一个断点之间把整条链路重新接起来模型权重进不来、pip 装不了包、Docker 镜像拉不动、工具回调解析不了内网域名甚至连 Agent 框架默认的遥测上报都会在启动阶段疯狂报错。这篇文章就围绕“隔离内网下 AI Agent 工程实战”这一整条主线把我从环境盘点、模型私有化、依赖搬运、框架选型、工具权限到并发治理的完整落地过程写出来适合正在做内网私有化部署、或者准备把 Agent 搬进生产环境的技术同学参考。里面每一步都是我实际跑过的附了可直接套用的命令、配置和避坑清单。1. 隔离内网不是“没有网”而是每一层都给你埋了断点1.1 先说清楚隔离内网到底断了什么很多人听到“隔离内网”四个字第一反应就是“没外网嘛装个离线包就行”。这个理解没有错但太粗了。真实的隔离内网环境网是有的而且内部带宽往往还不小问题是它把“对外部世界的访问”切成了好几层每一层都有可能成为卡住你的点。我这次遇到的环境典型特征是这样的网络层整个网段没有公网出口任何对外域名解析和连接都会被防火墙静默丢弃。软件源层pip、npm、Maven、Docker Hub 这些公共仓库全部不可达。模型层公共模型仓库和在线推理 API 都不通大模型只能本地私有化跑。服务层内网有自己的 DNS、自建 GitLab、制品库和容器镜像仓库但需要走特定的申请流程才能开通访问权限。策略层即便你申请到了某些通道安全策略也会限制端口、限制双向访问、要求加白名单。这说起来就是一串词但每一项落到 Agent 工程上都是真实的坑。比如 Agent 框架里常见的联网搜索工具、地图工具、天气工具都是在公网 API 之上封装的上线第一天如果没提前禁用或者替换成内网实现Agent 在执行任务时就会一直卡在“调用外部服务失败”而且错误信息还很不直观。1.2 一份 Agent 系统的断点盘点清单动手部署之前我建议你先别急着拷贝代码而是按下面这个清单把 Agent 系统依赖的每个外部环节过一遍确定哪些是必须自建的、哪些是可以砍掉的依赖项默认行为隔离内网的问题落地方案大模型权重启动时自动从公共模型仓下载权重文件巨大下载入口不通提前在联网构建机下载离线介质拷贝进内网LLM 推理服务常见框架默认对接云端 API云端 API 不可达内网部署 vLLM / Ollama提供 OpenAI 兼容接口Embedding 模型同上同上内网部署独立的 Embedding 服务Python 依赖pip 默认公共源公共源不可达离线 wheelhouse 目录或内网 PyPI 代理Docker 镜像Docker Hub 公共仓公共仓不可达私有镜像仓库 Harbor或 docker save/load 离线导入向量数据库自建或 SaaS只要内网可装即可内网部署 Milvus / pgvector / ESAgent 工具大量公共 API公网 API 不可达替换为内网服务接口或去掉该工具日志与监控默认上报云平台上报入口不通内网日志系统或先落地到本地文件模型下载脚本、遥测、更新检查框架默认联网启动即报错或等待超时显式关闭自动更新、遥测、外部调用这个清单看起来简单但你认真对照自己的 Agent 项目过一遍就会发现很多问题是“隐性”的。我举一个真实例子某个开源 Agent 框架在启动时会自动检查新版本这个检查走的是公共域名在内网环境里不会直接报“无法联网”而是卡在网络超时上启动流程硬生生多等两分钟日志还不明显。当时我们排了半天最后用strace才看到它卡在一个 GET 请求上禁用掉自动更新之后瞬间恢复正常。1.3 最隐蔽的断点框架自以为是的“联网”隔离内网部署最怕的不是显性的依赖缺失而是那些默认假设“我能上网”的代码逻辑。除开版本检查我还遇到过这些情况模型 tokenizer 加载时尝试从公共模型仓下载缺失的配置文件LangChain / LangGraph 生态里某些内置工具默认使用 SaaS API训练好的 Agent 在构建 prompt 时调用公共 DNS-over-HTTPS 做域名解析Agent 的 trace 和 evaluation 模块默认把运行日志回传到公共 SaaS 平台。处理这类问题的原则其实只有一条内网环境下把所有 Agent 组件都默认当成“零外网假设”来设计。哪里要用外部服务哪里就要有一层抽象和替代方案。这也是为什么我在进内网之前会把 Agent 的代码先通读一遍把所有与外部交互的调用点找出来统一收口到一个可配置的网关层里。这个思路在后文工具权限的部分还会详细展开。2. 模型从哪来内网私有化推理的选型与落地2.1 推理服务两条路线vLLM 与 Ollama模型是 Agent 的发动机。在隔离内网里模型这条线需要解决两个问题一是权重怎么进来二是用什么服务把权重跑起来。推理服务这一层我实际对比过两条路线对比项vLLMOllama并发能力高内置 PagedAttention 和连续批处理中等适合轻量场景显存利用精细可控自动管理可控性弱OpenAI 兼容接口原生支持通过额外配置支持自定义模型支持导入多种模型格式适合场景生产环境、高并发、多模型服务开发验证、小团队内部试点如果 Agent 要面向部门级甚至更大范围提供服务我的建议是直接上 vLLM。它在并发推理上的优势对隔离内网这种“算力有限、又要扛多人请求”的场景非常关键。Ollama 也不是不能用但它更适合个人开发机验证场景一旦请求量上来吞吐和排队控制能力会明显吃力。2.2 权重搬运与目录约定权重文件本身动辄十几个 GB 到几十个 GB必须提前在能访问公共模型仓库的联网构建机上把模型拉取好然后通过离线介质移动硬盘、加密 U 盘、审批过的传输通道拷进内网服务器。我习惯的目录约定是这样的/opt/models/ Qwen2.5-7B-Instruct/ # 对话主模型 bge-m3/ # Embedding 模型拷入之后先做一件事校验文件完整性。不要只看文件大小要逐个文件算 SHA256跟下载时的校验值比对。我见过太多因为传输中断导致模型文件缺几个字节、推理服务反复加载失败的情况浪费的时间足够写完一个 Agent 模块。搬运模型的地方贴一张带校验值和拷贝时间的说明卡团队协作时能省掉大量沟通成本。模型文件放好后还有一个很容易漏的点把模型目录的权限设置好。Agent 服务本身运行的账号建议是独立的普通用户不要用 root 跑推理服务否则后续的安全审计会很难看。2.3 用 vLLM 起一个内网可用的 OpenAI 兼容服务vLLM 提供了 OpenAI 兼容接口这让 Agent 侧对接非常轻松。内网启动命令我一般是这样写的vllm serve /opt/models/Qwen2.5-7B-Instruct \ --served-model-name local-qwen \ --port 8000 \ --host 0.0.0.0 \ --gpu-memory-utilization 0.92 \ --max-model-len 8192 \ --max-num-seqs 64 \ --enforce-eager几个参数解释一下--served-model-name对外暴露的模型名。客户端请求时用这个名字和本地路径解耦。--gpu-memory-utilization 0.92控制显存利用率。不要写 1.0留给 CUDA context 一点余量。--max-model-len 8192上下文最大长度。这个值越大KV cache 占用越多能同时处理的并发数就越少。--max-num-seqs 64单次批处理的最大序列数。这是并发吞吐的关键参数。--enforce-eager关掉 CUDA graph 优化牺牲一点性能换启动稳定性。我在某些内网机器上遇到过图形驱动兼容问题加上这个参数能避开不少坑。启动服务后验证一下健康检查接口curl http://127.0.0.1:8000/health返回OK就说明服务起来了。Agent 侧对接时只需要把 base_url 指向这个内网地址from openai import OpenAI client OpenAI( base_urlhttp://model-infer.internal:8000/v1, api_keyinternal-not-used, )这里有个小细节虽然内网不强制校验 key但接口兼容 OpenAI 规范建议还是给个虚拟 key避免某些客户端 SDK 因为 key 为空直接拒绝请求。2.4 Embedding 模型也必须在内网很多人把注意力全放在对话模型上忽略了 Embedding 模型。Agent 的 RAG 链路里文档切片、向量化、检索全部依赖 Embedding 模型。如果对话模型放在内网、Embedding 却还指向外部服务那上线必炸。我在内网部署的是 bge-m3 这类中英双语效果不错的 Embedding 模型同样是起一个独立的 OpenAI 兼容服务。这里特别提醒一点Embedding 模型也有上下文长度上限默认可能只有 512 或者 1024 个 token。文档切片的时候如果 chunk 切得太大超过模型上限的内容会被截断向量质量直线下降表现为“检索出来的结果看着相关其实驴唇不对马嘴”。后文踩坑部分我会专门讲这个问题。3. 软件供应链整体搬迁Python 依赖、Docker 镜像与代码仓库的离线搬运3.1 Python 依赖一轮 pip download 搞定 wheelhouseAgent 工程基本都是 Python 写的依赖动辄上百个。在隔离内网里最蠢的办法是把每个包拷来拷去最省心的办法是做一个 wheelhouse。在联网构建机上把项目的依赖全部下载到本地目录pip download -r requirements.txt -d ./wheelhouse \ --platform manylinux2014_x86_64 \ --python-version 311 \ --implementation cp \ --abi cp311 \ --only-binary:all: \ --no-cache-dir这几个参数的作用--platform、--python-version、--implementation、--abi这四个组合在一起是为了让下载结果精准匹配内网生产机器的 Python 版本和架构。很多 AI 相关包比如 pydantic-core、tokenizers是有原生扩展的如果构建机是 Mac直接pip download不带平台参数下下来一堆 Mac 的 wheel进内网根本装不上。--only-binary:all:只下载预编译轮子。这样能避免把源码包带进去。不过有些包没有预编译版本遇到这种情况就要单独处理把源码包也放进去或者让内网机器上配好编译工具链。拷进内网之后安装时用本地目录作为唯一源pip install --no-index --find-links/data/wheelhouse -r requirements.txt--no-index很关键它强制 pip 不要碰公共索引只从本地目录找包。如果同一个包在不同机器上重复安装我还会配一个内网 PyPI 服务但初期 wheelhouse 已经足够。3.2 Docker 镜像内网 Registry 才是正经路容器化部署是 Agent 工程内网落地的首选。但环境问题恰恰也出在这里Docker Hub 进不去镜像拉不下来。我当时走的是两条腿的方案。第一条腿在联网构建机上把镜像先按项目维度打好然后推送到内网自建的 Harbor 镜像仓库。内网机器上配置docker login harbor.internal --username xxx docker pull harbor.internal/ai-agent/gateway:1.2.0第二条腿针对那些已经存在于某台机器上的镜像直接离线导出导入docker save -o gateway.tar harbor.internal/ai-agent/gateway:1.2.0 # 拷贝 gateway.tar 进内网机器 docker load -i gateway.tar离线导入适合一次性迁移但后续版本更新会非常麻烦所以健康的内网环境一定要有私有镜像仓库作为唯一可信源最好配合镜像签名和漏洞扫描一起上。另外提醒一句内网机器上 Docker 的镜像加速配置如果你配的是公共加速器地址在内网环境会直接导致拉取超时。检查一下/etc/docker/daemon.json把公网加速器地址清掉只保留内网 registry 地址或者干脆不配。3.3 代码与制品内网 GitLab 制品库Agent 的代码、编排配置、Prompt 模板都属于敏感资产。在隔离内网里我建议第一件事就是把代码迁到内网 GitLab通过 Merge Request 做变更评审。CI/CD 流水线全部指向内网 Runner镜像产物直接推到内网 Harbor。这里有个实操建议Agent 的 Prompt 和模型配置不要硬编码在代码里建议放到配置中心内网可以用自建的 Nacos 或者 Consul。原因很简单——环境隔离后你不可能每次优化 Prompt 都重新发一次版让运营同学直接改配置中心里的 Prompt 模板是内网环境里最顺手的迭代方式。3.4 依赖的版本漂移问题离线依赖搬运有一个容易忽视的隐患版本漂移。公网环境里requirements.txt 不锁版本也许能跑但离线环境里一旦某个传递依赖在构建机上被解析成新版本进内网后装不上或者行为不一致排查起来非常痛苦。我的做法是在构建机上用 pip-tools 生成锁定版本的完整依赖清单pip-compile requirements.in -o requirements.txt这样生成的 requirements.txt 每个包都有精确版本号和哈希校验值。进内网安装时再增加--require-hashes强制校验确保内网装的依赖和构建机上完全一致。隔离内网没有试错的余裕越早把版本锁死越少踩“这边能跑那边跑不了”的坑。4. Agent 编排框架怎么选LangGraph、Spring AI 与 Rust 的取舍4.1 主流框架在内网约束下怎么选模型服务解决了依赖也搬进来了接下来就是 Agent 本身。这一节我结合隔离内网的特殊约束聊聊框架选型。我把目前主流的三条路线放在一起对比框架 / 方案开发效率并发与资源占用内网适配度适合团队LangChain LangGraph FastAPI高生态全中等主要靠服务拆分扛并发高所有组件均可本地化Python 技术栈快速落地Spring AI中高Java 开发效率稳定较高Java 生态成熟高贴近企业现有治理体系Java 技术栈已有 Spring Cloud 基础设施Rust 自研 Agent低开发慢极高静态编译、单资源占用小很高单体交付友好对性能极致敏感团队愿意投入隔离内网环境有一个特点外部资源进不来但内部服务其实往往比较丰富。如果你的团队本身就有一套 Java 微服务体系和注册中心、配置中心、网关那 Spring AI 反而是最平滑的路线——它不需要你额外引入一整套 Python 运行时还能跟现有的服务治理直接打通。但对大多数做 AI Agent 的团队来说Python 生态仍然是第一选择。LangChain 和 LangGraph 有大量现成的工具封装、模型抽象和状态编排能力LangGraph 的图执行模型在处理多步推理、条件分支、人机回环这类复杂 Agent 逻辑时尤其顺手。FastAPI 则负责把 Agent 能力包装成对上层稳定的 HTTP 接口整个链路既简单又可控。4.2 一个最小可运行的 Agent 骨架我实际用的组合是 FastAPI LangChain LangGraph。LangGraph 把 Agent 跑成一个状态图核心逻辑是定义状态类型和节点节点之间用边连接条件分支决定下一步往哪走。一个最小骨架大致长这样from typing import TypedDict from langgraph.graph import StateGraph, END class AgentState(TypedDict): messages: list next_action: str def call_model(state: AgentState) - AgentState: # 调用内网 vLLM 服务生成回复 return {messages: state[messages], next_action: route} def call_tool(state: AgentState) - AgentState: # 根据 next_action 调用内部工具 return {messages: state[messages], next_action: end} def route_after_model(state: AgentState) - str: # 根据模型输出决定是否需要调工具 return tools if state[next_action] need_tool else END graph StateGraph(AgentState) graph.add_node(model, call_model) graph.add_node(tools, call_tool) graph.set_entry_point(model) graph.add_conditional_edges(model, route_after_model, {tools: tools, END: END}) graph.add_edge(tools, model) agent graph.compile()对外暴露接口时FastAPI 这边我建议用同步接口配合线程池或者直接用异步接口避免阻塞事件循环from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class AgentRequest(BaseModel): message: str app.post(/agent) async def run_agent(req: AgentRequest): result agent.invoke({messages: [{role: user, content: req.message}]}) return {answer: result[messages][-1][content]}这只是一个最小的落地骨架。实际生产里我会把模型调用放在独立 Worker 里处理接口层只负责接收任务和返回状态这样才能把并发控制住。4.3 Spring AI 什么时候更合适如果你的团队全是 Java 工程师公司内部系统服务发现、配置管理、网关全是 Spring Cloud 体系这种情况下硬上一套 Python Agent 服务意味着要额外维护一套 Python 运行时、监控告警体系还要打通两个技术栈之间的运维通道成本不小。Spring AI 的价值在于它把模型对接、Prompt 模板、工具调用这些 Agent 能力做成了 Java 世界熟悉的风格能直接融入到现有服务里。隔离内网里你不需要引入新的服务只需要给已有的 Java 服务加上 Agent 能力和私有化模型服务地址即可。我见过不少政企和金融客户内部就是这么做的整套体系非常统一。4.4 Rust 写的 Agent 值不值得相关热词里提到“基于 Rust 语言 AI Agent”我也借这个机会说点实际看法。Rust 写 Agent 的优势是并发模型好、内存占用低、单二进制交付对隔离内网里强调最小化攻击面、快速部署的场景很友好。某大厂的向量检索、网关类组件用 Rust 写服务单实例扛很高 QPS这是真实存在的。但我不建议一上来就用 Rust 写整套 Agent 编排逻辑。原因是 Agent 的核心价值在于快速迭代Prompt、工具、状态图逻辑每天都在变Rust 的编译和类型约束会拖慢这个节奏。我见过比较聪明的做法是编排层用 PythonLangGraph但把高频调用的网关和工具转发层用 Rust 做成独立 sidecar 服务两边各取所长。5. 工具链路的网关与权限让 Agent 只能调用“允许被调用的”5.1 先把一句话刻在脑子里LLM 不是可信执行者内网环境最忌讳的事情就是让 Agent 拿着大模型的能力去任意操作内部系统。LLM 本身只是概率模型它完全可能说出一个不存在的工具名或者给工具传一个不存在的参数。如果 Agent 代码直接根据模型输出的 JSON 去执行内部命令风险非常大。我见过一个事故案例Agent 在回答问题时模型幻觉出一个“批量删除用户”的工具调用幸好工具侧有二次确认才没有闯祸。这不是模型笨是设计上没把“不可信”三个字落实。5.2 工具注册表与权限码设计我在工程里要求所有 Agent 工具必须显式注册并且每个工具都有独立的权限码。没有注册的工具即使模型在输出里提到它Agent 也不会把它当成可调用的函数。工具注册的写法大致是这样TOOL_REGISTRY {} def tool(name: str, permission: str): def decorator(fn): TOOL_REGISTRY[name] {fn: fn, permission: permission} return fn return decorator tool(query_order, permissionorder:read) def query_order(order_id: str): # 走内网订单服务 ... tool(create_ticket, permissionticket:write) def create_ticket(title: str, content: str): ...工具调用入口做统一拦截def execute_tool(name: str, args: dict, caller: str): spec TOOL_REGISTRY.get(name) if spec is None: raise ToolNotFound(ftool {name} is not registered) if not check_permission(caller, spec[permission]): raise PermissionDenied(fcaller {caller} lacks {spec[permission]}) return spec[fn](**args)这样做的目的是把“哪些工具可以用”收拢到一个显式的清单里而不是让模型自由发挥。5.3 内网服务鉴权与链路追踪工具侧校验只是第一道门。Agent 服务调用内网业务系统时还需要做服务间鉴权。内网环境一般有两种做法一种是基于 Token 的简单认证服务调用方在 Header 里带上由认证中心签发的 token目标服务校验 token 的有效性和 scope另一种是 mTLS 双向证书认证适合安全要求极高的场景但部署复杂度会明显上升。还有一个非常关键的工程实践审计日志。Agent 每一次调用工具都应该记录以下内容会话 ID 与用户身份工具名称、传入参数、返回结果的摘要模型生成的原始意图耗时和状态码。内网 Agent 出问题的时候审计日志是唯一的真相来源。不要把审计日志简单地打到 stdout要落到独立的日志文件或者内网日志系统里保留足够长的周期。5.4 一个授权拦截的代码示例把上面几个点合起来我在网关层是这样写的from fastapi import Request, HTTPException AUTHORIZED_TOKENS load_internal_tokens() def verify_request(request: Request): token request.headers.get(X-Internal-Token) if token not in AUTHORIZED_TOKENS: raise HTTPException(status_code403, detailforbidden) return AUTHORIZED_TOKENS[token] def call_guarded_tool(tool_name: str, args: dict, user: str): trace_id generate_trace_id() try: result execute_tool(tool_name, args, user) audit_log(trace_idtrace_id, tooltool_name, argsargs, statusok) return result except Exception as e: audit_log(trace_idtrace_id, tooltool_name, argsargs, statuserror, errorstr(e)) raise隔离内网里的 Agent 安全核心就是“白名单思想”。宁可让 Agent 因为权限不足拒绝执行任务也不能让它因为权限过宽把内部系统搞得一团糟。6. 并发与资源水位小算力团队在内网扛住 Agent 请求的架构方案6.1 瓶颈定位LLM 推理是生产线的瓶颈工序“AI Agent 怎么扛并发”是大家问得很多的问题。答案先给出来在隔离内网这种算力有限的环境里不要指望靠 Agent 应用层硬扛并发而要把 LLM 推理当成整条流水线的瓶颈工序来管理。我推荐的架构分层很简单用户请求 - 接入层Nginx / Ingress - Agent API 服务FastAPI Uvicorn多 Worker - 任务队列Redis Streams / RabbitMQ - Agent Worker耗时的图编排、工具调用 - 内网 vLLM 推理服务 - 内部工具服务 / 向量库接口层收到请求后不直接同步执行完整的 Agent 图而是先把任务丢进队列由 Worker 异步消费。因为单个 Agent 任务动辄要多次调用 LLM单次推理几秒到几十秒很常见如果接口层同步做请求量稍微一上来Uvicorn 的 Worker 很快就全被占满新请求全部排队用户体验就是“转圈半天没反应”。6.2 关键参数怎么给用 vLLM 做推理服务时并发吞吐主要靠这几个参数配合参数作用我的推荐值--max-num-seqs单次批量调度的最大序列数32~64显存够就调高--max-model-len上下文窗口长度按业务需要一般 8192 足够--gpu-memory-utilization显存利用率上限0.90~0.95Uvicorn--workersAgent API 服务进程数CPU 核心数 / 2Uvicorn 的 Worker 不是越多越好。Agent 服务里很多工作是在等 LLM 返回进程数受 CPU 和内存约束开太多反而增加上下文切换开销。我一般先给 CPU 核数的一半压测后逐步上调。有一个点非常容易被忽视Agent Worker 调用 LLM 时HTTP 连接不能每次新建。要在代码里维护一个持久化的连接池避免高并发下把 socket 耗尽。同时所有 LLM 调用都必须设置超时和重试且重试要带退避策略。内网环境偶尔也会出现推理服务重启或者宿主机负载飙高的情况没有重试策略一个瞬时故障就会导致整批任务失败。6.3 流式输出与超时控制内网用户调用 Agent 时流式输出能显著改善体验。FastAPI 里我这样实现 SSE 流式返回from fastapi.responses import StreamingResponse app.post(/agent/stream) async def agent_stream(req: AgentRequest): async def event_generator(): async for chunk in agent.astream({messages: [{role: user, content: req.message}]}): yield fdata: {chunk}\n\n return StreamingResponse(event_generator(), media_typetext/event-stream)流式接口同样要把整体超时设计好。我习惯把单次 Agent 任务的完整超时设为 120 秒LLM 单次调用超时设为 60 秒。超过就中断任务并返回明确错误码避免用户拿着一个永远转圈的页面干瞪眼。6.4 水平扩展与状态持久化隔离内网也不是永远只有一台机器。当请求量超过单机承载能力水平扩展就是必然选择。扩展时有两个核心问题要先解决任务队列要能跨节点共享Redis Streams 是个很轻量的选择Agent 的会话状态要能被多个 Worker 共享这就必须引入持久化的 Checkpoint 存储。LangGraph 的 Checkpointer 默认是存在内存里的一旦服务重启所有会话上下文全部丢失。实际生产环境我会用 Postgres 作为 Checkpointer 的存储后端配置大致这样from langgraph.checkpoint.postgres import PostgresSaver checkpointer PostgresSaver.from_conn_string( postgresql://agent:passwordpg.internal/agent_state ) agent graph.compile(checkpointercheckpointer)这样 Agent 的每一次图执行状态都会落到数据库里服务重启后会话还能继续多副本部署时也不会出现“这台机器上的会话上下文那台机器不知道”的问题。6.5 一个并发估算的例子最后给一个非常粗略的并发估算思路。假设你手里有一张 80GB 显存的卡跑 7B 量级的对话模型max-num-seqs开到 64模型单次生成大约 500 token每 token 耗时 20ms 左右那么单条请求的生成时间大约 10 秒单卡理想状态下能同时处理 64 个请求实际结合排队和网络开销稳定并发在 10~20 个请求每秒是常见水位。如果模型是 70B 量级显存占用会急剧上升单卡可能只能同时处理 8~16 个序列并发吞吐会明显下降。所以模型规模的选型不只是效果问题还直接影响整个 Agent 系统的并发天花板。内网算力有限时我建议优先选效果达标且推理成本可控的模型而不是追大。7. 四个让我熬夜排查的内网 Agent 问题与完整定位过程7.1 问题一Agent 调用工具一直超时根因是内网 DNS 解析上线第二天有同事反馈Agent 在回答需要查内部订单信息的问题时总是转几圈然后报“工具调用失败”。我第一反应是工具服务挂了结果直接 curl 工具接口秒回。那么问题就出在 Agent 服务到工具服务之间。排查链路我是这样走的看 Agent Worker 日志发现每次工具调用都卡在连接建立阶段然后一直重试直到超时手动在 Worker 容器里curl工具服务地址发现也是卡住不动用getent hosts order-service.internal验证 DNS发现解析不了进一步看/etc/resolv.conf容器里指向的 DNS 服务器是内网 DNS但内网 DNS 的 zone 配置里压根没有order-service.internal这条记录。根因清楚了内网 DNS 没有服务域名解析记录。工具服务本身没挂是域名找不到。当时项目着急我先是把工具服务的 IP 写到了容器的 hosts 文件里应急然后走正规流程把服务域名统一登记到内网 DNS。这个问题本质上不是 Agent 的问题而是内网环境“服务发现”体系的建设问题。建议大家在进内网第一天就把 Agent 依赖的工具服务域名清单梳理好一次性提交 DNS 登记能省不少麻烦。7.2 问题二检索质量上线后断崖下跌根因是 Embedding 截断Agent 的 RAG 能力上线后效果不错但运行一周后发现部分长文档的检索结果质量明显下降。一开始我怀疑是文档更新导致数据变脏查了一圈没发现问题。后来把用户问题对应的文档块向量拉出来跟原始文本比对发现向量对应的文本明显比源文档短了一截。真相是文档切片切得很长超过了 Embedding 模型的最大输入长度超长部分在向量化时被静默截断了。也就是说向量表达的内容和源文档的后半段信息完全无关。这类问题在调试时非常隐蔽因为程序不报错只是向量质量下降。解决方式很直接把文档切片的 chunk 大小调小控制在 Embedding 模型最大长度的 50%~70%。同时统一 tokenizer 的统计口径因为中英文混合文档用字符数算长度不准要用 token 数算。修复之后检索质量回到正常水平。7.3 问题三服务一重启Agent 就“失忆”有一次内网机房做了一个例行重启服务起来之后所有用户都反馈对话上下文丢了之前聊到一半的会话全乱了。排查发现 LangGraph 的 Checkpointer 用的是默认内存实现进程一重启状态图的所有中间状态全部清空当然表现为“失忆”。这是我前面提到的持久化问题的现场版。把 Checkpointer 切换到 Postgres 之后重启服务会话依然不丢。这里也提醒大家任何有状态的服务在进隔离内网前就要把状态存储设计好不要等出了故障再补因为内网的排障链路通常比公网要慢。7.4 问题四并发峰值时 vLLM 直接 OOM压测的时候发现一个很吓人的现象并发一旦到某个临界点vLLM 服务直接报 CUDA OOM部分请求返回 500。我当时的第一个想法是“显存不够了要换更大的卡”但仔细一看显存其实没有满是某个环节把显存资源切碎了。排查过程是这样的看 vLLM 启动日志发现max-model-len设得很大每个序列的 KV cache 预留空间都很大再看max-num-seqs发现批处理序列数也调得很高两者相乘预分配的显存总量超过物理显存。KV cache 是按序列数和上下文长度预分配的模型权重只占显存的一部分剩下的都用来给 KV cache 兜底。参数配得太激进预分配就把显存耗光了。解决办法是把max-model-len从 16384 降回 8192max-num-seqs从 128 降到 64并把gpu-memory-utilization从 0.95 微调到 0.92。改动之后压测稳定峰值不再 OOM。这个问题的教训是vLLM 的显存参数不是越大越好要结合物理显存、模型参数量和实际并发量一起算而不是照抄网上某个配置。最后再分享一个我自己用出来的小技巧。内网环境的时钟问题非常容易被忽略——Agent 服务、推理服务、工具服务、数据库分布在多台机器上如果时区设置不统一审计日志里的时间线对不上排障时会凭空多出很多干扰信息。我现在的习惯是所有机器统一使用 UTC 存储时间对外展示时再转本地时区并且每个服务都在启动脚本里显式设置时区环境变量。如果你马上也要做隔离内网下的 Agent 部署建议先把这个看似不起眼的小事提前定好。