網(wǎng)AI Agent工程實戰(zhàn):SQLite+MCP+Vue輕量可信棧)
1. 項目概述為什么在隔離內(nèi)網(wǎng)里做 AI Agent 不是“降級”而是“硬核落地”“隔離內(nèi)網(wǎng)下 AI Agent 工程實戰(zhàn)”——這八個字一出來很多剛接觸 AI 工程化的同學第一反應是“啊沒網(wǎng)絡怎么跑大模型連 Hugging Face 都打不開Agent 還怎么思考”但恰恰相反真正要進生產(chǎn)、進金融、進制造、進能源、進政務系統(tǒng)的 AI Agent90% 的首落地場景就是隔離內(nèi)網(wǎng)。不是不能聯(lián)網(wǎng)而是“不允許聯(lián)網(wǎng)”。不是技術退步而是安全前置、責任壓實、數(shù)據(jù)主權落地的必然選擇。我?guī)F隊做過 7 個行業(yè)客戶的 AI Agent 落地項目其中 6 個明確要求全鏈路離線、模型本地化、工具鏈可控、日志可審計、無外網(wǎng)依賴??蛻粽f得很直白“你們的 demo 可以跑在云上但我們的核心業(yè)務系統(tǒng)連 ping 外網(wǎng)都通不過防火墻策略。”所以“隔離內(nèi)網(wǎng)”不是限制條件而是真實世界的準入門檻而“AI Agent 工程實戰(zhàn)”也不是調(diào)幾個 API 就完事它是一整套從模型壓縮、工具編排、狀態(tài)持久化、前端交互到運維監(jiān)控的閉環(huán)能力。你看到的熱搜詞里反復出現(xiàn)的SQLite、Vue SPA、MCP Tools、ngrok、frp其實暴露了兩個現(xiàn)實矛盾一邊是開發(fā)者想用 LangChain/LangGraph 快速搭框架結果發(fā)現(xiàn)內(nèi)網(wǎng)連 pip install 都得走離線包另一邊是業(yè)務方只認三件事能不能跑在國產(chǎn) CPU 上、能不能接 OA/ERP/工控系統(tǒng)、出問題時能不能 5 分鐘內(nèi)定位到哪一行代碼觸發(fā)了哪條 SQL。所以這個項目的核心不是“把云上 Agent 搬進內(nèi)網(wǎng)”而是重新定義 AI Agent 的工程基線? 模型層不依賴 OpenAI 或千問 API用量化后的 Qwen2-0.5B 或 Phi-3-mini 做推理引擎? 工具層不用遠程 function call用 MCPModel Control Protocol標準封裝本地可執(zhí)行工具如 Python 腳本、Shell 命令、數(shù)據(jù)庫查詢? 記憶層放棄 Redis/PostgreSQL用 SQLite 實現(xiàn)輕量、單文件、ACID 兼容的會話狀態(tài)與知識緩存? 界面層不托管在 Vercel 或 Netlify用 Vue SPA 打包成純靜態(tài)資源Nginx 或?qū)毸姘逯苯?serve? 調(diào)試層沒有 ngrok/frp 的“偽穿透”而是用內(nèi)網(wǎng) DNS反向代理本地 WebSocket 通道實現(xiàn)真·開發(fā)聯(lián)調(diào)。這不是“閹割版 AI”而是“扎根版 AI”——就像拖拉機不需要 GPS 導航也能深耕萬畝良田AI Agent 在內(nèi)網(wǎng)的價值從來不在“聯(lián)網(wǎng)查天氣”而在“自動比對兩份 ERP 出庫單差異”、“實時解析 DCS 控制臺日志告警”、“按 GMP 規(guī)范生成 QA 檢驗記錄”。如果你正被“AI 落地難”困擾或者正在寫標書、做 PoC、準備給信息科匯報方案這篇內(nèi)容就是你手邊那張沒寫在 PPT 里的工程檢查表。它不講概念只列命令不畫架構圖只放 config不談“未來已來”只說“今天就能跑起來”。2. 整體設計思路放棄“云原生幻想”構建四層內(nèi)網(wǎng)可信棧2.1 為什么必須放棄 LangChain 默認范式LangChain 默認設計是為云服務優(yōu)化的Tool 調(diào)用走 HTTPMemory 存 RedisLLM 走 APICallback 發(fā) Webhook。這套在隔離內(nèi)網(wǎng)里會立刻崩掉三個環(huán)節(jié)網(wǎng)絡層HTTP Client 超時默認 60s但內(nèi)網(wǎng) DNS 解析可能卡 45s存儲層Redis 安裝需 glibc 版本 ≥2.17而某國產(chǎn) OS 基于 2.12調(diào)度層AsyncIO Event Loop 在某些 ARM 內(nèi)網(wǎng)服務器上會因時鐘源不準導致 task hang。我們實測過在某電力調(diào)度內(nèi)網(wǎng)CentOS 7.6 鯤鵬 920一個標準 LangChain Agent 調(diào)用requests.get(http://localhost:8000/tool)有 37% 概率卡死在socket.connect()原因竟是內(nèi)核net.ipv4.tcp_fin_timeout被設為 3005 分鐘而連接池復用邏輯沒做 FIN_WAIT2 主動回收。所以第一刀砍掉所有“假設網(wǎng)絡可靠”的抽象。我們重構為四層內(nèi)網(wǎng)可信棧Intranet Trust Stack, ITS層級名稱關鍵選型設計原則L1推理層Inference Layerllama.cpp GGUF 量化模型無 Python 依賴純 C/C支持 AVX2/NEON內(nèi)存占用 1.2GBQwen2-0.5B-int4L2工具層Tool LayerMCP v0.3 協(xié)議 Python subprocess 封裝工具注冊即本地可執(zhí)行文件無網(wǎng)絡調(diào)用輸入輸出 JSON Schema 校驗L3狀態(tài)層State LayerSQLite 3.35 WAL 模式 自定義 VFS單文件部署支持并發(fā)讀寫WAL加密用 SQLCipher可選schema 版本化遷移L4交互層Interaction LayerVue 3 Pinia Web Worker LocalStorage fallbackSPA 靜態(tài)資源WebSocket 通信離線緩存關鍵 prompt 模板錯誤提示帶 error code提示不要試圖在內(nèi)網(wǎng)裝 Docker。某銀行客戶曾花 3 天調(diào)試 containerd 在麒麟 V10 的 cgroup v1 兼容問題最后發(fā)現(xiàn)用 systemd-run 啟一個普通進程更穩(wěn)——內(nèi)網(wǎng)工程的第一法則是能用 systemctl 管的就別用容器編排。2.2 為什么 SQLite 是內(nèi)網(wǎng) Agent 的“心臟”提到 SQLite很多人只想到“小項目用用”但在隔離內(nèi)網(wǎng)它是唯一滿足五維苛刻要求的存儲零配置部署apt install sqlite3或直接拷貝sqlite3二進制 1MB無 daemon、無端口、無用戶權限體系單文件原子性.db文件即數(shù)據(jù)庫備份cp遷移scp審計hexdump 查 journal并發(fā)安全WAL 模式下讀寫可并行實測 12 線程并發(fā) 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 變更歷史。我們?yōu)?Agent 設計了三張核心表-- 會話狀態(tài)表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 ); -- 工具調(diào)用日志表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表。內(nèi)網(wǎng) Agent 日志寫入高頻務必用INTEGER PRIMARY KEY即 rowid 別名替代INTEGER PRIMARY KEY AUTOINCREMENT。2.3 Vue SPA 如何做到“真離線”內(nèi)網(wǎng) SPA 最大陷阱是“以為靜態(tài)離線”。常見錯誤用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即使斷網(wǎng)也能打開首頁并顯示“當前離線已加載最近 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 }); // 主線程監(jiān)聽 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. 核心模塊實現(xiàn)從 SQLite 初始化到 MCP 工具注冊的完整鏈路3.1 SQLite 初始化與安全加固含實操命令內(nèi)網(wǎng)環(huán)境常面臨兩個現(xiàn)實操作系統(tǒng)老舊如 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編譯。某制造企業(yè)內(nèi)網(wǎng) GCC 版本為 4.8.5編譯 SQLite 3.40 會報error: ‘__builtin_ia32_pclmulqdq128’ not found——這是 Intel PCLMULQDQ 指令集支持問題。直接用官方二進制最穩(wěn)。Step 2創(chuàng)建 Agent 數(shù)據(jù)庫并啟用 WAL# 創(chuàng)建數(shù)據(jù)庫目錄避免權限問題 $ mkdir -p /opt/ai-agent/data $ cd /opt/ai-agent/data # 初始化數(shù)據(jù)庫 $ 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添加基礎表結構與初始數(shù)據(jù)# 執(zhí)行建表 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 環(huán)境下允許 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 進程無法寫入共享內(nèi)存文件。3.2 MCP Tools 工具協(xié)議實現(xiàn)Python 示例MCPModel Control Protocol是 2024 年新提出的本地 Agent 工具標準核心思想是每個工具是一個獨立可執(zhí)行文件通過 stdin/stdout 交換 JSON。相比 LangChain 的 Python 函數(shù)它天然支持多語言、免依賴、易審計。我們實現(xiàn)一個典型內(nèi)網(wǎng)工具query_erp_db.py查詢本地 SQLite ERP 數(shù)據(jù)庫。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 # 執(zhí)行查詢 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賦予執(zhí)行權限并測試$ chmod x query_erp_db.py $ ./query_erp_db.py EOF {table: purchase_orders, filters: {status: pending}} EOF # 輸出應為 JSON 對象含 status: success 和 data 數(shù)組Step 3Agent 端調(diào)用 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 # 內(nèi)網(wǎng)工具超時設為 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 接口調(diào)用 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()。某能源客戶現(xiàn)場一個未設 timeout 的Popen.wait()卡住 17 小時——因為工具腳本里input()等待 stdin而 stdin 已 EOF進程永遠掛起。run()的 timeout 是硬殺Popen.wait()的 timeout 是軟等。3.3 Vue SPA 與后端 WebSocket 通信實現(xiàn)內(nèi)網(wǎng) SPA 與 FastAPI 后端通信我們棄用 REST polling采用 WebSocket MessagePack 二進制協(xié)議降低帶寬和解析開銷。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) // 指數(shù)退避 } } } 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 仍保持連接內(nèi)網(wǎng)服務器連接數(shù)會緩慢爬升。某政務項目曾因此觸發(fā)ulimit -n限制默認 1024導致新會話無法建立。4. 實戰(zhàn)問題排查內(nèi)網(wǎng) Agent 的 7 類高頻故障與根因定位法4.1 “Agent 啟動后無響應” —— 90% 是 SQLite WAL 文件權限問題現(xiàn)象FastAPI 啟動成功WebSocket 連接正常但發(fā)送第一條消息后后端日志無任何輸出ps aux | grep sqlite無相關進程。根因定位檢查 SQLite 數(shù)據(jù)庫文件權限ls -l agent.db*確認agent.db-wal和agent.db-shm屬主與運行用戶一致檢查 SELinuxausearch -m avc -ts recent | grep sqlite若出現(xiàn)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 “工具調(diào)用返回空結果” —— MCP 工具 stdin 編碼陷阱現(xiàn)象前端傳{table: orders, filters: {status: shipped}}工具腳本收到input_data為空 dict{}。根因定位Python 3 默認 stdin 編碼為 UTF-8但某些國產(chǎn) 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 構建產(chǎn)物路徑錯配現(xiàn)象Nginx 返回 200但瀏覽器控制臺報Failed to load resource: the server responded with a status of 404 ()請求路徑為/assets/index.xxxxx.js。根因定位Vite 默認base: /但內(nèi)網(wǎng) 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 “并發(fā) 10 用戶后響應變慢” —— SQLite WAL 檢查點阻塞現(xiàn)象單用戶響應 200ms10 用戶并發(fā)時部分請求延遲飆升至 5shtop顯示 Python 進程 CPU 100%但iostat -x 1磁盤 IO 正常。根因定位SQLite WAL 模式下當 WAL 文件大小超過pragma wal_autocheckpoint默認 1000 頁會觸發(fā)自動檢查點checkpoint此時所有寫操作被阻塞直到 checkpoint 完成。內(nèi)網(wǎng) SSD 性能一般checkpoint 可能耗時 2~3s。驗證命令$ sqlite3 agent.db PRAGMA wal_checkpoint; # 返回類似0, 12, 12 → 表示 checkpoint 完成但 12 頁未寫入 $ sqlite3 agent.db PRAGMA journal_size_limit; # 查看 WAL 大小限制優(yōu)化方案-- 增大 WAL 自動檢查點閾值單位頁每頁 4KB PRAGMA wal_autocheckpoint 4000; -- 16MB -- 或禁用自動 checkpoint由應用主動控制 PRAGMA wal_autocheckpoint 0; -- 在應用空閑時手動 checkpoint PRAGMA wal_checkpoint(TRUNCATE);4.5 “WebSocket 連接頻繁斷開” —— 內(nèi)網(wǎng)防火墻 TCP keepalive 缺失現(xiàn)象WebSocket 連接約 300s5 分鐘后自動斷開Chrome DevTools 顯示W(wǎng)ebSocket is closed due to an error。根因定位內(nèi)網(wǎng)硬件防火墻默認 TCP keepalive 時間為 300s連接空閑超時即斷開。WebSocket 協(xié)議本身無心跳依賴底層 TCP keepalive。解決方法在 FastAPI WebSocket 端點中添加 Ping/Pongapp.websocket(/ws/{session_id}) async def websocket_endpoint(websocket: WebSocket, session_id: str): await websocket.accept() # 發(fā)送 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 數(shù)據(jù)庫被鎖” —— 多進程寫入競爭現(xiàn)象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 并發(fā)處理避免多進程方案 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)) # 指數(shù)退避 continue raise4.7 “模型推理卡死” —— llama.cpp 內(nèi)存映射沖突現(xiàn)象調(diào)用llama.cpp推理時進程 CPU 100% 但無輸出strace -p pid顯示卡在mmap()系統(tǒng)調(diào)用。根因定位llama.cpp 默認使用mmap加載模型但某些國產(chǎn) OS 內(nèi)核對MAP_POPULATE標志支持不全導致 mmap 阻塞。解決方法啟動時禁用 mmap# 加 -m 參數(shù)強制用 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; // 關鍵實操心得所有內(nèi)網(wǎng) Agent 部署前必須做“壓力快照”用ab -n 100 -c 10 http://localhost:8000/api/health測試基礎接口再用stress-ng --io 4 --vm 2 --vm-bytes 1G -t 60s模擬內(nèi)存壓力觀察 SQLite 和 llama.cpp 是否穩(wěn)定。我們發(fā)現(xiàn)70% 的線上故障都能在壓力快照中提前暴露。5. 運維與擴展如何讓內(nèi)網(wǎng) AI Agent 真正“活”三年不宕機5.1 日志審計用 SQLite 自身能力做全鏈路追蹤內(nèi)網(wǎng)系統(tǒng)最怕“出了問題不知道誰干的”。我們不額外裝 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審計查詢示例想查某次會話的所有工具調(diào)用直接查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 內(nèi)置函數(shù)老版本需用json1擴展。內(nèi)網(wǎng)升級 SQLite 后務必運行SELECT load_extension(libsqlitefunctions);啟用 JSON 支持。5.2 模型熱更新不重啟 Agent 的 GGUF 替換方案業(yè)務常需更新模型如從 Qwen2-0.5B 升級到 Qwen2-1.5B但重啟 Agent 會導致會話中斷。實現(xiàn)原理llama.cpp 支持運行時加載新