第一次听到“隔离内网里部署AI Agent”这个需求时,我并没有太当回事。模型文件、代码仓库都攥在手里,无非是把公网的部署流程换到一个没外网的环境里再跑一遍。现实很快打脸:模型权重拷不进去、Python依赖装到一半报错、内网盘里散落着各种版本的安装包却没人说得清哪个配套哪个,而最让我崩溃的是,LangChain在Agent调用工具时默认还会去请求外网。那段时间我几乎每天都在“这功能公网十分钟能搞定,内网折腾一下午”的循环里煎熬。后来我慢慢摸清了门道,把整套基于FastAPI、LangChain和LangGraph的AI Agent方案在隔离内网里稳稳跑了起来。这篇文章就把我踩过的坑、验证过可行的路径和能直接抄走的配置整理出来,给同样被困在内网里的朋友做个参考。
1. 为什么说隔离内网里的Agent开发和公网是两种工程
先聊清楚一个前提:很多人理解的“隔离内网”就是一台不能上外网的电脑,顶多装软件麻烦点。但真正在政企、制造、能源这类环境里泡过的人都知道,事情远不止这么简单。
1.1 隔离内网的两种形态,决定了你的工作量
隔离内网大致分两类。一类是物理隔离,机房和办公网之间没有网络链路,甚至连USB接口都管控;另一类是逻辑隔离,比如网络安全区的防火墙策略,只放行特定协议和特定IP端口,但可以访问镜像仓库、内网DNS,模型文件也能通过审批走通道。物理隔离环境里,你连pip install的基础源都没有,所有依赖必须用离线分发的方式解决;逻辑隔离环境相对友好,但往往规则复杂,今天能通的某个服务下周可能就变了策略,代码里的超时设置和重试逻辑反而比公网环境要求更高。
我当时遇到的是物理隔离加一台新建的GPU服务器。坦白说,一开始我心里想的是“总会有办法的”,结果第一周全在解决环境问题,Agent的核心逻辑一行没写。这也是很多人对隔离内网开发的第一大误判:以为工作是写Agent,实际上工作是先解决“怎么把Agent需要的一切搬进去”。
1.2 模型、依赖、工具,三个地方轮番劝退你
第一个卡点是模型。AI Agent的核心是LLM推理,公网环境下直接调API就行,几行代码结束战斗;隔离内网则必须私有化部署一套推理服务。模型文件从哪来、用哪个版本、量化到什么精度能塞进当前显存,这些问题没有一个可以蒙混过关。
第二个卡点是Python依赖。LangChain、LangGraph这类框架的依赖树非常庞大,而且更新极快,很多底层库对Python版本和操作系统版本都有严格要求。你在公网环境pip install一秒装完的东西,离线环境下如果版本没锁定好,装一半卡住是常态。更麻烦的是,部分库在安装时要拉取一些动态文件甚至要联网编译,这种在离线环境几乎无解。
第三个卡点是外部工具。Agent的价值在于“能干活”,可它干活一般是靠调用API、操作数据库、对接内部系统。公网环境里,模型服务、工具服务、业务系统都挂在互联网上,只要配好密钥就能串起来;隔离内网里,这些服务可能散在不同网段的机器上,数据库账号要单独申请,消息队列不会自动帮你创建,甚至目标系统只允许指定来源IP访问。你不得不在Agent和业务系统之间再写一堆适配层。
这些问题的共同点是:它们不发生在代码逻辑上,而发生在交付路径上。这也是为什么我认为隔离内网里的Agent工程,本质上更像一个系统交付项目,而不是一个算法开发项目。如果一开始没有这个心态,后面每一步都会很痛苦。
2. 模型与依赖的离线落地,先把地基打牢
这一节说的是我个人觉得整个项目中最枯燥、但最不能出错的部分:把公网环境的一整套东西原封不动搬进内网。
2.1 模型选型:先用显存倒推,不要先谈效果
在隔离内网做Agent,模型选择的第一原则不是效果最好,而是“你的卡能跑得动”。我见过不少团队在设备还没到位时就锁定了70B级别的模型,等到机器一看只有一张48G卡,再回头换模型,白白浪费工期。
我建议按显存倒推模型参数量,参考以下经验值:
| 模型规模 | 量化方式 | 推理所需显存(约) | 适用的硬件举例 | 落地体感 |
|---|---|---|---|---|
| 7B/8B | Q4_K_M | 6-8GB | 单卡RTX 4090 24G | 轻量内部问答,工具调用可用,速度快 |
| 14B | Q4_K_M | 12-16GB | 单卡A10/A100 24G+ | 综合能力和工具调用比较稳,我最终选了这档 |
| 32B | Q4_K_M | 22-26GB | 单卡A100 40G/H100 | 复杂规划和长文档理解更好,但对GPU压力大 |
| 70B | Q4_K_M | 40-48GB | A100 80G或双卡 | 最接近商业API体验,但部署和调优门槛高 |
这个表格不是精确测算,但能给你一个起步的参考线。隔离内网环境硬件很难临时扩容,所以选型时宁可保守:先在目标机器上让模型以最低显存跑通,再去追求效果。我在实际项目中最终选了14B量化版本,配合后文会讲到的结构化提示词工程,整体效果已经能满足大部分内部场景。
关于推理框架,我对比过三条路线:Ollama、vLLM、llama.cpp。Ollama安装简单,适合快速验证,但生产环境的并发控制粒度不够细;llama.cpp在纯CPU机器上也能跑,适合没有GPU的极端环境;vLLM吞吐高,支持OpenAI兼容接口,最适合给Agent这种需要频繁调用的场景做后端服务。我当时在GPU服务器上用的是vLLM,另外在备用CPU节点上放了llama.cpp,以防GPU故障时Agent至少还能降级运行。
2.2 依赖搬运的正确姿势
离线安装Python依赖,如果只用pip install xxx现找包,十有八九会陷入依赖地狱。我的做法是提前做一次极致锁定。先在公网环境里建一个干净的虚拟环境,把Agent服务的所有依赖装好、测试通过,然后生成一份精确到小版本的requirements.txt:
pip freeze > requirements_lock.txt接着在联网机器上用以下命令把依赖包全部拉下来:
pip download -r requirements_lock.txt -d ./offline_packages \ --platform manylinux2014_x86_64 \ --python-version 3.11 \ --only-binary=:all:这里有几个需要注意的细节:--platform、--python-version、--only-binary=:all:这三个参数是在目标机器和下载机器系统架构不同时必须带的,否则下载下来的wheel可能根本不是内网机器能装的那个版本。进入内网后,安装命令是:
pip install --no-index --find-links=./offline_packages -r requirements_lock.txt如果你是逻辑隔离环境,能连内网私有PyPI源,也可以把offline_packages推到源站上,团队成员直接用--index-url指向内网源安装,比拷贝目录更高效。
另外说一个很容易被忽略的问题:LangChain生态的代码版本变化非常快,在某一次升级后,即使同一个类名,参数签名和返回结构都会变。所以requirements_lock.txt必须和项目代码一起纳入版本管理,迁移到内网时也要保证代码和依赖是同一个发布批次,千万不要“代码拷最新的,依赖装旧的”,出问题的时候很难定位。
2.3 Embedding模型与向量库的取舍
Agent要做私有知识库问答,就免不了向量检索。Embedding模型同样需要离线加载,我建议选择sentence-transformers框架支持的中文模型,比如bge-large-zh这类。在公网下好整个模型目录,拷贝进内网后直接通过本地路径加载:
from sentence_transformers import SentenceTransformer embedder = SentenceTransformer("/opt/models/bge-large-zh")这里最大的坑是sentence-transformers在加载模型时会尝试联网检查版本更新。解决方法是设置环境变量HF_HUB_OFFLINE=1,强制走离线模式,不然每次首次调用都会卡在超时上。
向量数据库的选择,公网环境很多人直接用Chroma或者FAISS,但隔离内网里我更推荐用PostgreSQL加pgvector插件。理由很简单:内网环境大概率已经存在PostgreSQL实例,并且有成熟的备份、权限和运维体系,Agent服务直接复用数据库,不用额外维护一套新组件。pgvector在千万级向量规模下性能足够,关键是它能和业务数据放在同一个事务里做一致性处理,这才是被很多人忽略的优势。做一个对比你就明白了:
| 方案 | 适合场景 | 内网落地难度 | 生产可靠性 |
|---|---|---|---|
| FAISS | 单机快速原型 | 低,但缺少自带持久化和权限控制 | 一般 |
| Chroma | 小规模独立知识库 | 低,但内网资料少,社区方案少 | 一般 |
| Milvus | 亿级向量规模 | 高,组件多,运维复杂 | 高 |
| PostgreSQL + pgvector | 与业务数据共存的场景 | 低,复用现有DBA | 高 |
我在隔离内网里没有引入任何独立的向量数据库,所有Agent的知识检索都走PostgreSQL,一次事务搞定“业务提单查询”和“相似文档召回”,这套组合在稳定性上完全够用。
3. FastAPI + LangGraph 搭服务骨架:从对话到行动
地基打好之后,我才开始写Agent核心逻辑。很多教程会把LangChain和LangGraph拆开讲,但在隔离内网的实际工程里,它们是一套组合拳:LangChain负责工具、模型、检索这些基础组件,LangGraph负责把“理解意图-调用工具-整理结果”编排成有状态的工作流。我用FastAPI把这套工作流包成REST接口,给上游业务系统调用。
3.1 FastAPI这一层到底管什么
FastAPI在这里不是主角,但它的职责边界一定要清晰。在我的项目里,FastAPI只做四件事:接收入站请求、校验身份权限、把请求交给LangGraph工作流、异步返回结果。不要试图把Agent业务逻辑塞进路由函数里,否则后面做并发控制和任务复用时会非常痛苦。
一个典型的入口长这样:
from fastapi import FastAPI, Depends, HTTPException from pydantic import BaseModel from agent_core.graph import build_agent_graph app = FastAPI() class AgentRequest(BaseModel): session_id: str user_query: str class AgentResponse(BaseModel): session_id: str answer: str trace: list[str] compiled_graph = build_agent_graph() @app.post("/api/agent/invoke", response_model=AgentResponse) async def invoke_agent(req: AgentRequest): # 这里不要写业务逻辑,只做参数校验 result = await compiled_graph.ainvoke({ "session_id": req.session_id, "user_query": req.user_query, }) if not result.get("success"): raise HTTPException(status_code=502, detail="agent execution failed") return AgentResponse(**result)关于鉴权,隔离内网不像公网那样暴露在攻击面下,但也不能裸奔。为了避免被同网段其他服务误调用或内部越权,每个调用方分配一个Token,自定义一个依赖函数做校验即可。不要为了省事在Filter里写死IP白名单,因为内网服务的IP经常因为资源调度而变动。
3.2 LangGraph图编排落地:把“计划-执行”变成可维护的代码
LangGraph的核心价值是把一个Agent的运行过程显式地画成图:节点是动作,边是转移条件。隔离内网环境里没有外部API可依赖,也很少有现成的Agent编排平台,所以这套逻辑自己维护,LangGraph反而成了最合适的选择——它不依赖云服务,纯本地运行,状态存在内存或可持久化的存储里。
我的Agent图设计成四个节点:意图识别、计划生成、工具调用、结果汇总。计划生成节点调用的是本地LLM,工具调用节点通过LangChain的Tool机制去执行内网操作。
from typing import TypedDict, Literal from langgraph.graph import StateGraph, START, END class AgentState(TypedDict): session_id: str user_query: str plan: str tool_name: str tool_input: dict tool_output: str answer: str def parse_intent(state: AgentState): # 调用本地LLM,判断用户是想闲聊还是想办业务 intent = llm_judge(state["user_query"]) return {"plan": intent} def select_tool(state: AgentState): # 根据意图选出具体工具和参数 return { "tool_name": "query_ticket", "tool_input": {"keyword": state["user_query"]}, } def run_tool(state: AgentState): # 通过LangChain工具注册表执行,避免硬编码 result = tool_registry.call(state["tool_name"], state["tool_input"]) return {"tool_output": result} def answer_with_result(state: AgentState): # 把工具结果附带到提示词里,请求LLM做最终回答 messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": build_final_prompt(state)}, ] resp = local_llm.chat(messages) return {"answer": resp} # 条件路由 def route_after_intent(state: AgentState) -> Literal["need_tool", "direct_answer"]: return "need_tool" if state["plan"] == "business" else "direct_answer" builder = StateGraph(AgentState) builder.add_node("parse_intent", parse_intent) builder.add_node("select_tool", select_tool) builder.add_node("run_tool", run_tool) builder.add_node("answer_with_result", answer_with_result) builder.add_edge(START, "parse_intent") builder.add_conditional_edges( "parse_intent", route_after_intent, {"need_tool": "select_tool", "direct_answer": "answer_with_result"} ) builder.add_edge("select_tool", "run_tool") builder.add_edge("run_tool", "answer_with_result") builder.add_edge("answer_with_result", END) app_graph = builder.compile()这套设计的好处是,每个节点的职责单一,出问题时可以直接看日志定位到具体节点。坏处是,如果节点之间隐式共享了太多全局状态,图的可维护性会迅速退化。实际编码时要克制,尽量让每个节点的输入输出都用state显式传递。
LangGraph的图结构在隔离内网里还有一种额外价值:你可以把每一步的中间结果全部打印到日志里,这比公网环境里大家习惯的“黑盒调用一次API”更可控。因为内网环境里业务方往往还要求你能回答“它为什么干这件事”,有图结构做支撑,审计从技术上就更容易实现。
3.3 内网LLM调用封装的那些小细节
不管后端跑的是vLLM还是Ollama,它们一般都会暴露一个OpenAI兼容的HTTP接口。LangChain可以直接通过ChatOpenAI类来连接,只需要把base_url指向内网地址。这里有两个细节要特别注意。
from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="qwen2.5-14b-instruct", base_url="http://172.16.10.20:8000/v1", api_key="internal-placeholder", temperature=0.1, max_tokens=1024, timeout=60, )第一个细节是api_key。即使内网推理服务不做真实鉴权,也必须传一个非空字符串,因为OpenAI的SDK在api_key为空时会直接抛异常。第二个细节是timeout。本地模型在长文本场景下首字延迟可能很高,默认的几十秒超时往往不够用,建议放到60秒以上,并且在调用侧做重试和熔断。如果不设置重试,一旦推理服务正在处理一个超长请求,Agent的本次调用就可能直接失败。
另外,我强烈建议在agent内部再包一层简单的LLM调用封装,统一管理重试和降级:
def chat_with_fallback(messages: list[dict]) -> str: try: resp = llm.invoke(messages) return resp.content except Exception: # 降级到CPU节点上的llama.cpp服务 fallback = ChatOpenAI( model="qwen2.5-14b-instruct", base_url="http://172.16.20.30:8080/v1", api_key="internal-placeholder", ) return fallback.invoke(messages).content这里多写几行代码,换来的是GPU故障时Agent还能以较慢速度继续服务,对业务连续性来说非常值。
4. AI Agent怎么扛并发:先找瓶颈,再谈方案
搜索热词里“ai agent怎么扛并发”是很多人最关心的。我的经验是,隔离内网里做并发,第一件事不是打开nginx调worker数,而是先搞清楚你的请求到底会被哪一层堵住。
4.1 并发瓶颈通常不在你的FastAPI上
Agent服务的主流程是:HTTP请求进来,规划节点调用LLM,执行节点调用内网工具,最后LLM生成答案返回。整条链路里,最慢、最容易成为吞吐瓶颈的是LLM推理。一个14B量化模型在单卡A100上生成一个800字回答,大概要10到20秒。即使FastAPI本身可以秒级响应,整个Agent请求也是慢的。
如果你用FastAPI的异步接口配合LangGraph的ainvoke,请求之间在I/O层是并发调度的,但LLM服务内部如果串行处理,依然是排队。所以真正要解决的是推理服务的并发能力。vLLM的优势在这里就出来了,它通过PagedAttention和continuous batching技术,可以用很小额外的显存同时处理多个请求,把GPU利用率拉满。
部署vLLM时建议开启--max-num-seqs参数限制每个GPU上并行的序列数,我当时根据显存和请求长度设置为16,超过这个数的请求会排队而不是直接报错。真实并发模型下,单卡A100配14B模型,大概能稳定支持五六个同时调用的Agent会话,每个会话的响应时间还长得“可见”,这对内部工具类场景已经完全够用。
4.2 三种并发策略的取舍
从工程角度看,阻止服务被打垮的方案有三层,你可以按需组合。
第一层是入口限流。用令牌桶限制单用户或全局的调用频率,超出直接返回“排队中”。隔离内网里业务方往往能理解这种设计,因为内部系统本来就经常有服务窗口。
from fastapi import FastAPI, HTTPException import time rate_limit_store = {} def check_rate_limit(user: str, max_calls: int = 10, window: int = 60): now = time.time() window_start = now - window if user not in rate_limit_store: rate_limit_store[user] = [] recent = [t for t in rate_limit_store[user] if t > window_start] if len(recent) >= max_calls: raise HTTPException(status_code=429, detail="too many requests") recent.append(now) rate_limit_store[user] = recent这只是一个极简演示,生产环境建议用Redis做分布式限流计数。但如果你的Agent只部署一个节点,本地字典也够了。
第二层是信号量控制并发。给整个Agent图执行包一个全局信号量,比如同时最多8个Agent请求在跑,多余的请求在入口处挂起等待。
import asyncio agent_semaphore = asyncio.Semaphore(8) async def invoke_with_global_limit(req: AgentRequest): async with agent_semaphore: return await compiled_graph.ainvoke({...})这个方法特别好用,因为它同时保护了下游的LLM服务和内网数据库。防止代理在一个业务高峰时段把内网数据库连接池打爆,这是隔离环境里最容易出事故的地方。
第三层是异步任务队列。如果业务本身可以接受“提交任务后迟一点拿结果”,就把请求拆成两步:先返回task_id,后台用Celery或简单的任务队列慢慢跑,用户或上游系统轮询任务状态。这在“AI Agent扛并发”的讨论里其实是终极解法。很多耗时长的Agent应用,比如批量生成报告、批量分析日志,根本不要求同步返回,异步化之后并发压力瞬间下降一个数量级。
from fastapi import BackgroundTasks task_store: dict[str, dict] = {} def run_agent_task(session_id: str, user_query: str): task_store[session_id] = {"status": "running", "result": ""} try: result = compiled_graph.invoke({"session_id": session_id, "user_query": user_query}) task_store[session_id] = {"status": "done", "result": result["answer"]} except Exception: task_store[session_id] = {"status": "failed", "result": ""} @app.post("/api/agent/async") async def async_agent(req: AgentRequest, bg: BackgroundTasks): bg.add_task(run_agent_task, req.session_id, req.user_query) return {"task_id": req.session_id, "status": "running"}隔离内网里,由于没有云厂商的消息队列可以白嫖,我建议第一版老老实实用关系库建一张任务表,配合后台worker线程轮询更新,这套方案虽土但在内网环境里最不容易出故障。
4.3 一个可用的并发容量估算方法
网络上有很多关于LLM吞吐和QPS的复杂测算方法,但对大多数内部Agent应用,简单版本已经够用。核心估算是:
单实例可支持并发数 = GPU推理服务的单请求平均生成速度(tokens/s) ÷ 单请求平均生成长度
举例:假设你的vLLM部署的模型在满负载时能输出500 tokens/s,每个Agent请求的LLM部分平均生成600 token。那么每秒最多完成0.83个请求,一分钟约50个请求。如果你的业务峰值是每分钟200次调用,那么至少需要保证单实例能跑满,或者把Agent图拆成多副本部署。
这里很容易翻车的一个点是:LLM的生成token数不是平均几百就行,工具调用型Agent经常需要生成一长串JSON或工具参数,这部分token可能高达1500甚至更多。所以容量规划时务必用实际业务请求做打点统计,而不是拍脑袋定一个数。
同时不要忘了一个成本极低的优化:语义缓存。内网环境里,同一个问题被反复问的概率远高于公网。把用户请求先做embedding,在向量库里查一下语义相似度,相似度超过0.95就直接返回历史答案,不再调用LLM。这个优化在内部运维问答场景效果惊人,我实测缓存命中率能到30%以上。
5. Agent中台化落地:让智能体真的在业务里“下地干活”
项目做到这里,Agent已经能稳定回答问题和执行一些工具。但要想让它被业务方认可,必须从“我提供了一个API”升级到“我提供了一套Agent能力中台”。这个转变不只是架构上的,更是交付姿态上的。
5.1 从“给接口”到“给中台”,差的是三个抽象
单纯把Agent包装成一个REST接口交付出去,业务方很快会抱怨:为什么这个接口只认你们定义的字段?为什么你想调我们系统时还要单独提权限?中台化要抽出三层:
- 能力注册层:业务系统新增一个可供Agent调用的能力时,只需在注册中心登记,不需要改Agent代码。
- 知识接入层:知识库文档、FAQ、历史工单可以按权限动态挂载到某个Agent上,不需要重编译。
- 会话管理层:会话状态统一存储,支持人工接管、审计追溯和中断恢复。
我在内网里用一张资源注册表和一套访问Token完成了第一版中台。每个业务Agent注册时提交自己的名称、描述、技能列表和数据权限,调用方按Agent维度获取访问凭证,这样业务部门能自助创建新Agent,而不是每次找开发排期。
这里有个通用经验:不要为了中台而中台。如果你的组织只有一条业务线和三个工具,直接硬编码反而更好维护。中台的价值在“有多个业务方都要接入、工具数量持续增长”时才真正体现出来。隔离内网环境里,组件越多越难运维,做设计时要时刻问自己:这个抽象真的能省事吗,还是只是看着好看。
5.2 工具注册规范:让Agent学会该有的规矩
工具是Agent执行力的来源。在内网环境里,定义一个清晰的工具接入规范,比写1000行Agent代码更重要。我的规范包含四部分组成:名称、描述、参数JSON Schema、执行入口。其中描述最为关键,因为LLM是靠描述来决定调用哪个工具的,描述写得模棱两可,再好的模型也会选错工具。
{ "name": "query_asset_by_ip", "description": "按IP地址查询内网资产归属、负责人和开放端口,用于故障排查和服务发现", "parameters": { "type": "object", "properties": { "ip": { "type": "string", "description": "目标IP地址,例如192.168.1.100" }, "include_ports": { "type": "boolean", "description": "是否返回端口信息,默认false" } }, "required": ["ip"] }, "execution": { "type": "http", "endpoint": "http://ops-asset-api:8080/api/asset/query", "method": "POST", "api_key_env": "ASSET_API_KEY" } }工具执行后一定要有标准返回结构,我统一用{success: true/false, data, error_message}三件套,这样Agent做结果汇总时不会迷失在千奇百怪的工具返回值里。这一条本身是从失败里学来的:一开始内网某个工具返回的是纯文本的一坨表格,LLM在总结时频频产生幻觉,格式标准化后幻觉发生率立刻下降。
工具注册后还必须在调用链路上做权限校验。内网是一个高信任环境,但Agent的调用能力越强,越要控制谁能触发。我把每个工具都挂一条“白名单角色”配置,没有权限的Agent直接拒绝执行,这比事后追溯靠谱得多。
5.3 对话不是目的,任务闭环才是
“让AI真的下地干活”这句话我很赞同。很多时候我们做Agent,眼光只停在“能答上问题”就算成功,但业务方真正要的是“问题背后的事被解决掉”。所以我在Agent图里加了一个“结果确认”环节:当Agent调用工具成功、生成答案后,它会把操作结果和提交记录一并写入审计库,并且需要通过一个确认接口让请求方确认任务是否闭环。如果是查询类任务,返回答案就算闭环;如果是操作类任务,要等对方业务系统回调成功才闭环。
举个实际例子:内部团队用Agent做“帮我查某个服务在哪个节点上,然后重启它”。查询阶段,Agent能准确找到节点;重启阶段,Agent调用运维平台的执行接口,然后轮询运维平台的任务状态,成功才给用户返回“已重启,当前进程状态正常”。整个过程,用户只需要发一句话,Agent在各个系统之间穿梭,最后给出可验证结果。做到这一步,业务方才会真正把Agent当工具用,而不是当玩具。
在这一节最后,我想提醒一点:隔离内网里的Agent中台,不要过度依赖可视化编排工具。你会发现在没有外网的环境里,最稳定的依然是代码化、版本化的流程定义。LangGraph的图就是代码,出了问题可以直接走代码评审、灰度发布、快速回滚,这一套在政企环境里是硬要求,可视化拖拽虽然方便,但很难满足审计要求。
6. 复盘:我踩过的坑和你大概率也会踩的坑
最后一部分,我把整个项目里最有代表性的问题整理成清单,每个都标注了表现、原因和对策,这些经验是我花了不少加班时间换来的,希望你能直接绕过。
| 坑 | 表现 | 根因 | 对策 |
|---|---|---|---|
| LangChain版本升级 | 相同代码在不同机器上调用方式不一致甚至报错 | 框架更新快,接口不稳定 | requirements锁定精确版本,代码和依赖同批次迁移 |
| 本地模型幻觉 | Agent一本正经胡编工具参数 | 中文模型对工具描述的遵循能力不足 | 工具描述写详细,参数枚举全列出来,必要时few-shot |
| 模型tokenizer不匹配 | 生成结果乱码、重复 | 只拷贝了模型权重,没拷词表文件 | 从公开源完整下载整个模型目录,不要只挑权重 |
| vLLM服务OOM | 高并发下服务崩溃 | --max-num-seqs设置过大 | 按显存调低并发数,留20%显存余量 |
| 向量库检索质量差 | 知识问答答非所问 | Embedding模型和内网文档领域不匹配 | 选择中文场景训练过的Embedding模型,并把文档切成适合的段落 |
| 内网证书问题 | HTTPS调用报SSL错误 | 内网自签证书不在信任链 | 关闭校验或将自签证书加入信任库,根据安全策略评估风险 |
6.1 我最想单独说说的两个隐蔽问题
第一个是LLM的temperature参数。隔离内网里做工具调用型Agent,很多人习惯沿用对话场景的temperature=0.7,结果模型在生成工具参数时经常发挥“创造力”,导致参数格式错乱。我的做法是:规划节点和工具参数生成用低随机度(temperature=0.1),最终面向用户的生成环节再适度放开。同一个Agent图里不同节点使用不同参数,在LangGraph里是需要额外设计的,但效果立竿见影。
第二个是内网环境的DNS和服务发现。公网环境里服务之间调用走域名解析很自然,内网环境往往DNS记录不全,或者干脆没有内网DNS。我曾在代码里写了http://agent-core这样的域名,结果到内网完全解析不了。最后统一改成配置中心下发IP地址,并约定每个服务启动时检查配置是否就绪。宁可配置繁琐一点,也不要依赖你控制不了的内网DNS服务。这种做法看着土,但实际排错时能少掉好多头发。
6.2 对其他技术栈的顺带观察:Rust与Spring AI的补位
搜索热词里出现了“基于rust语言ai agent”和“spring ai agent”,我在项目里没有正式采用,但做过调研,可以给你一点参考。Rust的高性能很适合做工具侧的执行引擎或者网关层,比如用Rust写一个让Agent调用的内网资产扫描服务,Python的Agent通过HTTP调用它,两边都不吃亏。但让Agent本身的核心编排逻辑用Rust重写,当前生态还不够成熟,遇到模型调用和工具编排的复杂逻辑时,开发效率会比Python低不少。
Spring AI则适合纯Java技术栈的团队,特别是隔离内网里大量存量系统都是Spring Boot,用它来把Agent能力嵌入业务系统,链路最短。不过Spring AI的Agent编排能力和LangGraph相比还比较基础,复杂状态机场景需要自己补轮子。我的倾向是:如果你的团队是Java为主的交付团队,就优先用Spring AI;如果Agent本身是要作为一个高灵活度的新系统,LangChain加LangGraph的生态还是更顺手。这两种选择不存在谁绝对优于谁,关键看Agent在你们组织结构里由谁负责长期迭代。
6.3 如果让我重来一遍,第一步会做什么
如果这段经历能重来,我会在第一周就放弃“先跑通公网Demo再迁移”的念头,直接在内网环境里做最小闭环:一个本地LLM推理服务,一条FastAPI路由,一个只能调用一个工具的LangGraph图。先让这条极简链路在内网彻底跑通,再逐步把知识库、业务工具、多轮会话功能加进去。
这样做的好处非常实际。隔离内网环境复杂,越早发现环境问题,越能降低返工成本。而且最小闭环能让业务方在极短时间里看到“AI确实在内网里干活了”,这种信心建设比任何PPT都有效。
最后再分享一个我在多次内网实战后沉淀下来的想法:Agent在隔离内网里落地,最大的敌人不是模型效果,不是并发性能,而是“不可见”。公网环境里你依赖的每一个组件、每一次算法调优,至少都能通过搜索查到别人的答案;隔离内网里,你遇到的每一个诡异问题都可能是你专属的“限定皮肤”,必须靠架构简化、日志可观测、版本控制和逐步推进来对抗这种不确定性。不要期待一次部署就风平浪静,把排查问题的手段准备充分,剩下的就是耐心把它做扎实。