AI Agent 开发实战(7):如何接入搜索和数据库工具?

文章目录

✍创作者:全栈弄潮儿

🏡 个人主页:全栈弄潮儿的个人主页

🏙️ 个人社区,欢迎你的加入:全栈开发社区

📙 专栏:AI Agent 开发实战:从 0 到生产级智能体

这是《AI Agent 开发实战:从 0 到生产级智能体》的第 7 篇。

上一篇我们开发了真实的天气查询工具,让 Agent 能够接入真实的 API。但一个生产级的 Agent 通常需要接入多种工具------网页搜索、数据库查询、文件操作、第三方服务等。

本篇我们要接入两类关键工具:网页搜索数据库查询。这两类工具让 Agent 能够获取实时信息和业务数据,是构建实用 Agent 的基础。

完成后,你会得到一个能够搜索网页并查询业务数据的工具型 Agent,它能够:

text 复制代码
用户提出信息查询需求
    ↓
Agent 判断使用哪种工具
    ↓
调用搜索工具或数据库查询工具
    ↓
处理查询结果并生成回答

一、本篇目标

完成下面 5 件事:

  1. 接入网页搜索工具,让 Agent 能够搜索实时信息。
  2. 接入数据库查询工具,让 Agent 能够查询业务数据。
  3. 理解工具权限边界,区分只读查询与写入操作。
  4. 实现 SQL 注入防护,防止模型生成危险 SQL。
  5. 构建一个多工具协作的工具型 Agent。

本篇的最终验收标准是:

text 复制代码
[ ] 网页搜索工具能够返回实时搜索结果。
[ ] 数据库查询工具能够查询业务数据(如用户信息、订单数据)。
[ ] 工具权限边界清晰,只读操作与写入操作严格区分。
[ ] SQL 注入防护有效,能够拦截危险 SQL。
[ ] 完成"工具型 Agent V1"的全部功能验收。

二、核心概念:多工具协作与权限边界

1. 多工具协作的挑战

当 Agent 拥有多个工具时,会面临几个挑战:

  • 工具选择:Agent 需要判断当前任务应该使用哪个工具。
  • 工具组合:某些任务可能需要组合多个工具(如先搜索再查询数据库)。
  • 权限控制:不同工具的权限不同,需要严格控制。

2. 工具权限边界

工具权限边界是 Agent 安全的核心。我们需要区分两类操作:

只读操作(Read)

  • 查询数据(如搜索、数据库查询)
  • 读取文件
  • 获取信息

只读操作通常是安全的,不会改变系统状态。

写入操作(Write)

  • 修改数据(如更新数据库)
  • 创建文件
  • 删除数据
  • 发送邮件

写入操作会改变系统状态,需要更严格的权限控制。

3. SQL 注入防护

当 Agent 能够生成 SQL 查询时,必须防止 SQL 注入攻击。常见的防护措施:

  • 参数化查询:使用参数化 SQL,不直接拼接用户输入。
  • 输入校验:检查用户输入是否包含危险字符。
  • 权限最小化:数据库账号只授予必要的权限(如只读权限)。
  • 白名单机制:只允许查询特定的表和字段。

三、深入理解:工具开发的关键细节

1. 网页搜索工具的设计

网页搜索工具需要:

  • 搜索 API:接入搜索引擎 API(如 Google Custom Search、Bing Search API)。
  • 结果过滤:过滤掉不相关的结果,提取关键信息。
  • 结果摘要:将搜索结果转换成人类可读的摘要。

2. 数据库查询工具的设计

数据库查询工具需要:

  • 数据库连接:建立与数据库的连接。
  • SQL 生成:根据用户需求生成 SQL 查询。
  • 结果格式化:将查询结果转换成结构化数据。
  • 安全防护:防止 SQL 注入和越权访问。

3. 工具权限控制

工具权限控制需要:

  • 权限分级:区分只读和写入权限。
  • 操作审计:记录所有工具调用,便于追溯。
  • 异常处理:处理权限不足、操作失败等异常。

四、案例分析:常见的工具开发失败

1. 案例 1:SQL 注入攻击

问题:用户输入恶意 SQL,导致数据泄露:

python 复制代码
def query_database(sql: str) -> str:
    cursor.execute(sql)  # 直接执行用户输入的 SQL
    return cursor.fetchall()

当用户输入 SELECT * FROM users; DROP TABLE users; 时,数据库被删除。

原因:没有做 SQL 注入防护,直接执行用户输入的 SQL。

解决

