多工具Agent:专用工具让AI更稳更安全
从单工具到多工具先回忆上一章的核心循环用户消息进入messages。模型返回普通回答或者返回tool_calls。Harness 根据tool_call.function.name找到真实函数。Harness 执行工具并把结果作为role: tool写回messages。模型看到工具结果后继续推理。这个循环不需要因为工具变多而改变。真正需要扩展的是两部分工具实现真实的 Python 函数比如read_file()、write_file()。工具说明传给模型的TOOLS告诉模型工具名称、用途和参数结构。多工具 Agent 的关键不是让模型“执行更多东西”而是让 Harness 能按工具名稳定分发到不同处理函数。为什么不要只靠 bash既然已经有bash为什么还要单独定义读取文件、写入文件、列目录这些工具因为bash太强也太宽。它既能ls也能cat还能做很多危险动作。对于模型来说一个宽泛工具虽然“自由”但也意味着参数空间巨大、失败模式复杂、安全边界模糊。相比之下专用工具更适合 Agentread_file(path, limit)的意图比bash(sed -n ...)清楚write_file(path, content)的参数结构比拼 shell 命令稳定edit_file(path, old_text, new_text)更容易做安全检查list_dir(path)的输出可以控制格式减少模型解析成本。工具不是越万能越好。很多时候工具越窄Agent 越稳。先定义几个文件工具这一篇先在上一章bash的基础上再定义四个文件类工具工具作用list_dir列出目录内容read_file读取文件内容write_file写入或创建文件edit_file按旧文本替换一处内容代码如下import jsonimport osimport subprocessfrom pathlib import Pathfrom typing import Any, Callablefrom openai import OpenAIclient OpenAI(base_urlhttp://localhost:8080/v1,api_keyno-need,)MODEL gpt-oss-20b-Q4_K_M.ggufWORKDIR Path(os.getcwd()).resolve()def bash(command: str) - str:Execute a bash command and return the output.dangerous_keywords [rm, sudo, shutdown, reboot, init, poweroff]if any(keyword in command for keyword in dangerous_keywords):return Error: command contains dangerous keywords.try:result subprocess.run(command,shellTrue,cwdWORKDIR,capture_outputTrue,textTrue,timeout120,)output (result.stdout result.stderr).strip()return output[:5000] if output else 执行完成但没有输出。except subprocess.TimeoutExpired:return Error: command execution timed out.except Exception as exc:return fError: an unexpected error occurred - {exc}def safe_path(path: str) - Path:target (WORKDIR / path).resolve()if not target.is_relative_to(WORKDIR):raise ValueError(fUnsafe path: {path})return targetdef list_dir(path: str .) - str:try:target safe_path(path)if not target.exists():return fError: path does not exist - {path}if not target.is_dir():return fError: path is not a directory - {path}items []for child in sorted(target.iterdir(), keylambda item: item.name):suffix / if child.is_dir() else items.append(f{child.name}{suffix})return \n.join(items) if items else 目录为空。except Exception as exc:return fError: {exc}def read_file(path: str, limit: int 200) - str:try:target safe_path(path)if not target.exists():return fError: file does not exist - {path}if not target.is_file():return fError: path is not a file - {path}text target.read_text(encodingutf-8)lines text.splitlines()if limit 0 and len(lines) limit:lines lines[:limit] [f... ({len(lines) - limit} more lines)]return \n.join(lines)[:50000]except Exception as exc:return fError: {exc}def write_file(path: str, content: str) - str:try:target safe_path(path)target.parent.mkdir(parentsTrue, exist_okTrue)target.write_text(content, encodingutf-8)return fWrote {len(content)} characters to {path}except Exception as exc:return fError: {exc}def edit_file(path: str, old_text: str, new_text: str) - str:try:target safe_path(path)if not target.exists():return fError: file does not exist - {path}if not target.is_file():return fError: path is not a file - {path}content target.read_text(encodingutf-8)count content.count(old_text)if count 0:return fError: text not found in {path}if count 1:return fError: text is not unique in {path}, found {count} matchestarget.write_text(content.replace(old_text, new_text, 1), encodingutf-8)return fEdited {path}except Exception as exc:return fError: {exc}这里保留几个细节。第一safe_path()很重要。模型生成的路径不能直接信任。../../somewhere这种路径如果不处理就可能逃出当前工作目录。这里用resolve()得到绝对路径再用is_relative_to(WORKDIR)确认目标仍在工作目录里。第二read_file()加了limit。Agent 很容易因为一次读取大文件把上下文塞满。文件读取工具最好默认有行数或字节数限制。第三edit_file()要求old_text只出现一次。如果旧文本出现多次直接替换第一处可能会改错位置。这个限制虽然保守但对教学版 Agent 来说更容易解释也更安全。第四bash仍然只是教学示例。真实项目里不要只靠关键词过滤来保护 shell 工具。文件类工具的第一层安全边界是把所有路径限制在工作目录内。把工具说明给模型实现了真实函数之后还要扩充TOOLS。这一步和上一篇完全一样工具定义不是给 Python 解释器看的而是给模型看的。TOOLS [{type: function,function: {name: bash,description: Execute a shell command and return the output.,parameters: {type: object,properties: {command: {type: string,description: The shell command to execute.,}},required: [command],},},},{type: function,function: {name: list_dir,description: List files and directories under a path.,parameters: {type: object,properties: {path: {type: string,description: Directory path relative to the current working directory.,}},},},},{type: function,function: {name: read_file,description: Read a UTF-8 text file with an optional line limit.,parameters: {type: object,properties: {path: {type: string,description: File path relative to the current working directory.,},limit: {type: integer,description: Maximum number of lines to return. Defaults to 200.,},},required: [path],},},},{type: function,function: {name: write_file,description: Write UTF-8 text content to a file, creating parent directories if needed.,parameters: {type: object,properties: {path: {type: string,description: File path relative to the current working directory.,},content: {type: string,description: The full text content to write.,},},required: [path, content],},},},{type: function,function: {name: edit_file,description: Edit a file by replacing one unique old_text occurrence with new_text.,parameters: {type: object,properties: {path: {type: string,description: File path relative to the current working directory.,},old_text: {type: string,description: The exact text to replace. It must appear exactly once.,},new_text: {type: string,description: The replacement text.,},},required: [path, old_text, new_text],},},},]工具定义有两个方向要对齐。一边要对齐模型name、description、parameters要足够清楚让模型知道什么时候该选哪个工具。另一边要对齐 Harness工具名必须能映射到真实处理函数参数名也要和处理函数的入参一致。比如read_file的 schema 里有path和limit真实函数也最好就是read_file(path: str, limit: int 200)。第一版用 if/elif 分发工具工具变多之后最直接的写法是在 Agent Loop 里判断工具名for tool_call in assistant_message.tool_calls:arguments json.loads(tool_call.function.arguments)if tool_call.function.name bash:tool_result bash(arguments[command])elif tool_call.function.name list_dir:tool_result list_dir(arguments.get(path, .))elif tool_call.function.name read_file:tool_result read_file(arguments[path], arguments.get(limit, 200))elif tool_call.function.name write_file:tool_result write_file(arguments[path], arguments[content])elif tool_call.function.name edit_file:tool_result edit_file(arguments[path],arguments[old_text],arguments[new_text],)else:tool_result fError: unknown tool {tool_call.function.name}这个版本能跑也很直观。但问题也明显每增加一个工具都要改 Agent Loop工具分发逻辑和循环控制逻辑混在一起参数解析和错误处理会重复工具多了以后if/elif会越来越长。Agent Loop 是 Harness 的核心路径不应该因为新增一个普通工具就频繁改动。更好的方式是把“工具名 - 处理函数”的关系独立出来。第二版用 TOOL_HANDLERS 注册工具我们可以增加一个工具注册表TOOL_HANDLERS: dict[str, Callable[..., str]] {bash: bash,list_dir: list_dir,read_file: read_file,write_file: write_file,edit_file: edit_file,}这个映射表的含义很直接如果模型请求调用read_fileHarness 就从TOOL_HANDLERS[read_file]里取出真实函数然后把解析出来的参数传进去。这样做之后可以把单个工具调用的执行过程封装成一个函数def run_tool_call(tool_call: Any) - str:name tool_call.function.namehandler TOOL_HANDLERS.get(name)if handler is None:return fError: unknown tool {name}try:arguments json.loads(tool_call.function.arguments or {})except json.JSONDecodeError as exc:return fError: invalid JSON arguments for {name} - {exc}if not isinstance(arguments, dict):return fError: arguments for {name} must be a JSON object.try:return handler(**arguments)except TypeError as exc:return fError: invalid arguments for {name} - {exc}except Exception as exc:return fError: tool {name} failed - {exc}这段代码承担了四件事根据工具名找到处理函数把模型生成的 JSON 字符串解析成 dict确认参数结构是 JSON object执行工具并把异常转换成模型能读懂的文本结果。这里再次强调parameters能帮助模型生成更接近正确的参数但它不等于运行时安全。Harness 仍然要做解析、校验和错误处理。改造后的 Agent Loop有了run_tool_call()之后Agent Loop 就能保持和上一篇几乎一样的结构SYSTEM_PROMPT f你是一个编程助手工作在当前目录{WORKDIR}。你可以使用工具读取文件、列出目录、写入文件、编辑文件和执行必要的 shell 命令。调用工具前先判断需要什么信息拿到工具结果后再决定是否继续调用工具或给出最终回答。def run_loop(user_message: str, max_steps: int 8) - str:messages: list[dict[str, Any]] [{role: user, content: user_message}]for _ in range(max_steps):response client.chat.completions.create(modelMODEL,messages[{role: system, content: SYSTEM_PROMPT}] messages,toolsTOOLS,)assistant_message response.choices[0].messageif not assistant_message.tool_calls:final_answer assistant_message.content or messages.append({role: assistant, content: final_answer})return final_answermessages.append({role: assistant,content: assistant_message.content or ,tool_calls: [{id: tool_call.id,type: function,function: {name: tool_call.function.name,arguments: tool_call.function.arguments,},}for tool_call in assistant_message.tool_calls],})for tool_call in assistant_message.tool_calls:tool_result run_tool_call(tool_call)messages.append({role: tool,tool_call_id: tool_call.id,content: tool_result,})return 达到最大循环次数任务仍未完成。if __name__ __main__:user_input input(Enter your message: )output run_loop(user_input)print(Response from LLM:, output)注意这里仍然保留了上一篇的几个关键点没有tool_calls时说明模型给出了最终回答有tool_calls时先把带有tool_calls的assistant消息写入历史每个工具执行结果都用role: tool写回tool_call_id必须对应原来的tool_call.idmax_steps防止循环失控。变化只有一个Agent Loop 不再关心具体有哪些工具它只负责遍历模型请求的工具调用然后交给run_tool_call()。一轮里出现多个工具调用怎么办上一篇已经提到过assistant_message.tool_calls是一个列表。也就是说模型一轮里可能请求多个工具调用。比如它可能同时请求list_dir({path: .})read_file({path: README.md, limit: 80})Harness 的处理方式很简单遍历这个列表为每个tool_call执行一次工具并追加一条对应的tool消息。这里最重要的是保持对应关系for tool_call in assistant_message.tool_calls:tool_result run_tool_call(tool_call)messages.append({role: tool,tool_call_id: tool_call.id,content: tool_result,})如果有两个工具调用就应该追加两条tool消息。每条tool消息都用自己的tool_call_id指回对应的请求。不要把多个工具结果随便合并成一条普通文本否则模型很难判断哪个结果对应哪个调用。新增工具时要做什么有了工具注册表之后新增工具的步骤变得很固定。假设后面要增加一个http_get(url)工具大致只需要做三件事定义真实函数http_get(url: str) - str在TOOLS里添加工具说明让模型知道它可以调用在TOOL_HANDLERS里添加http_get: http_get。Agent Loop 不需要改。这就是注册表的价值工具扩展发生在边缘循环控制保持稳定。当然真实项目里可以继续往前走把TOOLS和TOOL_HANDLERS合并成更完整的工具对象。例如每个工具都包含工具名工具描述参数 schema处理函数权限等级是否需要用户审批返回结果最大长度超时时间。这时工具系统就不只是一个 dict而会慢慢变成 Harness 里的一个核心模块。小结这篇在上一篇最小 Agent Loop 的基础上继续做了三件事定义多个专用工具让 Agent 不再只依赖bash用TOOLS把每个工具的名称、描述和参数结构告诉模型用TOOL_HANDLERS把工具名映射到真实处理函数让 Agent Loop 保持稳定。到这里我们已经有了一个更像样的工具系统雏形模型负责选择工具Harness 负责校验参数、执行函数、回传结果。

相关新闻

Hello-World项目架构解析:60+语言文件的组织与管理技巧

Hello-World项目架构解析:60+语言文件的组织与管理技巧

Hello-World项目架构解析:60语言文件的组织与管理技巧 【免费下载链接】Hello-World Hello World in all possible programmnig languages 项目地址: https://gitcode.com/gh_mirrors/hellowo/Hello-World Hello-World项目是一个旨在汇集多种编程语言实现&qu…

2026/10/17 6:09:52 阅读更多 →
50个Dify工作流模板:零基础也能上手的AI自动化工具箱

50个Dify工作流模板:零基础也能上手的AI自动化工具箱

50个Dify工作流模板:零基础也能上手的AI自动化工具箱 【免费下载链接】Awesome-Dify-Workflow 分享一些好用的 Dify DSL 工作流程,自用、学习两相宜。 Sharing some Dify workflows. 项目地址: https://gitcode.com/GitHub_Trending/aw/Awesome-Dify-W…

2026/10/16 7:10:11 阅读更多 →
10分钟上手GeoFire for Java:从安装到实现位置查询的完整教程

10分钟上手GeoFire for Java:从安装到实现位置查询的完整教程

10分钟上手GeoFire for Java:从安装到实现位置查询的完整教程 【免费下载链接】geofire-java GeoFire for Java - Realtime location queries with Firebase 项目地址: https://gitcode.com/gh_mirrors/ge/geofire-java GeoFire for Java 是一款基于 Firebas…

2026/10/16 7:32:59 阅读更多 →

最新新闻

CCF CSP历年真题高效刷题指南:从89页题解到考场实战

CCF CSP历年真题高效刷题指南:从89页题解到考场实战

简介:这份资料面向备战CCF CSP软件能力认证的考生与算法学习者,精选历年认证考试真题并配以详尽解答,共89页,以DOC文档形式呈现,适合从基础巩固到考前冲刺的各个复习阶段。压缩包内仅含1个docx文件,体积约1…

2026/10/17 22:14:16 阅读更多 →
BFCL v4榜单深度解读:为什么Qwen3.6-35B-A3B-NVFP4在多轮与Agentic任务上掉分最多?

BFCL v4榜单深度解读:为什么Qwen3.6-35B-A3B-NVFP4在多轮与Agentic任务上掉分最多?

【免费下载链接】Qwen3.6-35B-A3B-NVFP4 项目地址: https://ai.gitcode.com/hf_mirrors/RedHatAI/Qwen3.6-35B-A3B-NVFP4 点击查看 免费下载 本文带你完整解读 RedHatAI/Qwen3.6-35B-A3B-NVFP4 ——这是阿里 Qwen3.6-35B-A3B 混合专家(MoE)大…

2026/10/17 22:14:16 阅读更多 →
BONES-SEED时间分段标签实战:用NVIDIA Kimodo时间戳精准切分复杂动作(附代码)

BONES-SEED时间分段标签实战:用NVIDIA Kimodo时间戳精准切分复杂动作(附代码)

数据集机器人具身智能 【免费下载链接】seed 项目地址: https://ai.gitcode.com/hf_mirrors/bones-studio/seed 点击查看 免费下载 BONES-SEED(Skeletal Everyday Embodiment Dataset)是一个包含 142,220 条标注人体动作的开源动捕数据集&am…

2026/10/17 22:14:16 阅读更多 →
如何读懂 Fara1.5-9B-OptiQ-4bit 的 Chat Template:Jinja 模板如何实现工具调用与思考模式

如何读懂 Fara1.5-9B-OptiQ-4bit 的 Chat Template:Jinja 模板如何实现工具调用与思考模式

人工智能大模型基础模型本地部署多模态计算机视觉AI Agent 【免费下载链接】Fara1.5-9B-OptiQ-4bit 项目地址: https://ai.gitcode.com/hf_mirrors/mlx-community/Fara1.5-9B-OptiQ-4bit 点击查看 免费下载 Fara1.5-9B-OptiQ-4bit 是微软开源的计算机使用&#xff…

2026/10/17 22:14:16 阅读更多 →
security-audit-skill 拆解:让 Agent 稳定执行安全审计的技能包设计

security-audit-skill 拆解:让 Agent 稳定执行安全审计的技能包设计

1. 从零拆解 security-audit-skill:一个让 Agent 真正会做安全审计的技能包第一次看到security-audit-skill这个标题,我脑子里蹦出来的不是“又一个安全扫描脚本”,而是一个更具体的问题:怎么让一个通用 Agent 在拿到一份代码仓库…

2026/10/17 22:14:16 阅读更多 →
智能名片个人版V8.4.2整站源码:前后端部署与小程序对接指南

智能名片个人版V8.4.2整站源码:前后端部署与小程序对接指南

简介:一份面向毕业设计及小程序开发者的整站商业源码,基于龙兵平台智能名片个人版V8.4.2,包含小程序前端与配套网站后台,可解决智能名片展示、分享、数据反馈等场景的快速搭建问题。压缩包内共2000个文件,以358个php后…

2026/10/17 22:13:15 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →