
上个月我们组接到一个需求在公司隔离内网里搭一套 AI Agent让模型按自然语言指令去查内部系统、整理数据、触发后续流程。刚开始大家觉得不就是调个模型API嘛真正动起手来才发现“隔离”这两个字把几乎所有“开箱即用”的东西全部挡在了外面——模型权重进不来、Python依赖装不上、Agent要调的服务散落在内网各个角落、并发一上来模型服务自己先垮。这篇文章是从零到一落地这套系统的完整过程包括架构选型、离线模型接入、LangGraph工作流设计、并发改造、离线部署以及中间踩过的一堆坑。内容偏工程实践适合在隔离内网、私有化环境或者受控网络里做 AI 应用的团队参考尤其是手里已经有一个明确业务场景、想让 Agent“真的下地干活”的读者。1. 隔离内网为什么会让 Agent 工程“打回原形”1.1 先说清楚“隔离”到底隔离了什么我们常说的“隔离内网”在不同企业里的形态并不一样。有的是物理隔离机器之间不连公网有的是逻辑隔离防火墙策略只放行特定端口还有的是数据隔离网络通但数据出域要审批。无论哪种对 AI Agent 工程来说影响是共性的第一SaaS 大模型 API 不可达。市面上大部分 Agent 框架默认对接的都是云端模型服务隔离内网里这一层直接断掉必须换成内网自建的推理服务。第二依赖安装难。pip install、npm install、docker pull这些平时最不起眼的动作在没有外部源的网络里全都要变成离线搬运。LangChain 生态里很多组件默认还会拉Hub模板、公共Embedding模型这也是要逐个排查的隐性问题。第三Agent 要调用的工具系统形态复杂。我在实际项目里遇到的内部系统有老HTTP接口、有内部RPC、有只允许特定域名访问的网关甚至还有只能从某台跳板机访问的业务系统。Agent 要去编排这些工具第一个要解决的问题不是“模型能不能理解”而是“这个接口在内网环境里到底能不能被稳定调用”。第四数据和知识库的边界。Agent 的上下文、RAG 检索源、会话记忆这些数据都不能出域。向量库、文档库、搜索引擎全部要部署在内网这意味着整条数据链路都要自己搭建和维护。1.2 云端的 Agent 架构搬到内网为什么跑不起来我一开始也天真地以为把现有 Agent 代码拷到内网机器上就能跑。实际跑起来才发现默认配置下有一堆隐式的外部依赖很多 LangChain 组件的默认实现比如搜索工具、网页读取、公共Embedding模型都会试图访问外部服务部分框架的初始化逻辑会去拉取配置模板或者做遥测上报虽然不影响主流程但在严格受控网络里会被安全策略拦成超时提示词模板、模型列表这类元信息也经常有默认的外部源。所以从云端架构迁移到隔离内网不能只改一个base_url而是要把“一切外部依赖”都显式地替换成内网实现。搜索换成内部ES、Embedding换成离线模型、工具调用换成内网API每一步都要做一次“依赖审计”。1.3 动手前先定边界能省一个月的返工我们在项目初期踩过最大的坑是需求边界没有冻结就开始做技术选型。Agent 能做什么、不能做什么哪些内部系统可以暴露给模型调用所有权限操作是否需要人审批这些一定要在动手前定清楚。我建议用一个“最小闭环”来跑通全链路比如“让 Agent 根据用户提问从内部文档库检索资料并整理成结构化回答”。这个闭环虽然简单但已经涵盖了模型接入、Embedding、向量检索、提示词编排四条核心链路。闭环跑通之后再逐步把工具调用、记忆、多轮对话、并发能力叠加上去比一上来就设计一个大而全的架构要稳得多。2. 总体技术方案FastAPI LangChain LangGraph 离线全家桶2.1 每个组件在整个系统里负责什么隔离内网环境下不需要追求花哨的架构而是要追求每一层职责清晰、可替换、可排查。我们最终的技术栈是这样的层级选型职责对外服务层FastAPI接收用户请求提供SSE流式接口做鉴权和限流Agent编排层LangGraph定义Agent的状态机、节点、条件分支和循环工具层LangChain Tool封装内网API、数据库查询、流程触发等能力推理层vLLM 本地部署提供 OpenAI 兼容接口加载量化模型向量与记忆Milvus SQLite短期会话记忆和长期知识库检索部署形态Docker 离线镜像依赖锁定内网快速复制环境2.2 为什么不用 Spring AI、Rust 自研或者 Django选型阶段我们确实把市面上的主流方向都过了一遍。这个项目前后对比过三个方向Spring AI适合 Java 技术栈成熟、团队以 Java 为主的团队治理能力强但 AI 生态相对滞后隔离内网环境下很多组件需要自己补适配层开发速度会明显慢下来。Rust 自研 Agent 运行时在并发和资源占用上有绝对优势但 Agent 的核心价值在业务编排和工具生态Rust 的AI生态太薄模型调用、向量检索、LangChain工具集全都要自己封装交付周期不现实。Django 做 Web 层Django 本身很成熟但 FastAPI 的原生异步支持、SSE 流式响应和轻量程度更适合 Agent 这种“长请求、流式输出、高并发等待”的场景。最终选择 Python FastAPI LangChain/LangGraph核心原因就一句话这个场景的瓶颈不在语言性能而在AI生态的迭代速度和排错效率。LangGraph 提供的状态图和条件分支能直接解决 Agent 的复杂控制流我们没必要重新造一遍轮子。2.3 请求在系统里是怎么流转的一个用户请求从进入到返回完整的链路是这样的用户通过内外网统一入口把请求发给 FastAPI 服务FastAPI 生成request_id把请求交给 LangGraph 的图执行器LangGraph 按状态图先执行plan节点模型拆解用户意图执行tool节点调用内网工具服务获取数据执行verify节点校验结果是否满足要求不满足就回到工具节点重新调用最终结果通过 SSE 流式返回给用户。并发问题不放在编排层硬扛而是通过在推理层用 vLLM 持续批处理、在编排层用异步和信号量做限流把压力分散到独立的服务上。这个思路在后面第5节会详细展开。3. 离线模型服务的搭建与接口适配3.1 模型导入、量化选型和显存预算隔离内网没有现成的模型仓库模型权重需要走合规的离线方式前置导入内网。我们统一放在内网共享存储/data/models下面以目录为单位管理每个模型目录附带哈希校验文件。量化选型方面我前后试过 FP16、GPTQ、AWQ 和 GGUF 的 Q4_K_M。经验是内网业务场景面对的多是长上下文的工具调用和知识库问答7B~14B 量级的模型用 Q4 量化后性价比最高响应速度和显存占用都能兼顾。FP16 的效果确实更稳但显存占用翻倍吞吐明显下降如果不是对输出质量极其敏感不建议在初期用。以 7B 模型为例FP16 需要约 14GB 显存Q4 量化后只需要约 5GB 左右加上 KV Cache 和推理框架的预分配一张 24GB 显卡可以比较从容地部署。显存预算建议上浮 15% 到 20%给持续批处理和波峰请求留余地否则并发一上来很容易 OOM。3.2 推理服务选型vLLM 是首选Ollama 是备选推理服务我们最终用了 vLLM因为它的 PagedAttention 和 Continuous Batching 机制对高并发、多请求的场景提升非常明显。启动命令大致是vllm serve /data/models/Qwen2.5-7B-Instruct \ --served-model-name local-llm \ --max-model-len 8192 \ --gpu-memory-utilization 0.85 \ --port 8000这里有几个参数值得展开说明--served-model-name很重要。内网内模型名可以自定义但外部调用方和服务端必须一致我们统一约定为local-llm--max-model-len 8192是为了控住显存占用和推理耗时隔离内网的 Agent 通常不需要 32K 的超长上下文--gpu-memory-utilization 0.85是给系统留出余量贴满 0.95 在并发突增时非常容易 OOM。如果团队运维能力比较弱或者只是验证概念阶段Ollama 也可以作为备选。它部署简单但并发吞吐上限相比 vLLM 有明显差距等真正扛业务压力时还是要切到 vLLM 或类 TensorRT-LLM 的方案。3.3 LangChain 侧怎么接本地模型选推理服务时我特意强调要 OpenAI 兼容接口就是为了让 LangChain 的接入成本降到最低。接入代码非常简洁from langchain_openai import ChatOpenAI llm ChatOpenAI( base_urlhttp://infer-vllm:8000/v1, api_keyinternal-local, modellocal-llm, temperature0.1, max_tokens2048, timeout60, )api_key随便填一个非空值就行本地服务不校验但字段不能为空。temperature我压到 0.1因为 Agent 的工具调用和结果整理都要求稳定输出温度高了会出现各种莫名奇妙的格式问题。Embedding 模型同样要离线处理。我们用 BGE 系列模型加载到本地路径通过sentence-transformers提供向量化服务向量库使用内网部署的 Milvus。这里有一个很容易被忽略的点Chat 模型和 Embedding 模型是两个独立服务不能混用模型路径否则维度对不上检索出来的结果全是乱的。4. Agent 编排层LangGraph 工作流实战4.1 为什么用 Graph 而不是 Chain早期我们尝试过用传统的 LangChain Chain 做顺序调用后来发现并不可行。真实的 Agent 场景里控制流不是简单的一条线模型可能要先查库存、再查供应商、发现信息不够又回去重新查甚至要多次循环。用 Chain 表达这种复杂的分支和循环代码会越来越拧巴。LangGraph 的核心价值是把 Agent 的执行过程建模成一张有向图节点是具体的处理逻辑边是节点间的跳转条件。状态管理、分支判断、循环回路都变得很直观。4.2 一个可复用的工作流骨架我们项目里最常用的一个流程是“需求拆解 → 工具调用 → 结果校验 → 输出”对应的 LangGraph 代码骨架大概是这样的from typing import TypedDict, List from langgraph.graph import StateGraph, END class AgentState(TypedDict): messages: list plan: str tool_results: List[dict] ok: bool def plan_node(state: AgentState): # 让模型拆解用户需求返回工具调用序列 return {plan: query_stock - generate_order - verify} def execute_node(state: AgentState): # 按 plan 依次执行工具 results run_tools(state[plan]) return {tool_results: results} def verify_node(state: AgentState): # 校验工具结果是否完整 ok check_results(state[tool_results]) return {ok: ok} def respond_node(state: AgentState): # 把结果整理成最终回复 return {messages: [format_response(state[tool_results])]} graph StateGraph(AgentState) graph.add_node(plan, plan_node) graph.add_node(execute, execute_node) graph.add_node(verify, verify_node) graph.add_node(respond, respond_node) graph.add_edge(plan, execute) graph.add_edge(execute, verify) graph.add_conditional_edges( verify, lambda state: execute if not state[ok] else respond, ) graph.add_edge(respond, END)这里有一个投产必须加上的细节循环必须设置最大重试次数。内网工具接口偶尔不稳定如果没有上限Agent 可能在一个失败的节点上无限循环把模型调用次数烧光。我们在 State 里加了一个retry_count字段超过 3 次就直接进入兜底回复节点。4.3 工具注册与内网 API 对接工具层是隔离内网 Agent 最花精力的一层。我们用 LangChain 的tool装饰器封装内部接口from langchain_core.tools import tool import httpx tool def query_asset(asset_id: str) - str: 查询资产信息入参: asset_id resp httpx.get( fhttp://asset-svc.internal/api/v1/assets/{asset_id}, headers{Host: asset.internal.example}, timeout10, ) resp.raise_for_status() return resp.text[:2000]工具返回内容一定要做截断。模型上下文是有上限的工具返回一大坨JSON原始数据既浪费 token 又干扰模型理解。我们在封装层统一做清洗只保留关键字段并限制单次返回不超过 2000 字符。4.4 记忆与上下文的本地化Agent 的记忆分两层短期会话记忆用 SQLite 存绑定session_id每条对话记录带上时间戳长期知识记忆用向量库把历史问答中的高价值片段做向量化存储。这里要提醒一下内网环境的会话数据往往是敏感数据落库之前要先定好保留周期和脱敏策略。我们在存储层做了字段级别脱敏涉及工号、手机号、金额的字段统一打码再入库。5. 并发扛量从串行到流式的性能改造5.1 先弄清楚并发瓶颈到底在哪“AI Agent 怎么扛并发”是我们在内网联调时最头疼的问题。压测之后发现瓶颈往往不在流量入口而在三个深层位置。第一是模型推理服务。一次 Agent 请求可能触发多次模型调用拆解意图一次、生成工具参数一次、整理回答一次三次推理串下来单用户延迟轻松超过 10 秒。并发一上来推理请求全部堆在一个队列里。vLLM 的持续批处理能缓解排队但吞吐上限依然存在。第二是工具调用的阻塞。如果 FastAPI 的接口是同步函数里面调requests.get访问内网系统一个请求阻塞住事件循环后面所有请求都在等。第三是 Agent 编排本身的串行特性。LangGraph 的节点如果都写成同步执行整个流程就是一个不可并行的长事务。5.2 异步化改造与并发限流我们的改造分三步走。第一步FastAPI 接口全部改成async def工具调用层用httpx.AsyncClient替换requestsasync def call_internal_api(url: str): async with httpx.AsyncClient(timeout15) as client: resp await client.get(url) return resp.text第二步遇到不支持异步的 SDK 或老接口用asyncio.to_thread把它扔到线程池里执行并且用信号量限制并发数避免线程池被占满。第三步给整个 Agent 编排入口加并发闸门sem asyncio.Semaphore(8) async def run_agent(payload: dict): async with sem: return await agent.arun(payload)这里限流值 8 不是拍脑袋定的。我们通过压测发现7B 模型在单卡下超过 8 路并发 Agent 请求后P95 延迟急转直上、错误率开始出现模型服务的显存也逼近告警线。限流之后整体吞吐反而更稳定。5.3 压测数据说话这是我们内网环境下一组有代表性的实测数据模型是 7B 量化版单张 24GB 显卡Agent 完成一次完整工具调用链并发数P50 延迟P95 延迟错误率14.2s5.1s0%45.8s8.9s0%87.6s14.2s0%1612.4s28s3.2%可以明显看到16 并发时延迟和错误率都开始恶化。所以最终我们线上限流就放在 8同时通过消息队列把超出阈值的请求排队处理保证核心请求的成功率。5.4 流式输出大幅改善体验原来我们等 Agent 完整跑完所有节点才统一返回用户面对的是一个十几秒的白屏。后来改造为 SSE 流式输出模型每生成一段内容就实时推到前端from fastapi.responses import StreamingResponse app.post(/agent/stream) async def agent_stream(payload: dict): return StreamingResponse(event_generator(payload), media_typetext/event-stream)流式改造之后虽然整体完成时间没变但用户的感知等待时间从十几秒降到了两三秒体验完全不一样。对于内部系统的操作型Agent这一点非常值得做。6. 离线部署与依赖管理没有公网源的安装方式6.1 Python 依赖的离线安装隔离内网里没有 PyPI 源但依赖总是要装的。第一次我们尝试导出requirements.txt后手工拷贝 whl发现不同机器的 Python 版本和平台标签都不一样装到一半开始报兼容性错误。正确做法是在与线上一致的 Python 版本和镜像环境下提前把依赖锁定并整体导出pip download -r requirements.txt -d wheelhouse \ --platform manylinux2014_x86_64 \ --python-version 3.11 \ --only-binary:all:然后把整个wheelhouse目录拷贝进内网执行离线安装pip install --no-index --find-linkswheelhouse -r requirements.txt这里的关键是--platform和--python-version必须与最终运行环境一致否则导出和安装会出现严重的兼容性错位。如果团队规模大、服务多更建议在内网搭建一个 PyPI 镜像服务比如 devpi 或 Nexus多人协作时效率会高很多。6.2 Docker 镜像的离线搬运镜像层面的离线部署我们走的是“外网打包、内网加载”的流程# 在可访问外部镜像源的构建机上 docker save fastapi-agent:1.2.0 -o fastapi-agent-1.2.0.tar # 拷贝 tar 包到内网 docker load -i fastapi-agent-1.2.0.tar有一个从踩坑中总结出来的原则模型文件不要打进 Docker 镜像。镜像里只包含代码、依赖和配置文件模型权重通过 volume 挂载进来。否则每次模型微调、升级全量重传几个 GB 甚至几十 GB 的镜像发布速度不可接受。docker run -d \ -p 8080:8080 \ -v /data/models:/data/models:ro \ -v /data/logs:/app/logs \ fastapi-agent:1.2.06.3 模型与代码的版本管理隔离内网环境下模型和代码必须分开管理版本。代码走公司的 GitLab镜像打 tag模型目录用哈希校验文件记录版本任何一次模型替换都更新哈希文件。这样出问题的时候能快速定位是代码变更还是模型变更导致的。升级策略我们用的是最简单的蓝绿方式先启动新版本容器健康检查通过后再一次性切换流量入口。对于内部系统来说这种方式的稳定性和回滚速度都足够用。7. 实战踩坑记录四个让我印象最深的问题7.1 工具调用超时根因是内网服务发现现象是 Agent 调用资产系统接口时频繁超时但手动curl又正常。我用curl -v观察请求详情发现容器解析出来的 IP 没问题但接口返回的是网关 403。进一步排查才发现内部网关只信任特定域名容器环境变量里HTTP_PROXY和直连方式混合导致带Host头和网关期望不一致。最终的修复方式就是工具封装层显式携带正确的Host头同时统一走内网服务发现网关。这个问题给了我一个教训隔离内网的工具调用网络层的不确定性比模型本身高得多联调时要先做接口连通性测试再让 Agent 接入。7.2 模型输出不听话JSON 解析频繁失败量化模型在工具调用模式下的稳定性和云端大模型确实有差距。模型偶尔会在 JSON 前后输出多余的描述性文字或者用 Markdown 代码块包裹 JSON导致 LangChain 的解析器直接报错。我的处理方案是三管齐下一是temperature压到 0.1二是在提示词里明确指定输出格式三是在代码里写一个clean_json函数做兜底把代码块标记、首尾非 JSON 字符全部剥离后再解析。这个兜底函数救了无数次线上问题强烈建议做。7.3 并发一上来就 OOM上线初期我们把max-model-len设得偏大gpu-memory-utilization又顶到 0.95再加上 Agent 每次请求携带的上下文里包含之前多轮对话记录显存瞬间被打满。解决办法分三层第一把max-model-len下调到业务够用的 8192第二在 Agent 编排层对上下文做裁剪超过阈值的旧消息先压缩成摘要再进入模型第三显存利用率留 10% 以上的余量。三层都做了之后OOM 基本没有再出现过。7.4 Agent 链路太长日志追踪比普通服务更重要一个请求要经过模型、编排、工具、向量库四个环节出了问题很难定位。我们给每个请求分配了request_id在 LangGraph 的每个节点入口和出口都打印结构化日志包含当前节点名、输入摘要、耗时时长和状态。工具调用层单独记录调用参数、返回码和耗时。有一次线上问题就是靠日志链条定位的某个工具调用返回了空列表模型没有识别到异常直接把空结果当成正常数据输出给用户。如果日志里没有工具层的入参出参记录这个问题要排查很久。8. 上线之后的运维体会从能跑到跑稳项目上线后我复盘了几个对最后稳定性贡献非常大的动作。我们做了一套内部评测集大概 30 条真实业务问题覆盖文档问答、数据查询、流程触发三类典型场景。每次改提示词、换模型、调参数先在这套评测集上自动跑一遍人工标注满意度再决定是否全量上线。隔离内网环境下没有线上庞大的反馈数据开源社区的评测集又不贴合业务自建一套小而精的评测集是性价比最高的质量保障手段。灰度发布也值得一提。最开始我们直接全量开放给业务方用结果半天就被各种 badcase 淹没了。后来改成先让一个小团队试用两周收集真实反馈修复工具层和提示词的问题再逐步扩大范围。内部工具类 Agent 的用户耐心比外部用户更低第一次体验不好就很难再拉回来了。最后一个体会是隔离内网做 AI Agent九成精力其实花在工程上模型本身反而不是最难的环节。网络规划、依赖管理、日志追踪、并发控制这些看似不性感的“搬砖活”才是决定这套系统能不能长期稳定跑下去的关键。并发的增长也是分阶段的先让一条链路稳定跑通再叠加复杂度比一次性设计一个大而全的架构要靠谱得多。