python 复制代码
def query_database(query: str) -> str:
    # 只允许 SELECT 查询
    if not query.strip().upper().startswith("SELECT"):
        return json.dumps({"error": "只允许查询操作"})
    
    # 使用参数化查询
    cursor.execute(query, params)
    return cursor.fetchall()

2. 案例 2:工具权限越界

问题:Agent 能够执行写入操作,导致数据被意外修改:

python 复制代码
TOOLS = [
    {"name": "query_database", "description": "查询数据库"},
    {"name": "update_database", "description": "更新数据库"},  # 不应该暴露给 Agent
]

原因:工具权限控制不严格,Agent 能够调用写入工具。

解决

python 复制代码
# 只暴露只读工具
TOOLS = [
    {"name": "query_database", "description": "查询数据库"},
    # 不暴露写入工具
]

3. 案例 3:搜索结果未过滤

问题:搜索结果包含大量无关信息,模型无法理解:

python 复制代码
def search_web(query: str) -> str:
    results = search_api.search(query)
    return json.dumps(results)  # 返回所有结果

原因:没有过滤和摘要搜索结果,返回了过多无关信息。

解决

python 复制代码
def search_web(query: str) -> str:
    results = search_api.search(query)
    
    # 过滤和摘要
    filtered = []
    for r in results[:5]:  # 只取前 5 条
        filtered.append({
            "title": r.title,
            "snippet": r.snippet[:200],  # 截取摘要
            "url": r.url
        })
    
    return json.dumps(filtered, ensure_ascii=False)

五、项目需求:接入搜索和数据库工具

为了让 Agent 能够搜索网页并查询业务数据,我们需要:

  • 接入网页搜索 API(如 DuckDuckGo Search API)。
  • 接入数据库(如 SQLite),创建示例业务数据。
  • 实现 SQL 注入防护。
  • 区分只读查询与写入操作。

暂时不做:

  • 不实现数据库写入操作(只实现只读查询)。
  • 不实现复杂的权限管理系统。
  • 不实现搜索结果的高级过滤。

这几个限制很重要。我们要先验证"多工具协作 + 权限控制"的基本流程能否稳定工作,再引入更复杂的功能。

六、准备开发环境

1. 安装依赖

requirements.txt 中添加依赖:

text 复制代码
openai>=1.0.0
python-dotenv>=1.0.0
requests>=2.31.0
duckduckgo-search>=4.0.0

安装依赖:

bash 复制代码
pip install -r requirements.txt

2. 创建示例数据库

创建 init_db.py,初始化示例数据库:

python 复制代码
import sqlite3

def init_database():
    """初始化示例数据库"""
    conn = sqlite3.connect("business.db")
    cursor = conn.cursor()
    
    # 创建用户表
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS users (
            id INTEGER PRIMARY KEY,
            name TEXT NOT NULL,
            email TEXT NOT NULL,
            department TEXT NOT NULL,
            salary REAL NOT NULL
        )
    """)
    
    # 创建订单表
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS orders (
            id INTEGER PRIMARY KEY,
            user_id INTEGER NOT NULL,
            product TEXT NOT NULL,
            amount REAL NOT NULL,
            status TEXT NOT NULL,
            created_at TEXT NOT NULL
        )
    """)
    
    # 插入示例数据
    cursor.executemany("INSERT INTO users VALUES (?, ?, ?, ?, ?)", [
        (1, "张三", "zhangsan@example.com", "技术部", 15000),
        (2, "李四", "lisi@example.com", "产品部", 12000),
        (3, "王五", "wangwu@example.com", "市场部", 13000),
        (4, "赵六", "zhaoliu@example.com", "技术部", 18000),
        (5, "钱七", "qianqi@example.com", "产品部", 14000),
    ])
    
    cursor.executemany("INSERT INTO orders VALUES (?, ?, ?, ?, ?, ?)", [
        (1, 1, "笔记本电脑", 8000, "已完成", "2024-01-15"),
        (2, 2, "显示器", 2000, "已完成", "2024-01-20"),
        (3, 3, "键盘", 500, "待发货", "2024-02-01"),
        (4, 1, "鼠标", 200, "已完成", "2024-02-10"),
        (5, 4, "耳机", 800, "已取消", "2024-02-15"),
    ])
    
    conn.commit()
    conn.close()
    print("数据库初始化完成")

if __name__ == "__main__":
    init_database()

运行初始化脚本:

bash 复制代码
python init_db.py

七、实现多工具 Agent

创建 tool_agent.py

python 复制代码
from __future__ import annotations

import json
import sqlite3
from typing import Any, Callable

from duckduckgo_search import DDGS
from openai import OpenAI


