上一关我们完整落地了工业级网络搜索工具,让 Agent 具备了联网实时检索、故障熔断、防过度调用的成熟能力,补齐了智能体获取外部实时信息的核心短板。至此,Stage2 基础工具链路已经跑通。
接下来,我们希望给 Agent 装上"笔"、"数据库钥匙"和"独立大脑",也就是 File I/O(文件读写)、Text-to-SQL(数据库查询)、和 Code Execution(代码执行器)。但是,当前 Agent 代码架构存在致命工程隐患:所有工具调度、异常捕获、权限判断逻辑全部堆砌在主循环函数中,代码臃肿耦合、扩展性极差,并不容易扩展工具。
为了避免老旧架构彻底失控,出现逻辑混乱、报错难排查、新增工具成本极高的问题,本篇将着眼调整旧结构。本篇不新增任何业务功能,只完成底层架构解耦重构,搭建标准化工具注册中心,统一工具注册、安全校验、异常捕获、执行调度规范,同时优化 RAG 知识库懒加载机制,为后续所有高阶工具的低成本、规范化接入打下基础。
因此,我们在这第 2.3 关的计划如下:
| 阶段 | 内容 | 难度 | 耗时 | 核心目标 |
|---|---|---|---|---|
| 阶段 0 | 工具注册中心架构重构(前置必备) | 低 | 1h | 解耦主循环与工具逻辑,统一管理所有工具 Schema、执行函数、安全校验、异常处理 |
| 阶段 1 | File I/O 文件读写工具 | 最低 | 2-3h | 实现受限目录文档写入,防御路径穿越,支持新建 / 追加、MD/TXT/PDF 导出 |
| 阶段 2 | Text-to-SQL 双工具 | 中 | 3-4h | 数据库结构动态获取、SQL 语句安全拦截、只读账号隔离、SQL 错误自动回传给模型调试 |
| 阶段 3 | Code Execution 代码沙箱执行 | 高 | 4h+ | 超时控制、AST 静态风险代码拦截、捕获标准输出 / 错误,本地测试安全方案 |
一、阶段 0:工具注册中心架构重构
1. 重构思路
- 新建
ToolRegistry注册类:统一注册所有工具,保存「函数 + 权限校验函数 + 工具名称映射」 - 抽离通用工具安全校验、异常捕获逻辑,从主循环剥离
- 统一工具返回标准格式(成功 / 错误模板),LLM 更容易解析工具结果
- 后续新增工具仅需注册函数 + 补充 tools 数组的 function schema,不用修改主循环核心逻辑
2. 具体步骤
导入新增库
python
from typing import Callable, Dict, Any, Optional
import re
from pathlib import Path
import time
解释:
-
from typing import Callable, Dict, Any, Optional:Python类型注解专用库,只给代码做类型标注,不影响运行,方便 IDE 提示、静态检查。Callable:代表可调用对象(函数、方法、lambda),用于标注变量是一个函数Dict:字典类型标注,Dict[键类型, 值类型],等价新版dictAny:任意类型,代表不限制数据类型,什么值都能存Optional:可选类型 ,等价类型 | None,表示变量可以是指定类型也可以是 None
-
import re:Python正则表达式标准库,专门做文本匹配、提取、替换、校验。- 常用场景:手机号 / 邮箱校验; 批量替换文本、提取数字、关键词过滤
-
from pathlib import Path:Python3.4+ 现代文件路径工具,替代老旧os.path,面向对象操作文件 / 文件夹。- 优势:代码更简洁、跨 Windows/macOS/Linux 自动适配斜杠
-
import time:时间处理标准库,获取时间戳、程序延时、计时。time.sleep(2):暂停 2 秒time.time():获取当前时间戳(浮点秒数,用于代码耗时统计)
新增工具注册中心
我们需要在代码的一开始新增一个类 ,即工具注册与执行的管理类用来统一管理程序里的各种工具函数,实现注册、安全校验、执行、异常捕获的一体化处理。这也是非常实用的模块化工具管理组件。
- 首先,我们在代码开头定义
ToolRegistry类,这是整个工具注册中心的核心,专门负责工具的存储、注册、查询和执行。
python
# -------------------------- 新增:工具注册中心 --------------------------
class ToolRegistry:
def __init__(self):
# key: 工具函数名,value: 执行函数 + 前置安全校验函数
self.tools_map: Dict[str, Dict[str, Any]] = {}
类的__init__初始化方法里,创建了一个空字典tools_map,这个字典是核心存储结构:键是工具的函数名称,值是另一个字典,里面存放工具的执行函数和前置安全校验函数,代码里的类型注解是为了规范数据类型,让代码更易读、更严谨。
- 接下来是
register注册方法,它的作用是把工具函数注册到中心里。
python
def register(self, func_name: str, execute_func: Callable, security_check: Optional[Callable] = None):
"""注册工具:名称、执行函数、可选前置安全校验函数"""
self.tools_map[func_name] = {
"exec": execute_func,
"security_check": security_check
}
这里需要传入三个参数:工具的名字、工具的核心执行函数、可选的前置安全校验函数(默认可以不传)。方法内部会把这两个函数打包成字典,以工具名为键,存入tools_map中,完成工具的注册。
- 然后是
get_tool查询方法,很简单,根据传入的工具名字,从tools_map里查找对应的工具信息,找到就返回工具字典,找不到就返回 None,是一个辅助查询的方法。
python
def get_tool(self, func_name: str) -> Optional[Dict[str, Any]]:
return self.tools_map.get(func_name)
- 接着是最核心的
run_tool方法,这是统一执行所有工具的入口,所有工具都通过这个方法调用。
python
def run_tool(self, func_name: str, **kwargs) -> str:
"""统一执行入口:先安全校验,再执行工具,统一捕获异常"""
tool_info = self.get_tool(func_name)
if not tool_info:
return f"【工具错误】不存在工具 {func_name}"
# 执行前置安全校验
check_func = tool_info.get("security_check")
if check_func is not None:
check_res = check_func(**kwargs)
if check_res is not None:
# 校验不通过,返回错误信息,直接终止工具执行
return f"【安全拦截】{check_res}"
# 执行工具主逻辑,统一捕获异常
try:
result = tool_info["exec"](**kwargs)
return f"【工具执行成功】\n{result}"
except Exception as e:
err_msg = str(e)
return f"【工具执行失败】错误信息:{err_msg}"
它做了四件关键的事:
第一,先通过get_tool检查工具是否存在,不存在就直接返回工具不存在的错误提示;
第二,执行前置安全校验,如果注册时传入了安全校验函数,就先运行这个函数,把调用参数传进去,如果校验函数返回了内容,就说明校验不通过,直接返回安全拦截的提示,不再执行工具;
第三,校验通过后,尝试执行工具的核心逻辑,用try包裹执行过程,避免程序崩溃;
第四,统一处理结果:执行成功就返回成功提示和结果,执行出错就捕获异常,返回失败提示和错误信息。
- 最后,全局实例化了
ToolRegistry类,创建了一个叫tool_registry的全局对象,整个程序里都可以直接用这个对象来注册新工具、运行已注册的工具,不需要重复创建实例,使用起来非常方便。
python
# 全局实例化工具注册器
tool_registry = ToolRegistry()
简单总结这个组件的价值:把分散的工具函数统一管理,强制加上安全校验,统一处理异常和返回格式,让工具的调用更安全、更规范、更易维护。
安全校验函数:
- 安全校验函数就是一个普通可调用函数,是工具真正执行之前运行的前置钩子函数。它不做业务本身,专门做参数合法性、权限、风险检查。
- 约定契约:校验函数返回
None= 校验通过直接放行;返回字符串 = 校验失败,字符串内容就是拦截提示文本。- 举例:工具是删除文件,执行函数负责真正删文件;安全校验函数就检查:目标路径是不是系统高危目录、传入路径参数是否为空、是否禁止该操作。
- 它是可选参数,不是必填。普通低风险工具可以不传,传
None即可;高危 且只看输入函数就可以判断是否违规的工具才需要绑定校验逻辑。- 正常是:先写工具业务函数,再写配套安全校验函数(普通独立函数),注册的时候把两个函数对象一起传给 register。
将原有工具函数注册到注册中心
在两个函数定义后加上注册到注册中心的代码:
python
# 将原有工具注册到注册中心(无安全前置校验,传None)
tool_registry.register("query_knowledge_base", query_knowledge_base, None)
tool_registry.register("web_search", web_search, None)
注意:即使已经有工具注册中心,仍然需要写
tools数组。因为tools数组是写给大模型看的,让大模型明白有什么工具可以调用。而ToolRegistry工具注册中心是执行层,只负责当收到模型发来工具名字时进行调用。如果没有tools数组,则大模型不知道有哪些工具,就不能指挥ToolRegistry。
3. 原有 RAG 工具流程优化
现在的写法下,程序一启动,立刻加载 BGE embedding、打开 ChromaDB。哪怕全程不调用知识库,启动就要加载模型、消耗内存。
优点是第一次调用 RAG 速度快;缺点是哪怕不用 RAG 也要承担加载开销;如果 Chroma 数据库损坏,整个 Agent 程序直接启动崩溃,而不是调用工具的时候才报错。
因此,在新的阶段开始前,我们先调整一下代码结构,将 RAG 改成懒加载。不要每次调用工具都重新初始化一遍(会极慢)。全局放变量初始为 None,进入 query_knowledge_base 函数时判断,如果实例是 None,才执行一次初始化;如果实例不是 None,则直接使用。
因此我们在全局位置定义两个初始变量,保留 persist_directory 这个字符串常量并让程序在读取其位置时采用相对该脚本的相置,其他部分全部移入query_knowledge_base 函数,并在函数中加入 global 声明。
全局位置:
python
embeddings = None
vectordb = None
SCRIPT_DIR = Path(__file__).parent.absolute()
persist_directory = str(SCRIPT_DIR / "chroma_db_wheat")
进入函数后:
python
def query_knowledge_base(query: str) -> str:
global embeddings, vectordb
if embeddings is None or vectordb is None:
if not Path(persist_directory).exists():
return "本地向量知识库不存在,请先运行 Stage_1/prepare_knowledge_base.py 生成知识库。"
print("正在连接本地向量知识库...")
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-zh-v1.5",
model_kwargs={
'device': 'cpu',
'local_files_only': True #这样就只需要用本地离线缓存模型了,节省验证时间
},
encode_kwargs={'normalize_embeddings': True}
)
vectordb = Chroma(persist_directory=persist_directory, embedding_function=embeddings)
print(f"\n[执行工具] 正在向量数据库中搜索与 '{query}' 相关的资料...")
效果:程序启动不加载向量库;第一次调用 query_knowledge_base 工具的时候才加载模型与数据库;后续重复调用复用实例,不会重复加载。
注意:在进入函数后必须先加
global声明,即global embeddings, vectordb。这个声明的作用是将函数内部赋值作用到全局变量,从而保证懒加载的成功。
二、本篇总结 & 下期预告
本篇我们完成了 Agent 核心架构的关键升级,彻底告别了杂乱的硬编码工具调度模式。通过 ToolRegistry 工具注册中心,实现了工具逻辑与主循环的完全解耦,统一了全量工具的注册、校验、执行、报错输出规范;同时完成了存量 RAG、网络搜索工具的适配迁移,优化了知识库懒加载机制,解决了程序启动冗余开销、单体代码臃肿、扩展性差等工程痛点,且全程兼容原有所有功能,做到重构无破坏性迭代。
稳固的底层架构已经搭建完成,具备了批量接入高阶工具的能力。下一关我们将基于全新的工具注册中心,落地第一个进阶业务工具------安全可控的 File I/O 文件读写工具,让 Agent 拥有自主整理、生成、导出本地文档的能力,解锁智能报告生成核心能力。