網(wǎng)AI Agent工程實(shí)戰(zhàn):SQLite+MCP+Vue輕量可信棧)
1. 項(xiàng)目概述為什么在隔離內(nèi)網(wǎng)里做 AI Agent 不是“降級(jí)”而是“硬核落地”“隔離內(nèi)網(wǎng)下 AI Agent 工程實(shí)戰(zhàn)”——這八個(gè)字一出來(lái)很多剛接觸 AI 工程化的同學(xué)第一反應(yīng)是“啊沒(méi)網(wǎng)絡(luò)怎么跑大模型連 Hugging Face 都打不開(kāi)Agent 還怎么思考”但恰恰相反真正要進(jìn)生產(chǎn)、進(jìn)金融、進(jìn)制造、進(jìn)能源、進(jìn)政務(wù)系統(tǒng)的 AI Agent90% 的首落地場(chǎng)景就是隔離內(nèi)網(wǎng)。不是不能聯(lián)網(wǎng)而是“不允許聯(lián)網(wǎng)”。不是技術(shù)退步而是安全前置、責(zé)任壓實(shí)、數(shù)據(jù)主權(quán)落地的必然選擇。我?guī)F(tuán)隊(duì)做過(guò) 7 個(gè)行業(yè)客戶的 AI Agent 落地項(xiàng)目其中 6 個(gè)明確要求全鏈路離線、模型本地化、工具鏈可控、日志可審計(jì)、無(wú)外網(wǎng)依賴??蛻粽f(shuō)得很直白“你們的 demo 可以跑在云上但我們的核心業(yè)務(wù)系統(tǒng)連 ping 外網(wǎng)都通不過(guò)防火墻策略?!彼浴案綦x內(nèi)網(wǎng)”不是限制條件而是真實(shí)世界的準(zhǔn)入門檻而“AI Agent 工程實(shí)戰(zhàn)”也不是調(diào)幾個(gè) API 就完事它是一整套從模型壓縮、工具編排、狀態(tài)持久化、前端交互到運(yùn)維監(jiān)控的閉環(huán)能力。你看到的熱搜詞里反復(fù)出現(xiàn)的SQLite、Vue SPA、MCP Tools、ngrok、frp其實(shí)暴露了兩個(gè)現(xiàn)實(shí)矛盾一邊是開(kāi)發(fā)者想用 LangChain/LangGraph 快速搭框架結(jié)果發(fā)現(xiàn)內(nèi)網(wǎng)連 pip install 都得走離線包另一邊是業(yè)務(wù)方只認(rèn)三件事能不能跑在國(guó)產(chǎn) CPU 上、能不能接 OA/ERP/工控系統(tǒng)、出問(wèn)題時(shí)能不能 5 分鐘內(nèi)定位到哪一行代碼觸發(fā)了哪條 SQL。所以這個(gè)項(xiàng)目的核心不是“把云上 Agent 搬進(jìn)內(nèi)網(wǎng)”而是重新定義 AI Agent 的工程基線? 模型層不依賴 OpenAI 或千問(wèn) API用量化后的 Qwen2-0.5B 或 Phi-3-mini 做推理引擎? 工具層不用遠(yuǎn)程 function call用 MCPModel Control Protocol標(biāo)準(zhǔn)封裝本地可執(zhí)行工具如 Python 腳本、Shell 命令、數(shù)據(jù)庫(kù)查詢? 記憶層放棄 Redis/PostgreSQL用 SQLite 實(shí)現(xiàn)輕量、單文件、ACID 兼容的會(huì)話狀態(tài)與知識(shí)緩存? 界面層不托管在 Vercel 或 Netlify用 Vue SPA 打包成純靜態(tài)資源Nginx 或?qū)毸姘逯苯?serve? 調(diào)試層沒(méi)有 ngrok/frp 的“偽穿透”而是用內(nèi)網(wǎng) DNS反向代理本地 WebSocket 通道實(shí)現(xiàn)真·開(kāi)發(fā)聯(lián)調(diào)。這不是“閹割版 AI”而是“扎根版 AI”——就像拖拉機(jī)不需要 GPS 導(dǎo)航也能深耕萬(wàn)畝良田AI Agent 在內(nèi)網(wǎng)的價(jià)值從來(lái)不在“聯(lián)網(wǎng)查天氣”而在“自動(dòng)比對(duì)兩份 ERP 出庫(kù)單差異”、“實(shí)時(shí)解析 DCS 控制臺(tái)日志告警”、“按 GMP 規(guī)范生成 QA 檢驗(yàn)記錄”。如果你正被“AI 落地難”困擾或者正在寫標(biāo)書、做 PoC、準(zhǔn)備給信息科匯報(bào)方案這篇內(nèi)容就是你手邊那張沒(méi)寫在 PPT 里的工程檢查表。它不講概念只列命令不畫架構(gòu)圖只放 config不談“未來(lái)已來(lái)”只說(shuō)“今天就能跑起來(lái)”。2. 整體設(shè)計(jì)思路放棄“云原生幻想”構(gòu)建四層內(nèi)網(wǎng)可信棧2.1 為什么必須放棄 LangChain 默認(rèn)范式LangChain 默認(rèn)設(shè)計(jì)是為云服務(wù)優(yōu)化的Tool 調(diào)用走 HTTPMemory 存 RedisLLM 走 APICallback 發(fā) Webhook。這套在隔離內(nèi)網(wǎng)里會(huì)立刻崩掉三個(gè)環(huán)節(jié)網(wǎng)絡(luò)層HTTP Client 超時(shí)默認(rèn) 60s但內(nèi)網(wǎng) DNS 解析可能卡 45s存儲(chǔ)層Redis 安裝需 glibc 版本 ≥2.17而某國(guó)產(chǎn) OS 基于 2.12調(diào)度層AsyncIO Event Loop 在某些 ARM 內(nèi)網(wǎng)服務(wù)器上會(huì)因時(shí)鐘源不準(zhǔn)導(dǎo)致 task hang。我們實(shí)測(cè)過(guò)在某電力調(diào)度內(nèi)網(wǎng)CentOS 7.6 鯤鵬 920一個(gè)標(biāo)準(zhǔn) LangChain Agent 調(diào)用requests.get(http://localhost:8000/tool)有 37% 概率卡死在socket.connect()原因竟是內(nèi)核net.ipv4.tcp_fin_timeout被設(shè)為 3005 分鐘而連接池復(fù)用邏輯沒(méi)做 FIN_WAIT2 主動(dòng)回收。所以第一刀砍掉所有“假設(shè)網(wǎng)絡(luò)可靠”的抽象。我們重構(gòu)為四層內(nèi)網(wǎng)可信棧Intranet Trust Stack, ITS層級(jí)名稱關(guān)鍵選型設(shè)計(jì)原則L1推理層Inference Layerllama.cpp GGUF 量化模型無(wú) Python 依賴純 C/C支持 AVX2/NEON內(nèi)存占用 1.2GBQwen2-0.5B-int4L2工具層Tool LayerMCP v0.3 協(xié)議 Python subprocess 封裝工具注冊(cè)即本地可執(zhí)行文件無(wú)網(wǎng)絡(luò)調(diào)用輸入輸出 JSON Schema 校驗(yàn)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 通信離線緩存關(guān)鍵 prompt 模板錯(cuò)誤提示帶 error code提示不要試圖在內(nèi)網(wǎng)裝 Docker。某銀行客戶曾花 3 天調(diào)試 containerd 在麒麟 V10 的 cgroup v1 兼容問(wèn)題最后發(fā)現(xiàn)用 systemd-run 啟一個(gè)普通進(jìn)程更穩(wěn)——內(nèi)網(wǎng)工程的第一法則是能用 systemctl 管的就別用容器編排。2.2 為什么 SQLite 是內(nèi)網(wǎng) Agent 的“心臟”提到 SQLite很多人只想到“小項(xiàng)目用用”但在隔離內(nèi)網(wǎng)它是唯一滿足五維苛刻要求的存儲(chǔ)零配置部署apt install sqlite3或直接拷貝sqlite3二進(jìn)制 1MB無(wú) daemon、無(wú)端口、無(wú)用戶權(quán)限體系單文件原子性.db文件即數(shù)據(jù)庫(kù)備份cp遷移scp審計(jì)hexdump 查 journal并發(fā)安全WAL 模式下讀寫可并行實(shí)測(cè) 12 線程并發(fā) INSERT SELECTTPS 840±30無(wú)鎖等待嵌入式友好Python 的sqlite3模塊是標(biāo)準(zhǔn)庫(kù)Node.js 用better-sqlite3Go 用mattn/go-sqlite3全語(yǔ)言原生支持可審計(jì)性強(qiáng)PRAGMA journal_mode WAL; PRAGMA synchronous NORMAL;組合下每條 INSERT 都寫入-wal文件可用sqlite3 your.db SELECT * FROM sqlite_master;直接查 schema 變更歷史。我們?yōu)?Agent 設(shè)計(jì)了三張核心表-- 會(huì)話狀態(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)) ); -- 知識(shí)片段緩存表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做主鍵性能陷阱。實(shí)測(cè)在 50 萬(wàn)行tool_log表中INSERT INTO tool_log (...) VALUES (...)比INSERT INTO tool_log (id, ...) VALUES (NULL, ...)慢 2.3 倍——因?yàn)榍罢邚?qiáng)制維護(hù)單獨(dú)的sqlite_sequence表。內(nèi)網(wǎng) Agent 日志寫入高頻務(wù)必用INTEGER PRIMARY KEY即 rowid 別名替代INTEGER PRIMARY KEY AUTOINCREMENT。2.3 Vue SPA 如何做到“真離線”內(nèi)網(wǎng) SPA 最大陷阱是“以為靜態(tài)離線”。常見(jiàn)錯(cuò)誤用axios請(qǐng)求/api/chat但忘了 Nginx 沒(méi)配location /api { proxy_pass http://localhost:8000; }import { createPinia } from pinia但沒(méi)把pinia打包進(jìn) vendor chunk導(dǎo)致首次加載 404用new WebSocket(ws://localhost:8000/ws)但防火墻只開(kāi)了 80/443WebSocket 被攔截。我們的解決方案是三層離線保障構(gòu)建時(shí)離線Vite 配置build.rollupOptions.external [vue, pinia, axios]用pnpm add -D vite-plugin-static-copy把node_modules/axios/dist/axios.min.js復(fù)制到public/libs/HTML 中script src/libs/axios.min.js/script運(yùn)行時(shí)降級(jí)WebSocket 連接失敗后自動(dòng) fallback 到fetch(/api/poll?sessionxxx)輪詢間隔 2s最大重試 5 次緩存兜底service-worker.js緩存/index.html,/assets/*.js,/prompts/*.json即使斷網(wǎng)也能打開(kāi)首頁(yè)并顯示“當(dāng)前離線已加載最近 3 條 prompt 模板”。關(guān)鍵代碼片段src/stores/chat.ts// 使用 Web Worker 隔離耗時(shí)操作避免 UI 卡頓 const worker new Worker(new URL(./chat.worker.ts, import.meta.url)); worker.postMessage({ type: INIT, sessionId: currentSession.value }); // 主線程監(jiān)聽(tīng) Worker 消息 worker.onmessage (e) { if (e.data.type STREAM_CHUNK) { // 流式追加到消息列表非一次性渲染 messages.value.push(e.data.chunk); } }; // 錯(cuò)誤時(shí)本地回滾 worker.onerror () { // 從 localStorage 讀取最近一次成功會(huì)話 const lastSuccess localStorage.getItem(last_chat_success); if (lastSuccess) { messages.value JSON.parse(lastSuccess); } };3. 核心模塊實(shí)現(xiàn)從 SQLite 初始化到 MCP 工具注冊(cè)的完整鏈路3.1 SQLite 初始化與安全加固含實(shí)操命令內(nèi)網(wǎng)環(huán)境常面臨兩個(gè)現(xiàn)實(shí)操作系統(tǒng)老舊如 CentOS 7 默認(rèn) SQLite 3.7.17不支持 WAL 模式安全策略禁止 root 運(yùn)行但sqlite3二進(jìn)制需手動(dòng)升級(jí)。Step 1檢測(cè)并升級(jí) SQLite# 查看當(dāng)前版本 $ sqlite3 --version 3.7.17 # 下載預(yù)編譯二進(jìn)制官方提供 $ 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 # 應(yīng)輸出 3.42.0實(shí)操心得不要用./configure make編譯。某制造企業(yè)內(nèi)網(wǎng) GCC 版本為 4.8.5編譯 SQLite 3.40 會(huì)報(bào)error: ‘__builtin_ia32_pclmulqdq128’ not found——這是 Intel PCLMULQDQ 指令集支持問(wèn)題。直接用官方二進(jìn)制最穩(wěn)。Step 2創(chuàng)建 Agent 數(shù)據(jù)庫(kù)并啟用 WAL# 創(chuàng)建數(shù)據(jù)庫(kù)目錄避免權(quán)限問(wèn)題 $ mkdir -p /opt/ai-agent/data $ cd /opt/ai-agent/data # 初始化數(shù)據(jù)庫(kù) $ sqlite3 agent.db EOF PRAGMA journal_mode WAL; PRAGMA synchronous NORMAL; PRAGMA temp_store MEMORY; PRAGMA mmap_size 268435456; -- 256MB VACUUM; EOF # 驗(yàn)證 WAL 是否生效 $ sqlite3 agent.db PRAGMA journal_mode; # 輸出應(yīng)為walStep 3添加基礎(chǔ)表結(jié)構(gòu)與初始數(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); -- 插入一條測(cè)試會(huì)話 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設(shè)置文件權(quán)限與 SELinux 策略若啟用# 設(shè)置屬組假設(shè)運(yùn)行用戶為 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 模式必需的臨時(shí)文件權(quán)限必須與.db文件一致。曾有客戶因chmod 644 agent.db-wal導(dǎo)致 Agent 啟動(dòng)時(shí)報(bào)database is locked——因?yàn)?SQLite 進(jìn)程無(wú)法寫入共享內(nèi)存文件。3.2 MCP Tools 工具協(xié)議實(shí)現(xiàn)Python 示例MCPModel Control Protocol是 2024 年新提出的本地 Agent 工具標(biāo)準(zhǔn)核心思想是每個(gè)工具是一個(gè)獨(dú)立可執(zhí)行文件通過(guò) stdin/stdout 交換 JSON。相比 LangChain 的 Python 函數(shù)它天然支持多語(yǔ)言、免依賴、易審計(jì)。我們實(shí)現(xiàn)一個(gè)典型內(nèi)網(wǎng)工具query_erp_db.py查詢本地 SQLite ERP 數(shù)據(jù)庫(kù)。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, {}) # 安全校驗(yàn)只允許查詢白名單表 allowed_tables [purchase_orders, inventory, vendors] if table_name not in allowed_tables: raise ValueError(fTable {table_name} not in whitelist) # 構(gòu)建 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 結(jié)果 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í)行權(quán)限并測(cè)試$ chmod x query_erp_db.py $ ./query_erp_db.py EOF {table: purchase_orders, filters: {status: pending}} EOF # 輸出應(yīng)為 JSON 對(duì)象含 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)工具超時(shí)設(shè)為 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)實(shí)操心得MCP 工具必須用subprocess.run(..., timeout...)絕不能用subprocess.Popenwait()。某能源客戶現(xiàn)場(chǎng)一個(gè)未設(shè) timeout 的Popen.wait()卡住 17 小時(shí)——因?yàn)楣ぞ吣_本里input()等待 stdin而 stdin 已 EOF進(jìn)程永遠(yuǎn)掛起。run()的 timeout 是硬殺Popen.wait()的 timeout 是軟等。3.3 Vue SPA 與后端 WebSocket 通信實(shí)現(xiàn)內(nèi)網(wǎng) SPA 與 FastAPI 后端通信我們棄用 REST polling采用 WebSocket MessagePack 二進(jìn)制協(xié)議降低帶寬和解析開(kāi)銷。Step 1FastAPI 后端 WebSocket 端點(diǎn)# 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) # 處理消息如啟動(dòng) 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)) // 處理流式響應(yīng) 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必須關(guān)閉 WebSocket否則路由切換后 socket 仍保持連接內(nèi)網(wǎng)服務(wù)器連接數(shù)會(huì)緩慢爬升。某政務(wù)項(xiàng)目曾因此觸發(fā)ulimit -n限制默認(rèn) 1024導(dǎo)致新會(huì)話無(wú)法建立。4. 實(shí)戰(zhàn)問(wèn)題排查內(nèi)網(wǎng) Agent 的 7 類高頻故障與根因定位法4.1 “Agent 啟動(dòng)后無(wú)響應(yīng)” —— 90% 是 SQLite WAL 文件權(quán)限問(wèn)題現(xiàn)象FastAPI 啟動(dòng)成功WebSocket 連接正常但發(fā)送第一條消息后后端日志無(wú)任何輸出ps aux | grep sqlite無(wú)相關(guān)進(jìn)程。根因定位檢查 SQLite 數(shù)據(jù)庫(kù)文件權(quán)限ls -l agent.db*確認(rèn)agent.db-wal和agent.db-shm屬主與運(yùn)行用戶一致檢查 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可能增長(zhǎng)至 1GB填滿/tmp如果 SQLite 臨時(shí)目錄指向/tmp。解決命令# 強(qiáng)制 SQLite 使用指定臨時(shí)目錄避免 /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)用返回空結(jié)果” —— MCP 工具 stdin 編碼陷阱現(xiàn)象前端傳{table: orders, filters: {status: shipped}}工具腳本收到input_data為空 dict{}。根因定位Python 3 默認(rèn) stdin 編碼為 UTF-8但某些國(guó)產(chǎn) OS 終端 locale 為zh_CN.GB18030導(dǎo)致sys.stdin.buffer.read()讀到亂碼json.load()報(bào)JSONDecodeError腳本 exit(1)但 FastAPI 未捕獲 stderr返回空響應(yīng)。驗(yàn)證方法# 手動(dòng)測(cè)試編碼 $ echo {table:orders} | iconv -f GB18030 -t UTF-8 | python3 query_erp_db.py # 若成功則是編碼問(wèn)題解決方法在 MCP 工具腳本開(kāi)頭強(qiáng)制設(shè)置 stdin 編碼import sys import io # 強(qiáng)制 stdin 為 UTF-8 sys.stdin io.TextIOWrapper( sys.stdin.buffer, encodingutf-8, errorsreplace )4.3 “Vue 頁(yè)面白屏” —— Vite 構(gòu)建產(chǎn)物路徑錯(cuò)配現(xiàn)象Nginx 返回 200但瀏覽器控制臺(tái)報(bào)Failed to load resource: the server responded with a status of 404 ()請(qǐng)求路徑為/assets/index.xxxxx.js。根因定位Vite 默認(rèn)base: /但內(nèi)網(wǎng) Nginx 配置為location /ai-agent/ { alias /var/www/ai-agent/; }導(dǎo)致 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 用戶后響應(yīng)變慢” —— SQLite WAL 檢查點(diǎn)阻塞現(xiàn)象單用戶響應(yīng) 200ms10 用戶并發(fā)時(shí)部分請(qǐng)求延遲飆升至 5shtop顯示 Python 進(jìn)程 CPU 100%但iostat -x 1磁盤 IO 正常。根因定位SQLite WAL 模式下當(dāng) WAL 文件大小超過(guò)pragma wal_autocheckpoint默認(rèn) 1000 頁(yè)會(huì)觸發(fā)自動(dòng)檢查點(diǎn)checkpoint此時(shí)所有寫操作被阻塞直到 checkpoint 完成。內(nèi)網(wǎng) SSD 性能一般checkpoint 可能耗時(shí) 2~3s。驗(yàn)證命令$ sqlite3 agent.db PRAGMA wal_checkpoint; # 返回類似0, 12, 12 → 表示 checkpoint 完成但 12 頁(yè)未寫入 $ sqlite3 agent.db PRAGMA journal_size_limit; # 查看 WAL 大小限制優(yōu)化方案-- 增大 WAL 自動(dòng)檢查點(diǎn)閾值單位頁(yè)每頁(yè) 4KB PRAGMA wal_autocheckpoint 4000; -- 16MB -- 或禁用自動(dòng) checkpoint由應(yīng)用主動(dòng)控制 PRAGMA wal_autocheckpoint 0; -- 在應(yīng)用空閑時(shí)手動(dòng) checkpoint PRAGMA wal_checkpoint(TRUNCATE);4.5 “WebSocket 連接頻繁斷開(kāi)” —— 內(nèi)網(wǎng)防火墻 TCP keepalive 缺失現(xiàn)象WebSocket 連接約 300s5 分鐘后自動(dòng)斷開(kāi)Chrome DevTools 顯示W(wǎng)ebSocket is closed due to an error。根因定位內(nèi)網(wǎng)硬件防火墻默認(rèn) TCP keepalive 時(shí)間為 300s連接空閑超時(shí)即斷開(kāi)。WebSocket 協(xié)議本身無(wú)心跳依賴底層 TCP keepalive。解決方法在 FastAPI WebSocket 端點(diǎn)中添加 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前端接收后回復(fù) Pongsocket.onmessage (event) { const msg JSON.parse(event.data) if (msg.type PING) { socket.send(JSON.stringify({ type: PONG })) } }4.6 “SQLite 數(shù)據(jù)庫(kù)被鎖” —— 多進(jìn)程寫入競(jìng)爭(zhēng)現(xiàn)象Agent 日志表tool_log寫入失敗報(bào)database is locked但PRAGMA locking_mode;顯示NORMAL。根因定位SQLite 默認(rèn)locking_mode NORMAL允許多個(gè)連接讀但寫連接需獨(dú)占。當(dāng)多個(gè) FastAPI worker 進(jìn)程如用 Uvicorn--workers 4同時(shí)寫tool_log會(huì)因鎖競(jìng)爭(zhēng)失敗。解決方法方案 A推薦改用--workers 1--reload用 asyncio 并發(fā)處理避免多進(jìn)程方案 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推理時(shí)進(jìn)程 CPU 100% 但無(wú)輸出strace -p pid顯示卡在mmap()系統(tǒng)調(diào)用。根因定位llama.cpp 默認(rèn)使用mmap加載模型但某些國(guó)產(chǎn) OS 內(nèi)核對(duì)MAP_POPULATE標(biāo)志支持不全導(dǎo)致 mmap 阻塞。解決方法啟動(dòng)時(shí)禁用 mmap# 加 -m 參數(shù)強(qiáng)制用 malloc 加載 ./main -m models/qwen2-0.5b.Q4_K_M.gguf -p Hello -n 128 --no-mmap或在代碼中設(shè)置// llama.cpp/src/llama.cpp llama_context_params params llama_context_params_default(); params.use_mmap false; // 關(guān)鍵實(shí)操心得所有內(nèi)網(wǎng) Agent 部署前必須做“壓力快照”用ab -n 100 -c 10 http://localhost:8000/api/health測(cè)試基礎(chǔ)接口再用stress-ng --io 4 --vm 2 --vm-bytes 1G -t 60s模擬內(nèi)存壓力觀察 SQLite 和 llama.cpp 是否穩(wěn)定。我們發(fā)現(xiàn)70% 的線上故障都能在壓力快照中提前暴露。5. 運(yùn)維與擴(kuò)展如何讓內(nèi)網(wǎng) AI Agent 真正“活”三年不宕機(jī)5.1 日志審計(jì)用 SQLite 自身能力做全鏈路追蹤內(nèi)網(wǎng)系統(tǒng)最怕“出了問(wèn)題不知道誰(shuí)干的”。我們不額外裝 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審計(jì)查詢示例想查某次會(huì)話的所有工具調(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擴(kuò)展。內(nèi)網(wǎng)升級(jí) SQLite 后務(wù)必運(yùn)行SELECT load_extension(libsqlitefunctions);啟用 JSON 支持。5.2 模型熱更新不重啟 Agent 的 GGUF 替換方案業(yè)務(wù)常需更新模型如從 Qwen2-0.5B 升級(jí)到 Qwen2-1.5B但重啟 Agent 會(huì)導(dǎo)致會(huì)話中斷。實(shí)現(xiàn)原理llama.cpp 支持運(yùn)行時(shí)加載新