SYSTEM_PROMPT = """你是 WorkMate,一个专业的工具型助手。

你可以使用以下工具来帮助用户:
1. 网页搜索工具:搜索实时信息
2. 数据库查询工具:查询业务数据(用户信息、订单数据)

当用户提出需求时,你应该:
- 判断应该使用哪个工具
- 调用相应的工具
- 基于工具返回的结果,生成友好的回答

注意:
- 如果用户询问实时信息(如新闻、天气),使用网页搜索工具
- 如果用户询问业务数据(如用户信息、订单),使用数据库查询工具
- 如果工具返回错误,友好地告知用户并说明原因
- 回答要简洁明了,包含关键信息"""


# 工具定义
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "search_web",
            "description": "搜索网页,获取实时信息,如新闻、知识、技术等",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "搜索关键词,如'Python 教程'、'AI 最新进展'"
                    },
                    "max_results": {
                        "type": "integer",
                        "description": "返回结果数量,默认为 5",
                        "default": 5
                    }
                },
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "query_database",
            "description": "查询业务数据库,获取用户信息、订单数据等",
            "parameters": {
                "type": "object",
                "properties": {
                    "sql": {
                        "type": "string",
                        "description": "SQL 查询语句,只允许 SELECT 查询,如'SELECT * FROM users WHERE department=\"技术部\"'"
                    }
                },
                "required": ["sql"]
            }
        }
    }
]


class WebSearchTool:
    """网页搜索工具"""

    def search(self, query: str, max_results: int = 5) -> str:
        """搜索网页"""
        try:
            with DDGS() as ddgs:
                results = list(ddgs.text(query, max_results=max_results))
                
                # 过滤和摘要
                filtered = []
                for r in results:
                    filtered.append({
                        "title": r.get("title", ""),
                        "snippet": r.get("body", "")[:200],
                        "url": r.get("href", "")
                    })
                
                return json.dumps(filtered, ensure_ascii=False)
        
        except Exception as e:
            return json.dumps({"error": f"搜索失败:{e}"})


class DatabaseTool:
    """数据库查询工具"""

    def __init__(self, db_path: str = "business.db"):
        self.db_path = db_path

    def query(self, sql: str) -> str:
        """查询数据库"""
        # SQL 注入防护:只允许 SELECT 查询
        sql_upper = sql.strip().upper()
        if not sql_upper.startswith("SELECT"):
            return json.dumps({"error": "只允许查询操作,不允许修改数据"})
        
        # 检查危险关键词
        dangerous_keywords = ["DROP", "DELETE", "UPDATE", "INSERT", "ALTER", "TRUNCATE"]
        for keyword in dangerous_keywords:
            if keyword in sql_upper:
                return json.dumps({"error": f"检测到危险操作:{keyword}"})
        
        try:
            conn = sqlite3.connect(self.db_path)
            cursor = conn.cursor()
            
            cursor.execute(sql)
            rows = cursor.fetchall()
            
            # 获取列名
            columns = [desc[0] for desc in cursor.description]
            
            # 转换成字典列表
            results = []
            for row in rows:
                results.append(dict(zip(columns, row)))
            
            conn.close()
            
            return json.dumps({
                "columns": columns,
                "data": results,
                "count": len(results)
            }, ensure_ascii=False)
        
        except sqlite3.Error as e:
            return json.dumps({"error": f"数据库查询失败:{e}"})
        except Exception as e:
            return json.dumps({"error": f"查询失败:{e}"})


# 工具实例
web_search_tool = WebSearchTool()
database_tool = DatabaseTool()


def search_web_wrapper(args: dict) -> str:
    """网页搜索工具的包装函数"""
    query = args.get("query", "")
    max_results = args.get("max_results", 5)
    return web_search_tool.search(query, max_results)


def query_database_wrapper(args: dict) -> str:
    """数据库查询工具的包装函数"""
    sql = args.get("sql", "")
    return database_tool.query(sql)


TOOL_REGISTRY: dict[str, Callable] = {
    "search_web": search_web_wrapper,
    "query_database": query_database_wrapper,
}


