S02 工具路由分发|结构化整理教程(提出问题→分析问题→解决问题|承接S01最小Agent循环,新手友好)
前置铺垫:S01完成Agent基础闭环:用户提问→LLM→工具执行→结果回填消息→新一轮推理;S02核心目标:解耦工具与主循环、新增工具不用修改agent_loop代码、给文件操作增加安全隔离,新增工具分发层,落地4类常用工具:bash、read_file、write_file、edit_file。
一、提出问题:S01硬编码工具带来3个致命痛点
痛点1:工具变多后if/elif代码无限膨胀,维护困难
S01工具执行靠if/elif判断工具名称,只有单个计算器工具没问题;一旦扩展读写文件、shell等多个工具,run_tool函数会堆满条件分支,每新增一个工具就必须修改工具判断代码,代码臃肿难迭代。
痛点2:纯bash操作无路径管控,存在高危安全漏洞
早期全部依赖Shell命令操作文件,没有访问限制:模型可通过指令读取系统隐私文件(cat /etc/passwd、C:\Windows\system32);sed/cat遇到特殊字符容易命令报错、程序崩溃,缺少安全沙箱隔离。
痛点3:新增工具需要改动Agent主循环,破坏闭环稳定性
工具执行逻辑和agent_loop主循环代码深度绑定,想要拓展功能就要修改循环本体,S01定型的Agent闭环被频繁改动,极易引入循环逻辑bug。
项目诉求 :搭建分层架构,做到:主循环代码永久固定不动;新增工具仅需注册对应函数;限制文件只能在指定目录访问。
二、分析问题:根源拆解+核心概念通俗解析
1、问题根源
S01没有分层设计,工具路由分发逻辑与Agent主循环代码耦合,缺少独立中间调度层,工具的新增、修改会连带改动主流程代码。
2、标准分层架构(S02设计核心)
用户输入 → LLM(根据TOOLS工具说明书选择工具+生成参数) → 【Dispatch工具分发层(字典映射)】 → 独立工具处理函数 → tool_result结果 → 写入messages → 下一轮LLM
S02黄金准则:新增一个工具,只新增1个handler函数 + 补充工具schema,Agent主循环一行代码不用改。
3、2个必背核心名词
- TOOL_HANDLERS(分发映射字典) :代码路由表,
key=工具名称(和TOOLS内name严格一致)、value=工具执行函数,用字典查表彻底替换if/elif判断; - safe_path(路径沙箱) :安全校验函数,强制所有文件操作路径必须落在自定义工作目录
WORKDIR内部,路径越界直接抛出异常,杜绝跨目录窃取文件。
4、S01 vs S02 组件变更对照表
| 组件 | S01(旧版硬编码) | S02(新版分发架构) |
|---|---|---|
| 可用工具 | 单一工具(计算器/bash) | 4个标准化工具:bash/read_file/write_file/edit_file |
| 工具分发 | if/elif逐个判断 | TOOL_HANDLERS字典查表分发 |
| 文件安全 | 无任何路径限制 | safe_path沙箱路径校验 |
| Agent主循环 | 循环代码 | 完全复用S01,零修改 |
5、进阶隐藏问题:原生messages不能直接发给API
项目内部messages会附带自定义元数据(_internal/_timestamp/_source),直接传给DeepSeek接口会返回400报错;
API强制3条协议规则:
① 每一条tool_use调用,必须配套一条tool_result结果(通过tool_use_id绑定);
② user/assistant消息必须交替出现,不能连续同角色;
③ 只保留接口协议规定字段,剔除项目内部自定义字段。
→ 解决方案:新增normalize_messages消息规范化函数,发送API前清洗消息。
三、解决问题:分模块落地代码(兼容DeepSeek API,完全沿用S01主循环)
模块1:定义工作目录+safe_path安全沙箱(解决文件越权访问)
python
from pathlib import Path
# 全局限定:所有文件只能操作agent_workspace文件夹
WORKDIR = Path("./agent_workspace")
WORKDIR.mkdir(exist_ok=True)
def safe_path(p: str) -> Path:
full_path = (WORKDIR / p).resolve()
# 路径不在工作目录内,直接禁止访问
if not full_path.is_relative_to(WORKDIR):
raise ValueError(f"非法路径:{p},禁止访问工作目录外文件")
return full_path
模块2:编写4个独立工具Handler(read/write/edit/bash)
每个工具单独封装逻辑,所有文件操作强制先走safe_path校验路径:
python
import subprocess
# 读取文件,limit可选限制读取行数
def run_read(path: str, limit: int = None) -> str:
fp = safe_path(path)
text = fp.read_text(encoding="utf-8")
lines = text.splitlines()
if limit and len(lines) > limit:
lines = lines[:limit]
return "\n".join(lines)[:50000] # 限制超长返回
# 写入文件
def run_write(path: str, content: str) -> str:
fp = safe_path(path)
fp.write_text(content, encoding="utf-8")
return f"成功写入文件:{path}"
# 替换文本编辑文件
def run_edit(path: str, old_text: str, new_text: str) -> str:
fp = safe_path(path)
raw = fp.read_text(encoding="utf-8")
new_content = raw.replace(old_text, new_text)
fp.write_text(new_content, encoding="utf-8")
return f"{path} 文件内容替换完成"
# 执行shell终端命令
def run_bash(command: str) -> str:
try:
res = subprocess.check_output(command, shell=True, encoding="utf-8", stderr=subprocess.STDOUT)
return res
except Exception as e:
return f"命令执行失败:{str(e)}"
模块3:TOOL_HANDLERS分发字典(S02最核心,替代所有if判断)
python
# key和TOOLS里工具name完全一致,新增工具只在这里追加一行
TOOL_HANDLERS = {
"bash": lambda **kw: run_bash(kw["command"]),
"read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
"write_file": lambda **kw: run_write(kw["path"], kw["content"]),
"edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"])
}
模块4:Agent主循环(整体复用S01,仅修改工具执行片段)
agent_loop的while循环、消息追加、模型请求逻辑全部照搬S01,只把原if判断替换为字典查表:
python
# 循环内工具执行核心代码(仅此处改动)
import json
tool_returns = []
for tool_call in msg.tool_calls:
args = json.loads(tool_call.function.arguments) # 解析模型返回的参数JSON字符串
handler = TOOL_HANDLERS.get(tool_call.function.name) # 查表找函数
if handler:
output = handler(**args)
else:
output = f"未知工具:{tool_call.function.name}"
tool_returns.append({"role": "tool", "tool_call_id": tool_call.id, "content": output})
state["messages"].extend(tool_returns)
state["turn_count"] += 1
模块5:TOOLS配置(给DeepSeek的工具说明书,同S01规则)
TOOLS列表用来告诉模型可用工具,function.name必须和TOOL_HANDLERS字典key一一对应,示例:
python
TOOLS = [
{
"type": "function",
"function": {
"name": "read_file",
"description": "读取工作目录下指定txt文件内容,path为相对路径,limit可选限制读取行数",
"parameters": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}
}
},
# 依次补充 write_file / edit_file / bash 三个工具配置
]
模块6:进阶:消息规范化 normalize_messages(新手初学可暂时跳过)
每次调用DeepSeek API前,调用该函数清洗messages,生成接口合规的消息副本,原始内部messages保持不变:
python
# 调用模型时使用规范化消息
resp = client.chat.completions.create(
model=MODEL,
messages=normalize_messages(state["messages"]),
tools=TOOLS
)
初学优先跑通分发+沙箱,规范化放到后期复杂项目再落地。
四、新手实战测试(4组实操指令)
- 运行脚本:
python s02_tool_use.py - 4条测试Prompt(英文指令模型识别效果更好)
1)Create a file called greet.py with a greet(name) function→ write_file新建代码文件
2)Edit greet.py to add docstring for greet function→ edit_file编辑注释
3)Read greet.py to view full source code→ read_file读取内容
4)ls ./agent_workspace→ bash查看目录文件
五、拓展练习与后续学习
- 入门练手 :在当前架构新增计算器工具,仅需三步:①写
run_calc计算函数;②TOOL_HANDLERS字典新增一行;③TOOLS补充计算器描述,全程不用修改agent_loop主循环,直观感受解耦优势; - 进阶学习 :
s02a-tool-control-plane.md工具控制平面,后续可拓展:工具权限、文件缓存、MCP客户端、调用日志,属于框架升级内容,初学不用深究。
六、S02一句话总结
依托TOOL_HANDLERS字典实现工具解耦分发,依托safe_path实现文件访问安全;新增工具只补handler和工具描述,Agent主循环永久不用改动。
S02 工具路由分发|完整可运行实战代码(DeepSeek API 兼容版)
核心特性 :安全沙箱 + 工具字典分发 + 4大实用工具(读/写/编辑文件 + Bash)+ 零修改主循环
一键运行:替换 API Key 即可直接执行,无需额外配置
python
from openai import OpenAI
import json
from pathlib import Path
import subprocess
# ===================== 1. 全局配置(必填你的DeepSeek Key)=====================
DEEPSEEK_API_KEY = "你的DeepSeek API Key"
client = OpenAI(
api_key=DEEPSEEK_API_KEY,
base_url="https://api.deepseek.com"
)
MODEL = "deepseek-chat"
# 系统提示词:强制模型使用工具,禁止编造结果
SYSTEM_PROMPT = """
你是专业的AI智能体,必须严格调用工具完成任务:
1. 文件操作只能用 read_file/write_file/edit_file,禁止手写内容
2. 终端命令只能用 bash 执行
3. 拿到工具结果后再输出最终答案,禁止凭空回答
"""
# ===================== 2. 安全沙箱(核心:限制文件只能在指定目录操作)=====================
# 所有文件只能操作此文件夹,杜绝系统文件风险
WORKDIR = Path("./agent_workspace")
WORKDIR.mkdir(exist_ok=True) # 自动创建目录
def safe_path(file_path: str) -> Path:
"""安全路径校验:禁止访问工作目录外的文件"""
full_path = (WORKDIR / file_path).resolve()
if not full_path.is_relative_to(WORKDIR):
raise PermissionError(f"非法路径!仅允许操作 {WORKDIR} 内的文件")
return full_path
# ===================== 3. 4个核心工具函数(解耦独立,新增工具只加这里)=====================
def run_bash(command: str) -> str:
"""执行终端Bash命令"""
try:
result = subprocess.check_output(
command, shell=True, encoding="utf-8",
stderr=subprocess.STDOUT, timeout=10
)
return f"命令执行成功:\n{result}"
except Exception as e:
return f"命令执行失败:{str(e)}"
def run_read_file(path: str, limit: int = None) -> str:
"""读取文件内容,limit限制行数"""
try:
file_path = safe_path(path)
content = file_path.read_text(encoding="utf-8")
if limit:
lines = content.splitlines()[:limit]
content = "\n".join(lines)
return f"文件 {path} 内容:\n{content}"
except Exception as e:
return f"读取文件失败:{str(e)}"
def run_write_file(path: str, content: str) -> str:
"""写入文件(覆盖写入)"""
try:
file_path = safe_path(path)
file_path.write_text(content, encoding="utf-8")
return f"✅ 成功写入文件:{path}"
except Exception as e:
return f"写入文件失败:{str(e)}"
def run_edit_file(path: str, old_text: str, new_text: str) -> str:
"""替换文本编辑文件"""
try:
file_path = safe_path(path)
original = file_path.read_text(encoding="utf-8")
updated = original.replace(old_text, new_text)
file_path.write_text(updated, encoding="utf-8")
return f"✅ 成功编辑文件:{path}"
except Exception as e:
return f"编辑文件失败:{str(e)}"
# ===================== 4. 工具路由字典(S02核心!替代if/elif,查表分发)=====================
# 格式:工具名(和TOOLS一致) → 对应执行函数
TOOL_HANDLERS = {
"bash": run_bash,
"read_file": run_read_file,
"write_file": run_write_file,
"edit_file": run_edit_file
}
# ===================== 5. 工具说明书(告诉模型有哪些工具可用)=====================
TOOLS = [
{
"type": "function",
"function": {
"name": "bash",
"description": "执行终端命令,支持ls、dir等基础指令",
"parameters": {
"type": "object",
"properties": {"command": {"type": "string", "description": "要执行的命令"}},
"required": ["command"]
}
}
},
{
"type": "function",
"function": {
"name": "read_file",
"description": "读取工作目录内的文件",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "文件路径,如 test.txt"},
"limit": {"type": "integer", "description": "限制读取行数,可选"}
},
"required": ["path"]
}
}
},
{
"type": "function",
"function": {
"name": "write_file",
"description": "写入文本到文件",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "文件路径"},
"content": {"type": "string", "description": "要写入的内容"}
},
"required": ["path", "content"]
}
}
},
{
"type": "function",
"function": {
"name": "edit_file",
"description": "替换文件内的文本",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "文件路径"},
"old_text": {"type": "string", "description": "要替换的旧内容"},
"new_text": {"type": "string", "description": "新内容"}
},
"required": ["path", "old_text", "new_text"]
}
}
}
]
# ===================== 6. Agent主循环(完全复用S01,零修改!)=====================
def agent_loop(user_input):
# 初始化状态:记忆 + 轮次
state = {
"messages": [{"role": "user", "content": user_input}],
"turn_count": 1
}
while True:
print(f"\n=== 第 {state['turn_count']} 轮推理 ===")
# 调用DeepSeek API
resp = client.chat.completions.create(
model=MODEL,
messages=[{"role": "system", "content": SYSTEM_PROMPT}] + state["messages"],
tools=TOOLS
)
# 获取模型回复
msg = resp.choices[0].message
state["messages"].append(msg.model_dump(exclude_none=True))
# 无工具调用 → 输出答案,结束循环
if not msg.tool_calls:
print("\n🎉 最终回答:", msg.content)
return
# ===================== 工具分发执行(S02核心逻辑)=====================
tool_results = []
for tool_call in msg.tool_calls:
# 解析模型返回的参数
func = tool_call.function
args = json.loads(func.arguments)
# 查表找到对应工具函数并执行
handler = TOOL_HANDLERS.get(func.name)
if handler:
result = handler(**args)
else:
result = f"❌ 未知工具:{func.name}"
# 组装工具结果,回传给模型
tool_results.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
# 工具结果写入记忆,进入下一轮
state["messages"].extend(tool_results)
state["turn_count"] += 1
# ===================== 7. 实战测试(一键运行4个工具)=====================
if __name__ == "__main__":
# 测试1:写入文件
agent_loop("创建一个文件叫 note.txt,内容写:S02工具路由分发学习成功")
# 测试2:读取文件
agent_loop("读取 note.txt 文件的内容")
# 测试3:编辑文件
agent_loop("把 note.txt 里的内容替换为:S02工具路由分发+安全沙箱双完成")
# 测试4:Bash命令查看目录
agent_loop("执行命令查看 agent_workspace 文件夹里的文件")
🚀 运行说明(初学者必看)
- 替换密钥 :把代码里
你的DeepSeek API Key换成你自己的密钥 - 安装依赖 :
pip install openai - 直接运行 :执行代码,会自动完成:
- 创建
agent_workspace安全目录 - 自动调用4个工具(写/读/编辑文件 + Bash)
- 打印每一轮推理过程 + 最终答案
- 创建
✅ 核心验证效果
你会看到控制台输出:
- 第1轮:模型调用
write_file写入笔记 - 第2轮:模型调用
read_file读取内容 - 第3轮:模型调用
edit_file修改内容 - 第4轮:模型调用
bash查看文件列表
🎯 S02 核心亮点(一眼看懂)
- 无if/elif :用
TOOL_HANDLERS字典查表分发工具 - 主循环零修改:新增工具只加函数 + 注册字典,不用动循环代码
- 安全沙箱 :所有文件强制在
agent_workspace内操作,绝对安全 - 标准化工具:4个高频工具开箱即用,可无限扩展