文章目录
-
- 一、本篇目标
- 二、核心概念:多工具协作与权限边界
-
- [1. 多工具协作的挑战](#1. 多工具协作的挑战)
- [2. 工具权限边界](#2. 工具权限边界)
- [3. SQL 注入防护](#3. SQL 注入防护)
- 三、深入理解:工具开发的关键细节
-
- [1. 网页搜索工具的设计](#1. 网页搜索工具的设计)
- [2. 数据库查询工具的设计](#2. 数据库查询工具的设计)
- [3. 工具权限控制](#3. 工具权限控制)
- 四、案例分析:常见的工具开发失败
-
- [1. 案例 1:SQL 注入攻击](#1. 案例 1:SQL 注入攻击)
- [2. 案例 2:工具权限越界](#2. 案例 2:工具权限越界)
- [3. 案例 3:搜索结果未过滤](#3. 案例 3:搜索结果未过滤)
- 五、项目需求:接入搜索和数据库工具
- 六、准备开发环境
-
- [1. 安装依赖](#1. 安装依赖)
- [2. 创建示例数据库](#2. 创建示例数据库)
- [七、实现多工具 Agent](#七、实现多工具 Agent)
- 八、运行效果
- 九、常见问题与排错
-
- [1. 数据库查询失败](#1. 数据库查询失败)
- [2. 网页搜索失败](#2. 网页搜索失败)
- [3. Agent 选择了错误的工具](#3. Agent 选择了错误的工具)
- 十、工程化改进:现在还不能上线
- 十一、本篇小结
- 十二、课后练习
-
- [练习 1:增加文件查询工具](#练习 1:增加文件查询工具)
- [练习 2:实现查询结果缓存](#练习 2:实现查询结果缓存)
- [练习 3:增加查询权限控制](#练习 3:增加查询权限控制)
- 阶段验收
✍创作者:全栈弄潮儿
🏡 个人主页:全栈弄潮儿的个人主页
🏙️ 个人社区,欢迎你的加入:全栈开发社区

这是《AI Agent 开发实战:从 0 到生产级智能体》的第 7 篇。
上一篇我们开发了真实的天气查询工具,让 Agent 能够接入真实的 API。但一个生产级的 Agent 通常需要接入多种工具------网页搜索、数据库查询、文件操作、第三方服务等。
本篇我们要接入两类关键工具:网页搜索 和数据库查询。这两类工具让 Agent 能够获取实时信息和业务数据,是构建实用 Agent 的基础。
完成后,你会得到一个能够搜索网页并查询业务数据的工具型 Agent,它能够:
text
用户提出信息查询需求
↓
Agent 判断使用哪种工具
↓
调用搜索工具或数据库查询工具
↓
处理查询结果并生成回答
一、本篇目标
完成下面 5 件事:
- 接入网页搜索工具,让 Agent 能够搜索实时信息。
- 接入数据库查询工具,让 Agent 能够查询业务数据。
- 理解工具权限边界,区分只读查询与写入操作。
- 实现 SQL 注入防护,防止模型生成危险 SQL。
- 构建一个多工具协作的工具型 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 的核心能力:
WebSearchTool:封装了网页搜索功能,使用 DuckDuckGo Search API。DatabaseTool:封装了数据库查询功能,包含 SQL 注入防护。- SQL 注入防护:只允许 SELECT 查询,检查危险关键词。
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
会话上下文已清空。
这里要重点观察几个现象:
- Agent 能够自动判断使用哪个工具(数据库查询 vs 网页搜索)。
- 数据库查询工具能够正确返回业务数据。
- 网页搜索工具能够返回实时信息。
- 当用户尝试执行危险操作(如删除数据)时,工具能够拦截并返回错误。
九、常见问题与排错
1. 数据库查询失败
检查以下几点:
- 数据库文件
business.db是否存在。 - SQL 语句是否正确。
- 是否包含危险关键词(如 DROP、DELETE)。
2. 网页搜索失败
可能的原因:
- 网络连接不稳定。
- DuckDuckGo Search API 限流。
- 搜索关键词过于复杂。
解决方案:
- 检查网络连接。
- 减少搜索频率。
- 简化搜索关键词。
3. Agent 选择了错误的工具
可能的原因:
- 工具描述不够清晰。
- 用户需求模糊。
解决方案:
- 优化工具描述,让 Agent 更容易理解工具的用途。
- 引导用户明确需求。
十、工程化改进:现在还不能上线
这个工具型 Agent 能够工作,不代表它已经是生产系统。至少还存在以下问题:
| 问题 | 当前实现 | 后续方向 |
|---|---|---|
| 数据库连接 | 每次查询都新建连接 | 使用连接池 |
| 权限控制 | 简单的关键词检查 | 完整的权限管理系统 |
| 查询优化 | 没有查询优化 | 添加查询缓存和索引 |
| 错误恢复 | 失败直接返回错误 | 增加自动重试机制 |
| 监控 | 没有调用日志 | 增加调用日志和监控 |
这里有一个需要提前建立的工程判断:多工具协作是 Agent 从"单功能"到"多功能"的关键一步。
下一篇我们会学习如何构建可复用的工具库,让工具管理更加规范。
十一、本篇小结
今天完成的不是一个简单的工具接入,而是多工具协作的完整流程:
text
工具选择
↓
权限控制
↓
工具调用
↓
结果处理
请记住这三个结论:
- 多工具协作需要考虑工具选择、权限控制和结果处理,不能只关注单个工具。
- 工具权限边界是 Agent 安全的核心,必须严格区分只读操作与写入操作。
- SQL 注入防护是数据库查询工具的必备功能,必须在本阶段掌握。
十二、课后练习
请在不改变 Agent 核心结构的前提下,完成下面练习:
练习 1:增加文件查询工具
接入文件查询工具,让 Agent 能够查询本地文件内容。
练习 2:实现查询结果缓存
对于相同的数据库查询,在 10 分钟内直接返回缓存结果。
练习 3:增加查询权限控制
实现更细粒度的权限控制,如只允许查询特定的表和字段。
阶段验收
当你能够解释多工具协作的完整流程,并且可以独立接入其他工具(如邮件查询、日历查询)时,第二阶段第三篇验收通过:
完成"工具型 Agent V1",支持多工具协作、权限控制和 SQL 注入防护。
接下来,进入第二阶段的第四篇:
Day 8:打造可靠的工具调用 Agent
✍坚持原创,求关注,点赞,收藏