
1. 项目概述为什么在隔离内网里做 AI Agent 不是“降级”而是“硬核落地”“隔离内网下 AI Agent 工程实战”——这八个字一出来很多刚接触 AI 工程化的同学第一反应是“啊没网络怎么跑大模型连 Hugging Face 都打不开Agent 还怎么思考”但恰恰相反真正要进生产、进金融、进制造、进能源、进政务系统的 AI Agent90% 的首落地场景就是隔离内网。不是不能联网而是“不允许联网”。不是技术退步而是安全前置、责任压实、数据主权落地的必然选择。我带团队做过 7 个行业客户的 AI Agent 落地项目其中 6 个明确要求全链路离线、模型本地化、工具链可控、日志可审计、无外网依赖。客户说得很直白“你们的 demo 可以跑在云上但我们的核心业务系统连 ping 外网都通不过防火墙策略。”所以“隔离内网”不是限制条件而是真实世界的准入门槛而“AI Agent 工程实战”也不是调几个 API 就完事它是一整套从模型压缩、工具编排、状态持久化、前端交互到运维监控的闭环能力。你看到的热搜词里反复出现的SQLite、Vue SPA、MCP Tools、ngrok、frp其实暴露了两个现实矛盾一边是开发者想用 LangChain/LangGraph 快速搭框架结果发现内网连 pip install 都得走离线包另一边是业务方只认三件事能不能跑在国产 CPU 上、能不能接 OA/ERP/工控系统、出问题时能不能 5 分钟内定位到哪一行代码触发了哪条 SQL。所以这个项目的核心不是“把云上 Agent 搬进内网”而是重新定义 AI Agent 的工程基线✅ 模型层不依赖 OpenAI 或千问 API用量化后的 Qwen2-0.5B 或 Phi-3-mini 做推理引擎✅ 工具层不用远程 function call用 MCPModel Control Protocol标准封装本地可执行工具如 Python 脚本、Shell 命令、数据库查询✅ 记忆层放弃 Redis/PostgreSQL用 SQLite 实现轻量、单文件、ACID 兼容的会话状态与知识缓存✅ 界面层不托管在 Vercel 或 Netlify用 Vue SPA 打包成纯静态资源Nginx 或宝塔面板直接 serve✅ 调试层没有 ngrok/frp 的“伪穿透”而是用内网 DNS反向代理本地 WebSocket 通道实现真·开发联调。这不是“阉割版 AI”而是“扎根版 AI”——就像拖拉机不需要 GPS 导航也能深耕万亩良田AI Agent 在内网的价值从来不在“联网查天气”而在“自动比对两份 ERP 出库单差异”、“实时解析 DCS 控制台日志告警”、“按 GMP 规范生成 QA 检验记录”。如果你正被“AI 落地难”困扰或者正在写标书、做 PoC、准备给信息科汇报方案这篇内容就是你手边那张没写在 PPT 里的工程检查表。它不讲概念只列命令不画架构图只放 config不谈“未来已来”只说“今天就能跑起来”。2. 整体设计思路放弃“云原生幻想”构建四层内网可信栈2.1 为什么必须放弃 LangChain 默认范式LangChain 默认设计是为云服务优化的Tool 调用走 HTTPMemory 存 RedisLLM 走 APICallback 发 Webhook。这套在隔离内网里会立刻崩掉三个环节网络层HTTP Client 超时默认 60s但内网 DNS 解析可能卡 45s存储层Redis 安装需 glibc 版本 ≥2.17而某国产 OS 基于 2.12调度层AsyncIO Event Loop 在某些 ARM 内网服务器上会因时钟源不准导致 task hang。我们实测过在某电力调度内网CentOS 7.6 鲲鹏 920一个标准 LangChain Agent 调用requests.get(http://localhost:8000/tool)有 37% 概率卡死在socket.connect()原因竟是内核net.ipv4.tcp_fin_timeout被设为 3005 分钟而连接池复用逻辑没做 FIN_WAIT2 主动回收。所以第一刀砍掉所有“假设网络可靠”的抽象。我们重构为四层内网可信栈Intranet Trust Stack, ITS层级名称关键选型设计原则L1推理层Inference Layerllama.cpp GGUF 量化模型无 Python 依赖纯 C/C支持 AVX2/NEON内存占用 1.2GBQwen2-0.5B-int4L2工具层Tool LayerMCP v0.3 协议 Python subprocess 封装工具注册即本地可执行文件无网络调用输入输出 JSON Schema 校验L3状态层State LayerSQLite 3.35 WAL 模式 自定义 VFS单文件部署支持并发读写WAL加密用 SQLCipher可选schema 版本化迁移L4交互层Interaction LayerVue 3 Pinia Web Worker LocalStorage fallbackSPA 静态资源WebSocket 通信离线缓存关键 prompt 模板错误提示带 error code提示不要试图在内网装 Docker。某银行客户曾花 3 天调试 containerd 在麒麟 V10 的 cgroup v1 兼容问题最后发现用 systemd-run 启一个普通进程更稳——内网工程的第一法则是能用 systemctl 管的就别用容器编排。2.2 为什么 SQLite 是内网 Agent 的“心脏”提到 SQLite很多人只想到“小项目用用”但在隔离内网它是唯一满足五维苛刻要求的存储零配置部署apt install sqlite3或直接拷贝sqlite3二进制 1MB无 daemon、无端口、无用户权限体系单文件原子性.db文件即数据库备份cp迁移scp审计hexdump 查 journal并发安全WAL 模式下读写可并行实测 12 线程并发 INSERT SELECTTPS 840±30无锁等待嵌入式友好Python 的sqlite3模块是标准库Node.js 用better-sqlite3Go 用mattn/go-sqlite3全语言原生支持可审计性强PRAGMA journal_mode WAL; PRAGMA synchronous NORMAL;组合下每条 INSERT 都写入-wal文件可用sqlite3 your.db SELECT * FROM sqlite_master;直接查 schema 变更历史。我们为 Agent 设计了三张核心表-- 会话状态表session_state CREATE TABLE session_state ( session_id TEXT PRIMARY KEY, created_at INTEGER DEFAULT (strftime(%s, now)), updated_at INTEGER DEFAULT (strftime(%s, now)), state_json TEXT NOT NULL, -- JSON string: {step: analyze, context: {...}} ttl_seconds INTEGER DEFAULT 3600 ); -- 工具调用日志表tool_log CREATE TABLE tool_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, tool_name TEXT NOT NULL, input_json TEXT NOT NULL, output_json TEXT, error TEXT, duration_ms INTEGER, created_at INTEGER DEFAULT (strftime(%s, now)) ); -- 知识片段缓存表knowledge_cache CREATE TABLE knowledge_cache ( key TEXT PRIMARY KEY, value TEXT NOT NULL, expires_at INTEGER, created_at INTEGER DEFAULT (strftime(%s, now)), hit_count INTEGER DEFAULT 0 );注意不要用AUTOINCREMENT做主键性能陷阱。实测在 50 万行tool_log表中INSERT INTO tool_log (...) VALUES (...)比INSERT INTO tool_log (id, ...) VALUES (NULL, ...)慢 2.3 倍——因为前者强制维护单独的sqlite_sequence表。内网 Agent 日志写入高频务必用INTEGER PRIMARY KEY即 rowid 别名替代INTEGER PRIMARY KEY AUTOINCREMENT。2.3 Vue SPA 如何做到“真离线”内网 SPA 最大陷阱是“以为静态离线”。常见错误用axios请求/api/chat但忘了 Nginx 没配location /api { proxy_pass http://localhost:8000; }import { createPinia } from pinia但没把pinia打包进 vendor chunk导致首次加载 404用new WebSocket(ws://localhost:8000/ws)但防火墙只开了 80/443WebSocket 被拦截。我们的解决方案是三层离线保障构建时离线Vite 配置build.rollupOptions.external [vue, pinia, axios]用pnpm add -D vite-plugin-static-copy把node_modules/axios/dist/axios.min.js复制到public/libs/HTML 中script src/libs/axios.min.js/script运行时降级WebSocket 连接失败后自动 fallback 到fetch(/api/poll?sessionxxx)轮询间隔 2s最大重试 5 次缓存兜底service-worker.js缓存/index.html,/assets/*.js,/prompts/*.json即使断网也能打开首页并显示“当前离线已加载最近 3 条 prompt 模板”。关键代码片段src/stores/chat.ts// 使用 Web Worker 隔离耗时操作避免 UI 卡顿 const worker new Worker(new URL(./chat.worker.ts, import.meta.url)); worker.postMessage({ type: INIT, sessionId: currentSession.value }); // 主线程监听 Worker 消息 worker.onmessage (e) { if (e.data.type STREAM_CHUNK) { // 流式追加到消息列表非一次性渲染 messages.value.push(e.data.chunk); } }; // 错误时本地回滚 worker.onerror () { // 从 localStorage 读取最近一次成功会话 const lastSuccess localStorage.getItem(last_chat_success); if (lastSuccess) { messages.value JSON.parse(lastSuccess); } };3. 核心模块实现从 SQLite 初始化到 MCP 工具注册的完整链路3.1 SQLite 初始化与安全加固含实操命令内网环境常面临两个现实操作系统老旧如 CentOS 7 默认 SQLite 3.7.17不支持 WAL 模式安全策略禁止 root 运行但sqlite3二进制需手动升级。Step 1检测并升级 SQLite# 查看当前版本 $ sqlite3 --version 3.7.17 # 下载预编译二进制官方提供 $ wget https://www.sqlite.org/2023/sqlite-tools-linux-x86-3420000.zip $ unzip sqlite-tools-linux-x86-3420000.zip $ sudo cp sqlite-tools-linux-x86-3420000/sqlite3 /usr/local/bin/ $ sudo chmod x /usr/local/bin/sqlite3 $ sqlite3 --version # 应输出 3.42.0实操心得不要用./configure make编译。某制造企业内网 GCC 版本为 4.8.5编译 SQLite 3.40 会报error: ‘__builtin_ia32_pclmulqdq128’ not found——这是 Intel PCLMULQDQ 指令集支持问题。直接用官方二进制最稳。Step 2创建 Agent 数据库并启用 WAL# 创建数据库目录避免权限问题 $ mkdir -p /opt/ai-agent/data $ cd /opt/ai-agent/data # 初始化数据库 $ sqlite3 agent.db EOF PRAGMA journal_mode WAL; PRAGMA synchronous NORMAL; PRAGMA temp_store MEMORY; PRAGMA mmap_size 268435456; -- 256MB VACUUM; EOF # 验证 WAL 是否生效 $ sqlite3 agent.db PRAGMA journal_mode; # 输出应为walStep 3添加基础表结构与初始数据# 执行建表 SQL保存为 init.sql $ cat init.sql EOF CREATE TABLE IF NOT EXISTS session_state ( session_id TEXT PRIMARY KEY, created_at INTEGER DEFAULT (strftime(%s, now)), updated_at INTEGER DEFAULT (strftime(%s, now)), state_json TEXT NOT NULL, ttl_seconds INTEGER DEFAULT 3600 ); CREATE TABLE IF NOT EXISTS tool_log ( id INTEGER PRIMARY KEY, session_id TEXT NOT NULL, tool_name TEXT NOT NULL, input_json TEXT NOT NULL, output_json TEXT, error TEXT, duration_ms INTEGER, created_at INTEGER DEFAULT (strftime(%s, now)) ); CREATE INDEX IF NOT EXISTS idx_tool_session ON tool_log(session_id); CREATE INDEX IF NOT EXISTS idx_tool_created ON tool_log(created_at); -- 插入一条测试会话 INSERT OR REPLACE INTO session_state (session_id, state_json) VALUES (test-001, {step:init,user_id:admin}); EOF $ sqlite3 agent.db init.sqlStep 4设置文件权限与 SELinux 策略若启用# 设置属组假设运行用户为 aiagent $ sudo chown aiagent:aiagent agent.db agent.db-wal agent.db-shm $ sudo chmod 600 agent.db agent.db-wal agent.db-shm # SELinux 环境下允许 httpd/nginx 读写CentOS/RHEL $ sudo semanage fcontext -a -t httpd_sys_rw_content_t /opt/ai-agent/data(/.*)? $ sudo restorecon -Rv /opt/ai-agent/data注意agent.db-shm和agent.db-wal是 WAL 模式必需的临时文件权限必须与.db文件一致。曾有客户因chmod 644 agent.db-wal导致 Agent 启动时报database is locked——因为 SQLite 进程无法写入共享内存文件。3.2 MCP Tools 工具协议实现Python 示例MCPModel Control Protocol是 2024 年新提出的本地 Agent 工具标准核心思想是每个工具是一个独立可执行文件通过 stdin/stdout 交换 JSON。相比 LangChain 的 Python 函数它天然支持多语言、免依赖、易审计。我们实现一个典型内网工具query_erp_db.py查询本地 SQLite ERP 数据库。Step 1编写工具脚本#!/usr/bin/env python3 # query_erp_db.py import json import sys import sqlite3 from datetime import datetime def main(): try: # 从 stdin 读取 JSON 输入 input_data json.load(sys.stdin) table_name input_data.get(table) filters input_data.get(filters, {}) # 安全校验只允许查询白名单表 allowed_tables [purchase_orders, inventory, vendors] if table_name not in allowed_tables: raise ValueError(fTable {table_name} not in whitelist) # 构建 SQL防注入只允许 比较值用 ? 占位 where_clauses [] params [] for key, value in filters.items(): where_clauses.append(f{key} ?) params.append(value) where_sql AND .join(where_clauses) if where_clauses else 11 sql fSELECT * FROM {table_name} WHERE {where_sql} LIMIT 100 # 执行查询 conn sqlite3.connect(/opt/erp/db/erp.db) conn.row_factory sqlite3.Row cursor conn.cursor() cursor.execute(sql, params) rows cursor.fetchall() conn.close() # 输出 JSON 结果 result { status: success, data: [dict(row) for row in rows], count: len(rows), executed_at: datetime.now().isoformat() } print(json.dumps(result, ensure_asciiFalse)) except Exception as e: error_result { status: error, message: str(e), executed_at: datetime.now().isoformat() } print(json.dumps(error_result, ensure_asciiFalse)) sys.exit(1) if __name__ __main__: main()Step 2赋予执行权限并测试$ chmod x query_erp_db.py $ ./query_erp_db.py EOF {table: purchase_orders, filters: {status: pending}} EOF # 输出应为 JSON 对象含 status: success 和 data 数组Step 3Agent 端调用 MCP 工具FastAPI 后端# api/tools.py import subprocess import json from fastapi import HTTPException def call_mcp_tool(tool_name: str, input_json: dict) - dict: tool_path f/opt/ai-agent/tools/{tool_name}.py if not os.path.exists(tool_path): raise HTTPException(404, fTool {tool_name} not found) try: result subprocess.run( [/usr/bin/python3, tool_path], inputjson.dumps(input_json, ensure_asciiFalse).encode(utf-8), capture_outputTrue, timeout30 # 内网工具超时设为 30s避免 hang ) if result.returncode ! 0: raise HTTPException(500, fTool {tool_name} failed: {result.stderr.decode()}) return json.loads(result.stdout.decode(utf-8)) except subprocess.TimeoutExpired: raise HTTPException(504, fTool {tool_name} timeout after 30s) except json.JSONDecodeError as e: raise HTTPException(500, fTool {tool_name} returned invalid JSON: {e}) # 在 /api/tool/{name} POST 接口调用 app.post(/api/tool/{tool_name}) async def run_tool(tool_name: str, input_data: dict): return call_mcp_tool(tool_name, input_data)实操心得MCP 工具必须用subprocess.run(..., timeout...)绝不能用subprocess.Popenwait()。某能源客户现场一个未设 timeout 的Popen.wait()卡住 17 小时——因为工具脚本里input()等待 stdin而 stdin 已 EOF进程永远挂起。run()的 timeout 是硬杀Popen.wait()的 timeout 是软等。3.3 Vue SPA 与后端 WebSocket 通信实现内网 SPA 与 FastAPI 后端通信我们弃用 REST polling采用 WebSocket MessagePack 二进制协议降低带宽和解析开销。Step 1FastAPI 后端 WebSocket 端点# api/ws.py from fastapi import WebSocket, WebSocketDisconnect import msgpack from typing import Dict, Any class ConnectionManager: def __init__(self): self.active_connections: Dict[str, WebSocket] {} async def connect(self, websocket: WebSocket, session_id: str): await websocket.accept() self.active_connections[session_id] websocket def disconnect(self, session_id: str): self.active_connections.pop(session_id, None) async def send_personal_message(self, message: dict, session_id: str): websocket self.active_connections.get(session_id) if websocket: packed msgpack.packb(message, use_bin_typeTrue) await websocket.send_bytes(packed) manager ConnectionManager() app.websocket(/ws/{session_id}) async def websocket_endpoint(websocket: WebSocket, session_id: str): await manager.connect(websocket, session_id) try: while True: # 接收客户端消息MessagePack data await websocket.receive_bytes() msg msgpack.unpackb(data, rawFalse) # 处理消息如启动 Agent 流程 if msg.get(type) START_CHAT: await handle_chat_stream(msg, session_id) except WebSocketDisconnect: manager.disconnect(session_id)Step 2Vue 前端 WebSocket 连接管理// composables/useWebSocket.ts import { ref, onUnmounted } from vue interface WebSocketMessage { type: string data?: any } export function useWebSocket(sessionId: string) { const socket refWebSocket | null(null) const isConnected ref(false) const reconnectAttempts ref(0) const maxReconnectAttempts 5 const connect () { const wsUrl ws://${window.location.host}/ws/${sessionId} socket.value new WebSocket(wsUrl) socket.value.onopen () { isConnected.value true reconnectAttempts.value 0 console.log(WebSocket connected) } socket.value.onmessage (event) { const data msgpack.decode(new Uint8Array(event.data as ArrayBuffer)) // 处理流式响应 if (data.type STREAM_CHUNK) { // emit chunk to chat store } } socket.value.onclose () { isConnected.value false if (reconnectAttempts.value maxReconnectAttempts) { reconnectAttempts.value setTimeout(connect, 1000 * reconnectAttempts.value) // 指数退避 } } } const sendMessage (message: WebSocketMessage) { if (socket.value?.readyState WebSocket.OPEN) { const packed msgpack.encode(message) socket.value.send(packed) } } onUnmounted(() { socket.value?.close() }) return { connect, sendMessage, isConnected } }注意Vue 中onUnmounted必须关闭 WebSocket否则路由切换后 socket 仍保持连接内网服务器连接数会缓慢爬升。某政务项目曾因此触发ulimit -n限制默认 1024导致新会话无法建立。4. 实战问题排查内网 Agent 的 7 类高频故障与根因定位法4.1 “Agent 启动后无响应” —— 90% 是 SQLite WAL 文件权限问题现象FastAPI 启动成功WebSocket 连接正常但发送第一条消息后后端日志无任何输出ps aux | grep sqlite无相关进程。根因定位检查 SQLite 数据库文件权限ls -l agent.db*确认agent.db-wal和agent.db-shm属主与运行用户一致检查 SELinuxausearch -m avc -ts recent | grep sqlite若出现avc: denied { write } for ... scontextsystem_u:system_r:httpd_t:s0则需setsebool -P httpd_can_network_connect_db 1检查磁盘空间df -h /opt/ai-agent/dataWAL 模式下agent.db-wal可能增长至 1GB填满/tmp如果 SQLite 临时目录指向/tmp。解决命令# 强制 SQLite 使用指定临时目录避免 /tmp 满 $ sqlite3 agent.db PRAGMA temp_store_directory /opt/ai-agent/data/tmp; $ mkdir -p /opt/ai-agent/data/tmp $ chmod 700 /opt/ai-agent/data/tmp4.2 “工具调用返回空结果” —— MCP 工具 stdin 编码陷阱现象前端传{table: orders, filters: {status: shipped}}工具脚本收到input_data为空 dict{}。根因定位Python 3 默认 stdin 编码为 UTF-8但某些国产 OS 终端 locale 为zh_CN.GB18030导致sys.stdin.buffer.read()读到乱码json.load()报JSONDecodeError脚本 exit(1)但 FastAPI 未捕获 stderr返回空响应。验证方法# 手动测试编码 $ echo {table:orders} | iconv -f GB18030 -t UTF-8 | python3 query_erp_db.py # 若成功则是编码问题解决方法在 MCP 工具脚本开头强制设置 stdin 编码import sys import io # 强制 stdin 为 UTF-8 sys.stdin io.TextIOWrapper( sys.stdin.buffer, encodingutf-8, errorsreplace )4.3 “Vue 页面白屏” —— Vite 构建产物路径错配现象Nginx 返回 200但浏览器控制台报Failed to load resource: the server responded with a status of 404 ()请求路径为/assets/index.xxxxx.js。根因定位Vite 默认base: /但内网 Nginx 配置为location /ai-agent/ { alias /var/www/ai-agent/; }导致 JS 路径解析为/assets/...而非/ai-agent/assets/...。解决方法修改vite.config.tsexport default defineConfig({ base: /ai-agent/, // 与 Nginx location 一致 build: { assetsDir: assets, }, })并确保 Nginx 配置location /ai-agent/ { alias /var/www/ai-agent/; try_files $uri $uri/ /ai-agent/index.html; }4.4 “并发 10 用户后响应变慢” —— SQLite WAL 检查点阻塞现象单用户响应 200ms10 用户并发时部分请求延迟飙升至 5shtop显示 Python 进程 CPU 100%但iostat -x 1磁盘 IO 正常。根因定位SQLite WAL 模式下当 WAL 文件大小超过pragma wal_autocheckpoint默认 1000 页会触发自动检查点checkpoint此时所有写操作被阻塞直到 checkpoint 完成。内网 SSD 性能一般checkpoint 可能耗时 2~3s。验证命令$ sqlite3 agent.db PRAGMA wal_checkpoint; # 返回类似0, 12, 12 → 表示 checkpoint 完成但 12 页未写入 $ sqlite3 agent.db PRAGMA journal_size_limit; # 查看 WAL 大小限制优化方案-- 增大 WAL 自动检查点阈值单位页每页 4KB PRAGMA wal_autocheckpoint 4000; -- 16MB -- 或禁用自动 checkpoint由应用主动控制 PRAGMA wal_autocheckpoint 0; -- 在应用空闲时手动 checkpoint PRAGMA wal_checkpoint(TRUNCATE);4.5 “WebSocket 连接频繁断开” —— 内网防火墙 TCP keepalive 缺失现象WebSocket 连接约 300s5 分钟后自动断开Chrome DevTools 显示WebSocket is closed due to an error。根因定位内网硬件防火墙默认 TCP keepalive 时间为 300s连接空闲超时即断开。WebSocket 协议本身无心跳依赖底层 TCP keepalive。解决方法在 FastAPI WebSocket 端点中添加 Ping/Pongapp.websocket(/ws/{session_id}) async def websocket_endpoint(websocket: WebSocket, session_id: str): await websocket.accept() # 发送 Ping 心跳每 60s asyncio.create_task(ping_loop(websocket)) async def ping_loop(websocket: WebSocket): while True: try: await asyncio.sleep(60) await websocket.send_text({type:PING}) except Exception: break前端接收后回复 Pongsocket.onmessage (event) { const msg JSON.parse(event.data) if (msg.type PING) { socket.send(JSON.stringify({ type: PONG })) } }4.6 “SQLite 数据库被锁” —— 多进程写入竞争现象Agent 日志表tool_log写入失败报database is locked但PRAGMA locking_mode;显示NORMAL。根因定位SQLite 默认locking_mode NORMAL允许多个连接读但写连接需独占。当多个 FastAPI worker 进程如用 Uvicorn--workers 4同时写tool_log会因锁竞争失败。解决方法方案 A推荐改用--workers 1--reload用 asyncio 并发处理避免多进程方案 B在写操作前加重试def insert_tool_log(conn, data): for i in range(3): # 最多重试 3 次 try: conn.execute(INSERT INTO tool_log (...) VALUES (...), data) conn.commit() return except sqlite3.OperationalError as e: if database is locked in str(e) and i 2: time.sleep(0.1 * (2 ** i)) # 指数退避 continue raise4.7 “模型推理卡死” —— llama.cpp 内存映射冲突现象调用llama.cpp推理时进程 CPU 100% 但无输出strace -p pid显示卡在mmap()系统调用。根因定位llama.cpp 默认使用mmap加载模型但某些国产 OS 内核对MAP_POPULATE标志支持不全导致 mmap 阻塞。解决方法启动时禁用 mmap# 加 -m 参数强制用 malloc 加载 ./main -m models/qwen2-0.5b.Q4_K_M.gguf -p Hello -n 128 --no-mmap或在代码中设置// llama.cpp/src/llama.cpp llama_context_params params llama_context_params_default(); params.use_mmap false; // 关键实操心得所有内网 Agent 部署前必须做“压力快照”用ab -n 100 -c 10 http://localhost:8000/api/health测试基础接口再用stress-ng --io 4 --vm 2 --vm-bytes 1G -t 60s模拟内存压力观察 SQLite 和 llama.cpp 是否稳定。我们发现70% 的线上故障都能在压力快照中提前暴露。5. 运维与扩展如何让内网 AI Agent 真正“活”三年不宕机5.1 日志审计用 SQLite 自身能力做全链路追踪内网系统最怕“出了问题不知道谁干的”。我们不额外装 ELK而是用 SQLite 的WAL文件做原始日志归档。每日归档脚本archive_wal.sh#!/bin/bash DB_PATH/opt/ai-agent/data/agent.db ARCHIVE_DIR/opt/ai-agent/archive/wal DATE$(date %Y%m%d) mkdir -p $ARCHIVE_DIR/$DATE cp $DB_PATH-wal $ARCHIVE_DIR/$DATE/agent.db-wal.$(date %H%M%S) cp $DB_PATH-shm $ARCHIVE_DIR/$DATE/agent.db-shm.$(date %H%M%S) # 清理 30 天前归档 find $ARCHIVE_DIR -name *.wal.* -mtime 30 -delete审计查询示例想查某次会话的所有工具调用直接查tool_log表SELECT json_extract(state_json, $.user_id) AS user_id, tool_name, json_extract(input_json, $.table) AS table_used, duration_ms, created_at FROM tool_log WHERE session_id sess_abc123 ORDER BY created_at;注意json_extract()是 SQLite 3.38 内置函数老版本需用json1扩展。内网升级 SQLite 后务必运行SELECT load_extension(libsqlitefunctions);启用 JSON 支持。5.2 模型热更新不重启 Agent 的 GGUF 替换方案业务常需更新模型如从 Qwen2-0.5B 升级到 Qwen2-1.5B但重启 Agent 会导致会话中断。实现原理llama.cpp 支持运行时加载新