
1. 隔离内网下的 AI Agent 工程到底难在哪先说说我为什么会碰这个题目。去年下半年开始团队里陆续有人尝试用 AI Agent 做一些内部工具的自动化比如自动整理工单、自动生成周报、自动跑数据核对。一开始大家都是在能连外网的开发机上玩用云端大模型 API配几个 MCP 工具跑得挺欢。但真正要落地到业务系统里问题就来了——我们的生产环境是物理隔离的内网没有外网出口不能调用任何云端服务所有东西必须在内网里自给自足。这个约束一下子把很多看起来很美的方案打回了原形。云端大模型用不了得换成内网部署的开源模型在线 MCP 服务连不上得自己搭本地 MCP Server依赖包不能随时 pip install得提前把离线包准备好甚至连 SQLite 这种轻量数据库都得考虑它在内网环境下的读写性能和并发问题。再加上前端要用 Vue 做一个可视化的 Agent 操作界面整个工程就变成了一个麻雀虽小五脏俱全的完整项目。我前后折腾了大概两个月踩了不少坑也总结出一套相对稳定的方案。这篇文章就把整个工程实战过程拆开讲包括架构选型、MCP 工具链搭建、Skills 设计、SQLite 数据层、Vue 前端集成以及内网环境下特有的那些坑。适合谁看如果你正在做 AI Agent 开发尤其是要在受限网络环境里落地或者你对 MCP、Skills 这些概念还比较模糊想找一个完整的实战参考那这篇应该能帮到你。我会尽量把为什么这么选讲清楚而不是只丢一堆配置让你抄。2. 整体架构设计与选型思路2.1 为什么是本地模型 MCP Skills这套组合隔离内网最大的约束就是没有外部服务可用。这意味着三件事模型必须本地部署工具调用必须本地实现数据必须本地存储。基于这个前提我把架构分成了四层。最底层是模型层跑的是内网部署的开源大模型通过一个兼容 OpenAI 接口的本地推理服务暴露出来。这样上层代码不用关心底层是什么模型换模型只需要改一个 base_url。中间是Agent 核心层负责对话管理、工具调度、上下文维护。再往上是工具层也就是 MCP Server 和 Skills 的集合Agent 通过 MCP 协议调用这些工具。最上面是交互层用 Vue 做的 Web 界面方便非技术同事也能用。为什么选 MCP 而不是自己写一套工具调用协议因为 MCP 已经是事实上的标准了社区里有大量现成的 Server 实现比如文件操作、数据库查询、HTTP 请求这些通用能力直接拿来用就行。而且 MCP 的协议设计比较清晰工具描述、参数 schema、返回值格式都有规范Agent 理解起来不容易出错。自己造轮子的话光是工具描述这块就得反复调不划算。Skills 这块要单独说一下。很多人把 Skills 和 MCP 搞混其实它们解决的是不同层面的问题。MCP 解决的是Agent 能调用什么工具Skills 解决的是Agent 在什么场景下该怎么用这些工具。举个例子MCP 提供了一个query_sqlite工具但 Agent 不知道什么时候该查、查完怎么解读结果这就是 Skill 要干的事。Skill 本质上是一段结构化的提示词加流程说明告诉 Agent 在特定任务下的操作步骤和注意事项。2.2 数据层为什么选 SQLite 而不是别的内网环境下数据库选型其实没太多选择。MySQL 和 PostgreSQL 当然更强大但部署和维护成本高对于一个 Agent 辅助工具来说有点重。SQLite 的优势很明显零配置、单文件、不需要独立进程拷贝一个.db文件就能迁移整个数据库。但 SQLite 也有它的脾气。最典型的就是并发写入问题。SQLite 默认是串行写入的同一时刻只能有一个写操作。如果 Agent 频繁写日志或者更新状态很容易遇到database is locked的错误。我的解决办法是开启 WAL 模式让读写可以并行同时把写操作尽量合并成批量提交。还有一个坑是字段类型修改。SQLite 不支持直接ALTER COLUMN改类型只能通过建新表、导数据、删旧表、改名这套流程来绕。这个后面会详细讲。2.3 Vue 前端在整个工程里的定位前端这块我一开始想的是能用就行毕竟核心是 Agent 逻辑。但实际用下来发现一个好的操作界面能极大提升 Agent 的可用性。比如工具调用的过程可视化、中间结果的展示、历史会话的管理这些如果没有界面纯靠命令行非技术同事根本用不起来。选 Vue 是因为团队里前端同学熟悉而且 Vue 的生态比较完整路由、状态管理、组件库都有成熟方案。内网环境下装依赖是个麻烦事需要提前把node_modules打包好或者搭一个内网的 npm 镜像。这个后面实操部分会讲。3. MCP 工具链在内网环境下的搭建细节3.1 MCP 到底是什么用大白话讲清楚MCP 全称是 Model Context Protocol翻译过来叫模型上下文协议。你可以把它理解成 Agent 和外部工具之间的统一插座标准。以前每个工具都要写一套自己的调用接口Agent 得分别适配有了 MCP 之后所有工具都按同一个协议暴露能力Agent 只需要会一种调用方式就行。一个 MCP Server 本质上就是一个进程它对外声明我有哪些工具、每个工具需要什么参数、返回什么格式。Agent 启动时会去连接这些 Server拉取工具列表然后在需要的时候调用。整个过程是标准化的所以社区里的 Server 可以互相复用。在内网环境下MCP Server 必须本地运行。我常用的几个是文件系统 Server读写本地文件、SQLite Server数据库操作、Shell Server执行命令。这些都有开源实现下载下来直接跑就行不需要联网。3.2 内网部署 MCP Server 的实操步骤假设你已经拿到了 MCP Server 的离线包部署流程大概是这样的。首先确认运行环境MCP Server 通常用 Node.js 或 Python 写的所以内网机器上得有对应的运行时。Node.js 的话建议 18 以上Python 建议 3.10 以上。以文件系统 Server 为例启动命令大概是这样node /path/to/mcp-server-filesystem/dist/index.js /allowed/directory这里的/allowed/directory是安全边界Server 只能访问这个目录下的文件。这个设计很重要内网环境下虽然相对安全但也不能让 Agent 随便读写整个文件系统。启动之后Agent 那边需要配置连接。配置文件通常是一个 JSON长这样{ mcpServers: { filesystem: { command: node, args: [/path/to/mcp-server-filesystem/dist/index.js, /data/agent-workspace] }, sqlite: { command: node, args: [/path/to/mcp-server-sqlite/dist/index.js, /data/agent.db] } } }配置好之后重启 Agent它就会自动连接这些 Server 并拉取工具列表。注意内网环境下路径一定要用绝对路径相对路径在不同工作目录下启动会出问题。我在这上面浪费过半天时间Agent 一直报工具不可用最后发现是路径没写对。3.3 自研 MCP Server 的时机和要点现成的 Server 覆盖不了所有需求。比如我们内部有个工单系统需要 Agent 能查询和更新工单状态这就得自己写一个 MCP Server。自研的时候有几个要点。第一工具描述要写清楚。Agent 是靠描述来理解工具用途的描述模糊它就会乱调。比如查询工单这种描述太笼统应该写成根据工单 ID 或状态查询工单列表返回工单编号、标题、当前状态、负责人。第二参数校验要做足。Agent 有时候会传一些奇怪的参数Server 端必须做好校验返回明确的错误信息这样 Agent 才能自我纠正。第三返回值要结构化。尽量返回 JSON 格式字段命名清晰避免返回一大段自然语言让 Agent 去解析。自研 Server 的代码结构其实不复杂核心就是注册工具、定义 schema、实现处理逻辑。用官方 SDK 的话一个简单的 Server 几十行代码就能搞定。4. Skills 设计与 Agent 行为控制4.1 Skills 和 MCP 的分工别再搞混了前面提过MCP 管能做什么Skills 管怎么做。举个具体例子你就明白了。假设 Agent 要完成生成本周工单统计报告这个任务。MCP 提供的能力是query_sqlite查数据库、write_file写文件、read_file读文件。但光有这些能力Agent 不知道该先查什么、怎么聚合、报告格式是什么。这时候 Skill 就派上用场了。一个 Skill 通常包含触发条件什么任务下启用、操作步骤先做什么后做什么、注意事项容易出错的地方、输出格式最终结果长什么样。它本质上是一段精心设计的提示词但比普通提示词更结构化、更可复用。4.2 一个实用 Skill 的完整结构我拿工单统计报告这个 Skill 举例讲讲它的结构。首先是元信息部分声明 Skill 名称、适用场景、依赖的工具。name: weekly-ticket-report description: 生成本周工单统计报告 triggers: - 生成本周工单报告 - 统计本周工单 tools: - query_sqlite - write_file然后是执行步骤部分用自然语言描述清楚每一步。## 执行步骤 1. 计算本周的起止日期周一 00:00 到周日 23:59 2. 查询工单表筛选创建时间在本周范围内的记录 3. 按状态分组统计数量待处理、处理中、已完成、已关闭 4. 按负责人分组统计处理量 5. 计算平均处理时长已完成工单的关闭时间减去创建时间 6. 将统计结果写入 /reports/weekly-{日期}.md最后是注意事项和输出模板。## 注意事项 - 日期计算要用本地时区不要用 UTC - 处理时长为空的工单未完成不计入平均值 - 如果本周没有工单输出本周无工单而不是空报告 ## 输出模板 # 本周工单统计{起止日期} ## 总体情况 - 新增工单{数量} - 已完成{数量} ...这样设计的好处是Agent 每次执行这个任务时行为一致不会今天这么干明天那么干。而且 Skill 可以版本化管理改进了流程就更新 Skill 文件不用动 Agent 核心代码。4.3 Skills 的加载和触发机制Skills 文件放在一个固定目录下Agent 启动时扫描加载。触发方式有两种一种是用户显式指定比如在对话里说用工单报告 Skill另一种是 Agent 根据用户意图自动匹配。自动匹配这块要小心匹配错了会闹笑话。我的做法是给每个 Skill 定义明确的触发关键词Agent 先做关键词匹配匹配到多个再让用户确认。不要一上来就靠语义相似度内网模型的能力参差不齐语义匹配经常翻车。实操心得Skill 的触发条件宁可写窄一点也不要写太宽。写宽了会导致 Agent 在不该用的时候乱用反而降低可靠性。我一开始把报告这个词作为触发词结果用户说帮我看看这个报告文件也会触发工单报告 Skill非常尴尬。5. SQLite 数据层的性能与运维实战5.1 十万条数据查询到底要多久热词里有个十万条数据 sqlite 查询需要多久这个问题我被问过很多次。实测下来在普通机械硬盘上十万条数据的全表扫描大概在 100-300 毫秒如果有合适的索引走索引查询能降到 10 毫秒以内。SSD 上会更快全表扫描大概 50-100 毫秒。但这个数字有个前提表结构合理、查询语句不烂。我见过最离谱的情况是有人把十万条数据存成一个 JSON 字符串塞进一个字段里每次查询都要全表读出来再解析那查询时间直接飙到几秒。所以关键不是数据量而是表设计和索引。对于 Agent 场景我建议把经常查询的字段都建上索引比如工单 ID、状态、创建时间。但索引也不是越多越好每个索引都会增加写入开销。一般控制在 3-5 个索引比较合适。5.2 WAL 模式解决并发写入锁前面提到的database is locked问题根源是 SQLite 默认的日志模式DELETE 模式在写入时会锁住整个数据库。开启 WALWrite-Ahead Logging模式后读和写可以并行写入性能也更好。开启方式很简单执行一次就行PRAGMA journal_modeWAL;这个设置是持久化的执行一次之后数据库文件会记住。但要注意WAL 模式会额外生成-wal和-shm两个文件备份的时候要一起备份不然数据会丢。还有一个参数是busy_timeout设置成 5000 毫秒意思是遇到锁的时候等 5 秒再报错而不是立刻失败。这个对 Agent 这种可能并发调用的场景很有用。PRAGMA busy_timeout5000;5.3 修改字段类型的正确姿势SQLite 不支持直接改字段类型这是它的设计哲学决定的动态类型。但实际开发中总会遇到要改类型的情况比如一开始把时间存成了字符串后来想改成整数时间戳。标准流程是四步建新表、导数据、删旧表、改名。假设有个tickets表要把created_at从 TEXT 改成 INTEGER。-- 1. 建新表 CREATE TABLE tickets_new ( id INTEGER PRIMARY KEY, title TEXT, created_at INTEGER, status TEXT ); -- 2. 导数据注意类型转换 INSERT INTO tickets_new (id, title, created_at, status) SELECT id, title, strftime(%s, created_at), status FROM tickets; -- 3. 删旧表 DROP TABLE tickets; -- 4. 改名 ALTER TABLE tickets_new RENAME TO tickets;整个过程要放在一个事务里避免中途出错导致数据不一致。另外操作前一定要备份.db文件这个不用我多说。注意如果表上有索引或触发器重建后要重新创建。我踩过一次坑改完字段类型发现查询变慢了查了半天才想起来索引没重建。5.4 用 DB Browser for SQLite 做日常维护内网环境下没有花哨的数据库管理工具DB Browser for SQLite 是个不错的选择。它是图形化界面支持浏览数据、执行 SQL、导入导出。离线安装包大概几十兆拷进内网直接装。我常用它做几件事查看表结构和索引、手动执行一些维护 SQL、导出数据做备份。特别是排查问题时能直观看到数据长什么样比在代码里打日志方便多了。6. Vue 前端集成与内网部署6.1 内网装 Vue 依赖的离线方案内网装 npm 依赖是个老大难。我的方案是在外网机器上把node_modules完整打包连同package.json和package-lock.json一起拷进内网。内网机器上直接解压到项目目录不要执行npm install因为内网连不上 registry。但这样有个问题如果后续要加新依赖又得重新打包。所以更好的方案是在内网搭一个 npm 私有镜像用 verdaccio 之类的工具。不过这个搭建成本高一些看团队规模决定。Vue 项目本身的环境配置vue.config.js里要注意把publicPath设成相对路径不然部署到子目录下会 404。module.exports { publicPath: ./, outputDir: dist, productionSourceMap: false }6.2 Agent 交互界面的核心组件设计前端界面我主要做了三个核心组件。第一个是对话区展示用户和 Agent 的交互历史支持流式输出。流式输出这块要注意内网模型的响应速度可能不稳定要做好缓冲和重连。第二个是工具调用可视化。Agent 调用 MCP 工具时界面上要能看到正在调用 query_sqlite、返回 23 条记录这样的信息。这个对调试和建立信任很重要用户能看到 Agent 在干什么而不是干等。第三个是会话管理。支持新建会话、切换历史会话、删除会话。会话数据存在 SQLite 里前端通过 API 拉取。Vue 的路由配置大概是这样const routes [ { path: /, component: ChatView }, { path: /sessions, component: SessionListView }, { path: /tools, component: ToolMonitorView } ]6.3 流式输出的前端处理Agent 的回复是流式的前端要能逐字显示。用 fetch 的 ReadableStream 处理比较合适。const response await fetch(/api/chat, { method: POST, body: JSON.stringify({ message }) }) const reader response.body.getReader() const decoder new TextDecoder() while (true) { const { done, value } await reader.read() if (done) break const chunk decoder.decode(value) // 追加到消息列表 appendToMessage(chunk) }这里有个细节内网模型有时候会输出不完整的中文一个 UTF-8 字符被拆成两个 chunk所以要用TextDecoder的流式模式不要每次单独解码。7. 常见问题与排查技巧实录7.1 Agent 调用工具失败怎么排查工具调用失败是最常见的问题排查思路按这个顺序走。先看 MCP Server 进程还在不在有时候 Server 崩了 Agent 不知道还在傻等。然后看 Agent 的日志确认它到底调用了哪个工具、传了什么参数。最后看 Server 端的日志确认参数有没有收到、处理逻辑有没有报错。我整理了一个速查表现象可能原因排查方法工具列表为空Server 未启动或配置路径错检查进程和配置文件调用超时Server 处理慢或死锁看 Server 日志检查数据库锁参数错误Agent 理解偏差看 Agent 日志里的实际参数返回格式异常Server 实现问题手动调用工具验证7.2 模型输出不稳定的应对内网开源模型的能力参差不齐输出不稳定是常态。我的应对策略有三条。第一降低单次任务复杂度把大任务拆成小步骤每步都让 Agent 确认。第二用 Skill 约束行为把流程写死减少模型的自由发挥空间。第三加校验和重试对关键输出做格式校验不合格就重试。还有个小技巧在提示词里明确要求如果信息不足先提问再执行这样能避免 Agent 瞎猜。7.3 内网环境特有的坑内网环境下有些坑是外网遇不到的。比如时间同步内网机器可能没有 NTP 服务时间会漂移导致 Agent 生成的报告日期不对。解决办法是定期手动校时或者在代码里用相对时间而不是绝对时间。还有磁盘空间内网机器通常配置不高Agent 跑久了日志和数据库会占满磁盘。要加日志轮转和数据库清理策略。最后一个坑是依赖版本内网装不了新包所以一开始就要把版本锁死package-lock.json和requirements.txt都要提交到版本控制避免不同机器上跑出不同结果。8. 一些个人体会这套东西跑通之后最大的感受是隔离内网做 AI Agent难点不在 AI而在工程。模型能力固然重要但真正决定能不能落地的是那些琐碎的工程细节——路径配置、并发控制、依赖管理、错误处理。这些在外网环境下可能被各种云服务掩盖了但在内网里全都暴露出来必须一个个解决。另外Skills 的设计比我想象中重要得多。一开始我觉得这就是提示词随便写写就行。后来发现好的 Skill 能让一个能力一般的模型表现得像个专家而烂的 Skill 能让一个强模型表现得像个傻子。这块值得花时间打磨。最后分享一个小技巧在内网部署之前先在外网环境把整个流程跑通然后把所有依赖、配置、数据都打包再整体迁移到内网。这样能避免在内网里反复调试效率高很多。我一开始是直接在内网搭结果光是装依赖就折腾了一周后来改成外网打包再迁移半天就搞定了。