class Agent:
    """工具型 Agent"""

    def __init__(
        self,
        client: OpenAI,
        model: str,
    ) -> None:
        self.client = client
        self.model = model
        self.messages: list[dict[str, Any]] = [
            {"role": "system", "content": SYSTEM_PROMPT}
        ]

    def reset(self) -> None:
        """清空当前会话,但保留系统指令。"""
        self.messages = [{"role": "system", "content": SYSTEM_PROMPT}]

    def run(self, user_input: str) -> str:
        """执行一次对话,包含工具调用循环"""
        self.messages.append({"role": "user", "content": user_input})

        while True:
            # 调用模型
            response = self.client.chat.completions.create(
                model=self.model,
                messages=self.messages,
                tools=TOOLS,
                tool_choice="auto",
            )

            message = response.choices[0].message

            # 如果没有工具调用,直接返回
            if not message.tool_calls:
                self.messages.append({"role": "assistant", "content": message.content})
                return message.content

            # 有工具调用,处理每个调用
            self.messages.append({
                "role": "assistant",
                "content": message.content,
                "tool_calls": [
                    {
                        "id": tc.id,
                        "type": "function",
                        "function": {
                            "name": tc.function.name,
                            "arguments": tc.function.arguments
                        }
                    }
                    for tc in message.tool_calls
                ]
            })

            # 执行每个工具调用
            for tool_call in message.tool_calls:
                tool_name = tool_call.function.name
                tool_args = json.loads(tool_call.function.arguments)

                # 执行工具
                if tool_name in TOOL_REGISTRY:
                    try:
                        result = TOOL_REGISTRY[tool_name](tool_args)
                    except Exception as e:
                        result = json.dumps({"error": f"工具执行失败:{e}"})
                else:
                    result = json.dumps({"error": f"未知工具:{tool_name}"})

                # 把工具结果加入消息历史
                self.messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": result
                })

            # 继续循环,让模型根据工具结果生成最终回答


def build_agent() -> Agent:
    """构建 Agent"""
    import os
    from dotenv import load_dotenv
    
    load_dotenv()
    
    # 初始化 OpenAI 客户端
    api_key = os.getenv("OPENAI_API_KEY")
    if not api_key or api_key == "replace-with-your-key":
        raise RuntimeError(
            "没有找到有效的 OPENAI_API_KEY,请先在 .env 中配置 API Key。"
        )
    
    client_options: dict[str, Any] = {"api_key": api_key}
    base_url = os.getenv("OPENAI_BASE_URL")
    if base_url:
        client_options["base_url"] = base_url
    
    client = OpenAI(**client_options)
    model = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
    
    return Agent(client=client, model=model)


def main() -> None:
    try:
        agent = build_agent()
    except Exception as exc:
        print(f"启动失败:{exc}")
        return
    
    print("工具型 Agent 已启动。")
    print("输入 /reset 清空上下文,输入 /exit 退出。")
    
    while True:
        try:
            user_input = input("\n你:").strip()
        except (EOFError, KeyboardInterrupt):
            print("\nAgent 已退出。")
            return
        
        if not user_input:
            continue
        if user_input == "/exit":
            print("Agent 已退出。")
            return
        if user_input == "/reset":
            agent.reset()
            print("会话上下文已清空。")
            continue
        
        try:
            print(f"\nAgent:{agent.run(user_input)}")
        except Exception as exc:
            print(f"调用失败:{exc}")


if __name__ == "__main__":
    main()

这段代码真正做了什么

代码实现了多工具 Agent 的核心能力:

  1. WebSearchTool:封装了网页搜索功能,使用 DuckDuckGo Search API。
  2. DatabaseTool:封装了数据库查询功能,包含 SQL 注入防护。
  3. SQL 注入防护:只允许 SELECT 查询,检查危险关键词。
  4. Agent:复用第5篇的框架,集成多个工具。

Agent 现在能够根据用户需求,自动选择合适的工具(搜索或数据库查询),并处理查询结果。

八、运行效果

启动程序:

bash 复制代码
python tool_agent.py

一次可能的运行过程如下:

text 复制代码
工具型 Agent 已启动。
输入 /reset 清空上下文,输入 /exit 退出。

你:技术部有哪些员工

Agent:技术部有以下员工:
1. 张三,邮箱:zhangsan@example.com,薪资:15000 元
2. 赵六,邮箱:zhaoliu@example.com,薪资:18000 元

你:查一下最近的订单情况

Agent:最近的订单情况如下:
1. 订单 #1:用户 ID 1,产品:笔记本电脑,金额:8000 元,状态:已完成
2. 订单 #2:用户 ID 2,产品:显示器,金额:2000 元,状态:已完成
3. 订单 #3:用户 ID 3,产品:键盘,金额:500 元,状态:待发货
4. 订单 #4:用户 ID 1,产品:鼠标,金额:200 元,状态:已完成
5. 订单 #5:用户 ID 4,产品:耳机,金额:800 元,状态:已取消

你:搜索一下 Python 的最新版本

Agent:根据搜索结果,Python 的最新版本信息如下:
1. Python 3.12  released on October 2, 2023
2. Python 3.11 released on October 24, 2022
3. Python 3.10 released on October 4, 2021

