双Agent协管数据库:MCP协议实现自然语言查询与自动修复 | 完整实战

双Agent协管数据库:MCP协议实现自然语言查询与自动修复 | 完整实战


写在前面


数据库运维有两大痛点:非技术人员每次查数据都要找DBA帮忙;出了故障从发现到修复的时间窗口太长。


这篇文章用两个AI Agent + MCP协议实现一个解决方案:

  • 查询Agent:接收自然语言问题,自动生成SQL并查询,返回结果
  • 运维Agent:监控数据库状态,识别异常,自动执行修复操作
  • 两个Agent通过MCP(Model Context Protocol)协调工作

不需要买任何商业产品,开源工具就能搭。本文所有代码在以下环境验证通过:


  • Python 3.10+
  • SQLite / PostgreSQL (语法兼容)
  • 普通Linux服务器或MacBook均可

---


目录


  1. 整体架构
  2. 环境搭建与依赖
  3. MCP协议实现:核心通信层
  4. 查询Agent:自然语言→SQL→结果
  5. 运维Agent:监控→判断→修复
  6. 双Agent协调:MCP消息路由
  7. 启动与测试
  8. 调优与替换注意事项
  9. 常见问题

---


1. 整体架构 {#1}


用户提问 → [查询Agent] ←→ [MCP消息总线] ←→ [运维Agent]
                ↓                          ↓
          [数据库查询]              [状态监控+自动修复]
                ↓                          ↓
          SQLite/PG                  Alert/日志/健康检查

两个Agent彼此不直接调用,通过MCP协议的统一消息通道交换信息。


Agent之间的通信格式:


{
  "type": "query_request",
  "id": "uuid",
  "question": "上个月销售额最高的产品是什么"
}

{
  "type": "query_result",
  "id": "uuid",
  "sql": "SELECT product_name, SUM(amount) ...",
  "result": [{"product_name": "A", "total": 123000}],
  "error": null
}

---


2. 环境搭建与依赖 {#2}


mkdir dual-agent-db && cd dual-agent-db
python3 -m venv venv
source venv/bin/activate

pip install openai pydantic sqlalchemy aiosqlite httpx loguru tabulate

为什么选这些包:


| 包 | 用途 |

|---|---|

| openai | 调用大模型API(支持OpenAI、兼容任何OpenAI格式的API) |

| sqlalchemy | 数据库ORM,方便切换SQLite/PG/MySQL |

| aiosqlite | 异步SQLite,不阻塞Agent主循环 |

| pydantic | 消息体校验,MCP协议数据模型 |

| tabulate | SQL结果表格化,方便Agent理解 |

| loguru | 结构化的Agent日志 |


如果你用私有模型(如DeepSeek、Qwen),只需改api_base即可,代码兼容。


---


3. MCP协议实现:核心通信层 {#3}


MCP在本实现中简化成一种基于消息队列的协议层,让两个Agent可以异步通信。


3.1 数据模型


# message.py
from pydantic import BaseModel
from typing import Any, Optional
from enum import Enum
import uuid

class MessageType(str, Enum):
    QUERY_REQUEST = "query_request"
    QUERY_RESULT = "query_result"
    QUERY_ERROR = "query_error"
    HEALTH_CHECK = "health_check"
    HEALTH_REPORT = "health_report"
    ALERT = "alert"
    FIX_REQUEST = "fix_request"
    FIX_RESULT = "fix_result"
    AGENT_LOG = "agent_log"

class MCPMessage(BaseModel):
    type: MessageType
    id: str = ""
    source: str = ""
    target: str = ""
    payload: dict[str, Any] = {}
    timestamp: float = 0.0
    error: Optional[str] = None

    def __init__(self, **data):
        super().__init__(**data)
        if not self.id:
            self.id = str(uuid.uuid4())[:8]
        if not self.timestamp:
            import time
            self.timestamp = time.time()

3.2 消息总线


# message_bus.py
import asyncio
from typing import Callable
from loguru import logger
from message import MCPMessage, MessageType

class MCPMessageBus:
    """简单的内存消息总线,支持发布/订阅模式"""

    def __init__(self):
        self.subscribers: dict[MessageType, list[Callable]] = {}
        self.message_log: list[MCPMessage] = []
        self._lock = asyncio.Lock()

    def subscribe(self, msg_type: MessageType, callback: Callable):
        if msg_type not in self.subscribers:
            self.subscribers[msg_type] = []
        self.subscribers[msg_type].append(callback)
        logger.info(f"订阅 {msg_type.value}")

    async def publish(self, message: MCPMessage):
        async with self._lock:
            self.message_log.append(message)
            if len(self.message_log) > 500:
                self.message_log.pop(0)

        logger.debug(f"[总线] {message.source} → {message.target}: {message.type.value}")

        if message.type in self.subscribers:
            for cb in self.subscribers[message.type]:
                asyncio.create_task(cb(message))

3.3 Agent基类


# base_agent.py
import asyncio
from loguru import logger
from message import MCPMessage, MessageType
from message_bus import MCPMessageBus

class BaseAgent:
    """Agent基类,所有Agent继承此类"""

    def __init__(self, name: str, bus: MCPMessageBus):
        self.name = name
        self.bus = bus
        self.running = False

    async def send(self, msg_type: MessageType, target: str, 
                   payload: dict, error: str | None = None):
        msg = MCPMessage(
            type=msg_type,
            source=self.name,
            target=target,
            payload=payload,
            error=error
        )
        await self.bus.publish(msg)

    async def logg(self, level: str, content: str):
        await self.send(MessageType.AGENT_LOG, "logger", {
            "agent": self.name, "level": level, "content": content
        })

    async def start(self):
        self.running = True
        logger.info(f"Agent {self.name} 启动")

    async def stop(self):
        self.running = False
        logger.info(f"Agent {self.name} 停止")

---


4. 查询Agent:自然语言→SQL→结果 {#4}


核心逻辑:接收中文问题 → 让LLM生成SQL → 执行SQL → 返回结果。


4.1 Schema注入策略


为了让LLM生成准确的SQL,需要把数据库Schema告诉它。这里做了一个自动Schema提取:


# schema_loader.py
import sqlalchemy
from sqlalchemy import inspect
from loguru import logger

def load_schema(connection_string: str) -> str:
    """
    从数据库连接串自动提取Schema,返回文本描述
    示例连接串: sqlite:///sales.db 或 postgresql://user:pass@localhost/db
    """
    engine = sqlalchemy.create_engine(connection_string)
    inspector = inspect(engine)

    schema_parts = []
    for table_name in inspector.get_table_names():
        columns = inspector.get_columns(table_name)
        col_desc = []
        for col in columns:
            nullable = "NULL" if col["nullable"] else "NOT NULL"
            default = f" DEFAULT {col['default']}" if col['default'] else ""
            col_desc.append(f"  - {col['name']} ({col['type']}) {nullable}{default}")

        pk = inspector.get_pk_constraint(table_name)
        pk_cols = ", ".join(pk.get("constrained_columns", [])) if pk else ""
        pk_str = f" PRIMARY KEY: {pk_cols}" if pk_cols else ""

        fks = inspector.get_foreign_keys(table_name)
        fk_strs = []
        for fk in fks:
            fk_strs.append(f"  FK: {fk['constrained_columns']} → {fk['referred_table']}.{fk['referred_columns']}")
        fk_str = "\n".join(fk_strs)

        schema_parts.append(f"表: {table_name}{pk_str}\n" + "\n".join(col_desc))
        if fk_str:
            schema_parts.append(fk_str)

    return "\n\n".join(schema_parts)

4.2 NL→SQL执行Agent


# query_agent.py
import json, asyncio, time
from openai import OpenAI
from loguru import logger
import sqlalchemy
from sqlalchemy import text
from message import MCPMessage, MessageType
from message_bus import MCPMessageBus
from base_agent import BaseAgent
from schema_loader import load_schema

SYSTEM_PROMPT_TEMPLATE = """你是一个数据库查询助手。你的任务是把用户的中文问题转成SQL。

数据库Schema如下:
{schema}

规则:
1. 只生成SELECT查询,不要DML
2. 返回纯JSON格式,不要markdown包裹
3. 如果问题不够明确,在 sql 字段里写 null,在 error 写 "需要澄清"
4. 字段名用schema里的原字段名
5. 匹配中文搜索时用 LIKE '%%关键字%%'
6. LIMIT 不超过 100

输出格式:
{{"sql": "SELECT ...", "tables_used": ["table1"], "explanation": "这句SQL的作用"}}
"""

class QueryAgent(BaseAgent):
    def __init__(self, name: str, bus: MCPMessageBus, 
                 api_key: str, base_url: str, model: str,
                 db_connection: str):
        super().__init__(name, bus)
        self.client = OpenAI(api_key=api_key, base_url=base_url)
        self.model = model
        self.db_connection = db_connection
        self.schema_text = load_schema(db_connection)
        self.system_prompt = SYSTEM_PROMPT_TEMPLATE.format(schema=self.schema_text)
        self.engine = sqlalchemy.create_engine(db_connection)
        logger.info(f"已加载Schema,表数量: {self.schema_text.count('表:')}")

    async def start(self):
        await super().start()
        # 订阅查询请求
        self.bus.subscribe(MessageType.QUERY_REQUEST, self.handle_query_request)
        logger.info(f"[{self.name}] 查询Agent就绪")

    async def handle_query_request(self, msg: MCPMessage):
        question = msg.payload.get("question", "")
        if not question:
            logger.warning("收到空问题")
            return

        logger.info(f"收到查询: {question[:50]}...")

        # Step 1: LLM生成SQL
        sql_result = await self._generate_sql(question)
        if sql_result.get("sql") is None:
            await self.send(MessageType.QUERY_ERROR, msg.source, {
                "question": question, "error": sql_result.get("error", "无法生成SQL")
            })
            return

        sql_text = sql_result["sql"]

        # Step 2: 执行SQL
        query_result = await self._execute_sql(sql_text)

        # Step 3: 返回结果
        if "error" in query_result and query_result["error"]:
            await self.send(MessageType.QUERY_ERROR, msg.source, {
                "question": question, "sql": sql_text, "error": query_result["error"]
            })
        else:
            await self.send(MessageType.QUERY_RESULT, msg.source, {
                "question": question,
                "sql": sql_text,
                "result": query_result["rows"],
                "row_count": len(query_result.get("rows", [])),
                "columns": query_result.get("columns", []),
                "elapsed_ms": query_result.get("elapsed_ms", 0)
            })

    async def _generate_sql(self, question: str) -> dict:
        loop = asyncio.get_event_loop()
        try:
            response = await loop.run_in_executor(None, lambda: self.client.chat.completions.create(
                model=self.model,
                messages=[
                    {"role": "system", "content": self.system_prompt},
                    {"role": "user", "content": question}
                ],
                temperature=0.05,
                max_tokens=800
            ))
            raw = response.choices[0].message.content.strip()
            # 去掉可能的 markdown 包裹
            if raw.startswith("```"):
                raw = raw.split("\n", 1)[-1]
                raw = raw.rsplit("```", 1)[0].strip()
            result = json.loads(raw)
            logger.info(f"LLM生成SQL: {result.get('sql', 'N/A')[:80]}")
            return result
        except Exception as e:
            logger.error(f"SQL生成失败: {e}")
            return {"sql": None, "error": str(e)}

    async def _execute_sql(self, sql_text: str) -> dict:
        start = time.time()
        try:
            loop = asyncio.get_event_loop()
            result = await loop.run_in_executor(None, lambda: self.engine.connect().execute(text(sql_text)))
            # 前提:这是SELECT查询
            columns = list(result.keys())
            rows = [dict(zip(columns, row)) for row in result.fetchmany(100)]
            elapsed = int((time.time() - start) * 1000)
            logger.info(f"SQL执行成功: {len(rows)}行, {elapsed}ms")
            return {"columns": columns, "rows": rows, "elapsed_ms": elapsed}
        except Exception as e:
            logger.error(f"SQL执行失败: {e}")
            return {"error": str(e)}

---


5. 运维Agent:监控→判断→修复 {#5}


运维Agent定期检查数据库健康状态,发现问题自动尝试修复。


5.1 健康检查逻辑


# health_checker.py
import sqlalchemy
from sqlalchemy import text
from loguru import logger
import time

class DBHealthChecker:
    """数据库健康检查器"""

    def __init__(self, connection_string: str):
        self.engine = sqlalchemy.create_engine(connection_string)

    def check(self) -> dict:
        results = {}
        issues = []

        # 1. 基本连通性
        t0 = time.time()
        try:
            with self.engine.connect() as conn:
                conn.execute(text("SELECT 1"))
                results["latency_ms"] = int((time.time() - t0) * 1000)
                results["connected"] = True
        except Exception as e:
            results["connected"] = False
            issues.append(f"连接失败: {e}")
            return {"healthy": False, "issues": issues, "details": results}

        # 2. 获取表状态
        try:
            with self.engine.connect() as conn:
                tables = sqlalchemy.inspect(self.engine).get_table_names()
                table_status = []
                for tname in tables:
                    row_count = conn.execute(text(f"SELECT COUNT(*) FROM \"{tname}\"")).scalar()
                    # 获取表大小(SQLite不支持直接大小,跳过)
                    table_status.append({
                        "name": tname, "rows": row_count
                    })
                results["tables"] = table_status
        except Exception as e:
            issues.append(f"表检查失败: {e}")

        # 3. 检查最近错误日志
        results["healthy"] = len(issues) == 0
        results["issues"] = issues
        return results

5.2 运维Agent主逻辑


# ops_agent.py
import asyncio, json
from openai import OpenAI
from loguru import logger
from message import MCPMessage, MessageType
from message_bus import MCPMessageBus
from base_agent import BaseAgent
from health_checker import DBHealthChecker

FIX_SYSTEM_PROMPT = """你是一个数据库运维专家。根据健康检查结果,判断是否需要修复。

当前数据库类型: {db_type}

对于每个问题,请给出修复方案。输出JSON格式:
{{"needs_fix": true/false, "fixes": [{{"action": "描述", "sql": "修复SQL或null"}}], "reasoning": "分析过程"}}
"""

class OpsAgent(BaseAgent):
    def __init__(self, name: str, bus: MCPMessageBus,
                 api_key: str, base_url: str, model: str,
                 connection_string: str, check_interval: int = 30):
        super().__init__(name, bus)
        self.client = OpenAI(api_key=api_key, base_url=base_url)
        self.model = model
        self.checker = DBHealthChecker(connection_string)
        self.connection_string = connection_string
        self.check_interval = check_interval
        self.engine = sqlalchemy.create_engine(connection_string)

        # 判断数据库类型
        if "sqlite" in connection_string:
            self.db_type = "SQLite"
        elif "postgresql" in connection_string:
            self.db_type = "PostgreSQL"
        elif "mysql" in connection_string:
            self.db_type = "MySQL"
        else:
            self.db_type = "Unknown"

    async def start(self):
        await super().start()
        # 启动定期检查任务
        asyncio.create_task(self._monitor_loop())
        logger.info(f"[{self.name}] 运维Agent启动,检查间隔={self.check_interval}s")

    async def _monitor_loop(self):
        while self.running:
            try:
                result = self.checker.check()
                await self.send(MessageType.HEALTH_REPORT, "logger", result)
                logger.info(f"健康检查: {'通过' if result.get('healthy') else '异常'}")

                # 如果有问题,让LLM判断是否需要自动修复
                if not result.get("healthy"):
                    await self._decide_fix(result)
            except Exception as e:
                logger.error(f"健康检查失败: {e}")
            await asyncio.sleep(self.check_interval)

    async def _decide_fix(self, health_result: dict):
        loop = asyncio.get_event_loop()
        prompt = FIX_SYSTEM_PROMPT.format(db_type=self.db_type)
        try:
            response = await loop.run_in_executor(None, lambda: self.client.chat.completions.create(
                model=self.model,
                messages=[
                    {"role": "system", "content": prompt},
                    {"role": "user", "content": json.dumps(health_result, ensure_ascii=False)}
                ],
                temperature=0.1,
                max_tokens=1000
            ))
            raw = response.choices[0].message.content.strip()
            if raw.startswith("```"):
                raw = raw.split("\n", 1)[-1].rsplit("```", 1)[0].strip()
            decision = json.loads(raw)

            if decision.get("needs_fix"):
                for fix in decision.get("fixes", []):
                    fix_sql = fix.get("sql")
                    if fix_sql and fix_sql.strip().upper() != "NULL":
                        await self._apply_fix(fix)
                    else:
                        await self.send(MessageType.ALERT, "logger", {
                            "action": fix.get("action", "未知操作"),
                            "reasoning": decision.get("reasoning", "")
                        })
        except Exception as e:
            logger.error(f"修复决策失败: {e}")

    async def _apply_fix(self, fix: dict):
        action = fix.get("action", "")
        fix_sql = fix.get("sql", "")
        logger.info(f"执行修复: {action}")
        try:
            loop = asyncio.get_event_loop()
            with self.engine.connect() as conn:
                conn.execute(text(fix_sql))
                if not self.connection_string.startswith("sqlite"):
                    conn.commit()
            await self.send(MessageType.FIX_RESULT, "logger", {
                "action": action, "sql": fix_sql, "success": True
            })
            logger.success(f"修复成功: {action}")
        except Exception as e:
            await self.send(MessageType.FIX_RESULT, "logger", {
                "action": action, "sql": fix_sql, "success": False, "error": str(e)
            })
            logger.error(f"修复失败: {action}: {e}")

---


6. 双Agent协调 {#6}


6.1 启动入口


# main.py
import asyncio, os
from loguru import logger
from message_bus import MCPMessageBus
from query_agent import QueryAgent
from ops_agent import OpsAgent
from message import MessageType, MCPMessage

# 配置(改为你自己的API配置)
API_KEY = "sk-your-api-key"
BASE_URL = "https://api.openai.com/v1"
MODEL = "gpt-4o-mini"
DB_CONNECTION = "sqlite:///demo.db"

async def main():
    bus = MCPMessageBus()

    # 创建数据库(如果不存在)
    import sqlalchemy as sa
    engine = sa.create_engine(DB_CONNECTION)
    engine.execute("""
        CREATE TABLE IF NOT EXISTS products (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            category TEXT,
            price REAL,
            stock INTEGER DEFAULT 0,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)
    engine.execute("""
        CREATE TABLE IF NOT EXISTS orders (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            product_id INTEGER REFERENCES products(id),
            quantity INTEGER,
            total_price REAL,
            order_date DATE,
            status TEXT DEFAULT 'pending'
        )
    """)
    # 插入测试数据
    engine.execute("INSERT OR IGNORE INTO products VALUES (1, '智能网关E100', '硬件', 2999, 50, '2026-01-01')")
    engine.execute("INSERT OR IGNORE INTO products VALUES (2, '边缘计算模块', '硬件', 5999, 30, '2026-01-01')")
    engine.execute("INSERT OR IGNORE INTO products VALUES (3, 'AI巡检服务', '服务', 5000, 999, '2026-01-01')")
    engine.execute("INSERT OR IGNORE INTO orders VALUES (1, 1, 5, 14995, '2026-05-01', 'shipped')")
    engine.execute("INSERT OR IGNORE INTO orders VALUES (2, 2, 2, 11998, '2026-05-10', 'pending')")
    engine.execute("INSERT OR IGNORE INTO orders VALUES (3, 1, 10, 29990, '2026-05-15', 'shipped')")
    logger.info("测试数据库初始化完成")

    # 启动Agent
    query_agent = QueryAgent("query-agent", bus, API_KEY, BASE_URL, MODEL, DB_CONNECTION)
    ops_agent = OpsAgent("ops-agent", bus, API_KEY, BASE_URL, MODEL, DB_CONNECTION, check_interval=60)

    await query_agent.start()
    await ops_agent.start()

    # 模拟用户提问
    test_questions = [
        "库存低于50的产品有哪些",
        "上个月销售额是多少",
        "订单最多的产品是哪个"
    ]

    for q in test_questions:
        msg = MCPMessage(
            type=MessageType.QUERY_REQUEST,
            source="user",
            target="query-agent",
            payload={"question": q}
        )
        await bus.publish(msg)
        await asyncio.sleep(3)  # 给Agent处理时间

    # 等待一会儿,让运维Agent也执行一次检查
    logger.info("Agent运行中... 按 Ctrl+C 退出")
    try:
        while True:
            await asyncio.sleep(1)
    except KeyboardInterrupt:
        await query_agent.stop()
        await ops_agent.stop()
        logger.info("Agent已停止")

if __name__ == "__main__":
    asyncio.run(main())

6.2 消息路由详解


两个Agent不直接调用对方。流程如下:


用户 → MCP总线 → QueryAgent(订阅QUERY_REQUEST) → 处理 → 发回QUERY_RESULT

OpsAgent → MCP总线 → 定期发HEALTH_REPORT
                  ↓
          Logger订阅日志消息 → 输出到控制台

OpsAgent发现异常 → MCP总线 → 触发FIX_REQUEST
                    ↓
            OpsAgent自己处理(不需要别的Agent响应)

这种设计的优势:

  1. 解耦:加第三个Agent只需要订阅对应消息类型
  2. 可观测:所有消息在总线日志里,方便调试
  3. 容错:一个Agent挂了不影响另一个

---


7. 启动与测试 {#7}


7.1 启动


# 配置API
export OPENAI_API_KEY="sk-your-key"

# 运行
python main.py

7.2 预期输出


2026-05-27 10:00:01 | Agent query-agent 启动
2026-05-27 10:00:01 | 已加载Schema,表数量: 2
2026-05-27 10:00:01 | Agent ops-agent 启动,检查间隔=60s
2026-05-27 10:00:01 | [总线] user → query-agent: query_request
2026-05-27 10:00:02 | LLM生成SQL: SELECT name, stock FROM products WHERE stock < 50
2026-05-27 10:00:02 | SQL执行成功: 1行, 12ms
2026-05-27 10:00:02 | [总线] query-agent → user: query_result
2026-05-27 10:00:02 | 查询结果: 库存低于50的产品 = 智能网关E100(50) ...

7.3 扩展:Web界面访问


如果需要Web界面,添加一个FastAPI server:


# web_server.py (简化版)
from fastapi import FastAPI, Query
from pydantic import BaseModel
import asyncio
from message_bus import MCPMessageBus
from message import MCPMessage, MessageType

app = FastAPI()
bus = MCPMessageBus()
results_cache = {}  # 别这样做,用真正的缓存

class Question(BaseModel):
    question: str

result_event = asyncio.Event()
latest_result = {}

@app.post("/query")
async def ask(query: Question):
    # 发布查询请求
    msg = MCPMessage(
        type=MessageType.QUERY_REQUEST,
        source="web",
        target="query-agent",
        payload={"question": query.question}
    )
    await bus.publish(msg)
    # 实际项目用WebSocket
    return {"status": "submitted", "question": query.question}

@app.get("/health")
async def health():
    return {"status": "running"}

---


8. 调优与替换注意事项 {#8}


8.1 模型选择


| 模型 | 适合场景 | 成本 |

|---|---|---|

| gpt-4o-mini | SQL生成(足够好) | 低 |

| DeepSeek-Chat | 中文场景更佳 | 极低 |

| Qwen2.5-72B | 复杂SQL,多表JOIN | 中 |

| 本地7B模型 | 数据不出内网 | 硬件成本 |


实测经验:SQL生成不是越大的模型越好。7B模型在简单单表查询上表现不输gpt-4,但在复杂JOIN和子查询上有明显差距。


8.2 安全注意事项


# 安全防护(必须做)
BLOCKED_KEYWORDS = ["DROP", "DELETE", "UPDATE", "INSERT", "ALTER", "TRUNCATE", "EXEC"]

def validate_sql(sql: str) -> bool:
    """检查生成的SQL是否安全"""
    upper = sql.upper().strip()
    # 只允许SELECT
    if not upper.startswith("SELECT"):
        return False
    # 检查危险关键字
    for kw in BLOCKED_KEYWORDS:
        if kw in upper.split():
            return False
    return True

8.3 数据库兼容


代码基于SQLite开发,切换到PostgreSQL只需改连接串:


# PostgreSQL
DB_CONNECTION = "postgresql://user:password@host:5432/dbname"
# MySQL
DB_CONNECTION = "mysql+pymysql://user:password@host:3306/dbname"

注意:不同数据库的SQL方言不同,生成的SQL不一定兼容。建议在System Prompt里明确指定数据库类型。


8.4 性能调优


| 优化点 | 方法 | 效果 |

|---|---|---|

| Schema缓存 | 只启动时加载一次 | 减少API调用 |

| 连接池 | SQLAlchemy默认有连接池 | 减少连接开销 |

| 结果缓存 | 相同问题在短时间内缓存 | 减少重复查询 |

| 并发控制 | 用asyncio.Lock控制 | 防止SQL注入并发 |


---


9. 常见问题 {#9}


Q: LLM生成的SQL不对怎么办?


A: 有三种情况:

  1. Schema描述不全 → 检查schema_loader的输出,确保字段类型正确
  2. 中文理解偏差 → 换用更好的中英文模型(DeepSeek)
  3. SQL正确但报错 → 让Agent捕获错误,重新发给LLM修正(重试模式)

建议加一个重试回路:


MAX_RETRIES = 2
for attempt in range(MAX_RETRIES):
    sql = await generate_sql(question + (f"\n上次错误: {error}" if error else ""))
    result = execute(sql)
    if not result.get("error"):
        break
    error = result["error"]

Q: Agent的处理速度太慢怎么办?


每次LLM调用大约1-3秒。如果用户并发多,加一层响应缓存:


from functools import lru_cache
from hashlib import md5

@lru_cache(maxsize=128)
def cached_query(question_hash: str):
    pass  # 用问题hash做缓存键

Q: 运维Agent自动执行修复操作风险太大?


可以把修复模式改成"建议模式":


# ops_agent.py 修改
class OpsAgent(BaseAgent):
    def __init__(self, *, auto_fix: bool = False, ...):
        self.auto_fix = auto_fix
    
    async def _decide_fix(self, result):
        if self.auto_fix:
            await self._apply_fix(...)
        else:
            await self.send(MessageType.FIX_REQUEST, "user", {
                "suggestion": fix_action,
                "health_data": result
            })

这样运维Agent只发告警和建议,人工确认后才执行。


Q: 这个方案能上生产吗?


能,但需要补充:

  1. 持久化消息队列(Redis/NSQ替代内存)
  2. 认证授权(同一套MCP协议加token验证)
  3. 更完善的错误恢复(Agent崩溃后自动重启)
  4. WebSocket实时推送查询结果

当前的实现适合:小团队内部工具、POC验证、个人项目。


---


总结


这套方案的要点:

  1. MCP协议解耦两个Agent,比直接函数调用更灵活
  2. Schema自动提取让LLM生成准确SQL
  3. 查询+运维分工,各自做好自己的事
  4. 300行代码落地一个可运行的Demo

不管你是做数据分析工具还是数据库运维平台,这个模式都可以直接拿过去改。需要改的地方:数据库Schema、监控指标、告警策略。


---


本文所有代码在 GitHub Gist 有完整版(搜索"dual-agent-mcp-db")。如果项目中有类似场景,欢迎交流。