隔离内网环境下AI Agent落地实践:从LangGraph编排到vLLM私有化部署
发布时间:2026/10/6 15:17:17 作者:尧图编辑部 阅读量:1,286

在隔离内网里做 AI Agent和你在公网环境写 demo 完全是两码事。几天前我刚把一个 Agent 项目从个人开发机搬到客户的隔离内网里第一天就吃了大亏模型必须走本地私有化推理工具调用全部指向内网接口连装一个 Python 依赖都得先解决离线源的问题。这篇就是完整复盘从需求拆解、架构设计、模型底座选型到 LangGraph 编排、并发压测和问题排查全部基于我实际落地的流程。看完你至少能少走我三分之一的弯路。这套系统适合谁参考两类人一是企业内部正在做基于私有化大模型 自动化工具的 AI 应用工程师二是准备把 Agent 从公网迁移到隔离内网的团队负责人。它解决的核心痛点是模型怎么在断网环境下跑起来、Agent 怎么安全地调用内网服务、并发上去后怎么不雪崩。1. 项目背景与需求拆解1.1 为什么要在隔离内网里做 Agent很多业务系统天然就不能连公网。金融、运营商、政务、大型制造企业的核心生产区数据出不去外部大模型接口自然也用不了。但业务方又想要AI 能直接干活——查库存、看工单、读报表、生成值班记录这些诉求全靠人肉完成太浪费于是隔离内网 AI Agent就成了唯一出路。所谓隔离内网不只是没外网那么简单。它是三层约束层层叠加第一网络层断外所有通信只能在内网完成第二模型层必须私有化部署不能调用任何线上大模型 API第三工具层能对接的只有企业内部服务比如统一认证、数据库、工单系统、监控平台。Agent 的本质是大模型 工具调用在隔离内网里工具从公网 API 换成了内网服务大模型从 SaaS 换成了本地推理架构思路要全部重来。我这次的项目是给一个生产区搭智能运维助手员工用自然语言提问最近两小时支付接口的报错率是多少、帮我把这几个工单标成高优先级Agent 负责理解意图、拆解任务、调内网监控 API 和工单 API、最后汇总答案。生产区是典型的隔离内网对数据不出域有硬性要求模型推理必须在自己机房完成。1.2 隔离环境带来的四个硬约束这四条约束看起来是环境问题实际上决定了整个技术选型我把它们列在最前面模型必须本地化部署。推理依赖 GPU 资源能跑多大模型、用不用量化、并发上限是多少全由机房硬件决定而不是由云厂商决定。依赖必须离线安装。所有 Python 包、模型权重、向量库都要先在外网环境准备好再想办法传进去pip install 直接从 PyPI 拉包是行不通的。工具调用只能面向内网服务。不能依赖回调公网 webhookAgent 的工具集必须全部封装成对内网服务的安全调用。并发能力受限于本地资源。公网可以弹性扩容隔离内网一般就固定几台 GPU 服务器必须把吞吐做好否则用户一多就卡死。这四个约束里最容易翻车的是第三条。业务方会觉得反正内网安全工具随便调但实践经验告诉我内网工具恰恰是 Agent 事故高发地LLM 输出不可控参数校验没做好一句话就能让 Agent 去调接口把数据批量改坏。后面我会单独讲工具层的安全设计。2. 整体架构设计与技术选型2.1 Agent 主流程从用户问题到任务闭环Agent 系统的核心不是模型多聪明而是怎么把一次问答变成一次可靠的任务执行。隔离内网环境里没有兜底的公网服务主流程设计必须保守且可追踪。我采用的是经典的理解-规划-执行-综合四段式用户问题进来后先做意图识别和必要的信息抽取确定是否需要调用工具还是直接问答就行。如果需要工具Agent 生成一份简短的执行计划按顺序或并行调用工具每步拿到结果后决定下一步动作最后把多步结果汇总成用户可以理解的回答。所有过程记录到日志里方便事后审查——这在隔离内网尤其重要操作要留痕。流程本身不复杂难在可靠。LLM 在规划步骤时可能给出不存在的工具名工具返回的数据可能不符合预期格式内网某个服务可能正好在发布重启。所以我在框架层加了两个机制一是工具名和参数的严格校验规划结果必须过一层白名单过滤才能执行二是对每一步的调用结果做格式校验失败了就走重试或降级策略而不是把错误原样抛给用户。2.2 Agent 框架选型LangGraph 还是自研编排架构选型阶段我们在三条路线之间犹豫了很久。第一条是 LangChain LangGraph 组合第二条是 Spring AI第三条是完全自研编排器。最终选了 LangGraph FastAPI原因是它把有状态编排、条件跳转、人工确认这类能力都内置了不需要我从零造轮子而且对 Python 技术栈的团队最友好。我对比过这三条路线的适用场景整理成一张表供参考路线适用场景优点坑点LangChain LangGraphPython 团队需要复杂状态编排、多 Agent 协作社区生态大图编排能力强支持断点续跑抽象层级多版本升级快需要锁定版本Spring AIJava 团队已有 Spring 微服务体系与现有 Java 工程融合好Agent 编排能力比 LangGraph 弱生态相对小自研编排器超轻量场景只想调一次大模型加一次工具可控性最高依赖最少并发、重试、状态管理全要自己写后期成本高选 LangGraph 还有一个关键原因隔离内网环境里调试成本高重新部署一次要半天所以框架本身的可靠性和可观测性比炫技重要。LangGraph 自带状态检查点和逐步回放出问题时我能把 Agent 的完整执行轨迹打出来这在断网环境里价值巨大。2.3 模型底座离线部署的几条可行路线隔离内网没有公网模型 API模型底座只能在开源权重 本地推理引擎这个组合里选。我的经验是离线环境首选 Qwen 系列和 GLM 系列原因有两个中文能力强、权重在开源社区好找且自带商用许可。代码生成类任务可以再挂一个 CodeLlama 衍生模型。推理引擎的选择直接影响并发和响应速度。我从轻到重列一下主流路线Ollama llama.cpp最轻量安装方便适合内网快速验证和单人使用跑 7B 以下模型够用但并发和吞吐一般也不适合大规模提供 OpenAI 兼容服务。vLLM 部分量化模型工程上最平衡的方案PagedAttention 和 Continuous Batching 带来的吞吐提升非常明显官方提供 OpenAI 兼容 API接 FastAPI 几乎零成本。TensorRT-LLM性能天花板高但部署复杂度大需要针对模型专门做引擎构建隔离内网里换一次模型要折腾好久适合模型基本固定、追求极致吞吐的团队。我这次选了 vLLM。原因很直白拿一台 A100 80G 跑 32B 量化模型就能支撑几十个并发用户响应速度和稳定性都达标。这里给一个算显存的经验公式模型参数占用量约等于 参数量 × 每个参数的字节数。FP16 是 2 字节INT8 是 1 字节INT4 是 0.5 字节。一个 32B 模型 FP16 需要 64GB 显存加上激活和 KV Cache80G 单卡很紧张所以线上我用 AWQ 量化的 32B 模型权重降到约 16GB留出充足 KV Cache 空间吞吐一下子舒服了。3. 核心模块实现与实操细节3.1 模型统一接入层让上层只认 OpenAI 协议隔离内网里经常要同时跑多个模型比如一个 32B 主模型负责复杂对话一个 7B 小模型负责意图分类和工具规划。如果每个 Agent 都直接连各自的推理服务切换模型时就改到怀疑人生。所以我在模型层做了一层统一抽象所有推理服务都暴露 OpenAI 兼容接口上层代码只面向统一的 client。vLLM 官方支持 OpenAI 风格 API启动起来非常方便。服务端起一个模型实例vllm serve /data/models/qwen2.5-32b-instruct-awq \ --served-model-name qwen2.5-32b-instruct \ --port 8001 \ --max-model-len 8192 \ --gpu-memory-utilization 0.9上层 Python 接入就一行指向内网地址的事from openai import OpenAI client OpenAI( base_urlhttp://192.168.10.20:8001/v1, api_keyinternal-not-required, )再包一层路由按场景选择模型MODEL_ROUTE { planner: http://192.168.10.20:8002/v1, # 7B 小模型负责规划 reasoner: http://192.168.10.20:8001/v1, # 32B 主模型负责综合 } def get_client(role: str) - OpenAI: return OpenAI(base_urlMODEL_ROUTE[role], api_keyinternal)实操心得统一接入层的关键不是代码量而是约定。团队必须约定所有模型请求都走这一层任何人不得绕过它直连推理服务。隔离内网里出问题不好热修规范比技术更管用。另外vLLM 启动参数里的--max-model-len一定要按实际业务来设太大 KV Cache 会被撑爆设太小长文档场景会直接报长度超限。3.2 工具服务封装与权限管控Agent 的手必须上锁工具调用是整个 Agent 系统里风险最高的环节。LLM 本质是概率生成它规划出来的调库存接口可能带上了错误的参数甚至被用户精心构造的提示词诱导去调用错误工具。在隔离内网里一个内部工单系统、一个配置库被 Agent 误操作了影响面比公网还严重因为内网权限往往更宽。我采用了两层防护。第一层是工具注册白名单Agent 能看到的工具枚举完全由代码注册表决定模型不认识的工具一个都调不到第二层是参数强校验每个工具的参数都有 JSON Schema执行前用 Pydantic 做类型校验把非预期参数挡在调用链之外。工具封装我用一个简单的注册表实现from pydantic import BaseModel, Field from typing import Dict, Callable TOOL_REGISTRY: Dict[str, dict] {} def register_tool(name: str, description: str, schema: type[BaseModel]): def decorator(func: Callable): TOOL_REGISTRY[name] {description: description, schema: schema, func: func} return func return decorator class QueryErrorRateParams(BaseModel): start_time: str Field(description开始时间ISO 格式) end_time: str Field(description结束时间ISO 格式) service: str Field(description服务名必须等于 payment-api) register_tool(query_error_rate, 查询指定服务在时间范围内的错误率, QueryErrorRateParams) def query_error_rate(params: QueryErrorRateParams): # 内部实现调监控平台接口 ...真正执行前必须过一道分发器def dispatch(name: str, raw_params: dict): tool TOOL_REGISTRY.get(name) if not tool: raise ValueError(funknown tool: {name}) validated tool[schema].model_validate(raw_params) return tool[func](validated)两层防护加在一起效果很直接即使模型被绕晕了产生不安全的调用想法到了校验层也会被拦住。注意工具层永远不要把数据库连接直接暴露给模型。我见过有人图省事写了个execute_sql工具让 Agent 自由查询结果一个 查出所有表里包含用户手机号的列名 的请求直接把生产库扫挂了。工具要按业务动作封装不要暴露底层操作原语。3.3 多 Agent 协作与状态编排从链式到图式早期版本我用的是简单链式调用LLM 生成计划 → 逐条执行 → 汇总回答。但真实场景里用户的需求往往有分支先查错率错率高于阈值就自动建工单低于阈值就只返回报告。这种分支逻辑用链式写开始乱套我这才切到 LangGraph 的图编排。LangGraph 的核心是 StateGraph先定义一个全局状态对象每个节点读状态、改状态再用条件边控制走向。我用一个简单例子说明from typing_extensions import TypedDict from langgraph.graph import StateGraph, END class AgentState(TypedDict): user_input: str error_rate: float need_ticket: bool final_answer: str def check_error(state: AgentState): # 调用查询工具 state[error_rate] dispatch(query_error_rate, { start_time: 2025-01-01T00:00:00, end_time: 2025-01-01T02:00:00, service: payment-api, }).get(rate) state[need_ticket] state[error_rate] 0.05 return state def open_ticket(state: AgentState): # 手工建单工具 ... return state def reply(state: AgentState): state[final_answer] f当前错误率 {state[error_rate]:.2%}。 return state graph StateGraph(AgentState) graph.add_node(check, check_error) graph.add_node(ticket, open_ticket) graph.add_node(reply, reply) graph.add_edge(check, reply) graph.add_conditional_edges(check, lambda s: ticket if s[need_ticket] else reply) graph.add_edge(ticket, reply) graph.add_edge(reply, END)这套编排方式的最大优势是状态显式化。整个任务过程中间变量全部放在 state 里出问题可以回放、可以断点续跑。隔离内网环境里没有完善的监控大盘这种可追踪性省了我大量排查时间。特别提醒写操作类的工具一定要加人工确认节点。我在 LangGraph 里给创建工单修改配置这类动作加了 interrupt 节点Agent 执行到这一步会停下来等人工审核通过才继续。上线后这个机制至少拦下了三四次 Agent 的误操作是整套系统里性价比最高的一笔投入。4. 并发、性能与稳定性实战4.1 并发模型从直连同步到线程池与任务队列隔离内网的硬件预算有限并发设计不能按公网那套起一百个 Pod 随便造的思路来。最开始我直接用 FastAPI 的 async 接口去调 OpenAI SDK结果发现模型推理接口本质是阻塞的——vLLM 内部有自己的调度但 HTTP 层同步等待时FastAPI 的事件循环会被卡住并发一上来接口整体堵死。后来改成两层方案快速问答场景使用 FastAPI 线程池把同步的模型调用丢给 ThreadPoolExecutor每个线程持有一个独立 client实测能把并发承载从个位数提高到几十耗时较长的多步 Agent 任务直接走任务队列FastAPI 接口只负责接收任务并返回任务 ID后台 Worker 消费队列执行完整流程用户用轮询或 SSE 拿结果。from fastapi import FastAPI from concurrent.futures import ThreadPoolExecutor app FastAPI() executor ThreadPoolExecutor(max_workers16) app.post(/agent) async def submit_agent_task(payload: dict): loop asyncio.get_event_loop() task_future loop.run_in_executor(executor, run_agent_pipeline, payload) # 注意复杂场景建议直接放任务队列这里仅为轻量示例并发上限怎么估算以一台 A100 80G 跑 32B AWQ 量化模型为例vLLM 的 decode 阶段总吞吐大约在每秒 1500 到 2500 token 左右这个数字取决于 KV Cache 和模型结构。假设每次请求平均生成 600 个 token那理论每秒能处理 3 到 4 个请求一分钟大概 200 个左右。但这只是理想值实际还要刨掉 prefill 的计算消耗和业务处理时间。所以给业务线的承诺我会打五折单实例支撑 100 个并发以内的 Agent 问答超出就上多实例 负载均衡。4.2 流式输出与超时控制体验和稳定性必须一起设计隔离内网用户对响应速度的耐心比公网用户更差——他们习惯了内部系统等几秒的风格但 Agent 动辄要生成几百字如果全部等完整再返回一个 30B 模型生成 800 token 可能要 20 秒交互完全不可用。流式输出是必选项。我用 SSEServer-Sent Events把模型 token 逐字推给前端FastAPI 实现很简单from fastapi.responses import StreamingResponse app.post(/chat/stream) async def chat_stream(payload: dict): async def event_generator(): stream client.chat.completions.create( modelqwen2.5-32b-instruct, messagespayload[messages], streamTrue, ) for chunk in stream: if chunk.choices[0].delta.content: yield f{chunk.choices[0].delta.content} return StreamingResponse(event_generator(), media_typetext/plain)流式输出的同时超时控制要分级。工具调用类操作我设置 10 秒内必须返回结果模型首次 token 的等待时间TTFT设定 15 秒上限整体 Agent 流程控制在 120 秒内超过就返回任务处理中请稍后查看。分级超时能让用户感知到系统还在跑而不是莫名卡死。4.3 容错与重试别让一次偶发拖垮整个会话隔离内网服务虽然网络稳定但模型推理偶尔会超时内网工具在发布窗口也会抖动。Agent 流程是多步长链路任何一步失败都可能让整个任务报废。我总结了三条容错经验指数退避重试模型 API 返回 503 或超时退避 1 秒、2 秒、4 秒重试三次最多三次再失败就走降级路径。这个策略在 vLLM 偶发排队时很管用。工具调用必须幂等这是踩坑踩出来的教训。建工单这个工具第一次调用超时了但服务端其实已经建单成功重试一次就建了两张重复工单。后来所有写操作工具都加上了幂等键——客户端生成请求 ID服务端按请求 ID 去重重试只会返回第一次的结果不会重复执行。降级路径大模型挂了至少让用户能拿到提示和入口。有一次 32B 模型因为显存溢出挂了我把流量自动切到 7B 模型虽然回答质量明显下降但至少任务没中断。容错设计的原则很简单隔离内网没有外部云服务帮我兜着的选项每个环节都要想到如果这里挂了接下来怎么办。5. 常见问题与排查实录5.1 问题一Agent 级联超时根因是 prefill 太长现象是用户提问后Agent 在规划阶段就卡了 20 多秒整体流程屡屡超时。一开始我以为是模型算力不够排查后才发现根因是 prefill 太长为了让模型准确我把工具描述、系统提示词、历史上下文全塞进 prompt一次性进来三千多 token每个请求都要先跑一遍长文 prefill推理服务的吞吐被严重拖低。解决思路是减负和分流系统提示词里的工具说明从五千字精简到两千字只留下工具名、参数、行为边界详细说明挪到工具层意图分类这种简单任务单独走 7B 小模型只需要短 prompt把主模型的 prefill 压力降下来。优化后 TTFT 从 15 秒降到了 3 秒以内。5.2 问题二离线依赖安装反复折腾干脆锁定版本走全离线隔离内网环境没法直接 pip 装包我们最初的做法是人工拷 wheel 包结果缺一个依赖就多一轮传输。后来我改用中转机 全量 wheelhouse方案在能联网的中转机上用与生产机相同的操作系统和 Python 版本执行pip download -r requirements.txt -d wheelhouse \ --platform manylinux2014_x86_64 \ --python-version 310 \ --only-binary:all:然后把整个 wheelhouse 目录通过内部介质传进隔离内网在目标机器上执行pip install --no-index --find-links./wheelhouse -r requirements.txt注意中转机的系统和 Python 版本必须与目标机一致否则二进制兼容性会坑人。另外 requirements 要用 pip-tools 或 uv 锁定出完整的传递依赖清单不能只在 requirements 里写顶层包。这一步做好了后面部署任何 Python 服务都能用到同一套离线源效率翻倍。5.3 问题三LLM 规划出危险工具调用被白名单拦住了现象是用户用自然语言引导 Agent直接修改所有低优先级工单的优先级模型规划结果里出现了update_all_tickets_batch这个工具。这个工具其实没注册但如果不做白名单过滤LangGraph 可能会尝试复用类似的通用 SQL 工具去执行那后果就严重了。我们在分发器里加了双重保险第一重工具名必须存在于注册表否则直接拒绝第二重参数值会用 Pydantic 校验比如update_ticket_priority的 ticket_id 字段只接受长度为 6 位到 10 位的工单编号任何不符合格式的输入都走异常处理。经验是永远不要假设 LLM 会遵守系统提示词里的行为规则校验层必须独立存在。5.4 常见问题速查表问题可能原因解决动作Agent 规划阶段超时prefill 过长导致精简系统提示词意图分类走小模型工具重复执行重试不幂等写操作统一加幂等键模型输出乱引工具工具描述不清工具名规范化参数 Schema 强制校验vLLM 显存溢出max-model-len 设置过大按业务实际长度设置必要时降量化精度内网依赖装不上传递依赖缺失用 pip download 全量 wheelhouse 方案并发一高接口卡死async 代码里混同步阻塞调用同步模型调用丢线程池长任务走队列用户等待太久体验差无流式输出模型生成走 SSE 流式6. 实测数据与个人感想6.1 一组有代表性的压测数据系统上线前我做了一轮内部压测压测场景是查询错率 生成报告 按需建工单的混合流程模型为 32B AWQ 量化部署在单张 A100 80G 上。数据如下并发数平均响应时间P95 响应时间整体成功率备注106.8s9.2s100%流式输出用户感知良好3011.5s16.8s98.5%偶发重试未出现雪崩5018.4s27.6s96.2%建议启动多实例8029.8s42.5s92.0%单实例已到瓶颈这个数据印证了一个判断单卡单模型在 50 并发以内是可用的超过就要考虑拆分实例或对任务做优先级队列。业务侧最终给的并发预估在 30 左右所以单实例就扛住了。6.2 我踩过的几个坑和体会整个项目下来最有价值的一条体会是隔离内网里做 Agent工程纪律比技术炫技重要十倍。公网环境里出问题可以快速打开文档、查社区、换一个库试试隔离内网里每一步都要先想在前面版本锁定、工具白名单、幂等重试、人工确认这些看起来不酷的机制才是系统能长期稳定跑下去的根基。另外一个体会是不要把 Agent 想得太智能。它本质还是LLM 做规划 工具做执行的自动化流水线模型会在你意想不到的地方出幺蛾子。我在上线后陆续收到过几次用户的反馈比如帮我查一下昨天的告警会被理解成查昨天全天的告警而不是最近 24 小时这种语义歧义问题只能靠不断积累的用户反馈去微调提示词和工具描述没有一劳永逸的解法。最后再分享一个实用小技巧给 Agent 的所有外部调用都加一行 request-id 关联日志。用户在群里反馈刚才那个任务结果不对你只要拿到时间点就可以顺着 request-id 把模型的输入、规划步骤、工具调用参数、每一步耗时完整回放出来。这个习惯帮我省了无数次排查成本强烈建议所有做 Agent 工程的人都从第一天就做起来。