你:删除所有用户数据

Agent:抱歉,我只允许查询操作,不允许修改数据。

你:/reset
会话上下文已清空。

这里要重点观察几个现象:

  1. Agent 能够自动判断使用哪个工具(数据库查询 vs 网页搜索)。
  2. 数据库查询工具能够正确返回业务数据。
  3. 网页搜索工具能够返回实时信息。
  4. 当用户尝试执行危险操作(如删除数据)时,工具能够拦截并返回错误。

九、常见问题与排错

1. 数据库查询失败

检查以下几点:

  • 数据库文件 business.db 是否存在。
  • SQL 语句是否正确。
  • 是否包含危险关键词(如 DROP、DELETE)。

2. 网页搜索失败

可能的原因:

  • 网络连接不稳定。
  • DuckDuckGo Search API 限流。
  • 搜索关键词过于复杂。

解决方案:

  • 检查网络连接。
  • 减少搜索频率。
  • 简化搜索关键词。

3. Agent 选择了错误的工具

可能的原因:

  • 工具描述不够清晰。
  • 用户需求模糊。

解决方案:

  • 优化工具描述,让 Agent 更容易理解工具的用途。
  • 引导用户明确需求。

十、工程化改进:现在还不能上线

这个工具型 Agent 能够工作,不代表它已经是生产系统。至少还存在以下问题:

问题 当前实现 后续方向
数据库连接 每次查询都新建连接 使用连接池
权限控制 简单的关键词检查 完整的权限管理系统
查询优化 没有查询优化 添加查询缓存和索引
错误恢复 失败直接返回错误 增加自动重试机制
监控 没有调用日志 增加调用日志和监控

这里有一个需要提前建立的工程判断:多工具协作是 Agent 从"单功能"到"多功能"的关键一步。

下一篇我们会学习如何构建可复用的工具库,让工具管理更加规范。

十一、本篇小结

今天完成的不是一个简单的工具接入,而是多工具协作的完整流程:

text 复制代码
工具选择
    ↓
权限控制
    ↓
工具调用
    ↓
结果处理

请记住这三个结论:

  1. 多工具协作需要考虑工具选择、权限控制和结果处理,不能只关注单个工具。
  2. 工具权限边界是 Agent 安全的核心,必须严格区分只读操作与写入操作。
  3. SQL 注入防护是数据库查询工具的必备功能,必须在本阶段掌握。

十二、课后练习

请在不改变 Agent 核心结构的前提下,完成下面练习:

练习 1:增加文件查询工具

接入文件查询工具,让 Agent 能够查询本地文件内容。

练习 2:实现查询结果缓存

对于相同的数据库查询,在 10 分钟内直接返回缓存结果。

练习 3:增加查询权限控制

实现更细粒度的权限控制,如只允许查询特定的表和字段。

阶段验收

当你能够解释多工具协作的完整流程,并且可以独立接入其他工具(如邮件查询、日历查询)时,第二阶段第三篇验收通过:

完成"工具型 Agent V1",支持多工具协作、权限控制和 SQL 注入防护。

接下来,进入第二阶段的第四篇:

Day 8:打造可靠的工具调用 Agent


✍坚持原创,求关注,点赞,收藏

相关推荐
tachibana21 小时前
什么是 Function Calling ?
数据库·人工智能·ai·llm·agent
砚底藏山河1 小时前
并发与限频工程:把20只的2秒压到0.5秒不封号(魔码量化实战 #03)
java·开发语言·数据库·python·金融
prog_61031 小时前
【笔记】用cursor手搓cursor(十)
人工智能·笔记·大语言模型·agent
沙淘金数据服务2 小时前
电商订单数据整合难题拆解:多源异构数据如何实现自动化治理?
大数据·人工智能·数据治理·数据清洗·电商·外贸
万岳科技系统开发2 小时前
AI数字人直播系统源码搭建指南:数字人、语音与直播如何实现
人工智能
论文复现现场2 小时前
本地 PyTorch 训练 OOM,第一次租 RTX 4090 云 GPU 怎么迁移项目?从环境检查到 100 Step 跑通
人工智能·pytorch·python·深度学习·cuda
云雀衔光2 小时前
MCP 协议全景:为什么它是 AI 连接工具的「USB-C」
java·开发语言·数据库·人工智能·ai编程
kyle~2 小时前
ISP--- RAW 图像 从传感器噪声模型到快门时序与频闪效应
人工智能·计算机视觉·接口隔离原则
外域速览2 小时前
AI开始训练AI:黄仁勋喊AGI已到
大数据·人工智能·agi